@peripheries/engine 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +21 -2
  2. package/bin/engine.mjs +5 -0
  3. package/package.json +45 -4
  4. package/src/commands/bundle-size.mjs +4 -0
  5. package/src/commands/check-bundle-leak.mjs +2 -0
  6. package/src/commands/check-cutover-ownership.mjs +3 -0
  7. package/src/commands/check-design-refs.mjs +5 -0
  8. package/src/commands/check-fixtures.mjs +9 -0
  9. package/src/commands/check-props-schema.mjs +3 -0
  10. package/src/commands/check-skills.mjs +4 -0
  11. package/src/commands/check-template-routes.mjs +7 -0
  12. package/src/commands/check-wired-bindings.mjs +7 -0
  13. package/src/commands/check-work-orders.mjs +11 -0
  14. package/src/commands/check.mjs +5 -0
  15. package/src/commands/cutover.mjs +22 -0
  16. package/src/commands/deploy.mjs +5 -0
  17. package/src/commands/emit-contracts.mjs +12 -0
  18. package/src/commands/emit-knowledge.mjs +2 -0
  19. package/src/commands/externals.d.mts +2 -0
  20. package/src/commands/externals.mjs +1 -0
  21. package/src/commands/register-components.mjs +5 -0
  22. package/src/commands/upload-bundle.mjs +2 -0
  23. package/src/commands/validate-externals.mjs +4 -0
  24. package/src/index.d.mts +39 -0
  25. package/src/index.mjs +1 -0
  26. package/src/lib/citation-markers.mjs +1 -0
  27. package/src/lib/config.d.mts +1 -0
  28. package/src/lib/config.mjs +1 -0
  29. package/src/lib/contract-shape.mjs +1 -0
  30. package/src/lib/env.mjs +1 -0
  31. package/src/lib/floor.mjs +4 -0
  32. package/src/lib/jsdoc-parser.d.mts +8 -0
  33. package/src/lib/jsdoc-parser.mjs +1 -0
  34. package/src/lib/knowledge.d.mts +15 -0
  35. package/src/lib/knowledge.mjs +4 -0
  36. package/src/lib/repo-identity.mjs +4 -0
  37. package/src/lib/root.d.mts +1 -0
  38. package/src/lib/root.mjs +1 -0
  39. package/src/lib/run.mjs +1 -0
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # @peripheries/engine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Build, check, catalog knowledge and deploy for peripheries component repos:
4
+ one `engine` CLI, driven by `engine.config.ts` in the repo.
5
+
6
+ ```ts
7
+ import { defineConfig } from '@peripheries/engine'
8
+
9
+ export default defineConfig({
10
+ org: '<org-slug>',
11
+ lineages: ['main', { name: '<form-lineage>', kind: 'form' }, '<page-lineage>'],
12
+ })
13
+ ```
14
+
15
+ ```bash
16
+ engine deploy [--develop|--prod] [--lineage <name>] [--skip-build]
17
+ engine upload | register | cutover | knowledge | contracts
18
+ engine bundle-size | check-bundle-leak | validate-externals
19
+ ```
20
+
21
+ Credentials are read from the environment of the repo that runs it; the
22
+ package holds none.
package/bin/engine.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{COMMANDS,runCommand}from"../src/lib/run.mjs";const[command,...args]=process.argv.slice(2);if(!command||command==="--help"||command==="-h"||!(command in COMMANDS)){const known=Object.keys(COMMANDS).join(", ");if(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h")console.error(`[engine] unknown command "${command}"`);console.log(`usage: engine <command> [args]
3
+ commands: ${known}
4
+
5
+ engine deploy [--develop|--prod] [--lineage <name>] [--skip-build]`);process.exit(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h"?2:0)}process.exit(runCommand(command,args));
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@peripheries/engine",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Build, check, knowledge and deploy for peripheries component repos",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/peripheries-components/component-engine.git"
10
+ },
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "bin": {
15
+ "engine": "./bin/engine.mjs"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./src/index.d.mts",
20
+ "default": "./src/index.mjs"
21
+ },
22
+ "./knowledge": {
23
+ "types": "./src/lib/knowledge.d.mts",
24
+ "default": "./src/lib/knowledge.mjs"
25
+ },
26
+ "./jsdoc": {
27
+ "types": "./src/lib/jsdoc-parser.d.mts",
28
+ "default": "./src/lib/jsdoc-parser.mjs"
29
+ },
30
+ "./externals": {
31
+ "types": "./src/commands/externals.d.mts",
32
+ "default": "./src/commands/externals.mjs"
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@supabase/supabase-js": "2.112.0",
37
+ "semver": "7.8.5",
38
+ "tsx": "4.23.5"
39
+ },
40
+ "peerDependencies": {
41
+ "@peripheries/renderer": ">=0.6.2 <1",
42
+ "@peripheries/runtime": ">=0.6.2 <1"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,readFileSync,readdirSync,statSync,writeFileSync}from"node:fs";import{gzipSync}from"node:zlib";import{dirname,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const REPO_ROOT=REPO_ROOT_;const BASELINE=resolve(REPO_ROOT,"bundle-size.json");const argv=process.argv.slice(2);const UPDATE=argv.includes("--update");const CI=argv.includes("--ci");const WARN_GZ=10*1024;const DISCUSS_GZ=25*1024;function lineages(){const found=[];const push=(name,file)=>{if(existsSync(file))found.push({name,file:relative(REPO_ROOT,file)})};push("main",resolve(REPO_ROOT,"dist/index.js"));push("public",resolve(REPO_ROOT,"dist-public/index.js"));const bundles=resolve(REPO_ROOT,"dist-bundles");if(existsSync(bundles)){for(const entry of readdirSync(bundles)){push(entry,resolve(bundles,entry,"index.js"))}}return found}function measure({name,file}){const abs=resolve(REPO_ROOT,file);const bytes=statSync(abs).size;const gzip=gzipSync(readFileSync(abs),{level:9}).length;return{name,file,bytes,gzip}}const kb=n=>`${(n/1024).toFixed(1)}KB`;const delta=n=>n===0?"\xB10":n>0?`+${kb(n)}`:`\u2212${kb(-n)}`;const measured=lineages().map(measure);if(measured.length===0){console.log("[size] no built bundle found \u2014 run `npm run build` first. Not a failure.");process.exit(0)}const baseline=existsSync(BASELINE)?JSON.parse(readFileSync(BASELINE,"utf8")):null;const prior=new Map((baseline?.lineages??[]).map(l=>[l.name,l]));if(UPDATE){writeFileSync(BASELINE,JSON.stringify({_comment:"Bundle-size baseline. Re-recorded deliberately with `npm run size -- --update`, ideally in the same commit as the growth it accounts for. Reported by CI, never gated.",recorded:new Date().toISOString().slice(0,10),lineages:measured.map(({name,file,bytes,gzip})=>({name,file,bytes,gzip}))},null,2)+"\n","utf8");console.log(`[size] baseline re-recorded for ${measured.length} lineage(s)`)}const lines=[];let worstGz=0;for(const m of measured){const was=prior.get(m.name);if(!was){lines.push(` ${m.name.padEnd(14)} ${kb(m.gzip).padStart(8)} gz ${kb(m.bytes).padStart(9)} raw (new lineage \u2014 no baseline)`);continue}const dGz=m.gzip-was.gzip;const dRaw=m.bytes-was.bytes;worstGz=Math.max(worstGz,dGz);lines.push(` ${m.name.padEnd(14)} ${kb(m.gzip).padStart(8)} gz ${kb(m.bytes).padStart(9)} raw ${delta(dGz).padStart(9)} gz ${delta(dRaw).padStart(9)} raw`)}const heading=UPDATE?baseline?` \u2014 deltas vs the previous baseline (${baseline.recorded}), now re-recorded`:" \u2014 first baseline recorded, nothing to compare against yet":baseline?` vs baseline ${baseline.recorded}`:" \u2014 no baseline recorded yet";console.log(`[size] ${measured.length} lineage(s)${heading}`);for(const line of lines)console.log(line);if(!UPDATE&&baseline){if(worstGz>=DISCUSS_GZ){const msg=`bundle grew ${delta(worstGz)} gzipped since ${baseline.recorded} \u2014 worth a strategy-table row explaining it (\xA74.3)`;console.log(CI?`::warning::${msg}`:`
3
+ [size] DISCUSS \u2014 ${msg}`)}else if(worstGz>=WARN_GZ){const msg=`bundle grew ${delta(worstGz)} gzipped since ${baseline.recorded}`;console.log(CI?`::warning::${msg}`:`
4
+ [size] ${msg}`)}}if(CI&&process.env.GITHUB_STEP_SUMMARY){const{appendFileSync}=await import("node:fs");const rows=measured.map(m=>{const was=prior.get(m.name);return`| ${m.name} | ${kb(m.gzip)} | ${kb(m.bytes)} | ${was?delta(m.gzip-was.gzip):"\u2014"} |`});appendFileSync(process.env.GITHUB_STEP_SUMMARY,[`### Bundle size${baseline?` (baseline ${baseline.recorded})`:""}`,"","| lineage | gzip | raw | \u0394 gzip |","|---|---|---|---|",...rows,""].join("\n"))}process.exit(0);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,readFileSync,readdirSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const REQUIRE_BUILD=process.argv.includes("--require-build");const SENTINELS=[{needle:"VENDORED FROM THE HOST",what:"the host code kept in src/harness/host-compat"},{needle:"data-missing-component",what:"@peripheries/renderer (the page renderer)"},{needle:"[harness]",what:"harness render code"},{needle:"onUnresolvedReference",what:"the harness page renderer"}];function bundles(){const out=[];const main=resolve(repoRoot,"dist/index.js");if(existsSync(main))out.push(main);const bundleRoot=resolve(repoRoot,"dist-bundles");if(existsSync(bundleRoot)){for(const slug of readdirSync(bundleRoot)){const file=join(bundleRoot,slug,"index.js");if(existsSync(file))out.push(file)}}return out}const files=bundles();if(files.length===0){const msg="[check-bundle-leak] no built bundle found (dist/index.js, dist-bundles/*/index.js).";if(REQUIRE_BUILD){console.error(`${msg} Run \`npm run build\` first.`);process.exit(1)}console.warn(`${msg} Skipping (pass --require-build to make this fatal).`);process.exit(0)}let failed=false;for(const file of files){const source=readFileSync(file,"utf8");const hits=SENTINELS.filter(s=>source.includes(s.needle));const rel=relative(repoRoot,file);if(hits.length===0){console.log(`[check-bundle-leak] OK \u2014 ${rel} (${(source.length/1024).toFixed(0)} KB)`);continue}failed=true;console.error(`[check-bundle-leak] LEAK in ${rel}:`);for(const hit of hits){console.error(` \xB7 found "${hit.needle}" \u2014 ${hit.what} is in the shipped bundle`)}}if(failed){console.error('\n Something under src/components/ or a src/index*.ts barrel reached into\n src/harness/ or test/. Find it with:\n npx eslint "src/**/*.{ts,tsx}" (the no-restricted-imports guard)\n and move whatever is shared into src/components/_shared/ instead.');process.exit(1)}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import{readFileSync,readdirSync,statSync}from"node:fs";import{dirname,extname,join,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{assertNonVacuous}from"../lib/floor.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const REPO_ROOT=REPO_ROOT_;const ROOTS=["template","scripts","src",".claude/skills",".claude/commands"];const EXTENSIONS=new Set([".ts",".tsx",".mjs",".js",".md",".json"]);const SKIP_DIRS=new Set(["node_modules","dist","dist-bundles","__output__","__generated__"]);const SELF="scripts/check-cutover-ownership.mjs";const BANNED=[/run\s+(the\s+)?cutover\s+(from|in|out\s+of)\s+the\s+host/i,/cutover\s+(is\s+)?(run\s+)?(from|by)\s+the\s+host\s+repo/i,/use\s+the\s+host'?s?\s+(scripts\/)?cutover\.ts/i,/run\s+the\s+host'?s?\s+(own\s+)?(scripts\/)?cutover(\.ts)?/i,/promote\s+.{0,30}\bfrom\s+the\s+host\s+repo/i,/fix\s+(it\s+)?with\s+the\s+host'?s?\s+(scripts\/)?cutover\.ts/i,/cutover\s+(runs|lives|belongs)\s+(in|from)\s+the\s+host/i,/cutover\s+is\s+never\s+run\s+from\s+this\s+repo/i,/(any\s+)?leftover\s+cutover\.mjs\s+.{0,40}\bdefect/i];const EXCUSED=[/\b(do|does|must|should|will|can)\s*n[o']?t\b/i,/\bnever\s+(run\s+)?(use\s+)?the\s+host/i,/\brefus(e|es|ed|ing)\b/i,/\b(used\s+to|no\s+longer|until|before|was\s+moved|moved\s+back|historically|formerly|pre-move)\b/i,/\bidentical\s+(to|semantics)\b/i,/\bported\s+(from|byte-for-byte)\b/i,/\bstale\b/i,/\bdisregard\b/i,/\bREPO_OWNED_CUTOVER\b/];function walk(dir,out=[]){let entries;try{entries=readdirSync(dir,{withFileTypes:true})}catch{return out}for(const e of entries){if(e.name.startsWith(".")&&e.name!==".claude"){if(!dir.includes(".claude"))continue}const full=join(dir,e.name);if(e.isDirectory()){if(SKIP_DIRS.has(e.name))continue;walk(full,out)}else if(EXTENSIONS.has(extname(e.name))){out.push(full)}}return out}const files=ROOTS.flatMap(r=>{const abs=resolve(REPO_ROOT,r);try{return statSync(abs).isDirectory()?walk(abs):[abs]}catch{return[]}});const violations=[];let excusedCount=0;let scanned=0;for(const file of files){const where=relative(REPO_ROOT,file);if(where===SELF)continue;scanned++;const lines=readFileSync(file,"utf8").split("\n");lines.forEach((line,i)=>{if(!BANNED.some(re=>re.test(line)))return;const clauses=line.split(/\s+—\s+|\s+--\s+|[;.]\s+|\)\s+/);const guilty=clauses.filter(c=>BANNED.some(re=>re.test(c)));if(guilty.length>0&&guilty.every(c=>EXCUSED.some(re=>re.test(c)))){excusedCount++;return}if(guilty.length===0&&EXCUSED.some(re=>re.test(line))){excusedCount++;return}violations.push({where,line:i+1,text:line.trim()})})}if(violations.length>0){console.error(`[cutover-ownership] ${violations.length} place(s) tell a reader to run cutover from the host:
3
+ `);for(const v of violations){console.error(` ${v.where}:${v.line}`);console.error(` ${v.text.slice(0,160)}`)}console.error("");console.error(" Cutover runs from this repo, through the engine. The host has no");console.error(" cutover of its own to run, so an instruction to use one is not merely");console.error(" stale: following it fails, or writes the same rows a second way.");console.error("");console.error(" engine cutover --version <v> --develop");console.error("");console.error(" To NAME the host's script in order to forbid it, compare against it,");console.error(' or describe what it used to do, say so on the same line ("do not",');console.error(' "never", "refuses", "used to", "ported from"). Recording history is');console.error(" fine; instructing the reader to go and do it is not.");process.exit(1)}assertNonVacuous("cutover-ownership",{files:{count:scanned,unit:"operative file(s)"}});console.log(`[cutover-ownership] ok \u2014 ${scanned} operative file(s) scanned; ${excusedCount} line(s) name the host's script to forbid, compare or historicise it. docs/plans/** deliberately not scanned (records, not instructions).`);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,readFileSync,readdirSync,statSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{assertNonVacuous}from"../lib/floor.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const REPO_ROOT=REPO_ROOT_;const DESIGN_EXT=String.raw`(?:dc\.html|html|md|png|jpg|jpeg|svg|pdf|fig)`;const CITATION=new RegExp(String.raw`(?<![A-Za-z0-9._-])(?:[A-Za-z0-9._-]+/)*design/[A-Za-z0-9 ()._/-]+?\.${DESIGN_EXT}`,"g");const DIR_CITATION=/(?<![A-Za-z0-9._-])design\/[A-Za-z0-9._-]+(?![A-Za-z0-9 ._/-])/g;const NON_DURABLE=[/^\.scratch\//,/^node_modules\//,/^dist/];const SOURCE_DIRS=["src"];const SOURCE_EXT=/\.(ts|tsx|mjs|js|md)$/;function walk(dir,out=[]){for(const entry of readdirSync(dir)){if(entry==="node_modules"||entry.startsWith("."))continue;const full=join(dir,entry);if(statSync(full).isDirectory())walk(full,out);else if(SOURCE_EXT.test(entry))out.push(full)}return out}function templateFiles(){const roots=["src/org_templates","src/project_templates"].map(d=>resolve(REPO_ROOT,d));const found=[];for(const root of roots){if(!existsSync(root))continue;for(const entry of readdirSync(root)){const file=join(root,entry,"template.json");if(existsSync(file))found.push(file);const pagesDir=join(root,entry,"pages");if(!existsSync(pagesDir))continue;for(const slug of readdirSync(pagesDir)){const pageFile=join(pagesDir,slug,"page.json");if(existsSync(pageFile))found.push(pageFile)}}}return found}const findings=[];function checkDirectory(refPath,where){const abs=resolve(REPO_ROOT,refPath);if(existsSync(abs)&&statSync(abs).isDirectory())return;findings.push({where,ref:refPath,why:existsSync(abs)?"is cited as an initiative directory but is a file":"does not exist in this repo"})}function check(refPath,where){const nonDurable=NON_DURABLE.find(re=>re.test(refPath));if(nonDurable){findings.push({where,ref:refPath,why:"points into a gitignored / generated location \u2014 it exists on at most one machine"});return}if(!existsSync(resolve(REPO_ROOT,refPath))){findings.push({where,ref:refPath,why:"does not exist in this repo"})}}for(const file of templateFiles()){const where=relative(REPO_ROOT,file);let json;try{json=JSON.parse(readFileSync(file,"utf8"))}catch(err){console.error(`[design-refs] could not parse ${where}: ${err.message}`);process.exit(1)}for(const ref of json?.metadata?.design_refs??[])check(ref,`${where} \u2192 metadata.design_refs`)}let cited=0;for(const dir of SOURCE_DIRS){const root=resolve(REPO_ROOT,dir);if(!existsSync(root))continue;for(const file of walk(root)){const where=relative(REPO_ROOT,file);if(where==="scripts/check-design-refs.mjs")continue;const raw=readFileSync(file,"utf8");const joined=raw.replace(/\/\n\s*(?:\/\/|\*)[ \t]?/g,"/").replace(/\n\s*(?:\/\/|\*)[ \t]?/g," ").replace(/[ \t]{2,}/g," ");const text=`${raw}
3
+ ${joined}`;const seen=new Set;for(const match of text.matchAll(CITATION)){const ref=match[0].replace(/[ .]+$/,"");if(seen.has(ref))continue;seen.add(ref);cited++;check(ref,where)}for(const match of text.matchAll(DIR_CITATION)){const ref=match[0];if(seen.has(ref))continue;if([...seen].some(f=>f.startsWith(`${ref}/`)))continue;seen.add(ref);cited++;checkDirectory(ref,where)}}}if(findings.length>0){console.error(`[design-refs] ${findings.length} dangling design reference(s):
4
+ `);for(const f of findings){console.error(` ${f.where}`);console.error(` "${f.ref}"`);console.error(` ${f.why}
5
+ `)}console.error("Fix by pointing at the in-repo source under design/<initiative>/, or by removing the\nreference if no durable source exists. Do not point at .scratch/ \u2014 that is how the\nswiss design-system document was lost.");process.exit(1)}assertNonVacuous("design-refs",{citations:{count:cited,unit:"source citation(s)"}});console.log(`[design-refs] ok \u2014 ${cited} source citation(s) and every template design_ref resolve in-repo.`);
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env tsx
2
+ import{existsSync,readFileSync,readdirSync,statSync,writeFileSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toRepoRelative}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const REPO_ROOT=REPO_ROOT_;const argv=process.argv.slice(2);const STRICT=argv.includes("--strict");const UPDATE_BASELINE=argv.includes("--update-baseline");const BASELINE_PATH=resolve(REPO_ROOT,"scripts/__baselines__/fixtures.json");function readBaseline(){if(!existsSync(BASELINE_PATH))return{classes:{}};return JSON.parse(readFileSync(BASELINE_PATH,"utf8"))}function barrels(){const out=["src/index.ts"];for(const f of readdirSync(join(REPO_ROOT,"src"))){if(/^index\.[a-z0-9-]+\.ts$/.test(f)&&!f.endsWith(".test.ts"))out.push(`src/${f}`)}return out}function walk(dir,pred,out=[]){if(!existsSync(dir))return out;for(const e of readdirSync(dir)){const full=join(dir,e);if(statSync(full).isDirectory())walk(full,pred,out);else if(pred(full))out.push(full)}return out}function storeDeclaringComponents(){const found=new Set;const pages=["src/project_templates","src/org_templates"].flatMap(r=>walk(join(REPO_ROOT,r),f=>f.endsWith("page.json")));for(const file of pages){let json;try{json=JSON.parse(readFileSync(file,"utf8"))}catch{continue}const cells=json?.content?.cells??[];for(const cell of cells){if(!(cell?.selections||cell?.viewState||cell?.live))continue;if(typeof cell.component==="string")found.add(cell.component)}}return found}const NARROW_PX=480;const isLoading=s=>s.status?.isLoading===true;const isError=s=>s.status?.isError===true||Boolean(s.status?.errors);const isNarrow=s=>typeof s.viewport?.width==="number"&&s.viewport.width<=NARROW_PX;const declaresStore=s=>Boolean(s.selections||s.viewState||s.live||s.storeFixture);function arrayProp(s,name){const v=s.props?.[name]??s.data?.[name];return Array.isArray(v)?v:void 0}async function coveredElsewhere(){const out=new Map;for(const file of walk(join(REPO_ROOT,"src/components"),f=>/\.fixtures\.tsx?$/.test(f))){let mod;try{mod=await import(file)}catch{continue}for(const name of mod.covers??[])out.set(name,toRepoRelative(REPO_ROOT,file))}return out}const storeComponents=storeDeclaringComponents();const covered=await coveredElsewhere();const findings=[];const byClass=new Map;const seen=new Set;let components=0;function record(cls,ok,component,message){const bucket=byClass.get(cls)??{checked:0,failed:0};bucket.checked++;if(!ok){bucket.failed++;findings.push({component,cls,message})}byClass.set(cls,bucket)}for(const entry of barrels()){for(const{name,source}of readExportsFromIndex(REPO_ROOT,entry)){if(seen.has(name))continue;const filePath=resolveComponentFilePath(REPO_ROOT,source);if(!filePath||!hasComponentJSDoc(filePath,name))continue;seen.add(name);components++;const base=filePath.replace(/\.(tsx?|jsx?)$/,"");const byExport=join(dirname(filePath),name);const fixtureFile=[`${base}.fixtures.tsx`,`${base}.fixtures.ts`,`${byExport}.fixtures.tsx`,`${byExport}.fixtures.ts`].find(existsSync);if(!fixtureFile&&covered.has(name)){record("exists",true,name,"");continue}record("exists",Boolean(fixtureFile),name,`no fixtures file beside ${toRepoRelative(REPO_ROOT,filePath)}`);if(!fixtureFile)continue;let scenarios=[];try{const mod=await import(fixtureFile);scenarios=mod.scenarios??mod.default?.scenarios??[]}catch(err){const why=err instanceof Error?err.message:String(err);record("exists",false,name,`fixtures module failed to import: ${why.slice(0,120)}`);continue}record("exists",scenarios.length>0,name,"fixtures module declares no scenarios");const first=scenarios[0];if(!first)continue;record("intent",Boolean(first.designIntent),name,`default scenario "${first.name}" has no designIntent \u2014 the eval step skips it silently`);record("narrow",scenarios.some(isNarrow),name,`no scenario under ${NARROW_PX}px \u2014 reflow is unproven`);const jsdoc=parseJSDocFile(filePath,name)??{};const props=jsdoc.props_schema?.properties??{};const propNames=Object.keys(props);const dataBearing=Boolean(jsdoc.props_schema?.data)||propNames.includes("data")||propNames.includes("status");if(dataBearing){record("loading",scenarios.some(isLoading),name,"declares host data but no loading scenario");record("error",scenarios.some(isError),name,"declares host data but no error scenario")}const required=jsdoc.props_schema?.required??[];const listProps=propNames.filter(p=>props[p]?.type==="array"&&required.includes(p));for(const p of listProps){const values=scenarios.map(s=>arrayProp(s,p)).filter(Boolean);if(values.length===0)continue;record("empty",values.some(v=>v.length===0),name,`array prop "${p}" is never empty in any scenario`);record("dense",values.some(v=>v.length>=5),name,`array prop "${p}" never carries 5+ items \u2014 density is unproven`)}if(storeComponents.has(name)){record("store",scenarios.some(declaresStore),name,"a page.json cell declares store channels for this component, but no scenario does")}}}const order=["exists","intent","narrow","loading","error","empty","dense","store"];const baseline=readBaseline();const observed={};for(const cls of order)observed[cls]=byClass.get(cls)?.failed??0;if(UPDATE_BASELINE){const raised=order.filter(c=>observed[c]>(baseline.classes[c]??0));writeFileSync(BASELINE_PATH,JSON.stringify({_comment:"Per-class fixture-coverage failure counts. A FLOOR TO BE LOWERED, never raised \u2014 exceeding one fails `npm run check:fixtures`. Regenerate with `npm run check:fixtures -- --update-baseline` only when a number goes DOWN. See the header of scripts/check-fixtures.mts.",classes:observed},null,2)+"\n","utf8");console.log(`[fixtures] baseline written to ${relative(REPO_ROOT,BASELINE_PATH)}`);if(raised.length>0){console.warn(`
3
+ [fixtures] WARNING \u2014 ${raised.length} class(es) went UP and were recorded anyway:
4
+ `+raised.map(c=>` ${c}: ${baseline.classes[c]??0} -> ${observed[c]}`).join("\n")+"\n\n A floor is meant to be lowered. Raising one records a regression as\n acceptable, which is exactly what this file exists to prevent. If that is\n deliberate, say why in the commit message.")}process.exit(0)}console.log(`[fixtures] ${components} components
5
+ `);const regressions=[];for(const cls of order){const b=byClass.get(cls);if(!b)continue;const floor=baseline.classes[cls]??0;const now=observed[cls];if(STRICT?now>0:now>floor)regressions.push({cls,was:floor,now});const verdict=now===0?"ok":now>floor?`${now} (floor ${floor}) OVER`:`${now}/${floor}`;console.log(` ${cls.padEnd(8)} ${String(b.checked-now).padStart(4)}/${String(b.checked).padEnd(4)} ${verdict}`)}function print(list,label){if(list.length===0)return;console.log(`
6
+ ${label}:`);for(const f of list)console.log(` ${f.component} [${f.cls}] \u2014 ${f.message}`)}if(regressions.length>0){const worse=new Set(regressions.map(r=>r.cls));print(findings.filter(f=>worse.has(f.cls)),"Gaps in the class(es) that regressed");console.error(`
7
+ [fixtures] ${regressions.length} class(es) above their baseline floor:
8
+ `+regressions.map(r=>` ${r.cls}: floor ${r.was}, now ${r.now}`).join("\n")+"\n\n Add the missing scenario, or \u2014 if the count is genuinely correct and\n higher \u2014 run `npm run check:fixtures -- --update-baseline` and say why.\n A floor is meant to be lowered.");process.exit(1)}print(findings,"Open gaps (at or under the floor \u2014 not failing, but this is the backlog)");const total=Object.values(observed).reduce((a,b)=>a+b,0);console.log(`
9
+ [fixtures] ok \u2014 no class above its baseline floor`+(total?`; ${total} gap(s) remain in the backlog above.`:"; every class is clean."));
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ import ts from"typescript";import{resolve}from"node:path";import{readFileSync}from"node:fs";import{readExportsFromIndex,resolveComponentFilePath,hasComponentJSDoc,parseJSDocFile}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const BARRELS=["src/index.ts","src/index.block-plans.ts","src/index.workbooks.ts"];const BLOCKING_FAMILIES=new Set(["core/document"]);const BLOCKING_NAMES=new Set(["Stat","StatGroup","KpiCardGrid","StatStrip","HighlightBox","DataTable","SectionHeader","DisclaimerBlock","MilestoneTimeline","ReportHeadlineStats","LineChart","AreaChart","BarChart","BarLineChart","Sparkline","PieChart","DonutChart","ScatterChart","MetricChart","WaterfallChart"]);const BLOCKING_FLOOR=45;const INTERNAL_PROPS=new Map([["profile","DocumentProfile"]]);const PROPLESS=new Set(["PageBreak"]);const HOST_INJECTED=new Set(["baseProps","data","status","actions","isLoading","error","className","style","key","ref"]);const showAll=process.argv.includes("--all");const configPath=resolve(repoRoot,"tsconfig.json");const config=ts.readConfigFile(configPath,ts.sys.readFile);const parsed=ts.parseJsonConfigFileContent(config.config,ts.sys,repoRoot);const program=ts.createProgram({rootNames:BARRELS.map(b=>resolve(repoRoot,b)),options:parsed.options});const checker=program.getTypeChecker();function readBlock(filePath,name){const content=readFileSync(filePath,"utf8");const block=(content.match(/\/\*\*[\s\S]*?\*\//g)??[]).find(b=>new RegExp(`@component\\s+${name}\\b`).test(b));const declared=new Set;if(!block)return{declared,exampleTags:0};for(const m of block.matchAll(/@props\s+\{[^}]+\}\s+(?:\[([^=\]]+)(?:=[^\]]+)?\]|(\w+(?:\.\w+)*))\s*-/g)){declared.add((m[1]??m[2]??"").trim().split(".")[0]??"")}return{declared,exampleTags:(block.match(/^\s*\*?\s*@example\b/gm)??[]).length}}function propsTypeOf(symbol,at){const target=symbol.flags&ts.SymbolFlags.Alias?checker.getAliasedSymbol(symbol):symbol;const type=checker.getTypeOfSymbolAtLocation(target,at);const sig=type.getCallSignatures()[0];const param=sig?.getParameters()[0];if(!param)return null;return checker.getTypeOfSymbolAtLocation(param,at)}function ownProps(type){const out=new Set;const members=type.isUnion()?type.types:[type];for(const member of members){for(const prop of checker.getPropertiesOfType(member)){const decls=prop.getDeclarations()??[];if(decls.length===0)continue;if(decls.every(d=>d.getSourceFile().fileName.includes("/node_modules/")))continue;if(HOST_INJECTED.has(prop.name))continue;const internalType=INTERNAL_PROPS.get(prop.name);if(internalType&&checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbol(prop)))===internalType)continue;out.add(prop.name)}}return out}function elementOf(type){const t=checker.getNonNullableType(type);if(checker.isArrayType(t))return checker.getTypeArguments(t)[0]??null;return null}function shapeFindings(schema,type,path,out){const items=schema?.type==="array"?schema.items:void 0;const element=elementOf(type);if(!items?.properties||!element)return;const fields=new Map(checker.getPropertiesOfType(checker.getNonNullableType(element)).map(p=>[p.name,p]));for(const name of fields.keys())if(!(name in items.properties)&&!HOST_INJECTED.has(name))out.push({code:"SHAPE_UNDECLARED",prop:`${path}[].${name}`});for(const[name,sub]of Object.entries(items.properties)){const field=fields.get(name);if(!field){if(items.additionalProperties!==true)out.push({code:"SHAPE_PHANTOM",prop:`${path}[].${name}`});continue}shapeFindings(sub,checker.getTypeOfSymbol(field),`${path}[].${name}`,out)}}const results=[];const seen=new Set;for(const barrel of BARRELS){const barrelFile=program.getSourceFile(resolve(repoRoot,barrel));if(!barrelFile)continue;const moduleSymbol=checker.getSymbolAtLocation(barrelFile);if(!moduleSymbol)continue;const exportsByName=new Map(checker.getExportsOfModule(moduleSymbol).map(s=>[s.name,s]));for(const{name,source}of readExportsFromIndex(repoRoot,barrel)){if(seen.has(name))continue;const filePath=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!filePath||!hasComponentJSDoc(filePath,name))continue;seen.add(name);const jsdoc=parseJSDocFile(filePath,name);const family=jsdoc.family??jsdoc.component_type??"?";const blocking=BLOCKING_FAMILIES.has(family)||BLOCKING_NAMES.has(name);const file=filePath.slice(repoRoot.length+1);const symbol=exportsByName.get(name);const type=symbol?propsTypeOf(symbol,barrelFile):null;if(!type){results.push({name,file,family,blocking,own:0,findings:[{code:"UNRESOLVED"}]});continue}const own=ownProps(type);const{declared,exampleTags}=readBlock(filePath,name);const examples=jsdoc.usage_examples??[];const findings=[];if(examples.length<exampleTags)findings.push({code:"EXAMPLE_UNPARSEABLE",prop:`${exampleTags-examples.length} of ${exampleTags}`});for(const key of new Set(examples.flatMap(e=>Object.keys(e)))){if(!own.has(key)&&!declared.has(key))findings.push({code:"EXAMPLE_UNKNOWN_PROP",prop:key})}if(blocking&&!jsdoc.agent_instructions)findings.push({code:"NO_INSTRUCTIONS"});if(blocking&&exampleTags===0)findings.push({code:"NO_EXAMPLE"});for(const p of own)if(!declared.has(p))findings.push({code:"UNDECLARED",prop:p});for(const p of declared)if(!own.has(p)&&!HOST_INJECTED.has(p))findings.push({code:"PHANTOM",prop:p});for(const[prop,schema]of Object.entries(jsdoc.props_schema?.properties??{})){const member=checker.getPropertyOfType(type,prop);if(member)shapeFindings(schema,checker.getTypeOfSymbol(member),prop,findings)}results.push({name,file,family,blocking,own:own.size,findings})}}const blockingSet=results.filter(r=>r.blocking);const failures=blockingSet.filter(r=>r.findings.length>0||r.own===0&&!PROPLESS.has(r.name));const reported=results.filter(r=>!r.blocking&&r.findings.length>0);function print(r){const detail=r.own===0&&r.findings.length===0?"no own props resolved (a component with none is a broken check, not a clean one)":r.findings.map(f=>f.prop?`${f.code} ${f.prop}`:f.code).join(", ");console.log(` ${r.name} (${r.family}) ${r.file}
3
+ ${detail}`)}if(failures.length){console.log(`[props-schema] \u2717 ${failures.length} blocking component(s) whose @props disagree with their props type:`);failures.forEach(print)}if(showAll&&reported.length){console.log(`[props-schema] non-blocking (${reported.length}):`);reported.forEach(print)}const missingNames=[...BLOCKING_NAMES].filter(n=>!results.some(r=>r.name===n));if(missingNames.length){console.log(`[props-schema] \u2717 BLOCKING_NAMES not found in any barrel: ${missingNames.join(", ")}`)}if(blockingSet.length<BLOCKING_FLOOR){console.log(`[props-schema] \u2717 only ${blockingSet.length} blocking component(s) checked; floor is ${BLOCKING_FLOOR}. The resolver or the scope broke.`)}const failed=failures.length>0||missingNames.length>0||blockingSet.length<BLOCKING_FLOOR;console.log(`[props-schema] ${failed?"\u2717":"ok"} \u2014 ${blockingSet.length} blocking checked, ${failures.length} failing; ${reported.length} of ${results.length-blockingSet.length} others disagree (reported${showAll?"":", --all to list"}).`);process.exit(failed?1:0);
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,readFileSync,readdirSync,statSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{FOREIGN,HISTORICAL,PLACEHOLDER,markerWindow}from"../lib/citation-markers.mjs";import{declBody,topLevelFields,topLevelKeys}from"../lib/contract-shape.mjs";import{assertNonVacuous}from"../lib/floor.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const REPO_ROOT=REPO_ROOT_;const DOC_ROOTS=[".claude/skills",".claude/commands"];const DOC_FILES=[".claude/CLAUDE.md"];const ROOTS=String.raw`src|scripts|template|docs|test|design|outlines|\.claude|\.github`;const PATH_RE=new RegExp(String.raw`(?<![A-Za-z0-9._@/-])((?:${ROOTS})/[A-Za-z0-9._/*{},-]*[A-Za-z0-9_*}])`,"g");const PARENT_RELATIVE_RE=/(?<![A-Za-z0-9._-])((?:\.\.\/)+[A-Za-z0-9._/-]*[A-Za-z0-9_])/g;const IMPORTISH=/\b(import|require|from)\b/;const NPM_RE=/npm run (?!--)([A-Za-z0-9:_-]+)/g;const OTHER_PM_RE=/(?<![A-Za-z0-9-])(pnpm|yarn)\s+(?!--)([A-Za-z0-9:@/._-]+)/g;const EXT_BOUNDARY=/\.(json|ts|tsx|mjs|mts|js|jsx|md|html|css|ya?ml)\./;function filePart(ref){const m=EXT_BOUNDARY.exec(ref);return m?ref.slice(0,m.index+m[0].length-1):ref}const TRY_EXT=["",".ts",".tsx",".mjs",".mts",".js",".json",".md"];const TRY_INDEX=["index.ts","index.tsx","index.mjs","SKILL.md"];function resolves(p,base=REPO_ROOT){const abs=resolve(base,p);for(const ext of TRY_EXT)if(existsSync(abs+ext))return true;for(const idx of TRY_INDEX)if(existsSync(join(abs,idx)))return true;return false}function walk(dir,out=[]){if(!existsSync(dir))return out;for(const entry of readdirSync(dir)){const full=join(dir,entry);if(statSync(full).isDirectory())walk(full,out);else if(entry.endsWith(".md"))out.push(full)}return out}function allowedMissing(text){const fm=/^---\n([\s\S]*?)\n---/.exec(text);if(!fm)return new Set;const inline=/^allowed_missing:\s*\[(.*)\]\s*$/m.exec(fm[1]);if(inline)return new Set(inline[1].split(",").map(s=>s.trim().replace(/^['"]|['"]$/g,"")));const block=/^allowed_missing:\s*\n((?:\s+-\s+.*\n?)+)/m.exec(fm[1]);if(!block)return new Set;return new Set([...block[1].matchAll(/-\s+(.*)/g)].map(m=>m[1].trim()))}const scripts=new Set(Object.keys(JSON.parse(readFileSync(join(REPO_ROOT,"package.json"),"utf8")).scripts??{}));const files=[...DOC_ROOTS.flatMap(d=>walk(resolve(REPO_ROOT,d))),...DOC_FILES.map(f=>resolve(REPO_ROOT,f)).filter(existsSync)];const findings=[];let checkedPaths=0;let checkedScripts=0;let exempted=0;for(const file of files){const where=relative(REPO_ROOT,file);if(where==="scripts/check-skills.mjs")continue;const text=readFileSync(file,"utf8");const allowed=allowedMissing(text);const lines=text.split("\n");let inFence=false;lines.forEach((line,i)=>{const at=`${where}:${i+1}`;if(line.trim().startsWith("```")){inFence=!inFence;return}const window=markerWindow(lines,i);for(const m of line.matchAll(NPM_RE)){const name=m[1];checkedScripts++;if(scripts.has(name))continue;if(HISTORICAL.test(window)||FOREIGN.test(window)||allowed.has(name)){exempted++;continue}findings.push({at,ref:`npm run ${name}`,why:"no such script in package.json",line})}for(const m of line.matchAll(OTHER_PM_RE)){if(HISTORICAL.test(window)||FOREIGN.test(window)||allowed.has(m[0])){exempted++;continue}findings.push({at,ref:m[0],why:`this repo is npm-only \u2014 say \`npm run \u2026\`, or mark the line as the host's`,line})}for(const m of inFence||IMPORTISH.test(line)?[]:line.matchAll(PARENT_RELATIVE_RE)){const ref=filePart(m[1]);if(/[*{]/.test(ref))continue;if(/(^|\/)(__output__|__snapshots__|dist|dist-bundles|node_modules)(\/|$)/.test(ref))continue;if(/^(\.\.\/)+$/.test(ref)||!/[A-Za-z0-9]/.test(ref.replace(/[./]/g,"")))continue;checkedPaths++;if(resolves(ref,dirname(file)))continue;if(allowed.has(ref)){exempted++;continue}if(PLACEHOLDER.test(ref)){exempted++;continue}if(HISTORICAL.test(window)||FOREIGN.test(window)){exempted++;continue}findings.push({at,ref,why:"does not exist relative to this file",line})}for(const m of line.matchAll(PATH_RE)){const ref=filePart(m[1]);if(/[*{]/.test(ref))continue;if(/(^|\/)(__output__|__snapshots__|dist|dist-bundles|node_modules)(\/|$)/.test(ref))continue;checkedPaths++;if(resolves(ref))continue;if(allowed.has(ref)){exempted++;continue}if(PLACEHOLDER.test(ref)){exempted++;continue}if(HISTORICAL.test(window)||FOREIGN.test(window)){exempted++;continue}findings.push({at,ref,why:"does not exist in this repo",line})}})}const CONTRACT_SRC="src/types/host-contract.ts";const contractSrc=existsSync(join(REPO_ROOT,CONTRACT_SRC))?readFileSync(join(REPO_ROOT,CONTRACT_SRC),"utf8"):null;let contractChecked=0;if(contractSrc){const exported=new Set([...contractSrc.matchAll(/export\s+(?:interface|type)\s+([A-Z]\w+)/g)].map(m=>m[1]));for(const file of files){const where=relative(REPO_ROOT,file);const text=readFileSync(file,"utf8");for(const block of text.matchAll(/```(?:ts|tsx|typescript)\n([\s\S]*?)```/g)){for(const decl of block[1].matchAll(/(?:export\s+)?(?:interface|type)\s+([A-Z]\w+)/g)){const name=decl[1];if(!exported.has(name))continue;const docFields=topLevelFields(declBody(block[1],name));const srcFields=topLevelFields(declBody(contractSrc,name));if(docFields===null||srcFields===null)continue;contractChecked++;const dk=[...docFields.keys()].sort();const sk=[...srcFields.keys()].sort();if(dk.join("|")!==sk.join("|")){findings.push({at:where,ref:name,why:`quoted shape has drifted from ${CONTRACT_SRC} \u2014 doc has [${dk.join(", ")}], source has [${sk.join(", ")}]`,line:`(fenced ts block declaring ${name})`});continue}for(const[field,srcType]of srcFields){const alias=/^([A-Z]\w+)$/.exec(srcType.trim());if(!alias)continue;const docInline=docFields.get(field)??"";if(!docInline.includes("{"))continue;const nestedDoc=topLevelKeys(docInline.slice(docInline.indexOf("{")+1,docInline.lastIndexOf("}")));const nestedSrc=topLevelKeys(declBody(contractSrc,alias[1]));if(nestedDoc===null||nestedSrc===null)continue;contractChecked++;if(nestedDoc.join("|")===nestedSrc.join("|"))continue;findings.push({at:where,ref:`${name}.${field}`,why:`inlined ${alias[1]} has drifted from ${CONTRACT_SRC} \u2014 doc has [${nestedDoc.join(", ")}], source has [${nestedSrc.join(", ")}]`,line:`(fenced ts block declaring ${name})`})}}}}}if(findings.length>0){console.error(`[skills] ${findings.length} dangling reference(s) in agent documentation:
3
+ `);for(const f of findings){console.error(` ${f.at}`);console.error(` "${f.ref}" \u2014 ${f.why}`);console.error(` ${f.line.trim().slice(0,120)}
4
+ `)}console.error('Fix the prose. If the reference is deliberate history, say so ON THE SAME LINE\n("retired 2026-08-08", "deleted", "no longer exists"); if it is a path in the HOST\nrepo, say "the host\'s \u2026". Both are recognised and exempt. Adding a path to a skill\'s\nfront-matter `allowed_missing:` is the last resort, not the first.');process.exit(1)}assertNonVacuous("skills",{paths:{count:checkedPaths,unit:"path citation(s)"},scripts:{count:checkedScripts,unit:"npm script(s)"},contractTypes:{count:contractChecked,unit:"quoted contract type(s)"}});console.log(`[skills] ok \u2014 ${checkedPaths} path citation(s) and ${checkedScripts} npm script(s) across ${files.length} document(s) resolve; ${exempted} exempt (historical / foreign / placeholder).`);console.log(`[skills] ok \u2014 ${contractChecked} quoted contract type(s) match ${CONTRACT_SRC} (field names and optionality; not type text \u2014 see this script's header).`);
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,readFileSync,readdirSync}from"node:fs";import{dirname,join,relative}from"node:path";import{fileURLToPath}from"node:url";import{loadConfig}from"../lib/config.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const ENGINE_CONFIG=await loadConfig();const GATED_TEMPLATES=new Set(ENGINE_CONFIG.checks.gated.projectTemplates);const GATED_ORGS=new Set(ENGINE_CONFIG.checks.gated.orgTemplates);const REQUIRED_PREFIX="/portal/project/{projectId}";const ALLOWED_PATH_PARAMS=new Set(["projectId"]);const HOST_PROVIDED_PARAMS=new Set(["projectId","clientId","userId","orgId","organizationId","advisorOrgId","pageId","pageType","processId"]);const repoRoot=REPO_ROOT_;const templatesRoot=join(repoRoot,"src","project_templates");const orgsRoot=join(repoRoot,"src","org_templates");const args=process.argv.slice(2);const gateAll=args.includes("--all-gated");const warnOnly=args.includes("--warn-only");const only=args.find(a=>!a.startsWith("--"));let exportedNames=null;let hasComponentJSDoc=null;let resolveComponentFilePath=null;let exportSkipReason=null;try{const parser=await import("../lib/jsdoc-parser.mjs");if(typeof parser.readExportsFromIndex!=="function"){exportSkipReason="jsdoc-parser.mjs exports no readExportsFromIndex"}else{exportedNames=new Map(ENGINE_CONFIG.lineages.flatMap(l=>existsSync(join(repoRoot,l.entry))?parser.readExportsFromIndex(repoRoot,l.entry).map(e=>[e.name,e.source]):[]));hasComponentJSDoc=typeof parser.hasComponentJSDoc==="function"?parser.hasComponentJSDoc:null;resolveComponentFilePath=typeof parser.resolveComponentFilePath==="function"?parser.resolveComponentFilePath:null}}catch(err){exportSkipReason=`could not load the engine's jsdoc parser \u2014 ${err.message}`}const GENERATED_CONTRACT=join(repoRoot,"src","types","host-contract.generated.ts");function generatedContractKeys(source,interfaceName){const open=source.indexOf(`export interface ${interfaceName} {`);if(open===-1)return null;const keys=new Set;let depth=0;for(let i=source.indexOf("{",open);i<source.length;i++){const ch=source[i];if(ch==="{"){depth+=1;continue}if(ch==="}"){depth-=1;if(depth===0)break;continue}if(depth===1&&ch==='"'){const end=source.indexOf('"',i+1);if(end===-1)break;const key=source.slice(i+1,end);if(source.slice(end+1,end+2)===":")keys.add(key);i=end}}return keys}let contractQueries=null;let contractMutations=null;let contractSkipReason=null;if(existsSync(GENERATED_CONTRACT)){const source=readFileSync(GENERATED_CONTRACT,"utf8");contractQueries=generatedContractKeys(source,"Queries");contractMutations=generatedContractKeys(source,"Mutations");if(!contractQueries||!contractMutations){contractSkipReason="host-contract.generated.ts has no Queries/Mutations interface";contractQueries=null;contractMutations=null}}else{contractSkipReason="src/types/host-contract.generated.ts is absent \u2014 run scripts/sync-host-types.mjs"}function parsePlaceholder(segment){if(segment.startsWith("[")&&segment.endsWith("]"))return segment.slice(1,-1);if(segment.startsWith("{")&&segment.endsWith("}"))return segment.slice(1,-1);return null}function placeholdersIn(path){return path.split("/").filter(Boolean).map(parsePlaceholder).filter(name=>name!==null)}function getRouteParam(params,name){if(!params)return void 0;if(Array.isArray(params)){const prefix=`${name}=`;const hit=params.find(p=>typeof p==="string"&&p.startsWith(prefix));return hit?hit.slice(prefix.length):void 0}return typeof params[name]==="string"?params[name]:void 0}function declaredPageTypes(templateDir){const pagesDir=join(templateDir,"pages");const found=new Map;if(!existsSync(pagesDir))return found;for(const entry of readdirSync(pagesDir,{withFileTypes:true})){if(!entry.isDirectory())continue;const file=join(pagesDir,entry.name,"page.json");if(!existsSync(file))continue;try{const page=JSON.parse(readFileSync(file,"utf8"));if(page.page_type)found.set(page.page_type,entry.name)}catch{}}return found}const errors=[];const warnings=[];let routeCount=0;let templateCount=0;let orgCount=0;let componentRefCount=0;function checkComponentName(where,name,problems){if(typeof name!=="string"||name===""){problems.push(`${where} declares no component name.`);return}componentRefCount+=1;if(name.startsWith("/")||name.startsWith("@"))return;if(!exportedNames)return;if(!exportedNames.has(name)){problems.push(`${where} component "${name}" is not exported from any lineage barrel, so register-components.mjs writes no component_registry row and the host renders "Component '${name}' not found in authorized registries".`);return}if(!hasComponentJSDoc||!resolveComponentFilePath)return;const filePath=resolveComponentFilePath(repoRoot,exportedNames.get(name));if(!filePath){problems.push(`${where} component "${name}" is exported from src/index.ts but its source ("${exportedNames.get(name)}") does not resolve \u2014 it cannot be registered.`);return}if(!hasComponentJSDoc(filePath,name)){problems.push(`${where} component "${name}" carries no \`@component ${name}\` JSDoc block, so register-components.mjs skips it and the host finds no registry row.`)}}function checkDeclarations(where,node,problems){const queries=node?.data?.queries??{};for(const[friendly,decl]of Object.entries(queries)){const id=typeof decl==="string"?decl:decl?.registry;if(typeof id!=="string"||id===""){problems.push(`${where} data.queries.${friendly} declares no registry id.`)}else if(contractQueries&&!contractQueries.has(id)){problems.push(`${where} data.queries.${friendly} \u2192 "${id}" is not a query in src/types/host-contract.generated.ts, so the binding resolves to nothing (and register-components.mjs hard-fails on it \u2014 ground rule 4).`)}const params=Array.isArray(decl?.params)?decl.params:[];for(const param of params){if(typeof param!=="string"||!param.startsWith("@params."))continue;const root=param.slice("@params.".length).split(".")[0];if(HOST_PROVIDED_PARAMS.has(root))continue;problems.push(`${where} data.queries.${friendly} reads ${param}, but "${root}" is not a key the host puts into ParameterContext. There is no context_parameters allowlist at org level, so it resolves to nothing and the query runs with a missing argument.`)}}for(const[friendly,decl]of Object.entries(node?.actions??{})){const id=typeof decl==="string"?decl:decl?.registry;if(typeof id!=="string"||id===""){problems.push(`${where} actions.${friendly} declares no registry id.`)}else if(contractMutations&&!contractMutations.has(id)){problems.push(`${where} actions.${friendly} \u2192 "${id}" is not a mutation in src/types/host-contract.generated.ts \u2014 the action binds nothing and the component reports unbound.`)}}}const templates=existsSync(templatesRoot)?readdirSync(templatesRoot,{withFileTypes:true}).filter(e=>e.isDirectory()).map(e=>e.name).filter(slug=>!only||slug===only):[];for(const slug of templates){const templateDir=join(templatesRoot,slug);const templateFile=join(templateDir,"template.json");if(!existsSync(templateFile))continue;templateCount+=1;const rel=relative(repoRoot,templateFile);const gated=gateAll||GATED_TEMPLATES.has(slug);const problems=gated?errors:warnings;let template;try{template=JSON.parse(readFileSync(templateFile,"utf8"))}catch(err){problems.push(`${rel}: invalid JSON \u2014 ${err.message}`);continue}const layout=template.layout??{};const routes=layout.whitelisted_routes;if(!Array.isArray(routes)||routes.length===0){problems.push(`${rel}: layout.whitelisted_routes is missing or empty \u2014 no page is reachable.`);continue}const pageTypes=declaredPageTypes(templateDir);const bySlug=new Map;const seenPaths=new Map;const isAppLayout=layout.layout_type==="app";for(const route of routes){routeCount+=1;const id=route.slug||route.path||route.label||"<unnamed>";const path=route.path||route.route;if(!route.slug){if(isAppLayout){problems.push(`${rel}: route "${id}" has no slug. layout_type is 'app', so the platform routes at /v/{slug} and without it the route is unreachable there even though the portal path resolves.`)}}else if(bySlug.has(route.slug)){problems.push(`${rel}: duplicate slug "${route.slug}".`)}else{bySlug.set(route.slug,route);if(!isAppLayout){warnings.push(`${rel}: route "${id}" declares slug "${route.slug}", but layout_type is ${JSON.stringify(layout.layout_type??null)}, not 'app' \u2014 so routesBySlug() is never consulted and the slug is inert. Harmless, but it implies a /v/{slug} surface that is not active.`)}}if(!path){problems.push(`${rel}: route "${id}" has no path.`);continue}const colon=path.split("/").filter(s=>s.startsWith(":"));if(colon.length>0){problems.push(`${rel}: route "${id}" path "${path}" uses ${colon.join(", ")} \u2014 :param is treated as a literal segment and the route will never match. Use {param}.`)}const stray=placeholdersIn(path).filter(p=>!ALLOWED_PATH_PARAMS.has(p));if(stray.length>0){problems.push(`${rel}: route "${id}" path "${path}" declares placeholder(s) ${stray.map(s=>`{${s}}`).join(", ")} \u2014 detail record ids travel as QUERY PARAMETERS (?${stray[0]}=<graph_node_id>), not path segments. The detail page traverses the same subtree as its list and selects the node matching @params.${stray[0]}.`)}if(path!==REQUIRED_PREFIX&&!path.startsWith(`${REQUIRED_PREFIX}/`)){problems.push(`${rel}: route "${id}" path "${path}" must start with "${REQUIRED_PREFIX}" \u2014 findRouteInLayout matches the raw request path and NavigationRenderer rewrites exactly the "/portal/project" prefix, so this can never match.`)}if(route.component==="ProjectPageByType"){if(Array.isArray(route.params)){problems.push(`${rel}: route "${id}" declares params as an array. Use the object form { "pageType": "..." }.`)}const pageType=getRouteParam(route.params,"pageType");if(!pageType){problems.push(`${rel}: route "${id}" targets ProjectPageByType with no params.pageType \u2014 it renders the "Page Type Required" card.`)}else if(pageTypes.size>0&&!pageTypes.has(pageType)){problems.push(`${rel}: route "${id}" pageType "${pageType}" matches no page.json in ${slug}/pages/ \u2014 declared: ${[...pageTypes.keys()].sort().join(", ")||"(none)"}.`)}}if(seenPaths.has(path)){problems.push(`${rel}: duplicate path "${path}" (also on "${seenPaths.get(path)}").`)}else{seenPaths.set(path,id)}}if(layout.home_slug&&!bySlug.has(layout.home_slug)){problems.push(`${rel}: layout.home_slug "${layout.home_slug}" names no route slug \u2014 known: ${[...bySlug.keys()].join(", ")||"(none)"}. The /v/{home_slug} redirect would 404.`)}if(layout.layout_type==="app"&&!layout.home_slug){problems.push(`${rel}: layout_type is "app" but there is no home_slug \u2014 a bare project URL has nowhere to redirect to.`)}const navItems=layout.sidebar?.navigation;if(Array.isArray(navItems)){for(const item of navItems){const navSlug=typeof item==="string"?item:item?.slug;if(!navSlug){problems.push(`${rel}: sidebar.navigation has an entry with no slug.`);continue}const route=bySlug.get(navSlug);if(!route){problems.push(`${rel}: sidebar.navigation references slug "${navSlug}" which is not in whitelisted_routes \u2014 the nav item would render but 404.`)}else if(route.show_in_nav===false){problems.push(`${rel}: sidebar.navigation references "${navSlug}" but that route sets show_in_nav: false \u2014 contradictory. Drill-in routes stay routable and out of nav.`)}}}const ctxParams=layout.context_parameters;if(ctxParams!==void 0&&!Array.isArray(ctxParams)){problems.push(`${rel}: layout.context_parameters must be an array of strings.`)}else if(Array.isArray(ctxParams)&&ctxParams.some(p=>typeof p!=="string")){problems.push(`${rel}: layout.context_parameters must contain only strings.`)}const switcher=layout.sidebar?.context_switcher;if(switcher?.enabled){if(!switcher.param){problems.push(`${rel}: context_switcher is enabled but declares no \`param\`.`)}else if(!Array.isArray(ctxParams)||!ctxParams.includes(switcher.param)){problems.push(`${rel}: context_switcher writes ?${switcher.param}= but "${switcher.param}" is not in layout.context_parameters \u2014 pickAllowedContextParams would refuse to merge it and every context would resolve to the same page.`)}if(!switcher.source?.kind){problems.push(`${rel}: context_switcher.source.kind is required (a resolver id, never a UUID).`)}}const create=switcher?.create;if(create&&create.enabled!==false){if(!create.template_process_id&&!create.template_name){problems.push(`${rel}: context_switcher.create declares neither template_process_id nor template_name, so there is no process template to clone and the create button always fails.`)}if(create.job?.mode==="code"){problems.push(`${rel}: context_switcher.create.job.mode is 'code' \u2014 code-mode jobs go to the per-repo sprite bridge, not the director machine that runs plan-builder. Setup would never run.`)}const fields=Array.isArray(create.fields)?create.fields:[];const keys=new Set;for(const field of fields){if(!field?.key){problems.push(`${rel}: context_switcher.create has a field with no \`key\`.`);continue}if(keys.has(field.key)){problems.push(`${rel}: context_switcher.create declares field "${field.key}" twice.`)}keys.add(field.key);if(field.target&&!["binding","definition","both"].includes(field.target)){problems.push(`${rel}: context_switcher.create field "${field.key}" has target "${field.target}"; must be binding | definition | both.`)}if(field.type&&!["text","textarea"].includes(field.type)){problems.push(`${rel}: context_switcher.create field "${field.key}" has type "${field.type}"; the modal renders text | textarea only.`)}}if(create.name_from&&!keys.has(create.name_from)){problems.push(`${rel}: context_switcher.create.name_from is "${create.name_from}" but no field declares that key \u2014 the record would be created unnamed and the slug would be empty.`)}if(!create.name_from&&fields.length>0){warnings.push(`${rel}: context_switcher.create declares fields but no name_from, so every record is created with the RPC's default name and no slug binding.`)}const labelFrom=switcher.source?.label_from;if(labelFrom?.startsWith("metadata.bindings.")){const bindingKey=labelFrom.slice("metadata.bindings.".length);const writesIt=keys.has(bindingKey)&&fields.some(f=>f.key===bindingKey&&(f.target??"binding")!=="definition");if(!writesIt&&bindingKey!==(create.slug_key??"product_slug")){warnings.push(`${rel}: source.label_from reads bindings.${bindingKey}, but create writes no such binding \u2014 new records would fall back to the process name in the switcher.`)}}}const routedTypes=new Set(routes.map(r=>getRouteParam(r.params,"pageType")).filter(Boolean));for(const[pageType,dir]of pageTypes){if(!routedTypes.has(pageType)){warnings.push(`${rel}: page "${dir}" (page_type "${pageType}") has no route \u2014 unreachable except by direct pageId URL.`)}}}const orgs=existsSync(orgsRoot)?readdirSync(orgsRoot,{withFileTypes:true}).filter(e=>e.isDirectory()).map(e=>e.name).filter(slug=>!only||slug===only):[];for(const slug of orgs){const templateFile=join(orgsRoot,slug,"template.json");if(!existsSync(templateFile))continue;orgCount+=1;const rel=relative(repoRoot,templateFile);const gated=gateAll||GATED_ORGS.has(slug);const problems=gated?errors:warnings;let template;try{template=JSON.parse(readFileSync(templateFile,"utf8"))}catch(err){problems.push(`${rel}: invalid JSON \u2014 ${err.message}`);continue}const layout=template.portal_layout??{};const routes=layout.whitelisted_routes;if(!Array.isArray(routes)||routes.length===0){problems.push(`${rel}: portal_layout.whitelisted_routes is missing or empty \u2014 no portal page is reachable.`);continue}const seenPaths=new Map;const routeByPath=new Map;for(const route of routes){routeCount+=1;const path=route.path||route.route;const id=path||route.label||"<unnamed>";if(!path){problems.push(`${rel}: route "${id}" has no path.`);continue}const colon=path.split("/").filter(s=>s.startsWith(":"));if(colon.length>0){problems.push(`${rel}: route "${path}" uses ${colon.join(", ")} \u2014 :param is treated as a literal segment and the route will never match. Use [param].`)}for(const placeholder of placeholdersIn(path)){if(placeholder.startsWith("...")){warnings.push(`${rel}: route "${path}" uses catch-all "[${placeholder}]", but matchesParameterizedPath compares segment counts \u2014 it matches exactly ONE segment, so deeper paths never resolve through findRouteInLayout.`)}}if(seenPaths.has(path)){problems.push(`${rel}: duplicate path "${path}" (also on "${seenPaths.get(path)}").`)}else{seenPaths.set(path,id);routeByPath.set(path,route)}checkComponentName(`${rel}: route "${path}"`,route.component,problems);checkDeclarations(`${rel}: route "${path}"`,route,problems);if(route.right_sidebar?.component){checkComponentName(`${rel}: route "${path}" right_sidebar`,route.right_sidebar.component,problems)}}const resolvesToRoute=target=>{if(seenPaths.has(target))return true;const targetSegments=target.split("/").filter(Boolean);for(const path of seenPaths.keys()){const routeSegments=path.split("/").filter(Boolean);if(routeSegments.length!==targetSegments.length)continue;const matches=routeSegments.every((segment,i)=>parsePlaceholder(segment)!==null||segment===targetSegments[i]);if(matches)return true}return false};const navPaths=[];if(layout.base_route?.path)navPaths.push([`base_route.path`,layout.base_route.path]);for(const[i,item]of(layout.tab_bar?.items??[]).entries()){if(item?.path)navPaths.push([`tab_bar.items[${i}].path`,item.path]);for(const[j,active]of(item?.active_paths??[]).entries()){navPaths.push([`tab_bar.items[${i}].active_paths[${j}]`,active])}}for(const[i,item]of(layout.more_menu?.items??[]).entries()){if(item?.path)navPaths.push([`more_menu.items[${i}].path`,item.path])}for(const[i,item]of(layout.sidebar?.navigation??[]).entries()){if(item?.path)navPaths.push([`sidebar.navigation[${i}].path`,item.path])}for(const[where,path]of navPaths){if(!resolvesToRoute(path)){problems.push(`${rel}: ${where} is "${path}" but no whitelisted_route matches it \u2014 the nav entry renders and then resolves to nothing.`)}}for(const[i,item]of(layout.sidebar?.navigation??[]).entries()){if(!item?.path||!item?.component)continue;const route=routeByPath.get(item.path);if(route&&route.component&&route.component!==item.component){problems.push(`${rel}: sidebar.navigation[${i}] claims component "${item.component}" for "${item.path}" but the whitelisted route declares "${route.component}" \u2014 nav and route disagree.`)}}const chrome=[["base_route",layout.base_route?.component],["app_bar",layout.app_bar?.component],["tab_bar",layout.tab_bar?.component],["more_menu",layout.more_menu?.component],["chat_rail",layout.chat_rail?.component],["right_sidebar",layout.right_sidebar?.component],["sidebar",layout.sidebar?.component],["assistant_overlay.component",layout.assistant_overlay?.component],["assistant_overlay.pill",layout.assistant_overlay?.pill]];for(const[where,name]of chrome){if(name===void 0||name===null)continue;checkComponentName(`${rel}: portal_layout.${where}`,name,problems)}if(layout.right_sidebar){checkDeclarations(`${rel}: portal_layout.right_sidebar`,layout.right_sidebar,problems)}}if(exportSkipReason){console.warn(`[check-template-routes] component-name checks SKIPPED \u2014 ${exportSkipReason}.
3
+ `)}if(contractSkipReason){console.warn(`[check-template-routes] registry-id checks SKIPPED \u2014 ${contractSkipReason}.
4
+ `)}const notGated=[...templates.filter(s=>!gateAll&&!GATED_TEMPLATES.has(s)),...orgs.filter(s=>!gateAll&&!GATED_ORGS.has(s))];if(warnings.length>0){console.warn(`[check-template-routes] ${warnings.length} warning(s):
5
+ `);for(const w of warnings)console.warn(` \xB7 ${w}`);if(notGated.length>0){console.warn(`
6
+ Not gated: ${notGated.join(", ")} \u2014 reported but not enforced. Run with --all-gated to enforce.`)}console.warn("")}if(errors.length>0){console.error(`[check-template-routes] ${errors.length} error(s):
7
+ `);for(const e of errors)console.error(` \u2022 ${e}`);console.error("")}console.log(`[check-template-routes] ${errors.length===0?"OK":`FAIL (${errors.length} error(s))`} \u2014 walked ${routeCount} route(s) and ${componentRefCount} component ref(s) across ${templateCount} project template(s) + ${orgCount} org template(s); ${warnings.length} warning(s); gated: ${gateAll?"all":[...GATED_TEMPLATES,...GATED_ORGS].join(", ")||"(none)"}.`);if(routeCount===0){console.error(`[check-template-routes] no routes found${only?` for "${only}"`:""} \u2014 nothing was validated.`);process.exit(1)}if(errors.length>0){if(!warnOnly)process.exit(1);console.warn("[check-template-routes] --warn-only: exiting 0 despite the errors above.")}
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import{readFileSync,existsSync,readdirSync}from"node:fs";import{join,dirname,relative}from"node:path";import{fileURLToPath}from"node:url";import{loadConfig}from"../lib/config.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const templatesRoot=join(repoRoot,"src","project_templates");const args=process.argv.slice(2);const only=args.find(a=>!a.startsWith("--"));const gateAll=args.includes("--all-gated");const warnOnly=args.includes("--warn-only");const ENGINE_CONFIG=await loadConfig();const GATED_TEMPLATES=new Set(ENGINE_CONFIG.checks.gated.projectTemplates);const STATE_SYSTEMS=new Set(["eng_data","eng_process","eng_model"]);const MAX_NODES_CEILING=500;const DEFAULT_MAX_NODES=50;const RESERVED_KEYS=new Set(["_traversal","params"]);const HOST_PROVIDED_PARAMS=new Set(["projectId","clientId","userId","orgId","organizationId","advisorOrgId","pageId","pageType","processId"]);let exportedNames=null;let hasComponentJSDoc=null;let resolveComponentFilePath=null;let a1SkipReason=null;try{const parser=await import("../lib/jsdoc-parser.mjs");if(typeof parser.readExportsFromIndex!=="function"){a1SkipReason="jsdoc-parser.mjs exports no readExportsFromIndex"}else{exportedNames=new Map(ENGINE_CONFIG.lineages.flatMap(l=>existsSync(join(repoRoot,l.entry))?parser.readExportsFromIndex(repoRoot,l.entry).map(e=>[e.name,e.source]):[]));hasComponentJSDoc=typeof parser.hasComponentJSDoc==="function"?parser.hasComponentJSDoc:null;resolveComponentFilePath=typeof parser.resolveComponentFilePath==="function"?parser.resolveComponentFilePath:null}}catch(err){a1SkipReason=`could not load the engine's jsdoc parser \u2014 ${err.message}`}const GENERATED_CONTRACT=join(repoRoot,"src","types","host-contract.generated.ts");function generatedContractKeys(source,interfaceName){const open=source.indexOf(`export interface ${interfaceName} {`);if(open===-1)return null;const keys=new Set;let depth=0;for(let i=source.indexOf("{",open);i<source.length;i++){const ch=source[i];if(ch==="{"){depth+=1;continue}if(ch==="}"){depth-=1;if(depth===0)break;continue}if(depth===1&&ch==='"'){const end=source.indexOf('"',i+1);if(end===-1)break;const key=source.slice(i+1,end);if(source.slice(end+1,end+2)===":")keys.add(key);i=end}}return keys}let contractQueries=null;let contractMutations=null;let e10SkipReason=null;if(existsSync(GENERATED_CONTRACT)){const source=readFileSync(GENERATED_CONTRACT,"utf8");contractQueries=generatedContractKeys(source,"Queries");contractMutations=generatedContractKeys(source,"Mutations");if(!contractQueries||!contractMutations){e10SkipReason="host-contract.generated.ts has no Queries/Mutations interface";contractQueries=null;contractMutations=null}}else{e10SkipReason="src/types/host-contract.generated.ts is absent \u2014 run sync-host-types.mjs"}function collectTokens(value,out=[]){if(typeof value==="string"){if(value.startsWith("@"))out.push(value)}else if(Array.isArray(value)){for(const v of value)collectTokens(v,out)}else if(value&&typeof value==="object"){for(const v of Object.values(value))collectTokens(v,out)}return out}function collectInterpolations(value,out=[]){if(typeof value==="string"){for(const match of value.matchAll(/\$\{(?:context\.)?([^}]+)\}/g)){out.push({raw:match[0],path:match[1]})}}else if(Array.isArray(value)){for(const v of value)collectInterpolations(v,out)}else if(value&&typeof value==="object"){for(const v of Object.values(value))collectInterpolations(v,out)}return out}function pageFilesFor(templateSlug){const pagesDir=join(templatesRoot,templateSlug,"pages");const shipped=[];const stale=[];if(!existsSync(pagesDir))return{shipped,stale};for(const entry of readdirSync(pagesDir,{withFileTypes:true})){if(!entry.isDirectory())continue;const dir=join(pagesDir,entry.name);for(const file of readdirSync(dir)){if(file==="page.json")shipped.push(join(dir,file));else if(/^page\..+\.json$/.test(file))stale.push(join(dir,file))}}return{shipped,stale}}const templates=existsSync(templatesRoot)?readdirSync(templatesRoot,{withFileTypes:true}).filter(e=>e.isDirectory()).map(e=>e.name).filter(slug=>!only||slug===only):[];const errors=[];const warnings=[];let fileCount=0;let staleCount=0;let cellCount=0;let documentPageCount=0;let tokenCount=0;let patternBCells=0;function checkComponentName(where,name,problems){if(!name){problems.push(`${where} has no component.`);return}if(name.startsWith("/")||name.startsWith("@"))return;if(!exportedNames)return;if(!exportedNames.has(name)){problems.push(`${where} component "${name}" is not exported from any lineage barrel, so register-components.mjs writes no component_registry row and the host renders "Component '${name}' not found in authorized registries".`);return}if(!hasComponentJSDoc||!resolveComponentFilePath)return;const filePath=resolveComponentFilePath(repoRoot,exportedNames.get(name));if(!filePath){problems.push(`${where} component "${name}" is exported from src/index.ts but its source file ("${exportedNames.get(name)}") does not resolve \u2014 it cannot be registered.`);return}if(!hasComponentJSDoc(filePath,name)){problems.push(`${where} component "${name}" carries no \`@component ${name}\` JSDoc block, so register-components.mjs skips it and the host finds no registry row.`)}}const W5_SEEN=new Map;function checkRegistryIds(where,label,cell,problems){if(!contractQueries||!contractMutations)return;const queries=cell.data?.queries??{};for(const[friendly,decl]of Object.entries(queries)){const id=typeof decl==="string"?decl:decl?.registry;if(typeof id!=="string"||id===""){if(decl&&typeof decl==="object"&&decl.graph_node_id)continue;const key=`${label}::${friendly}`;const seen=W5_SEEN.get(key);if(seen){seen.count+=1;continue}const record={count:1,index:warnings.length};W5_SEEN.set(key,record);warnings.push(`${label}: data.queries.${friendly} declares neither \`registry\` nor \`graph_node_id\` (keys: ${Object.keys(decl??{}).join(", ")||"none"}), so getStaticDataFromDeclarations resolves it to null. Harmless only because these cells bind through the traversal binding_key "${friendly}" instead.`);continue}if(!contractQueries.has(id)){problems.push(`${where} data.queries.${friendly} \u2192 "${id}" is not a query in src/types/host-contract.generated.ts, so the binding resolves to nothing (and register-components.mjs hard-fails on it \u2014 ground rule 4).`)}}for(const[friendly,decl]of Object.entries(cell.actions??{})){const id=typeof decl==="string"?decl:decl?.registry;if(typeof id!=="string"||id===""){problems.push(`${where} actions.${friendly} declares no registry id.`);continue}if(!contractMutations.has(id)){problems.push(`${where} actions.${friendly} \u2192 "${id}" is not a mutation in src/types/host-contract.generated.ts \u2014 the action binds nothing and the cell reports unbound.`)}}}function auditPage(file,slug,tag,allowedParams,problems){const rel=relative(repoRoot,file);const label=tag?`${rel} [${tag}]`:rel;let page;try{page=JSON.parse(readFileSync(file,"utf8"))}catch(err){problems.push(`${label}: invalid JSON \u2014 ${err.message}`);return}const declared=new Set(Object.keys(page.metadata?.binding_sources??{}));const cells=page.content?.cells??[];const pageLayout=page.layout??{};let structureChildren=null;let documentPages=0;if(pageLayout.type==="document"){documentPages+=1;structureChildren=new Set;const structure=Array.isArray(pageLayout.structure)?pageLayout.structure:null;if(!structure){problems.push(`${label}: layout.type is "document" but there is no layout.structure \u2014 StructureRenderer renders nothing.`)}else{const visit=node=>{if(!node)return;if(typeof node==="string"){structureChildren.add(node);return}if(node.cellId)structureChildren.add(node.cellId);if(node.id&&!node.cellId){problems.push(`${label}: structure node uses "id": "${node.id}" \u2014 the renderer reads "cellId" and ignores "id", so this node resolves to nothing and renders blank. Rename the key.`)}for(const child of node.children??[])visit(child)};for(const node of structure)visit(node)}}documentPageCount+=documentPages;const seenIds=new Set;for(const cell of cells){cellCount+=1;const where=`${label}: cell "${cell.id}"`;if(cell.id===void 0||cell.id===null||cell.id===""){problems.push(`${label}: a cell has no id.`)}else if(seenIds.has(cell.id)){problems.push(`${label}: duplicate cell id "${cell.id}".`)}else{seenIds.add(cell.id)}checkComponentName(where,cell.component,problems);checkRegistryIds(where,label,cell,problems);if(structureChildren&&cell.id&&!structureChildren.has(cell.id)){problems.push(`${where} is not claimed by any layout.structure[].children, so StructureRenderer clears it and the cell vanishes with no error.`)}const span=cell.layout?.span;if(pageLayout.type==="grid"&&span!==void 0){const columns=pageLayout.columns??12;if(!Number.isInteger(span)||span<1||span>columns){warnings.push(`${where} layout.span is ${JSON.stringify(span)} on a ${columns}-column grid \u2014 expected an integer 1..${columns}.`)}}const tokens=collectTokens(cell.props??{});tokenCount+=tokens.length;const traversal=cell.traversal;const keys=tokens.map(t=>t.slice(1).split(".")[0]);const usesPatternB=keys.includes("_traversal");if(usesPatternB)patternBCells+=1;const dataTokens=tokens.filter(t=>!t.startsWith("@params."));if(dataTokens.length>0&&!traversal){problems.push(`${where} has ${dataTokens.length} @token(s) but no traversal block \u2014 usePageCellTraversalData skips it and every token resolves to nothing.`)}if(tokens.length===0&&traversal){problems.push(`${where} declares traversal but has no @tokens \u2014 a BFS per render feeding nothing.`)}for(const token of tokens){if(!token.startsWith("@params."))continue;const param=token.slice("@params.".length).split(".")[0];if(HOST_PROVIDED_PARAMS.has(param))continue;if(allowedParams?.has(param))continue;problems.push(`${where} reads ${token} but "${param}" is neither host-provided nor in layout.context_parameters of ${slug}/template.json. pickAllowedContextParams drops an undeclared query parameter, so this resolves to nothing and the cell renders its empty state.`)}for(const{raw,path}of collectInterpolations(cell.props??{})){const root=path.split(".")[0];if(HOST_PROVIDED_PARAMS.has(root))continue;if(allowedParams?.has(root))continue;problems.push(`${where} interpolates ${raw} but "${root}" is neither host-provided nor in layout.context_parameters of ${slug}/template.json. On a miss the host leaves the literal "${raw}" in the prop, which reads as a real value downstream.`)}for(const[i,key]of keys.entries()){if(RESERVED_KEYS.has(key))continue;if(!declared.has(key)){problems.push(`${where} token ${tokens[i]} \u2192 key "${key}" is not in metadata.binding_sources, so no producer is documented for it.`)}}if(!traversal)continue;if(!Array.isArray(traversal.edgeTypes)||traversal.edgeTypes.length===0){problems.push(`${where} traversal has no edgeTypes. There is no RPC default \u2014 the walk returns nothing.`)}if(traversal.include_state!==void 0){if(!Array.isArray(traversal.include_state)){problems.push(`${where} traversal.include_state must be an array.`)}else{for(const system of traversal.include_state){if(!STATE_SYSTEMS.has(system)){problems.push(`${where} traversal.include_state contains ${JSON.stringify(system)} \u2014 expected one of ${[...STATE_SYSTEMS].join(" | ")}. resolveStateSystems drops it and the cell renders no state for that system.`)}}}}if(usesPatternB&&!traversal.anchor){problems.push(`${where} consumes @_traversal but declares no traversal.anchor. Auto-wire matches @<binding_key> tokens in props to create the data_flow source\u2192cell edge, and a Pattern B cell has none \u2014 without an anchor the edge must be hand-wired per instance.`)}if(traversal.maxNodes!==void 0){const n=traversal.maxNodes;if(!Number.isInteger(n)||n<=0){problems.push(`${where} traversal.maxNodes must be a positive integer, got ${JSON.stringify(n)}.`)}else if(n>MAX_NODES_CEILING){problems.push(`${where} traversal.maxNodes ${n} exceeds the host ceiling of ${MAX_NODES_CEILING}; it would be clamped.`)}}if(usesPatternB&&!traversal.include_state){warnings.push(`${where} consumes @_traversal.nodes with no include_state \u2014 nodes arrive with content only (no status, no criteria). Correct for a label-only view; otherwise add include_state.`)}if(usesPatternB&&traversal.maxNodes===void 0&&(traversal.maxDepth??3)>1){warnings.push(`${where} walks to depth ${traversal.maxDepth??3} at the default cap of ${DEFAULT_MAX_NODES} nodes, so it may truncate silently. Set traversal.maxNodes.`)}}}for(const slug of templates){const templateFile=join(templatesRoot,slug,"template.json");let allowedParams=null;if(existsSync(templateFile)){try{const template=JSON.parse(readFileSync(templateFile,"utf8"));const declaredParams=template.layout?.context_parameters;if(Array.isArray(declaredParams))allowedParams=new Set(declaredParams)}catch{}}const gated=gateAll||GATED_TEMPLATES.has(slug);const problems=gated?errors:warnings;const{shipped,stale}=pageFilesFor(slug);for(const file of stale){staleCount+=1;problems.push(`${relative(repoRoot,file)} exists beside page.json. page.json is the single source \u2014 the page-templates layer reads it and nothing in scripts/, src/ or test/ reads this file, so it is edited and never shipped. Merge it into page.json and delete it.`)}for(const file of shipped){fileCount+=1;auditPage(file,slug,null,allowedParams,problems)}for(const file of stale){auditPage(file,slug,"stale",allowedParams,warnings)}}for(const rec of W5_SEEN.values()){if(rec.count>1)warnings[rec.index]+=` Repeated on ${rec.count} cells.`}if(a1SkipReason){console.warn(`[check-wired-bindings] A1 SKIPPED \u2014 ${a1SkipReason}. Component names unchecked.
3
+ `)}if(e10SkipReason){console.warn(`[check-wired-bindings] E10 SKIPPED \u2014 ${e10SkipReason}. Registry ids unchecked.
4
+ `)}if(warnings.length>0){console.warn(`[check-wired-bindings] ${warnings.length} warning(s):
5
+ `);for(const w of warnings)console.warn(` \xB7 ${w}`);console.warn("")}const notGated=templates.filter(s=>!gateAll&&!GATED_TEMPLATES.has(s));if(notGated.length>0){console.warn(` Not gated: ${notGated.join(", ")} \u2014 reported but not enforced. Run with --all-gated to enforce.
6
+ `)}if(errors.length>0){console.error(`[check-wired-bindings] ${errors.length} error(s):
7
+ `);for(const e of errors)console.error(` \u2022 ${e}`);console.error("")}console.log(`[check-wired-bindings] ${errors.length===0?"OK":`FAIL (${errors.length} error(s))`} \u2014 walked ${cellCount} cell(s) and ${tokenCount} @token(s) across ${fileCount} page.json (${documentPageCount} document-layout) (+${staleCount} stale sibling(s)); ${patternBCells} Pattern B cell(s); ${warnings.length} warning(s); gated: ${gateAll?"all":[...GATED_TEMPLATES].join(", ")||"(none)"}.`);if(fileCount===0){console.error(`[check-wired-bindings] no page.json found under ${relative(repoRoot,templatesRoot)}${only?` for template "${only}"`:""} \u2014 nothing was validated.`);process.exit(1)}if(errors.length>0){if(!warnOnly)process.exit(1);console.warn("[check-wired-bindings] --warn-only: exiting 0 despite the errors above.")}
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,readFileSync,readdirSync,statSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{HISTORICAL,markerWindowAt}from"../lib/citation-markers.mjs";import{namesAnotherRepo,orgSlug,stripSelfPrefix}from"../lib/repo-identity.mjs";import{assertNonVacuous}from"../lib/floor.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const LOOKBACK=1;const REPO_ROOT=REPO_ROOT_;const SCAN_DIRS=["src","scripts","test","design","docs","outlines","template",".claude"];const SCAN_EXT=/\.(ts|tsx|mts|mjs|js|jsx|md|json)$/;const ABSOLUTE=/(?<![A-Za-z0-9._/-])((?:[A-Za-z0-9_-]+\/)?docs\/(?:plans\/[A-Za-z0-9._-]+\/)?work-orders\/[A-Za-z0-9._-]+\.md)/g;const PARENT_RELATIVE=/((?:\.\.\/)+(?:[A-Za-z0-9._-]+\/)*work-orders\/[A-Za-z0-9._-]+\.md)/g;const RELATIVE=/(?<![A-Za-z0-9._/-])(work-orders\/[A-Za-z0-9._-]+\.md)/g;const DIR_CITATION=/(?<![A-Za-z0-9._/-])((?:[A-Za-z0-9._-]+\/)*(?:plans\/[A-Za-z0-9._-]+\/)?work-orders)\/(?![A-Za-z0-9._-]*\.md|<)/g;function walk(dir,out=[]){if(!existsSync(dir))return out;for(const entry of readdirSync(dir)){if(entry==="node_modules"||entry==="__output__"||entry.startsWith(".git"))continue;const full=join(dir,entry);if(statSync(full).isDirectory())walk(full,out);else if(SCAN_EXT.test(entry))out.push(full)}return out}function markerWindowWithout(text,index,ref){return markerWindowAt(text,index,LOOKBACK).split(ref).join(" ")}const findings=[];let checked=0;let exempted=0;let vendoredSkipped=0;const foreignPrefixes=new Set;const VENDORED_BANNER=/VENDORED FROM THE HOST/;for(const dir of SCAN_DIRS){for(const file of walk(resolve(REPO_ROOT,dir))){const where=relative(REPO_ROOT,file);if(where==="scripts/check-work-orders.mjs")continue;const text=readFileSync(file,"utf8");if(VENDORED_BANNER.test(text.slice(0,400))){vendoredSkipped++;continue}const seen=new Set;for(const m of text.matchAll(ABSOLUTE)){const ref=m[1];if(seen.has(ref))continue;seen.add(ref);const prefix=ref.includes("/docs/")?ref.slice(0,ref.indexOf("/docs/")):"";if(prefix&&namesAnotherRepo(prefix)){foreignPrefixes.add(prefix);continue}const local=stripSelfPrefix(ref);checked++;if(existsSync(resolve(REPO_ROOT,local)))continue;if(HISTORICAL.test(markerWindowWithout(text,m.index,m[1]))){exempted++;continue}findings.push({where,ref,how:"no such file (repo-relative)"})}for(const m of text.matchAll(PARENT_RELATIVE)){const ref=m[1];if(seen.has(ref))continue;seen.add(ref);checked++;if(existsSync(resolve(dirname(file),ref)))continue;if(HISTORICAL.test(markerWindowWithout(text,m.index,m[1]))){exempted++;continue}findings.push({where,ref,how:"no such file (relative to this file)"})}for(const m of text.matchAll(RELATIVE)){const ref=m[1];if([...seen].some(x=>x.endsWith(ref)))continue;if([...seen].some(s=>s.endsWith(ref)))continue;if(seen.has(ref))continue;seen.add(ref);checked++;if(existsSync(resolve(dirname(file),ref)))continue;if(HISTORICAL.test(markerWindowWithout(text,m.index,m[1]))){exempted++;continue}findings.push({where,ref,how:"no such file (relative to this file)"})}}}for(const dir of SCAN_DIRS){for(const file of walk(resolve(REPO_ROOT,dir))){const where=relative(REPO_ROOT,file);if(where==="scripts/check-work-orders.mjs")continue;const text=readFileSync(file,"utf8");if(VENDORED_BANNER.test(text.slice(0,400)))continue;const seen=new Set;for(const m of text.matchAll(DIR_CITATION)){const ref=m[1];if(seen.has(ref))continue;seen.add(ref);const dirPrefix=ref.includes("/")?ref.slice(0,ref.indexOf("/")):"";if(dirPrefix&&namesAnotherRepo(dirPrefix)){foreignPrefixes.add(dirPrefix);continue}const abs=ref.includes("/")?resolve(REPO_ROOT,ref):resolve(dirname(file),ref);checked++;if(existsSync(abs)&&statSync(abs).isDirectory())continue;if(HISTORICAL.test(markerWindowWithout(text,m.index,m[1]))){exempted++;continue}findings.push({where,ref:`${ref}/`,how:existsSync(abs)?"names a directory but is a file":"no such directory"})}}}if(findings.length>0){console.error(`[work-orders] ${findings.length} dangling work-order reference(s):
3
+ `);for(const f of findings){console.error(` ${f.where}`);console.error(` "${f.ref}" \u2014 ${f.how}
4
+ `)}console.error(`A work order lives in the plan that owns it: docs/plans/<plan>/work-orders/<slug>.md.
5
+ Cite it ABSOLUTELY from src/, scripts/ and test/ \u2014 a code comment has no stable
6
+ relative position \u2014 and relatively from the plan docs beside it.
7
+ If the order lives in ANOTHER repo, prefix it with that repo's name
8
+ (<repo>/docs/plans/<plan>/work-orders/<slug>.md) \u2014 any prefix that is not
9
+ this repo ("${orgSlug()}") and not a directory here is taken as another repo.
10
+ Vendored files carry the host's own comments and are not scanned at all;
11
+ never rewrite one to satisfy this gate.`);process.exit(1)}assertNonVacuous("work-orders",{citations:{count:checked,unit:"in-repo citation(s)"}});console.log(`[work-orders] ok \u2014 ${checked} in-repo citation(s) resolve`+(foreignPrefixes.size?`; skipped citations belonging to ${[...foreignPrefixes].sort().join(", ")}`:"")+(vendoredSkipped?`; ${vendoredSkipped} vendored file(s) not scanned`:"")+(exempted?`; ${exempted} historical`:"")+".");
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{loadConfig}from"../lib/config.mjs";import{runCommand}from"../lib/run.mjs";const{lineages,checks}=await loadConfig();const named=process.argv.slice(2).filter(a=>!a.startsWith("--"));const list=named.length?named:checks.run;const results=[];for(const name of list){if(name==="knowledge"){for(const l of lineages){console.log(`
3
+ [check] knowledge --bundle ${l.name}`);results.push([`knowledge (${l.name})`,runCommand("knowledge",["--bundle",l.name,"--check"])])}continue}console.log(`
4
+ [check] ${name}`);results.push([name,runCommand(name,[])])}const failed=results.filter(([,status])=>status!==0);console.log(`
5
+ [check] ${results.length-failed.length}/${results.length} passed${failed.length?`; failed: ${failed.map(([n])=>n).join(", ")}`:""}.`);process.exit(failed.length?1:0);
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;if(!VERSION){console.error("[cutover] --version <v> is required.");process.exit(2)}const DRY_RUN=argv.includes("--dry-run");const explicitBundle=parseFlagValue(argv,"bundle");const wantsPublic=argv.includes("--public");if(wantsPublic&&explicitBundle&&explicitBundle!=="public"){console.error(`[cutover] --public conflicts with --bundle ${explicitBundle}. Pass one.`);process.exit(2)}const MAIN_LINEAGE="main";const PUBLIC_LINEAGE="public";const BUNDLE_NAME=explicitBundle??(wantsPublic?PUBLIC_LINEAGE:MAIN_LINEAGE);console.log(`
3
+ \u{1F3AF} cutover \u2014 env=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}
4
+ `);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[cutover] Could not resolve org "${ORG_SLUG}":`,orgError?.message??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;console.log(` org_id=${ORG_ID}`);const{data:bundleRow,error:bundleError}=await supabase.from("component_bundle").select("id, status, exports").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(bundleError){console.error("[cutover] Failed to fetch component_bundle:",bundleError.message);process.exit(1)}if(!bundleRow){console.error(`[cutover] No component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}). Has upload-bundle.mjs + register-components.mjs been run for this version?`);process.exit(1)}const BUNDLE_ID=bundleRow.id;console.log(` bundle_id=${BUNDLE_ID} status='${bundleRow.status}' exports=${bundleRow.exports?.length??0}`);if(bundleRow.status!=="registered"){console.error(`[cutover] Bundle is in status='${bundleRow.status}'. cutover requires 'registered'.
5
+ `+(bundleRow.status==="uploaded"?" \u2192 Run register-components.mjs for this version first.\n":bundleRow.status==="cutover"?" \u2192 This bundle is already active. Target a different version to roll forward/back.\n":" \u2192 Investigate the bundle row directly.\n"));process.exit(1)}await warnLiveDrift();if(BUNDLE_NAME===PUBLIC_LINEAGE){await cutoverPublic()}else if(BUNDLE_NAME!==MAIN_LINEAGE){await cutoverProject()}else{await cutoverMain()}async function cutoverMain(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, remote_version, remote_url, previous_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",MAIN_LINEAGE).eq("version",templateRow.remote_version).maybeSingle();previousBundleId=prev?.id??null}const newRemoteUrl=`/api/remotes/${ORG_SLUG}/${VERSION}/index.js`;console.log(`
6
+ org_template:`);console.log(` remote_version: ${templateRow.remote_version??"(none)"} \u2192 ${VERSION}`);console.log(` remote_url: ${templateRow.remote_url??"(none)"} \u2192 ${newRemoteUrl}`);console.log(` previous_version: ${templateRow.previous_version??"(none)"} \u2192 ${templateRow.remote_version??"(none)"}`);console.log(` remote_storage_bucket: \u2192 ${BUNDLE_BUCKET}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` component_bundle_lineage (main):`);console.log(` active_version: ${templateRow.remote_version??"(none)"} \u2192 ${VERSION}`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:templateUpdateError}=await supabase.from("org_template").update({remote_version:VERSION,previous_version:templateRow.remote_version??null,remote_url:newRemoteUrl,remote_storage_bucket:BUNDLE_BUCKET,updated_at:nowIso}).eq("id",templateRow.id);if(templateUpdateError){console.error("[cutover] Failed to update org_template:",templateUpdateError.message);process.exit(1)}const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] org_template was updated but bundle status update failed:",bundleUpdateError.message);console.error(` Manually run: update component_bundle set status='cutover', cutover_at='${nowIso}' where id='${BUNDLE_ID}';`);process.exit(1)}await setLiveBundle(MAIN_LINEAGE);await writeLineagePointer({bundleName:MAIN_LINEAGE,previousVersion:templateRow.remote_version??null,storageBucket:BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
7
+ \u2705 Promoted bundle ${BUNDLE_ID} to active for org "${ORG_SLUG}" on ${target}.
8
+ `)}async function cutoverPublic(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, public_remote_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.public_remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",PUBLIC_LINEAGE).eq("version",templateRow.public_remote_version).maybeSingle();previousBundleId=prev?.id??null}console.log(`
9
+ org_template (public lineage):`);console.log(` public_remote_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_public: false \u2192 true`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` component_bundle_lineage (public):`);console.log(` active_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,is_public:true,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}const{error:templateUpdateError}=await supabase.from("org_template").update({public_remote_version:VERSION,updated_at:nowIso}).eq("id",templateRow.id);if(templateUpdateError){console.error("[cutover] Failed to update org_template.public_remote_version:",templateUpdateError.message);process.exit(1)}await setLiveBundle(PUBLIC_LINEAGE);await writeLineagePointer({bundleName:PUBLIC_LINEAGE,previousVersion:templateRow.public_remote_version??null,storageBucket:PUBLIC_BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
10
+ \u2705 Promoted public bundle ${BUNDLE_ID} for org "${ORG_SLUG}".
11
+ `)}async function cutoverProject(){const{data:lineage}=await supabase.from("component_bundle_lineage").select("active_version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).maybeSingle();const previousVersion=lineage?.active_version??null;let previousBundleId=null;if(previousVersion){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",previousVersion).maybeSingle();previousBundleId=prev?.id??null}console.log(`
12
+ component_bundle_lineage (${BUNDLE_NAME}):`);console.log(` active_version: ${previousVersion??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` org_template: (untouched \u2014 project lineages have no org-wide pointer)`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}await setLiveBundle(BUNDLE_NAME);await writeLineagePointer({bundleName:BUNDLE_NAME,previousVersion,storageBucket:BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
13
+ \u2705 Promoted "${BUNDLE_NAME}" bundle ${BUNDLE_ID} for org "${ORG_SLUG}" to version ${VERSION}.
14
+ `);console.log(` Point a template at it with project_template.bundle_name = '${BUNDLE_NAME}'
15
+ (upload-project-template.mjs writes this from template.json's "bundle_name" key).
16
+ `)}async function setLiveBundle(bundleName){const{error:clearError}=await supabase.from("component_bundle").update({is_live:false}).eq("org_id",ORG_ID).eq("bundle_name",bundleName).eq("is_live",true);if(clearError){console.error(`[cutover] Failed to clear is_live for lineage "${bundleName}":`,clearError.message);process.exit(1)}const{error:setError}=await supabase.from("component_bundle").update({is_live:true}).eq("id",BUNDLE_ID);if(setError){console.error(`[cutover] Cleared the previous live bundle but failed to set is_live on ${BUNDLE_ID}:`,setError.message);console.error(` Lineage "${bundleName}" now has NO live bundle. Fix with:`);console.error(` update component_bundle set is_live = true where id = '${BUNDLE_ID}';`);process.exit(1)}}async function writeLineagePointer({bundleName,previousVersion,storageBucket}){const{error}=await supabase.from("component_bundle_lineage").upsert({org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:bundleName,active_version:VERSION,previous_version:previousVersion,storage_bucket:storageBucket,updated_at:new Date().toISOString()},{onConflict:"org_id,bundle_name"});if(error){console.error("[cutover] Failed to write component_bundle_lineage:",error.message);console.error(` The bundle status was updated but the lineage pointer was not \u2014 a project_template
17
+ referencing "${bundleName}" will fail its FK until this row exists.`);process.exit(1)}}async function retirePreviousBundle(previousBundleId,nowIso){if(!previousBundleId||previousBundleId===BUNDLE_ID)return;const{error}=await supabase.from("component_bundle").update({status:"rolled_back",rolled_back_at:nowIso}).eq("id",previousBundleId).eq("status","cutover");if(error){console.warn(`[cutover] Promoted, but failed to retire previous bundle ${previousBundleId}:`,error.message)}}async function warnLiveDrift(){const{data:drifted,error}=await supabase.from("component_bundle").select("version, cutover_at").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("status","cutover").eq("is_live",false).order("cutover_at",{ascending:false});if(error){console.warn(`[cutover] Could not check for is_live drift: ${error.message}`);return}if(!drifted?.length)return;const sample=drifted.slice(0,5).map(r=>`${r.version} (cutover_at=${r.cutover_at??"null"})`).join(", ");console.warn(`
18
+ \u26A0\uFE0F ${drifted.length} bundle(s) in lineage "${BUNDLE_NAME}" have status='cutover' but is_live=false:
19
+ ${sample}${drifted.length>5?", \u2026":""}
20
+ These are leftovers from this script's pre-rewrite version, which never set is_live.
21
+ This run will set is_live correctly regardless \u2014 just flagging the historical drift.
22
+ `)}
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{execSync,spawnSync}from"node:child_process";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue,parseTargetEnv}from"../lib/env.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{runCommand}from"../lib/run.mjs";const root=REPO_ROOT;const argv=process.argv.slice(2);const target=parseTargetEnv(argv);const targetFlag=target==="local"?[]:[`--${target}`];const skipBuild=argv.includes("--skip-build");const only=parseFlagValue(argv,"lineage");const{lineages}=await loadConfig();const selected=only?lineages.filter(l=>l.name===only):lineages;if(only&&selected.length===0)throw new Error(`--lineage ${only} is not in the config's lineages (${lineages.map(l=>l.name).join(", ")})`);const git=args=>execSync(`git ${args}`,{cwd:root,encoding:"utf8"}).trim();const version=process.env.VERSION||(git("status --porcelain")===""?`v_${git("rev-parse --short HEAD")}`:`v_${target}_${new Date().toISOString().replace(/[-:T]/g,"").slice(0,14)}`);function check(label,status){if(status!==0){console.error(`[deploy] ${label} failed (exit ${status}). Later lineages were not deployed.`);process.exit(status??1)}}function build(lineage){const{script,env={}}=lineage.build;console.log(`
3
+ [deploy] ${lineage.name} build: npm run ${script}`);const r=spawnSync("npm",["run",script],{cwd:root,stdio:"inherit",env:{...process.env,VERSION:version,...env}});check(`${lineage.name} build`,r.status)}function step(lineage,command,args){console.log(`
4
+ [deploy] ${lineage.name} ${command}: engine ${command} ${args.join(" ")}`);check(`${lineage.name} ${command}`,runCommand(command,args,{VERSION:version}))}console.log(`[deploy] target=${target} version=${version} lineages=${selected.map(l=>l.name).join(", ")}`);for(const lineage of selected){if(!skipBuild)build(lineage);const bundle=lineage.name==="main"?[]:["--bundle",lineage.name];step(lineage,"upload",[...targetFlag,...bundle]);step(lineage,"register",[...targetFlag,...bundle]);step(lineage,"cutover",[...targetFlag,"--version",version,...bundle])}console.log(`
5
+ [deploy] \u2705 ${selected.map(l=>l.name).join(", ")} live at ${version} on ${target}.`);
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{dirname,resolve}from"node:path";import{fileURLToPath}from"node:url";import{hasComponentJSDoc,parseJSDocFile,projectContractsManifest,readExportsFromIndex,resolveComponentFilePath}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;function flag(name,fallback){const i=process.argv.indexOf(`--${name}`);const value=i===-1?void 0:process.argv[i+1];if(i!==-1&&(value===void 0||value.startsWith("--"))){console.error(`[emit-contracts] --${name} needs a value`);process.exit(1)}return value??fallback}const entryRelative=flag("entry","src/index.ts");const outRelative=flag("out","dist/contracts.json");if(!existsSync(resolve(repoRoot,entryRelative))){console.error(`[emit-contracts] no entry barrel at ${entryRelative}. For a project lineage, check OA_PROJECT_BUNDLE is set.`);process.exit(1)}const outputPath=resolve(repoRoot,outRelative);const distRoot=dirname(outputPath);const exports=readExportsFromIndex(repoRoot,entryRelative);if(exports.length===0){console.warn(`[emit-contracts] No named exports in ${entryRelative} \u2014 nothing to emit.`)}const specs=exports.map(({name,source})=>({name,source,filePath:resolveComponentFilePath(repoRoot,source)})).filter(({name,filePath})=>hasComponentJSDoc(filePath,name)).map(({name,filePath})=>({component_name:name,...parseJSDocFile(filePath,name)}));const skippedCount=exports.length-specs.length;const manifest=projectContractsManifest(specs);const WRITTEN_TAG=/^\s*\*?\s*@(data|action|printPolicy)\s+\w/gm;let written=0;for(const{filePath}of exports.map(({name,source})=>({name,filePath:resolveComponentFilePath(repoRoot,source)})).filter(({name,filePath:filePath2})=>hasComponentJSDoc(filePath2,name))){if(!filePath)continue;written+=(readFileSync(filePath,"utf8").match(WRITTEN_TAG)??[]).length}const parsed=specs.reduce((n,spec)=>n+Object.keys(spec.props_schema?.data??{}).length+Object.keys(spec.props_schema?.actions??{}).length+(spec.print_policy?1:0),0);if(written>parsed){console.error(`[emit-contracts] ${written} @data/@action/@printPolicy tag(s) written, ${parsed} parsed \u2014 ${written-parsed} dropped SILENTLY.
3
+
4
+ A declaration the parser cannot read is not an error today: it is simply
5
+ absent from dist/contracts.json, which the host reads as "this component
6
+ declares nothing to enforce". The artifact stays internally consistent and
7
+ quietly wrong.
8
+
9
+ The grammar is exact \u2014 check the arrow and the \`- via\`:
10
+ @data <name>[?] {<Type>} - via <registry_key>
11
+ @action <name>[?] {<Input> \u2192 <Output>} - via <registry_key>
12
+ `);process.exit(1)}mkdirSync(distRoot,{recursive:true});writeFileSync(outputPath,JSON.stringify(manifest,null,2)+"\n","utf8");const componentCount=Object.keys(manifest.components).length;console.log(`[emit-contracts] Wrote ${outputPath} (contract_version=${manifest.contract_version}, ${componentCount} component${componentCount===1?"":"s"} with declarations from ${specs.length}/${exports.length} exports; ${skippedCount} non-component export${skippedCount===1?"":"s"} skipped; ${parsed}/${written} @data/@action tag(s) parsed).`);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env tsx
2
+ import{mkdirSync,writeFileSync}from"node:fs";import{dirname,resolve}from"node:path";import{checkCatalog}from"@peripheries/renderer/catalog";import{lineageConfig,loadConfig}from"../lib/config.mjs";import{buildKnowledge}from"../lib/knowledge.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const argv=process.argv.slice(2);const flag=name=>{const i=argv.indexOf(`--${name}`);return i===-1?void 0:argv[i+1]};const bundle=flag("bundle")??"main";const checkOnly=argv.includes("--check");const outPath=resolve(repoRoot,flag("out")??`${(await lineageConfig(bundle)).dist}/knowledge.json`);const org=flag("org")??(await loadConfig()).org;const{lineage,rows,links,registry,scopeLineage,scopeRegistry}=await buildKnowledge(bundle,org);const kits=rows.filter(r=>r.kind==="kit");const patterns=rows.filter(r=>r.kind==="pattern");const diagnostics=checkCatalog({lineage,rows,links,registry,scopeRegistry,inScope:(target,kind,key)=>target===scopeLineage&&kind==="component"?Boolean(scopeRegistry?.[key]):void 0});for(const d of diagnostics)console[d.severity==="error"?"error":"warn"](`[knowledge] ${d.severity} ${d.code} ${d.path??""}: ${d.message}`);const errors=diagnostics.filter(d=>d.severity==="error").length;console.log(`[knowledge] ${lineage}: ${rows.length} rows (${rows.filter(r=>r.kind==="component").length} components, ${kits.length} kits, ${patterns.length} patterns), ${links.length} links; ${errors} error(s)`);if(errors>0&&!argv.includes("--allow-errors"))process.exit(1);if(!checkOnly){mkdirSync(dirname(outPath),{recursive:true});writeFileSync(outPath,JSON.stringify({schema:"peripheries.knowledge/1",lineage,rows,links},null,2)+"\n");console.log(`[knowledge] wrote ${outPath}`)}
@@ -0,0 +1,2 @@
1
+
2
+ export const EXTERNALS: ReadonlyArray<string>;
@@ -0,0 +1 @@
1
+ const EXTERNALS=Object.freeze(["react","react/jsx-runtime","react/jsx-dev-runtime","react-dom","react-dom/client","zustand","@oa/store","@stack/store"]);export{EXTERNALS};
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";import{initFromArgv,loadEnv,parseFlagValue,parseTargetEnv}from"../lib/env.mjs";import{collectReferencedKeys,hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName,toRepoRelative}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import{lineageConfig,loadConfig,MAIN_LINEAGE,PUBLIC_LINEAGE}from"../lib/config.mjs";const repoRoot=REPO_ROOT_;const argv=process.argv.slice(2);function orgSlugFromConfig(){try{const raw=readFileSync(resolve(repoRoot,"template/config/org.json"),"utf8");return JSON.parse(raw).slug??null}catch{return null}}const EMIT_ROWS=parseFlagValue(argv,"emit-rows");const target=parseTargetEnv(argv);let ORG_SLUG;let supabase;if(EMIT_ROWS){loadEnv(target);ORG_SLUG=parseFlagValue(argv,"org")??process.env.ORG_SLUG??process.env.VITE_ORG_SLUG??(await loadConfig()).org??orgSlugFromConfig();if(!ORG_SLUG){console.error("[register-components] --emit-rows still needs an org: pass --org <slug>, set ORG_SLUG / VITE_ORG_SLUG, or set `org` in engine.config.ts.");process.exit(1)}}else{({orgSlug:ORG_SLUG,supabase}=initFromArgv(argv))}let VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;const BUNDLE_NAME=parseFlagValue(argv,"bundle")??"main";const LINEAGE=await lineageConfig(BUNDLE_NAME);const entryRelative=parseFlagValue(argv,"entry")??LINEAGE.entry;if(!existsSync(resolve(repoRoot,entryRelative))){console.error(`[register-components] no entry barrel at ${entryRelative} for lineage "${BUNDLE_NAME}".`);process.exit(1)}function entryRuleFor(){return{sourceRoot:LINEAGE.sourceRoot}}const ENTRY_RULE=entryRuleFor();const allExports=readExportsFromIndex(repoRoot,entryRelative);const exports=[];const skipped=[];for(const entry of allExports){const filePath=resolveComponentFilePath(repoRoot,entry.source);if(filePath&&hasComponentJSDoc(filePath,entry.name)){exports.push({...entry,filePath})}else{skipped.push(entry.name)}}if(exports.length===0){if(BUNDLE_NAME===MAIN_LINEAGE){console.error(`[register-components] No exports with an @component JSDoc block found in ${entryRelative}. The org bundle cannot be empty.`);process.exit(1)}console.warn(`[register-components] ${entryRelative} declares no components yet \u2014 registering the "${BUNDLE_NAME}" lineage with an empty component set so it can be cut over.`)}if(skipped.length>0){console.log(`[register-components] skipping ${skipped.length} export(s) with no matching @component block: ${skipped.join(", ")}`)}function buildSpecs(){return exports.map(({name,filePath})=>{const repoRelativePath=toRepoRelative(repoRoot,filePath);if(ENTRY_RULE.sourceRoot&&!repoRelativePath.startsWith(ENTRY_RULE.sourceRoot)){console.error(`[register-components] "${name}" is exported from ${entryRelative} but its source (${repoRelativePath}) is not under ${ENTRY_RULE.sourceRoot}. Refusing to register \u2014 the "${BUNDLE_NAME}" bundle may only publish components from that root.`);if(BUNDLE_NAME!==PUBLIC_LINEAGE){console.error(` If this is a shared component, do not re-export it here: it stays in the org's
3
+ main bundle and resolves through the host's cascade one layer down.`)}process.exit(1)}const jsdoc=parseJSDocFile(filePath,name);return{component_name:name,exposed_export:name,description:jsdoc.description??`${toDisplayName(name)} federated component`,props_schema:jsdoc.props_schema??null,file_path:filePath?`src/${toRepoRelative(repoRoot,filePath)}`:null,metadata:{source:"federated-remote",remote_org_slug:ORG_SLUG,...jsdoc.use_cases?{use_cases:jsdoc.use_cases}:{}}}})}if(EMIT_ROWS){const rows=buildSpecs();const outputPath=resolve(repoRoot,EMIT_ROWS);mkdirSync(dirname(outputPath),{recursive:true});writeFileSync(outputPath,JSON.stringify({_comment:"Generated by `register-components.mjs --emit-rows`. A dry run: these are the component_registry rows a real registration would write. Consumed by the L3 bundle harness to check the built bundle exports what the host will look for. Not checked in \u2014 regenerate rather than edit.",org_slug:ORG_SLUG,bundle_name:BUNDLE_NAME,entry:entryRelative,generated_at:new Date().toISOString(),rows},null,2)+"\n","utf8");console.log(`[register-components] --emit-rows \u2192 ${EMIT_ROWS} (${rows.length} row(s) for lineage "${BUNDLE_NAME}" from ${entryRelative}; no database touched).`);process.exit(0)}const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[register-components] Could not resolve org_id for org_slug="${ORG_SLUG}":`,orgError??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;let bundleRow;if(VERSION){const{data,error}=await supabase.from("component_bundle").select("id, status, version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(error||!data?.id){console.error(`[register-components] No component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}). Run upload-bundle.mjs for this version first.`);process.exit(1)}bundleRow=data}else{const{data,error}=await supabase.from("component_bundle").select("id, status, version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("status","uploaded").order("uploaded_at",{ascending:false}).limit(1).maybeSingle();if(error||!data?.id){console.error(`[register-components] No 'uploaded' bundle found for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}). Either run upload-bundle.mjs first or pass --version <v>.`);process.exit(1)}bundleRow=data;VERSION=data.version;console.log(`[register-components] auto-discovered most recent 'uploaded' bundle: version=${VERSION}`)}const BUNDLE_ID=bundleRow.id;console.log(`[register-components] target=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION} bundle_id=${BUNDLE_ID} status='${bundleRow.status}' \u2014 ${exports.length} components: ${exports.map(e=>e.name).join(", ")}`);const specs=buildSpecs();const referencedKeys=collectReferencedKeys(specs);if(referencedKeys.queries.size>0||referencedKeys.mutations.size>0){const allKeys=[...referencedKeys.queries,...referencedKeys.mutations];const{data:contractRows,error:contractError}=await supabase.from("component_contract").select("kind, registry_key, is_active, contract_version_removed").in("registry_key",allKeys);if(contractError){console.error("[register-components] Failed to read component_contract:",contractError);process.exit(2)}const indexByKey=new Map;for(const row of contractRows??[]){indexByKey.set(`${row.kind}:${row.registry_key}`,row)}const waived=new Set((parseFlagValue(argv,"allow-missing-contract")??"").split(",").map(s=>s.trim()).filter(Boolean));const waiversUsed=new Set;const problems=[];const warnings=[];for(const spec of specs){const data=spec.props_schema?.data??{};const actions2=spec.props_schema?.actions??{};for(const[propName,decl]of Object.entries(data)){const row=indexByKey.get(`query:${decl.registry}`);if(!row&&waived.has(decl.registry)&&decl.required===false){waiversUsed.add(decl.registry);warnings.push(`${spec.component_name}.${propName} \u2192 query "${decl.registry}" absent here, WAIVED (optional; the component must render without it)`)}else if(!row){problems.push(`${spec.component_name}.${propName} \u2192 unknown query "${decl.registry}"`+(waived.has(decl.registry)?" \u2014 waiver REFUSED: the declaration is required":""))}else if(!row.is_active){problems.push(`${spec.component_name}.${propName} \u2192 deprecated query "${decl.registry}" (removed at v${row.contract_version_removed??"?"})`)}}for(const[propName,decl]of Object.entries(actions2)){const row=indexByKey.get(`mutation:${decl.registry}`);if(!row&&waived.has(decl.registry)&&decl.required===false){waiversUsed.add(decl.registry);warnings.push(`${spec.component_name}.${propName} \u2192 mutation "${decl.registry}" absent here, WAIVED (optional; the component must degrade without it)`)}else if(!row){problems.push(`${spec.component_name}.${propName} \u2192 unknown mutation "${decl.registry}"`+(waived.has(decl.registry)?" \u2014 waiver REFUSED: the declaration is required":""))}else if(!row.is_active){problems.push(`${spec.component_name}.${propName} \u2192 deprecated mutation "${decl.registry}" (removed at v${row.contract_version_removed??"?"})`)}}}if(problems.length>0){console.error("[register-components] Contract validation FAILED:");for(const p of problems)console.error(` \xD7 ${p}`);console.error("");console.error(" Either add the missing keys to lib/portal/contract-manifest/ on");console.error(" the host (then run `pnpm sync-contract` there), or update the");console.error(" component JSDoc to reference an existing registry_key.");console.error("");console.error(" If a key is absent from THIS environment on purpose and the");console.error(" declaration is optional, name it explicitly:");console.error(" --allow-missing-contract <key>[,<key>...]");process.exit(1)}if(warnings.length>0){console.warn("[register-components] Contract validation passed WITH WAIVERS:");for(const w of warnings)console.warn(` \u26A0 ${w}`);console.warn(` These props will be undefined at runtime in this environment. Drop the flag once the host ships them.`)}for(const key of waived){if(!waiversUsed.has(key)){console.warn(`[register-components] \u26A0 waiver "${key}" was not needed \u2014 the key resolves here, or nothing declares it. Remove it from --allow-missing-contract.`)}}}const{data:existing,error:fetchError}=await supabase.from("component_registry").select("id, component_name, delivery_mode, exposed_export, is_active, org_id, bundle_id, description, props_schema, file_path, metadata").eq("org_slug",ORG_SLUG).eq("bundle_id",BUNDLE_ID);if(fetchError){console.error("[register-components] Failed to fetch existing rows:",fetchError);process.exit(2)}const existingByName=new Map(existing?.map(row=>[row.component_name,row])??[]);const actions=[];for(const spec of specs){const row=existingByName.get(spec.component_name);const payload={...spec,org_id:ORG_ID,org_slug:ORG_SLUG,bundle_id:BUNDLE_ID,delivery_mode:"federated",is_active:true};if(row){const changed=row.is_active!==true||row.delivery_mode!=="federated"||row.exposed_export!==payload.exposed_export||row.org_id!==ORG_ID||row.bundle_id!==BUNDLE_ID||row.description!==payload.description||row.file_path!==payload.file_path||canonical(row.props_schema)!==canonical(payload.props_schema)||canonical(row.metadata)!==canonical(payload.metadata);if(changed){actions.push({kind:"update",id:row.id,patch:payload})}}else{actions.push({kind:"insert",payload})}}if(actions.length===0){console.log("[register-components] No changes \u2014 bundle registry already in sync.")}else{for(const action of actions){if(action.kind==="insert"){const{error}=await supabase.from("component_registry").insert(action.payload);if(error){console.error(`[register-components] INSERT failed for "${action.payload.component_name}":`,error);process.exit(1)}console.log(`[register-components] + inserted "${action.payload.component_name}"`)}else{const{error}=await supabase.from("component_registry").update(action.patch).eq("id",action.id);if(error){console.error(`[register-components] UPDATE failed for id=${action.id}:`,error);process.exit(1)}console.log(`[register-components] ~ updated id=${action.id}`)}}console.log(`[register-components] ${actions.length} registry change(s) applied.`)}const KNOWLEDGE_PATH=resolve(repoRoot,`${LINEAGE.dist}/knowledge.json`);if(!existsSync(KNOWLEDGE_PATH)){console.error(`[register-components] no ${KNOWLEDGE_PATH.replace(repoRoot+"/","")}: build the lineage first (the build runs scripts/emit-knowledge.mts). Agents read the catalog from it.`);process.exit(1)}const knowledge=JSON.parse(readFileSync(KNOWLEDGE_PATH,"utf8"));if(knowledge.lineage!==`${ORG_SLUG}/${BUNDLE_NAME}`){console.error(`[register-components] ${KNOWLEDGE_PATH} is for ${knowledge.lineage}, not ${ORG_SLUG}/${BUNDLE_NAME}. Rebuild.`);process.exit(1)}{const{error:clearError}=await supabase.from("component_knowledge").delete().eq("bundle_id",BUNDLE_ID);if(clearError){console.error("[register-components] Failed to clear this bundle's knowledge:",clearError);process.exit(1)}const{data:inserted,error:insertError}=await supabase.from("component_knowledge").insert(knowledge.rows.map(r=>({bundle_id:BUNDLE_ID,kind:r.kind,key:r.key,title:r.title,summary:r.summary,body:r.body??null,tags:r.tags??[],snippets:r.snippets??[],attributes:r.attributes??{},page_cell:r.page_cell??true,sort:r.sort??0}))).select("id, kind, key");if(insertError){console.error("[register-components] Failed to write knowledge rows:",insertError);process.exit(1)}const idOf=new Map(inserted.map(r=>[`${r.kind}:${r.key}`,r.id]));const linkRows=knowledge.links.map(l=>({from_id:idOf.get(`${l.from.kind}:${l.from.key}`),relation:l.relation,to_lineage:l.to.lineage??null,to_kind:l.to.kind,to_key:l.to.key,note:l.note??null}));if(linkRows.some(l=>!l.from_id)){console.error("[register-components] a knowledge link starts at a row that was not written. Rebuild.");process.exit(1)}if(linkRows.length>0){const{error:linkError}=await supabase.from("component_knowledge_link").insert(linkRows);if(linkError){console.error("[register-components] Failed to write knowledge links:",linkError);process.exit(1)}}console.log(`[register-components] knowledge: ${inserted.length} rows, ${linkRows.length} links for ${knowledge.lineage}`)}const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"registered",registered_at:new Date().toISOString(),exports:specs.map(s=>s.component_name),manifest:{exports:specs.map(s=>({component_name:s.component_name,exposed_export:s.exposed_export,file_path:s.file_path}))}}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error(`[register-components] Failed to mark bundle 'registered':`,bundleUpdateError);process.exit(1)}console.log(`[register-components] Done. bundle ${BUNDLE_ID} is now status='registered'.
4
+ Promote it with the engine (the host has no cutover of its own):
5
+ engine cutover --version ${VERSION}${BUNDLE_NAME===MAIN_LINEAGE?"":` --bundle ${BUNDLE_NAME}`}${target==="local"?"":` --${target}`}`);function canonical(value){return JSON.stringify(sortKeys(value))}function sortKeys(value){if(Array.isArray(value))return value.map(sortKeys);if(value&&typeof value==="object"){const sorted={};for(const key of Object.keys(value).sort())sorted[key]=sortKeys(value[key]);return sorted}return value}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{readFileSync,existsSync,statSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import{lineageConfig}from"../lib/config.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const BUNDLE_NAME=parseFlagValue(argv,"bundle")??"main";const dryRun=argv.includes("--dry-run");const IS_MAIN=BUNDLE_NAME==="main";const IS_PUBLIC=BUNDLE_NAME==="public";const IS_ORG_WIDE=IS_MAIN||IS_PUBLIC;const DEFAULT_BUCKET=IS_PUBLIC?PUBLIC_BUNDLE_BUCKET:BUNDLE_BUCKET;const STORAGE_BUCKET=process.env.STORAGE_BUCKET||DEFAULT_BUCKET;if(STORAGE_BUCKET!==DEFAULT_BUCKET){console.error(`[upload-bundle] STORAGE_BUCKET must be "${DEFAULT_BUCKET}" for lineage "${BUNDLE_NAME}", got "${STORAGE_BUCKET}".`);process.exit(2)}const VERSION=process.env.VERSION||`v_${target}_${new Date().toISOString().replace(/[-:T]/g,"").replace(/\..+$/,"").slice(0,14)}`;if(!process.env.VERSION){console.log(`[upload-bundle] VERSION auto-generated: ${VERSION}`);process.env.VERSION=VERSION}const distDir=resolve(REPO_ROOT_,(await lineageConfig(BUNDLE_NAME)).dist);const bundlePath=resolve(distDir,"index.js");if(!existsSync(bundlePath)){console.error(`[upload-bundle] ${bundlePath} does not exist. `+(IS_MAIN?"Did `npm run build` run?":`Did \`OA_PROJECT_BUNDLE=${BUNDLE_NAME} npm run build:bundle\` run?`));process.exit(1)}console.log(`[upload-bundle] lineage=${BUNDLE_NAME} dist=${distDir.split("/").slice(-2).join("/")}`);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[upload-bundle] Could not resolve org_id for org_slug="${ORG_SLUG}":`,orgError??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;async function uploadOne(localPath,remotePath,contentType){const body=readFileSync(localPath);if(dryRun){console.log(`[upload-bundle] would upload ${STORAGE_BUCKET}/${remotePath} (${body.byteLength} bytes)`);return}const{error}=await supabase.storage.from(STORAGE_BUCKET).upload(remotePath,body,{contentType,upsert:false});if(error){console.error(`[upload-bundle] Failed to upload ${remotePath}:`,error);process.exit(1)}console.log(`[upload-bundle] \u2191 ${remotePath} (${body.byteLength} bytes)`)}const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;await uploadOne(bundlePath,`${remoteBase}/index.js`,"application/javascript");const mapPath=resolve(distDir,"index.js.map");if(existsSync(mapPath)){await uploadOne(mapPath,`${remoteBase}/index.js.map`,"application/json")}const contractsPath=resolve(distDir,"contracts.json");if(existsSync(contractsPath)){await uploadOne(contractsPath,`${remoteBase}/contracts.json`,"application/json")}const{data:existingBundle,error:existingBundleError}=await supabase.from("component_bundle").select("id, status").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(existingBundleError){console.error(`[upload-bundle] Failed to look up existing component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}):`,existingBundleError);process.exit(1)}const bundleBytes=statSync(bundlePath).size;const hasSourcemap=existsSync(mapPath);const bundlePayload={org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:BUNDLE_NAME,version:VERSION,status:"uploaded",storage_bucket:STORAGE_BUCKET,storage_path:`${remoteBase}/index.js`,uploaded_at:new Date().toISOString(),registered_at:null,cutover_at:null,git_sha:process.env.DEPLOY_SHA??process.env.GITHUB_SHA??null,git_ref:process.env.DEPLOY_REF??process.env.GITHUB_REF??null,git_repo:process.env.GITHUB_REPOSITORY??null,process_id:process.env.PROCESS_ID||null,metadata:{bundle_bytes:bundleBytes,has_sourcemap:hasSourcemap,bundle_scope:BUNDLE_NAME,...process.env.GITHUB_RUN_ID?{github_run_id:process.env.GITHUB_RUN_ID}:{},...process.env.GITHUB_RUN_ATTEMPT?{github_run_attempt:process.env.GITHUB_RUN_ATTEMPT}:{}}};if(dryRun){console.log(`[upload-bundle] dry-run \u2014 would ${existingBundle?.id?`UPDATE ${existingBundle.id}`:"INSERT"} component_bundle:`);console.log(JSON.stringify(bundlePayload,null,2));console.log("[upload-bundle] dry-run \u2014 nothing written.")}else if(existingBundle?.id){const{error:updateError}=await supabase.from("component_bundle").update(bundlePayload).eq("id",existingBundle.id);if(updateError){console.error(`[upload-bundle] Failed to update component_bundle row:`,updateError);process.exit(1)}console.log(`[upload-bundle] ~ component_bundle ${existingBundle.id} reset to status='uploaded' (was '${existingBundle.status}').`)}else{const{data:inserted,error:insertError}=await supabase.from("component_bundle").insert(bundlePayload).select("id").single();if(insertError){console.error(`[upload-bundle] Failed to insert component_bundle row:`,insertError);process.exit(1)}console.log(`[upload-bundle] + component_bundle ${inserted.id} created (status='uploaded').`)}console.log(`[upload-bundle] Done \u2014 target=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}. Run register-components.mjs next.`);
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import{readFileSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";import semver from"semver";import{EXTERNALS}from"./externals.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const HOST_URL=process.env.HOST_URL;let hostImportMap;if(HOST_URL){const importMapUrl=new URL("/_shared/import-map.json",HOST_URL).toString();const skipLiveComparison=reason=>{console.warn(`[validate-externals] ${reason}
3
+ Skipping the live host-import-map comparison (warning, not a failure).
4
+ The offline half still ran: EXTERNALS vs peerDependencies.`);hostImportMap=void 0};try{const res=await fetch(importMapUrl);if(!res.ok){skipLiveComparison(`${importMapUrl} returned ${res.status} ${res.statusText}`)}else{hostImportMap=await res.json();if(!hostImportMap?.imports||!hostImportMap?.pins){skipLiveComparison(`${importMapUrl} is missing \`imports\` or \`pins\` \u2014 expected { imports: Record<string,string>, pins: Record<string,string> }.`)}}}catch(err){skipLiveComparison(`Could not reach ${importMapUrl}: ${err instanceof Error?err.message:err}`)}}else{console.warn("[validate-externals] HOST_URL not set \u2014 skipping live host-import-map comparison, checking only that EXTERNALS and peerDependencies agree locally.")}const pkgPath=resolve(REPO_ROOT_,"package.json");const pkg=JSON.parse(readFileSync(pkgPath,"utf8"));const peers={...pkg.peerDependencies??{}};const problems=[];for(const specifier of EXTERNALS){const basePackage=specifier.startsWith("@")?specifier.split("/").slice(0,2).join("/"):specifier.split("/")[0];const localRange=peers[basePackage];if(!localRange){problems.push(`"${basePackage}" is required external but missing from package.json peerDependencies.`);continue}if(!hostImportMap)continue;const hostUrl=hostImportMap.imports[specifier];const hostPin=hostImportMap.pins[specifier];if(!hostUrl){problems.push(`"${specifier}" is external in this repo but not in the host import map. Remove it from scripts/externals.mjs or update the host to pin it.`);continue}if(hostPin&&!semver.subset(localRange,hostPin)&&!majorsAgree(localRange,hostPin)){problems.push(`"${basePackage}": host pins ${hostPin} but this repo declares ${localRange}. Majors must agree.`)}}if(hostImportMap){for(const hostSpecifier of Object.keys(hostImportMap.imports)){if(!EXTERNALS.includes(hostSpecifier)){problems.push(`Host import map includes "${hostSpecifier}" which is not in scripts/externals.mjs. Likely fix: add it to the externals list and peerDependencies.`)}}}if(problems.length===0){console.log(hostImportMap?`[validate-externals] OK \u2014 ${EXTERNALS.length} externals match host import map.`:`[validate-externals] OK \u2014 ${EXTERNALS.length} externals have matching peerDependencies (host comparison skipped).`);process.exit(0)}for(const p of problems){console.error(`[validate-externals] ERROR: ${p}`)}process.exit(1);function majorsAgree(a,b){const aMajor=extractMajor(a);const bMajor=extractMajor(b);if(aMajor==null||bMajor==null)return true;return aMajor===bMajor}function extractMajor(range){const match=range.match(/(\d+)/);return match?Number(match[1]):null}
@@ -0,0 +1,39 @@
1
+ export type LineageKind = 'main' | 'public' | 'page' | 'form';
2
+
3
+ export type LineageSpec =
4
+ | string
5
+ | {
6
+ name: string;
7
+ kind?: LineageKind;
8
+ entry?: string;
9
+ sourceRoot?: string | null;
10
+ dist?: string;
11
+ build?: { script: string; env?: Record<string, string> };
12
+ };
13
+
14
+ export type EngineConfig = {
15
+ org: string;
16
+ lineages?: LineageSpec[];
17
+ checks?: {
18
+ run?: string[];
19
+ peerRepos?: string[];
20
+ gated?: { projectTemplates?: string[]; orgTemplates?: string[] };
21
+ };
22
+ };
23
+
24
+ export type Lineage = {
25
+ name: string;
26
+ kind: LineageKind;
27
+ entry: string;
28
+ sourceRoot: string | null;
29
+ dist: string;
30
+ build: { script: string; env?: Record<string, string> };
31
+ };
32
+
33
+ export const DEFAULT_CHECKS: string[];
34
+ export const MAIN_LINEAGE: 'main';
35
+ export const PUBLIC_LINEAGE: 'public';
36
+ export function defineConfig(config: EngineConfig): EngineConfig;
37
+ export function lineageDefaults(name: string, kind?: LineageKind): Lineage;
38
+ export function loadConfig(): Promise<{ org: string; lineages: Lineage[]; checks: { run: string[]; peerRepos: string[]; gated: { projectTemplates: string[]; orgTemplates: string[] } } }>;
39
+ export function lineageConfig(name: string): Promise<Lineage>;
package/src/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{DEFAULT_CHECKS,defineConfig,lineageConfig,lineageDefaults,loadConfig,MAIN_LINEAGE,PUBLIC_LINEAGE}from"./lib/config.mjs";export{DEFAULT_CHECKS,MAIN_LINEAGE,PUBLIC_LINEAGE,defineConfig,lineageConfig,lineageDefaults,loadConfig};
@@ -0,0 +1 @@
1
+ import{loadConfig}from"./config.mjs";import{assertNotSelf,selfNames}from"./repo-identity.mjs";const HISTORICAL=/\b(retire[ds]?|delet(e|ed|ing)|remov(e|ed)|mov(e|ed)|used to|pre-move|former(ly)?|superseded|stale|out of date|no longer|(n?ever|neither) existed|gone|replaced)\b/i;const FAMILY_REPO_NAMES=(await loadConfig()).checks.peerRepos;const OTHER_REPO_NAMES=FAMILY_REPO_NAMES.filter(n=>!selfNames().has(n));const FOREIGN=new RegExp([String.raw`\bthe host\b`,String.raw`\bhost'?s\b`,"host-side","host repo","host CI","host PR","OA host",String.raw`\bupstream repo\b`,String.raw`\bsibling repo\b`,String.raw`\bpeer repo\b`,...OTHER_REPO_NAMES.sort((a,b)=>b.length-a.length).map(n=>String.raw`\b${n}\b`)].join("|"),"i");assertNotSelf(OTHER_REPO_NAMES,"FAMILY_REPO_NAMES in citation-markers.mjs");const PLACEHOLDER=/(^|\/)(My[A-Z]|Your[A-Z]|Example[A-Z]|Foo|Bar|ComponentName|<)/;const LIST_ITEM=/^\s*(?:\/\/|\*(?!\s*\/))?\s*(?:[-*+]|\d+[.)])\s+\S/;function markerWindow(lines,i,lookback=3){const parts=[lines[i]];const fromListItem=LIST_ITEM.test(lines[i]??"");let spent=0;for(let j=i-1;j>=0&&spent<lookback;j--){const prev=lines[j];if(prev===void 0)break;const trimmed=prev.trim();if(trimmed===""||trimmed.startsWith("```"))continue;if(fromListItem&&LIST_ITEM.test(prev))continue;parts.push(prev);spent++}return parts.join("\n")}function markerWindowAt(text,index,lookback=3){const upto=text.slice(0,index);const lineNo=upto.split("\n").length-1;return markerWindow(text.split("\n"),lineNo,lookback)}export{FOREIGN,HISTORICAL,OTHER_REPO_NAMES,PLACEHOLDER,markerWindow,markerWindowAt};
@@ -0,0 +1 @@
1
+ export { DEFAULT_CHECKS, defineConfig, lineageConfig, lineageDefaults, loadConfig, MAIN_LINEAGE, PUBLIC_LINEAGE } from '../index.mjs';
@@ -0,0 +1 @@
1
+ import{existsSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{REPO_ROOT}from"./root.mjs";const MAIN_LINEAGE="main";const DEFAULT_CHECKS=["validate-externals","check-wired","check-routes","check-design-refs","check-skills","check-work-orders","check-cutover-ownership","check-fixtures","knowledge"];const PUBLIC_LINEAGE="public";const CONFIG_FILES=["engine.config.ts","engine.config.mts","engine.config.mjs","engine.config.js"];function defineConfig(config){return config}function lineageDefaults(name,kind){if(name===MAIN_LINEAGE){return{name,kind:"main",entry:"src/index.ts",sourceRoot:null,dist:"dist",build:{script:"build"}}}if(name===PUBLIC_LINEAGE){return{name,kind:"public",entry:"src/index.public.ts",sourceRoot:"components/public/",dist:"dist-public",build:{script:"build:public"}}}const k=kind??"page";return{name,kind:k,entry:`src/index.${name}.ts`,sourceRoot:k==="form"?`components/form/${name}/`:`components/project_page/${name}/`,dist:`dist-bundles/${name}`,build:{script:"build:bundle",env:{OA_PROJECT_BUNDLE:name}}}}function normalize(raw,origin){if(!raw||typeof raw!=="object")throw new Error(`[engine] ${origin} must export a config object`);if(typeof raw.org!=="string"||!raw.org)throw new Error(`[engine] ${origin}: \`org\` (the org slug) is required`);const list=raw.lineages??[MAIN_LINEAGE];const lineages=list.map(l=>{const spec=typeof l==="string"?{name:l}:l;if(!spec?.name)throw new Error(`[engine] ${origin}: every lineage needs a name`);return{...lineageDefaults(spec.name,spec.kind),...spec}});if(lineages[0]?.name!==MAIN_LINEAGE)throw new Error(`[engine] ${origin}: lineages must start with "main"`);const gated=raw.checks?.gated??{};const checks={run:raw.checks?.run??DEFAULT_CHECKS,peerRepos:raw.checks?.peerRepos??[],gated:{projectTemplates:gated.projectTemplates??[],orgTemplates:gated.orgTemplates??[]}};return{org:raw.org,lineages,checks}}let cached;async function loadConfig(){if(cached)return cached;const file=CONFIG_FILES.map(f=>resolve(REPO_ROOT,f)).find(f=>existsSync(f));if(file){const mod=await import(pathToFileURL(file).href);cached=normalize(mod.default??mod,file);return cached}const pkg=JSON.parse(readFileSync(resolve(REPO_ROOT,"package.json"),"utf8"));let org=pkg.oaBundle?.name;const orgJson=resolve(REPO_ROOT,"template/config/org.json");if(existsSync(orgJson))org=JSON.parse(readFileSync(orgJson,"utf8")).slug??org;const lineages=(pkg.oaBundle?.lineages??[MAIN_LINEAGE]).map(n=>n==="workbooks"?{name:n,kind:"form"}:n);cached=normalize({org,lineages},"package.json oaBundle");return cached}async function lineageConfig(name){const{lineages}=await loadConfig();return lineages.find(l=>l.name===name)??lineageDefaults(name)}export{DEFAULT_CHECKS,MAIN_LINEAGE,PUBLIC_LINEAGE,defineConfig,lineageConfig,lineageDefaults,loadConfig};
@@ -0,0 +1 @@
1
+ function stripArrows(text){return text.replace(/=>/g,"@@")}function declBody(text,name){const m=new RegExp(String.raw`(?:export\s+)?(interface|type)\s+${name}\b`).exec(text);if(!m)return null;const after=m.index+m[0].length;if(m[1]==="type"){const end=text.indexOf(";",after);return end===-1?null:text.slice(after,end)}const open=text.indexOf("{",after);if(open===-1)return null;let depth=0;for(let i=open;i<text.length;i++){if(text[i]==="{")depth++;else if(text[i]==="}"&&--depth===0)return text.slice(open+1,i)}return null}const stripComments=s=>s.replace(/\/\/[^\n]*/g,"").replace(/\/\*[\s\S]*?\*\//g,"");function topLevelKeys(body){if(body===null||body===void 0)return null;const clean=stripArrows(stripComments(body));const keys=[];let depth=0;let atFieldStart=true;let buf="";for(const ch of clean){if(ch==="{"||ch==="("||ch==="<"){depth++;continue}if(ch==="}"||ch===")"||ch===">"){depth--;continue}if(depth>0)continue;if(ch===";"||ch===","||ch==="\n"){atFieldStart=true;buf="";continue}if(ch===":"){const key=buf.trim();if(atFieldStart&&key)keys.push(/^\[/.test(key)?"[index]":key.replace(/\s+/g,""));atFieldStart=false;buf="";continue}buf+=ch}return[...new Set(keys)].sort()}function topLevelFields(body){if(body===null||body===void 0)return null;const clean=stripArrows(stripComments(body));const out=new Map;let i=0;while(i<clean.length){let key="";while(i<clean.length&&!":;,\n{}".includes(clean[i]))key+=clean[i++];if(clean[i]!==":"){i++;continue}i++;key=key.trim();if(!key)continue;let depth=0;let val="";for(;i<clean.length;i++){const ch=clean[i];if("{(<".includes(ch))depth++;else if("})>".includes(ch))depth--;else if(depth===0&&";,\n".includes(ch))break;val+=ch}out.set(/^\[/.test(key)?"[index]":key.replace(/\s+/g,""),val.trim())}return out}export{declBody,stripArrows,topLevelFields,topLevelKeys};
@@ -0,0 +1 @@
1
+ import{createClient}from"@supabase/supabase-js";import{readFileSync,existsSync}from"node:fs";import{resolve}from"node:path";const ENV_FILE={local:".env.local",develop:".env.develop",prod:".env.prod"};const BUNDLE_BUCKET="bundles";const PUBLIC_BUNDLE_BUCKET="bundles-public";function parseTargetEnv(argv=process.argv.slice(2)){const wantsDevelop=argv.includes("--develop");const wantsProd=argv.includes("--prod");if(wantsDevelop&&wantsProd){throw new Error("Pass at most one of --develop / --prod (default is local).")}if(wantsProd)return"prod";if(wantsDevelop)return"develop";return"local"}function parseFlagValue(argv,flag){const longEq=`--${flag}=`;for(let i=0;i<argv.length;i++){const tok=argv[i];if(tok===`--${flag}`){const next=argv[i+1];if(!next||next.startsWith("--")){throw new Error(`--${flag} requires a value`)}return next}if(tok.startsWith(longEq)){return tok.slice(longEq.length)}}return null}function resolveOrgSlug(argv=process.argv.slice(2)){const fromFlag=parseFlagValue(argv,"org");const slug=fromFlag??process.env.ORG_SLUG??null;if(!slug){throw new Error("org slug required \u2014 pass --org <slug> or set ORG_SLUG in your env file.")}if(!process.env.ORG_SLUG)process.env.ORG_SLUG=slug;return slug}function loadEnv(target){const envFile=ENV_FILE[target];if(!envFile)throw new Error(`Unknown target env "${target}"`);const primary=resolve(process.cwd(),envFile);const fallback=target==="local"?resolve(process.cwd(),".env"):null;const fileToLoad=existsSync(primary)?primary:fallback&&existsSync(fallback)?fallback:null;if(!fileToLoad){const hasCiEnv=process.env.SUPABASE_URL&&secretKeyFromEnv();if(!hasCiEnv){console.warn(`[env] No env file found for target "${target}" (looked for ${envFile}${fallback?" or .env":""}).`)}else{console.log(`[env] No ${envFile} on disk \u2014 using env vars from process.env (CI mode).`)}return null}const content=readFileSync(fileToLoad,"utf8");for(const rawLine of content.split("\n")){const line=rawLine.trim();if(!line||line.startsWith("#"))continue;const m=line.match(/^([^=]+)=(.*)$/);if(!m)continue;const key=m[1].trim();let value=m[2].trim();if(value.startsWith('"')&&value.endsWith('"')||value.startsWith("'")&&value.endsWith("'")){value=value.slice(1,-1)}if(process.env[key]===void 0)process.env[key]=value}console.log(`[env] Loaded ${target} from ${fileToLoad}`);return fileToLoad}function secretKeyFromEnv(){return process.env.SUPABASE_SECRET_KEY||process.env.SUPABASE_SERVICE_KEY||process.env.SUPABASE_SERVICE_ROLE_KEY}function getSupabaseConfig(){const url=process.env.SUPABASE_URL;const key=secretKeyFromEnv();if(!url)throw new Error("SUPABASE_URL not set");if(!key)throw new Error("SUPABASE_SECRET_KEY (or SUPABASE_SERVICE_KEY / SUPABASE_SERVICE_ROLE_KEY) not set");return{url,key}}function initFromArgv(argv=process.argv.slice(2)){const target=parseTargetEnv(argv);loadEnv(target);const orgSlug=resolveOrgSlug(argv);const{url,key}=getSupabaseConfig();const supabase=createClient(url,key,{auth:{persistSession:false,autoRefreshToken:false}});return{target,orgSlug,supabase}}export{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,getSupabaseConfig,initFromArgv,loadEnv,parseFlagValue,parseTargetEnv,resolveOrgSlug};
@@ -0,0 +1,4 @@
1
+ function assertNonVacuous(gate,sets){const empty=Object.entries(sets).filter(([,s])=>s.count<(s.floor??1));if(empty.length===0)return;const lines=empty.map(([name,s])=>` ${name}: examined ${s.count} ${s.unit}, floor is ${s.floor??1}`);throw new Error(`[${gate}] VACUOUS \u2014 this gate ran its checks over an empty (or near-empty) set and would have reported success:
2
+ ${lines.join("\n")}
3
+
4
+ Nothing was verified. Either the inputs moved, a glob resolved against the wrong directory, or a filter matched nothing. A gate's green has to mean "I looked and found no problems", never "I did not look".`)}export{assertNonVacuous};
@@ -0,0 +1,8 @@
1
+ export function readExportsFromIndex(repoRoot: string, entryRelative?: string): Array<{ name: string; source: string }>;
2
+ export function resolveComponentFilePath(repoRoot: string, relative: string): string | null;
3
+ export function hasComponentJSDoc(filePath: string, componentName: string): boolean;
4
+ export function parseJSDocFile(filePath: string, componentName: string): Record<string, unknown>;
5
+ export function parseJSDocBlock(fileContent: string, componentName?: string): Record<string, unknown>;
6
+ export function toDisplayName(pascal: string): string;
7
+ export function toRepoRelative(repoRoot: string, absPath: string): string;
8
+ export function inferComponentType(name: string): string;
@@ -0,0 +1 @@
1
+ import{readFileSync,existsSync}from"node:fs";import{resolve}from"node:path";function resolveComponentFilePath(repoRoot,relative){const base=resolve(repoRoot,"src",relative);const dirBasename=base.split("/").pop();const candidates=[`${base}.ts`,`${base}.tsx`,`${base}/${dirBasename}.tsx`,`${base}/${dirBasename}.ts`,`${base}/index.ts`,`${base}/index.tsx`];return candidates.find(p=>existsSync(p))??null}function toRepoRelative(repoRoot,absPath){const srcRoot=resolve(repoRoot,"src")+"/";return absPath.startsWith(srcRoot)?absPath.slice(srcRoot.length):absPath}function parseJSDocFile(filePath,componentName){try{const content=readFileSync(filePath,"utf8");return parseJSDocBlock(content,componentName)}catch{return{}}}function hasComponentJSDoc(filePath,componentName){if(!filePath)return false;let content;try{content=readFileSync(filePath,"utf8")}catch{return false}const blocks=content.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return false;return blocks.some(b=>componentName?new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b):b.includes("@component"))}function parseJSDocBlock(fileContent,componentName){const blocks=fileContent.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return{};const componentBlock=componentName?blocks.find(b=>new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b))??blocks.find(b=>b.includes("@component")):blocks.find(b=>b.includes("@component"));if(!componentBlock)return{};const body=componentBlock.replace(/\/\*\*|\*\//g,"").split("\n").map(line=>line.replace(/^\s*\*\s?/,"")).join("\n");const meta={};const descMatch=body.match(/@description\s+([\s\S]+?)(?=\n\s*@|$)/);if(descMatch){meta.description=descMatch[1].replace(/\s+/g," ").trim()}const agentInstructionsMatch=body.match(/@agentInstructions\s+([\s\S]+?)(?=\n\s*@\w+|$)/);if(agentInstructionsMatch){meta.agent_instructions=agentInstructionsMatch[1].trim()}const categoryMatch=body.match(/@category\s+([\w-]+(?:\/[\w-]+)*)/);if(categoryMatch){meta.family=categoryMatch[1];meta.component_type=categoryMatch[1].split("/")[0]}const shapes={raw:readShapes(body),resolving:new Set};const propsMatches=body.match(/@props\s+\{[^}]+\}\s+[^\n]+/g);if(propsMatches){const schema={type:"object",properties:{},required:[]};for(const line of propsMatches){const m=line.match(/@props\s+\{([^}]+)\}\s+(?:\[([^=\]]+)(?:=[^\]]+)?\]|([\w-]+(?:\.[\w-]+)*))\s*-\s*(.+)/);if(!m)continue;const[,type,optName,reqName,desc]=m;const propName=(optName||reqName).split(".").pop();schema.properties[propName]={...toSchemaFragment(type,shapes),description:desc.trim()};if(!optName)schema.required.push(propName)}if(Object.keys(schema.properties).length>0){if(schema.required.length===0)delete schema.required;meta.props_schema=schema}}const examples=[];let cursor=0;while(cursor<body.length){const tagRe=/(^|\n)\s*@example\b/g;tagRe.lastIndex=cursor;const tagMatch=tagRe.exec(body);if(!tagMatch)break;const tagIdx=tagMatch.index+tagMatch[0].length-"@example".length;const braceIdx=body.indexOf("{",tagIdx);if(braceIdx===-1)break;const end=findBalancedEnd(body,braceIdx);if(end===-1)break;const raw=body.slice(braceIdx,end+1).replace(/\s+/g," ").trim();try{examples.push(JSON.parse(raw))}catch{}cursor=end+1}if(examples.length>0)meta.usage_examples=examples;const depsMatch=body.match(/@dependencies\s+(\[[\s\S]*?\])/);if(depsMatch){try{meta.dependencies=JSON.parse(depsMatch[1])}catch{}}const useCasesMatch=body.match(/@useCases\s+(\[[\s\S]*?\])/);if(useCasesMatch){try{meta.use_cases=JSON.parse(useCasesMatch[1])}catch{}}const dataDecl={};for(const m of body.matchAll(/@data\s+(\w+)(\??)\s+\{([^}]+)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,type,registry]=m;dataDecl[name]={registry,type:type.trim(),required:optional!=="?"}}const actionDecl={};for(const m of body.matchAll(/@action\s+(\w+)(\??)\s+\{([^}]+?)\s*(?:→|->)\s*([^}]+?)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,input,output,registry]=m;actionDecl[name]={registry,input:input.trim(),output:output.trim(),required:optional!=="?"}}const printPolicy={};for(const m of body.matchAll(/@printPolicy\s+([^\n*]+)/g)){for(const token of m[1].trim().split(/\s+/)){if(!token)continue;const eq=token.indexOf("=");if(eq===-1)printPolicy[token]=true;else printPolicy[token.slice(0,eq)]=token.slice(eq+1)}}if(Object.keys(printPolicy).length>0)meta.print_policy=printPolicy;const one=tag=>body.match(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`))?.[1]?.trim();const all=tag=>[...body.matchAll(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`,"g"))].map(m=>m[1].trim());const names=raw=>{if(raw.startsWith("[")){try{return JSON.parse(raw).map(String)}catch{return[]}}return raw.split(",").map(x=>x.trim()).filter(Boolean)};const summary=one("summary");if(summary)meta.summary=summary.replace(/\s+/g," ");const kit=one("kit");if(kit)meta.kit=kit;const tags=one("tags");if(tags)meta.tags=names(tags);const pageCell=one("pageCell");if(pageCell!==void 0)meta.page_cell=pageCell!=="false";const links=[];for(const raw of all("instead")){const m=raw.match(/^(\w+)\s*(?:-\s*(.+))?$/);if(m)links.push({relation:"instead_of",to:m[1],note:m[2]?.trim()??null})}for(const[tag,relation]of[["parent","child_of"],["child","parent_of"]]){for(const raw of all(tag))for(const name of names(raw))links.push({relation,to:name,note:null})}for(const raw of all("related"))for(const name of names(raw))links.push({relation:"related",to:name,note:null});if(links.length>0)meta.links=links;if(/(?:^|\n)\s*@size\b/.test(body))meta.size=true;if(Object.keys(dataDecl).length>0||Object.keys(actionDecl).length>0){meta.props_schema=meta.props_schema??{type:"object",properties:{}};meta.props_schema.contract_version=meta.props_schema.contract_version??"1.0";if(Object.keys(dataDecl).length>0)meta.props_schema.data=dataDecl;if(Object.keys(actionDecl).length>0)meta.props_schema.actions=actionDecl}return meta}function collectReferencedKeys(specs){const queries=new Set;const mutations=new Set;for(const spec of specs){const data=spec.props_schema?.data??{};const actions=spec.props_schema?.actions??{};for(const decl of Object.values(data)){if(decl?.registry)queries.add(decl.registry)}for(const decl of Object.values(actions)){if(decl?.registry)mutations.add(decl.registry)}}return{queries,mutations}}function projectContractsManifest(specs){const out={contract_version:"1.0",components:{}};for(const spec of specs){const ps=spec.props_schema;const pp=spec.print_policy;if(!pp&&(!ps||!ps.data&&!ps.actions))continue;out.components[spec.component_name]={contract_version:ps?.contract_version??"1.0",...ps?.data?{data:ps.data}:{},...ps?.actions?{actions:ps.actions}:{},...pp?{print_policy:pp}:{}}}return out}function toDisplayName(pascal){return pascal.replace(/([A-Z])/g," $1").replace(/^./,s=>s.toUpperCase()).trim()}function inferComponentType(name){const n=name.toLowerCase();if(n.includes("chart")||n.includes("graph"))return"chart";if(n.includes("simulator")||n.includes("slider")||n.includes("interactive"))return"interactive";if(n.includes("header")||n.includes("layout")||n.includes("container"))return"layout";if(n.includes("card")||n.includes("tile")||n.includes("grid")||n.includes("strip")||n.includes("box"))return"display";return"ui"}function readExportsFromIndex(repoRoot,entryRelative="src/index.ts"){const indexPath=resolve(repoRoot,entryRelative);const indexSource=readFileSync(indexPath,"utf8");const defaultAsPattern=/export\s*\{\s*default\s+as\s+([A-Z][A-Za-z0-9_]*)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const namedExportPattern=/export\s*\{\s*([^}]+?)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const out=[];for(const match of indexSource.matchAll(defaultAsPattern)){out.push({name:match[1],source:match[2]})}for(const match of indexSource.matchAll(namedExportPattern)){if(/\bdefault\s+as\b/.test(match[1]))continue;const source=match[2];for(const raw of match[1].split(",")){const trimmed=raw.trim();if(!trimmed)continue;const aliasMatch=trimmed.match(/^([A-Za-z0-9_]+)\s+as\s+([A-Za-z0-9_]+)$/);const name=aliasMatch?aliasMatch[2]:trimmed;if(!/^[A-Z][A-Za-z0-9_]*$/.test(name))continue;if(out.some(e=>e.name===name))continue;out.push({name,source})}}return out}function escapeRegExp(literal){return literal.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function findBalancedEnd(text,openIdx){let depth=0;let i=openIdx;let inString=null;while(i<text.length){const ch=text[i];if(inString){if(ch==="\\")i++;else if(ch===inString)inString=null}else if(ch==='"'||ch==="'"){inString=ch}else if(ch==="{"||ch==="["){depth++}else if(ch==="}"||ch==="]"){depth--;if(depth===0)return i}i++}return-1}const TYPE_ALIASES={ValueSpec:{type:"object",properties:{value:{type:"number"},format:{type:"string"},precision:{type:"number"},signed:{type:"boolean"},unit:{type:"string"}},required:["value","format"]}};TYPE_ALIASES.FormattableValue={oneOf:[{type:"string"},TYPE_ALIASES.ValueSpec]};TYPE_ALIASES.ReactNode={};TYPE_ALIASES.Point={type:"object",properties:{x:{oneOf:[{type:"string"},{type:"number"}]},y:{type:"number"}},required:["x","y"]};TYPE_ALIASES.Series={type:"object",properties:{id:{type:"string"},label:{type:"string"},color:{type:"string"},strokeDasharray:{type:"string"},data:{type:"array",items:TYPE_ALIASES.Point}},required:["id","data"]};function expandTypeAlias(raw){const key=String(raw).trim();return Object.prototype.hasOwnProperty.call(TYPE_ALIASES,key)?structuredClone(TYPE_ALIASES[key]):null}function toSchemaFragment(raw,shapes){const token=String(raw).trim();const alias=expandTypeAlias(token);if(alias)return alias;const members=splitTopLevel(token,"|");if(members.length>1){const literals=members.map(m=>m.match(/^(['"])(.*)\1$/)?.[2]);if(literals.every(l=>l!==void 0))return{type:"string",enum:literals};return{oneOf:members.map(m=>toSchemaFragment(m,shapes))}}const element=token.endsWith("[]")?token.slice(0,-2).replace(/^\((.*)\)$/,"$1"):token.match(/^(?:Readonly)?Array<(.+)>$/)?.[1];if(element!==void 0){const items=toSchemaFragment(element,shapes);return"x-type"in items?{type:"array"}:{type:"array",items}}if(token.startsWith("{")&&token.endsWith("}"))return objectLiteral(token,shapes);const shape=shapes&&Object.prototype.hasOwnProperty.call(shapes.raw,token)?resolveShape(token,shapes):null;if(shape)return shape;const literal=token.match(/^(['"])(.*)\1$/);if(literal)return{type:"string",enum:[literal[2]]};if(/^\[.*\]$/.test(token))return{type:"array"};if(token==="null")return{type:"null"};if(/^(Readonly)?Record<.*>$/.test(token))return{type:"object"};const type=normalizeType(token);return["boolean","number","string","array","object"].includes(type)?{type}:{"x-type":token}}function readShapes(body){const raw={};for(const m of body.matchAll(/@shape\s+([A-Z]\w*)\s*(?=\{)/g)){const open=m.index+m[0].length;const end=findBalancedEnd(body,open);if(end>open)raw[m[1]]=body.slice(open,end+1).replace(/\s+/g," ")}return raw}function resolveShape(name,shapes){if(shapes.resolving.has(name))return{type:"object"};shapes.resolving.add(name);try{return toSchemaFragment(shapes.raw[name],shapes)}finally{shapes.resolving.delete(name)}}function objectLiteral(token,shapes){const schema={type:"object",properties:{}};const required=[];for(const field of splitTopLevel(token.slice(1,-1),",")){if(field==="..."){schema.additionalProperties=true;continue}const m=field.match(/^(['"]?)([\w$-]+)\1(\?)?\s*:\s*([\s\S]+)$/);if(!m)continue;schema.properties[m[2]]=toSchemaFragment(m[4],shapes);if(!m[3])required.push(m[2])}if(required.length)schema.required=required;return schema}function splitTopLevel(text,sep){const parts=[];let depth=0;let quote=null;let start=0;for(let i=0;i<text.length;i++){const ch=text[i];if(quote){if(ch===quote)quote=null}else if(ch==='"'||ch==="'")quote=ch;else if("[{(<".includes(ch))depth++;else if("]})>".includes(ch))depth--;else if(ch===sep&&depth===0){parts.push(text.slice(start,i).trim());start=i+1}}parts.push(text.slice(start).trim());return parts.filter(Boolean)}function normalizeType(raw){const t=raw.toLowerCase().trim();if(t==="boolean"||t==="bool")return"boolean";if(t==="number"||t==="integer"||t==="int"||t==="float")return"number";if(t==="string")return"string";if(t.endsWith("[]")||t.startsWith("array"))return"array";if(t.startsWith("{")||t==="object")return"object";return raw.trim()}export{collectReferencedKeys,expandTypeAlias,hasComponentJSDoc,inferComponentType,parseJSDocBlock,parseJSDocFile,projectContractsManifest,readExportsFromIndex,resolveComponentFilePath,toDisplayName,toRepoRelative,toSchemaFragment};
@@ -0,0 +1,15 @@
1
+ import type { KnowledgeLink, KnowledgeRow } from '@peripheries/runtime';
2
+ export type Knowledge = {
3
+ lineage: string;
4
+ rows: KnowledgeRow[];
5
+ links: KnowledgeLink[];
6
+ registry: Record<string, {
7
+ props_schema?: unknown;
8
+ }>;
9
+ scopeLineage?: string;
10
+ scopeRegistry?: Record<string, {
11
+ props_schema?: unknown;
12
+ }>;
13
+ };
14
+ export declare const holdsReference: (v: unknown) => boolean;
15
+ export declare function buildKnowledge(bundle: string, org: string): Promise<Knowledge>;
@@ -0,0 +1,4 @@
1
+ import{existsSync,readdirSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName}from"./jsdoc-parser.mjs";import{pathToFileURL}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";function registryOf(entry){const out={};for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(file&&hasComponentJSDoc(file,name))out[name]={props_schema:parseJSDocFile(file,name).props_schema??void 0}}return out}const repoRoot=REPO_ROOT_;function readDoc(path){const text=readFileSync(path,"utf8");const m=text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!m)throw new Error(`[knowledge] ${path} has no front matter`);const meta={};for(const line of m[1].split("\n")){const kv=line.match(/^([\w-]+):\s*(.*)$/);if(!kv)continue;const raw=kv[2].trim();try{meta[kv[1]]=JSON.parse(raw)}catch{meta[kv[1]]=raw}}return{meta,body:m[2].trim()}}const docsIn=dir=>existsSync(resolve(repoRoot,dir))?readdirSync(resolve(repoRoot,dir)).filter(f=>f.endsWith(".md")).sort().map(f=>({key:f.replace(/\.md$/,""),...readDoc(resolve(repoRoot,dir,f))})):[];const str=v=>typeof v==="string"?v:void 0;const strs=v=>Array.isArray(v)?v.map(String):[];function oneLine(text,fallback){const first=(text??"").split(/(?<=[.!?])\s+/)[0]?.trim()||fallback;if(first.length<=200)return first;return`${first.slice(0,199).replace(/\s+\S*$/,"")}\u2026`}function isPlainJson(v){if(v===null||["string","number","boolean"].includes(typeof v))return true;if(Array.isArray(v))return v.every(isPlainJson);if(typeof v!=="object"||Object.getPrototypeOf(v)!==Object.prototype||"$$typeof"in v)return false;return Object.values(v).every(x=>x===void 0||isPlainJson(x))}async function fixtureExamples(componentFile){const base=componentFile.replace(/\.tsx?$/,"");const file=[".fixtures.ts",".fixtures.tsx"].map(ext=>base+ext).find(f=>existsSync(f));if(!file)return[];const mod=await import(file);return(mod.scenarios??[]).filter(s=>["cell","whole-page"].includes(s.surface??"cell")&&s.props&&Object.keys(s.props).length>0&&isPlainJson(s.props)).map(s=>({name:s.name,props:JSON.parse(JSON.stringify(s.props))})).filter(s=>JSON.stringify(s.props).length<4e3).sort((a,b)=>JSON.stringify(a.props).length-JSON.stringify(b.props).length).slice(0,3)}const holdsReference=v=>typeof v==="string"&&(v.startsWith("@")||v.includes("${"))||(Array.isArray(v)?v.some(holdsReference):typeof v==="object"&&v!==null&&Object.values(v).some(holdsReference));async function loadVocabulary(){const file=["src/catalog/vocabulary.ts","src/catalog/vocabulary.tsx"].map(f=>resolve(repoRoot,f)).find(f=>existsSync(f));if(!file)return{};return(await import(pathToFileURL(file).href)).VOCABULARY??{}}async function buildKnowledge(bundle,org){const VOCABULARY=await loadVocabulary();const lineage=`${org}/${bundle}`;const entry=bundle==="main"?"src/index.ts":`src/index.${bundle}.ts`;const scopeLineage=`${org}/main`;const scopeRegistry=bundle==="main"?void 0:registryOf("src/index.ts");const bundleDocPath=resolve(repoRoot,"src/catalog",`${bundle}.md`);if(!existsSync(bundleDocPath))throw new Error(`[knowledge] no src/catalog/${bundle}.md: every lineage says how to compose a page from it.`);const bundleDoc=readDoc(bundleDocPath);const kits=docsIn("src/catalog/kits").filter(k=>(str(k.meta.bundle)??"main")===bundle);const patterns=docsIn("src/catalog/patterns").filter(p=>(str(p.meta.bundle)??"main")===bundle);const notPageCellFamilies=new Set(strs(bundleDoc.meta.not_page_cell_families));const pageCellExceptions=new Set(strs(bundleDoc.meta.page_cells));const notPageCells=new Set(strs(bundleDoc.meta.not_page_cells));const rows=[];const links=[];const registry={};rows.push({kind:"bundle",key:bundle,title:str(bundleDoc.meta.title)??lineage,summary:str(bundleDoc.meta.summary)??"",body:bundleDoc.body,attributes:{kits:kits.map(k=>k.key),patterns:patterns.map(p=>p.key)}});for(const kit of kits){const vocabulary=str(kit.meta.vocabulary);if(vocabulary&&!VOCABULARY[vocabulary])throw new Error(`[knowledge] kit ${kit.key} names vocabulary "${vocabulary}", which has no generator`);rows.push({kind:"kit",key:kit.key,title:str(kit.meta.title)??kit.key,summary:str(kit.meta.summary)??"",body:kit.body,tags:strs(kit.meta.tags),sort:Number(kit.meta.sort??0),attributes:{families:strs(kit.meta.families),...vocabulary?{vocabulary:VOCABULARY[vocabulary]()}:{}}})}const kitOfFamily=new Map(kits.flatMap(k=>strs(k.meta.families).map(f=>[f,k.key])));const cellId=name=>name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!file||!hasComponentJSDoc(file,name))continue;const j=parseJSDocFile(file,name);registry[name]={props_schema:j.props_schema??void 0};const family=j.family??"other";const pageCell=j.page_cell??(notPageCells.has(name)?false:pageCellExceptions.has(name)||!notPageCellFamilies.has(family));const weight=e=>(holdsReference(e)?1e6:0)+JSON.stringify(e).length;const examples=[...j.usage_examples??[]].sort((a,b)=>weight(a)-weight(b));const snippets=examples.length?examples.map((props,i)=>({title:`${name}, example ${i+1}`,cells:[{id:cellId(name),component:name,props}]})):(await fixtureExamples(file)).map(f=>({title:`${name}, ${f.name}`,cells:[{id:cellId(name),component:name,props:f.props}]}));const useCases=j.use_cases?.length?`
2
+
3
+ Use it for:
4
+ ${j.use_cases.map(u=>`- ${u}`).join("\n")}`:"";rows.push({kind:"component",key:name,title:toDisplayName(name),summary:oneLine(j.summary??j.description,`${toDisplayName(name)}.`),body:`${j.agent_instructions??j.description??""}${useCases}`.trim()||null,tags:j.tags??[],snippets,page_cell:pageCell,attributes:{family,...j.print_policy||j.size?{capabilities:{...j.print_policy?{print_policy:j.print_policy}:{},...j.size?{size:true}:{}}}:{}}});const kit=j.kit??kitOfFamily.get(family);if(kit)links.push({from:{kind:"component",key:name},relation:"part_of",to:{kind:"kit",key:kit}});for(const l of j.links??[])links.push({from:{kind:"component",key:name},relation:l.relation,to:{kind:"component",key:l.to},note:l.note})}for(const pattern of patterns){const slug=str(pattern.meta.page);if(!slug)throw new Error(`[knowledge] pattern ${pattern.key} names no page`);const dir=slug.includes("/")?resolve(repoRoot,"src",slug):resolve(repoRoot,"src/pages",slug);const page=JSON.parse(readFileSync(resolve(dir,"page.json"),"utf8"));const mock=existsSync(resolve(dir,"mock.json"))?JSON.parse(readFileSync(resolve(dir,"mock.json"),"utf8")):{};const data={...mock.data??{}};delete data.params;rows.push({kind:"pattern",key:pattern.key,title:str(pattern.meta.title)??pattern.key,summary:str(pattern.meta.summary)??"",body:pattern.body,tags:strs(pattern.meta.tags),sort:Number(pattern.meta.sort??0),snippets:[{title:str(pattern.meta.title)??pattern.key,intent:str(pattern.meta.summary),cells:page.content.cells,layout:page.layout,data}],attributes:{layout:page.layout?.type??"stack",...page.layout?.profile?{profile:page.layout.profile}:{},page:slug}});for(const component of new Set([...page.content.cells.map(c=>c.component).filter(c=>Boolean(c)),...strs(pattern.meta.uses)])){const inMain=scopeRegistry&&!registry[component]&&scopeRegistry[component];links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{...inMain?{lineage:scopeLineage}:{},kind:"component",key:component}})}for(const kit of strs(pattern.meta.kits))links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{kind:"kit",key:kit}})}return{lineage,rows,links,registry,...scopeRegistry?{scopeLineage,scopeRegistry}:{}}}export{buildKnowledge,holdsReference};
@@ -0,0 +1,4 @@
1
+ import{existsSync,readFileSync,readdirSync}from"node:fs";import{basename,dirname,join,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";const REPO_ROOT=REPO_ROOT_;function orgSlug(){try{const json=JSON.parse(readFileSync(join(REPO_ROOT,"template","config","templates.json"),"utf8"));const slug=json?.org_template?.slug;if(typeof slug==="string"&&slug.length>0)return slug}catch{}return basename(REPO_ROOT)}function selfNames(){return new Set([orgSlug(),basename(REPO_ROOT)])}let topLevelCache=null;function topLevel(){if(topLevelCache===null){topLevelCache=new Set(readdirSync(REPO_ROOT))}return topLevelCache}function namesAnotherRepo(segment){if(!segment)return false;if(selfNames().has(segment))return false;if(topLevel().has(segment))return false;return!existsSync(join(REPO_ROOT,segment))}function stripSelfPrefix(ref){for(const name of selfNames()){if(ref.startsWith(`${name}/`))return ref.slice(name.length+1)}return ref}function assertNotSelf(names,where){const mine=selfNames();const collisions=names.filter(n=>mine.has(n));if(collisions.length>0){throw new Error(`[repo-identity] ${where} lists this repo's own name (${collisions.join(", ")}) as foreign.
2
+ This repo is "${orgSlug()}". A repo that is foreign to itself exempts its own
3
+ citations wholesale and reports green having checked nothing \u2014 remove it from that
4
+ list. This is what templating a repo without reviewing the list does.`)}}export{REPO_ROOT,assertNotSelf,namesAnotherRepo,orgSlug,selfNames,stripSelfPrefix};
@@ -0,0 +1 @@
1
+ export const REPO_ROOT: string;
@@ -0,0 +1 @@
1
+ import{resolve}from"node:path";const REPO_ROOT=resolve(process.env.ENGINE_ROOT??process.cwd());export{REPO_ROOT};
@@ -0,0 +1 @@
1
+ import{spawnSync}from"node:child_process";import{existsSync}from"node:fs";import{dirname,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT}from"./root.mjs";const COMMANDS_DIR=resolve(dirname(fileURLToPath(import.meta.url)),"..","commands");const TSX=import.meta.resolve("tsx/esm");const COMMANDS={deploy:"deploy.mjs",check:"check.mjs",upload:"upload-bundle.mjs",register:"register-components.mjs",cutover:"cutover.mjs",knowledge:"emit-knowledge.mjs",contracts:"emit-contracts.mjs","bundle-size":"bundle-size.mjs","check-bundle-leak":"check-bundle-leak.mjs","validate-externals":"validate-externals.mjs","check-wired":"check-wired-bindings.mjs","check-routes":"check-template-routes.mjs","check-design-refs":"check-design-refs.mjs","check-skills":"check-skills.mjs","check-work-orders":"check-work-orders.mjs","check-cutover-ownership":"check-cutover-ownership.mjs","check-fixtures":"check-fixtures.mjs","check-props-schema":"check-props-schema.mjs"};function commandFile(name){if(!COMMANDS[name])return null;const file=resolve(COMMANDS_DIR,COMMANDS[name]);const ts=file.replace(/\.mjs$/,".mts");return existsSync(file)?file:existsSync(ts)?ts:null}function runCommand(name,args=[],env={}){const file=commandFile(name);if(!file)throw new Error(`[engine] unknown command "${name}"`);const r=spawnSync(process.execPath,["--import",TSX,file,...args],{cwd:REPO_ROOT,stdio:"inherit",env:{...process.env,ENGINE_ROOT:REPO_ROOT,...env}});return r.status??1}export{COMMANDS,commandFile,runCommand};