@davidorex/pi-context 0.26.0 → 0.28.1

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 (123) hide show
  1. package/CHANGELOG.md +71 -15
  2. package/README.md +71 -18
  3. package/dist/block-api.d.ts +165 -18
  4. package/dist/block-api.d.ts.map +1 -1
  5. package/dist/block-api.js +800 -57
  6. package/dist/block-api.js.map +1 -1
  7. package/dist/citation-rot-scanner.d.ts +80 -0
  8. package/dist/citation-rot-scanner.d.ts.map +1 -0
  9. package/dist/citation-rot-scanner.js +421 -0
  10. package/dist/citation-rot-scanner.js.map +1 -0
  11. package/dist/content-hash.d.ts +25 -0
  12. package/dist/content-hash.d.ts.map +1 -0
  13. package/dist/content-hash.js +62 -0
  14. package/dist/content-hash.js.map +1 -0
  15. package/dist/context-dir.d.ts +126 -1
  16. package/dist/context-dir.d.ts.map +1 -1
  17. package/dist/context-dir.js +208 -4
  18. package/dist/context-dir.js.map +1 -1
  19. package/dist/context-registry.d.ts +122 -0
  20. package/dist/context-registry.d.ts.map +1 -0
  21. package/dist/context-registry.js +189 -0
  22. package/dist/context-registry.js.map +1 -0
  23. package/dist/context-sdk.d.ts +179 -12
  24. package/dist/context-sdk.d.ts.map +1 -1
  25. package/dist/context-sdk.js +536 -101
  26. package/dist/context-sdk.js.map +1 -1
  27. package/dist/context.d.ts +222 -4
  28. package/dist/context.d.ts.map +1 -1
  29. package/dist/context.js +244 -56
  30. package/dist/context.js.map +1 -1
  31. package/dist/git-env.d.ts +2 -0
  32. package/dist/git-env.d.ts.map +1 -0
  33. package/dist/git-env.js +29 -0
  34. package/dist/git-env.js.map +1 -0
  35. package/dist/index.d.ts +124 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +440 -1125
  38. package/dist/index.js.map +1 -1
  39. package/dist/lens-view.d.ts.map +1 -1
  40. package/dist/lens-view.js +5 -3
  41. package/dist/lens-view.js.map +1 -1
  42. package/dist/migration-registry-loader.d.ts +121 -0
  43. package/dist/migration-registry-loader.d.ts.map +1 -0
  44. package/dist/migration-registry-loader.js +309 -0
  45. package/dist/migration-registry-loader.js.map +1 -0
  46. package/dist/migrations-store.d.ts +141 -0
  47. package/dist/migrations-store.d.ts.map +1 -0
  48. package/dist/migrations-store.js +206 -0
  49. package/dist/migrations-store.js.map +1 -0
  50. package/dist/object-store.d.ts +26 -0
  51. package/dist/object-store.d.ts.map +1 -0
  52. package/dist/object-store.js +104 -0
  53. package/dist/object-store.js.map +1 -0
  54. package/dist/ops-registry.d.ts +44 -0
  55. package/dist/ops-registry.d.ts.map +1 -0
  56. package/dist/ops-registry.js +1223 -0
  57. package/dist/ops-registry.js.map +1 -0
  58. package/dist/orientation.d.ts +16 -0
  59. package/dist/orientation.d.ts.map +1 -0
  60. package/dist/orientation.js +49 -0
  61. package/dist/orientation.js.map +1 -0
  62. package/dist/promote-item.d.ts +28 -0
  63. package/dist/promote-item.d.ts.map +1 -0
  64. package/dist/promote-item.js +257 -0
  65. package/dist/promote-item.js.map +1 -0
  66. package/dist/read-element.d.ts +129 -0
  67. package/dist/read-element.d.ts.map +1 -0
  68. package/dist/read-element.js +296 -0
  69. package/dist/read-element.js.map +1 -0
  70. package/dist/rename-canonical-id.d.ts.map +1 -1
  71. package/dist/rename-canonical-id.js +22 -13
  72. package/dist/rename-canonical-id.js.map +1 -1
  73. package/dist/roadmap-plan.d.ts.map +1 -1
  74. package/dist/roadmap-plan.js +23 -13
  75. package/dist/roadmap-plan.js.map +1 -1
  76. package/dist/schema-validator.d.ts +1 -0
  77. package/dist/schema-validator.d.ts.map +1 -1
  78. package/dist/schema-validator.js +7 -3
  79. package/dist/schema-validator.js.map +1 -1
  80. package/dist/schema-write.d.ts +78 -4
  81. package/dist/schema-write.d.ts.map +1 -1
  82. package/dist/schema-write.js +297 -34
  83. package/dist/schema-write.js.map +1 -1
  84. package/dist/status-vocab.d.ts +1 -0
  85. package/dist/status-vocab.d.ts.map +1 -1
  86. package/dist/status-vocab.js +4 -0
  87. package/dist/status-vocab.js.map +1 -1
  88. package/dist/truncate.d.ts +34 -0
  89. package/dist/truncate.d.ts.map +1 -0
  90. package/dist/truncate.js +85 -0
  91. package/dist/truncate.js.map +1 -0
  92. package/dist/write-schema-migration-tool.d.ts +46 -0
  93. package/dist/write-schema-migration-tool.d.ts.map +1 -0
  94. package/dist/write-schema-migration-tool.js +115 -0
  95. package/dist/write-schema-migration-tool.js.map +1 -0
  96. package/package.json +35 -2
  97. package/samples/blocks/work-orders.json +3 -0
  98. package/samples/conception.json +22 -3
  99. package/samples/migrations.json +133 -0
  100. package/samples/schemas/context-contracts.schema.json +19 -4
  101. package/samples/schemas/conventions.schema.json +68 -12
  102. package/samples/schemas/decisions.schema.json +125 -28
  103. package/samples/schemas/features.schema.json +140 -26
  104. package/samples/schemas/framework-gaps.schema.json +171 -26
  105. package/samples/schemas/issues.schema.json +86 -13
  106. package/samples/schemas/layer-plans.schema.json +195 -35
  107. package/samples/schemas/phase.schema.json +18 -3
  108. package/samples/schemas/rationale.schema.json +37 -7
  109. package/samples/schemas/requirements.schema.json +77 -10
  110. package/samples/schemas/research.schema.json +169 -36
  111. package/samples/schemas/spec-reviews.schema.json +88 -18
  112. package/samples/schemas/story.schema.json +18 -3
  113. package/samples/schemas/tasks.schema.json +66 -10
  114. package/samples/schemas/verification.schema.json +70 -12
  115. package/samples/schemas/work-orders.schema.json +195 -0
  116. package/schemas/bootstrap.schema.json +18 -6
  117. package/schemas/config.schema.json +58 -26
  118. package/schemas/context-registry.schema.json +46 -0
  119. package/schemas/migrations.schema.json +123 -0
  120. package/schemas/relations.schema.json +41 -10
  121. package/skill-narrative.md +42 -21
  122. package/skills/pi-context/SKILL.md +159 -65
  123. package/skills/pi-context/references/bundled-resources.md +7 -2
