@davidorex/pi-context 0.31.0 → 0.32.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 (71) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +12 -11
  3. package/dist/block-api.d.ts.map +1 -1
  4. package/dist/block-api.js +27 -2
  5. package/dist/block-api.js.map +1 -1
  6. package/dist/context-sdk.d.ts +71 -33
  7. package/dist/context-sdk.d.ts.map +1 -1
  8. package/dist/context-sdk.js +547 -149
  9. package/dist/context-sdk.js.map +1 -1
  10. package/dist/context.d.ts +153 -2
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +75 -5
  13. package/dist/context.js.map +1 -1
  14. package/dist/index.d.ts +71 -19
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +405 -94
  17. package/dist/index.js.map +1 -1
  18. package/dist/lens-view.d.ts +0 -5
  19. package/dist/lens-view.d.ts.map +1 -1
  20. package/dist/lens-view.js +43 -1
  21. package/dist/lens-view.js.map +1 -1
  22. package/dist/migration-registry-loader.d.ts +20 -12
  23. package/dist/migration-registry-loader.d.ts.map +1 -1
  24. package/dist/migration-registry-loader.js +46 -17
  25. package/dist/migration-registry-loader.js.map +1 -1
  26. package/dist/migrations-store.d.ts +45 -18
  27. package/dist/migrations-store.d.ts.map +1 -1
  28. package/dist/migrations-store.js +56 -22
  29. package/dist/migrations-store.js.map +1 -1
  30. package/dist/ops-registry.d.ts.map +1 -1
  31. package/dist/ops-registry.js +91 -118
  32. package/dist/ops-registry.js.map +1 -1
  33. package/dist/promote-item.d.ts.map +1 -1
  34. package/dist/promote-item.js +41 -12
  35. package/dist/promote-item.js.map +1 -1
  36. package/dist/roadmap-plan.d.ts +121 -99
  37. package/dist/roadmap-plan.d.ts.map +1 -1
  38. package/dist/roadmap-plan.js +281 -345
  39. package/dist/roadmap-plan.js.map +1 -1
  40. package/dist/status-vocab.d.ts +12 -2
  41. package/dist/status-vocab.d.ts.map +1 -1
  42. package/dist/status-vocab.js +14 -1
  43. package/dist/status-vocab.js.map +1 -1
  44. package/package.json +1 -1
  45. package/samples/blocks/milestone.json +3 -0
  46. package/samples/blocks/session-notes.json +1 -0
  47. package/samples/conception.json +308 -15
  48. package/samples/migrations.json +8 -0
  49. package/samples/schemas/context-contracts.schema.json +4 -0
  50. package/samples/schemas/conventions.schema.json +4 -0
  51. package/samples/schemas/decisions.schema.json +4 -0
  52. package/samples/schemas/features.schema.json +4 -0
  53. package/samples/schemas/framework-gaps.schema.json +4 -0
  54. package/samples/schemas/issues.schema.json +28 -0
  55. package/samples/schemas/layer-plans.schema.json +4 -0
  56. package/samples/schemas/milestone.schema.json +79 -0
  57. package/samples/schemas/phase.schema.json +4 -0
  58. package/samples/schemas/rationale.schema.json +4 -0
  59. package/samples/schemas/requirements.schema.json +4 -0
  60. package/samples/schemas/research.schema.json +4 -0
  61. package/samples/schemas/session-notes.schema.json +89 -0
  62. package/samples/schemas/spec-reviews.schema.json +4 -0
  63. package/samples/schemas/story.schema.json +8 -0
  64. package/samples/schemas/tasks.schema.json +4 -0
  65. package/samples/schemas/verification.schema.json +4 -0
  66. package/samples/schemas/work-orders.schema.json +4 -0
  67. package/schemas/config.schema.json +77 -3
  68. package/schemas/migrations.schema.json +25 -0
  69. package/skill-narrative.md +7 -5
  70. package/skills/pi-context/SKILL.md +44 -49
  71. package/skills/pi-context/references/bundled-resources.md +5 -1
@@ -10,6 +10,10 @@
10
10
  ],
11
11
  "additionalProperties": false,
