@davidorex/pi-context 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +21 -6
  3. package/dist/block-api.d.ts +13 -0
  4. package/dist/block-api.d.ts.map +1 -1
  5. package/dist/block-api.js +1 -1
  6. package/dist/block-api.js.map +1 -1
  7. package/dist/content-hash.d.ts +13 -0
  8. package/dist/content-hash.d.ts.map +1 -1
  9. package/dist/content-hash.js +16 -0
  10. package/dist/content-hash.js.map +1 -1
  11. package/dist/context-dir.d.ts +12 -0
  12. package/dist/context-dir.d.ts.map +1 -1
  13. package/dist/context-dir.js +14 -0
  14. package/dist/context-dir.js.map +1 -1
  15. package/dist/context.d.ts +60 -0
  16. package/dist/context.d.ts.map +1 -1
  17. package/dist/context.js +44 -0
  18. package/dist/context.js.map +1 -1
  19. package/dist/index.d.ts +534 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1879 -20
  22. package/dist/index.js.map +1 -1
  23. package/dist/migration-registry-loader.d.ts +16 -0
  24. package/dist/migration-registry-loader.d.ts.map +1 -1
  25. package/dist/migration-registry-loader.js +33 -0
  26. package/dist/migration-registry-loader.js.map +1 -1
  27. package/dist/ops-registry.d.ts +16 -0
  28. package/dist/ops-registry.d.ts.map +1 -1
  29. package/dist/ops-registry.js +270 -8
  30. package/dist/ops-registry.js.map +1 -1
  31. package/dist/pending-blocked-store.d.ts +83 -0
  32. package/dist/pending-blocked-store.d.ts.map +1 -0
  33. package/dist/pending-blocked-store.js +93 -0
  34. package/dist/pending-blocked-store.js.map +1 -0
  35. package/dist/schema-merge.d.ts +26 -0
  36. package/dist/schema-merge.d.ts.map +1 -0
  37. package/dist/schema-merge.js +176 -0
  38. package/dist/schema-merge.js.map +1 -0
  39. package/package.json +2 -1
  40. package/samples/conception.json +50 -0
  41. package/samples/schemas/framework-gaps.schema.json +1 -1
  42. package/samples/schemas/issues.schema.json +2 -2
  43. package/samples/schemas/layer-plans.schema.json +2 -2
  44. package/samples/schemas/research.schema.json +1 -1
  45. package/samples/schemas/work-orders.schema.json +2 -2
  46. package/schemas/config.schema.json +25 -1
  47. package/schemas/pending-blocked.schema.json +190 -0
  48. package/skill-narrative.md +7 -5
  49. package/skills/pi-context/SKILL.md +90 -7
  50. package/skills/pi-context/references/bundled-resources.md +2 -1
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Deterministic key/path-level draft-07 3-way schema merge (FEAT-006 T3 —
3
+ * TASK-036). Pure, no I/O: given the recorded merge BASE (the installed-from
4
+ * baseline schema body), OURS (the currently-installed, locally-edited body),
5
+ * and THEIRS (the catalog's current body), produce a merged body plus the set of
6
+ * irreconcilable per-path conflicts.
7
+ *
8
+ * Value equality throughout is canonical-JSON equality (`canonicalJson(a) ===
9
+ * canonicalJson(b)`) so structurally-equal-but-key-reordered values compare
10
+ * equal and the merge is order-insensitive + deterministic.
11
+ *
12
+ * The caller (`updateContext`) writes the merged body ONLY when the returned
13
+ * conflict set is empty; a non-empty conflict set means the merged body is left
14
+ * as OURS at the conflicting node(s) and the caller refrains from writing.
15
+ */
16
+ import { canonicalJson } from "./content-hash.js";
17
+ /**
18
+ * Module-private absent-key sentinel. A unique object (referential identity) so
19
+ * no representable JSON value can collide with "this key is absent on this
20
+ * side"; distinct from `null`/`undefined`, both of which are legitimate present
21
+ * values in a JSON document.
22
+ */
23
+ const MISSING = Symbol("schema-merge:MISSING");
24
+ function isMissing(v) {
25
+ return v === MISSING;
26
+ }
27
+ /** A plain JSON object: not MISSING, not null, not an array. */
28
+ function isPlainObject(v) {
29
+ return !isMissing(v) && typeof v === "object" && v !== null && !Array.isArray(v);
30
+ }
31
+ /** Present-and-an-array. */
32
+ function isArrayValue(v) {
33
+ return !isMissing(v) && Array.isArray(v);
34
+ }
35
+ /** Canonical-JSON value equality (both sides must be present). */
36
+ function valuesEqual(a, b) {
37
+ return canonicalJson(a) === canonicalJson(b);
38
+ }
39
+ /** `obj[key]` if the key is an own property, else MISSING. */
40
+ function get(obj, key) {
41
+ return Object.hasOwn(obj, key) ? obj[key] : MISSING;
42
+ }
43
+ /**
44
+ * Whether the node at `path` (with the present sides all arrays) is a SET-ARRAY:
45
+ * an array whose order/duplication is not semantically meaningful, mergeable as
46
+ * a set. Applies when the LAST path segment is `required` or `enum`, OR when the
47
+ * path's last segment is `type` (a `type` whose value is an array is a JSON
48
+ * Schema type-union list — set-semantic).
49
+ */
50
+ function isSetArrayPath(path) {
51
+ const last = path.includes(".") ? path.slice(path.lastIndexOf(".") + 1) : path;
52
+ return last === "required" || last === "enum" || last === "type";
53
+ }
54
+ /**
55
+ * 3-way set merge honoring adds AND removes, conflict-free:
56
+ * result = (ours ∩ theirs) ∪ (ours \ base) ∪ (theirs \ base)
57
+ * Element identity is canonical-JSON. The result is returned as an array sorted
58
+ * by each element's canonical JSON for determinism. A MISSING base is treated as
59
+ * the empty set.
60
+ */
61
+ function mergeSetArray(base, ours, theirs) {
62
+ const baseArr = isArrayValue(base) ? base : [];
63
+ const baseKeys = new Set(baseArr.map((e) => canonicalJson(e)));
64
+ const oursByKey = new Map();
65
+ for (const e of ours)
66
+ oursByKey.set(canonicalJson(e), e);
67
+ const theirsByKey = new Map();
68
+ for (const e of theirs)
69
+ theirsByKey.set(canonicalJson(e), e);
70
+ const resultByKey = new Map();
71
+ // (ours ∩ theirs): survivors present on both edited sides.
72
+ for (const [k, e] of oursByKey) {
73
+ if (theirsByKey.has(k))
74
+ resultByKey.set(k, e);
75
+ }
76
+ // (ours \ base): additions ours made.
77
+ for (const [k, e] of oursByKey) {
78
+ if (!baseKeys.has(k))
79
+ resultByKey.set(k, e);
80
+ }
81
+ // (theirs \ base): additions theirs made.
82
+ for (const [k, e] of theirsByKey) {
83
+ if (!baseKeys.has(k))
84
+ resultByKey.set(k, e);
85
+ }
86
+ const keys = [...resultByKey.keys()].sort();
87
+ return keys.map((k) => resultByKey.get(k));
88
+ }
89
+ /**
90
+ * Recursive 3-way merge of one node. Pushes any irreconcilable disagreement onto
91
+ * the shared `conflicts` array (tagged with `path`) and returns the merged value
92
+ * (which may be the MISSING sentinel to signal a key deletion to the parent
93
+ * object-merge).
94
+ */
95
+ function merge3(base, ours, theirs, path, conflicts) {
96
+ const basePresent = !isMissing(base);
97
+ const oursPresent = !isMissing(ours);
98
+ const theirsPresent = !isMissing(theirs);
99
+ // --- Add / remove handling (at least one side MISSING) ---------------------
100
+ if (!(basePresent && oursPresent && theirsPresent)) {
101
+ const presentCount = (basePresent ? 1 : 0) + (oursPresent ? 1 : 0) + (theirsPresent ? 1 : 0);
102
+ if (presentCount === 0)
103
+ return MISSING; // (cannot occur from a present parent)
104
+ if (presentCount === 1) {
105
+ // Present on exactly one side → take that side's value.
106
+ return oursPresent ? ours : theirsPresent ? theirs : base;
107
+ }
108
+ // presentCount === 2: exactly one side is MISSING.
109
+ if (!basePresent) {
110
+ // Both ours + theirs added this key (base absent). Equal adds converge;
111
+ // differing adds conflict.
112
+ if (valuesEqual(ours, theirs))
113
+ return ours;
114
+ conflicts.push({ path, base: undefined, ours, theirs });
115
+ return ours;
116
+ }
117
+ // One of ours/theirs removed the key; the OTHER (non-base) side is present.
118
+ const otherPresent = oursPresent ? ours : theirs;
119
+ if (valuesEqual(base, otherPresent)) {
120
+ // The surviving side left the value unchanged vs base ⇒ honor the removal.
121
+ return MISSING;
122
+ }
123
+ // Delete-vs-modify: one side removed it while the other changed it → conflict.
124
+ conflicts.push({ path, base, ours, theirs });
125
+ return ours;
126
+ }
127
+ // --- All three present -----------------------------------------------------
128
+ // Plain-object node: recurse per key over the sorted union.
129
+ if (isPlainObject(base) && isPlainObject(ours) && isPlainObject(theirs)) {
130
+ const keys = [...new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])].sort();
131
+ const out = {};
132
+ for (const k of keys) {
133
+ const childPath = path ? `${path}.${k}` : k;
134
+ const merged = merge3(get(base, k), get(ours, k), get(theirs, k), childPath, conflicts);
135
+ if (!isMissing(merged))
136
+ out[k] = merged; // omit deleted keys
137
+ }
138
+ return out;
139
+ }
140
+ // SET-ARRAY node: required / enum / type-union list, all-three-arrays.
141
+ if (isSetArrayPath(path) && isArrayValue(base) && isArrayValue(ours) && isArrayValue(theirs)) {
142
+ return mergeSetArray(base, ours, theirs);
143
+ }
144
+ // --- Atomic 3-way (scalars; non-set arrays like allOf/anyOf/oneOf/
145
+ // x-lifecycle.transitions; object-vs-scalar kind mismatch) -----------------
146
+ if (valuesEqual(base, ours))
147
+ return theirs; // ours unchanged → take theirs
148
+ if (valuesEqual(base, theirs))
149
+ return ours; // theirs unchanged → take ours
150
+ if (valuesEqual(ours, theirs))
151
+ return ours; // both changed the same way → converged
152
+ conflicts.push({ path, base, ours, theirs });
153
+ return ours; // leave the conflicting node as ours
154
+ }
155
+ /**
156
+ * Deterministic 3-way merge of three draft-07 schema bodies.
157
+ *
158
+ * @param base the recorded merge baseline body
159
+ * @param ours the currently-installed (locally-edited) body
160
+ * @param theirs the catalog's current body
161
+ * @returns `{ merged, conflicts }` — `merged` is the merged object (conflicting
162
+ * nodes left as OURS); `conflicts` is the per-path disagreement set. The
163
+ * caller writes `merged` only when `conflicts` is empty.
164
+ */
165
+ export function mergeSchema(base, ours, theirs) {
166
+ const conflicts = [];
167
+ const merged = merge3(base, ours, theirs, "", conflicts);
168
+ // The top-level call is over three objects, so merge3 returns an object
169
+ // (never MISSING — the three roots are present plain objects in every
170
+ // caller path; a defensive empty-object fallback keeps the return typed).
171
+ return {
172
+ merged: isPlainObject(merged) ? merged : {},
173
+ conflicts,
174
+ };
175
+ }
176
+ //# sourceMappingURL=schema-merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-merge.js","sourceRoot":"","sources":["../src/schema-merge.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidorex/pi-context",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,6 +20,7 @@
20
20
  "main": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",