@@ -72,5 +72,6 @@ export declare function validateFromFile(schemaPath: string, data: unknown, labe
72
72
  * Out of scope: writing the migrated form back to disk — call sites that need
73
73
  * persistence handle that themselves via the block-write surface.
74
74
  */
75
+ export declare function validateBlockWithMigrationForDir(substrateDir: string, schemaName: string, data: unknown, registry?: MigrationRegistry): unknown;
75
76
  export declare function validateBlockWithMigration(cwd: string, schemaName: string, data: unknown, registry?: MigrationRegistry): unknown;
76
77
  //# sourceMappingURL=schema-validator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../src/schema-validator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAIvC,OAAO,EAAE,KAAK,iBAAiB,EAAiB,MAAM,wBAAwB,CAAC;AAsE/E;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC;gBAEnB,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE;CAOhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAqB/F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMjF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAgB1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,0BAA0B,CACzC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAgCT"}
1
+ {"version":3,"file":"schema-validator.d.ts","sourceRoot":"","sources":["../src/schema-validator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAIvC,OAAO,EAAE,KAAK,iBAAiB,EAAiB,MAAM,wBAAwB,CAAC;AAuE/E;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC;gBAEnB,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE;CAOhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAqB/F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMjF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAgB1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,gCAAgC,CAC/C,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAgCT;AAED,wBAAgB,0BAA0B,CACzC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAET"}
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import _Ajv from "ajv";
5
5
  import _addFormats from "ajv-formats";
6
- import { schemaPath as schemaPathHelper } from "./context-dir.js";
6
+ import { resolveContextDir, schemaPathForDir as schemaPathForDirHelper } from "./context-dir.js";
7
7
  import { runMigrations } from "./schema-migrations.js";
8
8
  // Node16 module resolution + CJS interop: default import is the module namespace
9
9
  const Ajv = _Ajv.default ?? _Ajv;
@@ -33,6 +33,7 @@ const FRAMEWORK_SCHEMA_NAMES = [
33
33
  "config",
34
34
  "relations",
35
35
  "bootstrap",
36
+ "context-registry",
36
37
  "priority",
37
38
  "status",
38
39
  "severity",
@@ -191,8 +192,8 @@ export function validateFromFile(schemaPath, data, label) {
191
192
  * Out of scope: writing the migrated form back to disk — call sites that need
192
193
  * persistence handle that themselves via the block-write surface.
193
194
  */
194
- export function validateBlockWithMigration(cwd, schemaName, data, registry) {
195
- const schemaPath = schemaPathHelper(cwd, schemaName);
195
+ export function validateBlockWithMigrationForDir(substrateDir, schemaName, data, registry) {
196
+ const schemaPath = schemaPathForDirHelper(substrateDir, schemaName);
196
197
  if (!fs.existsSync(schemaPath)) {
197
198
  throw new Error(`validateBlockWithMigration: schema file not found at ${schemaPath}`);
198
199
  }
@@ -216,4 +217,7 @@ export function validateBlockWithMigration(cwd, schemaName, data, registry) {
216
217
  }
217
218
  return validate(schema, toValidate, schemaName);
218
219
  }
220
+ export function validateBlockWithMigration(cwd, schemaName, data, registry) {
221
+ return validateBlockWithMigrationForDir(resolveContextDir(cwd), schemaName, data, registry);
222
+ }
219
223
  //# sourceMappingURL=schema-validator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-validator.js","sourceRoot":"","sources":["../src/schema-validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,IAAI,MAAM,KAAK,CAAC;AACvB,OAAO,WAAW,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAA0B,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE/E,iFAAiF;AACjF,MAAM,GAAG,GAAI,IAAY,CAAC,OAAO,IAAI,IAAI,CAAC;AAC1C,MAAM,UAAU,GAAI,WAAmB,CAAC,OAAO,IAAI,WAAW,CAAC;AAC/D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AACxD,UAAU,CAAC,GAAG,CAAC,CAAC;AAEhB;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,sBAAsB,GAAG;IAC9B,QAAQ;IACR,WAAW;IACX,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,OAAO;IACP,qBAAqB;CACZ,CAAC;AAEX,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,4EAA4E;AAC5E,8EAA8E;AAC9E,mDAAmD;AACnD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AAE9D,KAAK,MAAM,IAAI,IAAI,sBAAsB,EAAE,CAAC;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,IAAI,cAAc,CAAC,CAAC;IAClE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,8DAA8D,UAAU,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACjD,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACd,sDAAsD,UAAU,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACvH,CAAC;IACH,CAAC;IACD,qEAAqE;IACrE,qEAAqE;IACrE,yEAAyE;IACzE,0BAA0B;IAC1B,MAAM,EAAE,GAAI,MAA2B,CAAC,GAAG,CAAC;IAC5C,IAAI,EAAE,IAAI,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC;QAC7B,SAAS;IACV,CAAC;IACD,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAChC,KAAK,CAAS;IACd,MAAM,CAAgB;IAE/B,YAAY,KAAa,EAAE,MAAqB;QAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,YAAY,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtF,KAAK,CAAC,yBAAyB,KAAK,KAAK,OAAO,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,MAA+B,EAAE,IAAa,EAAE,KAAa;IACrF,MAAM,EAAE,GAAG,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,IAAI,KAAc,CAAC;IACnB,IAAI,MAAwC,CAAC;IAC7C,IAAI,EAAE,EAAE,CAAC;QACR,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACjC,IAAI,MAAM,EAAE,CAAC;YACZ,KAAK,GAAG,MAAM,CAAC,IAAI,CAAY,CAAC;YAChC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QACxB,CAAC;aAAM,CAAC;YACP,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAY,CAAC;YAC9C,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QACrB,CAAC;IACF,CAAC;SAAM,CAAC;QACP,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAY,CAAC;QAC9C,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IACrB,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,eAAe,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAe,EAAE,KAAa;IACvE,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,CAAC,MAAkD,CAAC,CAAC;IACrF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,IAAa,EAAE,KAAa;IAChF,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACJ,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,0BAA0B,UAAU,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,gCAAgC,UAAU,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpH,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,0BAA0B,CACzC,GAAW,EACX,UAAkB,EAClB,IAAa,EACb,QAA4B;IAE5B,MAAM,UAAU,GAAG,gBAAgB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACrD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,wDAAwD,UAAU,EAAE,CAAC,CAAC;IACvF,CAAC;IAED,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACd,+CAA+C,UAAU,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAChH,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAM,CAAC,OAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;IAClG,MAAM,YAAY,GACjB,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,gBAAgB,IAAK,IAAgC;QACxF,CAAC,CAAG,IAAgC,CAAC,cAAqC;QAC1E,CAAC,CAAC,SAAS,CAAC;IAEd,IAAI,UAAU,GAAY,IAAI,CAAC;IAC/B,IAAI,aAAa,IAAI,YAAY,IAAI,aAAa,KAAK,YAAY,EAAE,CAAC;QACrE,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACd,wCAAwC,UAAU,6BAA6B,YAAY,uBAAuB,aAAa,yCAAyC,CACxK,CAAC;QACH,CAAC;QACD,UAAU,GAAG,aAAa,CAAC,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;IACrF,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"schema-validator.js","sourceRoot":"","sources":["../src/schema-validator.ts"],"names":[],"mappings":"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"}
@@ -30,10 +30,65 @@
30
30
  import type { DispatchContext } from "./dispatch-context.js";
31
31
  import { ValidationError } from "./schema-validator.js";
32
32
  /**
33
- * Read a schema from `<contextDir>/schemas/<schemaName>.schema.json` and
34
- * return the parsed object. Returns `null` when the file does not exist —
35
- * absence is a normal pre-write state, not an error. Throws when the file
36
- * exists but is unreadable or contains invalid JSON.
33
+ * The dotted key-paths of every array property at nesting depth ≥ 1 (reachable
34
+ * only by descending through ≥1 enclosing array's `items`) whose item shape
35
+ * declares an `"id"` property. A NESTED id-bearing array is a
36
+ * relationship-as-embedding an id-bearing item that should be a top-level
37
+ * entity joined by a closure-table membership edge, not embedded inside another
38
+ * item's array (content-addressed substrate identity, Cycle 9.2). Depth-0
39
+ * (top-level) id arrays are the normal block-item shape and are NOT flagged;
40
+ * nested NON-id arrays (e.g. a list of strings, or objects with no `id`) are NOT
41
+ * flagged.
42
+ *
43
+ * Trigger = an `"id"` DECLARATION on the nested array's item shape — NOT a
44
+ * full identity-field declaration (`collectArrayItemIdentityDecisions` keys on
45
+ * the IDENTITY_DECLARATION_FIELDS union; this guard keys on `id` alone, because
46
+ * an embedded item carrying any local `id` is the relationship-as-embedding
47
+ * smell regardless of whether it also declares oid/content_hash). An `id` is
48
+ * detected however it is declared (Cycle 9.3 hardening over the 9.2 form, which
49
+ * keyed only on `items.properties.id` + one-level local `$ref`):
50
+ * - `items.properties.id`;
51
+ * - `id` in `items.required` (no `properties.id` needed);
52
+ * - any `anyOf`/`oneOf`/`allOf` branch of the item shape declaring an `id`
53
+ * (recursive, any-branch — the branch may itself declare it via `required`);
54
+ * - tuple-form `items` (an array of subschemas) where ANY member declares an `id`;
55
+ * - a one-level `#/definitions/*` | `#/$defs/*` `$ref` resolving to a target
56
+ * that declares an `id` via any of the above.
57
+ *
58
+ * The walk mirrors `collectArrayItemIdentityDecisions` (block-api.ts) —
59
+ * `properties.*` + recurse into array `items` + recurse into object-valued
60
+ * props — extended to traverse composition branches + tuple-`items` members, and
61
+ * carries a `depth` incremented ONLY when it descends through an array's `items`.
62
+ * `$ref` resolution mirrors `resolveBlockItemSchema` (block-api.ts) and is
63
+ * cycle-guarded (a per-path visited set keyed on the resolved pointer string,
64
+ * plus a recursion-depth backstop) so self-referential / mutually-recursive
65
+ * `$defs` cannot loop. Unresolvable / external / unsupported `$ref`s are treated
66
+ * as opaque nodes; the pass NEVER throws (lint pass, not a load-time gate).
67
+ */
68
+ export declare function findNestedIdBearingArrays(schema: Record<string, unknown>): string[];
69
+ /**
70
+ * Throw when `schema` declares any nested id-bearing array (see
71
+ * `findNestedIdBearingArrays`). The message names every offending dotted
72
+ * key-path so the author knows exactly which embedded array to promote to a
73
+ * top-level entity + membership edge. Mirrors the meta-reject throw convention
74
+ * (a plain `Error` carrying a labeled message). A schema with no nested
75
+ * id-bearing array is a no-op.
76
+ */
77
+ export declare function assertNoNestedIdBearingArray(schema: Record<string, unknown>, label: string): void;
78
+ /**
79
+ * Dir-targeted twin of `readSchema`: read a schema from
80
+ * `<substrateDir>/schemas/<schemaName>.schema.json` and return the parsed
81
+ * object. Returns `null` when absent (a normal pre-write state). Throws when the
82
+ * file exists but is unreadable or holds invalid JSON. Does NOT meta-validate
83
+ * (mirrors `readSchema`).
84
+ */
85
+ export declare function readSchemaForDir(substrateDir: string, schemaName: string): object | null;
86
+ /**
87
+ * Read a schema from `<resolveContextDir(cwd)>/schemas/<schemaName>.schema.json`
88
+ * and return the parsed object. Thin cwd wrapper over `readSchemaForDir` — the
89
+ * read/parse logic lives in the ForDir body. Returns `null` when the file does
90
+ * not exist — absence is a normal pre-write state, not an error. Throws when the
91
+ * file exists but is unreadable or contains invalid JSON.
37
92
  *
38
93
  * Note: this reader does NOT meta-validate. Callers that need a guarantee
39
94
  * the file holds a valid schema should pipe the result through
@@ -53,6 +108,18 @@ export declare function readSchema(cwd: string, schemaName: string): object | nu
53
108
  * Overwrites any existing schema at the target path; callers that need
54
109
  * mutate-in-place semantics should use `updateSchema`.
55
110
  */
111
+ /**
112
+ * Dir-targeted twin of `writeSchema`: atomically write `schema` to
113
+ * `<substrateDir>/schemas/<schemaName>.schema.json` after meta-validation + the
114
+ * nested-id guard. Carries the full validate-then-write body; `writeSchema`
115
+ * delegates here via `resolveContextDir(cwd)`. Cross-substrate consumers (Cycle
116
+ * H migration / land-identity-fields) target a non-active substrate directly.
117
+ *
118
+ * `ctx` is accepted and IGNORED: schema files carry no author-attestation
119
+ * fields (mirrors `writeSchemaChecked`'s ctx parameter). Present for call-site
120
+ * parity with the attestation-aware block-write surfaces.
121
+ */
122
+ export declare function writeSchemaForDir(substrateDir: string, schemaName: string, schema: object, ctx?: DispatchContext): void;
56
123
  export declare function writeSchema(cwd: string, schemaName: string, schema: object): void;
57
124
  /**
58
125
  * Op-correct create-or-replace of a whole schema body, layered over
@@ -95,6 +162,13 @@ export declare function writeSchema(cwd: string, schemaName: string, schema: obj
95
162
  * supplied body and returns `{ written: false }` without touching disk —
96
163
  * keeping ONE validation path (no re-implemented validation for the preview).
97
164
  */
165
+ export declare function writeSchemaCheckedForDir(substrateDir: string, schemaName: string, schema: object, operation: "create" | "replace", ctx?: DispatchContext, opts?: {
166
+ dryRun?: boolean;
167
+ }): {
168
+ written: boolean;
169
+ operation: "create" | "replace";
170
+ schemaPath: string;
171
+ };
98
172
  export declare function writeSchemaChecked(cwd: string, schemaName: string, schema: object, operation: "create" | "replace", ctx?: DispatchContext, opts?: {
99
173
  dryRun?: boolean;
100
174
  }): {
@@ -1 +1 @@
1
- {"version":3,"file":"schema-write.d.ts","sourceRoot":"","sources":["../src/schema-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAA6B,MAAM,uBAAuB,CAAC;AAgBnF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAuBjF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,kBAAkB,CACjC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,QAAQ,GAAG,SAAS,EAC/B,GAAG,CAAC,EAAE,eAAe,EACrB,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAgC3E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAoBxG;AAID,OAAO,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"schema-write.d.ts","sourceRoot":"","sources":["../src/schema-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAA6B,MAAM,uBAAuB,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAyLnF;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAOjG;AAgBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBxF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEzE;AAED;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAChC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,eAAe,GACnB,IAAI,CA+BN;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEjF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,wBAAwB,CACvC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,QAAQ,GAAG,SAAS,EAC/B,GAAG,CAAC,EAAE,eAAe,EACrB,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAoC3E;AAED,wBAAgB,kBAAkB,CACjC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,QAAQ,GAAG,SAAS,EAC/B,GAAG,CAAC,EAAE,eAAe,EACrB,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAE3E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAoBxG;AAID,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -29,33 +29,254 @@
29
29
  */
30
30
  import fs from "node:fs";
31
31
  import path from "node:path";
32
- import { schemaPath } from "./context-dir.js";
32
+ import { resolveContextDir, schemaPathForDir } from "./context-dir.js";
33
33
  import { ValidationError, validateSchemaAgainstMeta } from "./schema-validator.js";
34
34
  /**
35
- * `<resolveContextDir(cwd)>/schemas/<schemaName>.schema.json` canonical schema
36
- * path, routed through `schemaPath` (context-dir) so write resolution is
37
- * BYTE-IDENTICAL to read resolution (FGAP-079 / DEC-0045). Previously this was
38
- * based on a config.root-honoring path-builder, which diverged from the
39
- * pointer-canonical read side (`schemaPath` / `validateBlockWithMigration`) under
40
- * a non-default `config.root` schemas would be written where reads/validation
41
- * could not find them. Delegating to `schemaPath` collapses the two paths to one
42
- * source AND inherits its `assertSubstrateName` guard (path-traversal rejection).
35
+ * The dotted key-paths of every array property at nesting depth ≥ 1 (reachable
36
+ * only by descending through ≥1 enclosing array's `items`) whose item shape
37
+ * declares an `"id"` property. A NESTED id-bearing array is a
38
+ * relationship-as-embedding an id-bearing item that should be a top-level
39
+ * entity joined by a closure-table membership edge, not embedded inside another
40
+ * item's array (content-addressed substrate identity, Cycle 9.2). Depth-0
41
+ * (top-level) id arrays are the normal block-item shape and are NOT flagged;
42
+ * nested NON-id arrays (e.g. a list of strings, or objects with no `id`) are NOT
43
+ * flagged.
44
+ *
45
+ * Trigger = an `"id"` DECLARATION on the nested array's item shape — NOT a
46
+ * full identity-field declaration (`collectArrayItemIdentityDecisions` keys on
47
+ * the IDENTITY_DECLARATION_FIELDS union; this guard keys on `id` alone, because
48
+ * an embedded item carrying any local `id` is the relationship-as-embedding
49
+ * smell regardless of whether it also declares oid/content_hash). An `id` is
50
+ * detected however it is declared (Cycle 9.3 hardening over the 9.2 form, which
51
+ * keyed only on `items.properties.id` + one-level local `$ref`):
52
+ * - `items.properties.id`;
53
+ * - `id` in `items.required` (no `properties.id` needed);
54
+ * - any `anyOf`/`oneOf`/`allOf` branch of the item shape declaring an `id`
55
+ * (recursive, any-branch — the branch may itself declare it via `required`);
56
+ * - tuple-form `items` (an array of subschemas) where ANY member declares an `id`;
57
+ * - a one-level `#/definitions/*` | `#/$defs/*` `$ref` resolving to a target
58
+ * that declares an `id` via any of the above.
59
+ *
60
+ * The walk mirrors `collectArrayItemIdentityDecisions` (block-api.ts) —
61
+ * `properties.*` + recurse into array `items` + recurse into object-valued
62
+ * props — extended to traverse composition branches + tuple-`items` members, and
63
+ * carries a `depth` incremented ONLY when it descends through an array's `items`.
64
+ * `$ref` resolution mirrors `resolveBlockItemSchema` (block-api.ts) and is
65
+ * cycle-guarded (a per-path visited set keyed on the resolved pointer string,
66
+ * plus a recursion-depth backstop) so self-referential / mutually-recursive
67
+ * `$defs` cannot loop. Unresolvable / external / unsupported `$ref`s are treated
68
+ * as opaque nodes; the pass NEVER throws (lint pass, not a load-time gate).
43
69
  */
44
- function schemaWritePath(cwd, schemaName) {
45
- return schemaPath(cwd, schemaName);
70
+ export function findNestedIdBearingArrays(schema) {
71
+ const hits = [];
72
+ // Backstop against pathological array-nesting depth even when the cycle-guard's
73
+ // pointer set fails to catch a loop (e.g. an unbounded chain of distinct inline
74
+ // subschemas reached by descending through array `items`).
75
+ const MAX_DEPTH = 256;
76
+ // SEPARATE structural-recursion backstop. `MAX_DEPTH` only counts array-`items`
77
+ // descents — composition-branch descent (anyOf/oneOf/allOf) and object-property
78
+ // recursion deliberately do NOT increment array-depth, so a pure-inline chain of
79
+ // distinct composition subschemas (no `$ref` at all, e.g. allOf nested 500 deep)
80
+ // would never trip MAX_DEPTH and would recurse unbounded. This counter increments
81
+ // on EVERY structural recursion (object-property + array-items + composition) and
82
+ // bounds total descent regardless of how depth is (or isn't) advanced. The
83
+ // pointer-`visited` guard already terminates ALL `$ref` cycles; this counter only
84
+ // backstops a finite-but-pathologically-deep PURE-INLINE chain (no `$ref`), itself
85
+ // bounded by the finite schema-file size — so the bound can be small with no
86
+ // practical downside, and small is what keeps termination stack-INdependent.
87
+ //
88
+ // Native ceiling, measured in this counter's own units (one `recursion++` per
89
+ // structural step) for the deepest single-recursion-per-level inline chain:
90
+ // - default node stack (~984 KB): RangeError past ~1562 recursion units
91
+ // - `node --stack-size=512`: RangeError past ~976 units
92
+ // - `node --stack-size=256`: RangeError past ~390 units
93
+ // A bound of 1024 therefore does NOT engage before native overflow on a 512 KB or
94
+ // smaller stack — termination would be stack-dependent. 128 sits with large margin
95
+ // below even the ~390-unit ceiling of a 256 KB stack (~67% headroom), so the
96
+ // counter RETURNS before any `RangeError` can escape regardless of stack size,
97
+ // preserving the lint-never-throws contract for a hostile pure-inline chain. It
98
+ // still vastly exceeds any realistic authored nesting (deepest shipped schema nests
99
+ // ~2 levels; real schemas stay well under ~20), and bounds recursion DEPTH per path
100
+ // only — breadth is unbounded, so a schema with hundreds of sibling id-bearing
101
+ // nested arrays still flags every one. Verified empirically at default and
102
+ // constrained (`--stack-size=512`/`256`) stacks against a 5000-deep inline chain.
103
+ const MAX_STRUCT_RECURSION = 128;
104
+ // Resolve a one-level local `$ref` on a schema node, mirroring
105
+ // resolveBlockItemSchema's supported refs (#/definitions/* | #/$defs/*).
106
+ // Unresolvable / unsupported / external refs are returned as the original node
107
+ // (opaque — the caller then finds no shape-bearing keys and stops). A `$ref`
108
+ // whose resolved pointer has already been seen on this descent path returns an
109
+ // empty object so a self-referential / mutually-recursive `$defs` cannot loop.
110
+ // Never throws — this is a lint pass, not a load-time validation gate. On a
111
+ // successful resolution the resolved pointer string is recorded in `visited`
112
+ // (which the caller passes as a path-local clone so sibling uses of the same
113
+ // `$ref` are not pruned — only cycles along one path are stopped).
114
+ function deref(node, visited) {
115
+ const ref = typeof node.$ref === "string" ? node.$ref : undefined;
116
+ if (!ref)
117
+ return node;
118
+ const m = /^#\/(definitions|\$defs)\/(.+)$/.exec(ref);
119
+ if (!m)
120
+ return node;
121
+ if (visited.has(ref))
122
+ return {};
123
+ const bag = schema[m[1]];
124
+ const target = bag?.[m[2]];
125
+ if (!target || typeof target !== "object")
126
+ return node;
127
+ visited.add(ref);
128
+ return target;
129
+ }
130
+ // The composition keywords whose members are arrays of subschemas; an `id`
131
+ // (or a deeper nested array) can be buried inside any branch.
132
+ function compositionBranches(node) {
133
+ const out = [];
134
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
135
+ const arr = node[key];
136
+ if (Array.isArray(arr)) {
137
+ for (const member of arr) {
138
+ if (member && typeof member === "object")
139
+ out.push(member);
140
+ }
141
+ }
142
+ }
143
+ return out;
144
+ }
145
+ // True iff this subschema declares an `id` — directly via `properties.id`, via
146
+ // `required` membership, or through ANY anyOf/oneOf/allOf branch (recursive,
147
+ // any-branch). Cycle-guarded through `deref`; the visited set is cloned per
148
+ // descent so it stops loops without pruning sibling references.
149
+ function declaresId(nodeRaw, visited, depth) {
150
+ if (!nodeRaw || typeof nodeRaw !== "object" || depth > MAX_DEPTH)
151
+ return false;
152
+ const node = deref(nodeRaw, visited);
153
+ const props = node.properties;
154
+ if (props && Object.hasOwn(props, "id"))
155
+ return true;
156
+ if (Array.isArray(node.required) && node.required.includes("id"))
157
+ return true;
158
+ for (const branch of compositionBranches(node)) {
159
+ if (declaresId(branch, new Set(visited), depth + 1))
160
+ return true;
161
+ }
162
+ return false;
163
+ }
164
+ // True iff the `items` of an array property is id-bearing: an object subschema →
165
+ // `declaresId`; a tuple (array of subschemas) → ANY member `declaresId`.
166
+ function itemsDeclareId(itemsRaw, visited) {
167
+ if (Array.isArray(itemsRaw)) {
168
+ return itemsRaw.some((member) => member !== null &&
169
+ typeof member === "object" &&
170
+ declaresId(member, new Set(visited), 0));
171
+ }
172
+ if (itemsRaw && typeof itemsRaw === "object") {
173
+ return declaresId(itemsRaw, new Set(visited), 0);
174
+ }
175
+ return false;
176
+ }
177
+ // Structural descent. From a (deref'd) node, visit every shape that can host a
178
+ // nested array: own `properties`, each composition branch's `properties`, and
179
+ // each tuple-`items` member. `depth` increments ONLY when descending through an
180
+ // array's `items`; an array property is the forbidden relationship-as-embedding
181
+ // when it is reached at `depth >= 1` AND its items declare an `id`. `recursion`
182
+ // increments on EVERY structural step (checked in `walk` AND at the composition
183
+ // loop entry) so a composition chain that never advances `depth` still terminates.
184
+ function descendShape(node, depth, keyPath, visited, recursion) {
185
+ const props = node.properties;
186
+ if (props) {
187
+ for (const [k, vRaw] of Object.entries(props)) {
188
+ if (!vRaw || typeof vRaw !== "object")
189
+ continue;
190
+ const v = vRaw;
191
+ if (v.type === "array" && v.items) {
192
+ // The id-peek uses a FRESH cycle-guard, independent of the structural
193
+ // `visited` set. "Does this item declare an id" is a distinct question
194
+ // from "have I already structurally walked this $ref"; reusing the
195
+ // polluted structural set would short-circuit a $ref back to a $def that
196
+ // declares the id (the descent already added that pointer), yielding a
197
+ // false negative on $ref-cyclic id-bearing schemas. `declaresId` has its
198
+ // own per-branch clone + MAX_DEPTH backstop and only recurses through
199
+ // composition (not arbitrary properties), so a fresh seed cannot loop.
200
+ if (depth >= 1 && itemsDeclareId(v.items, new Set())) {
201
+ hits.push(`${keyPath}${k}`);
202
+ }
203
+ // Descend into the array's items at depth+1 to catch deeper nesting,
204
+ // whether `items` is a single subschema or a tuple of subschemas.
205
+ const itemMembers = Array.isArray(v.items) ? v.items : [v.items];
206
+ for (const memberRaw of itemMembers) {
207
+ if (memberRaw && typeof memberRaw === "object") {
208
+ walk(memberRaw, depth + 1, `${keyPath}${k}.`, new Set(visited), recursion + 1);
209
+ }
210
+ }
211
+ }
212
+ else {
213
+ // Object-valued (non-array) property: recurse at the SAME depth so a
214
+ // nested object wrapper does not by itself count as array nesting.
215
+ walk(v, depth, `${keyPath}${k}.`, new Set(visited), recursion + 1);
216
+ }
217
+ }
218
+ }
219
+ // A nested array can be buried inside a composition branch; traverse each at
220
+ // the SAME `depth` (a branch is not itself an array `items` descent). Route the
221
+ // composition descent through `walk` — NOT a direct `descendShape` self-call — so
222
+ // the recursion is bounded by `walk`'s MAX_DEPTH / MAX_STRUCT_RECURSION backstops
223
+ // just like every other path, and pass the SAME running `visited` (a path-local
224
+ // clone that PRESERVES the ancestor refs already seen). `walk`'s `deref` then both
225
+ // records the resolved pointer into that clone AND checks `visited.has(ref)` on it,
226
+ // so a composition-routed `$ref` back to an ancestor `$def` already on this path
227
+ // resolves to `{}` (cycle caught) rather than recursing forever. A pure-inline
228
+ // composition chain (no `$ref`, so the pointer guard never fires) is instead bounded
229
+ // by MAX_STRUCT_RECURSION, which `walk` increments on entry.
230
+ for (const branch of compositionBranches(node)) {
231
+ if (branch && typeof branch === "object") {
232
+ walk(branch, depth, keyPath, new Set(visited), recursion + 1);
233
+ }
234
+ }
235
+ }
236
+ function walk(node, depth, keyPath, visited, recursion) {
237
+ if (!node || typeof node !== "object" || depth > MAX_DEPTH || recursion > MAX_STRUCT_RECURSION)
238
+ return;
239
+ descendShape(deref(node, visited), depth, keyPath, visited, recursion);
240
+ }
241
+ walk(schema, 0, "", new Set(), 0);
242
+ return hits;
46
243
  }
47
244
  /**
48
- * Read a schema from `<contextDir>/schemas/<schemaName>.schema.json` and
49
- * return the parsed object. Returns `null` when the file does not exist —
50
- * absence is a normal pre-write state, not an error. Throws when the file
51
- * exists but is unreadable or contains invalid JSON.
52
- *
53
- * Note: this reader does NOT meta-validate. Callers that need a guarantee
54
- * the file holds a valid schema should pipe the result through
55
- * `validateSchemaAgainstMeta` themselves; `updateSchema` already does.
245
+ * Throw when `schema` declares any nested id-bearing array (see
246
+ * `findNestedIdBearingArrays`). The message names every offending dotted
247
+ * key-path so the author knows exactly which embedded array to promote to a
248
+ * top-level entity + membership edge. Mirrors the meta-reject throw convention
249
+ * (a plain `Error` carrying a labeled message). A schema with no nested
250
+ * id-bearing array is a no-op.
56
251
  */
57
- export function readSchema(cwd, schemaName) {
58
- const p = schemaWritePath(cwd, schemaName);
252
+ export function assertNoNestedIdBearingArray(schema, label) {
253
+ const paths = findNestedIdBearingArrays(schema);
254
+ if (paths.length) {
255
+ throw new Error(`${label}: nested id-bearing arrays are forbidden (id-bearing items must be top-level blocks joined by membership edges, not embedded): ${paths.join(", ")}`);
256
+ }
257
+ }
258
+ /**
259
+ * `<substrateDir>/schemas/<schemaName>.schema.json` — canonical schema path for
260
+ * an EXPLICIT substrate directory, routed through `schemaPathForDir`
261
+ * (context-dir) so write resolution is BYTE-IDENTICAL to read resolution
262
+ * (FGAP-079 / DEC-0045) and inherits its `assertSubstrateName` guard
263
+ * (path-traversal rejection). This is the dir-targeted twin of the cwd-resolved
264
+ * write path; the cwd forms below delegate here via `resolveContextDir(cwd)`,
265
+ * so a cwd call lands BYTE-IDENTICALLY where it did when this routed through the
266
+ * cwd-resolving `schemaPath`.
267
+ */
268
+ function schemaWritePathForDir(substrateDir, schemaName) {
269
+ return schemaPathForDir(substrateDir, schemaName);
270
+ }
271
+ /**
272
+ * Dir-targeted twin of `readSchema`: read a schema from
273
+ * `<substrateDir>/schemas/<schemaName>.schema.json` and return the parsed
274
+ * object. Returns `null` when absent (a normal pre-write state). Throws when the
275
+ * file exists but is unreadable or holds invalid JSON. Does NOT meta-validate
276
+ * (mirrors `readSchema`).
277
+ */
278
+ export function readSchemaForDir(substrateDir, schemaName) {
279
+ const p = schemaWritePathForDir(substrateDir, schemaName);
59
280
  if (!fs.existsSync(p))
60
281
  return null;
61
282
  let raw;
@@ -63,15 +284,29 @@ export function readSchema(cwd, schemaName) {
63
284
  raw = fs.readFileSync(p, "utf-8");
64
285
  }
65
286
  catch (err) {
66
- throw new Error(`readSchema: failed to read ${p}: ${err instanceof Error ? err.message : String(err)}`);
287
+ throw new Error(`readSchemaForDir: failed to read ${p}: ${err instanceof Error ? err.message : String(err)}`);
67
288
  }
68
289
  try {
69
290
  return JSON.parse(raw);
70
291
  }
71
292
  catch (err) {
72
- throw new Error(`readSchema: invalid JSON in ${p}: ${err instanceof Error ? err.message : String(err)}`);
293
+ throw new Error(`readSchemaForDir: invalid JSON in ${p}: ${err instanceof Error ? err.message : String(err)}`);
73
294
  }
74
295
  }
296
+ /**
297
+ * Read a schema from `<resolveContextDir(cwd)>/schemas/<schemaName>.schema.json`
298
+ * and return the parsed object. Thin cwd wrapper over `readSchemaForDir` — the
299
+ * read/parse logic lives in the ForDir body. Returns `null` when the file does
300
+ * not exist — absence is a normal pre-write state, not an error. Throws when the
301
+ * file exists but is unreadable or contains invalid JSON.
302
+ *
303
+ * Note: this reader does NOT meta-validate. Callers that need a guarantee
304
+ * the file holds a valid schema should pipe the result through
305
+ * `validateSchemaAgainstMeta` themselves; `updateSchema` already does.
306
+ */
307
+ export function readSchema(cwd, schemaName) {
308
+ return readSchemaForDir(resolveContextDir(cwd), schemaName);
309
+ }
75
310
  /**
76
311
  * Atomically write `schema` to `<contextDir>/schemas/<schemaName>.schema.json`
77
312
  * after validating it against the JSON Schema draft-07 meta-schema.
@@ -85,10 +320,28 @@ export function readSchema(cwd, schemaName) {
85
320
  * Overwrites any existing schema at the target path; callers that need
86
321
  * mutate-in-place semantics should use `updateSchema`.
87
322
  */
88
- export function writeSchema(cwd, schemaName, schema) {
89
- const p = schemaWritePath(cwd, schemaName);
323
+ /**
324
+ * Dir-targeted twin of `writeSchema`: atomically write `schema` to
325
+ * `<substrateDir>/schemas/<schemaName>.schema.json` after meta-validation + the
326
+ * nested-id guard. Carries the full validate-then-write body; `writeSchema`
327
+ * delegates here via `resolveContextDir(cwd)`. Cross-substrate consumers (Cycle
328
+ * H migration / land-identity-fields) target a non-active substrate directly.
329
+ *
330
+ * `ctx` is accepted and IGNORED: schema files carry no author-attestation
331
+ * fields (mirrors `writeSchemaChecked`'s ctx parameter). Present for call-site
332
+ * parity with the attestation-aware block-write surfaces.
333
+ */
334
+ export function writeSchemaForDir(substrateDir, schemaName, schema, ctx) {
335
+ void ctx; // accepted for call-site parity; schema files carry no author fields.
336
+ const p = schemaWritePathForDir(substrateDir, schemaName);
90
337
  // (1) Meta-schema validation BEFORE any disk activity.
91
338
  validateSchemaAgainstMeta(schema, `schema '${schemaName}'`);
339
+ // (1b) Substrate-shape guard: reject a schema that embeds an id-bearing item
340
+ // inside another item's array (nested id-bearing array) — a
341
+ // relationship-as-embedding that must be a top-level entity + membership edge
342
+ // (content-addressed substrate identity, Cycle 9.2). Runs after meta-validation
343
+ // (so the body is structurally a schema) and before any disk activity.
344
+ assertNoNestedIdBearingArray(schema, `schema '${schemaName}'`);
92
345
  // (2) Ensure the schemas/ directory exists.
93
346
  fs.mkdirSync(path.dirname(p), { recursive: true });
94
347
  // (3) Atomic write: tmp + rename.
@@ -105,9 +358,12 @@ export function writeSchema(cwd, schemaName, schema) {
105
358
  /* best-effort cleanup — surface the original error below */
106
359
  }
107
360
  const msg = err instanceof Error ? err.message : String(err);
108
- throw new Error(`writeSchema: failed to write ${p}: ${msg}`);
361
+ throw new Error(`writeSchemaForDir: failed to write ${p}: ${msg}`);
109
362
  }
110
363
  }
364
+ export function writeSchema(cwd, schemaName, schema) {
365
+ writeSchemaForDir(resolveContextDir(cwd), schemaName, schema);
366
+ }
111
367
  /**
112
368
  * Op-correct create-or-replace of a whole schema body, layered over
113
369
  * `writeSchema` (FGAP-077 — the Pi-tool / CLI-facing schema-write op).
@@ -149,30 +405,37 @@ export function writeSchema(cwd, schemaName, schema) {
149
405
  * supplied body and returns `{ written: false }` without touching disk —
150
406
  * keeping ONE validation path (no re-implemented validation for the preview).
151
407
  */
152
- export function writeSchemaChecked(cwd, schemaName, schema, operation, ctx, opts) {
408
+ export function writeSchemaCheckedForDir(substrateDir, schemaName, schema, operation, ctx, opts) {
153
409
  void ctx; // accepted for call-site parity; schema files carry no author fields.
154
- const target = schemaWritePath(cwd, schemaName);
410
+ const target = schemaWritePathForDir(substrateDir, schemaName);
155
411
  // (1) Operation discriminator — reject unknown ops before any disk read.
156
412
  if (operation !== "create" && operation !== "replace") {
157
413
  throw new Error(`writeSchemaChecked: unknown operation '${operation}' — expected create | replace`);
158
414
  }
159
- // (2) Presence guard — readSchema returns null when the file is absent.
160
- const existing = readSchema(cwd, schemaName);
415
+ // (2) Presence guard — readSchemaForDir returns null when the file is absent.
416
+ const existing = readSchemaForDir(substrateDir, schemaName);
161
417
  if (operation === "create" && existing !== null) {
162
418
  throw new Error(`writeSchemaChecked: create collision — schema '${schemaName}' already exists at ${target}; use operation 'replace' to overwrite`);
163
419
  }
164
420
  if (operation === "replace" && existing === null) {
165
421
  throw new Error(`writeSchemaChecked: replace target missing — schema '${schemaName}' does not exist at ${target}; use operation 'create'`);
166
422
  }
167
- // (3) Dry-run: meta-validate via the SAME validator writeSchema uses, no write.
423
+ // (3) Dry-run: meta-validate via the SAME validator the write path uses, no
424
+ // write. The dry-run branch does NOT route through writeSchemaForDir (which
425
+ // carries the nested-id guard), so the guard is applied here too — a --dry-run
426
+ // preview of a nested-id schema must reject identically to a committing write.
168
427
  if (opts?.dryRun) {
169
428
  validateSchemaAgainstMeta(schema, `schema '${schemaName}' (dry-run)`);
429
+ assertNoNestedIdBearingArray(schema, `schema '${schemaName}' (dry-run)`);
170
430
  return { written: false, operation, schemaPath: target };
171
431
  }
172
- // (4) Commit: writeSchema re-meta-validates + writes atomically (tmp + rename).
173
- writeSchema(cwd, schemaName, schema);
432
+ // (4) Commit: writeSchemaForDir re-meta-validates + writes atomically (tmp + rename).
433
+ writeSchemaForDir(substrateDir, schemaName, schema, ctx);
174
434
  return { written: true, operation, schemaPath: target };
175
435
  }
436
+ export function writeSchemaChecked(cwd, schemaName, schema, operation, ctx, opts) {
437
+ return writeSchemaCheckedForDir(resolveContextDir(cwd), schemaName, schema, operation, ctx, opts);
438
+ }
176
439
  /**
177
440
  * Read the current schema at `<contextDir>/schemas/<schemaName>.schema.json`,
178
441
  * pass it to `mutator`, meta-validate the result, and atomically write it
@@ -191,7 +454,7 @@ export function writeSchemaChecked(cwd, schemaName, schema, operation, ctx, opts
191
454
  export function updateSchema(cwd, schemaName, mutator) {
192
455
  const current = readSchema(cwd, schemaName);
193
456
  if (current === null) {
194
- throw new Error(`updateSchema: schema '${schemaName}' does not exist at ${schemaWritePath(cwd, schemaName)}; use writeSchema to create it`);
457
+ throw new Error(`updateSchema: schema '${schemaName}' does not exist at ${schemaWritePathForDir(resolveContextDir(cwd), schemaName)}; use writeSchema to create it`);
195
458
  }
196
459
  const next = mutator(current);
197
460
  // validateSchemaAgainstMeta will throw ValidationError on a malformed result;