Three years of informal structure formalized in one session — city, domain, and funnel stage are now validated, canonical, and load-bearing on all 80 AICV nodes.
The AICV node corpus operated on a ghost schema. Eighty nodes carried three structural axes — city, domain, and funnel stage — but only city was in Zod, and even that was free-text. Forty-seven nodes (59%) used non-canonical city values like "Valley Wide" and "adjacent-communities." Domain didn't exist as a schema field at all. Funnel stage lived in agent_intent, an unvalidated array completely outside the Astro content API. The corpus was well-written but not machine-queryable.
Two commits. The first (30a1bf5) added three validated fields to src/content.config.ts: city as an enum of 11 canonical geography values, domain as an enum of 13 life-domain values, and funnel_stages as a validated array requiring at least one of 6 canonical stages. That commit intentionally broke the build.
The second commit (8c27101) repaired it across all 80 nodes. A Python migration script applied a per-node lookup table — city normalization for 32 nodes, domain assignment for all 80, funnel stage arrays for all 80. The diff: 80 files changed, 192 insertions, 32 deletions. Zero errors. astro check passed clean. Both commits were pushed together so production never saw the broken state.
32 city normalizations: 26 "Valley Wide" → "Coachella Valley," 4 "adjacent-communities" → "Adjacent Communities," 2 hyphenated and lowercase variants corrected. Three axes are now formally enforced — geography (11 canonical values), life-domain (13 values), funnel stage (6 values: Discover, Visit, Return, Satellite, Relocate, Build). The corpus is a machine-queryable three-axis knowledge graph.
The static JSON layer needs a re-run of build-static-json.cjs to propagate the new fields into nodes.json and llms-full.txt. The legacy agent_intent array remains alongside funnel_stages — structural debt, not a blocker. The four truly-meta nodes (node-zero, coachella-valley-intelligence-index, luxury-corridor, wellness-positioning) carry funnel_stages: ["Discover"] only for now; a proper concepts collection refactor is parked in the queue.
Session opened with a quick sync: aicv-playbook STATE.md updated to 147 briefs, sunshine-fm MEMORY.md live counter updated to 345 entries.