22
22
  "exports": {
23
+ "./package.json": "./package.json",
23
24
  ".": {
24
25
  "types": "./dist/index.d.ts",
25
26
  "default": "./dist/index.js"
@@ -327,6 +327,20 @@
327
327
  "source_kinds": ["tasks"],
328
328
  "target_kinds": ["decisions"]
329
329
  },
330
+ {
331
+ "canonical_id": "item_governed_by_convention",
332
+ "display_name": "governed by convention",
333
+ "category": "data_flow",
334
+ "source_kinds": ["decisions", "features", "tasks"],
335
+ "target_kinds": ["conventions"]
336
+ },
337
+ {
338
+ "canonical_id": "item_acknowledges_missing_convention",
339
+ "display_name": "acknowledges missing convention",
340
+ "category": "data_flow",
341
+ "source_kinds": ["decisions", "features", "tasks"],
342
+ "target_kinds": ["framework-gaps"]
343
+ },
330
344
  {
331
345
  "canonical_id": "item_derived_from_item",
332
346
  "display_name": "derived from",
@@ -479,6 +493,42 @@
479
493
  "forbid_target_bucket": "unknown",
480
494
  "severity": "warning",
481
495
  "message": "Task '{id}' is governed by a superseded or cancelled decision"
496
+ },
497
+ {
498
+ "id": "decision-articulates-convention",
499
+ "class": "requires-edge",
500
+ "block": "decisions",
501
+ "relation_types": [
502
+ "item_governed_by_convention",
503
+ "item_acknowledges_missing_convention"
504
+ ],
505
+ "direction": "as_parent",
506
+ "severity": "warning",
507
+ "message": "Decision '{id}' articulates no governing convention — add an item_governed_by_convention edge to a convention, or an item_acknowledges_missing_convention edge to a missing-convention gap"
508
+ },
509
+ {
510
+ "id": "feature-articulates-convention",
511
+ "class": "requires-edge",
512
+ "block": "features",
513
+ "relation_types": [
514
+ "item_governed_by_convention",
515
+ "item_acknowledges_missing_convention"
516
+ ],
517
+ "direction": "as_parent",
518
+ "severity": "warning",
519
+ "message": "Feature '{id}' articulates no governing convention — add an item_governed_by_convention edge to a convention, or an item_acknowledges_missing_convention edge to a missing-convention gap"
520
+ },
521
+ {
522
+ "id": "task-articulates-convention",
523
+ "class": "requires-edge",
524
+ "block": "tasks",
525
+ "relation_types": [
526
+ "item_governed_by_convention",
527
+ "item_acknowledges_missing_convention"
528
+ ],
529
+ "direction": "as_parent",
530
+ "severity": "warning",
531
+ "message": "Task '{id}' articulates no governing convention — add an item_governed_by_convention edge to a convention, or an item_acknowledges_missing_convention edge to a missing-convention gap"
482
532
  }
483
533
  ],
