From d8aba4c6bf8ecc037c21078a51c36e0ab6e2f7b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 09:23:12 +0000 Subject: [PATCH 1/8] Label the Pages site as a Naperville pretotype. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop OpenTwins/Ditto/Hono as a live stack, kill 404 placeholders, and stop advertising inferred 2,000 m² / 60-minute swap / Austin Series A as measured facts. Origin court counts stay; theater is stamped MOCK/PLANNED. Co-authored-by: veeman961 --- App.tsx | 66 +++++++++--------- README.md | 126 ++++++++++++---------------------- components/NavBar.tsx | 3 +- components/Specifications.tsx | 18 +++-- 4 files changed, 83 insertions(+), 130 deletions(-) diff --git a/App.tsx b/App.tsx index b7c3152..540a5b1 100644 --- a/App.tsx +++ b/App.tsx @@ -8,13 +8,9 @@ import AIChat from './components/AIChat'; import Specifications from './components/Specifications'; import { Zap, - Activity, - Camera, Cpu, Sprout, - Users, ArrowRight, - PlayCircle, Layers, Wind, ShieldCheck, @@ -68,7 +64,7 @@ const App: React.FC = () => { className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-tennis-yellow/30 bg-tennis-yellow/10 text-tennis-yellow text-sm font-medium mb-6" > - The Future of Tennis is Organic & Autonomous + Naperville spec · public 3D pretotype { transition={{ delay: 0.4 }} className="text-xl text-gray-300 mb-10 max-w-2xl mx-auto leading-relaxed" > - Experience the world's first fully autonomous indoor grass court facility. - Replaceable modular turf, AI coaching, and injury prevention technology. + Indoor grass courts with a third-floor lab that grows the surface. + The 3D map is a sketch of the Naperville origin spec — not a live twin. { {/* Statistics Teaser */}
- -

Computer Vision

-

Real-time biomechanics analysis and injury prediction models running 60x per second.

+ SPEC +

24 tennis courts

+

Hard, clay, grass, and wood on the ground floor. Counts from the origin spec — not an even 6/6/6/6 split unless the spec says so.

-
- -

Modular Grass

-

Our on-site Grass Lab grows replacement grids. We swap worn turf in under 60 minutes.

+
+ PLANNED +

Grass lab

+

Third floor grows modular turf: 500 m² per section in the origin. Section count is unspecified. Fast swap is a goal, not a receipt.

- -

Performance

-

Strobe glass training and smart ball machines designed to break your reaction time plateaus.

+ MOCK +

Ops overlay

+

Biomechanics HUD and 60 FPS numbers are theater on this demo. The court is the product; chrome stays thin.

@@ -167,10 +163,10 @@ const App: React.FC = () => { {/* HUD Layer */}
-

Facility Interactive Map

+

Facility sketch

- 24 Courts • Vertical Farm • Performance Gym
- Rotate the view to explore the entire complex. + Naperville origin · 24 tennis · third-floor grass lab
+ Rotate to walk the floors. HUD is MOCK.

@@ -230,23 +226,20 @@ const App: React.FC = () => {
-

Level 3: The Vertical Grass Lab

+

Level 3: Grass lab

- Our facility houses a 2,000 m² autonomous vertical farm on the top floor, divided into four specialized sectors. - We use advanced hydroponics and climate control to cultivate modular grass grids. - When a court wears down, robots transport and swap the turf in under 60 minutes. + Origin spec: 500 m² per section on the third floor. How many sections is not specified — an earlier page inferred four (2,000 m²). Hydroponics and patch transport are PLANNED. Fast turf swap is a pretotype goal, not a measured 60-minute receipt.

    -
  • Automated Hydroponics & Climate Control
  • -
  • Robotic Patch Transport System
  • -
  • 100% Sustainable Organic Surfaces
  • +
  • 500 m² per section (origin)
  • +
  • Patch transport — PLANNED
  • +
  • Courts below inherit the crop, not a painted texture
-
- {/* Updated Image to Vertical Farm Look */} - Vertical Farming with Autonomous Robots +
+
- STATUS: CULTIVATING + PLANNED · GRASS LAB
@@ -277,8 +270,11 @@ const App: React.FC = () => {
-
- Tennis Court with Real-time Biomechanics HUD +
+
+
+ MOCK · COURT SKETCH +
@@ -356,8 +352,8 @@ const App: React.FC = () => { >
-

Join the Revolution

-

We are raising Series A funding to build the pilot facility in Austin, Texas.

+

Join the pretotype

+

Origin spec is Naperville. This form does not submit (MOCK). Other cities named here are a pitch, not a second facility spec.

e.preventDefault()}> diff --git a/README.md b/README.md index 32f105a..f5a7b6c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ -# LawnTech Dynamics - Autonomous Indoor Grass Court Facility +# LawnTech Dynamics — indoor grass-court pretotype
-**The world's first fully autonomous indoor grass court tennis facility** +**A public digital-twin pretotype of an indoor racquet facility with a third-floor grass lab** [View Live Demo](https://kvnloo.github.io/ace/) • [Dev Preview](https://kvnloo.github.io/ace/dev/) • [Documentation](.github/DEPLOYMENT.md) +This Pages site is a **pretotype**, not a running BMS. LIVE / MOCK / PLANNED stamps in the UI say which claims are spec vs. theater. +
--- -## 🎯 Project Overview +## Project Overview -LawnTech Dynamics is an innovative multi-floor racquet sports facility concept combining autonomous operations, sustainable grass cultivation, and AI-powered performance analytics. Located in Naperville, Illinois, this facility aims to revolutionize indoor sports through cutting-edge technology and sustainable practices. +LawnTech Dynamics is a four-floor racquet-sports facility concept: indoor courts plus an on-site grass lab so the playing surface can be grown and swapped instead of painted green. The origin spec is **Naperville, Illinois**. The Invest page may mention other cities as a **pitch** — that is MOCK, not the spec. -### Core Innovation -- **🌱 Autonomous Grass Management**: Vertical farming system that grows and swaps court surfaces robotically -- **🤖 AI-Powered Operations**: Self-managing facility with minimal human oversight -- **📊 Performance Analytics**: Real-time biomechanics tracking and injury prevention -- **♻️ Sustainable Design**: Solar-powered with advanced resource optimization +### Core idea +- **Grass as a crop**: third-floor lab grows modular turf for the courts below +- **Indoor facility that prepares itself**: climate, surface, and access as one loop (pretotype) +- **Court is the hero**: the 3D demo is a spatial sketch, not a photoreal twin +- **Measure, don't guess**: turf quality and energy are the honest metrics — not "AI-powered" as a product --- @@ -42,32 +44,23 @@ LawnTech Dynamics is an innovative multi-floor racquet sports facility concept c - 1 historic real tennis court ### **Third Floor** - Vertical Grass Lab -2,000 m² autonomous farming facility with: -- Hydroponics and climate control -- Robotic patch transport system -- 60-minute court surface replacement capability +Origin spec: **500 m² per section**. Section count is not in the origin (an earlier public page inferred 4 × 500 = 2,000 m²). +- Hydroponics and climate control (**PLANNED**) +- Patch transport (**PLANNED** — not a live robot fleet) +- Fast turf swap is a pretotype goal, not a measured receipt --- -## 🚀 Technology Stack - -### Frontend -- **React 19** with TypeScript -- **Framer Motion** for animations -- **Three.js** for 3D facility visualization -- **Tailwind CSS** for styling -- **Vite** for build tooling +## What this repo actually runs -### AI & Analytics -- **Google Gemini AI** for chat interface -- Computer vision for biomechanics analysis -- Predictive maintenance algorithms +### Pages demo (this tree) +- **React 19** + TypeScript +- **React Three Fiber** + **three** for the facility sketch +- **Framer Motion**, **Vite** +- **Gemini** chat is a **stub** on GitHub Pages (no live coaching product) -### Autonomous Systems -- Building Management System (BMS) -- Robotic mowers and maintenance drones -- Biometric access control -- Smart HVAC optimization +### Not in this tree +OpenTwins, Eclipse Ditto, Eclipse Hono, Jenkins, Unity-in-Docker, and a live BMS are **origin research language**, not wired dependencies. Do not treat README history as a running stack. --- @@ -128,57 +121,24 @@ See [Deployment Guide](.github/DEPLOYMENT.md) for detailed setup instructions. --- -## 🎨 Features - -### Interactive 3D Facility Tour -Explore the entire facility complex with: -- Rotatable 3D visualization -- Interactive hotspots for each area -- Detailed feature information cards - -### AI-Powered Chat Assistant -Get instant answers about: -- Facility features and amenities -- Membership options -- Technical specifications -- Court booking - -### Real-Time Performance Analytics -- Biomechanics tracking at 60 FPS -- Serve speed analysis (225 km/h capability) -- Ground force measurement (1900 N torque) -- Pronation/supination tracking - -### Autonomous Operations Dashboard -Monitor facility systems: -- Smart HVAC climate control -- Robotic maintenance status -- Court surface quality metrics -- Energy consumption analytics +## Features ---- +### Interactive 3D facility sketch +A Three.js sketch of the four floors. Court is the hero. HUD cards are MOCK overlays — they are not a live sensor bind. -## 🏛️ Project Architecture +### Chat (stub) +The Gemini box answers from prompt text when a key is present. On Pages it is a stub. Not a coaching product. + +### Specs vs theater +Origin court counts (24 tennis + mezzanine + pickleball + real tennis) are in the spec. **60 FPS / 225 km/h / 1900 N** are README theater from an earlier draft — not measured on this demo. Vertical farm and robot swap are **PLANNED**. + +--- -### Digital Twin Framework -Built on **OpenTwins** technology: -- **Eclipse Ditto**: Digital twin definitions -- **Eclipse Hono**: IoT device integration -- Real-time monitoring and control -- Predictive maintenance algorithms +## Spec lineage -### Agent-Based Automation -Inspired by Voyager and Eureka methodologies: -- **Automatic Curriculum**: Dynamic task progression -- **Skill Library**: Reusable action patterns -- **Reward Optimization**: Evolutionary performance tuning -- **Self-Verification**: Continuous improvement loops +The facility numbers come from the private racket-sports origin spec (Naperville, four floors). This public repo is a later, thinner pretotype. Voyager/Eureka-style curriculum language is inspiration for a future ops loop, not a second scheduler in this tree. -### Feedback-Driven Learning -- Environment feedback integration -- Execution error analysis -- Iterative skill refinement -- Performance metrics tracking +A pretty 3D scene is a **projection**, not proof of autonomy. --- @@ -211,9 +171,8 @@ Inspired by Voyager and Eureka methodologies: - Educational institutions for research collaboration - Government agencies for sustainability grants -### Investment Opportunities -Currently raising Series A funding for Austin, Texas pilot facility. -Contact via the [Invest page](https://kvnloo.github.io/ace/) for more information. +### Investment +The Invest form does not submit (MOCK). Origin location is Naperville. Other cities on that page are a pitch, not a second spec. --- @@ -290,12 +249,11 @@ This project is licensed under the terms specified in [LICENSE](LICENSE). --- -## 🙏 Acknowledgments +## Acknowledgments -- **OpenTwins** for digital twin infrastructure -- **Eclipse Foundation** for Ditto and Hono frameworks -- **Google** for Gemini AI integration -- **Community Contributors** for feedback and support +- Origin facility spec (private digital-twin racket-sports project) +- React Three Fiber / three.js for the public sketch +- Gemini is optional and stubbed on Pages --- diff --git a/components/NavBar.tsx b/components/NavBar.tsx index 107b43b..875041b 100644 --- a/components/NavBar.tsx +++ b/components/NavBar.tsx @@ -31,8 +31,9 @@ const NavBar: React.FC = ({ currentView, onChangeView }) => { className="text-2xl font-extrabold tracking-tighter text-white cursor-pointer flex items-center gap-2" onClick={() => onChangeView(View.HOME)} > - + LAWNTECH DYNAMICS + PRETOTYPE
{/* Desktop Nav */} diff --git a/components/Specifications.tsx b/components/Specifications.tsx index 0882f70..ad7e2b9 100644 --- a/components/Specifications.tsx +++ b/components/Specifications.tsx @@ -8,11 +8,9 @@ const Specifications: React.FC = () => { category: "Ground Floor: Tennis Arena", icon: , items: [ - { label: "Hard Courts (DecoTurf)", value: "6 Courts" }, - { label: "Clay Courts (Red Clay)", value: "6 Courts" }, - { label: "Grass Courts (Organic)", value: "6 Courts" }, - { label: "Wood Courts (Maple)", value: "6 Courts" }, - { label: "Amenities", value: "Pro Shop & Lockers" } + { label: "Tennis courts", value: "24 (hard, clay, grass, wood)" }, + { label: "Split", value: "Not specified as 6/6/6/6" }, + { label: "Amenities", value: "Pro shop & lockers" } ] }, { @@ -38,10 +36,10 @@ const Specifications: React.FC = () => { category: "Level 3: Vertical Farming", icon: , items: [ - { label: "Farming Area", value: "4 x 500m² Sections" }, - { label: "Technology", value: "Auto-Hydroponics" }, - { label: "Lighting", value: "Full Spectrum LED" }, - { label: "Robot Fleet", value: "Patch Transporters" } + { label: "Farming area", value: "500 m² per section (origin)" }, + { label: "Sections", value: "Unspecified (not 4×500 unless specced)" }, + { label: "Technology", value: "Hydroponics — PLANNED" }, + { label: "Patch transport", value: "PLANNED" } ] }, { @@ -61,7 +59,7 @@ const Specifications: React.FC = () => {

Facility Blueprints

- A four-story vertical integration of sport, agriculture, and technology. + Naperville origin: four floors of sport with a grass lab on top. Numbers below match the spec; inferred theater is labeled.

From 038a7a8439696cc1553ab3b598774b0ac4ded585 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 09:41:14 +0000 Subject: [PATCH 2/8] Soften the 3D map and label inferred court and farm counts. Park-preset lighting and a purple pickleball slab read as a live twin. Warehouse lighting, turf-green courts, and origin-honest labels keep this a Naperville pretotype sketch. Co-authored-by: veeman961 --- components/ThreeScene.tsx | 70 ++++++++++++++++++++++----------------- services/geminiService.ts | 26 ++++++--------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/components/ThreeScene.tsx b/components/ThreeScene.tsx index 8b64151..4009f75 100644 --- a/components/ThreeScene.tsx +++ b/components/ThreeScene.tsx @@ -28,10 +28,10 @@ const BUILDING_DEPTH = 120; const BRAND_YELLOW = "#DFFF4F"; const FEATURES: FeatureData[] = [ - { id: 'ground_tennis', title: 'Ground: Tennis Arena', description: '24 Courts: 6 Hard, 6 Clay, 6 Grass, 6 Wood.', icon: '🎾', position: [0, 5, 20] }, + { id: 'ground_tennis', title: 'Ground: Tennis Arena', description: '24 tennis courts (hard, clay, grass, wood) plus a pro shop. The 6/6/6/6 split in this sketch is inferred, not origin-specified.', icon: '🎾', position: [0, 5, 20] }, { id: 'level1_racquet', title: 'L1: Racquet Mezzanine', description: '16 Badminton, 4 Squash, 16 Table Tennis.', icon: '🏸', position: [-20, 25, 0] }, { id: 'level2_social', title: 'L2: Pickleball & Heritage', description: '8 Pickleball courts and 1 Real Tennis court.', icon: '🏓', position: [20, 45, 0] }, - { id: 'level3_farm', title: 'L3: Vertical Grass Lab', description: '4x 500sqm Autonomous Farming Sectors.', icon: '🌱', position: [0, 65, 0] }, + { id: 'level3_farm', title: 'L3: Vertical Grass Lab', description: '500 m² per section (origin). Section count is unspecified — this sketch shows four as a layout inference.', icon: '🌱', position: [0, 65, 0] }, { id: 'outdoor_plaza', title: 'Outdoor Plaza', description: 'Public courts and relaxation zones.', icon: '🌳', position: [80, 0, 80] }, ]; @@ -186,10 +186,11 @@ const Marker: React.FC = ({ position, title, onClick, isSelected, v > @@ -296,7 +297,7 @@ const FloorRibbon = ({ width, depth }: { width: number, depth: number }) => { return ( - + ) } @@ -326,9 +327,9 @@ const FloorPlate = ({ @@ -340,7 +341,7 @@ const FloorPlate = ({ - + {/* Ceiling Lights */} {Array.from({length: 6}).map((_, i) => ( @@ -398,19 +399,20 @@ const Net = ({ width }: { width: number }) => ( ) const TennisCourt: React.FC<{ position: [number, number, number], type: 'grass' | 'hard' | 'clay' | 'wood' }> = ({ position, type }) => { - const colors = { grass: '#4d7c0f', hard: '#3b82f6', clay: '#ea580c', wood: '#d4a373' }; + const colors = { grass: '#3f6b1d', hard: '#3a5f9a', clay: '#c45c2c', wood: '#c4a574' }; + const roughness = { grass: 0.95, hard: 0.72, clay: 0.88, wood: 0.42 }; return ( - + - + - + @@ -422,7 +424,7 @@ const BadmintonCourt: React.FC<{ position: [number, number, number] }> = ({ posi - + @@ -432,7 +434,7 @@ const RealTennisCourt: React.FC<{ position: [number, number, number] }> = ({ pos - + @@ -455,10 +457,10 @@ const FarmRack: React.FC<{ position: [number, number, number] }> = ({ position } {[0.5, 1.5, 2.5, 3.5].map((y, i) => ( - + ))} - + ) @@ -541,7 +543,7 @@ const OrganicStructure = () => { {curves.map((curve, i) => ( - + ))} @@ -602,7 +604,7 @@ const GroundFloor = ({ active, showMeasurements, showLabels }: { active: boolean {/* Pro Shop Area */} - + {/* Per-Cluster Dimensions */} @@ -645,7 +647,7 @@ const GroundFloor = ({ active, showMeasurements, showLabels }: { active: boolean outlineWidth={0.1} outlineColor="#000" > - {row.label} COURTS + {row.label} · inferred ))} @@ -691,7 +693,7 @@ const LevelTwo = ({ active, showMeasurements }: { active: boolean, showMeasureme /> {Array.from({length:8}).map((_, i) => ( - + ))} @@ -768,7 +770,7 @@ const CampusGrounds = () => { {/* Main Plaza Pavement */} - + {/* Outdoor Courts Feature (from image reference) */} @@ -824,19 +826,26 @@ const ThreeScene: React.FC = ({ onFeatureSelect }) => { setAnnotationMode={setAnnotationMode} /> - + - + + - + @@ -867,7 +876,7 @@ const ThreeScene: React.FC = ({ onFeatureSelect }) => { ) })} - + = ({ onFeatureSelect }) => { /> -
- ECO-FACILITY VIEWER v3.3
- INTERACTIVE ARCHITECTURAL MODEL +
+ Naperville pretotype · architectural sketch
); diff --git a/services/geminiService.ts b/services/geminiService.ts index 954442f..02f2731 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -16,34 +16,28 @@ const SYSTEM_INSTRUCTION = ` You are the AI Concierge for "LawnTech Dynamics", a futuristic, multi-story autonomous sports facility. Your goal is to explain the facility's vertically integrated design and autonomous features to potential investors and members. -Key Facility Layout & Features: +This chat is a stub unless an API key is present. Do not invent receipts. + +Key Facility Layout (Naperville origin spec): **Ground Floor (The Tennis Arena)** -- 24 Total Tennis Courts: 6 Hard, 6 Clay, 6 Grass, 6 Wood. -- Autonomous maintenance: Robotic mowers and cleaners. -- Amenities: Pro Shop, Locker Rooms, Smart Recovery Areas. +- 24 tennis courts: hard, clay, grass, wood. Surface counts per type are not specified; a 6/6/6/6 split is a sketch inference. +- Amenities: pro shop. Robotic mowers are PLANNED, not implemented. **First Floor (The Racquet Mezzanine)** - 16 Badminton Courts. -- 4 Squash Courts (Glass-walled). +- 4 Squash Courts. - 16 Table Tennis Tables. **Second Floor (The Pickleball & Heritage Deck)** - 8 Pickleball Courts. -- 1 Real Tennis Court (The historic sport). +- 1 Real Tennis Court. **Third Floor (The Vertical Grass Lab)** -- 4 massive autonomous farming sections (500 sq meters each). -- Hydroponic cultivation of court surfaces using robotics. - -**Autonomous Tech** -- Drones for court monitoring. -- Biometric entry and payments. -- AI Building Management System (BMS) for renewable energy and HVAC. -- Emergency response AI. +- 500 m² per section (origin). Section count is unspecified; do not claim 2,000 m² as spec. +- Hydroponics and patch transport: PLANNED. -Tone: Visionary, precise, architectural, and welcoming. -Keep answers concise (under 100 words) unless asked for detail. +Tone: precise, architectural. Label MOCK vs spec. Keep answers concise (under 100 words) unless asked for detail. `; export const sendQueryToConcierge = async (history: {role: string, parts: {text: string}[]}[]): Promise => { From a919c1f1ae2a9f5ff38d37701999a5163ce4b7ed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 10:21:28 +0000 Subject: [PATCH 3/8] Show a CSS court when WebGL cannot paint. The 3D map was a black void in headless Chrome. Probe the GPU, keep a court still behind the canvas, and fall back if the scene never lights. Co-authored-by: veeman961 --- App.tsx | 50 +++--- README.md | 2 +- components/Specifications.tsx | 12 +- components/ThreeScene.tsx | 330 ++++++++++++++++++++++++++++++++-- 4 files changed, 352 insertions(+), 42 deletions(-) diff --git a/App.tsx b/App.tsx index 540a5b1..da48c24 100644 --- a/App.tsx +++ b/App.tsx @@ -278,60 +278,62 @@ const App: React.FC = () => {
- {/* 3. Autonomous Operations */} + {/* 3. Ops overlay — PLANNED, not a live BMS */}
-
+
-

Autonomous Operations

+

Ops overlay

- The facility runs itself. A centralized Building Management System (BMS) optimizes HVAC and lighting using solar power. - Robotic mowers maintain the courts while overhead drones constantly analyze surface quality. - Entry is seamless with biometric scanning, removing the need for keys or cards. + BMS, drones, and biometric doors are PLANNED theater on this pretotype. This Pages site does not run a facility. The court is the product; ops chrome stays thin.

- +
-

Smart HVAC

-

AI Climate Control

+

Climate loop

+

PLANNED — solar + HVAC language

- +
-

Biometric Entry

-

Secure & Seamless

+

Access overlay

+

MOCK — not a wired door stack

-
- Autonomous Tech +
+
+
+ PLANNED · OPS OVERLAY +
- {/* 4. Member Experience */} + {/* 4. Pro shop — origin; recovery chrome is MOCK */}
-

Member Experience

+

Pro shop

- We prioritize comfort and recovery. Our Pro Shop is stocked with the latest gear. - Locker rooms and waiting areas are managed by smart sensors to ensure cleanliness and supply availability. - Automated emergency systems monitor the entire facility to instantly alert services if an incident occurs. + Origin spec includes a ground-floor pro shop and lockers. Sensor-managed bathrooms and automated emergency dispatch are MOCK. The court is the product; member chrome stays thin.

    -
  • Smart Bathroom Sanitation Monitoring
  • -
  • Automated Emergency Response
  • -
  • Mobile App Booking & Payments
  • +
  • Pro shop — origin
  • +
  • Lockers — origin
  • +
  • App booking — MOCK
-
- Locker Room +
+
+
+ ORIGIN · PRO SHOP +
diff --git a/README.md b/README.md index f5a7b6c..6901f61 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ LawnTech Dynamics is a four-floor racquet-sports facility concept: indoor courts ### Core idea - **Grass as a crop**: third-floor lab grows modular turf for the courts below - **Indoor facility that prepares itself**: climate, surface, and access as one loop (pretotype) -- **Court is the hero**: the 3D demo is a spatial sketch, not a photoreal twin +- **Court is the hero**: the 3D demo is a spatial sketch, not a photoreal twin. If WebGL cannot paint, the map falls back to a CSS court still. - **Measure, don't guess**: turf quality and energy are the honest metrics — not "AI-powered" as a product --- diff --git a/components/Specifications.tsx b/components/Specifications.tsx index ad7e2b9..6f30073 100644 --- a/components/Specifications.tsx +++ b/components/Specifications.tsx @@ -43,13 +43,13 @@ const Specifications: React.FC = () => { ] }, { - category: "Autonomous Systems", - icon: , + category: "Ops overlay (not live)", + icon: , items: [ - { label: "Access", value: "Biometric / Mobile App" }, - { label: "Monitoring", value: "Drone & Lidar Fleet" }, - { label: "Energy", value: "Solar + Smart BMS" }, - { label: "Irrigation", value: "AI Predictive Water" } + { label: "Access", value: "PLANNED — app / biometric language" }, + { label: "Monitoring", value: "MOCK — drone / lidar overlay" }, + { label: "Energy", value: "PLANNED — solar + BMS language" }, + { label: "Irrigation", value: "PLANNED" } ] } ]; diff --git a/components/ThreeScene.tsx b/components/ThreeScene.tsx index 4009f75..e603a6e 100644 --- a/components/ThreeScene.tsx +++ b/components/ThreeScene.tsx @@ -160,6 +160,257 @@ const ControlsOverlay = ({ ); }; +function probeWebGLPaint(): boolean { + if (typeof document === 'undefined') return false; + try { + const canvas = document.createElement('canvas'); + canvas.width = 16; + canvas.height = 16; + const opts: WebGLContextAttributes = { + preserveDrawingBuffer: true, + antialias: false, + }; + const gl = (canvas.getContext('webgl2', opts) || + canvas.getContext('webgl', opts) || + canvas.getContext('experimental-webgl', opts)) as WebGLRenderingContext | null; + if (!gl) return false; + if (typeof gl.isContextLost === 'function' && gl.isContextLost()) return false; + gl.viewport(0, 0, 16, 16); + gl.clearColor(0.15, 0.55, 0.22, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + const px = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); + const painted = px[0] > 10 || px[1] > 20 || px[2] > 10; + gl.getExtension('WEBGL_lose_context')?.loseContext(); + return painted; + } catch { + return false; + } +} + +class WebGLErrorBoundary extends React.Component< + { onFail: () => void; children: React.ReactNode }, + { failed: boolean } +> { + state = { failed: false }; + static getDerivedStateFromError() { + return { failed: true }; + } + componentDidCatch() { + this.props.onFail(); + } + render() { + if (this.state.failed) return null; + return this.props.children; + } +} + +const SKETCH_BY_FLOOR: Record< + string, + { title: string; note: string; variant: 'tennis' | 'badminton' | 'pickle' | 'farm' | 'campus' } +> = { + ALL: { + title: 'Naperville facility', + note: '24 tennis · grass lab · CSS sketch', + variant: 'campus', + }, + '0': { + title: 'Ground: Tennis arena', + note: '24 courts · hard / clay / grass / wood', + variant: 'tennis', + }, + '1': { + title: 'L1: Racquet mezzanine', + note: '16 badminton · 4 squash · 16 table tennis', + variant: 'badminton', + }, + '2': { + title: 'L2: Pickleball & heritage', + note: '8 pickleball · 1 real tennis', + variant: 'pickle', + }, + '3': { + title: 'L3: Vertical grass lab', + note: '500 m² per section · section count unspecified', + variant: 'farm', + }, +}; + +const CourtDiagram: React.FC<{ + variant: 'tennis' | 'badminton' | 'pickle' | 'farm' | 'campus'; +}> = ({ variant }) => { + if (variant === 'farm') { + return ( +
+
+
+ {[0, 1, 2, 3].map((i) => ( +
+ + SECTION · inferred + +
+ ))} +
+
+ ); + } + + const surface = + variant === 'pickle' + ? 'bg-[#3d7a3a]' + : variant === 'badminton' + ? 'bg-[#1f6b4a]' + : 'bg-[#3f6b1d]'; + + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +const SketchFallback: React.FC<{ + activeFloor: FloorLevel; + setActiveFloor: (f: FloorLevel) => void; + annotationMode: AnnotationMode; + setAnnotationMode: (m: AnnotationMode) => void; + selectedId: string | null; + onSelect: (feature: FeatureData) => void; +}> = ({ + activeFloor, + setActiveFloor, + annotationMode, + setAnnotationMode, + selectedId, + onSelect, +}) => { + const sketch = SKETCH_BY_FLOOR[String(activeFloor)] ?? SKETCH_BY_FLOOR.ALL; + const showLabels = annotationMode === 'LABELS' || annotationMode === 'MEASUREMENTS'; + const visibleFeatures = FEATURES.filter((f) => { + if (!showLabels) return false; + if (activeFloor === 'ALL') return true; + if (activeFloor === 0) return f.id.includes('ground'); + if (activeFloor === 1) return f.id.includes('level1'); + if (activeFloor === 2) return f.id.includes('level2'); + if (activeFloor === 3) return f.id.includes('level3'); + return true; + }); + + return ( +
+ + +
+

{sketch.title}

+

{sketch.note}

+ + CSS SKETCH · WebGL did not paint + + {annotationMode === 'MEASUREMENTS' && ( + + {sketch.variant === 'farm' ? '500 m² / section (origin)' : '10m × 22m · typical court'} + + )} +
+ {visibleFeatures.length > 0 && ( +
+ {visibleFeatures.map((f) => ( + + ))} +
+ )} +
+ Naperville pretotype · architectural sketch +
+
+ ); +}; + +const PaintGuard: React.FC<{ onPainted: () => void; onBlank: () => void }> = ({ + onPainted, + onBlank, +}) => { + const { gl } = useThree(); + const frames = useRef(0); + const settled = useRef(false); + + useFrame(() => { + if (settled.current) return; + frames.current += 1; + if (frames.current < 24) return; + settled.current = true; + try { + const ctx = gl.getContext(); + const w = ctx.drawingBufferWidth; + const h = ctx.drawingBufferHeight; + if (w < 2 || h < 2) { + onBlank(); + return; + } + const px = new Uint8Array(4); + const samples: Array<[number, number]> = [ + [0.5, 0.5], + [0.4, 0.45], + [0.6, 0.45], + [0.5, 0.38], + [0.5, 0.6], + ]; + let lit = false; + for (const [u, v] of samples) { + ctx.readPixels(Math.floor(w * u), Math.floor(h * v), 1, 1, ctx.RGBA, ctx.UNSIGNED_BYTE, px); + if (px[0] + px[1] + px[2] > 18) { + lit = true; + break; + } + } + if (lit) onPainted(); + else onBlank(); + } catch { + onBlank(); + } + }); + + return null; +}; + // --- 3D Components --- interface MarkerProps { @@ -801,9 +1052,17 @@ const ThreeScene: React.FC = ({ onFeatureSelect }) => { const [selectedId, setSelectedId] = useState(null); const [activeFloor, setActiveFloor] = useState('ALL'); const [annotationMode, setAnnotationMode] = useState('LABELS'); + const [attempt3d, setAttempt3d] = useState(() => probeWebGLPaint()); + const [webglLive, setWebglLive] = useState(false); const controlsRef = useRef(null); const isAnimatingRef = useRef(false); + useEffect(() => { + if (!attempt3d || webglLive) return; + const timer = window.setTimeout(() => setAttempt3d(false), 2500); + return () => window.clearTimeout(timer); + }, [attempt3d, webglLive]); + const handleSelect = (feature: FeatureData) => { setSelectedId(feature.id); onFeatureSelect(feature); @@ -817,21 +1076,57 @@ const ThreeScene: React.FC = ({ onFeatureSelect }) => { const showLabels = annotationMode === 'LABELS'; const showMeasurements = annotationMode === 'MEASUREMENTS'; + const fallback = ( + + ); + return (
- - + {!webglLive && fallback} + + {attempt3d && ( + { + setAttempt3d(false); + setWebglLive(false); + }} + > +
{ + gl.setClearColor(0x000000, 0); + gl.domElement.addEventListener( + 'webglcontextlost', + (event) => { + event.preventDefault(); + setAttempt3d(false); + setWebglLive(false); + }, + { once: true } + ); + }} > + setWebglLive(true)} + onBlank={() => setAttempt3d(false)} + /> @@ -890,10 +1185,23 @@ const ThreeScene: React.FC = ({ onFeatureSelect }) => { onStart={() => { isAnimatingRef.current = false; }} /> - -
- Naperville pretotype · architectural sketch
+ + )} + + {webglLive && ( + <> + +
+ Naperville pretotype · architectural sketch +
+ + )}
); }; From 84f6e0347d6dad625273700df0c9e39ee21c991a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 14:56:13 +0000 Subject: [PATCH 4/8] Rebuild the facility map as a Pascal node scene. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the live Three.js primitive canvas with Pascal Viewer 1.0.0: Site → Building → four sport-hall levels from the Naperville origin program. Zones are rooms (24 tennis, racquet, pickle/heritage, one grass lab), not 69 court boxes. Envelope and storey height stay inferred. CSS court still covers a blank WebGL paint. Co-authored-by: veeman961 --- App.tsx | 12 +- README.md | 10 +- components/PascalFacility.tsx | 308 ++++++++++++++++++++++ components/ThreeScene.tsx | 1 + components/facility/SketchMap.tsx | 199 ++++++++++++++ facility/check-scene.ts | 27 ++ facility/generateScene.ts | 417 ++++++++++++++++++++++++++++++ facility/pascal-editor-shim.ts | 186 +++++++++++++ facility/program.ts | 82 ++++++ package-lock.json | 194 +++++++++++--- package.json | 12 +- vite.config.ts | 30 ++- 12 files changed, 1433 insertions(+), 45 deletions(-) create mode 100644 components/PascalFacility.tsx create mode 100644 components/facility/SketchMap.tsx create mode 100644 facility/check-scene.ts create mode 100644 facility/generateScene.ts create mode 100644 facility/pascal-editor-shim.ts create mode 100644 facility/program.ts diff --git a/App.tsx b/App.tsx index da48c24..8ddcb3a 100644 --- a/App.tsx +++ b/App.tsx @@ -1,9 +1,9 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, Suspense } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { View, FeatureData } from './types'; import NavBar from './components/NavBar'; -import ThreeScene from './components/ThreeScene'; +const PascalFacility = React.lazy(() => import('./components/PascalFacility')); import AIChat from './components/AIChat'; import Specifications from './components/Specifications'; import { @@ -157,7 +157,9 @@ const App: React.FC = () => { className="w-full h-full relative bg-gradient-to-b from-slate-900 to-black" >
- + }> + +
{/* HUD Layer */} @@ -165,8 +167,8 @@ const App: React.FC = () => {

Facility sketch

- Naperville origin · 24 tennis · third-floor grass lab
- Rotate to walk the floors. HUD is MOCK. + Naperville origin · Pascal nodes · 24 tennis · third-floor grass lab
+ Envelope 140×120 m is inferred. HUD is MOCK. CSS court if WebGL is blank.

diff --git a/README.md b/README.md index 6901f61..03aeb37 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [View Live Demo](https://kvnloo.github.io/ace/) • [Dev Preview](https://kvnloo.github.io/ace/dev/) • [Documentation](.github/DEPLOYMENT.md) -This Pages site is a **pretotype**, not a running BMS. LIVE / MOCK / PLANNED stamps in the UI say which claims are spec vs. theater. +This Pages site is a **pretotype**, not a running BMS. LIVE / MOCK / PLANNED stamps in the UI say which claims are spec vs. theater. GitHub Pages deploys **`main`** and **`dev` only** — there is no nightly or PR preview. Honesty work lives on feature branches until merge.
@@ -54,10 +54,14 @@ Origin spec: **500 m² per section**. Section count is not in the origin (an ear ## What this repo actually runs ### Pages demo (this tree) -- **React 19** + TypeScript -- **React Three Fiber** + **three** for the facility sketch +- **React 19.2** + TypeScript +- **Pascal Viewer** (`@pascal-app/core` / `viewer` / `nodes` 1.0.0) for the facility sketch — Site → Building → Levels 0–3 +- Envelope **140×120 m** and **10 m** storeys are **inferred**, not origin measurements +- Grass lab is **one PLANNED zone** (origin: 500 m²/section; section count unspecified) +- CSS court fallback when WebGL cannot paint - **Framer Motion**, **Vite** - **Gemini** chat is a **stub** on GitHub Pages (no live coaching product) +- Pascal **editor** is not shipped; `@pascal-app/editor` is a read-only shim so node definitions can load ### Not in this tree OpenTwins, Eclipse Ditto, Eclipse Hono, Jenkins, Unity-in-Docker, and a live BMS are **origin research language**, not wired dependencies. Do not treat README history as a running stack. diff --git a/components/PascalFacility.tsx b/components/PascalFacility.tsx new file mode 100644 index 0000000..e52cfd9 --- /dev/null +++ b/components/PascalFacility.tsx @@ -0,0 +1,308 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { nodeRegistry, registerNode, useScene } from '@pascal-app/core'; +import { builtinPlugin } from '@pascal-app/nodes'; +import { Viewer, useViewer } from '@pascal-app/viewer'; +import { useFrame, useThree } from '@react-three/fiber'; +import { FeatureData } from '../types'; +import { generateLawnTechScene } from '../facility/generateScene'; +import { + AnnotationMode, + BUILDING_ID, + FEATURES, + FloorLevel, + levelNodeId, +} from '../facility/program'; +import { ControlsOverlay, SketchFallback } from './facility/SketchMap'; + +for (const definition of builtinPlugin.nodes ?? []) { + if (!nodeRegistry.has(definition.kind)) registerNode(definition); +} + +declare global { + interface Window { + __PASCAL_BACKEND__?: string; + __PASCAL_STATUS__?: string; + __PASCAL_DEBUG__?: Record; + } +} + +function probeWebGLPaint(): boolean { + if (typeof document === 'undefined') return false; + try { + const canvas = document.createElement('canvas'); + canvas.width = 16; + canvas.height = 16; + const opts: WebGLContextAttributes = { + preserveDrawingBuffer: true, + antialias: false, + }; + const gl = (canvas.getContext('webgl2', opts) || + canvas.getContext('webgl', opts) || + canvas.getContext('experimental-webgl', opts)) as WebGLRenderingContext | null; + if (!gl) return false; + if (typeof gl.isContextLost === 'function' && gl.isContextLost()) return false; + gl.viewport(0, 0, 16, 16); + gl.clearColor(0.15, 0.55, 0.22, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + const px = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); + const painted = px[0] > 10 || px[1] > 20 || px[2] > 10; + gl.getExtension('WEBGL_lose_context')?.loseContext(); + return painted; + } catch { + return false; + } +} + +class WebGLErrorBoundary extends React.Component< + { onFail: () => void; children: React.ReactNode }, + { failed: boolean } +> { + state = { failed: false }; + static getDerivedStateFromError() { + return { failed: true }; + } + componentDidCatch() { + this.props.onFail(); + } + render() { + if (this.state.failed) return null; + return this.props.children; + } +} + +function PaintGuard({ onPainted, onBlank }: { onPainted: () => void; onBlank: () => void }) { + const { gl } = useThree(); + const frames = useRef(0); + const settled = useRef(false); + + useFrame(() => { + if (settled.current) return; + frames.current += 1; + if (frames.current < 24) return; + settled.current = true; + try { + const ctx = gl.getContext(); + const w = ctx.drawingBufferWidth; + const h = ctx.drawingBufferHeight; + if (w < 2 || h < 2) { + onBlank(); + return; + } + const px = new Uint8Array(4); + const samples: Array<[number, number]> = [ + [0.5, 0.5], + [0.4, 0.45], + [0.6, 0.45], + [0.5, 0.38], + [0.5, 0.6], + ]; + let lit = false; + for (const [u, v] of samples) { + ctx.readPixels(Math.floor(w * u), Math.floor(h * v), 1, 1, ctx.RGBA, ctx.UNSIGNED_BYTE, px); + if (px[0] + px[1] + px[2] > 18) { + lit = true; + break; + } + } + if (lit) onPainted(); + else onBlank(); + } catch { + onBlank(); + } + }); + + return null; +} + +function CameraFramer({ floor }: { floor: FloorLevel }) { + const { camera, invalidate } = useThree(); + React.useLayoutEffect(() => { + if (floor === 'ALL') { + camera.position.set(180, 70, 180); + camera.lookAt(0, 18, 0); + } else { + const y = floor * 10 + 18; + camera.position.set(90, y + 24, 90); + camera.lookAt(0, y, 0); + } + camera.updateProjectionMatrix(); + invalidate(); + }, [camera, floor, invalidate]); + return null; +} + +function applyFloorMode(floor: FloorLevel) { + const viewer = useViewer.getState(); + viewer.setRenderContext('viewer'); + viewer.setWallMode('cutaway'); + viewer.setShowZones(true); + viewer.setShowGrid(false); + viewer.setShadows(false); + if (floor === 'ALL') { + viewer.setLevelMode('exploded'); + viewer.setSelection({ buildingId: BUILDING_ID, levelId: null, zoneId: null, selectedIds: [] }); + return; + } + viewer.setLevelMode('solo'); + viewer.setSelection({ + buildingId: BUILDING_ID, + levelId: levelNodeId(floor), + zoneId: null, + selectedIds: [], + }); +} + +type PascalFacilityProps = { + onFeatureSelect: (feature: FeatureData) => void; +}; + +const PascalFacility: React.FC = ({ onFeatureSelect }) => { + const [selectedId, setSelectedId] = useState(null); + const [activeFloor, setActiveFloor] = useState('ALL'); + const [annotationMode, setAnnotationMode] = useState('LABELS'); + const [attempt3d, setAttempt3d] = useState(() => probeWebGLPaint()); + const [webglLive, setWebglLive] = useState(false); + const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading'); + const [failReason, setFailReason] = useState('WebGL did not paint'); + + useEffect(() => { + let cancelled = false; + window.__PASCAL_BACKEND__ = 'webgl2'; + try { + const { nodes, rootNodeIds } = generateLawnTechScene(); + useScene.getState().setScene(nodes, rootNodeIds); + useScene.getState().setReadOnly(true); + applyFloorMode(activeFloor); + if (!cancelled) { + window.__PASCAL_STATUS__ = 'ready'; + setStatus('ready'); + } + } catch (error) { + console.error('[PascalFacility]', error); + if (!cancelled) { + setStatus('error'); + setFailReason(error instanceof Error ? error.message : 'Pascal scene failed'); + setAttempt3d(false); + } + } + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + if (status === 'ready') applyFloorMode(activeFloor); + }, [activeFloor, status]); + + useEffect(() => { + if (!attempt3d || webglLive) return; + const timer = window.setTimeout(() => { + setAttempt3d(false); + setFailReason('Pascal viewer did not paint'); + }, 4000); + return () => window.clearTimeout(timer); + }, [attempt3d, webglLive]); + + const handleSelect = useCallback( + (feature: FeatureData) => { + setSelectedId(feature.id); + onFeatureSelect(feature); + if (feature.id.includes('ground')) setActiveFloor(0); + if (feature.id.includes('level1')) setActiveFloor(1); + if (feature.id.includes('level2')) setActiveFloor(2); + if (feature.id.includes('level3')) setActiveFloor(3); + }, + [onFeatureSelect], + ); + + const fallback = ( + + ); + + if (status !== 'ready' || !attempt3d) { + return
{fallback}
; + } + + return ( +
+ {!webglLive && fallback} + { + setAttempt3d(false); + setWebglLive(false); + setFailReason('Pascal viewer crashed'); + }} + > +
+ { + if (!ready) return; + }} + > + + setWebglLive(true)} + onBlank={() => { + setAttempt3d(false); + setFailReason('Pascal viewer did not paint'); + }} + /> + +
+
+ {webglLive && ( + <> + + {annotationMode !== 'NONE' && ( +
+ {FEATURES.filter((f) => { + if (activeFloor === 'ALL') return true; + if (activeFloor === 0) return f.id.includes('ground'); + if (activeFloor === 1) return f.id.includes('level1'); + if (activeFloor === 2) return f.id.includes('level2'); + if (activeFloor === 3) return f.id.includes('level3'); + return true; + }).map((f) => ( + + ))} +
+ )} +
+ Naperville pretotype · Pascal facility · envelope inferred +
+ + )} +
+ ); +}; + +export default PascalFacility; diff --git a/components/ThreeScene.tsx b/components/ThreeScene.tsx index e603a6e..f9f7bdf 100644 --- a/components/ThreeScene.tsx +++ b/components/ThreeScene.tsx @@ -1,4 +1,5 @@ +/** Legacy Three.js primitive facility. Map view now mounts `PascalFacility`. */ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Canvas, useFrame, useThree } from '@react-three/fiber'; import { diff --git a/components/facility/SketchMap.tsx b/components/facility/SketchMap.tsx new file mode 100644 index 0000000..781e0ad --- /dev/null +++ b/components/facility/SketchMap.tsx @@ -0,0 +1,199 @@ +import React from 'react'; +import { Layers, Ruler, Eye, Box, Maximize2 } from 'lucide-react'; +import { FeatureData } from '../types'; +import { + AnnotationMode, + FEATURES, + FloorLevel, + SKETCH_BY_FLOOR, +} from '../../facility/program'; + +export const ControlsOverlay: React.FC<{ + activeFloor: FloorLevel; + setActiveFloor: (f: FloorLevel) => void; + annotationMode: AnnotationMode; + setAnnotationMode: (m: AnnotationMode) => void; +}> = ({ activeFloor, setActiveFloor, annotationMode, setAnnotationMode }) => { + return ( +
+
+
+ Floor View +
+ {[ + { id: 3, label: 'L3: Lab' }, + { id: 2, label: 'L2: Social' }, + { id: 1, label: 'L1: Racquet' }, + { id: 0, label: 'G: Tennis' }, + { id: 'ALL', label: 'Full Facility' }, + ].map((item) => ( + + ))} +
+ +
+
+ Overlay +
+ + + +
+
+ ); +}; + +const CourtDiagram: React.FC<{ + variant: 'tennis' | 'badminton' | 'pickle' | 'farm' | 'campus'; +}> = ({ variant }) => { + if (variant === 'farm') { + return ( +
+
+
+ + GRASS LAB · one zone + + + 500 m² / section · count unspecified + +
+
+ ); + } + + const surface = + variant === 'pickle' ? 'bg-[#3d7a3a]' : variant === 'badminton' ? 'bg-[#1f6b4a]' : 'bg-[#3f6b1d]'; + + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export const SketchFallback: React.FC<{ + activeFloor: FloorLevel; + setActiveFloor: (f: FloorLevel) => void; + annotationMode: AnnotationMode; + setAnnotationMode: (m: AnnotationMode) => void; + selectedId: string | null; + onSelect: (feature: FeatureData) => void; + reason?: string; +}> = ({ + activeFloor, + setActiveFloor, + annotationMode, + setAnnotationMode, + selectedId, + onSelect, + reason = 'WebGL did not paint', +}) => { + const sketch = SKETCH_BY_FLOOR[String(activeFloor)] ?? SKETCH_BY_FLOOR.ALL; + const showLabels = annotationMode === 'LABELS' || annotationMode === 'MEASUREMENTS'; + const visibleFeatures = FEATURES.filter((f) => { + if (!showLabels) return false; + if (activeFloor === 'ALL') return true; + if (activeFloor === 0) return f.id.includes('ground'); + if (activeFloor === 1) return f.id.includes('level1'); + if (activeFloor === 2) return f.id.includes('level2'); + if (activeFloor === 3) return f.id.includes('level3'); + return true; + }); + + return ( +
+ + +
+

{sketch.title}

+

{sketch.note}

+ + CSS SKETCH · {reason} + + {annotationMode === 'MEASUREMENTS' && ( + + {sketch.variant === 'farm' + ? '500 m² / section (origin)' + : 'envelope 140×120 m · inferred'} + + )} +
+ {visibleFeatures.length > 0 && ( +
+ {visibleFeatures.map((f) => ( + + ))} +
+ )} +
+ Naperville pretotype · Pascal program sketch +
+
+ ); +}; diff --git a/facility/check-scene.ts b/facility/check-scene.ts new file mode 100644 index 0000000..69efb75 --- /dev/null +++ b/facility/check-scene.ts @@ -0,0 +1,27 @@ +import { generateLawnTechScene, sceneStats } from './generateScene.ts'; + +const scene = generateLawnTechScene(); +const stats = sceneStats(scene); +const required = ['site_naperville', 'building_lawntech', 'level_0', 'level_1', 'level_2', 'level_3', 'zone_grass-lab']; +const missing = required.filter((id) => !scene.nodes[id]); +const farmZones = Object.values(scene.nodes).filter( + (node) => node.type === 'zone' && String(node.id).includes('grass'), +); + +if (missing.length) { + throw new Error(`missing nodes: ${missing.join(', ')}`); +} +if (stats.kinds.level !== 4) { + throw new Error(`expected 4 levels, got ${stats.kinds.level}`); +} +if (farmZones.length !== 1) { + throw new Error(`expected one grass-lab zone, got ${farmZones.length}`); +} +if (stats.hasFourFarmSections) { + throw new Error('inferred farm sections leaked into the Pascal scene'); +} +if ((scene.nodes.site_naperville.metadata as { envelope?: string }).envelope !== 'inferred-140x120') { + throw new Error('site envelope stamp missing'); +} + +console.log(JSON.stringify({ ok: true, ...stats }, null, 2)); diff --git a/facility/generateScene.ts b/facility/generateScene.ts new file mode 100644 index 0000000..28ca91a --- /dev/null +++ b/facility/generateScene.ts @@ -0,0 +1,417 @@ +import { + BuildingNode, + DoorNode, + LevelNode, + SiteNode, + SlabNode, + WallNode, + WindowNode, + ZoneNode, + type AnyNode, + type AnyNodeId, +} from '@pascal-app/core'; +import { + BUILDING_DEPTH, + BUILDING_ID, + BUILDING_WIDTH, + SITE_ID, + WALL_HEIGHT, + WALL_THICKNESS, +} from './program.ts'; + +type MaterialPreset = 'concrete' | 'plaster' | 'glass' | 'wood' | 'metal'; +type SceneNodes = Record; +type Point = [number, number]; + +type Room = { + id: string; + name: string; + color: string; + polygon: Point[]; + floorFinish: string; + occupancy: string; + metadata: Record; +}; + +type Opening = { + id: string; + kind: 'door' | 'window'; + along: number; + width: number; + height: number; + sill?: number; +}; + +const X0 = -BUILDING_WIDTH / 2; +const Z0 = -BUILDING_DEPTH / 2; +const X1 = BUILDING_WIDTH / 2; +const Z1 = BUILDING_DEPTH / 2; + +function rect(x: number, z: number, w: number, d: number): Point[] { + return [ + [x, z], + [x + w, z], + [x + w, z + d], + [x, z + d], + ]; +} + +function add(nodes: SceneNodes, node: AnyNode) { + nodes[node.id] = node; + return node; +} + +function wallLen(start: Point, end: Point) { + return Math.hypot(end[0] - start[0], end[1] - start[1]); +} + +function addWall( + nodes: SceneNodes, + levelId: string, + id: `wall_${string}`, + start: Point, + end: Point, + material: MaterialPreset, + openings: Opening[] = [], +) { + const children = openings.map((opening) => + opening.kind === 'door' ? (`door_${opening.id.replace(/^door_/, '')}` as const) : (`window_${opening.id.replace(/^window_/, '')}` as const), + ); + const wall = WallNode.parse({ + id, + name: id, + parentId: levelId, + start, + end, + height: WALL_HEIGHT, + thickness: WALL_THICKNESS, + material: { preset: material }, + children, + frontSide: 'exterior', + backSide: 'interior', + }); + add(nodes, wall); + + const length = wallLen(start, end); + for (const opening of openings) { + const along = Math.min(Math.max(opening.along, opening.width / 2), length - opening.width / 2); + if (opening.kind === 'door') { + const doorId = `door_${opening.id.replace(/^door_/, '')}` as const; + add( + nodes, + DoorNode.parse({ + id: doorId, + name: opening.id, + parentId: wall.id, + wallId: wall.id, + position: [along, opening.height / 2, 0], + width: opening.width, + height: opening.height, + doorCategory: 'interior', + doorType: opening.width >= 3.5 ? 'double' : 'hinged', + }), + ); + } else { + const windowId = `window_${opening.id.replace(/^window_/, '')}` as const; + const sill = opening.sill ?? 1.2; + add( + nodes, + WindowNode.parse({ + id: windowId, + name: opening.id, + parentId: wall.id, + wallId: wall.id, + position: [along, sill + opening.height / 2, 0], + width: opening.width, + height: opening.height, + windowType: 'fixed', + material: { preset: 'glass' }, + }), + ); + } + } + + return wall; +} + +function addRoom(nodes: SceneNodes, levelId: string, room: Room, slabMaterial: MaterialPreset) { + const zone = ZoneNode.parse({ + id: `zone_${room.id}`, + parentId: levelId, + name: room.name, + polygon: room.polygon, + color: room.color, + spaceRole: 'room', + occupancy: room.occupancy, + floorFinish: room.floorFinish, + ceilingHeight: WALL_HEIGHT, + metadata: room.metadata, + }); + const slab = SlabNode.parse({ + id: `slab_${room.id}`, + name: `${room.name} slab`, + parentId: levelId, + polygon: room.polygon, + elevation: 0.05, + material: { preset: slabMaterial }, + }); + add(nodes, zone); + add(nodes, slab); + return [zone.id, slab.id] as const; +} + +function envelopeOpenings(floor: number): { south: Opening[]; east: Opening[]; north: Opening[]; west: Opening[] } { + const ribbon = (prefix: string): Opening[] => [ + { id: `${prefix}_a`, kind: 'window', along: 28, width: 8, height: 3.2, sill: 1.4 }, + { id: `${prefix}_b`, kind: 'window', along: 112, width: 8, height: 3.2, sill: 1.4 }, + ]; + const south: Opening[] = + floor === 0 + ? [ + { id: 'entry', kind: 'door', along: BUILDING_WIDTH / 2, width: 4.2, height: 3.2 }, + { id: 'l0_south_a', kind: 'window', along: 28, width: 8, height: 3.2, sill: 1.4 }, + { id: 'l0_south_b', kind: 'window', along: 112, width: 8, height: 3.2, sill: 1.4 }, + ] + : ribbon(`l${floor}_south`); + return { + south, + east: ribbon(`l${floor}_east`), + north: ribbon(`l${floor}_north`), + west: ribbon(`l${floor}_west`), + }; +} + +function roomsForFloor(floor: number): Room[] { + if (floor === 0) { + return [ + { + id: 'tennis', + name: 'Tennis arena', + color: '#3f6b1d', + polygon: rect(X0, Z0, 120, BUILDING_DEPTH), + floorFinish: 'sport-court', + occupancy: '24 tennis courts (hard/clay/grass/wood — split unspecified)', + metadata: { originCount: 24, honesty: 'SPEC', split: 'unspecified' }, + }, + { + id: 'pro-shop', + name: 'Pro shop', + color: '#8B5A2B', + polygon: rect(X0 + 120, Z0, 20, 60), + floorFinish: 'wood', + occupancy: 'retail', + metadata: { honesty: 'SPEC' }, + }, + { + id: 'lockers', + name: 'Lockers', + color: '#4a5568', + polygon: rect(X0 + 120, Z0 + 60, 20, 60), + floorFinish: 'tile', + occupancy: 'lockers', + metadata: { honesty: 'SPEC' }, + }, + ]; + } + if (floor === 1) { + return [ + { + id: 'badminton', + name: 'Badminton', + color: '#1f6b4a', + polygon: rect(X0, Z0, 80, BUILDING_DEPTH), + floorFinish: 'sport-court', + occupancy: '16 badminton courts', + metadata: { originCount: 16, honesty: 'SPEC' }, + }, + { + id: 'squash', + name: 'Squash', + color: '#2c5282', + polygon: rect(X0 + 80, Z0, 60, 60), + floorFinish: 'sport-court', + occupancy: '4 squash courts', + metadata: { originCount: 4, honesty: 'SPEC' }, + }, + { + id: 'table-tennis', + name: 'Table tennis', + color: '#2b6cb0', + polygon: rect(X0 + 80, Z0 + 60, 60, 60), + floorFinish: 'sport-court', + occupancy: '16 table tennis stations', + metadata: { originCount: 16, honesty: 'SPEC' }, + }, + ]; + } + if (floor === 2) { + return [ + { + id: 'pickleball', + name: 'Pickleball', + color: '#3d7a3a', + polygon: rect(X0, Z0, 90, BUILDING_DEPTH), + floorFinish: 'sport-court', + occupancy: '8 pickleball courts', + metadata: { originCount: 8, honesty: 'SPEC' }, + }, + { + id: 'real-tennis', + name: 'Real tennis', + color: '#6b4f2a', + polygon: rect(X0 + 90, Z0, 50, BUILDING_DEPTH), + floorFinish: 'sport-court', + occupancy: '1 real tennis court', + metadata: { originCount: 1, honesty: 'SPEC' }, + }, + ]; + } + return [ + { + id: 'grass-lab', + name: 'Grass lab', + color: '#1a3d24', + polygon: rect(X0, Z0, BUILDING_WIDTH, BUILDING_DEPTH), + floorFinish: 'grow-deck', + occupancy: '500 m² per section; section count unspecified', + metadata: { + honesty: 'PLANNED', + origin: '500m2-per-section', + sectionCount: 'unspecified', + }, + }, + ]; +} + +function interiorWalls(floor: number): Array<{ id: `wall_${string}`; start: Point; end: Point; doorAlong: number }> { + if (floor === 0) { + return [ + { id: 'wall_l0_amenity', start: [X0 + 120, Z0], end: [X0 + 120, Z1], doorAlong: 60 }, + { id: 'wall_l0_shop_lockers', start: [X0 + 120, 0], end: [X1, 0], doorAlong: 10 }, + ]; + } + if (floor === 1) { + return [ + { id: 'wall_l1_split', start: [X0 + 80, Z0], end: [X0 + 80, Z1], doorAlong: 60 }, + { id: 'wall_l1_squash_tt', start: [X0 + 80, 0], end: [X1, 0], doorAlong: 30 }, + ]; + } + if (floor === 2) { + return [{ id: 'wall_l2_heritage', start: [X0 + 90, Z0], end: [X0 + 90, Z1], doorAlong: 60 }]; + } + return []; +} + +export type LawnTechScene = { + nodes: SceneNodes; + rootNodeIds: AnyNodeId[]; +}; + +export function generateLawnTechScene(): LawnTechScene { + const nodes: SceneNodes = {}; + const sitePad = 20; + const site = SiteNode.parse({ + id: SITE_ID, + name: 'Naperville — LawnTech pretotype', + polygon: { + type: 'polygon', + points: [ + [X0 - sitePad, Z0 - sitePad], + [X1 + sitePad, Z0 - sitePad], + [X1 + sitePad, Z1 + sitePad], + [X0 - sitePad, Z1 + sitePad], + ], + }, + metadata: { + origin: 'Naperville', + honesty: 'pretotype', + envelope: 'inferred-140x120', + wallHeight: 'inferred-10m-sport-hall', + compiler: 'ace-facility@1', + farmSections: 'unspecified', + }, + children: [BUILDING_ID], + }); + + const building = BuildingNode.parse({ + id: BUILDING_ID, + name: 'LawnTech Dynamics', + parentId: site.id, + position: [0, 0, 0], + rotation: [0, 0, 0], + children: ['level_0', 'level_1', 'level_2', 'level_3'], + }); + + const names = [ + 'Ground — tennis arena', + 'L1 — racquet mezzanine', + 'L2 — pickleball & heritage', + 'L3 — grass lab', + ]; + + for (let floor = 0; floor < 4; floor += 1) { + const levelId = `level_${floor}` as const; + const rooms = roomsForFloor(floor); + const openings = envelopeOpenings(floor); + const childIds: string[] = []; + + for (const room of rooms) { + const [zoneId, slabId] = addRoom( + nodes, + levelId, + room, + floor === 3 ? 'concrete' : floor === 0 && room.id !== 'tennis' ? 'wood' : 'tile', + ); + childIds.push(zoneId, slabId); + } + + const south = addWall(nodes, levelId, `wall_l${floor}_south`, [X0, Z0], [X1, Z0], 'concrete', openings.south); + const east = addWall(nodes, levelId, `wall_l${floor}_east`, [X1, Z0], [X1, Z1], 'concrete', openings.east); + const north = addWall(nodes, levelId, `wall_l${floor}_north`, [X1, Z1], [X0, Z1], 'concrete', openings.north); + const west = addWall(nodes, levelId, `wall_l${floor}_west`, [X0, Z1], [X0, Z0], 'concrete', openings.west); + childIds.push(south.id, east.id, north.id, west.id); + + for (const partition of interiorWalls(floor)) { + const wall = addWall(nodes, levelId, partition.id, partition.start, partition.end, 'plaster', [ + { + id: `${partition.id}_door`, + kind: 'door', + along: partition.doorAlong, + width: 2.4, + height: 2.4, + }, + ]); + childIds.push(wall.id); + } + + const level = LevelNode.parse({ + id: levelId, + name: names[floor], + parentId: building.id, + level: floor, + height: WALL_HEIGHT, + children: childIds, + metadata: { honesty: floor === 3 ? 'PLANNED' : 'SPEC' }, + }); + add(nodes, level); + } + + add(nodes, building); + add(nodes, site); + + return { nodes, rootNodeIds: [site.id] }; +} + +export function sceneStats(scene: LawnTechScene) { + const kinds: Record = {}; + for (const node of Object.values(scene.nodes)) { + const kind = String((node as { type?: string }).type ?? 'unknown'); + kinds[kind] = (kinds[kind] ?? 0) + 1; + } + return { + nodeCount: Object.keys(scene.nodes).length, + roots: scene.rootNodeIds, + kinds, + hasFourFarmSections: Object.keys(scene.nodes).some((id) => String(id).includes('section')), + }; +} diff --git a/facility/pascal-editor-shim.ts b/facility/pascal-editor-shim.ts new file mode 100644 index 0000000..7750320 --- /dev/null +++ b/facility/pascal-editor-shim.ts @@ -0,0 +1,186 @@ +/** + * Read-only stub for `@pascal-app/editor`. + * + * `@pascal-app/nodes` 1.0.0 still imports editor tools/panels at module load. + * ACE mounts the Viewer only — never the Pascal editor, Next, or HomeForge. + */ + +export const EDITOR_LAYER = 1; +export const NO_RAYCAST = 0; +export const WALL_CONNECT_SNAP_RADIUS = 0.4; +export const WALL_JOIN_SNAP_RADIUS = 0.4; +export const ARROW_COLOR = '#DFFF4F'; +export const ARROW_SCALE = 1; +export const MEASUREMENT_ACTIVE_COLOR = '#DFFF4F'; +export const DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE = Math.PI; +export const DRAWING_TYPE_OPTIONS = [] as const; +export const DRAFTING_SURFACE_EXTENSION_KEY = 'pascal:editor/drafting-surface'; + +const empty = {}; +const noop = (..._args: unknown[]) => null; +const False = (..._args: unknown[]) => false; +const Zero = (..._args: unknown[]) => 0; + +export function cn(...parts: Array) { + return parts.filter(Boolean).join(' '); +} + +function storeHook(selector?: (state: Record) => unknown) { + return selector ? selector(empty) : empty; +} +storeHook.getState = () => empty; +storeHook.setState = () => {}; +storeHook.subscribe = () => () => {}; + +export const useAlignmentGuides = storeHook; +export const useDraftNode = storeHook; +export const useDragAction = storeHook; +export const useDrawingView = storeHook; +export const useEditingHole = storeHook; +export const useEditor = storeHook; +export const useFacingPose = storeHook; +export const useFenceCurveDraft = storeHook; +export const useFloorplanDraftPreview = storeHook; +export const useFloorplanRender = storeHook; +export const useFreshPlacementVisibility = storeHook; +export const useInteractionScope = storeHook; +export const useMeasurementDraft = storeHook; +export const useOpeningGuides = storeHook; +export const usePathDraftPreview = storeHook; +export const usePlacementCoordinator = storeHook; +export const usePlacementPreview = storeHook; +export const useRegistryToolContext = storeHook; +export const useSegmentDraftChain = storeHook; +export const useWallMoveGhosts = storeHook; +export const useWallSnapIndicator = storeHook; + +export const ActionButton = (_props: Record) => null; +export const ActionGroup = (_props: Record) => null; +export const CollectionsPopover = (_props: Record) => null; +export const CursorSphere = (_props: Record) => null; +export const DimensionPill = (_props: Record) => null; +export const DragBoundingBox = (_props: Record) => null; +export const FloorplanGeometryRenderer = (_props: Record) => null; +export const HandleArrow = (_props: Record) => null; +export const MeasurementPill = (_props: Record) => null; +export const MetricControl = (_props: Record) => null; +export const NodeActionMenu = (_props: Record) => null; +export const PanelSection = (_props: Record) => null; +export const PanelWrapper = (_props: Record) => null; +export const PlacementBox = (_props: Record) => null; +export const PlacementDimensionGuides = (_props: Record) => null; +export const PolygonEditor = (_props: Record) => null; +export const SegmentedControl = (_props: Record) => null; +export const SliderControl = (_props: Record) => null; +export const ToggleControl = (_props: Record) => null; + +export const activateQuickMeasurementHudSource = noop; +export const alignFloorplanDraftPoint = noop; +export const applyFloorplanAlignment = noop; +export const boundaryReshapeScope = noop; +export const buildFloorplanStairEntry = noop; +export const buildMeasurementAngleArcPoints = noop; +export const buildSvgAnnularSectorPath = noop; +export const buildSvgArcPath = noop; +export const buildSvgArrowHeadPoints = noop; +export const calculateItemRotation = noop; +export const chainEndJoinsExistingWall = False; +export const clearCeilingSnapFeedback = noop; +export const clearPlacementSurface = noop; +export const clearQuickMeasurementHudSource = noop; +export const clearSlabSnapFeedback = noop; +export const clearStructuralElevationGuide = noop; +export const clearSurfacePlanSnapFeedback = noop; +export const commitFreshPlacementSubtree = noop; +export const commitMeasurementDraft = noop; +export const consumePlacementDragRelease = noop; +export const createEditorApi = noop; +export const createFenceOnCurrentLevel = noop; +export const createFreshPlacementSubtree = noop; +export const createQuickMeasurementPointerScheduler = noop; +export const createSplineFenceOnCurrentLevel = noop; +export const createWallOnCurrentLevel = noop; +export const curveReshapeScope = noop; +export const duplicateRoofSubtree = noop; +export const duplicateStairSubtree = noop; +export const finishMeasurementDraft = noop; +export const floorplanGeometryMetadata = noop; +export const formatAngleRadians = noop; +export const formatAreaLabel = noop; +export const formatLinearMeasurement = noop; +export const formatMeasurement = noop; +export const formatVolumeLabel = noop; +export const getAngleArcToSegmentReference = noop; +export const getAngleToSegmentReference = noop; +export const getArcPlanPoint = noop; +export const getFloatingMenuScale = Zero; +export const getFloorStackPreviewPosition = noop; +export const getFloorplanWallThickness = Zero; +export const getGridEventScreenProjection = noop; +export const getLinearUnitLabel = Zero; +export const getSegmentAngleReferenceAtPoint = Zero; +export const getSegmentGridStep = Zero; +export const getSideFromNormal = noop; +export const getStairLevelOptions = noop; +export const handleMeasurementDraftEscape = noop; +export const hasRoofFaceChildOverlap = False; +export const holeEditScope = noop; +export const isAlignmentGuideActive = False; +export const isAngleSnapActive = False; +export const isFreshPlacementMetadata = False; +export const isGridSnapActive = False; +export const isMagneticSnapActive = False; +export const isSegmentLongEnough = False; +export const isValidWallSideFace = False; +export const linearControlValueToMeters = noop; +export const linearUnitToMeters = Zero; +export const markToolCancelConsumed = noop; +export const measurementFloorplanPresentationColor = noop; +export const measurementPolygonLabelAnchor = noop; +export const measurementPolygonMidpoints = noop; +export const measurementPresentationColor = noop; +export const meshEditScope = noop; +export const metersToLinearUnit = Zero; +export const movementSfxStepKey = noop; +export const parseMeasurement = noop; +export const projectAlignmentGuidesWorldToActiveBuildingLocal = noop; +export const publishHorizontalConstructionPlane = noop; +export const publishPlacementSurface = noop; +export const publishQuickMeasurementHudSource = noop; +export const publishResolvedElevationGuide = noop; +export const publishStructuralElevationGuide = noop; +export const readFloorplanContext = noop; +export const readFloorplanGeometryMetadata = noop; +export const readFloorplanMetricNotationOverride = noop; +export const resampleTerrainConstructionPlane = noop; +export const resolveAlignmentForActiveBuilding = noop; +export const resolveCeilingPlanPointSnap = noop; +export const resolveElevatorNodeSupportY = noop; +export const resolveElevatorSupportY = noop; +export const resolveEndpointWallSplit = noop; +export const resolveEventConstructionPlane = noop; +export const resolveLevelConstructionPlane = noop; +export const resolvePlanarCursorPosition = noop; +export const resolvePointerSupportSurface = noop; +export const resolveQuickMeasurementReport = noop; +export const resolveRoofWallHit = noop; +export const resolveSlabEdgeBandSnap = noop; +export const resolveSlabPlanPointSnap = noop; +export const resolveStairDestinationLevel = noop; +export const resolveStairFromLevelId = noop; +export const resolveStairToLevelId = noop; +export const resolveStructuralElevationSnap = noop; +export const resolveSurfacePlanPointSnap = noop; +export const snapBuildingLocalToWorldGrid = noop; +export const snapFenceDraftPoint = noop; +export const snapPointToGrid = noop; +export const snapScalarToGrid = noop; +export const snapToHalf = noop; +export const snapWallDraftPoint = noop; +export const snapWallDraftPointDetailed = noop; +export const snapWorldXZForActiveBuilding = noop; +export const stripPlacementMetadataFlags = noop; +export const swallowNextClick = noop; +export const triangulateMeasurementPolygon = noop; +export const triggerSFX = noop; +export const withFloorplanGeometryMetadata = noop; diff --git a/facility/program.ts b/facility/program.ts new file mode 100644 index 0000000..0eafdd4 --- /dev/null +++ b/facility/program.ts @@ -0,0 +1,82 @@ +import { FeatureData } from '../types'; + +/** Inferred envelope from the earlier Three.js sketch — not in the origin spec. */ +export const BUILDING_WIDTH = 140; +export const BUILDING_DEPTH = 120; +/** Sport-hall storey, not the 20 m theater height used by the old primitive scene. */ +export const WALL_HEIGHT = 10; +export const WALL_THICKNESS = 0.35; + +export const SITE_ID = 'site_naperville'; +export const BUILDING_ID = 'building_lawntech'; + +export type FloorLevel = 'ALL' | 0 | 1 | 2 | 3; +export type AnnotationMode = 'NONE' | 'LABELS' | 'MEASUREMENTS'; + +export const FEATURES: FeatureData[] = [ + { + id: 'ground_tennis', + title: 'Ground: Tennis Arena', + description: + '24 tennis courts (hard, clay, grass, wood) plus a pro shop. Surface split is unspecified in origin — this sketch does not invent 6/6/6/6.', + icon: '🎾', + position: [0, 5, 20], + }, + { + id: 'level1_racquet', + title: 'L1: Racquet Mezzanine', + description: '16 badminton, 4 squash, 16 table tennis.', + icon: '🏸', + position: [-20, 15, 0], + }, + { + id: 'level2_social', + title: 'L2: Pickleball & Heritage', + description: '8 pickleball courts and 1 real tennis court.', + icon: '🏓', + position: [20, 25, 0], + }, + { + id: 'level3_farm', + title: 'L3: Vertical Grass Lab', + description: + '500 m² per section (origin). Section count is unspecified — this sketch is one lab zone, not four inferred racks.', + icon: '🌱', + position: [0, 35, 0], + }, +]; + +export const SKETCH_BY_FLOOR: Record< + string, + { title: string; note: string; variant: 'tennis' | 'badminton' | 'pickle' | 'farm' | 'campus' } +> = { + ALL: { + title: 'Naperville facility', + note: '24 tennis · grass lab · Pascal / CSS sketch', + variant: 'campus', + }, + '0': { + title: 'Ground: Tennis arena', + note: '24 courts · hard / clay / grass / wood', + variant: 'tennis', + }, + '1': { + title: 'L1: Racquet mezzanine', + note: '16 badminton · 4 squash · 16 table tennis', + variant: 'badminton', + }, + '2': { + title: 'L2: Pickleball & heritage', + note: '8 pickleball · 1 real tennis', + variant: 'pickle', + }, + '3': { + title: 'L3: Vertical grass lab', + note: '500 m² per section · section count unspecified', + variant: 'farm', + }, +}; + +export function levelNodeId(floor: 0 | 1 | 2 | 3): `level_${string}` { + return `level_${floor}`; +} diff --git a/package-lock.json b/package-lock.json index 2f299f5..bc7d704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,16 @@ "version": "0.0.0", "dependencies": { "@google/genai": "^1.30.0", + "@pascal-app/core": "1.0.0", + "@pascal-app/nodes": "1.0.0", + "@pascal-app/viewer": "1.0.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.0", "framer-motion": "^12.23.24", "lucide-react": "^0.554.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "three": "^0.181.2" + "react": "19.2.1", + "react-dom": "19.2.1", + "three": "0.186.0" }, "devDependencies": { "@types/node": "^22.14.0", @@ -869,6 +872,91 @@ "three": ">= 0.159.0" } }, + "node_modules/@pascal-app/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pascal-app/core/-/core-1.0.0.tgz", + "integrity": "sha512-EKbRxIU6ZKugnouKcPAQ3l+zZYXHDce5pqw7yCp8ZeceSGIDDl5p2wTM41BWpF1Pn5MpL0RTbysiPNl/3frBEA==", + "license": "MIT", + "dependencies": { + "dedent": "^1.7.1", + "idb-keyval": "^6.2.2", + "mitt": "^3.0.1", + "nanoid": "^5.1.6", + "zod": ">=4.5.4 <4.6", + "zundo": "^2.3.0", + "zustand": "^5" + }, + "peerDependencies": { + "@react-three/drei": "^10", + "@react-three/fiber": "^9", + "react": "^18 || ^19", + "three": "^0.186" + } + }, + "node_modules/@pascal-app/core/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@pascal-app/nodes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pascal-app/nodes/-/nodes-1.0.0.tgz", + "integrity": "sha512-aNHeaKqAmJ17lUD3jCeYuJ+JCAQV+OgvWH0KQuQN3D9ozKcQQtgI80DzagfoXe7DZOXSWUdGlFf1DB+Xj6afIA==", + "license": "MIT", + "peerDependencies": { + "@pascal-app/core": "^1.0.0", + "@pascal-app/editor": "^1.0.0", + "@pascal-app/viewer": "^1.0.0", + "@react-three/drei": "^10", + "@react-three/fiber": "^9", + "lucide-react": "^1", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "three": "^0.186", + "zustand": "^5" + } + }, + "node_modules/@pascal-app/viewer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pascal-app/viewer/-/viewer-1.0.0.tgz", + "integrity": "sha512-fC+qTsRPyPzrv0AsYcQrzTXOP1iXcB28vkLVsM2Y4Fg7YU19zK2BqJsbo3e/iyEMbItbl73+vR1/DnQ48Na4/Q==", + "license": "MIT", + "dependencies": { + "three-bvh-csg": "^0.0.18", + "three-mesh-bvh": "^0.9.8", + "zustand": "^5" + }, + "peerDependencies": { + "@pascal-app/core": "^1.0.0", + "@react-three/drei": "^10", + "@react-three/fiber": "^9", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "three": "^0.186" + } + }, + "node_modules/@pascal-app/viewer/node_modules/three-mesh-bvh": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.9.15.tgz", + "integrity": "sha512-9xdEDkCzFoT8VPFH9DznwFSdbaIEjMvM106Iqq7TJurrIheUKDcdW4prfJTCxryih4XS/UxWs0H3K4MLMRFyOQ==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1364,16 +1452,6 @@ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", "license": "MIT" }, - "node_modules/@types/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", - "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, "node_modules/@types/react-reconciler": { "version": "0.32.3", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.3.tgz", @@ -1706,13 +1784,6 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1739,6 +1810,20 @@ } } }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/detect-gpu": { "version": "5.0.70", "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", @@ -2081,6 +2166,12 @@ "node": ">= 14" } }, + "node_modules/idb-keyval": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", + "license": "Apache-2.0" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2304,6 +2395,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "12.23.23", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", @@ -2492,24 +2589,24 @@ } }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.1" } }, "node_modules/react-dom/node_modules/scheduler": { @@ -2835,11 +2932,21 @@ } }, "node_modules/three": { - "version": "0.181.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.181.2.tgz", - "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", + "version": "0.186.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.0.tgz", + "integrity": "sha512-cr/fIM2ddMSVbYVgkfD4jLJv7Fh/8ZTjvo+7gQeSVGUZHxpx9FDwoL5iC7hUz/LiRA8wMbqfnb90xKfm1/HHkQ==", "license": "MIT" }, + "node_modules/three-bvh-csg": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/three-bvh-csg/-/three-bvh-csg-0.0.18.tgz", + "integrity": "sha512-M3GCZMmGFgASGuDf+YMamM83nVlD/vdwzVHcYbFxgW+g1S7/nKPiuY00YVHOMbjmJPh8mLevGZL65ItHUuGt2w==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.179.0", + "three-mesh-bvh": ">=0.9.7" + } + }, "node_modules/three-mesh-bvh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", @@ -3261,6 +3368,33 @@ "dev": true, "license": "ISC" }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zundo": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/zundo/-/zundo-2.3.0.tgz", + "integrity": "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/charkour" + }, + "peerDependencies": { + "zustand": "^4.3.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "zustand": { + "optional": false + } + } + }, "node_modules/zustand": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", diff --git a/package.json b/package.json index d05a4ec..d8abc69 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,21 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "check:facility": "npx --yes esbuild facility/check-scene.ts --bundle --platform=node --format=esm --outfile=/tmp/ace-check-facility.mjs && node /tmp/ace-check-facility.mjs" }, "dependencies": { + "@pascal-app/core": "1.0.0", + "@pascal-app/nodes": "1.0.0", + "@pascal-app/viewer": "1.0.0", "framer-motion": "^12.23.24", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "19.2.1", + "react-dom": "19.2.1", "@google/genai": "^1.30.0", "lucide-react": "^0.554.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.0", - "three": "^0.181.2" + "three": "0.186.0" }, "devDependencies": { "@types/node": "^22.14.0", diff --git a/vite.config.ts b/vite.config.ts index d18258b..a9900da 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,26 @@ import path from 'path'; -import { defineConfig, loadEnv } from 'vite'; +import { defineConfig, loadEnv, Plugin } from 'vite'; import react from '@vitejs/plugin-react'; +function pascalForceWebgl(): Plugin { + return { + name: 'ace-pascal-webgl', + enforce: 'pre', + transform(code, id) { + const normalized = id.split('?')[0].replaceAll('\\', '/'); + if (!normalized.endsWith('/renderer-capability.js')) return null; + if (!code.includes('function browserGpu()')) return null; + return { + code: code.replace( + 'function browserGpu() {', + `function browserGpu() {\n if (typeof window !== 'undefined' && window.__PASCAL_BACKEND__ === 'webgl2') return null;`, + ), + map: null, + }; + }, + }; +} + export default defineConfig(({ mode }) => { const env = loadEnv(mode, '.', ''); @@ -14,14 +33,19 @@ export default defineConfig(({ mode }) => { port: 3000, host: '0.0.0.0', }, - plugins: [react()], + plugins: [pascalForceWebgl(), react()], define: { 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'), + }, + optimizeDeps: { + exclude: ['@pascal-app/viewer', '@pascal-app/nodes'], }, resolve: { alias: { '@': path.resolve(__dirname, '.'), + '@pascal-app/editor': path.resolve(__dirname, 'facility/pascal-editor-shim.ts'), } } }; From f075c21b770850a26d2f58b5eeb493b22087edce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 20:18:28 +0000 Subject: [PATCH 5/8] Onboard the OSS loop and encode the APEX campus vision. ACE is a peak-performance pretotype, not a tennis-only twin. Pascal now compiles the Naperville racquet SPEC plus a VISION APEX wing (labs, physio, gym, pool, clubhouse). Mutation is n/a. Pages still deploy main/dev only. Co-authored-by: veeman961 --- .github/CODEOWNERS | 3 + .github/DEPLOYMENT.md | 2 + .github/ISSUE_TEMPLATE/bug.yml | 31 ++ .github/ISSUE_TEMPLATE/claim.yml | 25 ++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature.yml | 26 ++ .github/PULL_REQUEST_TEMPLATE.md | 37 +++ .github/dependabot.yml | 12 + .github/labeler.yml | 25 ++ .github/labels.md | 37 +++ .github/scripts/check-receipt.py | 140 ++++++++ .github/scripts/create-labels.sh | 38 +++ .github/scripts/expire-claims.sh | 99 ++++++ .github/workflows/automerge-nightly.yml | 31 ++ .github/workflows/automerge-preview.yml | 31 ++ .github/workflows/claim-expiry.yml | 20 ++ .github/workflows/labeler.yml | 18 ++ .github/workflows/promote-preview.yml | 27 ++ .github/workflows/receipt.yml | 22 ++ .github/workflows/scorecard.yml | 35 ++ .github/workflows/stale.yml | 26 ++ .github/workflows/validate.yml | 26 ++ .npmrc | 1 + .verified-oss-loop/README.md | 19 ++ .verified-oss-loop/inventory.yml | 166 ++++++++++ .verified-oss-loop/kit-inventory.py | 404 ++++++++++++++++++++++++ .verified-oss-loop/rollout.py | 121 +++++++ .verified-oss-loop/rollout.yml | 5 + AGENTS.md | 93 ++++++ App.tsx | 82 +++-- CONTRIBUTING.md | 44 +++ README.md | 197 ++++-------- ROADMAP.md | 24 ++ SECURITY.md | 6 + components/AIChat.tsx | 6 +- components/PascalFacility.tsx | 24 +- components/Specifications.tsx | 18 +- components/facility/SketchMap.tsx | 64 +++- facility/check-scene.ts | 55 +++- facility/generateScene.ts | 102 +++++- facility/program.ts | 51 ++- facility/vision.ts | 84 +++++ package-lock.json | 1 + package.json | 4 +- prompt.md | 57 ++++ roadmap.example.yml | 8 + scripts/cluster-similar-issues.py | 113 +++++++ scripts/ensure-rollout-branches.sh | 54 ++++ scripts/rollout.py | 121 +++++++ services/geminiService.ts | 47 ++- skills/anti-slop/SKILL.md | 30 ++ skills/autodevelop/SKILL.md | 28 ++ skills/dr-eggbot/SKILL.md | 34 ++ skills/orient/SKILL.md | 23 ++ skills/pstack/SKILL.md | 26 ++ skills/tdd/SKILL.md | 22 ++ skills/verify/SKILL.md | 22 ++ 57 files changed, 2651 insertions(+), 221 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/claim.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/labeler.yml create mode 100644 .github/labels.md create mode 100644 .github/scripts/check-receipt.py create mode 100755 .github/scripts/create-labels.sh create mode 100755 .github/scripts/expire-claims.sh create mode 100644 .github/workflows/automerge-nightly.yml create mode 100644 .github/workflows/automerge-preview.yml create mode 100644 .github/workflows/claim-expiry.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/promote-preview.yml create mode 100644 .github/workflows/receipt.yml create mode 100644 .github/workflows/scorecard.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .github/workflows/validate.yml create mode 100644 .npmrc create mode 100644 .verified-oss-loop/README.md create mode 100644 .verified-oss-loop/inventory.yml create mode 100644 .verified-oss-loop/kit-inventory.py create mode 100644 .verified-oss-loop/rollout.py create mode 100644 .verified-oss-loop/rollout.yml create mode 100644 AGENTS.md create mode 100644 CONTRIBUTING.md create mode 100644 ROADMAP.md create mode 100644 SECURITY.md create mode 100644 facility/vision.ts create mode 100644 prompt.md create mode 100644 roadmap.example.yml create mode 100644 scripts/cluster-similar-issues.py create mode 100755 scripts/ensure-rollout-branches.sh create mode 100644 scripts/rollout.py create mode 100644 skills/anti-slop/SKILL.md create mode 100644 skills/autodevelop/SKILL.md create mode 100644 skills/dr-eggbot/SKILL.md create mode 100644 skills/orient/SKILL.md create mode 100644 skills/pstack/SKILL.md create mode 100644 skills/tdd/SKILL.md create mode 100644 skills/verify/SKILL.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0e1c814 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# ace CODEOWNERS +# Maintainers own merge. Path owners review; they are not a merge bot. +* @kvnloo diff --git a/.github/DEPLOYMENT.md b/.github/DEPLOYMENT.md index 9215afe..14d0d06 100644 --- a/.github/DEPLOYMENT.md +++ b/.github/DEPLOYMENT.md @@ -32,6 +32,8 @@ After deployment, your sites will be available at: - **Production (main)**: `https://.github.io//` - **Development (dev)**: `https://.github.io//dev/` +Rolling Verified OSS Loop branches (`preview`, `nightly`) are **not** Pages slots. Do not invent `/preview//` without extending `.github/workflows/deploy.yml`. + Replace `` with your GitHub username and `` with your repository name. ## How It Works diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..c3978b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,31 @@ +name: Bug +description: Something is broken. Include a red command if you have one. +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Search open issues and PRs first. Issues are not claims — a claim lease comes after this is triaged `claimable`. + - type: textarea + id: expected + attributes: + label: Expected + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual + validations: + required: true + - type: textarea + id: red + attributes: + label: Red command + description: Exact command that fails, or "manual / no command yet" + placeholder: pytest tests/test_foo.py::test_bar + - type: textarea + id: environment + attributes: + label: Environment + placeholder: OS, language version, commit SHA diff --git a/.github/ISSUE_TEMPLATE/claim.yml b/.github/ISSUE_TEMPLATE/claim.yml new file mode 100644 index 0000000..0a87591 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/claim.yml @@ -0,0 +1,25 @@ +name: Claim +description: Bounded claim lease for an already-triaged claimable issue. Prefer a comment on the existing issue. +title: "claim: " +labels: ["claimed"] +body: + - type: markdown + attributes: + value: | + Prefer commenting this YAML on the existing issue. Open this form only if the project asks for a dedicated claim issue. + - type: textarea + id: lease + attributes: + label: Lease + value: | + ```yaml + issue: + claimant: + base_revision: + claimed_at: + expires_at: + scope: + work_url: + ``` + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..de0aa65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Discussions + url: https://github.com/kvnloo/ace/discussions + about: Ideas, adoption help, or questions that are not a claimable issue diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..5ac0089 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,26 @@ +name: Feature +description: A proposed change. Maintainers own whether it is claimable. +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Do not start work until this issue is `claimable` and you hold a claim lease. + - type: textarea + id: problem + attributes: + label: Problem + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + validations: + required: true + - type: textarea + id: acceptance + attributes: + label: Acceptance + description: What a reviewer can check on an exact head + placeholder: test name, docs path, or runtime check diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e85e1a3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ +## Summary + + + +## Checklist + +- [ ] **Ownership respected**: changes stay inside `AGENTS.md` boundaries +- [ ] **Searched issues/PRs**: no duplicate in-flight work +- [ ] **Claimed issue**: work started after a bounded claim, not from the issue title alone +- [ ] **Fail-then-pass**: bug fixes include the red command and the green command +- [ ] **No secrets**: no tokens, API keys, `.env`, or pairing files +- [ ] **Mode** (select one): + - [ ] **Unattended** — donated compute (cloud agent) + - [ ] **Copilot** — human-supervised +- [ ] **Workers never merge `main`/`dev`**: this PR does not grant worker merge of production + +## Evidence + +```yaml +issue: +base_revision: +head_revision: +tests: + red: + green: + sabotage: +mutation: n/a +runtime_evidence: [] +limitations: [] +ai_assistance: +``` + +Tests from another head are not evidence. If mutation is `n/a`, write `n/a` — do not invent a score. The receipt workflow fails the PR if these keys are empty or `head_revision` is not this PR's SHA. + +## Related + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..04d1c80 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# Stack-neutral: GitHub Actions only. Do not add npm/pip/cargo ecosystems here; +# oss-onboard must not bake a language lockfile updater into every target. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + labels: + - enhancement + - area:verify diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..fc2daaf --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,25 @@ +area:docs: + - changed-files: + - any-glob-to-any-file: + - 'docs/**/*' + - '*.md' + - 'AGENTS.md' + - 'README.md' + - 'CONTRIBUTING.md' + +area:loop: + - changed-files: + - any-glob-to-any-file: '.github/**/*' + +area:verify: + - changed-files: + - any-glob-to-any-file: + - 'tests/**/*' + - 'scripts/**/*' + - '**/*.test.*' + - 'skills/tdd/**' + - 'skills/verify/**' + - 'skills/orient/**' + - 'skills/anti-slop/**' + - 'skills/pstack/**' + - 'skills/dr-eggbot/**' diff --git a/.github/labels.md b/.github/labels.md new file mode 100644 index 0000000..cdc413b --- /dev/null +++ b/.github/labels.md @@ -0,0 +1,37 @@ +# Labels for the Verified OSS Loop + +Create these in GitHub Settings → Labels, or copy and run `.github/scripts/create-labels.sh` (`--labels` on init). + +## Loop + +| Name | Color | Meaning | +|---|---|---| +| claimable | 0E8A16 | Maintainers opened this for a bounded claim | +| claimed | FBCA04 | A live lease exists | +| needs-discussion | D876E3 | Maintainer or worker proposal; not a claim until promoted to claimable | +| needs-review | 5319E7 | Evidence receipt is attached; independent review next | +| blocked | D93F0B | External or policy block | +| keep | 1D76DB | KEEP after merge | +| discard | 6A737D | DISCARD with a lesson, not silence | + +## Priority + +| Name | Color | +|---|---| +| priority:P0 | D73A4A | +| priority:P1 | E99695 | +| priority:P2 | F9D0C4 | +| priority:P3 | FEF2C0 | + +## Kind + +| Name | Color | +|---|---| +| bug | D73A4A | +| enhancement | A2EEEF | +| security | B60205 | +| good-first-issue | 7057FF | +| area:docs | 0075CA | +| area:loop | C5DEF5 | +| area:verify | BFDADC | +| stale | FFFFFF | diff --git a/.github/scripts/check-receipt.py b/.github/scripts/check-receipt.py new file mode 100644 index 0000000..0e7e545 --- /dev/null +++ b/.github/scripts/check-receipt.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Check a Verified OSS Loop evidence receipt. Stdlib only. Does not merge.""" +from __future__ import annotations + +import argparse +import re +import sys +from typing import Any + +SHA_RE = re.compile(r"^[0-9a-f]{7,40}$", re.I) +REQUIRED_TOP = ("issue", "base_revision", "head_revision") + + +def extract_yaml_blocks(text: str) -> list[str]: + blocks = re.findall(r"```(?:ya?ml)\s*\n(.*?)```", text, re.S | re.I) + if blocks: + return blocks + stripped = text.strip() + if stripped.startswith("---") or re.search(r"^(issue|base_revision|head_revision)\s*:", stripped, re.M): + return [text] + return [text] + + +def parse_simple_yaml(block: str) -> dict[str, Any]: + """Tiny YAML subset: top-level keys and one nested mapping (tests).""" + data: dict[str, Any] = {} + current_map: dict[str, str] | None = None + current_key: str | None = None + for raw in block.splitlines(): + if not raw.strip() or raw.strip().startswith("#"): + continue + nested = re.match(r"^ ([A-Za-z0-9_]+)\s*:\s*(.*)$", raw) + if nested and current_map is not None: + current_map[nested.group(1)] = nested.group(2).strip() + continue + top = re.match(r"^([A-Za-z0-9_]+)\s*:\s*(.*)$", raw) + if not top: + continue + key, val = top.group(1), top.group(2).strip() + if val == "" or val == "|": + current_map = {} + data[key] = current_map + current_key = key + continue + data[key] = val + current_map = None + current_key = key + _ = current_key + return data + + +def pick_receipt(text: str) -> dict[str, Any]: + best: dict[str, Any] = {} + score = -1 + for block in extract_yaml_blocks(text): + parsed = parse_simple_yaml(block) + hits = sum(1 for k in REQUIRED_TOP if k in parsed) + if hits > score: + best, score = parsed, hits + return best + + +def nonempty(val: Any) -> bool: + if val is None: + return False + if isinstance(val, dict): + return any(nonempty(v) for v in val.values()) + return str(val).strip() not in ("", "null", "~") + + +def sha_ok(val: Any) -> bool: + s = str(val).strip().lower() + return bool(SHA_RE.match(s)) + + +def heads_match(receipt_head: str, expected: str) -> bool: + a, b = receipt_head.strip().lower(), expected.strip().lower() + return a == b or a.startswith(b) or b.startswith(a) + + +def check(text: str, expected_head: str | None = None) -> list[str]: + errors: list[str] = [] + data = pick_receipt(text) + if not data: + return ["no evidence YAML found (need a ```yaml receipt with issue/base_revision/head_revision)"] + + for key in REQUIRED_TOP: + if key not in data: + errors.append(f"missing {key}") + elif not nonempty(data[key]): + errors.append(f"{key} is empty") + + if "base_revision" in data and nonempty(data["base_revision"]) and not sha_ok(data["base_revision"]): + errors.append("base_revision is not a git SHA") + if "head_revision" in data and nonempty(data["head_revision"]) and not sha_ok(data["head_revision"]): + errors.append("head_revision is not a git SHA") + + tests = data.get("tests") + if not isinstance(tests, dict): + errors.append("missing tests mapping with red/green") + else: + if not nonempty(tests.get("red")): + errors.append("tests.red is empty") + if not nonempty(tests.get("green")): + errors.append("tests.green is empty") + + if expected_head: + head = str(data.get("head_revision") or "").strip() + if not head: + errors.append("head_revision missing; cannot bind to exact head") + elif not heads_match(head, expected_head): + errors.append( + f"head_revision {head} does not match PR head {expected_head} " + "(tests from another SHA are not evidence)" + ) + return errors + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Validate a Verified OSS Loop evidence receipt") + p.add_argument("--file", help="PR body or receipt markdown/YAML (default: stdin)") + p.add_argument("--head", help="Expected PR head SHA (exact-head check)") + args = p.parse_args(argv) + if args.file: + with open(args.file, encoding="utf-8") as f: + text = f.read() + else: + text = sys.stdin.read() + errors = check(text, args.head) + if errors: + print("receipt check failed:") + for e in errors: + print(f" - {e}") + return 1 + print("ok: evidence receipt is complete") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/create-labels.sh b/.github/scripts/create-labels.sh new file mode 100755 index 0000000..5655cf3 --- /dev/null +++ b/.github/scripts/create-labels.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Create Verified OSS Loop labels on the current GitHub repo. +# Requires: gh auth. Does not overwrite existing label descriptions. +set -euo pipefail + +if ! command -v gh >/dev/null 2>&1; then + echo "gh is required" >&2 + exit 1 +fi + +create() { + local name="$1" color="$2" desc="$3" + if gh label list --limit 200 --json name --jq '.[].name' | grep -Fxq "$name"; then + echo "exists: $name" + return 0 + fi + gh label create "$name" --color "$color" --description "$desc" +} + +create claimable "0E8A16" "Maintainers opened this for a bounded claim" +create claimed "FBCA04" "A live claim lease exists" +create "needs-discussion" "D876E3" "Maintainer or worker proposal; not a claim until promoted to claimable" +create needs-review "5319E7" "Evidence receipt attached; independent review next" +create blocked "D93F0B" "External or policy block" +create keep "1D76DB" "KEEP after merge" +create discard "6A737D" "DISCARD with a recorded lesson" +create "priority:P0" "D73A4A" "Critical" +create "priority:P1" "E99695" "High" +create "priority:P2" "F9D0C4" "Medium" +create "priority:P3" "FEF2C0" "Low" +create bug "D73A4A" "Something is broken" +create enhancement "A2EEEF" "Proposed change" +create security "B60205" "Security-sensitive; do not discuss exploits in public" +create "good-first-issue" "7057FF" "Good first claim after triage" +create "area:docs" "0075CA" "Documentation" +create "area:loop" "C5DEF5" "Claim/lease/receipt process" +create "area:verify" "BFDADC" "Tests, mutation, evidence" +create stale "FFFFFF" "Quiet; not a claim block" diff --git a/.github/scripts/expire-claims.sh b/.github/scripts/expire-claims.sh new file mode 100755 index 0000000..78f8555 --- /dev/null +++ b/.github/scripts/expire-claims.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Release expired Verified OSS Loop claim leases. Does not merge. Does not close issues. +# Usage: expire-claims.sh [--dry-run] [--max-age-hours 24] +set -euo pipefail + +DRY=0 +MAX_HOURS=24 +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY=1; shift ;; + --max-age-hours) MAX_HOURS="$2"; shift 2 ;; + -h|--help) + sed -n '2,4p' "$0" | sed 's/^# //' + exit 0 + ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +if ! command -v gh >/dev/null 2>&1; then + echo "gh is required" >&2 + exit 1 +fi + +export VOL_NOW_EPOCH +VOL_NOW_EPOCH="$(date -u +%s)" +export VOL_MAX_AGE_SECS=$((MAX_HOURS * 3600)) + +lease_state() { + # JSON comments array on stdin → two lines: yes|no and reason + python3 - <<'PY' +import datetime, json, os, re, sys +now = int(os.environ["VOL_NOW_EPOCH"]) +max_age = int(os.environ["VOL_MAX_AGE_SECS"]) +comments = json.load(sys.stdin) +iso = re.compile( + r"(?:expires_at|expires)\s*:\s*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:Z|[+-][0-9]{2}:[0-9]{2})?)", + re.I, +) +claim_at = None +expiry = None +for c in comments: + body = c.get("body") or "" + created = c.get("createdAt") or c.get("created_at") or "" + if re.search(r"(?i)claiming for|claimant:|claimed_at:", body): + try: + claim_at = int(datetime.datetime.fromisoformat(created.replace("Z", "+00:00")).timestamp()) + except Exception: + pass + for m in iso.finditer(body): + raw = m.group(1) + if raw.endswith("Z"): + raw = raw[:-1] + "+00:00" + try: + expiry = int(datetime.datetime.fromisoformat(raw).timestamp()) + except Exception: + pass +if expiry is not None: + expired = expiry <= now + reason = f"expires_at {expiry} <= now {now}" +elif claim_at is not None: + expired = (now - claim_at) >= max_age + reason = f"claim comment age {now - claim_at}s >= {max_age}s" +else: + expired = False + reason = "no claim timestamp" +print("yes" if expired else "no") +print(reason) +PY +} + +issues="$(gh issue list --label claimed --state open --limit 100 --json number --jq '.[].number')" +if [[ -z "$issues" ]]; then + echo "no claimed issues" + exit 0 +fi + +released=0 +while IFS= read -r num; do + [[ -n "$num" ]] || continue + comments="$(gh api "repos/{owner}/{repo}/issues/${num}/comments" --jq '[.[] | {body, createdAt: .created_at}]')" + result="$(printf '%s' "$comments" | lease_state)" + expired="$(printf '%s\n' "$result" | sed -n '1p')" + reason="$(printf '%s\n' "$result" | sed -n '2p')" + if [[ "$expired" != "yes" ]]; then + echo "keep #$num ($reason)" + continue + fi + echo "expire #$num ($reason)" + if [[ "$DRY" -eq 1 ]]; then + released=$((released + 1)) + continue + fi + gh issue edit "$num" --remove-label claimed --add-label claimable >/dev/null + gh issue comment "$num" --body "Claim lease expired (\`${reason}\`). Returned to \`claimable\`. Workers do not merge. A new claimant may take this issue." + released=$((released + 1)) +done <<<"$issues" + +echo "released $released lease(s)" diff --git a/.github/workflows/automerge-nightly.yml b/.github/workflows/automerge-nightly.yml new file mode 100644 index 0000000..c09d3e9 --- /dev/null +++ b/.github/workflows/automerge-nightly.yml @@ -0,0 +1,31 @@ +name: Automerge nightly + +on: + pull_request: + branches: [nightly] + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: write + pull-requests: write + +jobs: + automerge: + if: >- + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + - name: Enable auto-merge into nightly when scheme allows + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + run: | + if python3 scripts/rollout.py allow-automerge nightly; then + gh pr merge "$PR" --merge --auto --delete-branch=false + else + echo "scheme forbids nightly automerge" + fi diff --git a/.github/workflows/automerge-preview.yml b/.github/workflows/automerge-preview.yml new file mode 100644 index 0000000..5b59aa3 --- /dev/null +++ b/.github/workflows/automerge-preview.yml @@ -0,0 +1,31 @@ +name: Automerge preview + +on: + pull_request: + branches: [preview] + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: write + pull-requests: write + +jobs: + automerge: + if: >- + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + - name: Merge into preview when scheme allows + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + run: | + if python3 scripts/rollout.py allow-automerge preview; then + gh pr merge "$PR" --merge --delete-branch=false + else + echo "scheme forbids preview automerge" + fi diff --git a/.github/workflows/claim-expiry.yml b/.github/workflows/claim-expiry.yml new file mode 100644 index 0000000..f205960 --- /dev/null +++ b/.github/workflows/claim-expiry.yml @@ -0,0 +1,20 @@ +name: Expire claim leases + +on: + schedule: + - cron: '17 * * * *' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + expire: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Release expired claims + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash .github/scripts/expire-claims.sh diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..56ce45c --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,18 @@ +name: Auto-label PRs + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: .github/labeler.yml diff --git a/.github/workflows/promote-preview.yml b/.github/workflows/promote-preview.yml new file mode 100644 index 0000000..25c8c1e --- /dev/null +++ b/.github/workflows/promote-preview.yml @@ -0,0 +1,27 @@ +name: Promote preview to nightly + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + promote: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Merge preview into nightly when scheme allows + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if ! python3 scripts/rollout.py allow-promote preview-to-nightly; then + echo "scheme forbids preview→nightly promote" + exit 0 + fi + git fetch origin preview nightly + git checkout nightly + git merge --no-ff origin/preview -m "Promote preview into nightly" + git push origin nightly diff --git a/.github/workflows/receipt.yml b/.github/workflows/receipt.yml new file mode 100644 index 0000000..91ab965 --- /dev/null +++ b/.github/workflows/receipt.yml @@ -0,0 +1,22 @@ +name: Evidence receipt + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +jobs: + receipt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check evidence receipt and exact head + env: + BODY: ${{ github.event.pull_request.body }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: | + printf '%s' "$BODY" > /tmp/pr-body.md + python3 .github/scripts/check-receipt.py --file /tmp/pr-body.md --head "$HEAD" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..edb3a90 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,35 @@ +# OpenSSF Scorecard — security health, not a merge bot. +# Enable as a required check only if a maintainer wants it. Workers still never merge. +name: OpenSSF Scorecard + +on: + branch_protection_rule: + schedule: + - cron: '30 1 * * 1' + workflow_dispatch: + push: + branches: [main, master] + +permissions: read-all + +jobs: + analysis: + name: Scorecard + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: ossf/scorecard-action@v2.4.2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..4beacf1 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,26 @@ +name: Mark stale issues and PRs + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-issue-stale: 60 + days-before-issue-close: 14 + days-before-pr-stale: 30 + days-before-pr-close: 14 + stale-issue-label: stale + exempt-issue-labels: claimed,pinned,work-in-progress + stale-pr-message: | + This PR has been quiet. Workers do not merge. A maintainer will close or relabel. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..b90d20e --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,26 @@ +name: Validate + +on: + pull_request: + push: + branches: + - main + - preview + - nightly + - 'cursor/**' + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + - run: npm ci + - run: npm test + - run: npm run build diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..521a9f7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/.verified-oss-loop/README.md b/.verified-oss-loop/README.md new file mode 100644 index 0000000..966283a --- /dev/null +++ b/.verified-oss-loop/README.md @@ -0,0 +1,19 @@ +# Kit inventory + +This directory is written by `oss-onboard` / `init-oss-repo.sh` from [verified-oss-loop](https://github.com/kvnloo/verified-oss-loop). It is not a second loop. + +`inventory.yml` is the VCS-friendly provenance list: + +- **kit** — copied from the standard. Re-running onboard updates it when you have not edited the file. +- **modified** — started as kit, then changed in this repo. Re-run skips it. `--force` takes the new kit file. Or mark it `local`. +- **local** — added here (or marked local). Never overwritten, including under `--force`. + +New skills in a kit release appear on the next `oss-onboard` because they are missing dest files. + +```bash +python3 .verified-oss-loop/kit-inventory.py show --root . +python3 .verified-oss-loop/kit-inventory.py mark --root . --path skills/YOUR/SKILL.md --source local +python3 .verified-oss-loop/rollout.py show +``` + +Rollout scheme (`rollout.yml`): Arch-style `rolling` by default. `staged` or `stable` for slower repos. See the kit's [docs/rollout.md](https://github.com/kvnloo/verified-oss-loop/blob/main/docs/rollout.md). diff --git a/.verified-oss-loop/inventory.yml b/.verified-oss-loop/inventory.yml new file mode 100644 index 0000000..2744c6a --- /dev/null +++ b/.verified-oss-loop/inventory.yml @@ -0,0 +1,166 @@ +# Provenance for files copied from kvnloo/verified-oss-loop. +# source: kit = synced from the standard; local = developer-owned, never overwrite; +# modified = started as kit, local edits (re-run skips unless --force). +# Re-run oss-onboard to receive new kit skills. Mark a path local with: +# python3 scripts/kit-inventory.py mark --root . --path skills/NAME/SKILL.md --source local +schema: verified-oss-loop.inventory.v1 +kit: https://github.com/kvnloo/verified-oss-loop +kit_revision: 7ba2782090bef0a5fe8a8e039a700d76f23ca274 +synced_at: 2026-09-12T20:11:48Z +files: + - path: .github/CODEOWNERS + source: kit + sha256: 0270c50f0ecd8e2dd39f15715863a7909b9a2fe8e6a5e6492740844d9845f976 + kit_sha256: 0270c50f0ecd8e2dd39f15715863a7909b9a2fe8e6a5e6492740844d9845f976 + - path: .github/ISSUE_TEMPLATE/bug.yml + source: kit + sha256: c3be04fa057ed643073833d4b319dd45566b19fea5c47ed6c73038cd5de678b1 + kit_sha256: c3be04fa057ed643073833d4b319dd45566b19fea5c47ed6c73038cd5de678b1 + - path: .github/ISSUE_TEMPLATE/claim.yml + source: kit + sha256: 7f23ee391f54a05dda7649083a41bb10ac036ee0c2e28f3fb6a8ff788ad59899 + kit_sha256: 7f23ee391f54a05dda7649083a41bb10ac036ee0c2e28f3fb6a8ff788ad59899 + - path: .github/ISSUE_TEMPLATE/config.yml + source: kit + sha256: a22cd8312373f8c349d18e99e5f7895416d316d4f99a2df210366b1b1dbae92e + kit_sha256: a22cd8312373f8c349d18e99e5f7895416d316d4f99a2df210366b1b1dbae92e + - path: .github/ISSUE_TEMPLATE/feature.yml + source: kit + sha256: b54103aa4e35284f910619e5cb6ac22b100b6e2934091ef24a7f229b2534c3cd + kit_sha256: b54103aa4e35284f910619e5cb6ac22b100b6e2934091ef24a7f229b2534c3cd + - path: .github/PULL_REQUEST_TEMPLATE.md + source: modified + sha256: 81cd7758d58489d99a00085c1ea3a73c61620cf00a98b8570b67163dc8deecc8 + kit_sha256: 631bdec70a9b315bdd3d9f919f198716d6ea0ea1cdd021cc8cc15cadfa71e032 + - path: .github/dependabot.yml + source: kit + sha256: c8c84c10f737cdb53d3bb3e155fb9eab1efb4180b621bf0a137765df42e78020 + kit_sha256: c8c84c10f737cdb53d3bb3e155fb9eab1efb4180b621bf0a137765df42e78020 + - path: .github/labeler.yml + source: kit + sha256: 9574879997ebba5aa369389f2bacc007ea22f870e5ff951a4d368514b5758d28 + kit_sha256: 9574879997ebba5aa369389f2bacc007ea22f870e5ff951a4d368514b5758d28 + - path: .github/labels.md + source: kit + sha256: 9e269b8faffe1b137ee6b8819eebec0ed7c2a5740d810c9b76acdc106dcd083f + kit_sha256: 9e269b8faffe1b137ee6b8819eebec0ed7c2a5740d810c9b76acdc106dcd083f + - path: .github/scripts/check-receipt.py + source: kit + sha256: 188313013ff82f355612534a99f51efce0f2c2c5a1566cb5a4b1863f4e4b0063 + kit_sha256: 188313013ff82f355612534a99f51efce0f2c2c5a1566cb5a4b1863f4e4b0063 + - path: .github/scripts/create-labels.sh + source: kit + sha256: 5fcb9a409f80e5d0b68e41c777621ccdaeb1187bfb367b1e1dc614b20701f637 + kit_sha256: 5fcb9a409f80e5d0b68e41c777621ccdaeb1187bfb367b1e1dc614b20701f637 + - path: .github/scripts/expire-claims.sh + source: kit + sha256: f7b19ea5ae7226368a3515db9dd292ab20844fb09618bebb7c92f8abc3d6132d + kit_sha256: f7b19ea5ae7226368a3515db9dd292ab20844fb09618bebb7c92f8abc3d6132d + - path: .github/workflows/automerge-nightly.yml + source: kit + sha256: b1db0e5112b5f16cad13b3683ddf4ea699e398fc1c81221a8c34056e0d6c78b8 + kit_sha256: b1db0e5112b5f16cad13b3683ddf4ea699e398fc1c81221a8c34056e0d6c78b8 + - path: .github/workflows/automerge-preview.yml + source: kit + sha256: 573c5b5abd509eb84384f3ed21f77f76649b3e1c3bf105205c2d799af74242b3 + kit_sha256: 573c5b5abd509eb84384f3ed21f77f76649b3e1c3bf105205c2d799af74242b3 + - path: .github/workflows/claim-expiry.yml + source: kit + sha256: 23f4a98d0dff21e44251ae5804a00afd17d2c6214616f96083e6148347eff6b0 + kit_sha256: 23f4a98d0dff21e44251ae5804a00afd17d2c6214616f96083e6148347eff6b0 + - path: .github/workflows/labeler.yml + source: kit + sha256: 555c50172b8bc51d15fca6d896b4ed2d3420e01ae63af39013f95d7fcb99598b + kit_sha256: 555c50172b8bc51d15fca6d896b4ed2d3420e01ae63af39013f95d7fcb99598b + - path: .github/workflows/promote-preview.yml + source: kit + sha256: 0d27a95441c4f8483b4ffe6773d6323d40083615da77886536ab25b86d39da6d + kit_sha256: 0d27a95441c4f8483b4ffe6773d6323d40083615da77886536ab25b86d39da6d + - path: .github/workflows/receipt.yml + source: kit + sha256: 3b06f0f0928b6149beb187de8588280c637d2ed9167c2d4ad2c3457feb0af91f + kit_sha256: 3b06f0f0928b6149beb187de8588280c637d2ed9167c2d4ad2c3457feb0af91f + - path: .github/workflows/scorecard.yml + source: kit + sha256: e10cb2709765431fd549796883d441eac5425f4be9cfe54dd97ce635f02d0b43 + kit_sha256: e10cb2709765431fd549796883d441eac5425f4be9cfe54dd97ce635f02d0b43 + - path: .github/workflows/stale.yml + source: kit + sha256: 1c8b97c73c6dc87a541201e185dfd352fa0f8f738d29ba5981017124a451e798 + kit_sha256: 1c8b97c73c6dc87a541201e185dfd352fa0f8f738d29ba5981017124a451e798 + - path: .verified-oss-loop/README.md + source: kit + sha256: 35129e006379d553c6688159251ed77582e339c78b4dfe9e1fcaf020c0c8deef + kit_sha256: 35129e006379d553c6688159251ed77582e339c78b4dfe9e1fcaf020c0c8deef + - path: .verified-oss-loop/kit-inventory.py + source: kit + sha256: b393a6f25c5f86ddf9bdcb2ec0cd3ba4d8edc544dbe0b4029a5c2fd48d235cf6 + kit_sha256: b393a6f25c5f86ddf9bdcb2ec0cd3ba4d8edc544dbe0b4029a5c2fd48d235cf6 + - path: .verified-oss-loop/rollout.py + source: kit + sha256: 0f49ebbfb241fee00c21f2dbf587cb4c1807354a30a9c879ec0611b04ed83654 + kit_sha256: 0f49ebbfb241fee00c21f2dbf587cb4c1807354a30a9c879ec0611b04ed83654 + - path: .verified-oss-loop/rollout.yml + source: kit + sha256: 530e8b60c217e6e3db42b0e6799c30f7319a6c6b0c82874eb8f9cae6d18c1896 + kit_sha256: 530e8b60c217e6e3db42b0e6799c30f7319a6c6b0c82874eb8f9cae6d18c1896 + - path: AGENTS.md + source: local + sha256: 26a787d368b311f403e960f5762e318f46f252e01ca7e4276a40328623e2b4b7 + kit_sha256: 796c6248693433fb4d6ef6e4b8be675977bee0590097de8f73076b7c32b37263 + - path: CONTRIBUTING.md + source: kit + sha256: df73ebef9e33ca20433a1fc8ca8c40b3068c370df11cbc48a8c4caaa2eec03c7 + kit_sha256: df73ebef9e33ca20433a1fc8ca8c40b3068c370df11cbc48a8c4caaa2eec03c7 + - path: SECURITY.md + source: modified + sha256: f425442e0bb7efdb07b741eaa8e0997e9a8b03ce48f345a3f9e34c61745470c9 + kit_sha256: 3e7cb8117c023b4772f502f902c19a38921cc0901f4addf1b7a4c5c031fea04e + - path: prompt.md + source: kit + sha256: e68a9f288f9e702ab75e9b58fd8d6defda8f35299c28bc9b839ea8712f0f8bc6 + kit_sha256: e68a9f288f9e702ab75e9b58fd8d6defda8f35299c28bc9b839ea8712f0f8bc6 + - path: roadmap.example.yml + source: kit + sha256: 221df8f58a65aac116426397ae3957288279923ae18e622861b1dfbe1ccb9c53 + kit_sha256: 221df8f58a65aac116426397ae3957288279923ae18e622861b1dfbe1ccb9c53 + - path: scripts/cluster-similar-issues.py + source: kit + sha256: 35c21c0b5356bc319237101a3c4dc0ad6e3255024abf448e943cd780cb254ef2 + kit_sha256: 35c21c0b5356bc319237101a3c4dc0ad6e3255024abf448e943cd780cb254ef2 + - path: scripts/ensure-rollout-branches.sh + source: kit + sha256: ab0780c9f13d232827d3687727db823a2bec26a82a6aab420b8d0742fc4f2345 + kit_sha256: ab0780c9f13d232827d3687727db823a2bec26a82a6aab420b8d0742fc4f2345 + - path: scripts/rollout.py + source: kit + sha256: 0f49ebbfb241fee00c21f2dbf587cb4c1807354a30a9c879ec0611b04ed83654 + kit_sha256: 0f49ebbfb241fee00c21f2dbf587cb4c1807354a30a9c879ec0611b04ed83654 + - path: skills/anti-slop/SKILL.md + source: kit + sha256: 7151d5eea1dfd42f5cee0a37f0c03c5e151ab24a5d3f0fdaec94a65286facb76 + kit_sha256: 7151d5eea1dfd42f5cee0a37f0c03c5e151ab24a5d3f0fdaec94a65286facb76 + - path: skills/autodevelop/SKILL.md + source: kit + sha256: 1f421dd35fdaeaaa2c05472d1b8eec9d94592fb67092c4bbcf4eac0f25e88dce + kit_sha256: 1f421dd35fdaeaaa2c05472d1b8eec9d94592fb67092c4bbcf4eac0f25e88dce + - path: skills/dr-eggbot/SKILL.md + source: kit + sha256: 2d0aa3c4df219e25cbd89d6bacf8d0d2a01d5aeeaaeed963e6f3c9414b6855f5 + kit_sha256: 2d0aa3c4df219e25cbd89d6bacf8d0d2a01d5aeeaaeed963e6f3c9414b6855f5 + - path: skills/orient/SKILL.md + source: kit + sha256: 2f41f2ad4b411aa9e7fd9db704018838cd1e15eaca92eb45d5923886d1da6f2f + kit_sha256: 2f41f2ad4b411aa9e7fd9db704018838cd1e15eaca92eb45d5923886d1da6f2f + - path: skills/pstack/SKILL.md + source: kit + sha256: 8ba54e9382e1003d50a9851c79a1416f78da45babc84297a94841e2ff3c518c1 + kit_sha256: 8ba54e9382e1003d50a9851c79a1416f78da45babc84297a94841e2ff3c518c1 + - path: skills/tdd/SKILL.md + source: kit + sha256: 9f4ae8e66295ead9fd667b456eb886e1fa00c37cc2e792f189257b8b84661656 + kit_sha256: 9f4ae8e66295ead9fd667b456eb886e1fa00c37cc2e792f189257b8b84661656 + - path: skills/verify/SKILL.md + source: kit + sha256: 2fa5110bdc6b4e5254786e4e9058b7d9524618d02481da4d1c38481c1ffac7e0 + kit_sha256: 2fa5110bdc6b4e5254786e4e9058b7d9524618d02481da4d1c38481c1ffac7e0 diff --git a/.verified-oss-loop/kit-inventory.py b/.verified-oss-loop/kit-inventory.py new file mode 100644 index 0000000..88d8140 --- /dev/null +++ b/.verified-oss-loop/kit-inventory.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +"""Provenance inventory for files copied from verified-oss-loop. + +Kit-owned paths update on re-run when unmodified. source: local is never +overwritten, including under --force. Stdlib only. Does not merge. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import stat +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +SCHEMA = "verified-oss-loop.inventory.v1" +INV_DIR = ".verified-oss-loop" +INV_NAME = "inventory.yml" +DEFAULT_KIT = "https://github.com/kvnloo/verified-oss-loop" +SKILL_ROOTS = ("skills", ".agents/skills", ".claude/skills") +SOURCES = ("kit", "local", "modified") + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def now_rfc3339() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def inventory_path(root: Path) -> Path: + return root / INV_DIR / INV_NAME + + +def posix(path: str) -> str: + p = path.replace("\\", "/").strip() + while p.startswith("./"): + p = p[2:] + return p + + +def load_inventory(root: Path) -> dict[str, Any]: + path = inventory_path(root) + data: dict[str, Any] = { + "schema": SCHEMA, + "kit": DEFAULT_KIT, + "kit_revision": "", + "synced_at": "", + "files": {}, + } + if not path.is_file(): + return data + current: dict[str, str] | None = None + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.split("#", 1)[0].rstrip() + if not line.strip(): + continue + dashed = line.strip() + if dashed.startswith("- path:"): + rel = posix(dashed.split(":", 1)[1].strip()) + current = {"path": rel, "source": "kit", "sha256": "", "kit_sha256": ""} + data["files"][rel] = current + continue + if ":" not in line: + continue + key, val = line.split(":", 1) + key, val = key.strip(), val.strip() + if current is not None and key in ("source", "sha256", "kit_sha256"): + current[key] = val + elif current is None and key in ("schema", "kit", "kit_revision", "synced_at"): + data[key] = val + return data + + +def emit_inventory(data: dict[str, Any]) -> str: + lines = [ + "# Provenance for files copied from kvnloo/verified-oss-loop.", + "# source: kit = synced from the standard; local = developer-owned, never overwrite;", + "# modified = started as kit, local edits (re-run skips unless --force).", + "# Re-run oss-onboard to receive new kit skills. Mark a path local with:", + "# python3 scripts/kit-inventory.py mark --root . --path skills/NAME/SKILL.md --source local", + f"schema: {data.get('schema', SCHEMA)}", + f"kit: {data.get('kit') or DEFAULT_KIT}", + f"kit_revision: {data.get('kit_revision') or ''}", + f"synced_at: {data.get('synced_at') or ''}", + "files:", + ] + files = data.get("files") or {} + for rel in sorted(files): + entry = files[rel] + lines.append(f" - path: {rel}") + lines.append(f" source: {entry.get('source') or 'kit'}") + if entry.get("sha256"): + lines.append(f" sha256: {entry['sha256']}") + if entry.get("kit_sha256"): + lines.append(f" kit_sha256: {entry['kit_sha256']}") + lines.append("") + return "\n".join(lines) + + +def save_inventory(root: Path, data: dict[str, Any]) -> Path: + dest = inventory_path(root) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(emit_inventory(data), encoding="utf-8") + return dest + + +def append_session(session: Path, rec: dict[str, str]) -> None: + session.parent.mkdir(parents=True, exist_ok=True) + with session.open("a", encoding="utf-8") as f: + f.write(json.dumps(rec, sort_keys=True) + "\n") + + +def decide(root: Path, rel: str, incoming: Path, dest: Path, force: bool) -> tuple[bool, str, str]: + """Return (write?, action, reason). Never writes source=local.""" + rel = posix(rel) + inv = load_inventory(root) + entry = (inv.get("files") or {}).get(rel) + source = (entry or {}).get("source") or "" + + if source == "local": + return False, "skip", "local" + + if not dest.exists(): + return True, "write", "new" + + incoming_hash = sha256_file(incoming) + dest_hash = sha256_file(dest) + if dest_hash == incoming_hash: + return False, "unchanged", "already current" + + if force: + return True, "force", "force" + + if not entry: + # First inventory: existing kit-path content may be an older template + # or a local edit. Do not clobber; next run can --force or mark kit. + return False, "skip", "modified" + + recorded = entry.get("sha256") or "" + if source == "kit" and recorded and dest_hash == recorded: + return True, "update", "kit file unmodified since last sync" + + if source == "modified" and dest_hash == incoming_hash: + return True, "update", "matches new kit; reclaim" + + return False, "skip", "modified" + + +def cmd_apply(args: argparse.Namespace) -> int: + root = Path(args.root).resolve() + rel = posix(args.path) + incoming = Path(args.incoming) + dest = root / rel + if not incoming.is_file(): + print(f"incoming missing: {incoming}", file=sys.stderr) + return 2 + force = bool(args.force) + write, action, reason = decide(root, rel, incoming, dest, force) + dest.parent.mkdir(parents=True, exist_ok=True) + incoming_hash = sha256_file(incoming) + if write: + shutil.copyfile(incoming, dest) + if args.chmod: + mode = int(args.chmod, 8) + os.chmod(dest, stat.S_IMODE(dest.stat().st_mode) | mode) + print(f"{action}: {rel}") + else: + print(f"skip {rel} ({reason})") + if args.session: + append_session( + Path(args.session), + { + "path": rel, + "action": action, + "reason": reason, + "incoming_sha256": incoming_hash, + }, + ) + return 0 + + +def iter_extra_skills(root: Path, kit_paths: set[str]) -> list[Path]: + found: list[Path] = [] + for rel_root in SKILL_ROOTS: + base = root / rel_root + if not base.is_dir(): + continue + for p in base.rglob("SKILL.md"): + rel = p.relative_to(root).as_posix() + if rel not in kit_paths: + found.append(p) + return found + + +def cmd_finalize(args: argparse.Namespace) -> int: + root = Path(args.root).resolve() + old = load_inventory(root) + files: dict[str, dict[str, str]] = dict(old.get("files") or {}) + session_rows: list[dict[str, str]] = [] + if args.session and Path(args.session).is_file(): + for line in Path(args.session).read_text(encoding="utf-8").splitlines(): + if line.strip(): + session_rows.append(json.loads(line)) + + kit_paths: set[str] = set() + if args.kit_paths: + for line in Path(args.kit_paths).read_text(encoding="utf-8").splitlines(): + line = posix(line.strip()) + if line: + kit_paths.add(line) + for row in session_rows: + kit_paths.add(posix(row["path"])) + + by_path = {posix(r["path"]): r for r in session_rows} + + for rel in sorted(kit_paths): + dest = root / rel + prev = files.get(rel) or {} + row = by_path.get(rel) + action = (row or {}).get("action") or "" + reason = (row or {}).get("reason") or "" + incoming_hash = (row or {}).get("incoming_sha256") or "" + + if prev.get("source") == "local" or reason == "local" or action == "skip" and reason == "local": + entry = {"path": rel, "source": "local", "sha256": "", "kit_sha256": prev.get("kit_sha256") or ""} + if dest.is_file(): + entry["sha256"] = sha256_file(dest) + files[rel] = entry + continue + + if not dest.is_file(): + files.pop(rel, None) + continue + + dest_hash = sha256_file(dest) + if action in ("write", "update", "force", "unchanged"): + files[rel] = { + "path": rel, + "source": "kit", + "sha256": dest_hash, + "kit_sha256": incoming_hash or dest_hash, + } + elif reason == "modified" or action == "skip": + files[rel] = { + "path": rel, + "source": "modified", + "sha256": dest_hash, + "kit_sha256": incoming_hash or prev.get("kit_sha256") or "", + } + else: + files[rel] = { + "path": rel, + "source": prev.get("source") or "kit", + "sha256": dest_hash, + "kit_sha256": incoming_hash or prev.get("kit_sha256") or dest_hash, + } + + for extra in iter_extra_skills(root, kit_paths): + rel = extra.relative_to(root).as_posix() + prev = files.get(rel) or {} + files[rel] = { + "path": rel, + "source": "local", + "sha256": sha256_file(extra), + "kit_sha256": prev.get("kit_sha256") or "", + } + + # Keep previously recorded local files that are not skills (rare). + for rel, entry in list(files.items()): + if rel in kit_paths: + continue + if entry.get("source") != "local": + continue + dest = root / rel + if dest.is_file(): + entry["sha256"] = sha256_file(dest) + files[rel] = entry + else: + files.pop(rel, None) + + data = { + "schema": SCHEMA, + "kit": args.kit_url or old.get("kit") or DEFAULT_KIT, + "kit_revision": args.kit_revision or old.get("kit_revision") or "", + "synced_at": now_rfc3339(), + "files": files, + } + dest = save_inventory(root, data) + print(f"wrote {dest.relative_to(root).as_posix()}") + return 0 + + +def cmd_show(args: argparse.Namespace) -> int: + root = Path(args.root).resolve() + inv = load_inventory(root) + if not inventory_path(root).is_file(): + print("no inventory; run oss-onboard to create .verified-oss-loop/inventory.yml") + return 1 + grouped: dict[str, list[str]] = {"kit": [], "modified": [], "local": []} + for rel, entry in (inv.get("files") or {}).items(): + src = entry.get("source") or "kit" + grouped.setdefault(src, []).append(rel) + if args.json: + print(json.dumps({"kit": inv.get("kit"), "kit_revision": inv.get("kit_revision"), "synced_at": inv.get("synced_at"), "files": inv.get("files")}, indent=2)) + return 0 + print(f"kit: {inv.get('kit')} @ {inv.get('kit_revision') or '?'}") + print(f"synced_at: {inv.get('synced_at')}") + for src in ("kit", "modified", "local"): + paths = grouped.get(src) or [] + print(f"\n{src}:") + if not paths: + print(" (none)") + continue + for p in sorted(paths): + print(f" {p}") + return 0 + + +def cmd_source(args: argparse.Namespace) -> int: + root = Path(args.root).resolve() + rel = posix(args.path) + entry = (load_inventory(root).get("files") or {}).get(rel) + if not entry: + print("missing") + return 0 + print(entry.get("source") or "missing") + return 0 + + +def cmd_mark(args: argparse.Namespace) -> int: + root = Path(args.root).resolve() + rel = posix(args.path) + source = args.source + if source not in SOURCES: + print(f"source must be one of {SOURCES}", file=sys.stderr) + return 2 + dest = root / rel + if not dest.is_file(): + print(f"missing file: {rel}", file=sys.stderr) + return 1 + inv = load_inventory(root) + files = inv.setdefault("files", {}) + prev = files.get(rel) or {} + files[rel] = { + "path": rel, + "source": source, + "sha256": sha256_file(dest), + "kit_sha256": prev.get("kit_sha256") or "", + } + inv["synced_at"] = inv.get("synced_at") or now_rfc3339() + save_inventory(root, inv) + print(f"marked {rel} source={source}") + return 0 + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Verified OSS Loop kit inventory (provenance + sync)") + sub = p.add_subparsers(dest="cmd", required=True) + + ap = sub.add_parser("apply", help="copy incoming to dest if inventory allows") + ap.add_argument("--root", required=True) + ap.add_argument("--path", required=True, help="path relative to root") + ap.add_argument("--incoming", required=True) + ap.add_argument("--session", help="JSONL session file") + ap.add_argument("--force", action="store_true") + ap.add_argument("--chmod", help="octal bits to OR onto mode, e.g. 755") + ap.set_defaults(func=cmd_apply) + + fin = sub.add_parser("finalize", help="write inventory.yml after a sync") + fin.add_argument("--root", required=True) + fin.add_argument("--session") + fin.add_argument("--kit-paths", help="newline list of kit-relative paths attempted this run") + fin.add_argument("--kit-revision", default="") + fin.add_argument("--kit-url", default=DEFAULT_KIT) + fin.set_defaults(func=cmd_finalize) + + sh = sub.add_parser("show", help="print inventory grouped by source") + sh.add_argument("--root", required=True) + sh.add_argument("--json", action="store_true") + sh.set_defaults(func=cmd_show) + + src = sub.add_parser("source", help="print source for one path") + src.add_argument("--root", required=True) + src.add_argument("--path", required=True) + src.set_defaults(func=cmd_source) + + mk = sub.add_parser("mark", help="set source=local|kit|modified for a path") + mk.add_argument("--root", required=True) + mk.add_argument("--path", required=True) + mk.add_argument("--source", required=True, choices=SOURCES) + mk.set_defaults(func=cmd_mark) + + args = p.parse_args(argv) + return int(args.func(args)) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.verified-oss-loop/rollout.py b/.verified-oss-loop/rollout.py new file mode 100644 index 0000000..b8a76b8 --- /dev/null +++ b/.verified-oss-loop/rollout.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Read .verified-oss-loop/rollout.yml. Stdlib only. Does not merge.""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +SCHEMES = ("rolling", "staged", "stable") + + +def load_scheme(root: Path) -> str: + path = root / ".verified-oss-loop" / "rollout.yml" + scheme = "rolling" + if path.is_file(): + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.split("#", 1)[0].strip() + if line.startswith("scheme:"): + scheme = line.split(":", 1)[1].strip().strip("'\"") + break + if scheme not in SCHEMES: + raise SystemExit(f"unknown rollout scheme: {scheme}") + return scheme + + +def policy(scheme: str) -> dict[str, str | bool]: + if scheme == "stable": + return { + "scheme": scheme, + "worker_base": "main", + "feature_target": "main", + "overnight_target": "main", + "automerge_preview": False, + "automerge_nightly": False, + "promote_preview_to_nightly": False, + } + if scheme == "staged": + return { + "scheme": scheme, + "worker_base": "nightly", + "feature_target": "preview", + "overnight_target": "nightly", + "automerge_preview": True, + "automerge_nightly": False, + "promote_preview_to_nightly": True, + } + return { + "scheme": scheme, + "worker_base": "nightly", + "feature_target": "preview", + "overnight_target": "nightly", + "automerge_preview": True, + "automerge_nightly": True, + "promote_preview_to_nightly": True, + } + + +def cmd_show(root: Path) -> int: + p = policy(load_scheme(root)) + for k in ( + "scheme", + "worker_base", + "feature_target", + "overnight_target", + "automerge_preview", + "automerge_nightly", + "promote_preview_to_nightly", + ): + print(f"{k}={p[k]}".lower() if isinstance(p[k], bool) else f"{k}={p[k]}") + return 0 + + +def cmd_get(root: Path, key: str) -> int: + p = policy(load_scheme(root)) + if key not in p: + print(f"unknown key: {key}", file=sys.stderr) + return 2 + val = p[key] + print(str(val).lower() if isinstance(val, bool) else val) + return 0 + + +def cmd_allow(root: Path, channel: str) -> int: + if channel not in ("preview", "nightly"): + return 1 + p = policy(load_scheme(root)) + ok = bool(p[f"automerge_{channel}"]) + return 0 if ok else 1 + + +def cmd_allow_promote(root: Path, what: str) -> int: + if what != "preview-to-nightly": + return 1 + p = policy(load_scheme(root)) + return 0 if p["promote_preview_to_nightly"] else 1 + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Verified OSS Loop rollout scheme") + p.add_argument("--root", default=".") + sub = p.add_subparsers(dest="cmd", required=True) + sub.add_parser("show") + g = sub.add_parser("get") + g.add_argument("key") + a = sub.add_parser("allow-automerge") + a.add_argument("channel", choices=("preview", "nightly")) + pr = sub.add_parser("allow-promote") + pr.add_argument("what", choices=("preview-to-nightly",)) + args = p.parse_args(argv) + root = Path(args.root).resolve() + if args.cmd == "show": + return cmd_show(root) + if args.cmd == "get": + return cmd_get(root, args.key) + if args.cmd == "allow-promote": + return cmd_allow_promote(root, args.what) + return cmd_allow(root, args.channel) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.verified-oss-loop/rollout.yml b/.verified-oss-loop/rollout.yml new file mode 100644 index 0000000..ca5bc35 --- /dev/null +++ b/.verified-oss-loop/rollout.yml @@ -0,0 +1,5 @@ +# Rollout scheme — Arch-style rolling by default. +# rolling | staged | stable +# See docs/rollout.md in kvnloo/verified-oss-loop. Not a second contribution loop. +schema: verified-oss-loop.rollout.v1 +scheme: rolling diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8c02d02 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,93 @@ +# Notes for agents — ace + +You are a contributor, not a maintainer. Workers open PRs. They never merge `main` or `dev`. + +This project follows the [Verified OSS Loop](https://github.com/kvnloo/verified-oss-loop). Issues are not claims. AI work is untrusted until proven. + +## First 60 seconds + +1. Read this file, then `CONTRIBUTING.md`. +2. `git fetch origin`. `python3 .verified-oss-loop/rollout.py show`. Branch from `origin/$(python3 .verified-oss-loop/rollout.py get worker_base)` unless the issue names another base. Day-pass PRs target `feature_target`. Overnight unattended PRs target `overnight_target`. See `docs/rollout.md` in the kit (or `.verified-oss-loop/rollout.yml` here). +3. Search open issues and PRs. Do not duplicate in-flight work. +4. Orient (`skills/orient/SKILL.md`). If GitNexus MCP is already there: `query` → `context` → `impact`. Do not run `gitnexus analyze` unless a human asked. Else Serena symbols, else `rg` + read. + +```bash +gh issue list --label claimable --state open +gh pr list --state open +``` + +## Pick and claim + +Take **one** open issue labeled `claimable` and not `claimed`. Prefer `priority:P0`, then `P1`, then `good-first-issue`. Skip `needs-discussion` unless a human assigned it. + +If nothing is `claimable`: do not code. **Triage** — if a `needs-discussion` issue exists: one-paragraph proposal on the newest; stop. If none: mint **exactly one** issue from the first untracked item in `ROADMAP.md`, else a failing unit command from `AGENTS.md`, else docs drift; label **`needs-discussion` only**; stop. Do not self-apply `claimable`. Do not rewrite `ROADMAP.md`. **Stop** if triage found nothing untracked, a live claim exists, a competing PR covers the scope, or secrets are required. Comment the blocker only if an issue thread exists. Do not open a consolation PR. + +Claim comment (24h lease unless the project says otherwise): + +```text +claiming for autodevelop +claimant: +base: +expires: +scope: +``` + +Then add `claimed` and remove `claimable`. If a claim newer than 24h exists, pick a different issue. + +## Proof + +Commands were filled by `init-oss-repo.sh` / `oss-onboard` from the tree it saw, then ACE-ized. Do not invent a mutation score. Stryker is not adopted. + +| Layer | Command | +|---|---| +| Unit | `npm test` | +| Mutation | `n/a` — Stryker not adopted; do not invent a score | +| Runtime | `npm run build` | + +1. Name the intended vs current behavior. +2. Fail, then pass (see `skills/tdd/SKILL.md`). +3. Keep the smallest complete change (`skills/anti-slop/SKILL.md`). +4. Run unit tests on the touched surface. +5. If mutation is not `n/a`, run it on the contract you changed. A surviving mutant is a missing assertion. +6. Open a PR at `feature_target` (or `overnight_target` if unattended overnight). Fill `.github/PULL_REQUEST_TEMPLATE.md`. Never merge `main` or `dev`. Do not merge preview/nightly yourself; automerge may, when `rollout.yml` allows. +7. If the project runs an independent review bot (Greptile, CodeRabbit, Bugbot, Copilot, …), treat its comments as review, not merge. Fix real findings. Do not wait for a bot to approve itself. + +**This branch:** honesty PR #7 still targets `main` because it predates rolling `preview`/`nightly`. After the maintainer runs `scripts/ensure-rollout-branches.sh --push`, new worker PRs target `preview`. + +## ACE ownership + +ACE is a **peak-performance pretotype** — a campus for human flourishing from all angles (scientists, athletes, labs, physiotherapy, all sports). That vision comes from `origin/enhance/3D` (APEX). Mine the **program**. Do not resurrect the Three.js lab theater, debug FPS HUD, OpenTwins-as-wired, 88% completion badges, or `4 × 500 = 2,000 m²` as origin fact. + +The Naperville racquet building is the origin **SPEC** nested inside that vision: + +- 24 tennis / 16 badminton / 4 squash / 16 table tennis / 8 pickleball / 1 real tennis +- Grass lab: 500 m² per section; **section count unspecified** +- Envelope 140×120 m and 10 m storeys are **inferred** + +APEX rooms in Pascal (`building_apex`) are **VISION** named zones with inferred 30×30 m cells. Gym, pool, clubhouse, and physio are program identity, not origin measurements. + +### This repo runs + +- Pascal Viewer (`@pascal-app/core` / `viewer` / `nodes` 1.0.0) plus a CSS court/campus fallback +- `facility/generateScene.ts` compiles Site → LawnTech (SPEC) + APEX wing (VISION) +- `npm test` → `facility/check-scene.ts` (levels, one grass lab, nine VISION zones, no inferred farm sections) + +### Do not (ACE-specific) + +- Copy HomeForge / zerOS house, `NOW.yaml`, `:4210` bus, claim protocol, HYBRID, FBP, Keel, devices, or drones into this tree. Pascal embed + `plan2pascal` node mapping are the donor; ACE compiles four sport-hall floors by hand because plan2pascal v0 is single-level. +- Start ACE as a second live GPU loop. Homebase (`kvnloo/homebase-pickleball`) owns the live GPU path on host 0. +- Treat OpenTwins / Eclipse Ditto / Hono / Jenkins / Unity-in-Docker as wired dependencies. They are origin research language. +- Invent `/preview//` Pages slots. Current deploy is `main` → `/ace/` and `dev` → `/ace/dev/` only. Rolling git branches are not Pages. +- Commit Sunshine credentials, API tokens, or `.env`. +- Mutate Linear until a human YES on the relevant issue. + +## Do not + +- Commit secrets, tokens, `.env`, or pairing files. +- Merge `main` or `dev`. +- Redefine the roadmap. +- Claim mutation coverage that the stack cannot run. +- Overwrite `LICENSE`. +- Duplicate `AGENTS.md` into `CLAUDE.md` / `GEMINI.md` / copilot-instructions. +- Run `gitnexus analyze` as a side effect of a claim. +- Dump the pstack plugin or Dr Eggbot marketplace pack into this tree. Pointers: `skills/pstack/SKILL.md`, `skills/dr-eggbot/SKILL.md`. diff --git a/App.tsx b/App.tsx index 8ddcb3a..c9d06b7 100644 --- a/App.tsx +++ b/App.tsx @@ -14,7 +14,10 @@ import { Layers, Wind, ShieldCheck, - ShoppingBag + ShoppingBag, + Brain, + Heart, + Activity } from 'lucide-react'; const App: React.FC = () => { @@ -64,7 +67,7 @@ const App: React.FC = () => { className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-tennis-yellow/30 bg-tennis-yellow/10 text-tennis-yellow text-sm font-medium mb-6" > - Naperville spec · public 3D pretotype + VISION campus · Naperville racquet SPEC · pretotype { transition={{ delay: 0.3 }} className="text-5xl md:text-8xl font-extrabold tracking-tighter mb-6 leading-tight" > - GRASS.
- AUTONOMOUS.
- PERFECTION. + PEAK PERFORMANCE.
+ EVERY ANGLE.
{ transition={{ delay: 0.4 }} className="text-xl text-gray-300 mb-10 max-w-2xl mx-auto leading-relaxed" > - Indoor grass courts with a third-floor lab that grows the surface. - The 3D map is a sketch of the Naperville origin spec — not a live twin. + A campus for human flourishing — scientists, athletes, labs, physiotherapy, all sports. + Naperville racquet counts are the origin spec. APEX is the vision. This Pages site is a sketch, not a live twin. { {/* Statistics Teaser */}
+
+ VISION +

Peak-performance campus

+

Scientists, athletes, labs, physiotherapy, all sports. APEX rooms are named program — not origin measurements, not a live clinic.

+
SPEC

24 tennis courts

-

Hard, clay, grass, and wood on the ground floor. Counts from the origin spec — not an even 6/6/6/6 split unless the spec says so.

-
-
- PLANNED -

Grass lab

-

Third floor grows modular turf: 500 m² per section in the origin. Section count is unspecified. Fast swap is a goal, not a receipt.

+

Naperville origin: hard, clay, grass, and wood on the ground floor. Counts from the spec — not an even 6/6/6/6 split unless the spec says so.

- MOCK -

Ops overlay

-

Biomechanics HUD and 60 FPS numbers are theater on this demo. The court is the product; chrome stays thin.

+ PLANNED +

Grass lab + APEX wing

+

Third floor grows modular turf: 500 m² per section. Section count unspecified. APEX cells are inferred. Fast swap is a goal, not a receipt.

@@ -165,10 +167,10 @@ const App: React.FC = () => { {/* HUD Layer */}
-

Facility sketch

+

Campus sketch

- Naperville origin · Pascal nodes · 24 tennis · third-floor grass lab
- Envelope 140×120 m is inferred. HUD is MOCK. CSS court if WebGL is blank. + VISION APEX wing · Naperville racquet SPEC · Pascal nodes
+ Envelope and APEX cells inferred. HUD is MOCK. CSS sketch if WebGL is blank.

@@ -218,9 +220,41 @@ const App: React.FC = () => { className="h-full overflow-y-auto custom-scrollbar pb-20 px-6" >
-

Facility Amenities

+

Campus program

+

+ Peak performance from every angle. APEX labs, physio, gym, and pool are VISION. + Naperville racquet counts are SPEC. This site does not run a facility. +

+ +
+
+
+ +
+

APEX peak-performance wing

+

+ From origin/enhance/3D: biometric, cognitive, movement, research, and nutrition rooms plus recovery / physiotherapy. + Named program identity — not 147 biomarkers, not a measured VO₂ panel, not a live twin. +

+
    +
  • Biometric lab — VISION
  • +
  • Cognitive + movement + research — VISION
  • +
  • Gym, pool, clubhouse — VISION (all sports)
  • +
+
+
+
+ {Array.from({ length: 9 }).map((_, i) => ( +
+ ))} +
+
+ VISION · APEX CAMPUS +
+
+
{/* 1. Vertical Grass Lab */}
@@ -252,10 +286,10 @@ const App: React.FC = () => {
-

Multi-Sport Ecosystem

+

Naperville racquet (origin SPEC)

- Spanning the Ground, 1st, and 2nd floors, we offer a comprehensive racquet experience. - From the high-speed action of Badminton and Table Tennis on the Mezzanine to the social atmosphere of Pickleball and the historic elegance of Real Tennis on the upper deck. + First site nested in the campus vision. Ground, L1, and L2 are the origin racquet program — not all-sports counts. + All-sports identity lives on the APEX wing as VISION until a spec names those rooms.

@@ -357,7 +391,7 @@ const App: React.FC = () => {

Join the pretotype

-

Origin spec is Naperville. This form does not submit (MOCK). Other cities named here are a pitch, not a second facility spec.

+

Vision is a peak-performance campus. Origin spec is Naperville racquet. This form does not submit (MOCK).

e.preventDefault()}> diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8f04808 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contributing + +Humans and coding agents work on this repository together. This file is how to **use** the project and how to **contribute**. Project policy in `AGENTS.md` and `SECURITY.md` wins if anything conflicts. + +## Use + +See the README for install, run, and configuration. Do not commit secrets. + +## Contribute + +### Autodevelop (agents) + +If you were told to autodevelop, donate a coding pass, or pick the next issue: paste `prompt.md` (it links to this repo) and follow `AGENTS.md` and `skills/autodevelop/SKILL.md`. Do not invent a parallel process. + +Workers: + +1. Claim one `claimable` issue (24h lease). +2. `python3 .verified-oss-loop/rollout.py show`. Work on a branch from `origin/$(python3 .verified-oss-loop/rollout.py get worker_base)`. Day-pass PRs target `feature_target`. +3. Orient (`skills/orient/SKILL.md`), then fail, then pass. +4. Keep the smallest complete change (`skills/anti-slop/SKILL.md`). +5. Open a PR with an evidence receipt. +6. **Never merge `main` or `dev`.** + +### Humans + +Same loop, without the claim bot if you are a maintainer. You still do not need a second process. + +## Evidence + +Every PR fills `.github/PULL_REQUEST_TEMPLATE.md`: + +- issue number +- base SHA and head SHA +- red command (failing repro) and green command +- mutation command and score, or `n/a` +- contribution mode: unattended (cloud agent) or copilot (human-supervised) + +Tests from another head are not evidence. `--with-automation` adds a CI check that the YAML keys exist and `head_revision` matches the PR SHA. + +Independent review bots the project already installed (Greptile, CodeRabbit, Bugbot, Copilot, Codecov, CodSpeed) are reviewers and evidence, not merge authority. + +## Prior art + +Contribution contract: [kvnloo/verified-oss-loop](https://github.com/kvnloo/verified-oss-loop). Agent file convention: [agents.md](https://agents.md/). diff --git a/README.md b/README.md index 03aeb37..88540bd 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,44 @@ -# LawnTech Dynamics — indoor grass-court pretotype +# ACE — peak-performance campus pretotype
-**A public digital-twin pretotype of an indoor racquet facility with a third-floor grass lab** +**A public pretotype of a peak-performance campus for human flourishing — scientists, athletes, labs, physiotherapy, all sports.** -[View Live Demo](https://kvnloo.github.io/ace/) • [Dev Preview](https://kvnloo.github.io/ace/dev/) • [Documentation](.github/DEPLOYMENT.md) +[Live Pages (`main`)](https://kvnloo.github.io/ace/) • [Dev Preview](https://kvnloo.github.io/ace/dev/) • [Deployment](.github/DEPLOYMENT.md) -This Pages site is a **pretotype**, not a running BMS. LIVE / MOCK / PLANNED stamps in the UI say which claims are spec vs. theater. GitHub Pages deploys **`main`** and **`dev` only** — there is no nightly or PR preview. Honesty work lives on feature branches until merge. +This Pages site is a **pretotype**, not a running BMS or live twin. Stamps in the UI say which claims are **VISION**, **SPEC**, **PLANNED**, or **MOCK**. GitHub Pages deploys **`main`** and **`dev` only** — there is no nightly or PR preview. Honesty work lives on feature branches until merge.
--- -## Project Overview +## Vision vs spec -LawnTech Dynamics is a four-floor racquet-sports facility concept: indoor courts plus an on-site grass lab so the playing surface can be grown and swapped instead of painted green. The origin spec is **Naperville, Illinois**. The Invest page may mention other cities as a **pitch** — that is MOCK, not the spec. +The vision (from `origin/enhance/3D`, APEX) is a **peak-performance facility for all things pursuing human flourishing**: scientists, athletes, labs, physiotherapy, gym, pool, clubhouse, all sports — not tennis-only. -### Core idea -- **Grass as a crop**: third-floor lab grows modular turf for the courts below -- **Indoor facility that prepares itself**: climate, surface, and access as one loop (pretotype) -- **Court is the hero**: the 3D demo is a spatial sketch, not a photoreal twin. If WebGL cannot paint, the map falls back to a CSS court still. -- **Measure, don't guess**: turf quality and energy are the honest metrics — not "AI-powered" as a product +The Naperville racquet building is the origin **SPEC** nested inside that vision. This repo compiles both into Pascal: LawnTech (SPEC) plus an APEX wing (VISION named rooms, inferred cells). + +| Stamp | What | +|---|---| +| **VISION** | APEX labs (biometric, cognitive, movement, research, nutrition), recovery / physio, gym, pool, clubhouse. Not origin measurements. | +| **SPEC** | 24 tennis / 16 badminton / 4 squash / 16 table tennis / 8 pickleball / 1 real tennis. Grass lab: 500 m² per section, **section count unspecified**. | +| **PLANNED / MOCK** | Live BMS, drones, 60 FPS, Gemini as a product, 6/6/6/6 split, four farm racks, 60-minute turf-swap receipt. | +| **This repo runs** | Pascal Viewer + CSS fallback. Not a live facility. Homebase owns the live GPU loop. | + +Do not copy `enhance/3D` Three.js lab theater into this tree as the renderer. Mine the program; compile through Pascal. --- -## 🏗️ Facility Layout +## 🏗️ Naperville origin layout (SPEC) ### **Ground Floor** - Tennis Complex -24 premium tennis courts featuring: +24 tennis courts featuring: - Grass courts (replaceable modular turf) - Hard courts - Clay courts - Wood courts -- Pro shop and premium locker rooms +- Pro shop and locker rooms +- Surface split is **unspecified** (not 6/6/6/6 unless the spec says so) ### **First Floor** - Mezzanine Sports - 16 badminton courts @@ -55,24 +61,36 @@ Origin spec: **500 m² per section**. Section count is not in the origin (an ear ### Pages demo (this tree) - **React 19.2** + TypeScript -- **Pascal Viewer** (`@pascal-app/core` / `viewer` / `nodes` 1.0.0) for the facility sketch — Site → Building → Levels 0–3 +- **Pascal Viewer** (`@pascal-app/core` / `viewer` / `nodes` 1.0.0) — Site → LawnTech levels 0–3 + APEX VISION wing - Envelope **140×120 m** and **10 m** storeys are **inferred**, not origin measurements +- APEX rooms are **nine VISION zones** on inferred 30×30 m cells - Grass lab is **one PLANNED zone** (origin: 500 m²/section; section count unspecified) -- CSS court fallback when WebGL cannot paint +- CSS campus / court fallback when WebGL cannot paint - **Framer Motion**, **Vite** -- **Gemini** chat is a **stub** on GitHub Pages (no live coaching product) +- **Gemini** chat is a **stub** on GitHub Pages (not a concierge product) - Pascal **editor** is not shipped; `@pascal-app/editor` is a read-only shim so node definitions can load +### Proof + +```bash +npm test # facility/check-scene.ts +npm run build +``` + +Mutation is **n/a** — Stryker is not adopted. Do not invent a score. + ### Not in this tree OpenTwins, Eclipse Ditto, Eclipse Hono, Jenkins, Unity-in-Docker, and a live BMS are **origin research language**, not wired dependencies. Do not treat README history as a running stack. +Agents: read [`AGENTS.md`](AGENTS.md) and [`ROADMAP.md`](ROADMAP.md). Workers never merge `main` or `dev`. + --- ## 💻 Development ### Prerequisites - Node.js (v20 or higher) -- npm or yarn +- npm - Git ### Local Setup @@ -88,7 +106,7 @@ OpenTwins, Eclipse Ditto, Eclipse Hono, Jenkins, Unity-in-Docker, and a live BMS npm install ``` -3. **Configure environment** +3. **Configure environment** (optional; chat is a stub without it) Create a `.env.local` file: ```env GEMINI_API_KEY=your_api_key_here @@ -99,11 +117,10 @@ OpenTwins, Eclipse Ditto, Eclipse Hono, Jenkins, Unity-in-Docker, and a live BMS npm run dev ``` - Open [http://localhost:3000](http://localhost:3000) in your browser - ### Build for Production ```bash +npm test npm run build npm run preview ``` @@ -117,6 +134,8 @@ This project uses GitHub Actions for automated deployment to GitHub Pages: - **Production**: Automatically deploys from `main` branch to `/` - **Development**: Automatically deploys from `dev` branch to `/dev/` +Rolling git branches (`preview`, `nightly`) from the Verified OSS Loop are **not** Pages slots. Do not invent `/preview//` without extending `.github/workflows/deploy.yml`. + See [Deployment Guide](.github/DEPLOYMENT.md) for detailed setup instructions. ### Deployment URLs @@ -125,128 +144,37 @@ See [Deployment Guide](.github/DEPLOYMENT.md) for detailed setup instructions. --- -## Features - -### Interactive 3D facility sketch -A Three.js sketch of the four floors. Court is the hero. HUD cards are MOCK overlays — they are not a live sensor bind. - -### Chat (stub) -The Gemini box answers from prompt text when a key is present. On Pages it is a stub. Not a coaching product. - -### Specs vs theater -Origin court counts (24 tennis + mezzanine + pickleball + real tennis) are in the spec. **60 FPS / 225 km/h / 1900 N** are README theater from an earlier draft — not measured on this demo. Vertical farm and robot swap are **PLANNED**. - ---- - ## Spec lineage -The facility numbers come from the private racket-sports origin spec (Naperville, four floors). This public repo is a later, thinner pretotype. Voyager/Eureka-style curriculum language is inspiration for a future ops loop, not a second scheduler in this tree. +- **Vision program**: `origin/enhance/3D` APEX campus (labs, physio, gym, pool, clubhouse, all sports) +- **Origin numbers**: private racket-sports spec (Naperville, four floors) +- Voyager/Eureka-style curriculum language is inspiration for a future ops loop, not a second scheduler in this tree A pretty 3D scene is a **projection**, not proof of autonomy. --- -## 👥 Expert Roles - -| Role | Responsibility | -|------|----------------| -| **Architect** | Facility layout and safety compliance | -| **Digital Twin Modeler** | Virtual replica and simulation systems | -| **Automation Engineer** | Robotics and autonomous systems integration | -| **Sports Surface Specialist** | Court maintenance and quality assurance | -| **Building Systems Engineer** | BMS and energy optimization | - ---- - -## 🎯 Strategic Goals - -1. **Autonomous Excellence**: Achieve 24/7 operation with minimal human intervention -2. **Sustainability**: Net-zero energy consumption through solar and smart systems -3. **User Experience**: Seamless booking, access, and service delivery -4. **Performance**: Industry-leading analytics and injury prevention -5. **Community**: Local partnerships and educational collaborations - ---- - -## 🤝 Partnerships & Funding - -### Strategic Partnerships -- Local sports organizations for cost sharing -- Educational institutions for research collaboration -- Government agencies for sustainability grants - -### Investment -The Invest form does not submit (MOCK). Origin location is Naperville. Other cities on that page are a pitch, not a second spec. - ---- - -## 📊 Technical Specifications - -### Data Collection -- Multi-sensor array for court conditions -- Environmental monitoring (temperature, humidity, air quality) -- Player movement tracking and analysis -- Real-time video analytics - -### Simulation & Analysis -- Energy usage forecasting -- Maintenance schedule optimization -- Player traffic pattern analysis -- Resource allocation modeling - -### System Integration -- Centralized control via BMS -- Real-time IoT device communication -- Cloud-based analytics platform -- Mobile app for member access - ---- - ## 📝 Project Structure ``` ace/ -├── .github/ -│ ├── workflows/ # CI/CD pipelines -│ └── DEPLOYMENT.md # Deployment documentation -├── components/ # React components -│ ├── NavBar.tsx -│ ├── ThreeScene.tsx -│ ├── AIChat.tsx -│ └── Specifications.tsx -├── services/ # Service layer -├── App.tsx # Main application -├── index.tsx # Entry point -├── types.ts # TypeScript definitions -└── vite.config.ts # Build configuration -``` - ---- - -## 🔧 Configuration - -### Base Path Configuration -The project supports dynamic base paths for multi-environment deployment: - -```typescript -// vite.config.ts -const base = process.env.VITE_BASE_PATH || '/'; -``` - -Set via environment variable during build: -```bash -VITE_BASE_PATH=/dev/ npm run build +├── AGENTS.md # Verified OSS Loop + ACE ownership +├── ROADMAP.md # First untracked item is what autodevelop mints +├── facility/ # Pascal program + scene compiler +│ ├── program.ts +│ ├── vision.ts # APEX VISION rooms +│ ├── generateScene.ts +│ └── check-scene.ts +├── components/ +│ ├── PascalFacility.tsx +│ ├── facility/SketchMap.tsx +│ └── ThreeScene.tsx # unused legacy — do not remount +├── .github/workflows/ # Pages deploy + validate + loop receipts +└── App.tsx ``` --- -## 📖 Documentation - -- [Deployment Guide](.github/DEPLOYMENT.md) - GitHub Pages setup and troubleshooting -- [AI Studio Link](https://ai.studio/apps/drive/1Fc7kvKrC_eN-FRp7CerebmQONPqJW6z2) - Original project workspace - ---- - ## 📄 License This project is licensed under the terms specified in [LICENSE](LICENSE). @@ -256,15 +184,6 @@ This project is licensed under the terms specified in [LICENSE](LICENSE). ## Acknowledgments - Origin facility spec (private digital-twin racket-sports project) -- React Three Fiber / three.js for the public sketch +- APEX campus program from the `enhance/3D` branch +- Pascal Viewer for the public sketch - Gemini is optional and stubbed on Pages - ---- - -
- -**Built with ❤️ by the LawnTech Dynamics team** - -[Report Bug](https://github.com/kvnloo/ace/issues) • [Request Feature](https://github.com/kvnloo/ace/issues) • [Contact Us](https://kvnloo.github.io/ace/) - -
diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..f856d7e --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,24 @@ +# ACE roadmap + +Workers mint **exactly one** `needs-discussion` issue from the first untracked item. Do not rewrite this file to claim work. Do not self-apply `claimable`. + +## Honesty split + +| Stamp | Meaning | +|---|---| +| **VISION** | Peak-performance campus for human flourishing from all angles: scientists, athletes, labs, physiotherapy, all sports. Program mined from `origin/enhance/3D` (APEX). Not origin measurements. | +| **SPEC** | Naperville origin racquet counts: 24 tennis / 16 badminton / 4 squash / 16 table tennis / 8 pickleball / 1 real tennis. Grass lab: 500 m² per section, section count unspecified. | +| **PLANNED / MOCK** | Live BMS, drones, 60 FPS biomechanics, Gemini concierge product, 6/6/6/6 split, four farm racks, 60-minute turf-swap receipt. | +| **This repo runs** | Pascal pretotype + CSS fallback. Not a live facility. Homebase owns the live GPU loop. | + +## Untracked (oldest first) + +1. Delete or isolate unused `components/ThreeScene.tsx` so inferred 6/6/6/6 and four farm racks cannot remount if that canvas is imported again. +2. GitHub Pages still deploys only `main` (`/ace/`) and `dev` (`/ace/dev/`). Rolling `preview` / `nightly` branches have no Pages slot — do not invent `/preview//` without extending `.github/workflows/deploy.yml` and documenting it. +3. APEX Pascal rooms are named VISION zones with inferred 30×30 m cells. Do not promote those dimensions (or 147 biomarkers, +28% VO₂, 1,388 seats) to origin until a spec says so. +4. Do not start ACE as a second live GPU loop. Homebase (`kvnloo/homebase-pickleball`) owns the live GPU path. + +## Maintainer (not worker) + +- `.github/scripts/create-labels.sh` (gh write; workers cannot). +- `bash scripts/ensure-rollout-branches.sh --root . --push` so `preview` and `nightly` exist. After that, new worker PRs target `preview`. Existing honesty PR #7 still targets `main` because it predates rolling branches. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b28c8e8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Security + +- Do not open issues with secrets, pairing tokens, API keys, or private transcripts. +- Report vulnerabilities privately via GitHub Security Advisories on `kvnloo/ace`, or to @kvnloo. Do not file a public CVE-shaped issue unless the project says so. +- Unknown authority, undeclared privileged capability, and unsigned receipts fail closed when the project has a validator. +- This file never authorizes autonomous merge. diff --git a/components/AIChat.tsx b/components/AIChat.tsx index 7e02a5e..1d76c5a 100644 --- a/components/AIChat.tsx +++ b/components/AIChat.tsx @@ -9,7 +9,7 @@ const AIChat: React.FC = () => { const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [messages, setMessages] = useState([ - { role: 'model', text: "Welcome to LawnTech Dynamics. I'm your AI Concierge. Ask me about our autonomous grass courts or replacement modular grids." } + { role: 'model', text: "This chat is a stub. Ask about the VISION campus (APEX labs, physio, all sports) or the Naperville racquet SPEC." } ]); const scrollRef = useRef(null); @@ -58,7 +58,7 @@ const AIChat: React.FC = () => {
- Facility AI Concierge + Campus sketch guide
)}
- Naperville pretotype · Pascal facility · envelope inferred + Naperville pretotype · VISION campus · Pascal facility
)} diff --git a/components/Specifications.tsx b/components/Specifications.tsx index 6f30073..468b415 100644 --- a/components/Specifications.tsx +++ b/components/Specifications.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { motion } from 'framer-motion'; -import { CheckCircle2, Layers, Droplets, Zap, Home, Server } from 'lucide-react'; +import { CheckCircle2, Layers, Droplets, Zap, Home, Server, Brain } from 'lucide-react'; const Specifications: React.FC = () => { const specs = [ @@ -29,7 +29,7 @@ const Specifications: React.FC = () => { items: [ { label: "Pickleball Courts", value: "8 Courts" }, { label: "Real Tennis Court", value: "1 Historic Court" }, - { label: "Viewing Decks", value: "360° Glass Walkways" } + { label: "Viewing decks", value: "Not in origin spec" } ] }, { @@ -42,6 +42,16 @@ const Specifications: React.FC = () => { { label: "Patch transport", value: "PLANNED" } ] }, + { + category: "APEX campus (VISION)", + icon: , + items: [ + { label: "Labs", value: "Biometric, cognitive, movement, research, nutrition" }, + { label: "Recovery", value: "Physiotherapy + recovery suite" }, + { label: "Training", value: "Gym, pool, clubhouse — all sports" }, + { label: "Dimensions", value: "Inferred 30×30 m cells — not origin" } + ] + }, { category: "Ops overlay (not live)", icon: , @@ -57,9 +67,9 @@ const Specifications: React.FC = () => { return (
-

Facility Blueprints

+

Campus Blueprints

- Naperville origin: four floors of sport with a grass lab on top. Numbers below match the spec; inferred theater is labeled. + Vision: peak performance from every angle. SPEC numbers are Naperville racquet. APEX rooms are named VISION — not origin measurements.

diff --git a/components/facility/SketchMap.tsx b/components/facility/SketchMap.tsx index 781e0ad..775aa80 100644 --- a/components/facility/SketchMap.tsx +++ b/components/facility/SketchMap.tsx @@ -21,11 +21,12 @@ export const ControlsOverlay: React.FC<{ Floor View
{[ + { id: 'APEX', label: 'APEX: Campus' }, { id: 3, label: 'L3: Lab' }, { id: 2, label: 'L2: Social' }, { id: 1, label: 'L1: Racquet' }, { id: 0, label: 'G: Tennis' }, - { id: 'ALL', label: 'Full Facility' }, + { id: 'ALL', label: 'Full Campus' }, ].map((item) => (
)}
- Naperville pretotype · Pascal program sketch + Naperville pretotype · VISION campus · Pascal program sketch
); diff --git a/facility/check-scene.ts b/facility/check-scene.ts index 69efb75..55919db 100644 --- a/facility/check-scene.ts +++ b/facility/check-scene.ts @@ -1,27 +1,72 @@ import { generateLawnTechScene, sceneStats } from './generateScene.ts'; +import { APEX_BUILDING_ID, APEX_LEVEL_ID, APEX_ROOMS, APEX_ZONE_IDS } from './vision.ts'; const scene = generateLawnTechScene(); const stats = sceneStats(scene); -const required = ['site_naperville', 'building_lawntech', 'level_0', 'level_1', 'level_2', 'level_3', 'zone_grass-lab']; +const required = [ + 'site_naperville', + 'building_lawntech', + 'level_0', + 'level_1', + 'level_2', + 'level_3', + 'zone_grass-lab', + APEX_BUILDING_ID, + APEX_LEVEL_ID, + ...APEX_ZONE_IDS, +]; const missing = required.filter((id) => !scene.nodes[id]); const farmZones = Object.values(scene.nodes).filter( (node) => node.type === 'zone' && String(node.id).includes('grass'), ); +const visionZones = Object.values(scene.nodes).filter((node) => { + if (node.type !== 'zone') return false; + return (node.metadata as { honesty?: string } | undefined)?.honesty === 'VISION'; +}); +const siteMeta = scene.nodes.site_naperville.metadata as { + envelope?: string; + vision?: string; +}; if (missing.length) { throw new Error(`missing nodes: ${missing.join(', ')}`); } -if (stats.kinds.level !== 4) { - throw new Error(`expected 4 levels, got ${stats.kinds.level}`); +if (stats.kinds.building !== 2) { + throw new Error(`expected 2 buildings, got ${stats.kinds.building}`); +} +if (stats.kinds.level !== 5) { + throw new Error(`expected 4 LawnTech levels + 1 APEX level, got ${stats.kinds.level}`); } if (farmZones.length !== 1) { throw new Error(`expected one grass-lab zone, got ${farmZones.length}`); } +if (visionZones.length !== APEX_ROOMS.length) { + throw new Error(`expected ${APEX_ROOMS.length} VISION zones, got ${visionZones.length}`); +} if (stats.hasFourFarmSections) { throw new Error('inferred farm sections leaked into the Pascal scene'); } -if ((scene.nodes.site_naperville.metadata as { envelope?: string }).envelope !== 'inferred-140x120') { +if (siteMeta.envelope !== 'inferred-140x120') { throw new Error('site envelope stamp missing'); } +if (siteMeta.vision !== 'peak-performance-campus') { + throw new Error('site vision stamp missing'); +} -console.log(JSON.stringify({ ok: true, ...stats }, null, 2)); +const site = scene.nodes.site_naperville as { children?: string[] }; +if (!site.children?.includes('building_lawntech') || !site.children?.includes(APEX_BUILDING_ID)) { + throw new Error('site must parent both LawnTech and APEX'); +} + +console.log( + JSON.stringify( + { + ok: true, + ...stats, + visionZones: visionZones.length, + apexRooms: APEX_ROOMS.map((room) => room.id), + }, + null, + 2, + ), +); diff --git a/facility/generateScene.ts b/facility/generateScene.ts index 28ca91a..f2d3f3e 100644 --- a/facility/generateScene.ts +++ b/facility/generateScene.ts @@ -11,6 +11,7 @@ import { type AnyNodeId, } from '@pascal-app/core'; import { + APEX_OFFSET_X, BUILDING_DEPTH, BUILDING_ID, BUILDING_WIDTH, @@ -18,6 +19,13 @@ import { WALL_HEIGHT, WALL_THICKNESS, } from './program.ts'; +import { + APEX_BUILDING_ID, + APEX_DEPTH, + APEX_LEVEL_ID, + APEX_ROOMS, + APEX_WIDTH, +} from './vision.ts'; type MaterialPreset = 'concrete' | 'plaster' | 'glass' | 'wood' | 'metal'; type SceneNodes = Record; @@ -302,6 +310,89 @@ function interiorWalls(floor: number): Array<{ id: `wall_${string}`; start: Poin return []; } +function addApexCampus(nodes: SceneNodes, siteId: string) { + const x0 = -APEX_WIDTH / 2; + const z0 = -APEX_DEPTH / 2; + const cols = 3; + const rows = 3; + const cellW = APEX_WIDTH / cols; + const cellD = APEX_DEPTH / rows; + const childIds: string[] = []; + + APEX_ROOMS.forEach((room, index) => { + const col = index % cols; + const row = Math.floor(index / cols); + const [zoneId, slabId] = addRoom( + nodes, + APEX_LEVEL_ID, + { + id: room.id, + name: room.name, + color: room.color, + polygon: rect(x0 + col * cellW, z0 + row * cellD, cellW, cellD), + floorFinish: 'campus-vision', + occupancy: room.occupancy, + metadata: { + honesty: 'VISION', + source: 'enhance/3D-apex-campus', + dimensions: 'inferred-30x30-cells', + cluster: room.cluster, + }, + }, + 'tile', + ); + childIds.push(zoneId, slabId); + }); + + const south = addWall(nodes, APEX_LEVEL_ID, 'wall_apex_south', [x0, z0], [x0 + APEX_WIDTH, z0], 'glass', [ + { id: 'apex_entry', kind: 'door', along: APEX_WIDTH / 2, width: 3.6, height: 3.2 }, + ]); + const east = addWall( + nodes, + APEX_LEVEL_ID, + 'wall_apex_east', + [x0 + APEX_WIDTH, z0], + [x0 + APEX_WIDTH, z0 + APEX_DEPTH], + 'glass', + ); + const north = addWall( + nodes, + APEX_LEVEL_ID, + 'wall_apex_north', + [x0 + APEX_WIDTH, z0 + APEX_DEPTH], + [x0, z0 + APEX_DEPTH], + 'glass', + ); + const west = addWall(nodes, APEX_LEVEL_ID, 'wall_apex_west', [x0, z0 + APEX_DEPTH], [x0, z0], 'glass'); + childIds.push(south.id, east.id, north.id, west.id); + + add( + nodes, + LevelNode.parse({ + id: APEX_LEVEL_ID, + name: 'APEX — peak-performance wing', + parentId: APEX_BUILDING_ID, + level: 0, + height: WALL_HEIGHT, + children: childIds, + metadata: { honesty: 'VISION', source: 'enhance/3D-apex-campus' }, + }), + ); + + add( + nodes, + BuildingNode.parse({ + id: APEX_BUILDING_ID, + name: 'APEX peak-performance campus (VISION)', + parentId: siteId, + position: [APEX_OFFSET_X, 0, 0], + rotation: [0, 0, 0], + children: [APEX_LEVEL_ID], + metadata: { honesty: 'VISION', source: 'enhance/3D-apex-campus' }, + }), + ); +} + export type LawnTechScene = { nodes: SceneNodes; rootNodeIds: AnyNodeId[]; @@ -312,25 +403,27 @@ export function generateLawnTechScene(): LawnTechScene { const sitePad = 20; const site = SiteNode.parse({ id: SITE_ID, - name: 'Naperville — LawnTech pretotype', + name: 'Naperville — peak-performance pretotype', polygon: { type: 'polygon', points: [ [X0 - sitePad, Z0 - sitePad], - [X1 + sitePad, Z0 - sitePad], - [X1 + sitePad, Z1 + sitePad], + [APEX_OFFSET_X + APEX_WIDTH / 2 + sitePad, Z0 - sitePad], + [APEX_OFFSET_X + APEX_WIDTH / 2 + sitePad, Z1 + sitePad], [X0 - sitePad, Z1 + sitePad], ], }, metadata: { origin: 'Naperville', honesty: 'pretotype', + vision: 'peak-performance-campus', envelope: 'inferred-140x120', wallHeight: 'inferred-10m-sport-hall', compiler: 'ace-facility@1', farmSections: 'unspecified', + apex: 'vision-named-rooms-inferred-cells', }, - children: [BUILDING_ID], + children: [BUILDING_ID, APEX_BUILDING_ID], }); const building = BuildingNode.parse({ @@ -397,6 +490,7 @@ export function generateLawnTechScene(): LawnTechScene { } add(nodes, building); + addApexCampus(nodes, site.id); add(nodes, site); return { nodes, rootNodeIds: [site.id] }; diff --git a/facility/program.ts b/facility/program.ts index 0eafdd4..0fbafcb 100644 --- a/facility/program.ts +++ b/facility/program.ts @@ -1,4 +1,7 @@ import { FeatureData } from '../types'; +import { APEX_GAP } from './vision.ts'; + +export { APEX_BUILDING_ID, APEX_LEVEL_ID, APEX_ROOMS, APEX_ZONE_IDS } from './vision.ts'; /** Inferred envelope from the earlier Three.js sketch — not in the origin spec. */ export const BUILDING_WIDTH = 140; @@ -9,9 +12,12 @@ export const WALL_THICKNESS = 0.35; export const SITE_ID = 'site_naperville'; export const BUILDING_ID = 'building_lawntech'; +/** East of LawnTech. Gap is inferred — not an origin campus survey. */ +export const APEX_OFFSET_X = BUILDING_WIDTH + APEX_GAP; -export type FloorLevel = 'ALL' | 0 | 1 | 2 | 3; +export type FloorLevel = 'ALL' | 0 | 1 | 2 | 3 | 'APEX'; export type AnnotationMode = 'NONE' | 'LABELS' | 'MEASUREMENTS'; +export type SketchVariant = 'tennis' | 'badminton' | 'pickle' | 'farm' | 'campus' | 'apex'; export const FEATURES: FeatureData[] = [ { @@ -44,37 +50,66 @@ export const FEATURES: FeatureData[] = [ icon: '🌱', position: [0, 35, 0], }, + { + id: 'apex_labs', + title: 'APEX labs (VISION)', + description: + 'Biometric, cognitive, movement, research, and nutrition rooms from the enhance/3D campus program. Named VISION zones — not origin measurements.', + icon: '◎', + position: [APEX_OFFSET_X, 5, 0], + }, + { + id: 'apex_recovery', + title: 'Recovery / physio (VISION)', + description: + 'Recovery suite and physiotherapy as campus identity. Capacity unspecified. Not a live clinic twin.', + icon: '✚', + position: [APEX_OFFSET_X, 5, 20], + }, + { + id: 'apex_training', + title: 'Gym / pool / clubhouse (VISION)', + description: + 'All-sports training floor, pool, and clubhouse for scientists and athletes. User-story program, not origin counts.', + icon: '▣', + position: [APEX_OFFSET_X, 5, -20], + }, ]; export const SKETCH_BY_FLOOR: Record< string, - { title: string; note: string; variant: 'tennis' | 'badminton' | 'pickle' | 'farm' | 'campus' } + { title: string; note: string; variant: SketchVariant } > = { ALL: { - title: 'Naperville facility', - note: '24 tennis · grass lab · Pascal / CSS sketch', + title: 'Peak-performance campus', + note: 'VISION campus + Naperville racquet SPEC · Pascal / CSS sketch', variant: 'campus', }, '0': { title: 'Ground: Tennis arena', - note: '24 courts · hard / clay / grass / wood', + note: 'SPEC · 24 courts · hard / clay / grass / wood', variant: 'tennis', }, '1': { title: 'L1: Racquet mezzanine', - note: '16 badminton · 4 squash · 16 table tennis', + note: 'SPEC · 16 badminton · 4 squash · 16 table tennis', variant: 'badminton', }, '2': { title: 'L2: Pickleball & heritage', - note: '8 pickleball · 1 real tennis', + note: 'SPEC · 8 pickleball · 1 real tennis', variant: 'pickle', }, '3': { title: 'L3: Vertical grass lab', - note: '500 m² per section · section count unspecified', + note: 'PLANNED · 500 m² per section · section count unspecified', variant: 'farm', }, + APEX: { + title: 'APEX campus wing', + note: 'VISION · labs, physio, gym, pool, clubhouse · cells inferred', + variant: 'apex', + }, }; export function levelNodeId(floor: 0 | 1 | 2 | 3): `level_${string}` { diff --git a/facility/vision.ts b/facility/vision.ts new file mode 100644 index 0000000..83eda39 --- /dev/null +++ b/facility/vision.ts @@ -0,0 +1,84 @@ +/** Peak-performance campus program mined from origin/enhance/3D. Not origin measurements. */ + +export const APEX_BUILDING_ID = 'building_apex'; +export const APEX_LEVEL_ID = 'level_apex'; +export const APEX_WIDTH = 90; +export const APEX_DEPTH = 90; +export const APEX_GAP = 40; + +export type VisionRoom = { + id: string; + name: string; + color: string; + occupancy: string; + cluster: 'labs' | 'recovery' | 'training'; +}; + +/** Six APEX labs plus gym / pool / clubhouse. Capacities and cell size are inferred. */ +export const APEX_ROOMS: VisionRoom[] = [ + { + id: 'biometric', + name: 'Biometric lab', + color: '#7f1d1d', + occupancy: 'VISION — VO2 / force plate / mocap language; capacity unspecified', + cluster: 'labs', + }, + { + id: 'cognitive', + name: 'Cognitive lab', + color: '#1e3a5f', + occupancy: 'VISION — cognitive training; capacity unspecified', + cluster: 'labs', + }, + { + id: 'movement', + name: 'Movement studio', + color: '#14532d', + occupancy: 'VISION — movement optimization; capacity unspecified', + cluster: 'labs', + }, + { + id: 'research', + name: 'Research integration', + color: '#3b0764', + occupancy: 'VISION — scientists / protocol translation; capacity unspecified', + cluster: 'labs', + }, + { + id: 'nutrition', + name: 'Nutrition kitchen', + color: '#3f3f1a', + occupancy: 'VISION — personalized nutrition language; capacity unspecified', + cluster: 'labs', + }, + { + id: 'recovery', + name: 'Recovery / physiotherapy', + color: '#0f766e', + occupancy: 'VISION — recovery suite + physio; capacity unspecified', + cluster: 'recovery', + }, + { + id: 'gym', + name: 'Gym', + color: '#1f2937', + occupancy: 'VISION — all-sports training floor; capacity unspecified', + cluster: 'training', + }, + { + id: 'pool', + name: 'Pool', + color: '#1e40af', + occupancy: 'VISION — aquatic training; capacity unspecified', + cluster: 'training', + }, + { + id: 'clubhouse', + name: 'Clubhouse', + color: '#44403c', + occupancy: 'VISION — scientists / athletes / members; capacity unspecified', + cluster: 'training', + }, +]; + +export const APEX_ZONE_IDS = APEX_ROOMS.map((room) => `zone_${room.id}`); diff --git a/package-lock.json b/package-lock.json index bc7d704..bb3ee01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "devDependencies": { "@types/node": "^22.14.0", "@vitejs/plugin-react": "^5.0.0", + "esbuild": "^0.25.9", "typescript": "~5.8.2", "vite": "^6.2.0" } diff --git a/package.json b/package.json index d8abc69..85c0e6e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "check:facility": "npx --yes esbuild facility/check-scene.ts --bundle --platform=node --format=esm --outfile=/tmp/ace-check-facility.mjs && node /tmp/ace-check-facility.mjs" + "test": "npm run check:facility", + "check:facility": "esbuild facility/check-scene.ts --bundle --platform=node --format=esm --outfile=/tmp/ace-check-facility.mjs && node /tmp/ace-check-facility.mjs" }, "dependencies": { "@pascal-app/core": "1.0.0", @@ -25,6 +26,7 @@ "devDependencies": { "@types/node": "^22.14.0", "@vitejs/plugin-react": "^5.0.0", + "esbuild": "^0.25.9", "typescript": "~5.8.2", "vite": "^6.2.0" } diff --git a/prompt.md b/prompt.md new file mode 100644 index 0000000..276be7f --- /dev/null +++ b/prompt.md @@ -0,0 +1,57 @@ +# Autodevelop — ace + +Paste **this entire file** as the first message to any coding agent (Cursor, Codex, Claude Code, Hermes, …). Do not summarize it. Work in **this** repository, not a different clone. + +| | | +|---|---| +| **This repo** | https://github.com/kvnloo/ace | +| Clone | `git clone https://github.com/kvnloo/ace.git` | +| Protocol | [Verified OSS Loop](https://github.com/kvnloo/verified-oss-loop) (`SPEC.md` in that kit; this tree has `AGENTS.md`) | + +You are a contributor, not a maintainer. Donate **one** coding pass. **Never merge `main` or `dev`.** Default rollout is Arch-style `rolling` unless `.verified-oss-loop/rollout.yml` says otherwise. + +## Do + +1. Clone or open https://github.com/kvnloo/ace. `cd` into that tree. `git fetch origin`. `python3 .verified-oss-loop/rollout.py show`. Branch from `origin/$(python3 .verified-oss-loop/rollout.py get worker_base)` unless the issue names another base. Day-pass PRs target `feature_target`. Overnight PRs target `overnight_target`. +2. Read `AGENTS.md` and `CONTRIBUTING.md` in this repo. Follow them if they conflict with this file. +3. Search open issues and PRs. Stop on overlap. + +```bash +gh issue list --repo kvnloo/ace --label claimable --state open +gh pr list --repo kvnloo/ace --state open +``` + +4. Branch on the queue: + +- **Work** — open `claimable` and not `claimed`: claim **one** leaf. Prefer `priority:P0`, then `P1`, then `good-first-issue`. Skip `needs-discussion` unless a human assigned it. +- **Triage** — nothing `claimable`: do not code. If a `needs-discussion` issue exists: one-paragraph proposal on the newest; stop. If none: mint **exactly one** issue from the first untracked item in `ROADMAP.md`, else a failing unit command from `AGENTS.md`, else docs drift; label **`needs-discussion` only**; stop. Do not self-apply `claimable`. Do not rewrite `ROADMAP.md`. +- **Stop** — triage found nothing untracked, a live claim exists, a competing PR covers the scope, or secrets are required. + +Claim comment (24h lease unless the project says otherwise): + +```text +claiming for autodevelop +claimant: +base: +expires: +scope: +``` + +Then add `claimed` and remove `claimable`. + +5. Orient before edit (`skills/orient/SKILL.md`). If GitNexus MCP is already there: `query` → `context` → `impact`. Do not run `gitnexus analyze` unless a human asked. +6. Fail, then pass (`skills/tdd/SKILL.md`). Unit command: `npm test`. Mutation: `npx stryker run` (write `n/a` if that is what the tree says; do not invent a score). +7. Smallest complete change (`skills/anti-slop/SKILL.md`). If pstack is already installed, `/poteto-mode` is allowed; still never merge. +8. Open a PR on https://github.com/kvnloo/ace at `feature_target` (day) or `overnight_target` (overnight) with `.github/PULL_REQUEST_TEMPLATE.md` filled. Bind evidence to `head_revision`. **Do not merge `main` or `dev`.** + +## Stop + +- Triage found nothing untracked, a live claim exists, or a competing PR covers the scope. +- You would need secrets, pairing tokens, or maintainer credentials. +- You were about to merge `main`/`dev`, force-push `main`, overwrite `LICENSE`, or dump GitNexus/pstack into the tree. + +If you stop, comment the blocker only if an issue thread exists. Do not open a consolation PR. + +## Skills in this repo + +`skills/autodevelop/SKILL.md` is the same loop. `skills/verify/SKILL.md` is the receipt. Do not invent a parallel process. diff --git a/roadmap.example.yml b/roadmap.example.yml new file mode 100644 index 0000000..1637731 --- /dev/null +++ b/roadmap.example.yml @@ -0,0 +1,8 @@ +roadmap_revision: replace-with-git-sha +updated_at: 2026-01-01T00:00:00Z +owners: [YOUR_GITHUB_LOGIN] +items: + - issue: 1 + priority: P1 + acceptance: tests-or-docs-path + claimable: true diff --git a/scripts/cluster-similar-issues.py b/scripts/cluster-similar-issues.py new file mode 100644 index 0000000..c33f347 --- /dev/null +++ b/scripts/cluster-similar-issues.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Cluster similar issues from a local JSON fixture. + +Reads at most MAX_ISSUES records. Never scrapes a live tracker. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +MAX_ISSUES = 64 +STOP = { + "a", + "an", + "the", + "and", + "or", + "to", + "of", + "for", + "in", + "on", + "with", + "after", + "when", + "plus", + "own", +} + +TOKEN = re.compile(r"[a-z0-9]+") + + +def tokens(title: str) -> frozenset[str]: + return frozenset(t for t in TOKEN.findall(title.lower()) if t not in STOP and len(t) > 1) + + +def jaccard(a: frozenset[str], b: frozenset[str]) -> float: + if not a or not b: + return 0.0 + inter = len(a & b) + union = len(a | b) + return inter / union if union else 0.0 + + +def load_issues(path: Path, cap: int) -> list[dict]: + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, list): + raise SystemExit("fixture must be a JSON array of issues") + if len(data) > cap: + raise SystemExit(f"fixture has {len(data)} issues; cap is {cap}") + out = [] + for item in data: + if not isinstance(item, dict) or "number" not in item or "title" not in item: + raise SystemExit("each issue needs number and title") + out.append({"number": int(item["number"]), "title": str(item["title"])}) + return out + + +def cluster(issues: list[dict], threshold: float) -> list[list[int]]: + n = len(issues) + parent = list(range(n)) + + def find(i: int) -> int: + while parent[i] != i: + parent[i] = parent[parent[i]] + i = parent[i] + return i + + def union(i: int, j: int) -> None: + ri, rj = find(i), find(j) + if ri != rj: + parent[rj] = ri + + toks = [tokens(it["title"]) for it in issues] + for i in range(n): + for j in range(i + 1, n): + if jaccard(toks[i], toks[j]) >= threshold: + union(i, j) + + groups: dict[int, list[int]] = {} + for i, it in enumerate(issues): + groups.setdefault(find(i), []).append(it["number"]) + clusters = [sorted(v) for v in groups.values()] + clusters.sort(key=lambda c: (-len(c), c[0])) + return clusters + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Cluster similar issues from a local fixture") + p.add_argument("fixture", type=Path, help="JSON array of {number, title}") + p.add_argument("--threshold", type=float, default=0.45) + p.add_argument("--cap", type=int, default=MAX_ISSUES) + p.add_argument("--json", action="store_true") + args = p.parse_args(argv) + if args.cap > MAX_ISSUES: + print(f"cap cannot exceed {MAX_ISSUES}", file=sys.stderr) + return 2 + issues = load_issues(args.fixture, args.cap) + clusters = cluster(issues, args.threshold) + if args.json: + json.dump({"clusters": clusters}, sys.stdout) + sys.stdout.write("\n") + else: + for c in clusters: + print(" ".join(str(n) for n in c)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ensure-rollout-branches.sh b/scripts/ensure-rollout-branches.sh new file mode 100755 index 0000000..ec088a0 --- /dev/null +++ b/scripts/ensure-rollout-branches.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Create preview, nightly, and dev from the default branch if missing. +# Does not force-push. Does not touch main. +# Usage: ensure-rollout-branches.sh --root DIR [--push] +set -euo pipefail + +ROOT="." +PUSH=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --root) ROOT="$2"; shift 2 ;; + --push) PUSH=1; shift ;; + -h|--help) + echo "Usage: $0 [--root DIR] [--push]" + exit 0 + ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +ROOT="$(cd "$ROOT" && pwd)" +git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null + +DEFAULT=main +if git -C "$ROOT" rev-parse --verify -q origin/main >/dev/null; then + DEFAULT=main +elif git -C "$ROOT" symbolic-ref -q refs/remotes/origin/HEAD >/dev/null; then + DEFAULT="$(git -C "$ROOT" symbolic-ref --short refs/remotes/origin/HEAD | sed 's#^origin/##')" +fi + +scheme="rolling" +if [[ -f "$ROOT/.verified-oss-loop/rollout.yml" ]]; then + scheme="$(python3 "$(dirname "$0")/rollout.py" --root "$ROOT" get scheme)" +fi +if [[ "$scheme" == "stable" ]]; then + echo "scheme=stable: no extra channels" + exit 0 +fi + +for b in preview nightly dev; do + if git -C "$ROOT" rev-parse --verify -q "origin/$b" >/dev/null; then + echo "keep origin/$b" + continue + fi + if git -C "$ROOT" rev-parse --verify -q "$b" >/dev/null; then + echo "keep local $b" + else + git -C "$ROOT" branch "$b" "origin/$DEFAULT" + echo "created $b from origin/$DEFAULT" + fi + if [[ "$PUSH" -eq 1 ]]; then + git -C "$ROOT" push -u origin "$b" + fi +done diff --git a/scripts/rollout.py b/scripts/rollout.py new file mode 100644 index 0000000..b8a76b8 --- /dev/null +++ b/scripts/rollout.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Read .verified-oss-loop/rollout.yml. Stdlib only. Does not merge.""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +SCHEMES = ("rolling", "staged", "stable") + + +def load_scheme(root: Path) -> str: + path = root / ".verified-oss-loop" / "rollout.yml" + scheme = "rolling" + if path.is_file(): + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.split("#", 1)[0].strip() + if line.startswith("scheme:"): + scheme = line.split(":", 1)[1].strip().strip("'\"") + break + if scheme not in SCHEMES: + raise SystemExit(f"unknown rollout scheme: {scheme}") + return scheme + + +def policy(scheme: str) -> dict[str, str | bool]: + if scheme == "stable": + return { + "scheme": scheme, + "worker_base": "main", + "feature_target": "main", + "overnight_target": "main", + "automerge_preview": False, + "automerge_nightly": False, + "promote_preview_to_nightly": False, + } + if scheme == "staged": + return { + "scheme": scheme, + "worker_base": "nightly", + "feature_target": "preview", + "overnight_target": "nightly", + "automerge_preview": True, + "automerge_nightly": False, + "promote_preview_to_nightly": True, + } + return { + "scheme": scheme, + "worker_base": "nightly", + "feature_target": "preview", + "overnight_target": "nightly", + "automerge_preview": True, + "automerge_nightly": True, + "promote_preview_to_nightly": True, + } + + +def cmd_show(root: Path) -> int: + p = policy(load_scheme(root)) + for k in ( + "scheme", + "worker_base", + "feature_target", + "overnight_target", + "automerge_preview", + "automerge_nightly", + "promote_preview_to_nightly", + ): + print(f"{k}={p[k]}".lower() if isinstance(p[k], bool) else f"{k}={p[k]}") + return 0 + + +def cmd_get(root: Path, key: str) -> int: + p = policy(load_scheme(root)) + if key not in p: + print(f"unknown key: {key}", file=sys.stderr) + return 2 + val = p[key] + print(str(val).lower() if isinstance(val, bool) else val) + return 0 + + +def cmd_allow(root: Path, channel: str) -> int: + if channel not in ("preview", "nightly"): + return 1 + p = policy(load_scheme(root)) + ok = bool(p[f"automerge_{channel}"]) + return 0 if ok else 1 + + +def cmd_allow_promote(root: Path, what: str) -> int: + if what != "preview-to-nightly": + return 1 + p = policy(load_scheme(root)) + return 0 if p["promote_preview_to_nightly"] else 1 + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Verified OSS Loop rollout scheme") + p.add_argument("--root", default=".") + sub = p.add_subparsers(dest="cmd", required=True) + sub.add_parser("show") + g = sub.add_parser("get") + g.add_argument("key") + a = sub.add_parser("allow-automerge") + a.add_argument("channel", choices=("preview", "nightly")) + pr = sub.add_parser("allow-promote") + pr.add_argument("what", choices=("preview-to-nightly",)) + args = p.parse_args(argv) + root = Path(args.root).resolve() + if args.cmd == "show": + return cmd_show(root) + if args.cmd == "get": + return cmd_get(root, args.key) + if args.cmd == "allow-promote": + return cmd_allow_promote(root, args.what) + return cmd_allow(root, args.channel) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/services/geminiService.ts b/services/geminiService.ts index 02f2731..52df809 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -13,37 +13,32 @@ const getClient = (): GoogleGenAI | null => { }; const SYSTEM_INSTRUCTION = ` -You are the AI Concierge for "LawnTech Dynamics", a futuristic, multi-story autonomous sports facility. -Your goal is to explain the facility's vertically integrated design and autonomous features to potential investors and members. - -This chat is a stub unless an API key is present. Do not invent receipts. - -Key Facility Layout (Naperville origin spec): - -**Ground Floor (The Tennis Arena)** -- 24 tennis courts: hard, clay, grass, wood. Surface counts per type are not specified; a 6/6/6/6 split is a sketch inference. -- Amenities: pro shop. Robotic mowers are PLANNED, not implemented. - -**First Floor (The Racquet Mezzanine)** -- 16 Badminton Courts. -- 4 Squash Courts. -- 16 Table Tennis Tables. - -**Second Floor (The Pickleball & Heritage Deck)** -- 8 Pickleball Courts. -- 1 Real Tennis Court. - -**Third Floor (The Vertical Grass Lab)** -- 500 m² per section (origin). Section count is unspecified; do not claim 2,000 m² as spec. -- Hydroponics and patch transport: PLANNED. - -Tone: precise, architectural. Label MOCK vs spec. Keep answers concise (under 100 words) unless asked for detail. +You are a sketch guide for ACE / LawnTech Dynamics, a peak-performance campus pretotype. +Vision: human flourishing from all angles — scientists, athletes, labs, physiotherapy, all sports. +Do not invent receipts. Label VISION vs SPEC vs MOCK. + +**VISION (enhance/3D APEX — not origin measurements)** +- Biometric, cognitive, movement, research, nutrition labs +- Recovery / physiotherapy +- Gym, pool, clubhouse +- Cell sizes on the map are inferred + +**SPEC (Naperville origin)** +- Ground: 24 tennis (hard, clay, grass, wood). Split unspecified; not 6/6/6/6. +- L1: 16 badminton, 4 squash, 16 table tennis +- L2: 8 pickleball, 1 real tennis +- L3 grass lab: 500 m² per section; section count unspecified + +**MOCK / PLANNED** +- Live BMS, drones, 60 FPS biomechanics, Gemini as a product + +Tone: precise. Keep answers under 100 words unless asked for detail. `; export const sendQueryToConcierge = async (history: {role: string, parts: {text: string}[]}[]): Promise => { const client = getClient(); if (!client) { - return "I'm currently offline (API Key missing). Please imagine I gave you a brilliant answer about our autonomous multi-story complex!"; + return "I'm currently offline (API key missing). This chat is a stub, not a concierge product."; } try { diff --git a/skills/anti-slop/SKILL.md b/skills/anti-slop/SKILL.md new file mode 100644 index 0000000..a0457bf --- /dev/null +++ b/skills/anti-slop/SKILL.md @@ -0,0 +1,30 @@ +# Anti-slop + +The smallest complete change that matches this tree. Style of the files you touch wins over a generic “clean code” essay. + +## Patterns + +- One issue, one PR, one concern. +- Match naming, imports, error handling, and test style already in the file. +- Fail, then pass (`skills/tdd/SKILL.md`). Assert behavior, not the implementation’s private shape. +- Names from the domain, not `handleData2`. +- Impact, then edit (`skills/orient/SKILL.md`). Check sibling surfaces that read the same state. +- Comments explain why or a constraint. Delete comments that restate the next line. +- Delete dead code you added. Do not leave a helper used once “for later”. + +## Anti-patterns + +- Narrating comments (`// increment the counter`), emoji, or “As an AI…”. +- Speculative abstractions: a base class, factory, or helper for a single call site. +- Drive-by refactors, format-only churn, or rewriting files you did not need. +- Tests that only echo the implementation, compare duplicated fixtures, or never fail for the bug. +- Catch-all `except` / `type: ignore` / empty `catch` to make CI green. +- Unasked README, badges, or a second `AGENTS.md` / `CLAUDE.md`. +- Extra dependencies for a one-liner the stdlib or existing stack already covers. +- `gitnexus analyze` (or any indexer) injecting a second H1 into `AGENTS.md`. +- Inventing a second loop, a parallel skill tree, or TODO instead of finishing the claim. +- Broad glob rewrites (`**/*`) when the blast radius is one module. + +## Stop and shrink + +If the diff teaches a new architecture, you overshot. Split or delete until a reviewer can name the issue in the first screen. diff --git a/skills/autodevelop/SKILL.md b/skills/autodevelop/SKILL.md new file mode 100644 index 0000000..a96eb52 --- /dev/null +++ b/skills/autodevelop/SKILL.md @@ -0,0 +1,28 @@ +# Autodevelop + +Donate one coding pass. Do not invent a parallel process. + +## Do + +1. Read root `AGENTS.md` and `CONTRIBUTING.md`. +2. `git fetch origin`. `python3 .verified-oss-loop/rollout.py show` (or `python3 scripts/rollout.py show`). Branch from `origin/$(python3 .verified-oss-loop/rollout.py get worker_base)` unless the issue names another base. Day-pass PRs target `feature_target`. Overnight PRs target `overnight_target`. +3. Search open issues and PRs. Stop on overlap. +4. Branch on the queue. One claim; 24h lease unless the project says otherwise. + - **Work** — open `claimable` and not `claimed`: claim one leaf. Prefer `priority:P0`, then `P1`, then `good-first-issue`. Skip `needs-discussion` unless a human assigned it. + - **Triage** — nothing `claimable`: do not code. If a `needs-discussion` issue exists: one-paragraph proposal on the newest; stop. If none: mint **exactly one** issue from the first untracked item in `ROADMAP.md`, else a failing unit command from `AGENTS.md`, else docs drift; label **`needs-discussion` only**; stop. Do not self-apply `claimable`. Do not rewrite `ROADMAP.md`. + - **Stop** — triage found nothing untracked, a live claim exists, a competing PR covers the scope, or secrets are required. +5. Orient (`skills/orient/SKILL.md`). Graph MCP if present; otherwise search and read. Do not run `gitnexus analyze`. +6. Fail, then pass (`skills/tdd/SKILL.md`). +7. Shrink to the smallest complete change (`skills/anti-slop/SKILL.md`). If pstack is already installed, `/poteto-mode` is allowed; still never merge (`skills/pstack/SKILL.md`). New skills go through `skills/dr-eggbot/SKILL.md`. +8. Run the unit command from `AGENTS.md`. Mutation only if it is not `n/a`. +9. Open a PR at `feature_target` (day) or `overnight_target` (overnight) with the evidence receipt. **Never merge `main` or `dev`.** Independent review bots are not merge. Do not merge preview/nightly yourself. + +## Stop + +- Triage found nothing untracked. +- A live claim newer than the lease exists. +- A competing PR already covers the scope. +- You would need secrets, pairing tokens, or maintainer credentials. +- Mutation is `n/a` and you were about to write a score anyway. + +If you stop, comment the blocker only if an issue thread exists. Do not open a consolation PR. diff --git a/skills/dr-eggbot/SKILL.md b/skills/dr-eggbot/SKILL.md new file mode 100644 index 0000000..fc226b0 --- /dev/null +++ b/skills/dr-eggbot/SKILL.md @@ -0,0 +1,34 @@ +# Dr eggbot + +Design **one** skill, bot, or routine. Do not copy the Grok marketplace bot. Dr Eggbot (Lauren Tan) is a Grok Bot that authors other bots to the poteto-mode bar. This file is the portable job for any harness. + +Keep a local marketplace install as `source: local` in `.verified-oss-loop/inventory.yml`. This kit skill stays kit-owned and will update on `oss-onboard`. + +## One job + +1. Name the job in one sentence. +2. List anti-jobs (what it must not do). Include: merge `main`, invent a second loop, vendor GitNexus, dump pstack. +3. Name proof: command or user-visible check. No proof → do not ship the skill. +4. One voice. Short. Unslopped (`skills/anti-slop/SKILL.md`). +5. Drop leftover tools. A skill that needs five MCP servers for one job is two skills. + +## Authoring a SKILL.md + +- Lean. Codex truncates large instruction files. +- Point at existing kit skills instead of pasting them. +- If pstack is installed, `/poteto-mode` playbook “authoring a skill” is allowed. Still never merge. + +## Healthcheck (fleet) + +When asked to audit skills/bots/routines: + +- Unused or duplicate skills → propose delete, do not delete until a human picks. +- Prompts that restated `AGENTS.md` → shrink. +- Token-waste: wide globs, packed trees every turn, four review bots. +- Quiet when there is nothing to propose. + +## Do not + +- Default to a shareable template that redefines this repo’s loop. +- Create a second `AGENTS.md` / `CLAUDE.md`. +- Install marketplace bots into an Apache-2.0 tree. diff --git a/skills/orient/SKILL.md b/skills/orient/SKILL.md new file mode 100644 index 0000000..830da27 --- /dev/null +++ b/skills/orient/SKILL.md @@ -0,0 +1,23 @@ +# Orient + +Find the existing shape before you edit. Do not rewrite from a generated wiki. + +## First 60 seconds + +1. Read root `AGENTS.md` and `CONTRIBUTING.md`. Search open issues and PRs. +2. If GitNexus MCP tools are already present: `query` the concept, `context` on the symbol, `impact` before the first edit. Do not run `gitnexus analyze` or `setup` unless a human asked. +3. Else if Serena (or another LSP MCP) is present: `find_symbol`, then `find_referencing_symbols`. +4. Else: `rg` the name, open the files, optionally `ast-grep` for a structural pattern. +5. For a third-party library API, use Context7 or upstream docs. Do not guess signatures from memory. +6. Name the blast radius in one sentence (who calls this, which tests, which sibling surfaces). Then edit. + +DeepWiki, cluster dumps, and packed trees (repomix/gitingest) are orientation only. They are not evidence. + +GitNexus is PolyForm Noncommercial — use it if the maintainer already installed it. Do not vendor it. Serena is the MIT LSP fallback. + +## Do not + +- Index the repo or rewrite `AGENTS.md` / `CLAUDE.md` as a side effect of a claim. +- Duplicate instruction files (`CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md`) when `AGENTS.md` already exists. +- Treat a wiki answer as `tests.green`. +- Invent a second loop named after a tool. diff --git a/skills/pstack/SKILL.md b/skills/pstack/SKILL.md new file mode 100644 index 0000000..957c18d --- /dev/null +++ b/skills/pstack/SKILL.md @@ -0,0 +1,26 @@ +# pstack + +Harness-neutral pointer to [pstack](https://github.com/cursor/plugins/tree/main/pstack) (Lauren Tan / poteto, MIT). The Cursor plugin is the full pack (`/poteto-mode`, `/how`, `/unslop`, `/setup-pstack`, …). This kit does **not** copy that tree. + +Child repos that already installed pstack keep it: extra `skills/how`, `.cursor/skills/*`, plugin settings are `local` in `.verified-oss-loop/inventory.yml` and are never overwritten. + +## If pstack is already installed + +Use it for rigor. Typical entry: `/poteto-mode` with a checkable outcome. Still follow this protocol: + +- One claim. Isolated branch. +- Fail, then pass (`skills/tdd/SKILL.md`). +- Smallest complete change (`skills/anti-slop/SKILL.md`). +- **Workers never merge.** pstack shipping/autopilot playbooks that land PRs are maintainer-only. They are not worker authority. + +Do not run `/setup-pstack` or `/add-plugin pstack` from an unattended claim unless a human asked. + +## If it is not installed + +Do not vendor 100+ plugin files into the repo. Fall back to this kit: orient → TDD → anti-slop → verify. Maintainers who want the plugin install it in Cursor (`/add-plugin pstack`). + +## Do not + +- Copy pstack playbooks that merge `main`. +- Duplicate `AGENTS.md` into a second instruction file because a playbook asked. +- Treat plugin presence as evidence on the receipt. diff --git a/skills/tdd/SKILL.md b/skills/tdd/SKILL.md new file mode 100644 index 0000000..269760e --- /dev/null +++ b/skills/tdd/SKILL.md @@ -0,0 +1,22 @@ +# TDD + +Fail, then pass. The red command is part of the evidence receipt. + +## Do + +1. Name the intended behavior vs the current behavior in one sentence each. +2. Write or extend a test that fails for the right reason. Run the unit command from `AGENTS.md`. +3. Record that command and the failure as `tests.red`. +4. Change production code until the same command passes. Record `tests.green`. +5. Optional sabotage: break one assertion, rerun, expect fail. That is `tests.sabotage`. +6. Do not delete the red proof to make the log look clean. + +## Stack + +The unit command is detected at onboard time (`npm test`). If that placeholder is still `unknown`, detect the tree (`pyproject.toml`, `package.json`, `Cargo.toml`, `go.mod`) and pin the command in the PR. Do not copy another project's test runner. + +## Do not + +- Start with the fix and add a test that could only pass. +- Call a linter a test. +- Skip red because the bug is "obvious". diff --git a/skills/verify/SKILL.md b/skills/verify/SKILL.md new file mode 100644 index 0000000..64af74e --- /dev/null +++ b/skills/verify/SKILL.md @@ -0,0 +1,22 @@ +# Verify + +Tests are necessary, not sufficient. Generation and verification are separate. + +## Pyramid + +1. **Unit** — `npm test` on the touched surface. +2. **TDD** — red command, then green command (`skills/tdd/SKILL.md`). +3. **Mutation** — `npx stryker run` if that is not `n/a`. A surviving mutant is a missing assertion. If the stack has no mutator, write `n/a`. Do not invent a score. +4. **Runtime** — `n/a — project-specific`. If the project has no runtime check, write `n/a` and say what you did not run. + +## Receipt + +Bind every result to `head_revision`. Tests from another SHA are not evidence. Fill `.github/PULL_REQUEST_TEMPLATE.md`. + +The implementer does not self-approve. Independent review is a different person or a frozen evaluator. Workers never merge. + +## Fail closed + +- Unknown mutation tool → `n/a`, not `80`. +- Runtime you did not exercise → list it under `limitations`. +- Secrets, tokens, `.env` → stop. From 592b975e0d29b18323eb95bf815779c8cbe3343b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 12 Sep 2026 20:55:57 +0000 Subject: [PATCH 6/8] Keep APEX campus labels readable on the CSS sketch. The centered title covered the middle row of VISION rooms. Campus and APEX sketches keep the stamp in the corner so the 3x3 program stays visible. Co-authored-by: veeman961 --- components/facility/SketchMap.tsx | 39 +++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/components/facility/SketchMap.tsx b/components/facility/SketchMap.tsx index 775aa80..abee422 100644 --- a/components/facility/SketchMap.tsx +++ b/components/facility/SketchMap.tsx @@ -214,22 +214,31 @@ export const SketchFallback: React.FC<{ setAnnotationMode={setAnnotationMode} /> -
-

{sketch.title}

-

{sketch.note}

- - CSS SKETCH · {reason} - - {annotationMode === 'MEASUREMENTS' && ( - - {sketch.variant === 'farm' - ? '500 m² / section (origin)' - : sketch.variant === 'apex' || sketch.variant === 'campus' - ? 'APEX cells inferred · not origin' - : 'envelope 140×120 m · inferred'} + {sketch.variant === 'apex' || sketch.variant === 'campus' ? ( +
+ + CSS SKETCH · {reason} - )} -
+ {annotationMode === 'MEASUREMENTS' && ( +
+ APEX cells inferred · not origin +
+ )} +
+ ) : ( +
+

{sketch.title}

+

{sketch.note}

+ + CSS SKETCH · {reason} + + {annotationMode === 'MEASUREMENTS' && ( + + {sketch.variant === 'farm' ? '500 m² / section (origin)' : 'envelope 140×120 m · inferred'} + + )} +
+ )} {visibleFeatures.length > 0 && (
{visibleFeatures.map((f) => ( From 64483726c84a2257f99e707cb8836304d3632deb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 20:31:48 +0000 Subject: [PATCH 7/8] Turn ACE Pages into a HomeBase pretotype landing. Lead with the pickleball facility OS that shipped in the private twin. Keep the Pascal campus as a nested sketch. Do not start a second GPU loop or leak private facility details. Co-authored-by: veeman961 --- AGENTS.md | 9 +- App.tsx | 260 ++------------------------------- README.md | 29 ++-- ROADMAP.md | 2 +- components/AIChat.tsx | 2 +- components/HomeBaseLanding.tsx | 188 ++++++++++++++++++++++++ components/NavBar.tsx | 18 +-- components/Specifications.tsx | 17 ++- index.html | 2 +- landing/check-copy.ts | 71 +++++++++ landing/public.ts | 92 ++++++++++++ metadata.json | 6 +- package.json | 5 +- services/geminiService.ts | 36 ++--- 14 files changed, 430 insertions(+), 307 deletions(-) create mode 100644 components/HomeBaseLanding.tsx create mode 100644 landing/check-copy.ts create mode 100644 landing/public.ts diff --git a/AGENTS.md b/AGENTS.md index 8c02d02..a74612a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ Commands were filled by `init-oss-repo.sh` / `oss-onboard` from the tree it saw, | Layer | Command | |---|---| -| Unit | `npm test` | +| Unit | `npm test` (`check:facility` + `check:landing`) | | Mutation | `n/a` — Stryker not adopted; do not invent a score | | Runtime | `npm run build` | @@ -56,7 +56,9 @@ Commands were filled by `init-oss-repo.sh` / `oss-onboard` from the tree it saw, ## ACE ownership -ACE is a **peak-performance pretotype** — a campus for human flourishing from all angles (scientists, athletes, labs, physiotherapy, all sports). That vision comes from `origin/enhance/3D` (APEX). Mine the **program**. Do not resurrect the Three.js lab theater, debug FPS HUD, OpenTwins-as-wired, 88% completion badges, or `4 × 500 = 2,000 m²` as origin fact. +ACE Pages is the **public pretotype landing for HomeBase** (indoor pickleball digital twin + facility OS). HomeBase (`kvnloo/homebase-pickleball`, private) owns the live GPU loop. This tree must not start a second live GPU loop, bundle that engine, or publish private facility floor plans. + +A nested **peak-performance campus pretotype** remains on the Map view: Pascal Site → LawnTech (Naperville racquet **SPEC**) + APEX wing (**VISION** program from `origin/enhance/3D`). Mine the **program**. Do not resurrect the Three.js lab theater, debug FPS HUD, OpenTwins-as-wired, 88% completion badges, or `4 × 500 = 2,000 m²` as origin fact. The Naperville racquet building is the origin **SPEC** nested inside that vision: @@ -69,8 +71,9 @@ APEX rooms in Pascal (`building_apex`) are **VISION** named zones with inferred ### This repo runs - Pascal Viewer (`@pascal-app/core` / `viewer` / `nodes` 1.0.0) plus a CSS court/campus fallback +- Public HomeBase landing copy (`landing/public.ts`) — pretotype, not the live twin - `facility/generateScene.ts` compiles Site → LawnTech (SPEC) + APEX wing (VISION) -- `npm test` → `facility/check-scene.ts` (levels, one grass lab, nine VISION zones, no inferred farm sections) +- `npm test` → `facility/check-scene.ts` + `landing/check-copy.ts` ### Do not (ACE-specific) diff --git a/App.tsx b/App.tsx index c9d06b7..494ed9c 100644 --- a/App.tsx +++ b/App.tsx @@ -6,19 +6,8 @@ import NavBar from './components/NavBar'; const PascalFacility = React.lazy(() => import('./components/PascalFacility')); import AIChat from './components/AIChat'; import Specifications from './components/Specifications'; -import { - Zap, - Cpu, - Sprout, - ArrowRight, - Layers, - Wind, - ShieldCheck, - ShoppingBag, - Brain, - Heart, - Activity -} from 'lucide-react'; +import HomeBaseLanding, { HomeBaseProduct } from './components/HomeBaseLanding'; +import { campusNested, product } from './landing/public.ts'; const App: React.FC = () => { const [currentView, setCurrentView] = useState(View.HOME); @@ -54,83 +43,7 @@ const App: React.FC = () => { variants={pageVariants} className="h-full overflow-y-auto custom-scrollbar pb-20" > - {/* Hero Section */} -
-
-
- -
- - - VISION campus · Naperville racquet SPEC · pretotype - - - - PEAK PERFORMANCE.
- EVERY ANGLE. -
- - - A campus for human flourishing — scientists, athletes, labs, physiotherapy, all sports. - Naperville racquet counts are the origin spec. APEX is the vision. This Pages site is a sketch, not a live twin. - - - - - - -
-
- - {/* Statistics Teaser */} -
-
- VISION -

Peak-performance campus

-

Scientists, athletes, labs, physiotherapy, all sports. APEX rooms are named program — not origin measurements, not a live clinic.

-
-
- SPEC -

24 tennis courts

-

Naperville origin: hard, clay, grass, and wood on the ground floor. Counts from the spec — not an even 6/6/6/6 split unless the spec says so.

-
-
- PLANNED -

Grass lab + APEX wing

-

Third floor grows modular turf: 500 m² per section. Section count unspecified. APEX cells are inferred. Fast swap is a goal, not a receipt.

-
-
+ )} @@ -167,10 +80,10 @@ const App: React.FC = () => { {/* HUD Layer */}
-

Campus sketch

+

{campusNested.title}

- VISION APEX wing · Naperville racquet SPEC · Pascal nodes
- Envelope and APEX cells inferred. HUD is MOCK. CSS sketch if WebGL is blank. + {campusNested.body}
+ HUD is MOCK. CSS sketch if WebGL is blank. Not the {product.name} live twin.

@@ -219,162 +132,7 @@ const App: React.FC = () => { variants={pageVariants} className="h-full overflow-y-auto custom-scrollbar pb-20 px-6" > -
-

Campus program

-

- Peak performance from every angle. APEX labs, physio, gym, and pool are VISION. - Naperville racquet counts are SPEC. This site does not run a facility. -

- -
- -
-
-
- -
-

APEX peak-performance wing

-

- From origin/enhance/3D: biometric, cognitive, movement, research, and nutrition rooms plus recovery / physiotherapy. - Named program identity — not 147 biomarkers, not a measured VO₂ panel, not a live twin. -

-
    -
  • Biometric lab — VISION
  • -
  • Cognitive + movement + research — VISION
  • -
  • Gym, pool, clubhouse — VISION (all sports)
  • -
-
-
-
- {Array.from({ length: 9 }).map((_, i) => ( -
- ))} -
-
- VISION · APEX CAMPUS -
-
-
- - {/* 1. Vertical Grass Lab */} -
-
-
- -
-

Level 3: Grass lab

-

- Origin spec: 500 m² per section on the third floor. How many sections is not specified — an earlier page inferred four (2,000 m²). Hydroponics and patch transport are PLANNED. Fast turf swap is a pretotype goal, not a measured 60-minute receipt. -

-
    -
  • 500 m² per section (origin)
  • -
  • Patch transport — PLANNED
  • -
  • Courts below inherit the crop, not a painted texture
  • -
-
-
-
-
- PLANNED · GRASS LAB -
-
-
- - {/* 2. The Racquet Ecosystem */} -
-
-
- -
-

Naperville racquet (origin SPEC)

-

- First site nested in the campus vision. Ground, L1, and L2 are the origin racquet program — not all-sports counts. - All-sports identity lives on the APEX wing as VISION until a spec names those rooms. -

-
-
- Ground Floor - 24 Tennis Courts (Hard, Clay, Grass, Wood) -
-
- First Floor - 16 Badminton, 4 Squash, 16 Table Tennis -
-
- Second Floor - 8 Pickleball Courts, 1 Real Tennis Court -
-
-
-
-
-
- MOCK · COURT SKETCH -
-
-
- - {/* 3. Ops overlay — PLANNED, not a live BMS */} -
-
-
- -
-

Ops overlay

-

- BMS, drones, and biometric doors are PLANNED theater on this pretotype. This Pages site does not run a facility. The court is the product; ops chrome stays thin. -

-
-
- -
-

Climate loop

-

PLANNED — solar + HVAC language

-
-
-
- -
-

Access overlay

-

MOCK — not a wired door stack

-
-
-
-
-
-
-
- PLANNED · OPS OVERLAY -
-
-
- - {/* 4. Pro shop — origin; recovery chrome is MOCK */} -
-
-
- -
-

Pro shop

-

- Origin spec includes a ground-floor pro shop and lockers. Sensor-managed bathrooms and automated emergency dispatch are MOCK. The court is the product; member chrome stays thin. -

-
    -
  • Pro shop — origin
  • -
  • Lockers — origin
  • -
  • App booking — MOCK
  • -
-
-
-
-
- ORIGIN · PRO SHOP -
-
-
- -
-
+ )} @@ -390,8 +148,8 @@ const App: React.FC = () => { >
-

Join the pretotype

-

Vision is a peak-performance campus. Origin spec is Naperville racquet. This form does not submit (MOCK).

+

Join {product.name}

+

Public pretotype landing for the indoor pickleball facility OS. This form does not submit (MOCK).

e.preventDefault()}> diff --git a/README.md b/README.md index 88540bd..67aea07 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,33 @@ -# ACE — peak-performance campus pretotype +# ACE — HomeBase public landing
-**A public pretotype of a peak-performance campus for human flourishing — scientists, athletes, labs, physiotherapy, all sports.** +**Public pretotype landing for HomeBase — indoor pickleball digital twin and facility OS.** [Live Pages (`main`)](https://kvnloo.github.io/ace/) • [Dev Preview](https://kvnloo.github.io/ace/dev/) • [Deployment](.github/DEPLOYMENT.md) -This Pages site is a **pretotype**, not a running BMS or live twin. Stamps in the UI say which claims are **VISION**, **SPEC**, **PLANNED**, or **MOCK**. GitHub Pages deploys **`main`** and **`dev` only** — there is no nightly or PR preview. Honesty work lives on feature branches until merge. +This Pages site is a **pretotype landing**, not the live GPU twin. HomeBase (private product) owns that loop. Stamps in the UI say which claims are **SHIPPED**, **LIVE** (elsewhere), **VISION**, **SPEC**, **PLANNED**, or **MOCK**. GitHub Pages deploys **`main`** and **`dev` only**.
--- -## Vision vs spec +## What this site is -The vision (from `origin/enhance/3D`, APEX) is a **peak-performance facility for all things pursuing human flourishing**: scientists, athletes, labs, physiotherapy, gym, pool, clubhouse, all sports — not tennis-only. +ACE Pages is the public face of **HomeBase**: pickleball rules engine, 3D facility twin, and Facility OS (booking, cleaning robots, ROI). Those systems shipped in the private repo. This site does **not** start a second 60Hz loop, bundle the engine, or wire the booking API. -The Naperville racquet building is the origin **SPEC** nested inside that vision. This repo compiles both into Pascal: LawnTech (SPEC) plus an APEX wing (VISION named rooms, inferred cells). +A nested **Pascal campus sketch** remains: Naperville racquet **SPEC** plus an APEX peak-performance wing (**VISION**). That sketch is not the live pickleball twin. | Stamp | What | |---|---| -| **VISION** | APEX labs (biometric, cognitive, movement, research, nutrition), recovery / physio, gym, pool, clubhouse. Not origin measurements. | -| **SPEC** | 24 tennis / 16 badminton / 4 squash / 16 table tennis / 8 pickleball / 1 real tennis. Grass lab: 500 m² per section, **section count unspecified**. | -| **PLANNED / MOCK** | Live BMS, drones, 60 FPS, Gemini as a product, 6/6/6/6 split, four farm racks, 60-minute turf-swap receipt. | -| **This repo runs** | Pascal Viewer + CSS fallback. Not a live facility. Homebase owns the live GPU loop. | +| **SHIPPED** | USAPA 2025 engine + Facility OS MVP in the private HomeBase product (demo/sim data). Not this bundle. | +| **LIVE** | GPU twin (players, ball, cameras, scoreboards) — private HomeBase app only. | +| **VISION** | APEX labs, recovery / physio, gym, pool, clubhouse on the nested campus sketch. | +| **SPEC** | Naperville origin racquet counts on the nested sketch. Grass lab: 500 m² per section, section count unspecified. | +| **PLANNED / MOCK** | Blender-native GFX, web GFX quality lanes, waitlist form, Gemini chat. | +| **This repo runs** | Landing copy + Pascal Viewer + CSS fallback. HomeBase owns the live GPU loop. | -Do not copy `enhance/3D` Three.js lab theater into this tree as the renderer. Mine the program; compile through Pascal. +Do not copy facility floor plans or private twin source into this tree. Do not resurrect the Three.js lab theater as the renderer. Mine campus program through Pascal. --- @@ -60,8 +62,9 @@ Origin spec: **500 m² per section**. Section count is not in the origin (an ear ## What this repo actually runs ### Pages demo (this tree) +- **HomeBase landing copy** (`landing/public.ts`) — pretotype; does not run the live twin - **React 19.2** + TypeScript -- **Pascal Viewer** (`@pascal-app/core` / `viewer` / `nodes` 1.0.0) — Site → LawnTech levels 0–3 + APEX VISION wing +- **Pascal Viewer** (`@pascal-app/core` / `viewer` / `nodes` 1.0.0) — nested campus sketch: Site → LawnTech levels 0–3 + APEX VISION wing - Envelope **140×120 m** and **10 m** storeys are **inferred**, not origin measurements - APEX rooms are **nine VISION zones** on inferred 30×30 m cells - Grass lab is **one PLANNED zone** (origin: 500 m²/section; section count unspecified) @@ -73,7 +76,7 @@ Origin spec: **500 m² per section**. Section count is not in the origin (an ear ### Proof ```bash -npm test # facility/check-scene.ts +npm test # facility/check-scene.ts + landing/check-copy.ts npm run build ``` diff --git a/ROADMAP.md b/ROADMAP.md index f856d7e..3384286 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ Workers mint **exactly one** `needs-discussion` issue from the first untracked i | **VISION** | Peak-performance campus for human flourishing from all angles: scientists, athletes, labs, physiotherapy, all sports. Program mined from `origin/enhance/3D` (APEX). Not origin measurements. | | **SPEC** | Naperville origin racquet counts: 24 tennis / 16 badminton / 4 squash / 16 table tennis / 8 pickleball / 1 real tennis. Grass lab: 500 m² per section, section count unspecified. | | **PLANNED / MOCK** | Live BMS, drones, 60 FPS biomechanics, Gemini concierge product, 6/6/6/6 split, four farm racks, 60-minute turf-swap receipt. | -| **This repo runs** | Pascal pretotype + CSS fallback. Not a live facility. Homebase owns the live GPU loop. | +| **This repo runs** | HomeBase public landing + Pascal pretotype + CSS fallback. Not a live facility. Homebase owns the live GPU loop. | ## Untracked (oldest first) diff --git a/components/AIChat.tsx b/components/AIChat.tsx index 1d76c5a..3ce4079 100644 --- a/components/AIChat.tsx +++ b/components/AIChat.tsx @@ -9,7 +9,7 @@ const AIChat: React.FC = () => { const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [messages, setMessages] = useState([ - { role: 'model', text: "This chat is a stub. Ask about the VISION campus (APEX labs, physio, all sports) or the Naperville racquet SPEC." } + { role: 'model', text: "This chat is a stub. HomeBase is the indoor pickleball twin (live loop is private). Ask about SHIPPED engine / Facility OS, or the nested ACE campus SPEC." } ]); const scrollRef = useRef(null); diff --git a/components/HomeBaseLanding.tsx b/components/HomeBaseLanding.tsx new file mode 100644 index 0000000..b28b27c --- /dev/null +++ b/components/HomeBaseLanding.tsx @@ -0,0 +1,188 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { ArrowRight, Cpu, Layers, Zap } from 'lucide-react'; +import { campusNested, inFlight, lanes, pillars, product } from '../landing/public.ts'; +import { View } from '../types'; + +type Props = { + onChangeView: (view: View) => void; +}; + +const stampClass: Record = { + LIVE: 'text-tennis-yellow', + SHIPPED: 'text-tennis-yellow/80', + PLANNED: 'text-white/50', + VISION: 'text-tennis-yellow', + SPEC: 'text-white/70', + MOCK: 'text-orange-300/80', + PRETOTYPE: 'text-tennis-yellow', +}; + +const HomeBaseLanding: React.FC = ({ onChangeView }) => { + return ( +
+
+
+
+ +
+ + + {product.kicker} + + + + {product.name} + + + + THE FACILITY,
+ RUNNING. +
+ + + {product.subhead} + + + + + + +
+
+ +
+ {pillars.map((pillar) => ( +
+ + {pillar.stamp} + +

{pillar.title}

+

{pillar.body}

+
+ ))} +
+ +
+

{product.thisSite}

+

{product.liveTwin}

+
+
+ ); +}; + +export const HomeBaseProduct: React.FC<{ onChangeView: (view: View) => void }> = ({ onChangeView }) => { + return ( +
+

{product.name}

+

{product.subhead}

+

+ {product.thisSite} {product.liveTwin} +

+ +
+ {lanes.map((lane) => ( +
+
+ {lane.group === 'Play' ? : lane.group === 'Ops' ? : } +
+

{lane.group}

+
    + {lane.items.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+ +
+ {inFlight.map((item) => ( +
+ {item.stamp} +

{item.title}

+

{item.body}

+
+ ))} +
+ +
+
+ {campusNested.stamp} +

{campusNested.title}

+

{campusNested.body}

+ +
+
+
+ {Array.from({ length: 9 }).map((_, i) => ( +
+ ))} +
+
+ + VISION · ACE CAMPUS + +
+
+
+
+ ); +}; + +export default HomeBaseLanding; diff --git a/components/NavBar.tsx b/components/NavBar.tsx index 875041b..d644228 100644 --- a/components/NavBar.tsx +++ b/components/NavBar.tsx @@ -32,17 +32,17 @@ const NavBar: React.FC = ({ currentView, onChangeView }) => { onClick={() => onChangeView(View.HOME)} > - LAWNTECH DYNAMICS + HOMEBASE ACE PRETOTYPE
{/* Desktop Nav */}
- + + - - - + +