@davidorex/pi-context 0.26.0 → 0.28.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.
- package/CHANGELOG.md +69 -15
- package/README.md +71 -18
- package/dist/block-api.d.ts +165 -18
- package/dist/block-api.d.ts.map +1 -1
- package/dist/block-api.js +800 -57
- package/dist/block-api.js.map +1 -1
- package/dist/citation-rot-scanner.d.ts +80 -0
- package/dist/citation-rot-scanner.d.ts.map +1 -0
- package/dist/citation-rot-scanner.js +421 -0
- package/dist/citation-rot-scanner.js.map +1 -0
- package/dist/content-hash.d.ts +25 -0
- package/dist/content-hash.d.ts.map +1 -0
- package/dist/content-hash.js +62 -0
- package/dist/content-hash.js.map +1 -0
- package/dist/context-dir.d.ts +126 -1
- package/dist/context-dir.d.ts.map +1 -1
- package/dist/context-dir.js +208 -4
- package/dist/context-dir.js.map +1 -1
- package/dist/context-registry.d.ts +122 -0
- package/dist/context-registry.d.ts.map +1 -0
- package/dist/context-registry.js +189 -0
- package/dist/context-registry.js.map +1 -0
- package/dist/context-sdk.d.ts +179 -12
- package/dist/context-sdk.d.ts.map +1 -1
- package/dist/context-sdk.js +536 -101
- package/dist/context-sdk.js.map +1 -1
- package/dist/context.d.ts +222 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +244 -56
- package/dist/context.js.map +1 -1
- package/dist/git-env.d.ts +2 -0
- package/dist/git-env.d.ts.map +1 -0
- package/dist/git-env.js +29 -0
- package/dist/git-env.js.map +1 -0
- package/dist/index.d.ts +124 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +440 -1125
- package/dist/index.js.map +1 -1
- package/dist/lens-view.d.ts.map +1 -1
- package/dist/lens-view.js +5 -3
- package/dist/lens-view.js.map +1 -1
- package/dist/migration-registry-loader.d.ts +121 -0
- package/dist/migration-registry-loader.d.ts.map +1 -0
- package/dist/migration-registry-loader.js +309 -0
- package/dist/migration-registry-loader.js.map +1 -0
- package/dist/migrations-store.d.ts +141 -0
- package/dist/migrations-store.d.ts.map +1 -0
- package/dist/migrations-store.js +206 -0
- package/dist/migrations-store.js.map +1 -0
- package/dist/object-store.d.ts +26 -0
- package/dist/object-store.d.ts.map +1 -0
- package/dist/object-store.js +104 -0
- package/dist/object-store.js.map +1 -0
- package/dist/ops-registry.d.ts +44 -0
- package/dist/ops-registry.d.ts.map +1 -0
- package/dist/ops-registry.js +1223 -0
- package/dist/ops-registry.js.map +1 -0
- package/dist/orientation.d.ts +16 -0
- package/dist/orientation.d.ts.map +1 -0
- package/dist/orientation.js +49 -0
- package/dist/orientation.js.map +1 -0
- package/dist/promote-item.d.ts +28 -0
- package/dist/promote-item.d.ts.map +1 -0
- package/dist/promote-item.js +257 -0
- package/dist/promote-item.js.map +1 -0
- package/dist/read-element.d.ts +129 -0
- package/dist/read-element.d.ts.map +1 -0
- package/dist/read-element.js +296 -0
- package/dist/read-element.js.map +1 -0
- package/dist/rename-canonical-id.d.ts.map +1 -1
- package/dist/rename-canonical-id.js +22 -13
- package/dist/rename-canonical-id.js.map +1 -1
- package/dist/roadmap-plan.d.ts.map +1 -1
- package/dist/roadmap-plan.js +23 -13
- package/dist/roadmap-plan.js.map +1 -1
- package/dist/schema-validator.d.ts +1 -0
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +7 -3
- package/dist/schema-validator.js.map +1 -1
- package/dist/schema-write.d.ts +78 -4
- package/dist/schema-write.d.ts.map +1 -1
- package/dist/schema-write.js +297 -34
- package/dist/schema-write.js.map +1 -1
- package/dist/status-vocab.d.ts +1 -0
- package/dist/status-vocab.d.ts.map +1 -1
- package/dist/status-vocab.js +4 -0
- package/dist/status-vocab.js.map +1 -1
- package/dist/truncate.d.ts +34 -0
- package/dist/truncate.d.ts.map +1 -0
- package/dist/truncate.js +85 -0
- package/dist/truncate.js.map +1 -0
- package/dist/write-schema-migration-tool.d.ts +46 -0
- package/dist/write-schema-migration-tool.d.ts.map +1 -0
- package/dist/write-schema-migration-tool.js +115 -0
- package/dist/write-schema-migration-tool.js.map +1 -0
- package/package.json +35 -2
- package/samples/blocks/work-orders.json +3 -0
- package/samples/conception.json +22 -3
- package/samples/migrations.json +133 -0
- package/samples/schemas/context-contracts.schema.json +19 -4
- package/samples/schemas/conventions.schema.json +68 -12
- package/samples/schemas/decisions.schema.json +125 -28
- package/samples/schemas/features.schema.json +140 -26
- package/samples/schemas/framework-gaps.schema.json +171 -26
- package/samples/schemas/issues.schema.json +86 -13
- package/samples/schemas/layer-plans.schema.json +195 -35
- package/samples/schemas/phase.schema.json +18 -3
- package/samples/schemas/rationale.schema.json +37 -7
- package/samples/schemas/requirements.schema.json +77 -10
- package/samples/schemas/research.schema.json +169 -36
- package/samples/schemas/spec-reviews.schema.json +88 -18
- package/samples/schemas/story.schema.json +18 -3
- package/samples/schemas/tasks.schema.json +66 -10
- package/samples/schemas/verification.schema.json +70 -12
- package/samples/schemas/work-orders.schema.json +195 -0
- package/schemas/bootstrap.schema.json +18 -6
- package/schemas/config.schema.json +58 -26
- package/schemas/context-registry.schema.json +46 -0
- package/schemas/migrations.schema.json +123 -0
- package/schemas/relations.schema.json +41 -10
- package/skill-narrative.md +42 -21
- package/skills/pi-context/SKILL.md +159 -65
- 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;
|
|
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"}
|
package/dist/schema-validator.js
CHANGED
|
@@ -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 {
|
|
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
|
|
195
|
-
const schemaPath =
|
|
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,
|
|
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,iBAAiB,EAAE,gBAAgB,IAAI,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AACjG,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,kBAAkB;IAClB,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,gCAAgC,CAC/C,YAAoB,EACpB,UAAkB,EAClB,IAAa,EACb,QAA4B;IAE5B,MAAM,UAAU,GAAG,sBAAsB,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IACpE,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;AAED,MAAM,UAAU,0BAA0B,CACzC,GAAW,EACX,UAAkB,EAClB,IAAa,EACb,QAA4B;IAE5B,OAAO,gCAAgC,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7F,CAAC"}
|
package/dist/schema-write.d.ts
CHANGED
|
@@ -30,10 +30,65 @@
|
|
|
30
30
|
import type { DispatchContext } from "./dispatch-context.js";
|
|
31
31
|
import { ValidationError } from "./schema-validator.js";
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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;
|
|
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"}
|
package/dist/schema-write.js
CHANGED
|
@@ -29,33 +29,254 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import fs from "node:fs";
|
|
31
31
|
import path from "node:path";
|
|
32
|
-
import {
|
|
32
|
+
import { resolveContextDir, schemaPathForDir } from "./context-dir.js";
|
|
33
33
|
import { ValidationError, validateSchemaAgainstMeta } from "./schema-validator.js";
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
|
45
|
-
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
|
58
|
-
const
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
89
|
-
|
|
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(`
|
|
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
|
|
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 =
|
|
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 —
|
|
160
|
-
const existing =
|
|
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
|
|
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:
|
|
173
|
-
|
|
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 ${
|
|
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;
|