484
534
  "tool_operations": [],
@@ -3,7 +3,7 @@
3
3
  "$id": "pi-context://schemas/framework-gaps",
4
4
  "version": "1.1.1",
5
5
  "title": "Framework Gaps",
6
- "description": "Capability gaps in the pi-project-workflows framework — pieces that must exist natively for the target artifact-ownership model to be expressible as configuration rather than hand-enactment. Each gap carries evidence (file + line references), impact, canonical engineering vocabulary for the missing capability, proposed resolution, and lifecycle state. Framework gaps are distinct from feature-level defects: they describe what the framework cannot yet express at all, not bugs in expressible functionality.",
6
+ "description": "Capability gaps in the framework — pieces that must exist natively for the target artifact-ownership model to be expressible as configuration rather than hand-enactment. Each gap carries evidence (file + line references), impact, canonical engineering vocabulary for the missing capability, proposed resolution, and lifecycle state. Framework gaps are distinct from feature-level defects: they describe what the framework cannot yet express at all, not bugs in expressible functionality.",
7
7
  "type": "object",
8
8
  "required": [
9
9
  "gaps"
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "location": {
45
45
  "type": "string",
46
- "description": "File path + line number, e.g. packages/pi-workflows/src/dag.ts:219"
46
+ "description": "File path + line number, e.g. src/module/file.ts:NNN"
47
47
  },
48
48
  "status": {
49
49
  "type": "string",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "package": {
76
76
  "type": "string",
77
- "description": "Which monorepo package this issue touches (e.g. pi-workflows, pi-project, pi-behavior-monitors)"
77
+ "description": "Which package/module this issue touches (e.g. package-a, package-b)"
78
78
  },
79
79
  "source": {
80
80
  "type": "string",
@@ -3,7 +3,7 @@
3
3
  "$id": "pi-context://schemas/layer-plans",
4
4
  "version": "1.0.1",
5
5
  "title": "Layer Restructure Plans",
6
- "description": "Plans for restructuring the .project/ directory from flat-block storage into a layered artifact-ownership model. Each plan names its conceptual source (e.g. Muni five-layer), describes each layer's purpose and its current-vs-target block mapping, and sequences the enactment into phases with exit criteria. Layer plans are themselves L2 specification-layer artifacts — they are spec-level decisions about how the framework organizes project state.",
6
+ "description": "Plans for restructuring substrate storage into a layered artifact-ownership model. Each plan names its conceptual source (e.g. a layered architecture model), describes each layer's purpose and its current-vs-target block mapping, and sequences the enactment into phases with exit criteria. Layer plans are themselves L2 specification-layer artifacts — they are spec-level decisions about how the framework organizes project state.",
7
7
  "type": "object",
8
8
  "required": [
9
9
  "plans"
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "model": {
47
47
  "type": "string",
48
- "description": "Conceptual source — e.g. 'Muni five-layer'"
48
+ "description": "Conceptual source — e.g. 'a layered architecture model'"
49
49
  },
50
50
  "description": {
51
51
  "type": "string",
@@ -55,7 +55,7 @@
55
55
  "L4",
56
56
  "L5"
57
57
  ],
58
- "description": "Which Muni layer the research informs. L1 identity/domain; L2 specification; L3 work; L4 execution; L5 memory."
58
+ "description": "Which architecture layer the research informs. L1 identity/domain; L2 specification; L3 work; L4 execution; L5 memory."
59
59
  },
60
60
  "type": {
61
61
  "type": "string",
@@ -3,7 +3,7 @@
3
3
  "$id": "pi-context://schemas/work-orders",
4
4
  "version": "1.0.1",
5
5
  "title": "Work Orders",
6
- "description": "Orchestrator-authored work-order spec for the privileged JIT-agent that drives the in-pi bounded code-change loop. A work-order names the target agent, the typed input/output contracts, the substrate-block context handoff, the bounded scope within which the agent may operate, and the deterministic real-check criteria gating acceptance (deterministic verification; never LLM self-report — verdict comes from real checks the agent cannot fake). Substrate-block handoff to the consuming agent uses the ContextBlockRef shape: each context_blocks entry is either a string for whole-block injection, or an object { name, item?, depth?, focus? } for item-specific injection. To handoff a single work-order item to the consuming agent, the agent's .agent.yaml contextBlocks declares { name: \"work-orders\", item: \"WO-NNN\" }; the compiler injects the resolved item under template key _work_orders_item (with companion _work_orders_raw / _work_orders_depth / _work_orders_focus). Authorship: orchestrator is sole spec author; capability-widening edits are writer.kind=human.",
6
+ "description": "Orchestrator-authored work-order spec for the privileged JIT-agent that drives the bounded code-change loop. A work-order names the target agent, the typed input/output contracts, the substrate-block context handoff, the bounded scope within which the agent may operate, and the deterministic real-check criteria gating acceptance (deterministic verification; never LLM self-report — verdict comes from real checks the agent cannot fake). Substrate-block handoff to the consuming agent uses the ContextBlockRef shape: each context_blocks entry is either a string for whole-block injection, or an object { name, item?, depth?, focus? } for item-specific injection. To handoff a single work-order item to the consuming agent, the agent's .agent.yaml contextBlocks declares { name: \"work-orders\", item: \"WO-NNN\" }; the compiler injects the resolved item under template key _work_orders_item (with companion _work_orders_raw / _work_orders_depth / _work_orders_focus). Authorship: orchestrator is sole spec author; capability-widening edits are writer.kind=human.",
7
7
  "type": "object",
8
8
  "required": [
9
9
  "work_orders"
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "context_blocks": {
59
59
  "type": "array",
60
- "description": "Substrate-block references the privileged agent's .agent.yaml will consume via contextBlocks. Each entry: string for whole-block injection (_<name>), OR object { name, item?, depth?, focus? } for item-specific injection (per the existing ContextBlockRef shape in packages/pi-jit-agents/src/types.ts).",
60
+ "description": "Substrate-block references the privileged agent's .agent.yaml will consume via contextBlocks. Each entry: string for whole-block injection (_<name>), OR object { name, item?, depth?, focus? } for item-specific injection (per the ContextBlockRef shape defined by your agent layer).",
61
61
  "items": {
62
62
  "oneOf": [
63
63
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "pi-context://schemas/config",
4
- "version": "1.5.0",
4
+ "version": "1.6.0",
5
5
  "title": "pi-context substrate config",
6
6
  "description": "Self-bootstrapping config block. Validates the config that configures the substrate. ConfigBlock is the canonical registry for block kinds, status buckets, relation types, display strings, layers, naming aliases, hierarchy declarations, and lens specs.",
7
7
  "type": "object",
@@ -194,6 +194,30 @@
194
194
  "description": "Blocks the project has opted into via /context install.",
195
195
  "items": { "type": "string" }
196
196
  },
197
+ "installed_from": {
198
+ "type": "object",
199
+ "description": "Install baseline of the installed schemas: catalog source + per-schema content fingerprint recorded at install time so installed-vs-catalog drift can be detected. Blocks are user data and are not baselined. Written by /context install; preserved verbatim (including `at`) on an idempotent re-install whose catalog + assets are unchanged.",
200
+ "required": ["catalog", "catalog_version", "at", "assets"],
201
+ "additionalProperties": false,
202
+ "properties": {
203
+ "catalog": { "type": "string", "description": "pi-context package \"name@version\" the install baseline was taken from." },
204
+ "catalog_version": { "type": "string", "description": "samples/conception.json schema_version at baseline time." },
205
+ "at": { "type": "string", "description": "ISO-8601 timestamp of when this baseline content was established." },
206
+ "assets": {
207
+ "type": "object",
208
+ "description": "Per installed schema (keyed by canonical_id): content fingerprint + the schema file's own declared version.",
209
+ "additionalProperties": {
210
+ "type": "object",
211
+ "required": ["content_hash", "version"],
212
+ "additionalProperties": false,
213
+ "properties": {
214
+ "content_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
215
+ "version": { "type": "string" }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ },
197
221
  "tool_operations": {
198
222
  "type": "array",
199
223
  "description": "Operation-granular tool grant vocabulary. Each entry names a Pi tool (or coarser operation) that can be granted to a privileged JIT-agent. Operation-granular, opt-in from empty default.",
@@ -0,0 +1,190 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "pi-context://schemas/pending-blocked",
4
+ "version": "1.0.0",
5
+ "title": "pi-context substrate-persisted pending-blocked resync records",
6
+ "description": "Records of catalog-ahead schema resyncs that `update` refused (blocked) on the last live run. Each entry pins the target catalog schema (by content_hash into the object store) plus the migration chain reaching it, so `resolve-blocked` can re-validate the corrected block against the SAME pinned target the run blocked on, then — on pass — register the chain, write the target schema, advance the merge base, and clear the entry. Replaced wholesale each live `update` run (an empty set removes the file); never an append-only log.",
7
+ "type": "object",
8
+ "required": ["schema_version", "entries"],
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "schema_version": {
12
+ "type": "string",
13
+ "description": "Schema version of this pending-blocked.json file itself. Decoupled from any block schema version it references."
14
+ },
15
+ "entries": {
16
+ "type": "array",
17
+ "description": "One PendingBlockedEntry per schema the last live update run refused (blocked). The set is replaced wholesale each live run; an empty set means the file is removed.",
18
+ "items": { "$ref": "#/definitions/PendingBlockedEntry" }
19
+ }
20
+ },
21
+ "definitions": {
22
+ "PendingBlockedEntry": {
23
+ "type": "object",
24
+ "required": ["name", "reason", "target_hash", "chain", "blocked_at"],
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "name": {
28
+ "type": "string",
29
+ "description": "Canonical schema id (block_kind canonical_id) the resync was refused for."
30
+ },
31
+ "from": {
32
+ "type": "string",
33
+ "description": "Installed schema version the resync would walk forward FROM (the block's declared version pre-resync). Omitted when unreadable."
34
+ },
35
+ "to": {
36
+ "type": "string",
37
+ "description": "Target catalog schema version the resync would advance TO. Omitted when unreadable."
38
+ },
39
+ "reason": {
40
+ "type": "string",
41
+ "enum": ["no-migration-chain", "validation-failed"],
42
+ "description": "Why the live resync refused: no-migration-chain = no shipped chain reaches the catalog version; validation-failed = the forward-migrated items fail the catalog schema."
43
+ },
44
+ "target_hash": {
45
+ "type": "string",
46
+ "pattern": "^[0-9a-f]{64}$",
47
+ "description": "content_hash of the pinned target catalog schema body, stored in the object store. resolve-blocked re-validates the corrected block against THIS body."
48
+ },
49
+ "chain": {
50
+ "type": "array",
51
+ "description": "Ordered MigrationDecl list reaching the target version (one per hop). Empty when reason='no-migration-chain' (no chain reaches the target).",
52
+ "items": { "$ref": "#/definitions/MigrationDecl" }
53
+ },
54
+ "failures": {
55
+ "type": "array",
56
+ "description": "For reason='validation-failed', the per-item constraint failures the live resync produced. Omitted for no-migration-chain.",
57
+ "items": { "$ref": "#/definitions/BlockValidationFailure" }
58
+ },
59
+ "premarker_hash": {
60
+ "type": "string",
61
+ "pattern": "^[0-9a-f]{64}$",
62
+ "description": "content_hash of the wrapped pre-marker block-file bytes, stored in the object store, written when a live update inscribed git-style failure markers into the validation-blocked block file. Present only on marker-bearing entries; lets the caller restore the block file to its pre-marker bytes. Omitted when no markers were written."
63
+ },
64
+ "blocked_at": {
65
+ "type": "string",
66
+ "format": "date-time",
67
+ "description": "ISO-8601 timestamp the live update run recorded the block refusal."
68
+ }
69
+ }
70
+ },
71
+ "BlockValidationFailure": {
72
+ "type": "object",
73
+ "required": ["instancePath", "keyword", "message"],
74
+ "additionalProperties": false,
75
+ "properties": {
76
+ "itemId": {
77
+ "type": "string",
78
+ "description": "Failing block item's `id` when the instancePath resolves to one; omitted for envelope-level errors."
79
+ },
80
+ "instancePath": {
81
+ "type": "string",
82
+ "description": "AJV raw JSON pointer of the failing constraint."
83
+ },
84
+ "keyword": {
85
+ "type": "string",
86
+ "description": "AJV constraint keyword that failed (or 'error' for a synthetic non-AJV throw)."
87
+ },
88
+ "message": {
89
+ "type": "string",
90
+ "description": "AJV constraint message text."
91
+ }
92
+ }
93
+ },
94
+ "MigrationDecl": {
95
+ "type": "object",
96
+ "required": ["schemaName", "fromVersion", "toVersion", "kind", "created_by", "created_at"],
97
+ "additionalProperties": false,
98
+ "properties": {
99
+ "schemaName": {
100
+ "type": "string",
101
+ "description": "Canonical schema id (matches the schema's $id minus the URN prefix)."
102
+ },
103
+ "fromVersion": {
104
+ "type": "string",
105
+ "description": "Semver string of the source schema version this migration walks forward from."
106
+ },
107
+ "toVersion": {
108
+ "type": "string",
109
+ "description": "Semver string of the destination schema version this migration produces."
110
+ },
111
+ "kind": {
112
+ "type": "string",
113
+ "enum": ["identity", "declarative-transform"],
114
+ "description": "identity = data shape unchanged across the bump (no transform). declarative-transform = transform.operations[] applies to the data."
115
+ },
116
+ "transform": {
117
+ "$ref": "#/definitions/TransformSpec",
118
+ "description": "Required when kind='declarative-transform'; forbidden when kind='identity'."
119
+ },
120
+ "created_by": {
121
+ "type": "string",
122
+ "description": "Operator identity (writer.user) recorded at declaration write."
123
+ },
124
+ "created_at": {
125
+ "type": "string",
126
+ "format": "date-time",
127
+ "description": "ISO-8601 timestamp recorded at declaration write."
128
+ }
129
+ }
130
+ },
131
+ "TransformSpec": {
132
+ "type": "object",
133
+ "required": ["operations"],
134
+ "additionalProperties": false,
135
+ "properties": {
136
+ "operations": {
137
+ "type": "array",
138
+ "description": "Ordered TransformOp sequence applied in order to the block-item data shape.",
139
+ "items": { "$ref": "#/definitions/TransformOp" }
140
+ }
141
+ }
142
+ },
143
+ "TransformOp": {
144
+ "oneOf": [
145
+ {
146
+ "type": "object",
147
+ "required": ["op", "from", "to"],
148
+ "additionalProperties": false,
149
+ "properties": {
150
+ "op": { "type": "string", "enum": ["rename"] },
151
+ "from": { "type": "string" },
152
+ "to": { "type": "string" }
153
+ }
154
+ },
155
+ {
156
+ "type": "object",
157
+ "required": ["op", "path"],
158
+ "additionalProperties": false,
159
+ "properties": {
160
+ "op": { "type": "string", "enum": ["set"] },
161
+ "path": { "type": "string" },
162
+ "value": {}
163
+ }
164
+ },
165
+ {
166
+ "type": "object",
167
+ "required": ["op", "path"],
168
+ "additionalProperties": false,
169
+ "properties": {
170
+ "op": { "type": "string", "enum": ["delete"] },
171
+ "path": { "type": "string" }
172
+ }
173
+ },
174
+ {
175
+ "type": "object",
176
+ "required": ["op", "path", "type"],
177
+ "additionalProperties": false,
178
+ "properties": {
179
+ "op": { "type": "string", "enum": ["coerce"] },
180
+ "path": { "type": "string" },
181
+ "type": {
182
+ "type": "string",
183
+ "enum": ["string", "number", "boolean", "array", "object"]
184
+ }
185
+ }
186
+ }
187
+ ]
188
+ }
189
+ }
190
+ }
@@ -35,13 +35,15 @@ Every block write validates against `<root>/schemas/<blockname>.schema.json`. If
35
35
  </context_accept_all>
36
36
 
37
37
  <context_install>
38
- `/context install` reconciles the substrate against the `installed_schemas` and `installed_blocks` lists declared in `config.json`. For each declared name it copies the matching asset from the package-shipped samples catalog (`samples/schemas/` for schemas, `samples/blocks/` for starter blocks) into the substrate. Default behavior is skip-if-exists (preserves user edits); pass `--update` to overwrite and report the asset as `updated`. Sources missing from the catalog are reported as `notFound`. Empty install lists are not an error — the result is a clean no-op message instructing the user to edit `config.json`.
38
+ `/context install` (reflected CLI op: `pi-context context-install [--update]`, `authGated`) reconciles the substrate against the `installed_schemas` and `installed_blocks` lists declared in `config.json`. The slash command and the CLI op run the same install engine — the op's `--update` maps to the same migration-aware overwrite the slash command's `--update` does. For each declared name it copies the matching asset from the package-shipped samples catalog (`samples/schemas/` for schemas, `samples/blocks/` for starter blocks) into the substrate. Default behavior is skip-if-exists (preserves user edits). Pass `--update` to re-sync installed SCHEMAS through the migration registry: a same-version body change (or a non-versioned schema) is a verbatim re-sync (reported `resynced`); a schema `version` bump forward-migrates the populated block's items through the shipped catalog migration chain and re-validates them against the new schema (reported `migrated`); a bump with no safe migration — no shipped chain reaching the catalog version, or items that would fail the new schema — is refused, leaving BOTH the installed schema file AND the block file byte-unchanged (reported `blocked`). Preview drift first with `/context check-status`. Populated block data is never overwritten — a block holding items is preserved (reported as `preserved`) regardless of `--update`, while empty or absent blocks receive the catalog starter. Sources missing from the catalog are reported as `notFound`. Empty install lists are not an error — the result is a clean no-op message instructing the user to edit `config.json`. Install also records an install baseline in `config.installed_from`: the catalog source (`catalog` = pi-context `name@version`, `catalog_version` = conception `schema_version`), a baseline timestamp, and a per-schema fingerprint (`assets[canonical_id]` = content hash + the schema's own version) of the installed SCHEMAS — the basis for installed-vs-catalog drift detection. The baseline covers schemas only (blocks are user data). A re-install on an unchanged substrate is idempotent: the existing baseline is preserved verbatim (timestamp included) so `config.json` stays byte-identical. `/context check-status` is a separate read-only command that previews drift between the installed schemas and the catalog: it compares each installed schema against its recorded baseline, the catalog's current schema, and the currently-installed file, and reports `in-sync` / `catalog-ahead` (the package shipped a newer schema) / `locally-modified` (the installed file was edited) / `both-diverged` / `no-baseline` / `missing-catalog` / `missing-installed`. For each schema behind the catalog (`catalog-ahead` / `both-diverged`) it additionally surfaces which schema is behind and by what version gap — `behind` plus a `version_delta` carrying the baseline → catalog version pair (a declared version bump) or a content-only basis when the catalog body moved with the version string unchanged. It writes nothing. The reflected CLI op is `pi-context context-check-status --json`.
39
39
 
40
- The installable catalog IS the packaged conception (`samples/conception.json`): its `block_kinds` enumerate the available kinds, each carrying its schema (`samples/schemas/`) and starter block (`samples/blocks/`). The generated installable-catalog table below lists the authoritative names — declare any subset in `installed_*` and run `/context install`, or take the whole conception via `/context accept-all`.
40
+ The installable catalog IS the packaged conception (`samples/conception.json`): its `block_kinds` enumerate the available kinds, each carrying its schema (`samples/schemas/`) and starter block (`samples/blocks/`). The generated installable-catalog table below lists the authoritative names — declare any subset in `installed_*` and run `/context install`, or take the whole conception via `/context accept-all`. To inspect a catalog schema body itself, `pi-context read-catalog-schema --kind <canonical_id>` fetches and prints the verbatim bundled `*.schema.json` (the raw JSON Schema — `properties` / `definitions` / `$id`, distinct from the `read-samples-catalog` summary projection), so after `/context check-status` flags a schema behind the catalog you can diff that body locally against the installed `<substrate>/schemas/<name>.schema.json` without hunting through `node_modules`. It is read-only and package-intrinsic (reads the bundled catalog, mutates nothing).
41
41
  </context_install>
42
42
 
43
43
  <substrate_config>
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`.
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`. `installed_from` is the optional install baseline `/context install` records — the catalog source plus a per-schema content fingerprint of the installed schemas — for installed-vs-catalog drift detection.
45
+
46
+ A fresh substrate adopted via `/context accept-all` carries advisory (severity-`warning`) convention-articulation invariants: every decision, feature, and task should carry an `item_governed_by_convention` edge to a convention it follows, or an `item_acknowledges_missing_convention` edge to a missing-convention gap. `context-validate` reports an artifact that articulates neither as a warning (it does not error), so the advice surfaces without blocking writes; satisfy it by adding one of those two edges (`append-relation`) when filing or amending the artifact.
45
47
 
46
48
  `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.
47
49
 
@@ -85,7 +87,7 @@ The lens-view algorithm: `edgesForLens(lens, items, authoredEdges)` returns synt
85
87
 
86
88
  `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.
87
89
 
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.
90
+ Three 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-lens-view` projects a config-declared lens as a binned item-view — a bin→count summary, or one bin's items paged by `offset`/`limit`; `context-walk-descendants` returns the transitive descendant id list from a parent under a given relation_type.
89
91
  </substrate_validation>
90
92
 
91
93
  <block_item_reads>
@@ -139,7 +141,7 @@ On `session_start`, checks npm registry for newer versions of `@davidorex/pi-pro
139
141
 
140
142
  <success_criteria>
141
143
  - `<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.
142
- - `installed_schemas` / `installed_blocks` declared in `config.json` are reified by `/context install`; `--update` overwrites
144
+ - `installed_schemas` / `installed_blocks` declared in `config.json` are reified by `/context install`; `--update` re-syncs installed schemas through the migration registry (forward-migrate block items on a version bump, or refuse and leave unchanged when items can't be safely migrated), but populated block data is never overwritten (preserved) — only empty or absent blocks receive the catalog starter
143
145
  - Block writes validate against schemas — invalid data rejected with specific error
144
146
  - `/context status` returns current derived state without errors
145
147
  - `/context validate` returns no errors for well-formed cross-block references