@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
package/dist/context.js
CHANGED
|
@@ -22,8 +22,64 @@ import fs from "node:fs";
|
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import { appendManyToTypedFileIfAbsent, writeTypedFile } from "./block-api.js";
|
|
25
|
-
import { assertSubstrateName, resolveContextDir, SCHEMAS_DIR, tryResolveContextDir } from "./context-dir.js";
|
|
25
|
+
import { assertSubstrateName, mintSubstrateId, resolveContextDir, SCHEMAS_DIR, tryResolveContextDir, } from "./context-dir.js";
|
|
26
|
+
import { registerSubstrate } from "./context-registry.js";
|
|
26
27
|
import { ValidationError, validateFromFile } from "./schema-validator.js";
|
|
28
|
+
export function normalizeEndpoint(e) {
|
|
29
|
+
if (typeof e === "string") {
|
|
30
|
+
return { kind: "item", key: e, foreign: false };
|
|
31
|
+
}
|
|
32
|
+
if (e.kind === "lens_bin") {
|
|
33
|
+
return { kind: "lens_bin", key: e.bin, bin: e.bin };
|
|
34
|
+
}
|
|
35
|
+
return { kind: "item", key: e.refname ?? e.oid, foreign: !!e.substrate_id };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The consumer-layer node key for an endpoint — the string that joins against
|
|
39
|
+
* `idIndex` (keyed by item refname) or is compared to `lens.bins` labels. This
|
|
40
|
+
* is the normalize-to-string shim every walker / validator / grouping site reads
|
|
41
|
+
* in place of the bare `edge.parent`/`edge.child`. Returns byte-identical results
|
|
42
|
+
* for legacy string endpoints.
|
|
43
|
+
*/
|
|
44
|
+
export function endpointKey(e) {
|
|
45
|
+
return normalizeEndpoint(e).key;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The bin label for a `{kind:"lens_bin"}` endpoint; `null` for any item endpoint
|
|
49
|
+
* (string or structured). Lens sites test this so a lens_bin endpoint can never
|
|
50
|
+
* reach an `idIndex.get` path (Constraint 4). A legacy STRING parent is `null`
|
|
51
|
+
* here too — it stays bin-or-item ambiguous and is resolved by
|
|
52
|
+
* `lens.bins.includes(endpointKey(e))` exactly as before this cycle.
|
|
53
|
+
*/
|
|
54
|
+
export function endpointBin(e) {
|
|
55
|
+
const n = normalizeEndpoint(e);
|
|
56
|
+
return n.kind === "lens_bin" ? n.bin : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The DEDUP identity of an endpoint (the deliberate asymmetry vs the consumer
|
|
60
|
+
* node key): string → the string; structured item → `${substrate_id ?? ""}:${oid}`;
|
|
61
|
+
* lens_bin → `bin:${bin}`. This is what `identityKey` keys on for the
|
|
62
|
+
* append-if-absent no-op.
|
|
63
|
+
*
|
|
64
|
+
* The `${substrate_id ?? ""}:${oid}` colon-form is the EDGE DEDUP KEY ONLY — it is
|
|
65
|
+
* NOT the oid's own format. The oid is a bare 32-hex digest (see `mintOid`, which
|
|
66
|
+
* returns the digest with no substrate prefix and no colon). The `substrate_id`
|
|
67
|
+
* prefix is prepended here solely so two items that happen to share an oid across
|
|
68
|
+
* DIFFERENT substrates dedup as distinct edges; within one substrate the oid alone
|
|
69
|
+
* is content-addressed, so two items with the same oid but different refnames dedup
|
|
70
|
+
* together.
|
|
71
|
+
*
|
|
72
|
+
* A bare-refname STRING and a structured same-item with the SAME refname do NOT
|
|
73
|
+
* dedup-merge here (string key `"FGAP-1"` vs item key `":<oid>"`) — the documented,
|
|
74
|
+
* probed asymmetry.
|
|
75
|
+
*/
|
|
76
|
+
export function endpointIdentity(e) {
|
|
77
|
+
if (typeof e === "string")
|
|
78
|
+
return e;
|
|
79
|
+
if (e.kind === "lens_bin")
|
|
80
|
+
return `bin:${e.bin}`;
|
|
81
|
+
return `${e.substrate_id ?? ""}:${e.oid}`;
|
|
82
|
+
}
|
|
27
83
|
// ── Schema paths (bundled with the package) ──────────────────────────────────
|
|
28
84
|
/**
|
|
29
85
|
* Resolve the bundled config / relations schema files. Resolved relative to
|
|
@@ -43,12 +99,25 @@ function bundledSchemaPath(name) {
|
|
|
43
99
|
function configPath(cwd) {
|
|
44
100
|
return path.join(resolveContextDir(cwd), "config.json");
|
|
45
101
|
}
|
|
102
|
+
/** `<substrateDir>/config.json` — the dir-targeted twin of `configPath`. Takes
|
|
103
|
+
* the ALREADY-RESOLVED substrate dir directly (no pointer resolution), mirroring
|
|
104
|
+
* the Cycle-1 `*ForDir` pattern (cf. `relationsPathForDir`). The Cycle-10
|
|
105
|
+
* canonicalizer targets a non-active work-dupe substrate via this path. */
|
|
106
|
+
function configPathForDir(substrateDir) {
|
|
107
|
+
return path.join(substrateDir, "config.json");
|
|
108
|
+
}
|
|
46
109
|
/** `<resolveContextDir(cwd)>/relations.json` — same substrate-dir-relative
|
|
47
110
|
* resolution as configPath; previous `.project/`-fixed exemption removed for
|
|
48
111
|
* DEC-0015 compliance. */
|
|
49
112
|
function relationsPath(cwd) {
|
|
50
113
|
return path.join(resolveContextDir(cwd), "relations.json");
|
|
51
114
|
}
|
|
115
|
+
/** `<substrateDir>/relations.json` — the dir-targeted twin of `relationsPath`.
|
|
116
|
+
* Takes the ALREADY-RESOLVED substrate dir directly (no pointer resolution),
|
|
117
|
+
* mirroring the Cycle-1 `*ForDir` pattern. */
|
|
118
|
+
function relationsPathForDir(substrateDir) {
|
|
119
|
+
return path.join(substrateDir, "relations.json");
|
|
120
|
+
}
|
|
52
121
|
// ── Loaders ─────────────────────────────────────────────────────────────────
|
|
53
122
|
/**
|
|
54
123
|
* Resolve the substrate root for `cwd` — pointer-canonical (DEC-0045 / FGAP-079).
|
|
@@ -80,7 +149,19 @@ export function loadConfig(cwd) {
|
|
|
80
149
|
const root = tryResolveContextDir(cwd);
|
|
81
150
|
if (root === null)
|
|
82
151
|
return null;
|
|
83
|
-
|
|
152
|
+
return loadConfigForDir(root);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Dir-targeted twin of {@link loadConfig} (Cycle-1 `*ForDir` pattern). Reads +
|
|
156
|
+
* AJV-validates `<substrateDir>/config.json` against the bundled config schema
|
|
157
|
+
* for the ALREADY-RESOLVED substrate dir — no `.pi-context.json` pointer
|
|
158
|
+
* resolution. Returns `null` when the file is absent (a normal pre-write state).
|
|
159
|
+
* `loadConfig` is a thin wrapper resolving the active pointer dir; behaviour is
|
|
160
|
+
* byte-identical when called via cwd. Used by the Cycle-10 canonicalizer to read
|
|
161
|
+
* a NON-active (work-dupe) substrate's config in place.
|
|
162
|
+
*/
|
|
163
|
+
export function loadConfigForDir(substrateDir) {
|
|
164
|
+
const p = path.join(substrateDir, "config.json");
|
|
84
165
|
if (!fs.existsSync(p))
|
|
85
166
|
return null;
|
|
86
167
|
let raw;
|
|
@@ -88,14 +169,14 @@ export function loadConfig(cwd) {
|
|
|
88
169
|
raw = fs.readFileSync(p, "utf-8");
|
|
89
170
|
}
|
|
90
171
|
catch (err) {
|
|
91
|
-
throw new Error(`
|
|
172
|
+
throw new Error(`loadConfigForDir: failed to read ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
92
173
|
}
|
|
93
174
|
let data;
|
|
94
175
|
try {
|
|
95
176
|
data = JSON.parse(raw);
|
|
96
177
|
}
|
|
97
178
|
catch (err) {
|
|
98
|
-
throw new Error(`
|
|
179
|
+
throw new Error(`loadConfigForDir: invalid JSON in ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
180
|
}
|
|
100
181
|
validateFromFile(bundledSchemaPath("config"), data, `config.json (${p})`);
|
|
101
182
|
return data;
|
|
@@ -142,7 +223,19 @@ export function loadRelations(cwd) {
|
|
|
142
223
|
const root = tryResolveContextDir(cwd);
|
|
143
224
|
if (root === null)
|
|
144
225
|
return [];
|
|
145
|
-
|
|
226
|
+
return loadRelationsForDir(root);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Dir-targeted twin of {@link loadRelations} (Cycle-1 `*ForDir` pattern). Reads
|
|
230
|
+
* `<substrateDir>/relations.json` against the ALREADY-RESOLVED substrate dir —
|
|
231
|
+
* no `.pi-context.json` pointer resolution. Returns `[]` when the file is absent;
|
|
232
|
+
* AJV-validates the top-level `Edge[]` array against the bundled relations schema
|
|
233
|
+
* exactly as the cwd form does. `loadRelations` is a thin wrapper resolving the
|
|
234
|
+
* active pointer dir; behaviour is byte-identical when called via cwd. Used by
|
|
235
|
+
* the Phase-H migration to read a NON-active substrate's edges in place.
|
|
236
|
+
*/
|
|
237
|
+
export function loadRelationsForDir(substrateDir) {
|
|
238
|
+
const p = relationsPathForDir(substrateDir);
|
|
146
239
|
if (!fs.existsSync(p))
|
|
147
240
|
return [];
|
|
148
241
|
let raw;
|
|
@@ -177,7 +270,18 @@ export function loadRelations(cwd) {
|
|
|
177
270
|
* top-level-array stamping semantics documented in block-api).
|
|
178
271
|
*/
|
|
179
272
|
export function writeRelations(cwd, edges, ctx) {
|
|
180
|
-
|
|
273
|
+
writeRelationsForDir(resolveContextDir(cwd), edges, ctx);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Dir-targeted twin of `writeRelations` (Cycle-1 `*ForDir` pattern). Operates
|
|
277
|
+
* on `<substrateDir>/relations.json` against the ALREADY-RESOLVED substrate dir
|
|
278
|
+
* — no `.pi-context.json` pointer resolution. `writeRelations` is a thin
|
|
279
|
+
* wrapper resolving the active dir; behaviour is byte-identical when called via
|
|
280
|
+
* cwd. AJV-shape validation comes from `writeTypedFile`; same attestation-parity
|
|
281
|
+
* no-op semantics as the cwd form.
|
|
282
|
+
*/
|
|
283
|
+
export function writeRelationsForDir(substrateDir, edges, ctx) {
|
|
284
|
+
writeTypedFile(relationsPathForDir(substrateDir), bundledSchemaPath("relations"), edges, ctx, "relations.json");
|
|
181
285
|
}
|
|
182
286
|
/**
|
|
183
287
|
* Composite identity of an edge for append-if-absent dedup: the
|
|
@@ -189,7 +293,7 @@ export function writeRelations(cwd, edges, ctx) {
|
|
|
189
293
|
*/
|
|
190
294
|
const identityKey = (e) => {
|
|
191
295
|
const r = e;
|
|
192
|
-
return `${r.parent} ${r.child} ${r.relation_type}`;
|
|
296
|
+
return `${endpointIdentity(r.parent)} ${endpointIdentity(r.child)} ${r.relation_type}`;
|
|
193
297
|
};
|
|
194
298
|
/**
|
|
195
299
|
* Append closure-table edges to `<resolveContextDir(cwd)>/relations.json`,
|
|
@@ -214,7 +318,19 @@ const identityKey = (e) => {
|
|
|
214
318
|
* semantics documented in block-api).
|
|
215
319
|
*/
|
|
216
320
|
export function appendRelations(cwd, edges, ctx) {
|
|
217
|
-
return
|
|
321
|
+
return appendRelationsForDir(resolveContextDir(cwd), edges, ctx);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Dir-targeted twin of `appendRelations` (Cycle-1 `*ForDir` pattern). Operates
|
|
325
|
+
* on `<substrateDir>/relations.json` against the ALREADY-RESOLVED substrate dir
|
|
326
|
+
* — no pointer resolution. `appendRelations` is a thin wrapper resolving the
|
|
327
|
+
* active dir; behaviour is byte-identical when called via cwd. Same deferred-
|
|
328
|
+
* guard semantics: only AJV-shape validation + the (parent, child,
|
|
329
|
+
* relation_type) exact-duplicate-no-op; relation_type registration / endpoint
|
|
330
|
+
* resolution / cycle checks remain deferred to `validateContext`.
|
|
331
|
+
*/
|
|
332
|
+
export function appendRelationsForDir(substrateDir, edges, ctx) {
|
|
333
|
+
return appendManyToTypedFileIfAbsent(relationsPathForDir(substrateDir), bundledSchemaPath("relations"), null, edges, identityKey, ctx, "relations.json");
|
|
218
334
|
}
|
|
219
335
|
/**
|
|
220
336
|
* Append a single closure-table edge. Convenience over `appendRelations`:
|
|
@@ -223,7 +339,17 @@ export function appendRelations(cwd, edges, ctx) {
|
|
|
223
339
|
* no-op). Same deferred-guard semantics as `appendRelations`.
|
|
224
340
|
*/
|
|
225
341
|
export function appendRelation(cwd, edge, ctx) {
|
|
226
|
-
|
|
342
|
+
return appendRelationForDir(resolveContextDir(cwd), edge, ctx);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Dir-targeted twin of `appendRelation` (Cycle-1 `*ForDir` pattern). Operates
|
|
346
|
+
* on `<substrateDir>/relations.json` against the ALREADY-RESOLVED substrate dir.
|
|
347
|
+
* `appendRelation` is a thin wrapper resolving the active dir; behaviour is
|
|
348
|
+
* byte-identical when called via cwd. Same `{ appended }` semantics + deferred
|
|
349
|
+
* guards as `appendRelationsForDir`.
|
|
350
|
+
*/
|
|
351
|
+
export function appendRelationForDir(substrateDir, edge, ctx) {
|
|
352
|
+
const r = appendRelationsForDir(substrateDir, [edge], ctx);
|
|
227
353
|
return { appended: r.appended > 0 };
|
|
228
354
|
}
|
|
229
355
|
/**
|
|
@@ -232,7 +358,18 @@ export function appendRelation(cwd, edge, ctx) {
|
|
|
232
358
|
* Same cycle-safety reasoning as `writeRelations`.
|
|
233
359
|
*/
|
|
234
360
|
export function writeConfig(cwd, config, ctx) {
|
|
235
|
-
|
|
361
|
+
writeConfigForDir(resolveContextDir(cwd), config, ctx);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Dir-targeted twin of `writeConfig` (Cycle-1 `*ForDir` pattern). Atomic,
|
|
365
|
+
* AJV-validated write of `<substrateDir>/config.json` against the bundled config
|
|
366
|
+
* schema for the ALREADY-RESOLVED substrate dir — no pointer resolution.
|
|
367
|
+
* `writeConfig` is a thin wrapper resolving the active dir; behaviour is
|
|
368
|
+
* byte-identical when called via cwd. Same attestation-parity no-op semantics as
|
|
369
|
+
* the cwd form (the config schema declares no envelope author fields).
|
|
370
|
+
*/
|
|
371
|
+
export function writeConfigForDir(substrateDir, config, ctx) {
|
|
372
|
+
writeTypedFile(configPathForDir(substrateDir), bundledSchemaPath("config"), config, ctx, "config.json");
|
|
236
373
|
}
|
|
237
374
|
/**
|
|
238
375
|
* accept-all: adopt the package's canonical packaged conception
|
|
@@ -262,7 +399,21 @@ export function adoptConception(cwd) {
|
|
|
262
399
|
const samplesRoot = path.resolve(here, "..", "samples");
|
|
263
400
|
const conception = JSON.parse(fs.readFileSync(path.join(samplesRoot, "conception.json"), "utf-8"));
|
|
264
401
|
conception.root = root; // SET root from the resolved substrate dir — the conception template ships none (DEC-0041)
|
|
402
|
+
// Mint + register the per-substrate content-addressed substrate_id (Cycle 4).
|
|
403
|
+
// config.json is the sole SoT for substrate_id and first exists here at
|
|
404
|
+
// adopt-time (init only scaffolds the pointer + dirs), so minting wires into
|
|
405
|
+
// this path rather than bare init. Idempotent: skip the mint if the
|
|
406
|
+
// conception already carries a valid substrate_id (it ships none, so this
|
|
407
|
+
// mints on a fresh accept-all; a second accept-all returns early above on the
|
|
408
|
+
// existing-config branch, so this block never re-mints). registerSubstrate
|
|
409
|
+
// upserts the active substrate into the project-root registry under `root`
|
|
410
|
+
// (the project-root-relative substrate dir) with empty aliases — the
|
|
411
|
+
// SoT-drift invariant in validateContext then passes for this substrate.
|
|
412
|
+
if (typeof conception.substrate_id !== "string" || conception.substrate_id.length === 0) {
|
|
413
|
+
conception.substrate_id = mintSubstrateId();
|
|
414
|
+
}
|
|
265
415
|
writeConfig(cwd, conception);
|
|
416
|
+
registerSubstrate(cwd, conception.substrate_id, root, []);
|
|
266
417
|
return {
|
|
267
418
|
adopted: true,
|
|
268
419
|
configPath: path.relative(cwd, cfgPath),
|
|
@@ -287,6 +438,8 @@ const REGISTRY_DESCRIPTORS = {
|
|
|
287
438
|
installed_schemas: { kind: "string-array" },
|
|
288
439
|
installed_blocks: { kind: "string-array" },
|
|
289
440
|
hierarchy: { kind: "value-array" },
|
|
441
|
+
tool_operations: { kind: "keyed-array", idField: "canonical_id" },
|
|
442
|
+
tool_operations_forbidden: { kind: "string-array" },
|
|
290
443
|
};
|
|
291
444
|
/** Canonical identity join for a hierarchy triple (the `value-array` kind). */
|
|
292
445
|
function hierarchyKey(h) {
|
|
@@ -336,6 +489,20 @@ function hierarchyKey(h) {
|
|
|
336
489
|
* @throws ValidationError (from `writeConfig`) on a SHAPE violation.
|
|
337
490
|
*/
|
|
338
491
|
export function amendConfigEntry(cwd, registry, operation, key, entry, ctx, opts) {
|
|
492
|
+
return amendConfigEntryForDir(resolveContextDir(cwd), registry, operation, key, entry, ctx, opts);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Dir-targeted twin of `amendConfigEntry` (Cycle-1 `*ForDir` pattern). Carries
|
|
496
|
+
* the FULL scoped add / replace / remove body; `amendConfigEntry` delegates here
|
|
497
|
+
* via `resolveContextDir(cwd)`, so a cwd call is byte-identical to a ForDir call
|
|
498
|
+
* on the resolved active dir. Targets `<substrateDir>/config.json` directly via
|
|
499
|
+
* `loadConfigForDir` / `writeConfigForDir` (NO pointer resolution) so the
|
|
500
|
+
* Cycle-10 canonicalizer can register block_kinds / relation_types into a
|
|
501
|
+
* non-active work-dupe substrate in place. Both guard tiers
|
|
502
|
+
* (OP-CORRECTNESS + SHAPE-via-AJV) and the deep-clone-then-write-once discipline
|
|
503
|
+
* are identical to the documented `amendConfigEntry` contract above.
|
|
504
|
+
*/
|
|
505
|
+
export function amendConfigEntryForDir(substrateDir, registry, operation, key, entry, ctx, opts) {
|
|
339
506
|
// (1) Discriminator validation.
|
|
340
507
|
const descriptor = REGISTRY_DESCRIPTORS[registry];
|
|
341
508
|
if (!descriptor) {
|
|
@@ -353,7 +520,7 @@ export function amendConfigEntry(cwd, registry, operation, key, entry, ctx, opts
|
|
|
353
520
|
throw new Error(`amendConfigEntry: operation '${op}' requires an entry payload`);
|
|
354
521
|
}
|
|
355
522
|
// (2) Load + (3) deep clone (mutate the clone, write once at the end).
|
|
356
|
-
const config =
|
|
523
|
+
const config = loadConfigForDir(substrateDir);
|
|
357
524
|
if (!config) {
|
|
358
525
|
throw new Error("amendConfigEntry: no config.json");
|
|
359
526
|
}
|
|
@@ -495,7 +662,7 @@ export function amendConfigEntry(cwd, registry, operation, key, entry, ctx, opts
|
|
|
495
662
|
validateFromFile(bundledSchemaPath("config"), nextConfig, "config.json (dry-run)");
|
|
496
663
|
}
|
|
497
664
|
else {
|
|
498
|
-
|
|
665
|
+
writeConfigForDir(substrateDir, nextConfig, ctx);
|
|
499
666
|
}
|
|
500
667
|
}
|
|
501
668
|
// (6) Outcome.
|
|
@@ -615,9 +782,10 @@ export function walkDescendants(parentId, relationType, edges) {
|
|
|
615
782
|
continue;
|
|
616
783
|
visited.add(node);
|
|
617
784
|
for (const e of edges) {
|
|
618
|
-
if (e.parent === node && e.relation_type === relationType) {
|
|
619
|
-
|
|
620
|
-
|
|
785
|
+
if (endpointKey(e.parent) === node && e.relation_type === relationType) {
|
|
786
|
+
const childKey = endpointKey(e.child);
|
|
787
|
+
out.push(childKey);
|
|
788
|
+
stack.push(childKey);
|
|
621
789
|
}
|
|
622
790
|
}
|
|
623
791
|
}
|
|
@@ -649,9 +817,10 @@ export function walkAncestors(itemId, relationType, edges) {
|
|
|
649
817
|
continue;
|
|
650
818
|
visited.add(node);
|
|
651
819
|
for (const e of edges) {
|
|
652
|
-
if (e.child === node && e.relation_type === relationType) {
|
|
653
|
-
|
|
654
|
-
|
|
820
|
+
if (endpointKey(e.child) === node && e.relation_type === relationType) {
|
|
821
|
+
const parentKey = endpointKey(e.parent);
|
|
822
|
+
out.push(parentKey);
|
|
823
|
+
stack.push(parentKey);
|
|
655
824
|
}
|
|
656
825
|
}
|
|
657
826
|
}
|
|
@@ -687,13 +856,13 @@ export function walkAncestors(itemId, relationType, edges) {
|
|
|
687
856
|
*/
|
|
688
857
|
export function findReferences(itemId, edges, direction = "both") {
|
|
689
858
|
if (direction === "inbound") {
|
|
690
|
-
return edges.filter((e) => e.child === itemId);
|
|
859
|
+
return edges.filter((e) => endpointKey(e.child) === itemId);
|
|
691
860
|
}
|
|
692
861
|
if (direction === "outbound") {
|
|
693
|
-
return edges.filter((e) => e.parent === itemId);
|
|
862
|
+
return edges.filter((e) => endpointKey(e.parent) === itemId);
|
|
694
863
|
}
|
|
695
864
|
// "both": single-pass predicate; self-loop matches once via the OR.
|
|
696
|
-
return edges.filter((e) => e.child === itemId || e.parent === itemId);
|
|
865
|
+
return edges.filter((e) => endpointKey(e.child) === itemId || endpointKey(e.parent) === itemId);
|
|
697
866
|
}
|
|
698
867
|
/**
|
|
699
868
|
* Project items into bins under a lens. Items reachable through `lensEdges`
|
|
@@ -709,9 +878,15 @@ export function groupByLens(items, lens, lensEdges) {
|
|
|
709
878
|
const itemById = new Map(items.map((i) => [i.id, i]));
|
|
710
879
|
const placedIds = new Set();
|
|
711
880
|
for (const e of lensEdges) {
|
|
712
|
-
const item = itemById.get(e.child);
|
|
713
|
-
|
|
714
|
-
|
|
881
|
+
const item = itemById.get(endpointKey(e.child));
|
|
882
|
+
// Parent bin label: a structured {kind:"lens_bin"} contributes its bin; a
|
|
883
|
+
// legacy string parent stays bin-or-item-ambiguous and is resolved by
|
|
884
|
+
// `lens.bins.includes` (endpointBin is null for strings → fall back to the
|
|
885
|
+
// string key). A structured ITEM parent (endpointBin null, non-bin key)
|
|
886
|
+
// simply fails the includes test and is left for the (uncategorized) pass.
|
|
887
|
+
const binLabel = endpointBin(e.parent) ?? endpointKey(e.parent);
|
|
888
|
+
if (item && lens.bins.includes(binLabel)) {
|
|
889
|
+
grouped.get(binLabel)?.push(item);
|
|
715
890
|
placedIds.add(item.id);
|
|
716
891
|
}
|
|
717
892
|
}
|
|
@@ -755,23 +930,7 @@ export function listUncategorized(lens, grouped) {
|
|
|
755
930
|
});
|
|
756
931
|
return { uncategorized, suggestionTemplate };
|
|
757
932
|
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Validate authored edges against `config` registries (lenses + hierarchy +
|
|
761
|
-
* relation_types) and the cross-block id index supplied by the caller.
|
|
762
|
-
* Emits seven structured issue codes:
|
|
763
|
-
* - edge_unknown_relation_type
|
|
764
|
-
* - edge_parent_not_in_bins (lens edges)
|
|
765
|
-
* - edge_unresolved_parent / edge_parent_wrong_block (hierarchy edges)
|
|
766
|
-
* - edge_unresolved_child / edge_child_wrong_block (lens or hierarchy)
|
|
767
|
-
* - edge_cycle_detected (DFS recursion-stack on hierarchy relation_types)
|
|
768
|
-
*
|
|
769
|
-
* `itemsByBlock` is indexed by `BlockKindDecl.canonical_id` for hierarchy
|
|
770
|
-
* checks (parent / child must reside in the declared block) and by
|
|
771
|
-
* `LensSpec.target` for lens checks. Callers supply the index — this
|
|
772
|
-
* module does not read blocks itself, keeping it independent of block-api.
|
|
773
|
-
*/
|
|
774
|
-
export function validateRelations(config, relations, itemsByBlock) {
|
|
933
|
+
export function validateRelations(config, relations, itemsByBlock, resolve) {
|
|
775
934
|
const issues = [];
|
|
776
935
|
const lensesByRelType = new Map();
|
|
777
936
|
for (const l of config.lenses ?? []) {
|
|
@@ -796,6 +955,26 @@ export function validateRelations(config, relations, itemsByBlock) {
|
|
|
796
955
|
for (const edge of relations) {
|
|
797
956
|
const lens = lensesByRelType.get(edge.relation_type);
|
|
798
957
|
const hier = hierarchyByRelType.get(edge.relation_type);
|
|
958
|
+
// Consumer-layer node keys (the load-bearing pivot): string-typed for
|
|
959
|
+
// byte-identical diagnostics. A structured same-substrate item normalizes
|
|
960
|
+
// to its refname; a foreign item / project: string keys on its foreign
|
|
961
|
+
// string → idIndex miss → unresolved, exactly as today's sentinels.
|
|
962
|
+
const parentKey = endpointKey(edge.parent);
|
|
963
|
+
const childKey = endpointKey(edge.child);
|
|
964
|
+
// Block-resolution for the parent/child item endpoints. When a `resolve`
|
|
965
|
+
// hook is supplied (validateContext's F2 resolver), a resolved active OR
|
|
966
|
+
// foreign item contributes its `loc.block` — so a cross-substrate child
|
|
967
|
+
// under a lens/hierarchy relation_type resolves to its foreign block rather
|
|
968
|
+
// than missing the active-only `idIndex`. When OMITTED (test callers /
|
|
969
|
+
// standalone use), this falls back to the inline `idIndex.get(key)` —
|
|
970
|
+
// byte-identical to the pre-F2 behavior. (`undefined` from either path means
|
|
971
|
+
// "not found in any loaded block", exactly as before.)
|
|
972
|
+
const resolveBlock = (endpoint, key) => {
|
|
973
|
+
if (!resolve)
|
|
974
|
+
return idIndex.get(key);
|
|
975
|
+
const r = resolve(endpoint);
|
|
976
|
+
return r.status === "active" || r.status === "foreign" ? r.loc?.block : undefined;
|
|
977
|
+
};
|
|
799
978
|
if (!declaredRelTypes.has(edge.relation_type)) {
|
|
800
979
|
issues.push({
|
|
801
980
|
code: "edge_unknown_relation_type",
|
|
@@ -806,57 +985,62 @@ export function validateRelations(config, relations, itemsByBlock) {
|
|
|
806
985
|
continue;
|
|
807
986
|
}
|
|
808
987
|
if (lens) {
|
|
809
|
-
|
|
988
|
+
// Bin label: a structured {kind:"lens_bin"} parent contributes its bin;
|
|
989
|
+
// a legacy string parent is bin-or-item ambiguous, resolved by includes.
|
|
990
|
+
// A structured ITEM parent (endpointBin null) keys on its refname/oid and
|
|
991
|
+
// fails the includes test → edge_parent_not_in_bins (never an idIndex.get).
|
|
992
|
+
const parentBin = endpointBin(edge.parent) ?? parentKey;
|
|
993
|
+
if (!lens.bins.includes(parentBin)) {
|
|
810
994
|
issues.push({
|
|
811
995
|
code: "edge_parent_not_in_bins",
|
|
812
|
-
message: `lens-edge parent '${
|
|
996
|
+
message: `lens-edge parent '${parentBin}' is not in lens '${lens.id}' bins`,
|
|
813
997
|
edge,
|
|
814
998
|
});
|
|
815
999
|
}
|
|
816
|
-
const childBlock =
|
|
1000
|
+
const childBlock = resolveBlock(edge.child, childKey);
|
|
817
1001
|
if (!childBlock) {
|
|
818
1002
|
issues.push({
|
|
819
1003
|
code: "edge_unresolved_child",
|
|
820
|
-
message: `lens-edge child '${
|
|
1004
|
+
message: `lens-edge child '${childKey}' not found in any loaded block`,
|
|
821
1005
|
edge,
|
|
822
1006
|
});
|
|
823
1007
|
}
|
|
824
1008
|
else if (lens.target && childBlock !== lens.target) {
|
|
825
1009
|
issues.push({
|
|
826
1010
|
code: "edge_child_wrong_block",
|
|
827
|
-
message: `lens-edge child '${
|
|
1011
|
+
message: `lens-edge child '${childKey}' in block '${childBlock}', expected lens.target '${lens.target}'`,
|
|
828
1012
|
edge,
|
|
829
1013
|
});
|
|
830
1014
|
}
|
|
831
1015
|
}
|
|
832
1016
|
if (hier) {
|
|
833
|
-
const parentBlock =
|
|
1017
|
+
const parentBlock = resolveBlock(edge.parent, parentKey);
|
|
834
1018
|
if (!parentBlock) {
|
|
835
1019
|
issues.push({
|
|
836
1020
|
code: "edge_unresolved_parent",
|
|
837
|
-
message: `hierarchy-edge parent '${
|
|
1021
|
+
message: `hierarchy-edge parent '${parentKey}' not found in any loaded block`,
|
|
838
1022
|
edge,
|
|
839
1023
|
});
|
|
840
1024
|
}
|
|
841
1025
|
else if (parentBlock !== hier.parent_block) {
|
|
842
1026
|
issues.push({
|
|
843
1027
|
code: "edge_parent_wrong_block",
|
|
844
|
-
message: `hierarchy-edge parent '${
|
|
1028
|
+
message: `hierarchy-edge parent '${parentKey}' in block '${parentBlock}', expected '${hier.parent_block}'`,
|
|
845
1029
|
edge,
|
|
846
1030
|
});
|
|
847
1031
|
}
|
|
848
|
-
const childBlock =
|
|
1032
|
+
const childBlock = resolveBlock(edge.child, childKey);
|
|
849
1033
|
if (!childBlock) {
|
|
850
1034
|
issues.push({
|
|
851
1035
|
code: "edge_unresolved_child",
|
|
852
|
-
message: `hierarchy-edge child '${
|
|
1036
|
+
message: `hierarchy-edge child '${childKey}' not found in any loaded block`,
|
|
853
1037
|
edge,
|
|
854
1038
|
});
|
|
855
1039
|
}
|
|
856
1040
|
else if (childBlock !== hier.child_block) {
|
|
857
1041
|
issues.push({
|
|
858
1042
|
code: "edge_child_wrong_block",
|
|
859
|
-
message: `hierarchy-edge child '${
|
|
1043
|
+
message: `hierarchy-edge child '${childKey}' in block '${childBlock}', expected '${hier.child_block}'`,
|
|
860
1044
|
edge,
|
|
861
1045
|
});
|
|
862
1046
|
}
|
|
@@ -884,9 +1068,13 @@ export function validateRelations(config, relations, itemsByBlock) {
|
|
|
884
1068
|
for (const e of relations) {
|
|
885
1069
|
if (e.relation_type !== rt)
|
|
886
1070
|
continue;
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1071
|
+
// Cycle adjacency is keyed on the consumer node key (refname for items)
|
|
1072
|
+
// so a structured item and a legacy same-refname string collapse to one
|
|
1073
|
+
// graph node — byte-identical cycle diagnostics on string data.
|
|
1074
|
+
const pk = endpointKey(e.parent);
|
|
1075
|
+
const arr = adj.get(pk) ?? [];
|
|
1076
|
+
arr.push(endpointKey(e.child));
|
|
1077
|
+
adj.set(pk, arr);
|
|
890
1078
|
}
|
|
891
1079
|
const visited = new Set();
|
|
892
1080
|
const onStack = new Set();
|
|
@@ -962,7 +1150,7 @@ export function resolveComposition(cwd, lens) {
|
|
|
962
1150
|
}
|
|
963
1151
|
const ctx = loadContext(cwd);
|
|
964
1152
|
if (!ctx.config) {
|
|
965
|
-
throw new Error("resolveComposition: no
|
|
1153
|
+
throw new Error("resolveComposition: no <substrate-dir>/config.json");
|
|
966
1154
|
}
|
|
967
1155
|
return resolveCompositionInternal(cwd, lens, ctx.config, new Set());
|
|
968
1156
|
}
|