12
12
  "properties": {
13
+ "schema_version": {
14
+ "type": "string",
15
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
16
+ },
13
17
  "contracts": {
14
18
  "type": "array",
15
19
  "items": {
@@ -9,6 +9,10 @@
9
9
  "rules"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "rules": {
13
17
  "type": "array",
14
18
  "items": {
@@ -9,6 +9,10 @@
9
9
  "decisions"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "decisions": {
13
17
  "type": "array",
14
18
  "items": {
@@ -9,6 +9,10 @@
9
9
  "features"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "features": {
13
17
  "type": "array",
14
18
  "items": {
@@ -9,6 +9,10 @@
9
9
  "gaps"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "gaps": {
13
17
  "type": "array",
14
18
  "items": {
@@ -9,6 +9,10 @@
9
9
  "issues"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "issues": {
13
17
  "type": "array",
14
18
  "items": {
@@ -90,6 +94,10 @@
90
94
  "type": "string",
91
95
  "description": "Commit message or reference if resolved"
92
96
  },
97
+ "resolved_at": {
98
+ "type": "string",
99
+ "description": "ISO-8601 timestamp when the issue was resolved"
100
+ },
93
101
  "oid": {
94
102
  "type": "string",
95
103
  "pattern": "^[0-9a-f]{32}$",
@@ -108,5 +116,25 @@
108
116
  }
109
117
  }
110
118
  }
119
+ },
120
+ "x-lifecycle": {
121
+ "field": "status",
122
+ "states": [
123
+ "open",
124
+ "resolved",
125
+ "deferred"
126
+ ],
127
+ "transitions": [
128
+ {
129
+ "from": "open",
130
+ "to": "resolved",
131
+ "required_authority": "any"
132
+ },
133
+ {
134
+ "from": "open",
135
+ "to": "deferred",
136
+ "required_authority": "user"
137
+ }
138
+ ]
111
139
  }
112
140
  }
@@ -9,6 +9,10 @@
9
9
  "plans"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "plans": {
13
17
  "type": "array",
14
18
  "items": {
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "pi-context://schemas/milestone",
4
+ "version": "1.0.0",
5
+ "title": "Milestones",
6
+ "description": "Array-block of project milestones. A milestone is a pure phase-rollup checkpoint with no work of its own: its `status` (planned | reached) is DERIVED, never authored — a milestone is `reached` iff at least one phase is positioned in it (a phase_positioned_in_milestone edge) and the parent phase of every such edge buckets to complete, else `planned`. Phase membership lives in relations.json phase_positioned_in_milestone edges (phase -> milestone), not inline. `release` is an optional grouping string pending the normalized release entity.",
7
+ "type": "object",
8
+ "required": [
9
+ "milestones"
10
+ ],
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "schema_version": {
14
+ "type": "string",
15
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
16
+ },
17
+ "milestones": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "required": [
23
+ "id",
24
+ "name",
25
+ "status"
26
+ ],
27
+ "properties": {
28
+ "id": {
29
+ "type": "string",
30
+ "pattern": "^MILE-\\d{3,}$",
31
+ "description": "Stable canonical id; MILE-NNN (3+ digits), mirroring the STORY-/TASK-NNN convention."
32
+ },
33
+ "name": {
34
+ "type": "string"
35
+ },
36
+ "status": {
37
+ "type": "string",
38
+ "enum": [
39
+ "planned",
40
+ "reached"
41
+ ],
42
+ "description": "DERIVED phase-rollup, not authored: `reached` iff >=1 phase_positioned_in_milestone edge (in which the phase is the parent and the milestone the child) and the parent phase of every such edge buckets to complete; else `planned`. A milestone carries no work of its own; authored status is rejected by canon (substrate-derived-state)."
43
+ },
44
+ "release": {
45
+ "type": "string",
46
+ "description": "Optional release grouping label. Interim flat string pending the normalized release entity, which will subsume / edge this field."
47
+ },
48
+ "created_by": {
49
+ "type": "string"
50
+ },
51
+ "created_at": {
52
+ "type": "string"
53
+ },
54
+ "modified_by": {
55
+ "type": "string"
56
+ },
57
+ "modified_at": {
58
+ "type": "string"
59
+ },
60
+ "oid": {
61
+ "type": "string",
62
+ "pattern": "^[0-9a-f]{32}$",
63
+ "description": "Content-independent substrate-stable item identity (content-addressed substrate identity, Cycle 3). Minted once at item birth via mintOid(substrate_id); immutable across content versions. Optional in the schema so pre-Cycle-3 items validate; stamped on next write."
64
+ },
65
+ "content_hash": {
66
+ "type": "string",
67
+ "pattern": "^[0-9a-f]{64}$",
68
+ "description": "SHA-256 (hex) of the RFC-8785-canonical content projection of this item (metadata fields excluded). Recomputed on every stamping write; moves with content, stable across metadata-only churn."
69
+ },
70
+ "content_parent": {
71
+ "type": "string",
72
+ "pattern": "^[0-9a-f]{64}$",
73
+ "description": "content_hash of the immediately-prior version of this item; set on a content-changing update, absent on the first (v1) version. Forms the per-item content version chain."
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
@@ -10,6 +10,10 @@
10
10
  ],
11
11
  "additionalProperties": false,
12
12
  "properties": {
13
+ "schema_version": {
14
+ "type": "string",
15
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
16
+ },
13
17
  "phases": {
14
18
  "type": "array",
15
19
  "items": {
@@ -9,6 +9,10 @@
9
9
  "rationales"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "rationales": {
13
17
  "type": "array",
14
18
  "items": {
@@ -9,6 +9,10 @@
9
9
  "requirements"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "requirements": {
13
17
  "type": "array",
14
18
  "items": {
@@ -9,6 +9,10 @@
9
9
  "research"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "research": {
13
17
  "type": "array",
14
18
  "items": {
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "pi-context://schemas/session-notes",
4
+ "version": "1.0.0",
5
+ "title": "Session Notes",
6
+ "description": "Per-session handoff records: focus, discoveries, questions, decisions made, current status, and next steps captured at session end for cross-session continuity.",
7
+ "type": "object",
8
+ "required": [
9
+ "schema_version",
10
+ "sessions"
11
+ ],
12
+ "additionalProperties": false,
13
+ "properties": {
14
+ "schema_version": {
15
+ "type": "string"
16
+ },
17
+ "sessions": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "object",
21
+ "required": [
22
+ "id",
23
+ "timestamp",
24
+ "focus",
25
+ "current_status",
26
+ "next_steps"
27
+ ],
28
+ "additionalProperties": false,
29
+ "properties": {
30
+ "id": {
31
+ "type": "string",
32
+ "pattern": "^SESSION-\\d{3}$"
33
+ },
34
+ "timestamp": {
35
+ "type": "string",
36
+ "description": "ISO-8601 with local offset"
37
+ },
38
+ "focus": {
39
+ "type": "string",
40
+ "description": "1-2 sentence summary of what the session covered"
41
+ },
42
+ "discoveries": {
43
+ "type": "array",
44
+ "items": {
45
+ "type": "string"
46
+ }
47
+ },
48
+ "questions": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "decisions_made": {
55
+ "type": "array",
56
+ "items": {
57
+ "type": "string"
58
+ }
59
+ },
60
+ "current_status": {
61
+ "type": "string",
62
+ "description": "Prose summary of where the project stands at session end"
63
+ },
64
+ "next_steps": {
65
+ "type": "array",
66
+ "items": {
67
+ "type": "string"
68
+ }
69
+ },
70
+ "oid": {
71
+ "type": "string",
72
+ "pattern": "^[0-9a-f]{32}$",
73
+ "description": "Content-independent substrate-stable item identity (content-addressed substrate identity, Cycle 3). Minted once at item birth via mintOid(substrate_id); immutable across content versions. Optional in the schema so pre-Cycle-3 items validate; stamped on next write."
74
+ },
75
+ "content_hash": {
76
+ "type": "string",
77
+ "pattern": "^[0-9a-f]{64}$",
78
+ "description": "SHA-256 (hex) of the RFC-8785-canonical content projection of this item (metadata fields excluded). Recomputed on every stamping write; moves with content, stable across metadata-only churn."
79
+ },
80
+ "content_parent": {
81
+ "type": "string",
82
+ "pattern": "^[0-9a-f]{64}$",
83
+ "description": "content_hash of the immediately-prior version of this item; set on a content-changing update, absent on the first (v1) version. Forms the per-item content version chain."
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
@@ -9,6 +9,10 @@
9
9
  "reviews"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "reviews": {
13
17
  "type": "array",
14
18
  "items": {
@@ -10,6 +10,10 @@
10
10
  ],
11
11
  "additionalProperties": false,
12
12
  "properties": {
13
+ "schema_version": {
14
+ "type": "string",
15
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
16
+ },
13
17
  "stories": {
14
18
  "type": "array",
15
19
  "items": {
@@ -29,6 +33,10 @@
29
33
  "title": {
30
34
  "type": "string"
31
35
  },
36
+ "user_kind": {
37
+ "type": "string",
38
+ "description": "Which user the story speaks for (e.g. cli-user, operator, agent) — the 'As a <user_kind>' subject of the story statement."
39
+ },
32
40
  "status": {
33
41
  "type": "string",
34
42
  "enum": [
@@ -9,6 +9,10 @@
9
9
  "tasks"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "tasks": {
13
17
  "type": "array",
14
18
  "items": {
@@ -9,6 +9,10 @@
9
9
  "verifications"
10
10
  ],
11
11
  "properties": {
12
+ "schema_version": {
13
+ "type": "string",
14
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
15
+ },
12
16
  "verifications": {
13
17
  "type": "array",
14
18
  "items": {
@@ -10,6 +10,10 @@
10
10
  ],
11
11
  "additionalProperties": false,
12
12
  "properties": {
13
+ "schema_version": {
14
+ "type": "string",
15
+ "description": "Semver of the block schema this envelope was last written against. Stamped by the write path to the schema's current version; when it differs from the installed schema's version, the read/write gates walk the block through the registered migration chain (or fail loud when no chain reaches the current version). Absent on blocks not yet written by a stamping write path."
16
+ },
13
17
  "work_orders": {
14
18
  "type": "array",
15
19
  "items": {
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "pi-context://schemas/config",
4
- "version": "1.6.0",
4
+ "version": "1.8.0",
5
5
  "title": "pi-context substrate config",
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.",
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, lens specs, and the state-derivation registry that drives currentState (focus / in-flight / next-actions / blocked / milestone-rollup).",
7
7
  "type": "object",
8
8
  "required": ["schema_version", "block_kinds"],
9
9
  "additionalProperties": false,
@@ -67,6 +67,79 @@
67
67
  "enum": ["complete", "in_progress", "blocked", "todo", "unknown"]
68
68
  }
69
69
  },
70
+ "state_derivation": {
71
+ "type": "object",
72
+ "description": "Config-declared derivation registry for currentState — the kind/relation/rank/status/head-size couplings that previously lived as hardcoded literals in the deriver. A substrate without it reports 'state-derivation not configured'.",
73
+ "additionalProperties": false,
74
+ "properties": {
75
+ "in_flight": {
76
+ "type": "object",
77
+ "description": "Which block kinds at which status bucket count as in-flight (drives inFlight + focus). Stock: kinds ['tasks'], bucket 'in_progress'.",
78
+ "additionalProperties": false,
79
+ "required": ["kinds", "bucket"],
80
+ "properties": {
81
+ "kinds": { "type": "array", "items": { "type": "string" } },
82
+ "bucket": { "type": "string" }
83
+ }
84
+ },
85
+ "focus_fallback": {
86
+ "type": "object",
87
+ "description": "Focus fallback when nothing is in-flight: an item of `kind` at status `bucket`. Stock: kind 'phase', bucket 'in_progress'.",
88
+ "additionalProperties": false,
89
+ "required": ["kind", "bucket"],
90
+ "properties": {
91
+ "kind": { "type": "string" },
92
+ "bucket": { "type": "string" }
93
+ }
94
+ },
95
+ "next_ranked": {
96
+ "type": "array",
97
+ "description": "Ordered cross-kind push order for nextActions. Each entry selects items of `kind` at `bucket`, labels them `label`, and emits its `reason_template` (with `{rank_value}`/`{id}` tokens substituted) as each item's `reason`; optional `rank_field` + `rank_order` rank within the entry (absent → topo over the dep/gate graph). Stock: tasks (topo) then issues (priority-ranked) then framework-gaps (priority-ranked).",
98
+ "items": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "required": ["kind", "label", "bucket"],
102
+ "properties": {
103
+ "kind": { "type": "string" },
104
+ "label": { "type": "string" },
105
+ "bucket": { "type": "string" },
106
+ "rank_field": { "type": "string" },
107
+ "rank_order": { "type": "array", "items": { "type": "string" } },
108
+ "reason_template": { "type": "string", "description": "The `reason` string emitted for each item this entry surfaces. Tokens substituted literally: `{rank_value}` → the item's rank_field value (or 'unset' when absent; meaningful only for a field-ranked entry); `{id}` → the item id. Stock field-ranked (framework-gaps): 'open gap (priority {rank_value})'; stock topo (tasks): 'unblocked planned task'." }
109
+ }
110
+ }
111
+ },
112
+ "blocked_by": {
113
+ "type": "object",
114
+ "description": "The relation_types whose edges contribute blockers. Stock: ['task_depends_on_task', 'task_gated_by_item'].",
115
+ "additionalProperties": false,
116
+ "required": ["relation_types"],
117
+ "properties": {
118
+ "relation_types": { "type": "array", "items": { "type": "string" } }
119
+ }
120
+ },
121
+ "rollups": {
122
+ "type": "array",
123
+ "description": "Membership rollups (milestones). Each: items of `kind` whose members are parents of `membership_relation` edges; reached → `complete_status` when all members complete, else `incomplete_status`. Stock: milestone over phase_positioned_in_milestone, reached/planned.",
124
+ "items": {
125
+ "type": "object",
126
+ "additionalProperties": false,
127
+ "required": ["kind", "membership_relation", "complete_status", "incomplete_status"],
128
+ "properties": {
129
+ "kind": { "type": "string" },
130
+ "membership_relation": { "type": "string" },
131
+ "complete_status": { "type": "string" },
132
+ "incomplete_status": { "type": "string" }
133
+ }
134
+ }
135
+ },
136
+ "head_size": {
137
+ "type": "integer",
138
+ "minimum": 0,
139
+ "description": "Truncation cap for the nextActions head. Stock: 15."
140
+ }
141
+ }
142
+ },
70
143
  "display_strings": {
71
144
  "type": "object",
72
145
  "description": "Validation-code / opaque-slug → display label map.",
@@ -85,7 +158,8 @@
85
158
  "category": { "type": "string", "enum": ["ordering", "data_flow", "membership"] },
86
159
  "cycle_allowed": { "type": "boolean" },
87
160
  "source_kinds": { "type": "array", "items": { "type": "string" }, "description": "Block_kind canonical_ids this relation's source (edge.parent) may be; literal \"*\" = any kind. Optional; absence => endpoint-kind unchecked." },
88
- "target_kinds": { "type": "array", "items": { "type": "string" }, "description": "Block_kind canonical_ids this relation's target (edge.child) may be; literal \"*\" = any kind. Optional; absence => endpoint-kind unchecked." }
161
+ "target_kinds": { "type": "array", "items": { "type": "string" }, "description": "Block_kind canonical_ids this relation's target (edge.child) may be; literal \"*\" = any kind. Optional; absence => endpoint-kind unchecked." },
162
+ "role_direction": { "type": "string", "enum": ["as_parent", "as_child"], "description": "Which stored endpoint holds the relation's PRIMARY semantic role — the single orientation source of truth. Category interprets the role (ordering=antecedent, membership=container, data_flow=source); this names where it sits: as_parent=edge.parent, as_child=edge.child. Optional: set only for relations with a per-role consumer (deriver/rollup/roadmap/promote/write/read); role-less relations omit it." }
89
163
  }
90
164
  }
91
165
  },
@@ -116,6 +116,31 @@
116
116
  "enum": ["string", "number", "boolean", "array", "object"]
117
117
  }
118
118
  }
119
+ },
120
+ {
121
+ "type": "object",
122
+ "required": ["op", "path"],
123
+ "additionalProperties": false,
124
+ "properties": {
125
+ "op": { "type": "string", "enum": ["map_each"] },
126
+ "path": { "type": "string", "description": "Dotted '$'-prefixed path to the array whose elements are mapped." },
127
+ "table": {
128
+ "type": "object",
129
+ "description": "When present: each string element is replaced by table[element], or by {relation_type: element, item_endpoint: fallback} when unmatched.",
130
+ "additionalProperties": { "type": "object" }
131
+ },
132
+ "fallback": {
133
+ "type": "string",
134
+ "enum": ["parent", "child"],
135
+ "description": "item_endpoint used for an unmatched string element under `table`; defaults to 'parent' when absent."
136
+ },
137
+ "field": { "type": "string", "description": "When present (and `table` absent): set this field on every object element." },
138
+ "value": { "description": "Value assigned to `field` on each object element." }
139
+ },
140
+ "oneOf": [
141
+ { "required": ["table"], "not": { "required": ["field"] } },
142
+ { "required": ["field", "value"], "not": { "required": ["table"] } }
143
+ ]
119
144
  }
120
145
  ]
121
146
  }
@@ -13,7 +13,7 @@ pi-context manages structured project state in the substrate directory — a dir
13
13
  </objective>
14
14
 
15
15
  <block_files>
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.
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 at init (skeleton config) and again 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
17
 
18
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.
19
19
  </block_files>
@@ -27,11 +27,11 @@ Every block write validates against `<root>/schemas/<blockname>.schema.json`. If
27
27
  </schema_validation>
28
28
 
29
29
  <context_init>
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`.
30
+ `/context init <substrate-dir>` creates the substrate skeleton: the `.pi-context.json` bootstrap pointer (declaring the chosen substrate-dir name), the substrate root and its `schemas/` directory, a minimal SKELETON `config.json` (`schema_version`, empty `block_kinds`, `root`, and a minted + project-registered `substrate_id` — no vocabulary), and the catalog's `config` migration declarations seeded into `migrations.json`. No catalog vocabulary, schemas, or starter blocks are imposed (ship-no-defaults). Idempotent: re-running preserves existing dirs and never clobbers an existing `config.json`. Populate the substrate next with `/context accept-all` (adopt the canonical conception) followed by `/context install`.
31
31
  </context_init>
32
32
 
33
33
  <context_accept_all>
34
- `/context accept-all` adopts the package's canonical packaged conception (`samples/conception.json`) as the substrate's `config.json` — the full vocabulary (`block_kinds`, `relation_types`, `lenses`, `invariants`) plus the `installed_schemas` / `installed_blocks` manifest — with `root` set to the actual substrate dir. It writes `config.json` only (run `/context install` after to materialize the schemas + starter blocks) and is idempotent: it never overwrites an existing `config.json` (offer, don't impose). This is the accept-all path; per-entry step-through curation is a separate surface.
34
+ `/context accept-all` adopts the package's canonical packaged conception (`samples/conception.json`) as the substrate's `config.json` — the full vocabulary (`block_kinds`, `relation_types`, `lenses`, `invariants`) plus the `installed_schemas` / `installed_blocks` manifest — with `root` set to the actual substrate dir. It writes `config.json` only (run `/context install` after to materialize the schemas + starter blocks) and is idempotent: it never overwrites a populated `config.json` (offer, don't impose); the vocabulary-empty skeleton `config.json` written by init IS overwritten by the catalog, with the skeleton's on-disk `substrate_id` preserved. This is the accept-all path; per-entry step-through curation is a separate surface.
35
35
  </context_accept_all>
36
36
 
37
37
  <context_install>
@@ -59,7 +59,7 @@ Source-of-truth-drift invariant: `validateContext` requires the active `config.s
59
59
  </cross_substrate>
60
60
 
61
61
  <schema_versioning>
62
- 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`.
62
+ 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/map_each on dotted paths; `map_each` addresses an array — table mode maps each string element through a lookup, with unmatched elements becoming `{relation_type, item_endpoint}` under a parent/child fallback, and set-on-each mode sets a field on every object element). The loaded registry resolves the migration edge at the next read/write, so items walk forward without a process restart. Config loading is migration-aware: a `config.json` whose `schema_version` lags the bundled config schema is walked forward through the `config` migration chain in memory at load (the on-disk file is never rewritten); every substrate-lifecycle ceremony — init / accept-all / install / update / check-status / switch (existing-target and switch-back forms; the target substrate is seeded right after the pointer flip) / resolve-conflict / resolve-blocked — seeds the catalog's `config` identity declaration into `migrations.json` (idempotent) before its first config read, and a version mismatch with no resolvable chain throws. The ceremonies that reach identity-stamping writes (update, install, resolve-blocked) also establish substrate identity at entry: a config carrying no `substrate_id` gets one minted, persisted, and registered before the first stamping write — a pre-identity substrate heals on the sanctioned ceremony instead of refusing — reported in the ceremony result under `substrateIdEstablished` (live runs only; an established identity is never re-minted). On the write side, versioned-document envelopes converge: every block schema declares an optional top-level `schema_version`, and the write path stamps it (config.json's and migrations.json's included) to the owning schema's current `version` on every sanctioned write — an incoming envelope claiming an older version is first walked forward through the registered chain (or refused with the file left byte-unchanged when no chain reaches the current version), then persisted at the current version; reads of a stamped block validate the whole envelope migration-aware. A substrate whose installed schemas predate the `schema_version` property keeps writing unchanged until `/context update` lands it. A `block:<name>` reference resolves to `<contextDir>/schemas/<name>.schema.json`.
63
63
  </schema_versioning>
64
64
 
65
65
  <lens_views>
@@ -67,6 +67,8 @@ Lenses are named projections over a target block. A lens declares `id`, `target`
67
67
 
68
68
  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.
69
69
 
70
+ Edge orientation is declared once, read everywhere. Storage is uniform (`edge.parent` = source endpoint, `edge.child` = target endpoint); which endpoint holds a relation's PRIMARY semantic role (prerequisite/predecessor/gate for `ordering`, container for `membership`, source for `data_flow`) is `config.relation_types[].role_direction` — `as_parent` (primary at `edge.parent`) or `as_child` (primary at `edge.child`), optional and set only for relations with a per-role consumer. The `primaryEndpoint(edge, role_direction)` / `counterEndpoint(edge, role_direction)` helpers read the endpoint under that declaration, and the blocked/ready deriver (`state_derivation.blocked_by` gate-vs-dependency split), the milestone rollup, the derived roadmap (precedes + membership), and `promote-item`'s lineage edge all route through it rather than hardcoding parent/child. Authoring: `append-relation` / `append-relations` take EITHER raw `--parent`/`--child` OR role-typed `--primary`/`--counter` (mapped to parent/child via `role_direction`; mutually exclusive) — a bare `--parent`/`--child` append of a relation that is BOTH role-bearing and orientation-ambiguous (its source/target kinds overlap, incl. `"*"`) is rejected in favor of `--primary`/`--counter`, while a role-less or disjoint-kind relation appends bare unchanged. Reading: `context-walk-descendants` / `walk-ancestors` on a disjoint-kind relation queried from the wrong endpoint THROWS naming the correct op instead of returning an ambiguous `[]`. `replace-relation` writes raw endpoints verbatim (bypassing the orientation gate) — the re-orient affordance; run `context-validate` after.
71
+
70
72
  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.)
71
73
 
72
74
  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.
@@ -140,7 +142,7 @@ On `session_start`, checks npm registry for newer versions of `@davidorex/pi-pro
140
142
  </update_check>
141
143
 
142
144
  <success_criteria>
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.
145
+ - `<substrate-dir>/`, `<substrate-dir>/schemas/`, the `.pi-context.json` bootstrap pointer, a skeleton `config.json`, and a seeded `migrations.json` exist after `/context init <substrate-dir>` (init imposes no catalog: no vocabulary, 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.
144
146
  - `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
145
147
  - Block writes validate against schemas — invalid data rejected with specific error
146
148
  - `/context status` returns current derived state without errors