@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
@@ -0,0 +1,123 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "pi-context://schemas/migrations",
4
+ "version": "1.0.0",
5
+ "title": "pi-context substrate-persisted schema migration declarations",
6
+ "description": "Operator-authored declarations describing per-step schema version migrations. The migration-registry-loader reads this file at extension load (per cwd, cached) and converts each MigrationDecl into a MigrationFn registered against the per-process MigrationRegistry. kind='identity' asserts data is shape-compatible across the bump (no transform). kind='declarative-transform' carries a TransformSpec whose operations apply to dotted JSON paths on object-shaped block items.",
7
+ "type": "object",
8
+ "required": ["schema_version", "migrations"],
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "schema_version": {
12
+ "type": "string",
13
+ "description": "Schema version of this migrations.json file itself. Decoupled from the schema-version semver of any individual block schema it describes."
14
+ },
15
+ "migrations": {
16
+ "type": "array",
17
+ "description": "Ordered list of MigrationDecl entries. Order is informational; resolution walks fromVersion→toVersion edges per schemaName via the in-memory registry.",
18
+ "items": { "$ref": "#/definitions/MigrationDecl" }
19
+ }
20
+ },
21
+ "definitions": {
22
+ "MigrationDecl": {
23
+ "type": "object",
24
+ "required": ["schemaName", "fromVersion", "toVersion", "kind", "created_by", "created_at"],
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "schemaName": {
28
+ "type": "string",
29
+ "description": "Canonical schema id (matches the schema's $id minus the URN prefix). Conforms to assertSubstrateName alphabet (letters, digits, '-', '_')."
30
+ },
31
+ "fromVersion": {
32
+ "type": "string",
33
+ "description": "Semver string of the source schema version this migration walks forward from."
34
+ },
35
+ "toVersion": {
36
+ "type": "string",
37
+ "description": "Semver string of the destination schema version this migration produces. Must differ from fromVersion."
38
+ },
39
+ "kind": {
40
+ "type": "string",
41
+ "enum": ["identity", "declarative-transform"],
42
+ "description": "identity = operator asserts data shape is unchanged across the bump (no transform). declarative-transform = transform.operations[] applies to the data."
43
+ },
44
+ "transform": {
45
+ "$ref": "#/definitions/TransformSpec",
46
+ "description": "Required when kind='declarative-transform'; forbidden when kind='identity'. The presence/absence guard is enforced by the write-schema-migration Pi tool prior to AJV validation."
47
+ },
48
+ "created_by": {
49
+ "type": "string",
50
+ "description": "Operator identity (writer.user) recorded at declaration write."
51
+ },
52
+ "created_at": {
53
+ "type": "string",
54
+ "format": "date-time",
55
+ "description": "ISO-8601 timestamp recorded at declaration write."
56
+ }
57
+ }
58
+ },
59
+ "TransformSpec": {
60
+ "type": "object",
61
+ "required": ["operations"],
62
+ "additionalProperties": false,
63
+ "properties": {
64
+ "operations": {
65
+ "type": "array",
66
+ "description": "Ordered TransformOp sequence applied in order to the block-item data shape.",
67
+ "items": { "$ref": "#/definitions/TransformOp" }
68
+ }
69
+ }
70
+ },
71
+ "TransformOp": {
72
+ "oneOf": [
73
+ {
74
+ "type": "object",
75
+ "required": ["op", "from", "to"],
76
+ "additionalProperties": false,
77
+ "properties": {
78
+ "op": { "type": "string", "enum": ["rename"] },
79
+ "from": {
80
+ "type": "string",
81
+ "description": "Source JSON path in dotted notation prefixed with '$' (e.g. '$.field' or '$.nested.field')."
82
+ },
83
+ "to": { "type": "string" }
84
+ }
85
+ },
86
+ {
87
+ "type": "object",
88
+ "required": ["op", "path"],
89
+ "additionalProperties": false,
90
+ "properties": {
91
+ "op": { "type": "string", "enum": ["set"] },
92
+ "path": { "type": "string" },
93
+ "value": {
94
+ "description": "Value to set at path. Any JSON-serializable shape (string / number / boolean / null / array / object)."
95
+ }
96
+ }
97
+ },
98
+ {
99
+ "type": "object",
100
+ "required": ["op", "path"],
101
+ "additionalProperties": false,
102
+ "properties": {
103
+ "op": { "type": "string", "enum": ["delete"] },
104
+ "path": { "type": "string" }
105
+ }
106
+ },
107
+ {
108
+ "type": "object",
109
+ "required": ["op", "path", "type"],
110
+ "additionalProperties": false,
111
+ "properties": {
112
+ "op": { "type": "string", "enum": ["coerce"] },
113
+ "path": { "type": "string" },
114
+ "type": {
115
+ "type": "string",
116
+ "enum": ["string", "number", "boolean", "array", "object"]
117
+ }
118
+ }
119
+ }
120
+ ]
121
+ }
122
+ }
123
+ }
@@ -1,23 +1,54 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "pi-context://schemas/relations",
4
- "version": "1.0.0",
4
+ "version": "2.0.0",
5
5
  "title": "pi-context closure-table relations",
6
- "description": "Generic closure-table substrate. One row per (parent, child, relation_type) edge. Multiple parallel hierarchies coexist as different relation_type values. Reassignment is a single field write. The schema is intentionally permissive on edge.parent's namespacedisambiguation between hierarchy edges (parent ∈ canonical ids) and lens edges (parent ∈ lens.bins) is cross-document and lives at substrate-SDK validateRelations(). Cycle detection lives at SDK traversal primitives.",
6
+ "description": "Generic closure-table substrate. One row per (parent, child, relation_type) edge. Multiple parallel hierarchies coexist as different relation_type values. Reassignment is a single field write. Endpoints are DUAL-FORM (content-addressed-substrate-identity arc, Cycle 5): a LEGACY string (canonical id OR lens bin name disambiguated cross-document at validateRelations), OR a structured EdgeEndpoint object — {kind:'item', oid, refname?, substrate_id?, content_hash?} for an item endpoint, or {kind:'lens_bin', bin} for a virtual lens-bin parent. Existing string data validates against the string branch unchanged. Disambiguation between hierarchy edges (parent ∈ canonical ids) and lens edges (parent ∈ lens.bins) is cross-document and lives at substrate-SDK validateRelations(). Cycle detection lives at SDK traversal primitives. Cross-substrate RESOLUTION of foreign (substrate_id-bearing) endpoints is Cycle 8 — this schema only accepts the FORM.",
7
+ "$defs": {
8
+ "endpoint": {
9
+ "description": "An edge endpoint. Dual-form: a legacy string, or a structured EdgeEndpoint object.",
10
+ "oneOf": [
11
+ {
12
+ "type": "string",
13
+ "description": "LEGACY form. Either a canonical id (DEC-NNNN, FGAP-NNN, issue-NNN) or a bin name from a lens definition. Mixing is intentional — a lens bin acts as a virtual parent node. A 'project:'/'<alias>:'-prefixed string is a cross-substrate sentinel treated as unresolved until Cycle 8."
14
+ },
15
+ {
16
+ "type": "object",
17
+ "description": "Structured ITEM endpoint. oid is the content-addressed object id (dedup identity); refname is the legacy canonical id (consumer node identity). substrate_id present → foreign endpoint (Cycle-8 resolution). content_hash carried for drift detection.",
18
+ "required": ["kind", "oid"],
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "kind": { "const": "item" },
22
+ "oid": { "type": "string", "description": "Content-addressed object id." },
23
+ "refname": { "type": "string", "description": "Legacy canonical id / human-readable refname." },
24
+ "substrate_id": {
25
+ "type": "string",
26
+ "description": "Owning substrate id when the endpoint is FOREIGN; absent for same-substrate."
27
+ },
28
+ "content_hash": { "type": "string", "description": "Content hash carried for Cycle-8 drift detection." }
29
+ }
30
+ },
31
+ {
32
+ "type": "object",
33
+ "description": "Structured LENS_BIN endpoint — a virtual lens-bin parent node. NEVER resolves to an item.",
34
+ "required": ["kind", "bin"],
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "kind": { "const": "lens_bin" },
38
+ "bin": { "type": "string", "description": "Lens bin label." }
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ },
7
44
  "type": "array",
8
45
  "items": {
9
46
  "type": "object",
10
47
  "required": ["parent", "child", "relation_type"],
11
48
  "additionalProperties": false,
12
49
  "properties": {
13
- "parent": {
14
- "type": "string",
15
- "description": "Either a canonical id (DEC-NNNN, FGAP-NNN, issue-NNN) or a bin name from a lens definition. Mixing is intentional — a lens bin acts as a virtual parent node."
16
- },
17
- "child": {
18
- "type": "string",
19
- "description": "Canonical id of the member item."
20
- },
50
+ "parent": { "$ref": "#/$defs/endpoint" },
51
+ "child": { "$ref": "#/$defs/endpoint" },
21
52
  "relation_type": {
22
53
  "type": "string",
23
54
  "description": "Lens id, hierarchy edge type, or registered relation_type canonical_id. Allows multiple parallel hierarchies over the same items."
@@ -3,25 +3,31 @@ name: pi-context
3
3
  description: >
4
4
  Schema-driven project state management with typed JSON blocks, schema validation,
5
5
  substrate config, lens views, closure-table relations, and cross-block referential
6
- integrity. Use when managing .project/ blocks, scaffolding project structure,
6
+ integrity. Use when managing substrate blocks, scaffolding project structure,
7
7
  installing block kinds from the packaged samples catalog, validating project state,
8
8
  rendering lens views, or adding work items.
9
9
  ---
10
10
 
11
11
  <objective>
12
- pi-context manages structured project state in `.project/` — a directory of JSON block files validated against schemas. The substrate (config + lenses + closure-table relations) is degree-zero state that defines where the rest lives and how items group into views.
12
+ pi-context manages structured project state in the substrate directory — a directory of JSON block files validated against schemas. The substrate (config + lenses + closure-table relations) is degree-zero state that defines where the rest lives and how items group into views.
13
13
  </objective>
14
14
 
15
15
  <block_files>
16
- Blocks are JSON files under the substrate root (e.g., `gaps.json`, `decisions.json`). Each block has a corresponding schema in `<root>/schemas/`. When you write to a block via the tools, the data is validated against its schema before persisting. Writes are atomic (tmp file + rename) and serialised per block via `withBlockLock`. The substrate root is the dir chosen at init (recorded in the `.pi-context.json` bootstrap pointer) and written to `config.json`'s `root` field by `/context accept-all`; the framework ships no default (DEC-0015). block-api routes through `resolveContextDir(cwd)` — which resolves `config.root` when set and otherwise falls back to the pointer — so a relocated root reaches every read/write site.
16
+ Blocks are JSON files under the substrate root (one per block kind, each an array of items). Each block has a corresponding schema in `<root>/schemas/`. When you write to a block via the tools, the data is validated against its schema before persisting. Writes are atomic (tmp file + rename) and serialised per block via `withBlockLock`. The substrate root is the dir chosen at init (recorded in the `.pi-context.json` bootstrap pointer) and written to `config.json`'s `root` field by `/context accept-all`; the framework ships no default substrate-dir name. block-api routes through `resolveContextDir(cwd)` — which resolves `config.root` when set and otherwise falls back to the pointer — so a relocated root reaches every read/write site.
17
+
18
+ Each item in an identity-bearing block carries a three-layer identity, not a refname alone: `id` (the mutable human label, e.g. a kind-prefixed refname), `oid` (a content-independent 32-hex identity minted once at the item's birth and immutable thereafter, salted by the substrate's `substrate_id`), `content_hash` (a SHA-256 over the item's content projection — the item minus its metadata fields — so identical content deduplicates), and `content_parent` (the prior version's `content_hash`, forming a per-item version chain that advances only when content actually changed). A write carrying a different incoming `oid` is rejected. Identity stamping is a no-op unless the block's array subschema declares all three identity fields, scoping content-addressing to canonical schemas and leaving bespoke/test schemas untouched.
17
19
  </block_files>
18
20
 
21
+ <content_addressing>
22
+ On a stamping write the item's content projection (a shallow copy with the metadata fields removed) is persisted to `<substrate-dir>/objects/<content_hash>.json` — a content-addressed object store (idempotent, atomic tmp+rename; identical content yields a byte-identical file). The store is git-tracked: it is the integrity/version store, and gitignoring it would lose pinning. Object persistence is deferred until after the whole block clears schema validation, so a validation failure never orphans an object. The metadata fields excluded from the content hash are the mandatory floor `{id, oid, content_hash, content_parent}` (never hashable, never pullable into the hash by an override) plus a discretionary set (the author fields and `closed_by`/`closed_at`); a schema's item subschema may redefine the discretionary set via `x-identity.metadata_fields`, but the floor is always unioned in.
23
+ </content_addressing>
24
+
19
25
  <schema_validation>
20
26
  Every block write validates against `<root>/schemas/<blockname>.schema.json`. If the schema file doesn't exist, writes proceed without validation. Validation errors include the specific JSON Schema violations.
21
27
  </schema_validation>
22
28
 
23
29
  <context_init>
24
- `/context init <dir>` creates the substrate skeleton: the `.pi-context.json` bootstrap pointer (declaring the substrate-dir name per DEC-0015) plus the substrate root and its `schemas/` directory. Nothing is imposed — no `config.json`, no schemas, and no starter blocks are written (DEC-0011 ship-no-defaults). Idempotent: re-running preserves existing dirs. Populate the substrate next with `/context accept-all` (adopt the canonical conception) followed by `/context install`.
30
+ `/context init <substrate-dir>` creates the substrate skeleton: the `.pi-context.json` bootstrap pointer (declaring the chosen substrate-dir name) plus the substrate root and its `schemas/` directory. Nothing is imposed — no `config.json`, no schemas, and no starter blocks are written (ship-no-defaults). Idempotent: re-running preserves existing dirs. Populate the substrate next with `/context accept-all` (adopt the canonical conception) followed by `/context install`.
25
31
  </context_init>
26
32
 
27
33
  <context_accept_all>
@@ -35,17 +41,31 @@ The installable catalog IS the packaged conception (`samples/conception.json`):
35
41
  </context_install>
36
42
 
37
43
  <substrate_config>
38
- `.project/config.json` is the substrate bootstrap. Its `root` field declares where every other block, schema, agent, and template lives — closing the GitHub #3 surface where downstream consumers had to assume `.project/`. `naming` aliases canonical block ids to display names (used by `/context view` rendering). `hierarchy` declares legal closure-table edges (parent block → child block via relation_type). `lenses` declares named projections over a target block. `installed_schemas` / `installed_blocks` are the install manifest consumed by `/context install`.
44
+ `<substrate-dir>/config.json` is the substrate bootstrap. Its `root` field declares where every other block, schema, agent, and template lives — consumers resolve that dir via the `.pi-context.json` pointer plus `config.root`, never by assuming a fixed directory name. Its `substrate_id` field is the per-substrate root identity (pattern `sub-` followed by 16 hex), minted once and immutable on disk; it salts oid minting and identifies the substrate in the project-root registry. `naming` aliases canonical block ids to display names (used by `/context view` rendering). `hierarchy` declares legal closure-table edges (parent block → child block via relation_type). `lenses` declares named projections over a target block. `installed_schemas` / `installed_blocks` are the install manifest consumed by `/context install`.
39
45
 
40
- `config.json` and `relations.json` are exempt from `root` redirection — they always live at `.project/` because they are the substrate that defines `root`. All other state lives under `<config.root>/...` per `resolveContextDir(cwd)`. The package ships their schemas in `schemas/` (config.schema.json, relations.schema.json) and resolves them via three-tier search: project override > user override > package-shipped.
46
+ `config.json` and `relations.json` are exempt from `config.root` redirection — they always live at the substrate-dir root (the dir chosen at bootstrap, resolved via the `.pi-context.json` pointer, suggested `.context`) because they are the substrate that defines `root`. The substrate-dir root is whatever was chosen at bootstrap, not necessarily `.project`. All other state lives under `<config.root>/...` per `resolveContextDir(cwd)`. The package ships their schemas in `schemas/` (config.schema.json, relations.schema.json) and resolves them via three-tier search: project override > user override > package-shipped.
41
47
 
42
48
  The `loadContext(cwd)` SDK returns an mtime-keyed cached snapshot of `{ config, relations, configMtime, relationsMtime }` for one cwd. Consumers must not mutate.
43
49
  </substrate_config>
44
50
 
51
+ <cross_substrate>
52
+ A project can carry multiple substrates. The `.pi-context.json` bootstrap pointer names the single ACTIVE substrate dir; a separate project-root, git-tracked `.pi-context-registry.json` enumerates ALL substrates — a version plus `substrates: { <substrate_id>: { dir, aliases[] } }`. `resolveSubstrateDir(cwd, substrate_id)` and `resolveAlias(cwd, alias)` look up the registry and return null on a clean miss.
53
+
54
+ `resolveRef(cwd, ref)` classifies any endpoint into one of four statuses: `active` — resolved in the active substrate's index (a bare oid/refname, or any lens_bin endpoint, which is always active without an item lookup); `foreign` — a structured endpoint carrying a `substrate_id`, or an `<alias>:<refname>` string whose alias is registered, resolved in the foreign substrate's index; `dangling` — a locator naming a registered substrate where the oid/refname is absent; `unregistered` — a `substrate_id` or alias the registry does not carry. A string with a NON-leading `:` (the parse gates on `colon > 0`) is first attempted as an `<alias>:<refname>` parse; a leading-colon string is not alias-parsed.
55
+
56
+ Source-of-truth-drift invariant: `validateContext` requires the active `config.substrate_id` to have a registry entry whose `dir` resolves to the active substrate. A mismatch yields `substrate_id_registry_mismatch`; a missing entry yields `substrate_id_unregistered`.
57
+ </cross_substrate>
58
+
59
+ <schema_versioning>
60
+ Schemas are draft-07 JSON-Schema, one per block kind, under `<substrate-dir>/schemas/`. Package-shipped substrate-singleton schemas carry a `pi-context://schemas/<name>` `$id` plus a `version`. `<substrate-dir>/migrations.json` is the per-substrate schema-version migration registry. A schema `version` bump REQUIRES a companion migration declared via the `write-schema-migration` tool; without one, reading or writing an item that declares an older `schema_version` throws a version mismatch. Migration kinds are `identity` (shape-compatible, no transform) or `declarative-transform` (a TransformSpec of rename/set/delete/coerce on dotted paths). The loaded registry resolves the migration edge at the next read/write, so items walk forward without a process restart. A `block:<name>` reference resolves to `<contextDir>/schemas/<name>.schema.json`.
61
+ </schema_versioning>
62
+
45
63
  <lens_views>
46
64
  Lenses are named projections over a target block. A lens declares `id`, `target` (block name), `relation_type`, `derived_from_field` (optional — synthesizes edges from a per-item field instead of requiring authored edges), `bins` (named groupings), and `render_uncategorized`.
47
65
 
48
- Edges live in `.project/relations.json` as a closure table — each row is `{ parent, child, relation_type }`. `parent` is either a canonical id (hierarchy edges) or a lens.bins value (lens edges); disambiguation lives in `validateRelations`.
66
+ Edges live in `<substrate-dir>/relations.json` as a closure table — each row is `{ parent, child, relation_type, ordinal? }`. `relation_type` is a lens id, a hierarchy edge type, or a registered `relation_types[].canonical_id`; `ordinal` orders siblings within `(parent, relation_type)`. Endpoints (both `parent` and `child`) are dual-form: a legacy string (a canonical id, a lens bin name, or an `<alias>:<refname>` cross-substrate sentinel; disambiguation lives in `validateRelations`), OR a structured item endpoint `{ kind: "item", oid, refname?, substrate_id?, content_hash? }` where a present `substrate_id` marks a foreign endpoint and `content_hash` is carried for drift detection, OR a structured lens-bin endpoint `{ kind: "lens_bin", bin }` — a virtual parent that never resolves to an item.
67
+
68
+ The single-form rule: ALL inter-item relationships are closure-table edges. Embedded nested id-bearing arrays and FK-as-field are forbidden — a nested id-bearing array in a schema is flagged `nested_id_bearing_array` by `validateContext` with the remediation "promote to a top-level entity + membership edge". Containment is a membership edge carrying `ordinal`; the nested id-bearing array → top-level entity block + ordinal-bearing membership edges promotion is performed by the canonicalizer (the context-dir-migration `canonicalizeSubstrate` machinery, run as a repo-side migration script under `scripts/migration/` — not a packaged pi-context tool). (Distinct from the `promote-item` tool, which is a cross-substrate derivation: it promotes a substrate item INTO another registered substrate as a new content-addressed item, recording an `item_derived_from_item` lineage edge in the destination.)
49
69
 
50
70
  The lens-view algorithm: `edgesForLens(lens, items, authoredEdges)` returns synthetic edges (when `derived_from_field` is set) or filtered authored edges (otherwise). `groupByLens(items, lens, lensEdges)` produces a `Map<binName, ItemRecord[]>`. `walkDescendants(parentId, relationType, edges)` traverses the closure table from any parent.
51
71
 
@@ -61,7 +81,9 @@ The lens-view algorithm: `edgesForLens(lens, items, authoredEdges)` returns synt
61
81
  </context_view>
62
82
 
63
83
  <substrate_validation>
64
- `validateRelations(cwd)` (exposed as the `context-validate-relations` tool) checks the closure-table edges in `relations.json` against the config + per-block item snapshots. Diagnostics codes: `edge_unknown_relation_type`, `edge_parent_not_in_bins`, `edge_unresolved_parent`, `edge_unresolved_child`, `edge_parent_wrong_block`, `edge_child_wrong_block`, `edge_cycle_detected`. Returns `{ status: "clean" | "warnings" | "invalid", issues[] }` where each issue carries the offending edge or cycle path.
84
+ `validateRelations(cwd)` (exposed as the `context-validate-relations` tool) checks the closure-table edges in `relations.json` against the config + per-block item snapshots, with the `resolveRef` hook classifying foreign endpoints. Diagnostics codes: `edge_unknown_relation_type`, `edge_parent_not_in_bins`, `edge_unresolved_parent`, `edge_unresolved_child`, `edge_parent_wrong_block`, `edge_child_wrong_block`, `edge_cycle_detected`. Returns `{ status: "clean" | "warnings" | "invalid", issues[] }` where each issue carries the offending edge or cycle path.
85
+
86
+ `validateContext(cwd)` (the `context-validate` tool) layers the registry/identity invariants over cross-block referential integrity: `substrate_id_unregistered` and `substrate_id_registry_mismatch` (the source-of-truth-drift guard on the active substrate), `edge_endpoint_dangling` and `edge_endpoint_unregistered` (a structured endpoint naming a registered-but-absent or unregistered substrate), and `nested_id_bearing_array` (a schema embedding an id-bearing array instead of using a membership edge). Config-declared `invariants[]` and registered lens-validators are checked in the same pass.
65
87
 
66
88
  Two derived substrate tools complement validation: `context-edges-for-lens` returns the materialized `Edge[]` for a named lens (synthetic from `derived_from_field` or filtered authored edges); `context-walk-descendants` returns the transitive descendant id list from a parent under a given relation_type.
67
89
  </substrate_validation>
@@ -95,21 +117,20 @@ Item-level reads complement whole-block `read-block` (which is all-or-nothing an
95
117
  </duplicate_detection>
96
118
 
97
119
  <context_validate>
98
- Two separate validators address two concerns:
120
+ Two surfaces address related concerns. Cross-block referential integrity is EDGE-based: there are no per-block inline-FK field checks (no `task.phase`, `task.depends_on`, `decision.phase`, `gap.resolved_by`, `requirement.traces_to`/`depends_on`, `verification.target`, `rationale.related_decisions` scans) — `relations.json` closure-table edges are the sole reference surface.
99
121
 
100
- `/context validate` (the `context-validate` tool) checks cross-block referential integrity:
101
- - task.phase references a valid phase
102
- - task.depends_on references valid task IDs
103
- - decision.phase references a valid phase
104
- - gap.resolved_by references a valid ID
105
- - requirement.traces_to references valid phase/task IDs
106
- - requirement.depends_on references valid requirement IDs
107
- - verification.target references a valid target ID
108
- - rationale.related_decisions references valid decision IDs
122
+ `/context validate` (the `context-validate` tool → `validateContext`) is the project-wide check. It runs:
123
+ - Source-of-truth drift: when the active `config.substrate_id` is set, the project-root `.pi-context-registry.json` must carry an entry whose dir resolves to the active substrate dir (`substrate_id_unregistered`, `substrate_id_registry_mismatch`).
124
+ - Edge integrity: each edge's `parent`/`child` is classified via the unified id index across substrates — an endpoint naming an unregistered substrate alias/id errors (`edge_endpoint_unregistered`), one that resolves to no item errors (`edge_endpoint_dangling`); a `relation_type` absent from `config.relation_types[]` errors; when a relation_type declares `source_kinds`/`target_kinds`, an endpoint whose resolved block kind is outside the (non-`*`) declared set errors.
125
+ - Cycle detection: delegated to `validateRelations`; only its `edge_cycle_detected` diagnostics are merged in.
126
+ - Config-declared invariants: `requires-edge` and `status-consistency` classes are enforced generically from `config.invariants[]` DATA no block/status/relation_type vocabulary is hardcoded in source; each invariant's emitted code is its own `inv.id`.
127
+ - Status-vocabulary: an item `status` value with no key in the declared vocabulary is a warning (`status_unknown_value`).
128
+ - Nested id-bearing array: a schema array at nesting depth ≥ 1 whose item shape carries an `id` is a warning to promote it to a top-level entity + membership edge (`nested_id_bearing_array`).
129
+ - Lens validators: every validator registered via `registerLensValidator` is dispatched and its issues merged; a throwing validator surfaces as a warning (`lens_validator_failed:<name>`).
109
130
 
110
- Returns errors (broken dependency references) and warnings (unresolved cross-references).
131
+ Returns issues with `severity` error/warning; status is `invalid` (any error), `warnings`, or `clean`.
111
132
 
112
- The `context-validate-relations` tool (see `<substrate_validation>`) validates closure-table edges in `relations.json` a separate concern from cross-block ID resolution.
133
+ The lower-level `context-validate-relations` tool (→ `validateRelations`, see `<substrate_validation>`) checks the closure-table edges in `relations.json` in isolation: unregistered relation_type (`edge_unknown_relation_type`), lens-bin parent not among a lens's bins (`edge_parent_not_in_bins`), unresolved/wrong-block parent or child (`edge_unresolved_parent`, `edge_unresolved_child`, `edge_parent_wrong_block`, `edge_child_wrong_block`), and cycles (`edge_cycle_detected`).
113
134
  </context_validate>
114
135
 
115
136
  <update_check>
@@ -117,7 +138,7 @@ On `session_start`, checks npm registry for newer versions of `@davidorex/pi-pro
117
138
  </update_check>
118
139
 
119
140
  <success_criteria>
120
- - `.project/`, `.project/schemas/`, `.project/phases/`, and `.project/config.json` exist after `/context init`
141
+ - `<substrate-dir>/`, `<substrate-dir>/schemas/`, and the `.pi-context.json` bootstrap pointer exist after `/context init <substrate-dir>` (init is skeleton-only: no `config.json`, no schemas, no blocks until accept-all + install). Phases are not a directory — they live as an in-block array under `phase.json` (plural `phases` key); there is no `phases/` dir.
121
142
  - `installed_schemas` / `installed_blocks` declared in `config.json` are reified by `/context install`; `--update` overwrites
122
143
  - Block writes validate against schemas — invalid data rejected with specific error
123
144
  - `/context status` returns current derived state without errors