@davidorex/pi-context 0.30.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.
- package/CHANGELOG.md +127 -0
- package/README.md +27 -11
- package/dist/block-api.d.ts +13 -0
- package/dist/block-api.d.ts.map +1 -1
- package/dist/block-api.js +28 -3
- package/dist/block-api.js.map +1 -1
- package/dist/content-hash.d.ts +13 -0
- package/dist/content-hash.d.ts.map +1 -1
- package/dist/content-hash.js +16 -0
- package/dist/content-hash.js.map +1 -1
- package/dist/context-dir.d.ts +12 -0
- package/dist/context-dir.d.ts.map +1 -1
- package/dist/context-dir.js +14 -0
- package/dist/context-dir.js.map +1 -1
- package/dist/context-sdk.d.ts +71 -33
- package/dist/context-sdk.d.ts.map +1 -1
- package/dist/context-sdk.js +547 -149
- package/dist/context-sdk.js.map +1 -1
- package/dist/context.d.ts +213 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +119 -5
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +595 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2225 -55
- package/dist/index.js.map +1 -1
- package/dist/lens-view.d.ts +0 -5
- package/dist/lens-view.d.ts.map +1 -1
- package/dist/lens-view.js +43 -1
- package/dist/lens-view.js.map +1 -1
- package/dist/migration-registry-loader.d.ts +36 -12
- package/dist/migration-registry-loader.d.ts.map +1 -1
- package/dist/migration-registry-loader.js +79 -17
- package/dist/migration-registry-loader.js.map +1 -1
- package/dist/migrations-store.d.ts +45 -18
- package/dist/migrations-store.d.ts.map +1 -1
- package/dist/migrations-store.js +56 -22
- package/dist/migrations-store.js.map +1 -1
- package/dist/ops-registry.d.ts +16 -0
- package/dist/ops-registry.d.ts.map +1 -1
- package/dist/ops-registry.js +352 -117
- package/dist/ops-registry.js.map +1 -1
- package/dist/pending-blocked-store.d.ts +83 -0
- package/dist/pending-blocked-store.d.ts.map +1 -0
- package/dist/pending-blocked-store.js +93 -0
- package/dist/pending-blocked-store.js.map +1 -0
- package/dist/promote-item.d.ts.map +1 -1
- package/dist/promote-item.js +41 -12
- package/dist/promote-item.js.map +1 -1
- package/dist/roadmap-plan.d.ts +121 -99
- package/dist/roadmap-plan.d.ts.map +1 -1
- package/dist/roadmap-plan.js +281 -345
- package/dist/roadmap-plan.js.map +1 -1
- package/dist/schema-merge.d.ts +26 -0
- package/dist/schema-merge.d.ts.map +1 -0
- package/dist/schema-merge.js +176 -0
- package/dist/schema-merge.js.map +1 -0
- package/dist/status-vocab.d.ts +12 -2
- package/dist/status-vocab.d.ts.map +1 -1
- package/dist/status-vocab.js +14 -1
- package/dist/status-vocab.js.map +1 -1
- package/package.json +2 -1
- package/samples/blocks/milestone.json +3 -0
- package/samples/blocks/session-notes.json +1 -0
- package/samples/conception.json +358 -15
- package/samples/migrations.json +8 -0
- package/samples/schemas/context-contracts.schema.json +4 -0
- package/samples/schemas/conventions.schema.json +4 -0
- package/samples/schemas/decisions.schema.json +4 -0
- package/samples/schemas/features.schema.json +4 -0
- package/samples/schemas/framework-gaps.schema.json +5 -1
- package/samples/schemas/issues.schema.json +30 -2
- package/samples/schemas/layer-plans.schema.json +6 -2
- package/samples/schemas/milestone.schema.json +79 -0
- package/samples/schemas/phase.schema.json +4 -0
- package/samples/schemas/rationale.schema.json +4 -0
- package/samples/schemas/requirements.schema.json +4 -0
- package/samples/schemas/research.schema.json +5 -1
- package/samples/schemas/session-notes.schema.json +89 -0
- package/samples/schemas/spec-reviews.schema.json +4 -0
- package/samples/schemas/story.schema.json +8 -0
- package/samples/schemas/tasks.schema.json +4 -0
- package/samples/schemas/verification.schema.json +4 -0
- package/samples/schemas/work-orders.schema.json +6 -2
- package/schemas/config.schema.json +101 -3
- package/schemas/migrations.schema.json +25 -0
- package/schemas/pending-blocked.schema.json +190 -0
- package/skill-narrative.md +14 -10
- package/skills/pi-context/SKILL.md +127 -49
- package/skills/pi-context/references/bundled-resources.md +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,133 @@ All notable changes to this package are documented here. Format follows [Keep a
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.32.0] - 2026-07-05
|
|
8
|
+
|
|
9
|
+
- Ceremony-entry identity establishment: the substrate-lifecycle ceremonies that reach identity-stamping writes — `update` (live runs), `install`, and `resolve-blocked` — establish substrate identity at entry when `config.json` carries no `substrate_id`: one is minted (the same mint init uses), persisted through the sanctioned config write, and registered in the project registry before the ceremony's first stamping write, and the ceremony result reports it under `substrateIdEstablished`. A pre-identity substrate thereby heals on the sanctioned ceremony instead of refusing its populated-block version bump (the former mislabeled refusal), dry and live update runs agree on the outcome, and the two previously-skipped migrate-path tests are re-greened. An established identity is never re-minted (the on-disk id stays immutable), `--dryRun` establishes nothing (a preview performs no stamping write), and the block writer's own identity guard stays load-bearing for any write reaching it without identity.
|
|
10
|
+
- The rendered blocked report's trailing fix-items-then-resolve-blocked guidance renders only when at least one blocked entry actually persists a pending record (`validation-failed` / `no-migration-chain`); a report whose entries are all `write-failed` ends with the per-entry precondition guidance instead of a contradictory instruction to correct items that were never flagged invalid.
|
|
11
|
+
- Blocked-resync refusals are classified by cause: only an AJV validation failure reports reason `validation-failed`; any other throw at the resync write boundary (e.g. a substrate with no established `substrate_id` hitting the mandatory identity stamp, at either the same-version or version-bump arm) reports the new reason `write-failed` with the thrown message carried in the failures entry. A `write-failed` refusal inscribes no failure markers into the block file and persists no pending-blocked record — those consequences now fire only for genuine item-validation failures — and the rendered blocked report tells the operator the items were not flagged invalid instead of directing item fixes. `resolve-blocked`'s commit is now all-or-nothing: a throw partway through the commit restores every touched file byte-exact (migrations.json, the installed schema, the block file, config.json, the pending record) and reports the failure, where previously it could strand a partially-committed state (schema advanced, markers stripped, block unwritten, pending entry stale).
|
|
12
|
+
- `update`'s result makes per-component partial application legible in one place: an optional `partialApplication` field is populated exactly when the run both refused something (`blocked`, `refused`, or `conflicts` non-empty) and applied something (`resynced`, `migrated`, `merged`, `migrationsRegistered`, or any `registryAdditions` array non-empty) — carrying `applied`/`notApplied` mirrors of those channels plus a one-line summary naming what was applied alongside what was refused and why — so a run that blocks a schema while the additive registry propagation (or another schema's resync) lands no longer reads as a no-op. Computed for live and `--dryRun` runs (the preview reports the predicted partiality and declares itself a preview); a fully-clean or fully-refused run carries no field.
|
|
13
|
+
- The foreign-substrate config read feeding the relation porcelain's id index prefers the migration-aware loader over its prior raw parse, so a lagging-but-chained foreign config is read in its migrated shape — no reader of `config.json` sees a different shape than `loadConfig` where a chain resolves. Best-effort in two tiers: when the migration-aware load fails (unresolvable version, invalid), the raw parse is the fallback so the prefix invariant stays fed (a mis-filed foreign id keeps degrading to dangling rather than resolving); absent or unparsable still returns null.
|
|
14
|
+
- The composed `context-validate` verdict now covers block schema validity: a substrate-wide sweep validates every block that has both an installed schema and a block file against that schema — migration-aware when the envelope carries `schema_version` (a lagging version walks the registered chain; an unresolvable one surfaces), raw otherwise — and every failure lands as an ERROR naming the block, the failing item id, and the field/keyword (`block_schema_invalid`). Invalidity introduced without a write to the block (a schema resync, a hand edit) is no longer invisible behind a clean verdict. Pointer-less projects skip the sweep gracefully.
|
|
15
|
+
- `update`'s same-version resync arm re-validates populated blocks against the incoming catalog body BEFORE overwriting, and refuses on failure — the schema is reported `blocked` with per-item failures (reason `validation-failed`), the pending-blocked record and in-file failure markers follow the same flow as a version-bump refusal, and `--dryRun` predicts the same outcome. A same-version catalog change that narrows validity (a dropped property, an added required, a narrowed enum) can no longer silently invalidate existing items; compatible same-version changes still resync verbatim.
|
|
16
|
+
- The catalog story schema declares an optional `user_kind` on story items (the "As a `<user_kind>`" subject of the story statement), matching the field live stories carry.
|
|
17
|
+
- The bundled config schema is gated expand-contract at build time: `scripts/check-config-schema.ts` (pre-commit + CI, staged and `--base` modes diffing against the prior git revision) fails a non-additive `config.schema.json` change — a removed or renamed property key anywhere in the properties tree, or a new `required` entry on a pre-existing object — unless the same change set advances the schema `version` AND declares a packaged `config` migration reaching that version in `samples/migrations.json`. Additive changes (new optional fields, description/enum additions) pass untouched; a newly-added optional object's own initial requireds are additive by construction. New config fields land optional; removals/renames wait for a contract release paired with a migration.
|
|
18
|
+
- Envelope `schema_version` is live end-to-end: every block schema in the packaged catalog (`samples/schemas/`, 17 files; session-notes already declared it) admits an optional top-level `schema_version`, and `writeTypedFile` stamps every versioned-document envelope to the owning schema's current `version` on write — self-gated on the schema both declaring the property and carrying a `version`, so block writes, whole-block writes, `config.json` (converges to the bundled config schema version on its next sanctioned write, ending the permanently-persisted stale config version), and `migrations.json` all converge, while substrates whose installed schemas predate the property write unchanged until `/context update` lands it. With envelopes stamped, the previously-dormant read-time migration hook and pre-write version gate are active: an envelope claiming an older version is walked forward through the registered migration chain (read and write), and a version with no resolvable chain fails loud — the read throws and a write is refused with the block file left byte-unchanged.
|
|
19
|
+
- The packaged catalog (`samples/conception.json`) declares `role_direction` on the 17 relation_types that have a per-role consumer, so fresh `accept-all`/`install` substrates carry the orientation source of truth the deriver / rollup / roadmap / promote / write / read surfaces read. `as_parent` (primary at `edge.parent`): `task_depends_on_task`, `feature_depends_on_item`, `story_depends_on_story`, `requirement_depends_on_requirement`, `milestone_precedes_milestone`, `decision_supersedes_decision`, `research_supersedes_research`, `feature_contains_story`, `story_includes_item`. `as_child` (primary at `edge.child`): `task_gated_by_item`, `feature_gated_by_item`, `story_gated_by_item`, `decision_gated_by_item`, `phase_positioned_in_milestone`, `task_positioned_in_phase`, `item_derived_from_item`, `decision_derived_from_item`. The values are reverse-engineered from the convention already stored/consumed, so derived output is unchanged and the latent gate/rollup siblings are pre-empted; the other 27 relations (no role consumer) omit the field.
|
|
20
|
+
- The closure-table walk wrappers `walkLensDescendants` (`context-walk-descendants`) and `walkAncestorsByLens` (`walk-ancestors`) now SIGNAL a wrong-orientation query instead of silently returning `[]`. For a DISJOINT-kind relation (its `source_kinds` and `target_kinds` share no kind and neither is the `"*"` wildcard), a descendants walk is only meaningful from a SOURCE-kind id and an ancestors walk only from a TARGET-kind id; when the queried id resolves to a block that sits exclusively on the wrong end for the requested direction, the walk THROWS naming the correct op (a wrong-end `context-walk-descendants` points to `walk-ancestors`, and vice-versa). Same-kind / wildcard-endpoint relations, an unregistered relation, and an id that resolves to no block stay structurally un-disambiguatable and return `[]` as before; `find-references both` is symmetric and carries no signal.
|
|
21
|
+
- The relation-append porcelain (`append-relation` / `append-relations` ops + the `appendRelationByRef` / `appendRelationsByRef` SDK fns) accepts a ROLE-TYPED authoring form alongside the raw `{parent, child}` form: `--primary`/`--counter` (or `{primary, counter}` in an `append-relations` edge object) name the relation's semantic-role endpoints and are mapped to `parent`/`child` via the relation's declared `role_direction`. The two orientation pairs are mutually exclusive. For a relation that is BOTH role-bearing and orientation-ambiguous (its `source_kinds` and `target_kinds` overlap, including the `"*"` wildcard — e.g. a same-kind `task↔task` ordering relation), a bare `--parent`/`--child` append is now REJECTED with a message directing the author to `--primary`/`--counter`; the porcelain never guesses or auto-swaps. A role-less relation, or a role-bearing relation whose endpoint kinds are disjoint (self-orienting via the existing source/target-kind gate), is unaffected — its bare append still succeeds. Supplying `--primary`/`--counter` for a relation that declares no `role_direction` throws (there is no role to map).
|
|
22
|
+
- `promoteItem` orients the `item_derived_from_item` lineage edge it files into the destination substrate from that relation's declared `role_direction`: the SOURCE (the item derived from) is placed at the PRIMARY endpoint and the new-derived item at the COUNTER endpoint. `item_derived_from_item` is `as_child` (source at `edge.child`), reproducing the prior parent=new-derived / child=source layout; the `as_child` fallback preserves that layout when the destination config registers no `role_direction`. A destination declaring the relation `as_parent` mirrors the edge (parent=source, child=new-derived).
|
|
23
|
+
- The derived roadmap (`context-roadmap-load` / `context-roadmap-render` / `context-roadmap-validate`) reads the orientation of its three domain relations from their declared `role_direction` via the `primaryEndpoint`/`counterEndpoint` helpers rather than hardcoded parent/child picks: the precedes DAG treats the predecessor as the PRIMARY endpoint of `milestone_precedes_milestone` (fallback `as_parent`), and the phase/task membership tiers treat the container as the PRIMARY endpoint of `phase_positioned_in_milestone` / `task_positioned_in_phase` (fallback `as_child`). The former `MILESTONE_PRECEDES` / `PHASE_IN_MILESTONE` / `TASK_IN_PHASE` module constants are removed. With the stock (or absent) role_direction the derived order, membership, and validation output are unchanged; declaring a non-default `role_direction` reorients the roadmap accordingly.
|
|
24
|
+
- `currentState`'s config-declared membership rollups (`state_derivation.rollups`, e.g. the milestone rollup) read their container/member orientation from the membership relation's declared `role_direction` via the `primaryEndpoint`/`counterEndpoint` helpers: the CONTAINER (the rollup item) is the PRIMARY endpoint, the MEMBERS are the COUNTER endpoint. `phase_positioned_in_milestone` is `as_child` (container=milestone at `edge.child`, member=phase at `edge.parent`), reproducing the prior filter-child / map-parent selection exactly; a membership relation the config does not register with a `role_direction` defaults to `as_child` (the prior container=child convention). A `contains`-shaped (`as_parent`) membership relation now rolls up correctly (member read at the child endpoint) instead of finding zero members.
|
|
25
|
+
- `currentState` (the derivation behind `context-current-state` / `/context status`) partitions its `state_derivation.blocked_by` relations into gate-direction vs dependency-direction from each relation's declared `role_direction` rather than the former single `task_gated_by_item` string literal: a blocked_by relation declared `as_child` reads as a GATE (the waiting item at `edge.parent`, its gate target at `edge.child`), any other (`as_parent` or a relation the config does not register with a `role_direction`) reads as a DEPENDENCY (the item's prerequisites are the parents of edges whose child is the item). For the stock `blocked_by` set `{task_depends_on_task (as_parent), task_gated_by_item (as_child)}` the classification and the derived `blocked` / `nextActions` / `blockedBy` output are unchanged; the difference is that a same-shape gate sibling (`feature_/story_/decision_gated_by_item`) added to `blocked_by` now reads the correct gate direction by construction instead of the swapped dependency direction the literal-keyed deriver produced.
|
|
26
|
+
- Relation orientation — which endpoint holds a relation's PRIMARY semantic role (prerequisite/predecessor/gate for `ordering`, container for `membership`, source for `data_flow`) — is now declared once as config metadata rather than hardcoded per consumer. `config.relation_types[].role_direction` (`as_parent` | `as_child`, optional, reusing the existing invariant-direction vocabulary) names where the primary role sits: `as_parent`=`edge.parent`, `as_child`=`edge.child`. Two pure helpers `primaryEndpoint(edge, role_direction)` / `counterEndpoint(edge, role_direction)` (exported from `@davidorex/pi-context` and `@davidorex/pi-context/context-sdk`) read the endpoint under that declaration. The field is presence-gated — populated only for relations with a per-role consumer (deriver / rollup / roadmap / promote / write-orientation / read-signal); role-less relations omit it. The config schema advances to version 1.8.0 (the addition is optional; the packaged `samples/migrations.json` config chain carries a `1.0.0→1.8.0` identity migration, so a catalog-born config migrates cleanly and a config already declaring the field validates unchanged).
|
|
27
|
+
- The roadmap is a DERIVED view over the `milestone_precedes_milestone` relation, which the packaged catalog (`samples/conception.json`) now registers (category `ordering`, source/target `milestone`, modeled on `decision_supersedes_decision`): `context-roadmap-load` / `context-roadmap-render` / `context-roadmap-validate` take no parameters and derive the roadmap entirely from substrate state — milestone-block items topo-ordered by the authored precedes edges (order + cycles), each milestone carrying its derived status/phaseCount (currentState's config-declared milestone rollup), member phases via `phase_positioned_in_milestone`, per-phase tasks via `task_positioned_in_phase`, and per-phase + per-milestone status rollups. Adjacency in the loaded view and the rendered **Preceded by:** lines comes strictly from the authored edges, never inferred from order consecutive pairs. The filed design specified a precedes-CHAIN; the implementation generalizes chain → DAG (the approved delta): multiple starts and joins topo-order correctly, a linear chain being the degenerate case, and zero milestones is a valid empty view. The roadmap BLOCK is retired with the derivation: no `roadmap.json`, no ROADMAP- ids, no roadmap block kind — the `context-roadmap-list` op, the `/context roadmap-list` subcommand, the `loadRoadmap(cwd, roadmapId)` / `listRoadmaps` / `validateRoadmaps` / `PhaseSpec` / `RoadmapSpec` / `PhaseView` / `RoadmapView` exports, and the legacy `registry/schemas/roadmap.schema.json` fixture are removed (`/context roadmap-view` and `/context roadmap-validate` no longer take an id; the new exports are `loadRoadmap(cwd)`, `validateRoadmap`, `MilestoneRoadmapView`, `MilestoneView`, `PhaseRollupView`, `TaskRow`). Validation codes are rewired to the derived model: errors `roadmap_precedes_endpoint_missing` (a precedes-edge endpoint that is not a milestone-block item), `roadmap_milestone_cycle`, `roadmap_milestone_missing` (a `phase_positioned_in_milestone` edge whose child is not a known milestone); warning `roadmap_status_unknown_value`; and a new INFO tier — `roadmap_milestone_isolated` (a milestone with zero precedes edges while others are ordered) — that never affects the validation status (invalid iff any error, warnings iff any warning, else clean) and is excluded from the `context-validate` lens-validator merge, which contributes error-code issues only, block-framed `milestone`.
|
|
28
|
+
- The SKILL's `/context init` and `/context accept-all` descriptions state the ceremony's actual writes: init bootstraps the pointer + dirs, a vocabulary-empty SKELETON `config.json` (minted + registered `substrate_id`), and the seeded catalog `config` migration declarations; accept-all never overwrites a POPULATED config but DOES overwrite the init skeleton (preserving its on-disk `substrate_id`), and `config.json`'s `root` is written first at init and again at accept-all.
|
|
29
|
+
- `resyncSchema`'s internal collision-key separator is spelled as the `\u0000` escape sequence instead of a literal NUL byte — the produced runtime string is identical; `src/index.ts` (and the built `dist/index.js`) are plain text again for byte-sensitive tools.
|
|
30
|
+
- New `map_each` declarative-transform primitive (TransformOp, alongside rename/set/delete/coerce): addresses an ARRAY at a dotted `$`-path (absent path / non-array value no-op) and maps its elements in one of two modes — table mode replaces each string element through a lookup table, with an unmatched string becoming `{relation_type: <element>, item_endpoint: <fallback>}` under a `parent`/`child` fallback (default `parent`) and non-string elements passing through; set-on-each mode assigns a declared `field`=`value` on every object element (non-object elements skipped). The schema variant enforces table XOR field+value. Registered in `migrations.schema.json`, the `TransformOp` TS union, and the loader's op applicator; surfaced in the `write-schema-migration` op description.
|
|
31
|
+
- Config loading (`loadConfig` / `loadConfigForDir`) is migration-aware: when `config.json`'s `schema_version` differs from the bundled config schema's `version`, the substrate's registered `config` migration chain walks the data forward in memory before validation (the on-disk file is never rewritten); a mismatch with no resolvable chain throws (fail-fast — no raw-validate fallback). On version match the migration registry is never consulted.
|
|
32
|
+
- The packaged catalog (`samples/migrations.json`) ships a `config 1.0.0→1.7.0` identity migration declaration, and EVERY sanctioned ceremony entry point — `/context init` (`writeSkeletonConfig`), `/context accept-all` (`adoptConception`), `/context install` (`installContext`), `/context update` (`updateContext`), `/context check-status` (`checkStatus`), the `/context switch` existing-target and switch-back forms (`switchToExisting` / `switchToPrevious`, seeding the TARGET substrate right after the pointer flip), `/context resolve-conflict` (`resolveConflict`), and `/context resolve-blocked` (`resolveBlocked`) — seeds the catalog's `config` declarations into the substrate's `migrations.json` via the exported `seedCatalogConfigMigrationDecls(substrateDir, ctx?)` (idempotent: a present `(schemaName, fromVersion)` pair is never replaced; returns the appended edges) BEFORE its first config read, so catalog-born substrates and re-entered legacy substrates carry a resolvable config chain before any config read. The initial change seeded only init / accept-all / install; an adversarially-verified finding showed a lagging legacy substrate (1.0.0 config, no `migrations.json`) still threw on `update` / `check-status` / a pointer flip onto it — the class rule now applied is that every ceremony entry point seeds. `check-status` and a `--dryRun` update thereby perform this one idempotent seed write (the designed heal semantic, consistent with idempotent re-init healing) while remaining otherwise read-only. The seed helper is additionally a no-op on a NONEXISTENT substrate dir (guarded at its first statement, one guard covering every present and future call site): the seed heals substrates, it never materializes them — a bootstrap pointer naming a dir that was never created degrades with zero writes (no dir creation, no `migrations.json` residue) rather than the seed mkdir-ing the dir into existence; ceremonies that need the dir create it themselves before seeding — concretely, the WRITE-flavored ceremonies whose job is materializing config (`adoptConception` for accept-all, `writeSkeletonConfig` for init / switch -c) mkdir the substrate dir immediately before their seed call, so a first-touch accept-all on a pointer whose dir was never created still lands the seeded config chain before the config write, while the read-flavored ceremonies (check-status / update / switch-back onto a degenerate pointer) rely on the helper's no-op guard and write nothing. An EXISTING dir without a `config.json` remains seeded by design (mid-ceremony states such as init→accept-all need the seed before, and independent of, config presence).
|
|
33
|
+
- The `context-check-status` and `context-update` reflected op `description` strings state the seed-at-entry behavior: each names the one idempotent catalog-config-migration seed write performed before the first config read, with the drift report / `--dryRun` preview otherwise unchanged in their read-only framing.
|
|
34
|
+
- `seedCatalogConfigMigrationDecls` no-ops (returns `[]`, zero writes) when the substrate dir does not exist — the seed heals substrates, it never materializes them, so a read-flavored ceremony (`check-status`, `update`, a switch-back) invoked against a bootstrap pointer naming a nonexistent dir degrades exactly as before seeding existed (no dir creation, no `migrations.json` residue). The write-flavored ceremonies that legitimately materialize config (`accept-all`'s `adoptConception`, `init`'s `writeSkeletonConfig`) mkdir their substrate dir immediately before seeding, so a first-touch accept-all still lands the config chain before the config write.
|
|
35
|
+
- Every `migrations.json` write now invalidates the migration-registry cache at the write funnel (`writeMigrationsFileForDir`) rather than per mutation helper, so any write path through the store — including the new ceremony seeding — is cache-coherent in-process (fixes a staleness window where a registry warmed empty before a raw whole-file write kept serving the pre-write declarations).
|
|
36
|
+
- The packaged catalog (`samples/conception.json`) now registers the config vocabulary that shipped derivations already referenced but the catalog had not defined, so fresh `accept-all`/`install` substrates carry it and are internally consistent: relation_types `task_gated_by_item` (the blocking relation `state_derivation.blocked_by` already names), `task_advances_story` / `feature_advances_story` (the relations the `story-advancers` / `story-advancers-features` lenses project), `decision_derived_from_item` / `decision_escalates_underdetermined` (the relations the `decision-shows-derivation` invariant requires), and `session_touches_item`; the lenses `gaps-by-priority`, `features-by-status`, `story-advancers`, `story-advancers-features`; the invariant `decision-shows-derivation`; and the `session-notes` block kind (prefix `SESSION-`, array_key `sessions`) with its schema (`samples/schemas/session-notes.schema.json`), an empty starter block (`samples/blocks/session-notes.json`), and `session-notes` added to `installed_schemas` / `installed_blocks` so `install` materializes `session-notes.json`. A fresh catalog-built substrate now validates with no unregistered-relation reference and accepts `task_gated_by_item` / `task_advances_story` / `session_touches_item` edges.
|
|
37
|
+
- The packaged catalog (`samples/conception.json`) `state_derivation.next_ranked` push order is `tasks`, then `issues`, then `framework-gaps` — so `nextActions` (`context-current-state` / `/context status`) surfaces ready unblocked tasks ahead of open issues and open framework-gaps in the derived next-actions head (the deriver walks `next_ranked` in array order, then truncates to `head_size`). Each entry's content (kind, label, bucket, rank_field, rank_order, reason_template) is unchanged from before; only the entries' order in the array differs.
|
|
38
|
+
- The packaged catalog (`samples/conception.json`) now treats issues as a gap-sibling open-work kind: it ships relation_types `task_addresses_issue` (a task addressing an issue) and `issue_relates_to_issue` (an issue related to another issue); a `task-completed-issue-resolved` status-consistency invariant (warning) flagging a completed task whose `task_addresses_issue` target is not resolved; a `state_derivation.next_ranked` entry for `issues` (open issues ranked by `priority` critical→high→medium→low, surfaced in `nextActions` of `context-current-state` / `/context status` after tasks and before framework-gaps); and `issues-by-status` (open/resolved/deferred) + `issues-by-priority` (critical/high/medium/low) lenses. The issues schema (`samples/schemas/issues.schema.json`, version unchanged) additively gains an optional `resolved_at` (ISO-8601 resolution timestamp) field and an `x-lifecycle` block declaring the `status` states (open/resolved/deferred) and authority-gated transitions (open→resolved any, open→deferred user).
|
|
39
|
+
- Each `next_ranked` entry in the `state_derivation` registry now carries an optional `reason_template` (string) supplying the `reason` string `currentState` (`context-current-state` / `/context status`) emits for the items that entry surfaces in `nextActions`. The template substitutes `{rank_value}` (the item's `rank_field` value, or `unset` when absent) and `{id}` (the item id) literally where they appear. The packaged catalog ships the stock templates — `open gap (priority {rank_value})` on the priority-ranked `framework-gaps` entry and `unblocked planned task` on the topo `tasks` entry — so a custom `state_derivation` declaring non-`framework-gaps`/`tasks` kinds emits its own configured reason strings, and the `focus` fallback prefix is drawn from `focus_fallback.kind` (stock `phase`, yielding `phase: <id>`). The config schema is at version 1.7.0.
|
|
40
|
+
- A substrate whose config declares NO `state_derivation` makes `currentState` (`context-current-state` / `/context status`) report `focus: "state-derivation not configured"` with empty `inFlight` / `nextActions` / `blocked` / `milestones` — a signal distinct from a configured-but-empty substrate (which derives `focus: "no active focus."` with empty arrays). The derivation's in-flight kinds/bucket, focus fallback, next-actions ranking + order, blocking relation set, milestone rollup, and head-size cap are all read from the declared registry.
|
|
41
|
+
- `amend-config --registry state_derivation` addresses the `state_derivation` singleton per-key (a map-kind registry: its six top-level keys are the map entries), and `update` brings the catalog's whole `state_derivation` object into a substrate that lacks one while leaving an existing one untouched (merged whole, never body-merged).
|
|
42
|
+
- The packaged catalog (`samples/conception.json`) ships the stock `state_derivation` registry — `in_flight` over `tasks` at `in_progress`, `focus_fallback` on `phase` at `in_progress`, a `next_ranked` of `tasks` (topo) then priority-ranked `issues` then priority-ranked `framework-gaps` (P0..P3), `blocked_by` the `task_depends_on_task` + `task_gated_by_item` relations, a `milestone` rollup over `phase_positioned_in_milestone` (reached/planned), and `head_size` 15 — so fresh `accept-all`/`install` substrates carry the derivation declaration.
|
|
43
|
+
- `currentState` (the derivation behind `context-current-state` / `/context status`) derives its focus, in-flight set, next-actions ranking, blocked set, and milestone rollup from a new config-declared `state_derivation` registry rather than from values baked into the deriver. The registry is a singleton config object with six keys: `in_flight` (block kinds + status bucket counted as in-flight), `focus_fallback` (kind + bucket for the focus fallback when nothing is in-flight), `next_ranked` (the ordered cross-kind push order for next-actions; each entry selects a kind at a bucket and either ranks within itself via a named field + ordered value list or topo-orders over the blocking-relation graph), `blocked_by` (the relation_types whose edges contribute blockers), `rollups` (membership rollups such as milestones, with their complete/incomplete status strings), and `head_size` (the next-actions truncation cap). The packaged catalog (`samples/conception.json`) ships the stock registry — tasks/phase/issues/framework-gaps/milestone kinds, the `task_depends_on_task` + `task_gated_by_item` blocking relations, the `next_ranked` push order of `tasks` (topo) then priority-ranked `issues` then priority-ranked `framework-gaps` (P0..P3), and head size 15 — so fresh `accept-all`/`install` substrates carry it and the derived output is unchanged. A substrate whose config has no `state_derivation` reports `focus: "state-derivation not configured"` with empty arrays, distinguishing an un-configured substrate from a configured-but-empty one. `state_derivation` is addressable per-key through `amend-config --registry state_derivation` (a map-kind registry); `update` brings the whole object into a substrate that lacks it while leaving an existing one untouched. The config schema is at version 1.7.0.
|
|
44
|
+
- The packaged catalog ships an empty `milestone` starter block (`samples/blocks/milestone.json`, `{ "milestones": [] }`, matching the block kind's `data_path` `milestone.json`), so `install` / `context-install` materializes a `milestone.json` block in the substrate alongside the milestone schema.
|
|
45
|
+
- The packaged catalog (`samples/conception.json`) now ships a `milestone` block kind (prefix `MILE-`, array_key `milestones`, schema `samples/schemas/milestone.schema.json`) and its schema, so fresh `accept-all`/`install` substrates carry it. A milestone is a phase-rollup checkpoint with fields `id` (`MILE-NNN`), required `name`, required `status` (`planned` | `reached`), and an optional `release` grouping string. `status` is DERIVED, not authored: `currentState` (the derivation behind `context-current-state` / `/context status`) reports a milestone as `reached` when at least one phase is positioned in it via a `phase_positioned_in_milestone` edge (phase is the parent, milestone the child) and the parent phase of every such edge buckets to complete, and `planned` otherwise (covering no-phases and any-incomplete). The catalog also adds two membership relation_types — `phase_positioned_in_milestone` (phase → milestone, driving the rollup) and `story_includes_item` (story → any kind, the single relation expressing a story's scope) — and replaces the former `story_contains_task` with `story_includes_item`. A `status-consistency` invariant `reached-milestone-phases-complete` (severity error) backstops the derived state: a milestone whose own status buckets to complete must have the parent phase of every `phase_positioned_in_milestone` edge complete, surfaced by `context-validate`. `reached` maps to the complete status bucket in the default status vocabulary.
|
|
46
|
+
- `currentState` (the derivation behind `context-current-state` / `/context status`) now folds `task_gated_by_item` gates into task-readiness alongside the existing `task_depends_on_task` dependencies. A planned task carrying a `task_gated_by_item` edge to a target that has not reached the complete bucket is reported in `blocked` (the gate target id present in `blockedBy`, unioned with any unsatisfied dependency parents) and excluded from `nextActions`; a gate target of any kind reaching its complete status (gap→closed, decision→enacted, feature→complete, task→completed) releases the gate. Gate satisfaction reuses the same `bucket(target) === "complete"` check via the config-resolved status vocabulary that the status-consistency invariant engine uses — kind-general, no per-kind special-casing and no parallel completeness notion. A dangling gate target (id resolving to no item) is treated as satisfied/non-blocking, mirroring the dangling-dependency guard. Scope is the literal relation_type `task_gated_by_item`: `decision_gated_by_item` and other non-task `*_gated_by_item` edges do not alter `currentState`. A gate target in a terminal-abandoned status (`wontfix` / `superseded` / `cancelled`, which bucket to `unknown` rather than `complete`) keeps the gated task blocked; promoting such states to gate-releasing and generalizing the gate-aware derivation to all gate relation kinds is the config-driven refinement boundary, deferred. With no `task_gated_by_item` edges present the derivation output is unchanged.
|
|
47
|
+
- The packaged catalog (`samples/conception.json`) now ships three relation_types: `decision_raises_gap` (a decision raising a framework-gap), `decision_gated_by_item` (a decision enactment-gated by an item), and `gap_relates_to_gap` (a framework-gap related to another framework-gap), so fresh `accept-all`/`install` substrates carry them.
|
|
48
|
+
- The friendly-selector edge-write porcelain (`appendRelationByRef` / `appendRelationsByRef`) now validates each resolved edge against the relation_type registry at write time: an edge whose `relation_type` is not registered in `config.relation_types`, or whose resolved source/target endpoint block is not in that relation_type's declared `source_kinds` / `target_kinds`, throws and is never persisted (a batch is all-or-nothing — a single invalid edge rejects the whole `appendRelationsByRef` call before any write; `dryRun` rejects identically). A relation_type declaring neither `source_kinds` nor `target_kinds` is unchecked for endpoint kinds (presence-gated). This is the same registration + endpoint-kind check `context-validate` performs post-hoc, now factored into a shared `validateEdgeAgainstRegistry` helper invoked at both surfaces so write-time and validate-time verdicts are identical.
|
|
49
|
+
|
|
50
|
+
### Documentation
|
|
51
|
+
- Edge orientation is documented across the surfaces the metadata change touches: the `append-relation` / `append-relations` op `description` + `promptSnippet` state the raw-vs-role-typed authoring forms and the ambiguous-bare-append reject; `context-walk-descendants` / `walk-ancestors` state the disjoint-kind wrong-orientation throw (naming the counterpart op); `replace-relation` states that it writes raw endpoints verbatim, bypassing the orientation gate (the re-orient affordance, `context-validate` after). The package README's relations section gains an "Edge orientation" paragraph (the `role_direction` source of truth + `primaryEndpoint`/`counterEndpoint` + authoring/read behavior), and `skill-narrative.md` gains the matching orientation prose; the root README's tool enumeration carried no orientation claim to correct. Regenerate `skills/pi-context/SKILL.md` (`npm run skills`) to pick up the changed op strings.
|
|
52
|
+
- Regenerated `skills/pi-context/SKILL.md` from the edge-orientation `skill-narrative.md` + op-string edits: the tool entries now carry the role-typed `append-relation`/`append-relations` authoring + ambiguous-bare-append reject, the `context-walk-descendants`/`walk-ancestors` disjoint-kind wrong-orientation throw, and the `replace-relation` raw-endpoint gate-bypass note.
|
|
53
|
+
- The `next_ranked` doc strings — the `config.schema.json` `next_ranked` `description`, the `next_ranked` field JSDoc in `context.ts`, and the `CurrentState.nextActions` field doc + `nextActions` deriver inline comment in `context-sdk.ts` — state the stock cross-kind push order as tasks (topo-ordered) then issues (priority-ranked) then framework-gaps (priority-ranked), matching the packaged catalog's `state_derivation.next_ranked` array order.
|
|
54
|
+
- The `context-current-state` op `description` + `promptSnippet` state that focus, in-flight, ranked next-actions, blocked, and milestone rollups all derive from the config-declared `state_derivation` registry (in-flight kinds + bucket, focus fallback, the ordered next-actions push order with per-entry field-ranking or topo ordering, the blocking relation set, the membership rollups with their status strings, and the head-size cap), and that a substrate without the registry reports focus 'state-derivation not configured'.
|
|
55
|
+
- Regenerated `skills/pi-context/SKILL.md` for the `context-current-state` op's config-declared `state_derivation` derivation surface — the tool entry now states that focus, in-flight, ranked next-actions, blocked, and milestone rollups all derive from the declared `state_derivation` registry and that a substrate without it reports focus 'state-derivation not configured'.
|
|
56
|
+
- Regenerated `skills/pi-context/SKILL.md` for the `context-current-state` op's gate-aware readiness surface — the tool entry now states that task readiness honors both `task_depends_on_task` dependencies and `task_gated_by_item` gates and that a gate target reaching its complete status releases the gate.
|
|
57
|
+
- Regenerated `skills/pi-context/SKILL.md` + `references/bundled-resources.md` for the milestone block kind (now listed in the installable blocks/schemas inventory) and the corrected `context-roadmap-load`/`render`/`validate` op strings — a phase's milestone is a MILE- block id whose satisfaction reads the milestone's derived `reached` (a pure phase-rollup over the parent phases of its `phase_positioned_in_milestone` edges; phase is the parent, milestone the child), with the validate codes collapsed to `roadmap_milestone_missing`.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- `context-validate-relations` no longer reports `edge_parent_not_in_bins` for a content edge whose structured item parent rides a relation_type that also serves as an edge-materialization lens axis (a lens with `derived_from_field: null`, `bins: []`, e.g. `story-advancers` / `story-advancers-features`). `validateRelations` had applied the lens parent-in-bins contract to every edge matching a lens's relation_type, including the real content edges (task/feature → story), so structurally-correct advancer edges falsely invalidated the substrate. The bins check now applies only when the parent is a `lens_bin` endpoint — a genuine `lens_bin` parent not among the lens bins still errors; an item or legacy-string parent is validated on endpoint kind / existence only.
|
|
61
|
+
- `context-validate` again groups its edge-registry diagnostics by class — every relation_type-registration issue across all edges before every source/target-kind issue across all edges — matching the order surfaced before the `validateEdgeAgainstRegistry` refactor. That refactor's per-edge loop had interleaved the two classes (one edge's registration and kind issues, then the next edge's), changing the printed `issues[]` order; the merged loop now collects then partitions by class so the emission order is class-grouped again. Order-only — the set, count, wording, and severity of the issues are unchanged.
|
|
62
|
+
- Corrected the `phase_positioned_in_milestone` edge direction throughout the milestone phase-rollup: the phase is the edge PARENT and the milestone the CHILD (matching the relation_type registration `source_kinds: phase` / `target_kinds: milestone`, the closure parent=source/child=target convention, and the invariant's `direction: as_child` meaning the milestone is the child). The `currentState` `reached` deriver had selected edges where the milestone was the parent and read the child as the member phase; it now selects edges where the milestone is the child and reads the parent as the member phase, so a milestone derives `reached` only when at least one phase is positioned in it and the parent phase of every such edge buckets to complete. The schema, invariant message, op `description`, and roadmap fixtures that described the phase as the "child" are corrected to the parent-phase framing.
|
|
63
|
+
- Corrected the milestone block kind's `data_path` to `milestone.json` (matching the `data_path === canonical_id + ".json"` convention; `canonical_id` is `milestone`) and renamed the catalog starter block to `samples/blocks/milestone.json` accordingly. It had been authored as `milestones.json`, diverging from the convention.
|
|
64
|
+
|
|
65
|
+
## [0.31.0] - 2026-06-13
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- Internal refactor (usage unchanged): the in-memory migration-registry seeding shared by the read-only update/diagnostic paths is centralized in one `buildFreshRegistryWithChain` helper, and the git-style block-marker sentinel tokens are defined once as named constants the writer and detectors derive from.
|
|
69
|
+
- A repeat live `update` over a block that already carries failure markers retains the existing pending-blocked entry whole (the per-item failures, the migration chain, and the pre-marker restore reference) — the re-run report keeps the per-item diagnostic. The blocked report claims markers were written only for schemas where they actually were; dry-run and no-migration-chain reports carry no write claim.
|
|
70
|
+
|
|
71
|
+
### Documentation
|
|
72
|
+
- The `skill-narrative.md` `<substrate_config>` section and the package README's accept-all/install paragraph now note that a fresh accept-all substrate carries advisory (severity-`warning`) convention-articulation invariants — every decision/feature/task should carry an `item_governed_by_convention` edge to a convention or an `item_acknowledges_missing_convention` edge to a missing-convention gap, surfaced by `context-validate` as a warning that does not block writes.
|
|
73
|
+
- Regenerated `skills/pi-context/SKILL.md` for the `update` op's corrected `dryRun` description (the preview now states the precise per-schema outcome — resync / migrate / block — computed by in-memory forward-migration + re-validation).
|
|
74
|
+
- The `update` op's `promptSnippet` now states that a blocked resync persists a pending-blocked record (target catalog schema + the chain reaching it) resolved via `resolve-blocked` once the block's items are fixed — lockstep with the op's `description`.
|
|
75
|
+
- Regenerated `skills/pi-context/SKILL.md` for the `update` op's extended `promptSnippet` (the pending-blocked record + `resolve-blocked` resolution clause).
|
|
76
|
+
- Regenerated `skills/pi-context/SKILL.md` to surface the new `read-catalog-schema` op.
|
|
77
|
+
- Regenerated `skills/pi-context/SKILL.md` to surface the new `context-install` op.
|
|
78
|
+
- Regenerated `skills/pi-context/SKILL.md` to carry the fresh-substrate advisory convention-articulation note (the catalog's `*-articulates-convention` warning-severity invariants).
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
- The `read-catalog-schema` op's text-surface output is now byte-exact: it emits the catalog `*.schema.json` bytes verbatim (the file's own trailing newline preserved, none appended) via a declared `verbatimText` flag on the op honored by the CLI print path. Previously the print path appended a second newline, doubling the file's trailing `}\n` to `}\n\n` and surfacing a phantom trailing-empty-line under `read-catalog-schema --kind <k> | diff <installed-schema> -` even when content matched.
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
- New `context-install` op (`pi-context context-install`, `authGated`) reflects the install ceremony — the same engine the `/context install` slash command runs (`installContext`, unchanged; no behavior fork). It materializes the schemas + starter blocks declared in `config.json`'s `installed_schemas` / `installed_blocks` from the package samples catalog (`<substrate>/schemas/<name>.schema.json` per declared schema; `<substrate>/<name>.json` per declared block), records the install baseline (`config.installed_from`: catalog source + per-schema fingerprint, for installed-vs-catalog drift detection), and is idempotent on an unchanged substrate. Default skip-if-exists — `--update true` re-syncs existing installed schemas (migration-aware) and replaces empty/absent blocks with the catalog starter, while populated block data is always preserved (the flag maps to the engine's `overwrite` exactly as the slash command's `--update` does). The install ceremony was previously reachable only as the `/context install` command, not as a reflected op; with the op calling `installContext` directly, the parity gate now classifies `installContext` as op-backed (its `INTENTIONALLY_UNEXPOSED_WRITERS` allowlist entry removed).
|
|
85
|
+
- The packaged catalog (`samples/conception.json`) now ships the convention-articulation governance vocabulary — the two relation_types `item_governed_by_convention` (decisions/features/tasks → conventions) and `item_acknowledges_missing_convention` (decisions/features/tasks → framework-gaps), plus three `requires-edge` invariants (`decision-articulates-convention`, `feature-articulates-convention`, `task-articulates-convention`) at severity `warning` — so a fresh accept-all substrate carries advisory convention-articulation. Each invariant `as_parent` expects every decision/feature/task to carry an `item_governed_by_convention` edge to a convention or an `item_acknowledges_missing_convention` edge to a missing-convention gap; at severity warning an unarticulated artifact is reported by `context-validate` without erroring.
|
|
86
|
+
- New `read-catalog-schema` op (`pi-context read-catalog-schema --kind <canonical_id>`) fetches and prints the verbatim bundled catalog `*.schema.json` body for a named block kind — the raw JSON Schema (properties/definitions/$id), not the `read-samples-catalog` projection — so the body is diffable locally against the installed `<substrate>/schemas/<name>.schema.json` without touching node_modules. Read-only and package-intrinsic: it reads the extension's bundled samples catalog, independent of any project substrate, and mutates no installed schema, block, or config. The op returns the schema file's raw bytes as a string (printed as-is on the prose surface; carried as a string on the `--json` envelope). An unknown kind throws (non-zero CLI exit).
|
|
87
|
+
- New exported `readCatalogSchemaText(kindName): { kind, schemaPath, text }` (`@davidorex/pi-context`) reads the verbatim bundled catalog `*.schema.json` body for a named block_kind — the raw file bytes (raw JSON Schema, not the `read-samples-catalog` projection). It resolves the catalog through the shared catalog resolver (the same `samplesRoot` + `canonical_id`→`schema_path` map the installer and the drift detector use), takes no cwd, and reads only the package-bundled samples directory, so the read path touches no installed schema, block, or config. Throws on an unknown kind.
|
|
88
|
+
- The read-only `checkStatus` drift report now carries, per asset, two additive reporting fields surfacing which installed schemas are behind the catalog and by what version gap. A `behind?: boolean` is set true exactly for the catalog-moved states (`catalog-ahead` / `both-diverged`); a `version_delta?: { from?, to?, basis }` carries the baseline → catalog version pair with `basis: "version-bump"` when both versions are present and differ, or `basis: "content-only"` when the versions match or either is undefined (catalog-ahead is a content-hash comparison, so the catalog body can move while the version string is unchanged). The fields are computed after the existing drift-state classification (the classification is unchanged); not-behind assets carry neither field. `renderCheckStatus` annotates each behind asset's name inline with the gap — `name (1.0.0 -> 1.0.1)` for a version bump, `name (1.0.1, content changed)` / `name (content changed)` for a content-only drift — and keeps the per-state grouping + total line.
|
|
89
|
+
- The new reflected op `context-check-status` (`pi-context context-check-status --json`) returns the `checkStatus` drift report as a `{ json }` data op — the read-only front of the check-status → update --dryRun → update sequence; writes nothing. The drift report was previously reachable only as the `/context check-status` command, not as a reflected op.
|
|
90
|
+
- A live `/context update` that blocks a catalog-ahead schema on items failing the catalog schema (`validation-failed`) now inscribes git-style failure markers INTO the block file at the offending items/fields — full-line `<<<<<<< BLOCKED <name> <from> -> <to> <instancePath> [<keyword>]: <message>` / `>>>>>>> target: <name>@<to>` sentinels (envelope-level / unlocatable failures collect in a top-of-file header block). This is the default behavior of a validation-blocked schema (no flag, no mode). The pre-marker block bytes are pinned into the content-addressed object store (a new optional `premarker_hash` on the pending-blocked entry records the byte-exact restore point); the installed schema and `migrations.json` stay byte-unchanged (only the block file carries markers). The line-targeting walker lexes each line skipping JSON string literals, so braces/brackets inside string values do not mis-place a marker. `resolve-blocked` now detects + strips the full-line marker sentinels, re-validates the corrected block against the SAME pinned target, and on pass raw-writes the stripped text to disk before the commit so item `oid`s are preserved across the round-trip (`content_parent` advances only on genuinely changed items); a still-failing block leaves the marker file untouched. A `--dryRun` update writes no markers. The exported `renderBlocked` report now states the markers are in the block file (open it, fix the marked items, then `resolve-blocked`).
|
|
91
|
+
- A live `/context update` that BLOCKS a catalog-ahead schema now persists a pending-blocked record (`<substrateDir>/pending-blocked.json`, schema-versioned) and a new `resolve-blocked` op commits its resolution. Each blocked entry pins the TARGET catalog schema body into the content-addressed object store (`{ name, from?, to?, reason, target_hash, chain, failures?, blocked_at }`) plus the migration chain reaching it; the sidecar is replaced wholesale each live run (an empty set removes the file) and a `--dryRun` update writes neither the sidecar nor the pinned object. The blocked contract is unchanged — the installed schema, the block, and `migrations.json` remain byte-unchanged on a block; the sidecar + pinned object are additive, outside that contract. New `resolve-blocked` op (`pi-context resolve-blocked --schemaName <name>`, `authGated`) commits a block's resolution: it re-validates the corrected block against the SAME pinned target (forward-migrating in memory through the pinned chain when the block lags the target version), and on PASS registers the chain decls not already on disk, writes the target schema (`replace`), advances the migrated block's `schema_version` envelope to the target + persists it (skipping the block write when it has no items), advances the merge base to the target (so a subsequent `update` converges in-sync instead of re-blocking), and clears the pending entry (removing the file when empty); on FAIL it returns the remaining per-item `failures` and writes nothing (the pending record stays intact for a retry). An absent pending entry, or a missing pinned object, throws (non-zero CLI exit). Backed by the new exported `resolveBlocked(cwd, name, ctx?)` (returns `{ schemaName, resolved: false, failures }` or `{ schemaName, resolved: true, registeredMigrations, baseAdvancedTo }`), the `pending-blocked-store` helpers, and `pendingBlockedPathForDir` (`@davidorex/pi-context/context-dir`). The exported `renderBlocked` report now ends with a guidance line stating how to commit a resolution (fix the named items or widen the local schema → `resolve-blocked --schemaName <name> --yes`).
|
|
92
|
+
- `/context update` now carries a per-schema blocked-resync diagnostic under a new `UpdateResult.blockedDetail: Array<{ name; reason; from?; to?; failures? }>` (one entry per name in `blocked`). Each entry records why the catalog-ahead resync refused: `reason: "no-migration-chain"` (no shipped chain reaches the catalog version — carries the installed→catalog version pair) or `reason: "validation-failed"` (the forward-migrated items fail the catalog schema — carries the version pair plus per-item `failures`, each `{ itemId?, instancePath, keyword, message }` naming the failing item id, the field by JSON pointer, and the constraint that failed). `--dryRun` predicts the same `blockedDetail` it would land. The `blocked: string[]` list is unchanged. Backed by the new exported `renderBlocked(blockedDetail)` (a readable per-schema report — header `blocked: <name> (<from> -> <to>)` then the no-chain line or the per-item failure lines) plus the exported `BlockedDetail` / `BlockValidationFailure` interfaces (`@davidorex/pi-context`).
|
|
93
|
+
- New `validate-block-items` op (`pi-context validate-block-items --block <name>`) validates a block's items against the catalog schema version and returns the per-item failures without writing. It resolves the block's catalog block_kind, loads the installed block, forward-migrates its items in memory through the shipped chain when the block lags the catalog version (a fresh registry; never warms the project's cache), and validates against the catalog schema body. Returns `{ block, from?, to?, valid, failures }` (`from` the block's declared version, `to` the catalog version; `failures[]` each `{ itemId?, instancePath, keyword, message }`). Read-only — never overwrites the schema, the block, or `migrations.json`. An unknown block or a missing installed block file throws (non-zero CLI exit). Backed by the new exported `validateBlockItemsAgainstCatalog(cwd, blockName)`.
|
|
94
|
+
- New `context-lens-view` op (`pi-context context-lens-view --lensId <id>`) projects a config-declared lens (`config.lenses[]`) as a binned item-view, reusing `loadLensView`. Without `--bin` it returns a bin→count summary — `{ lens, kind, bins: { <bin>: count }, uncategorized, total }` over every declared bin (including empty bins → 0) plus the uncategorized count and the total item count, serialized whole (always under the read cap). With `--bin <name>` it returns that bin's items paged by `--offset`/`--limit` through the shared `pageArray` (`{ items, total, hasMore }`). Both routes serialize through `structureForRead` so an over-cap per-bin page fails closed (`data: null`, `complete: false`). Serves target, composition, and hand-curated lenses. An unknown lens id (or, with `--bin`, an undeclared bin) throws — surfacing a non-zero CLI exit.
|
|
95
|
+
- Each surfaced op now carries an optional `examples` field — copy-pasteable `pi-context <op> …` invocation strings consumed by the CLI's per-op `--help` (its `EXAMPLES` section and `--help --format json` model). It is help metadata only and is not part of the in-pi tool surface (the registered pi tool still exposes only name/label/description/promptSnippet/parameters).
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
- An addressed-single-node read now returns the WHOLE addressed subtree (capped), not a 50-item page of one incidental array child. `read-schema --path`, `read-config --registry` (and `--registry --id`), `list-tools name=<tool>`, and `read-samples-catalog kind=<kind>` each name ONE node; the op now passes `whole: true` to the addressed `structureForRead`, so an object node carrying an array child serializes every sibling key instead of collapsing to a page of that one array (dropping siblings). The 50KB read cap is retained — applied unconditionally after the whole-skip — so an over-cap addressed node still fails closed (`data: null`, `complete: false`, `truncated: true`); `whole: true` skips paging only, it does not bypass the cap. The whole-config / whole-schema no-arg wrapper reads and every intentional collection-paging read are unchanged.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
- `/context update --dryRun` now predicts the precise per-schema catalog-ahead outcome (`resynced` / `migrated` / `blocked`) by an in-memory forward-migration + re-validation, rather than listing every `catalog-ahead` schema under `resynced`. The dryRun plan buckets each catalog-ahead schema as the outcome a live `--update` would land; a dry-blocked schema reports no would-register migration declarations (`migrationsRegistered` excludes them).
|
|
102
|
+
- Regenerated the generated `SKILL.md` so the surfaced skill doc carries the new `resolve-conflict` op and the caller-reconciles conflict route.
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
- `/context update` now surfaces the migration declarations a version-bump re-sync registers into `migrations.json`, under a new `UpdateResult.migrationsRegistered: Array<{ schema; from; to }>`. When a `catalog-ahead` schema's installed `version` differs from the catalog `version`, the re-sync walks the shipped catalog migration chain and registers each not-already-present declaration; those appended declarations are now reported (each `{ schema, from, to }`). A same-version re-sync, or a `blocked` outcome (whose rollback reverts `migrations.json`), contributes nothing — the live report reflects post-rollback truth. `--dryRun` computes the would-register set READ-ONLY (the catalog chain minus the declarations already on disk) and reports it without writing. `resyncSchema`'s return widens from a bare status string to `{ status; registeredMigrations }`; `/context install`'s schema loop consumes only the status (the migration-decl reporting surface is on `/context update`).
|
|
106
|
+
- `/context install --update`'s empty-block overwrite now skips a block whose on-disk content already equals the catalog starter (JCS-canonical content equality, key-order/whitespace insensitive), reporting it under `skipped` rather than rewriting it. Previously an itemless block equal to the starter was copied over on every `--update` (an mtime-bumping no-op with identical bytes); it is now left byte-identical. An empty block whose content DIFFERS from the starter (e.g. an extra top-level field) is still overwritten and reported `updated`.
|
|
107
|
+
- `/context update` now also propagates catalog-new config-registry entries into the substrate config, additively. After the per-schema drift routing, `update` brings the four keyed-array config registries — `relation_types`, `invariants`, `block_kinds`, `lenses` — current with the packaged catalog: any catalog entry whose identity (`canonical_id` for `relation_types` / `block_kinds`, `id` for `invariants` / `lenses`) is ABSENT from the substrate config is appended, while every user-authored entry (absent from the catalog) and any locally-diverged body of an entry already present are preserved untouched (present entries are never replaced, removed, or body-merged). The added ids are reported under a new `UpdateResult.registryAdditions: { relation_types; invariants; block_kinds; lenses }` (`string[]` each). `--dryRun` computes and reports `registryAdditions` without writing. Backed by the new exported pure helper `mergeCatalogRegistries(existing, catalog): { merged, additions }` (reads each registry's identity field from the internal registry descriptors; deep-clones `existing`; no I/O) and the `RegistryAdditions` interface (`@davidorex/pi-context`). `status_buckets` (a map registry) is out of scope.
|
|
108
|
+
- New `resolve-conflict` op (`pi-context resolve-conflict --schemaName <name> [--schema <reconciled>]`) commits the reconciliation of a merge conflict `update` surfaced, completing the caller-as-reconciler path end-to-end. It writes the reconciled schema body (meta-validated + nested-id-guarded atomic `replace`, when `schema` is supplied; a JSON-string body is parsed like `write-schema`) AND advances the merge base for that schema to the catalog body. Advancing the base is the step a bare `write-schema` lacks: it stamps the catalog body as the install baseline (`config.installed_from.assets[name]`), so the next `update`'s 3-way check sees the schema as `locally-modified` (base === catalog ≠ the reconciled body) and `mergeSchema(base=catalog, ours=R, theirs=catalog)` takes R via the `base === theirs → ours` rule — auto-merging with zero conflicts and preserving the resolution; without it, `update` re-derives the same `both-diverged` conflict on every run. Omitting `schema` treats the current on-disk body as already reconciled and only advances the base. The op is `authGated` (auto-joins `gatedTools`) and returns `{ schemaName, wroteSchema, baseAdvancedTo }`. Backed by the new exported `resolveConflict(cwd, name, schema?, ctx?)` and `stampBaselineFromBody(cwd, name, body, version)` (the stamp mechanics — object-store put + `installed_from.assets[name]` set + config write — factored out of `refreshBaselineForSchema`, which now delegates to it with behavior unchanged).
|
|
109
|
+
- New exports backing the `pi-context update` conflict-surfacing path (consumed by `@davidorex/pi-context-cli`): `refreshBaselineForSchema(cwd, name)` re-stamps the install baseline (`config.installed_from.assets[name]`) from a schema's current on-disk body (idempotent: `true` + stamp-and-write when the freshly-computed `content_hash` differs from the recorded one, `false` no-op when unchanged or the file is absent) — `updateContext`'s post-loop baseline-refresh calls it per brought-current schema (resynced / migrated / auto-merged). `renderConflicts(conflicts)` renders an `UpdateResult["conflicts"]` set as a readable per-schema report and ends with a guidance line stating how the calling agent applies a reconciliation (reconcile the conflicting paths into a schema → `resolve-conflict --schemaName <name> --schema <reconciled>`, which writes the body AND advances the merge base to the catalog); pure, no I/O. The `SchemaConflict` interface is re-exported from the package root (`@davidorex/pi-context`).
|
|
110
|
+
- `/context update` now 3-way merges a locally-edited schema instead of unconditionally refusing it. For a `locally-modified` / `both-diverged` installed schema, `update` reconstructs the merge BASE from the content-addressed body stored under the install baseline's `content_hash` (`getObject`), then key/path-merges that BASE with OURS (the installed file) and THEIRS (the catalog file) via the new pure `mergeSchema(base, ours, theirs)`. The merge is deterministic and canonical-JSON-keyed: plain objects merge per key over the sorted union (a key deleted on one side and unchanged on the other is honored as a deletion); `required` / `enum` / array-valued `type` nodes merge as SETS honoring both adds and removes (`(ours ∩ theirs) ∪ (ours \ base) ∪ (theirs \ base)`, so a user narrowing an enum and the catalog widening it both apply); every other node merges atomically 3-way (base==ours → theirs, base==theirs → ours, ours==theirs → converged, all-three-differ → conflict). A conflict-free merge is written through `writeSchemaCheckedForDir` (meta-validated + nested-id-guarded; `--dryRun` validates without writing) and recorded under a new `UpdateResult.merged: string[]`; an auto-merged body is re-base-stamped and its baseline `content_hash` refreshed in the post-loop step (treated like a resync) so it becomes the next merge base. A merge that surfaces irreconcilable per-path disagreements writes NOTHING and records `{ name, conflicts }` under a new `UpdateResult.conflicts: Array<{ name; conflicts: SchemaConflict[] }>` (each `SchemaConflict` carries the dotted `path` + `base`/`ours`/`theirs` values). When no stamped base body is retrievable, or a parse/merge/validation throws, the schema falls back to the prior refuse-and-report behavior (recorded under `refused`), keeping its drift signal. New exports: `mergeSchema` + the `SchemaConflict` interface (`schema-merge`).
|
|
111
|
+
- `/context install` and `/context update` now base-stamp every installed/resynced SCHEMA body into the content-addressed object store (`<substrateDir>/objects/<content_hash>.json`) at each baseline-write site, keyed by the SAME `content_hash` recorded in `config.installed_from.assets`. The install loop stamps each schema's as-installed body under its install-baseline hash; the live (`!dryRun`) `update` refresh stamps each `resynced`/`migrated` schema's body under its NEW refreshed baseline hash (a `--dryRun` update stamps nothing). Stamping is idempotent (content-addressed — same hash implies same bytes). This makes the recorded merge base — the schema body a baseline hash refers to — durably retrievable via `getObject(substrateDir, content_hash)` rather than only fingerprinted, the precondition for a later three-way installed/baseline/catalog schema merge. Additive: no config-shape or signature change; the object store was previously unwired, so existing substrates gain stamped bodies on their next install/update.
|
|
112
|
+
- New `update` op (`pi-context update`) brings the installed substrate model (schemas) current with the packaged catalog. Per installed schema it consults the read-only drift check and routes by state: a schema the package shipped a newer version of (`catalog-ahead`) is re-synced through the migration-aware path (reported under `resynced` / `migrated`, or `blocked` when no safe migration exists); a schema edited locally (`locally-modified` / `both-diverged`) is REFUSED — never overwritten — and recorded under `refused` for reconciliation; an undecidable / absent schema (`no-baseline` / `missing-catalog` / `missing-installed`) is recorded under `reported`; an already-current (`in-sync`) schema is a no-op (recorded under `inSync`). After a live run the install baseline (`config.installed_from.assets`) refreshes for the schemas actually brought current (`resynced` / `migrated`) so a subsequent `/context check-status` reports them `in-sync`; a `refused` schema keeps its baseline (and its drift signal). Pass `--dryRun` to compute and return the per-schema action plan WITHOUT writing anything. Backed by the new exported `updateContext(cwd, { dryRun? }): UpdateResult`.
|
|
113
|
+
- `/context check-status` previews installed-vs-catalog schema drift WITHOUT writing anything. This standalone read-only command runs `checkStatus(cwd): CheckStatusReport` (and renders it via `renderCheckStatus(report): string`), comparing, per installed schema, the recorded install baseline (`config.installed_from.assets[name].content_hash`) against the catalog's current schema file and the currently-installed schema file, and classifies each as `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`. Each per-asset entry also carries `baseline_version`, `catalog_version`, and `installed_modified`. Every file-hash read is wrapped so a corrupt file degrades to a `missing-*`/diverged classification rather than throwing. The detector performs zero writes (no config write, no copy, no mkdir). A shared `resolveCatalog()` read helper (samples root + `canonical_id`→paths map) is extracted so the installer and the drift detector resolve the catalog identically.
|
|
114
|
+
- `/context install` records an install baseline of the installed SCHEMAS in a new optional `config.installed_from`: the catalog source (`catalog` = pi-context package `name@version`, `catalog_version` = `samples/conception.json` `schema_version`), a baseline timestamp (`at`), and per-schema fingerprints (`assets[canonical_id] = { content_hash, version }`, the content hash over the installed schema file via JCS, plus the schema's own declared `version`). Blocks are user data and are NOT baselined — only the re-syncable model (schemas) is fingerprinted. The baseline is the basis for later installed-vs-catalog drift detection. Idempotent: a re-install on an unchanged substrate (deep-equal `catalog` + `catalog_version` + `assets`) preserves the existing baseline verbatim — including `at` — so the resulting `config.json` is byte-identical; only a content change refreshes `at`. New exported `computeFileContentHash(filePath)` (content hash of a JSON file via canonical JSON). `installed_from` is additive-optional in `config.schema.json` (bumped to `1.6.0`); configs without it remain valid.
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
- Regenerated the generated `SKILL.md` so the surfaced skill doc carries the `update` op description's surfaced-migration-reporting addition.
|
|
118
|
+
- Regenerated the generated `SKILL.md` so the surfaced skill doc carries the `update` op description's config-registry-propagation addition.
|
|
119
|
+
- Regenerated the generated `SKILL.md` so the surfaced skill doc carries the `update` op's refreshed `description`/`promptSnippet` (the drift-aware 3-way-merge behavior).
|
|
120
|
+
- The `update` op's surfaced `description` / `promptSnippet` strings now state the drift-aware 3-way-merge behavior (resync `in-sync`/`catalog-ahead`; 3-way merge `locally-modified`/`both-diverged` preserving non-conflicting edits; conflicts → returned in the op output (under `conflicts`) plus a report for the calling agent to reconcile and commit via `resolve-conflict`; `--dryRun` previews) — they previously described `locally-modified` as unconditionally refused, which no longer matches the implemented merge arm.
|
|
121
|
+
- Reverted the schema-resync identity-stamping escape hatch — `/context install --update`'s migration-aware re-sync now stamps identity unconditionally like every other write, instead of skipping identity stamping when the substrate has no established substrate_id.
|
|
122
|
+
- `/context install --update`'s schema re-sync now restores `migrations.json` to its pre-call bytes when it refuses a schema (the `blocked` path previously left the just-registered migration declarations on disk). The version-bump path registers the shipped migration chain into `migrations.json` before forward-validating the block; when validation refuses, the captured pre-call bytes are written back (or the file removed when it did not exist pre-call), so a `blocked` outcome leaves the schema file, the block file, AND `migrations.json` all byte-unchanged.
|
|
123
|
+
- `/context install --update` now re-syncs installed SCHEMAS through the migration registry instead of a blind `fs.copyFileSync`. A new per-schema `resyncSchema` helper decides: same installed/catalog `version` (description-only drift, or a non-versioned schema) → verbatim overwrite (reported `resynced`); a `version` bump with a shipped catalog migration chain whose forward-migrated block items re-validate against the new schema → overwrite + block re-written via the migration path (reported `migrated`); a `version` bump with NO shipped chain reaching the catalog version, OR migrated items that would fail the new schema → refused, leaving BOTH the installed schema file AND the block file byte-unchanged (reported `blocked`). The version-bump path registers each shipped decl (idempotently) into the substrate's `migrations.json`, overwrites the schema, then forward-migrates + AJV-validates the populated block against the now-installed schema; on any failure it rolls the schema file back to its captured pre-call bytes and never touches the block. `InstallResult` gains `resynced` / `migrated` / `blocked` string arrays (schema `--update` outcomes route into these, not `updated`; fresh installs still report `installed`), and the install summary renders `Re-synced (…)` / `Migrated (…)` / `Blocked (N, no safe migration — left unchanged): …` lines (a `blocked` outcome escalates the notify level to warning). After the loop, the install baseline (`config.installed_from.assets[name]`) refreshes for every `resynced`/`migrated` schema so a subsequent `/context check-status` reports them `in-sync`. Closes the "no safe schema re-sync" gap — a schema version bump can no longer strand already-filed block items under a schema they fail. Preview drift first with `/context check-status`.
|
|
124
|
+
- The read-only schema-drift report is surfaced as the standalone `/context check-status` command, not a flag on `/context install` — the command surface separates the (mutating) install from the (read-only) drift check.
|
|
125
|
+
- `/context install --update` no longer overwrites a populated block — block data files are preserved (reported as `preserved`); only installed schemas are refreshed by `--update`. Empty or absent blocks still receive the catalog starter. The block-existence branch now reads the destination block (via `readBlockForDir` + `forEachBlockArray`) and, when any array carries items, preserves it regardless of `--update`; a block that cannot be read/confirmed-empty is treated as populated (preserved) by default. `InstallResult` gains a `preserved: string[]` field and the install summary renders a `Preserved (…)` line. Closes a data-loss path where `--update` copied an empty catalog starter over a block holding filed items.
|
|
126
|
+
- Genericized originating-project identity out of the installable sample schema descriptions (`samples/schemas/` — issues, work-orders, framework-gaps, layer-plans, research): hardcoded monorepo package names, the project layer-model proper noun, the `in-pi` runtime qualifier, and the internal `.project/` directory are replaced with generic placeholders, so an installed or exported context model no longer carries this repo's identity into the installing project. Description text only; no structural/schema-shape change. The model's own intrinsic vocabulary (closure-table `relations.json`, the L1–L5 layer mapping, substrate terms) is deliberately preserved. A portable strip-list (`analysis/2026-06-05-disinfect-schema-descriptions.md`) lets other projects sanitize their own installed copies.
|
|
127
|
+
|
|
128
|
+
### Removed
|
|
129
|
+
- The `getConflictMergeInputs(cwd, name)` and `installedSchemaOnDiskHash(cwd, name)` exports — both added earlier in this same `[Unreleased]` window and removed before any release — are gone. They existed only to feed the subordinate conflict resolver (the mergetool reconstructed BASE/OURS/THEIRS bodies and snapshotted the installed-schema hash before/after the session). With the resolver retired in favor of returning the conflict set to the calling agent, neither has a remaining consumer. `refreshBaselineForSchema` (now `updateContext`-internal) and `renderConflicts` (the report renderer) remain exported.
|
|
130
|
+
|
|
131
|
+
### Fixed
|
|
132
|
+
- `/context update`'s 3-way merge now records a merged schema's install baseline as the CATALOG body (theirs), not the merged on-disk body. A merge that KEEPS a local divergence — a reconciled conflict (the resolved body `R` ≠ catalog) OR a disjoint auto-merge (a local-only field the catalog lacks) — now reads as `locally-modified` on the next `check-status` (baseline === catalog, installed differs), so the next `update` re-takes the local divergence via the `base === theirs → ours` rule and preserves it. Previously the baseline was stamped from the merged body, so the following `update` read the schema as `catalog-ahead` and RESYNCED it to the catalog — silently clobbering the kept-local divergence on the very next run. The kept-local divergence is now a stable fixed point across repeated updates.
|
|
133
|
+
|
|
7
134
|
## [0.30.0] - 2026-06-04
|
|
8
135
|
|
|
9
136
|
### Added
|
package/README.md
CHANGED
|
@@ -15,9 +15,14 @@ pi install npm:@davidorex/pi-context
|
|
|
15
15
|
```
|
|
16
16
|
/context init <substrate-dir> # create the empty substrate skeleton
|
|
17
17
|
/context install # reconcile <substrate-dir>/ against installed_* lists in config.json
|
|
18
|
+
/context check-status # read-only: report which installed schemas are behind the catalog + the version gap (writes nothing beyond the idempotent config-migration seed)
|
|
19
|
+
/context update [--dryRun] # bring the installed schema model current with the catalog, preserving local edits (3-way merge); --dryRun previews
|
|
20
|
+
/context resolve-conflict --schemaName <name> [--schema <reconciled>] # commit a reconciled merge conflict: write the resolved body + advance the merge base to the catalog
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
`init` (and `switch -c`) is intentionally minimal: it writes the bootstrap pointer, the substrate/schemas dirs, and a minimal schema-valid **skeleton config** (`schema_version` + empty `block_kinds` + `root` + a minted, registered `substrate_id`) — no schemas, no starter blocks (ship-no-defaults). The skeleton is never-clobbered: an idempotent re-init leaves an existing config untouched. From the skeleton there are two onward paths: adopt the packaged conception with `/context accept-all` (overwrites a skeleton config, never a populated one; writes `config.json` from `samples/conception.json` and preserves the skeleton's `substrate_id`), then `/context install`; or build a custom vocabulary directly via `amend-config` / `write-schema` / `append-block-item` (no catalog adoption). The opt-in install ceremony copies the `installed_schemas` / `installed_blocks` declared in `config.json` from the package samples catalog (`samples/blocks/` and `samples/schemas/`); it is idempotent,
|
|
23
|
+
`init` (and `switch -c`) is intentionally minimal: it writes the bootstrap pointer, the substrate/schemas dirs, and a minimal schema-valid **skeleton config** (`schema_version` + empty `block_kinds` + `root` + a minted, registered `substrate_id`) — no schemas, no starter blocks (ship-no-defaults). The skeleton is never-clobbered: an idempotent re-init leaves an existing config untouched. From the skeleton there are two onward paths: adopt the packaged conception with `/context accept-all` (overwrites a skeleton config, never a populated one; writes `config.json` from `samples/conception.json` and preserves the skeleton's `substrate_id`), then `/context install`; or build a custom vocabulary directly via `amend-config` / `write-schema` / `append-block-item` (no catalog adoption). The opt-in install ceremony copies the `installed_schemas` / `installed_blocks` declared in `config.json` from the package samples catalog (`samples/blocks/` and `samples/schemas/`); it is idempotent. Populated block data is never overwritten (reported as `preserved`), while empty or absent blocks get the catalog starter. Install records an **install baseline** (`config.installed_from`) — the catalog source + a per-schema content fingerprint of the installed schemas — for installed-vs-catalog drift detection; the baseline covers schemas only. The adopted conception also 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 articulating neither as a warning (not an error), so the advice surfaces without blocking writes.
|
|
24
|
+
|
|
25
|
+
`/context update` is the drift-aware, customization-preserving path for bringing the installed schema model current with the catalog (it supersedes the former `/context install --update`). Per installed schema it consults the drift classification: an `in-sync` schema is a no-op; a `catalog-ahead` schema re-syncs through the migration-aware path; a `locally-modified` / `both-diverged` schema is reconciled by a deterministic 3-way merge of base (the as-installed schema body in the object store, keyed by the recorded baseline `content_hash`) × ours (the installed schema) × theirs (the catalog schema) — disjoint edits auto-merge so both the user's and the catalog's changes survive (`required` / `enum` / array-valued `type` nodes merge as sets), and a schema with irreconcilable per-path conflicts is left unmodified — the conflict set is returned in the op output (under `conflicts`) alongside a readable report, and the calling agent reconciles it then commits via `/context resolve-conflict` (no subordinate resolver is spawned). `update` also additively propagates catalog-new config-registry entries (`relation_types` / `invariants` / `block_kinds` / `lenses`) absent from the config, preserving every user-authored entry and any locally-diverged body of an existing entry (additive-only; the added ids are reported under `registryAdditions`). A version-bump `catalog-ahead` resync registers the shipped catalog migration chain's declarations into `migrations.json`, reported under `migrationsRegistered` (each `{ schema, from, to }`). `--dryRun` predicts the precise per-schema outcome (resync / migrate / block / merge / conflict) by running the forward-migration and re-validation in memory, alongside the config-registry entries that would be added and the migration declarations that would be registered, writing nothing beyond the idempotent config-migration ceremony seed. Because update applies per-component (a blocked schema rolls back only itself; the registry propagation writes regardless), a run that refuses any schema while applying registry additions or other-schema resyncs reports the partiality under `partialApplication` — `applied`/`notApplied` mirrors of the result channels plus a one-line summary naming what was applied alongside what was refused and why — so a blocked run never reads as a no-op (`--dryRun` reports the predicted partiality in the same shape). The ceremonies that reach identity-stamping writes (update, install, resolve-blocked) establish substrate identity at entry: a config with 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 under `substrateIdEstablished` (live runs only; an established identity is never re-minted). Preview drift first with `/context check-status`, which reports which installed schemas are behind the catalog and by what version gap.
|
|
21
26
|
|
|
22
27
|
## How It Works
|
|
23
28
|
|
|
@@ -60,6 +65,8 @@ Every item in an identity-bearing block carries a three-layer identity (the bloc
|
|
|
60
65
|
|
|
61
66
|
On a stamping write, the content projection is persisted to `<substrate-dir>/objects/<content_hash>.json` — a content-addressed, git-tracked object store (one file per content version). The metadata fields excluded from the hash are the mandatory floor `{id, oid, content_hash, content_parent}` 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 excluded.
|
|
62
67
|
|
|
68
|
+
`/context install` and `/context update` also base-stamp each as-installed / resynced schema body into `objects/<content_hash>.json`, keyed by the schema's baseline `content_hash` recorded in `config.installed_from.assets`. This stamped body is the merge BASE the `/context update` 3-way merge reconstructs for a locally-modified schema.
|
|
69
|
+
|
|
63
70
|
### Cross-substrate: substrate_id + registry
|
|
64
71
|
|
|
65
72
|
Each substrate's `config.json` carries a `substrate_id` (pattern `sub-` + 16 hex), minted once and immutable on disk. A project-root, git-tracked `.pi-context-registry.json` enumerates *all* substrates by `substrate_id` (each mapped to its `dir` and any `aliases`), distinct from the `.pi-context.json` pointer which names only the one active substrate. `resolveRef(cwd, ref)` classifies any endpoint as `active` (resolved in the active substrate), `foreign` (a registered `substrate_id` or `<alias>:<refname>` resolved in another substrate), `dangling` (a registered substrate that lacks the named item), or `unregistered` (a substrate_id/alias the registry does not carry). `validateContext` requires the active `config.substrate_id` to have a matching registry entry, guarding against source-of-truth drift.
|
|
@@ -68,20 +75,22 @@ Each substrate's `config.json` carries a `substrate_id` (pattern `sub-` + 16 hex
|
|
|
68
75
|
|
|
69
76
|
All inter-item relationships are closure-table edges in `<substrate-dir>/relations.json` — `{parent, child, relation_type, ordinal?}` rows. Endpoints are dual-form: a legacy string (a canonical id, a lens bin name, or an `<alias>:<refname>` cross-substrate sentinel), or a structured `{kind:"item", oid, refname?, substrate_id?, content_hash?}` (where `substrate_id` marks a foreign endpoint), or a structured `{kind:"lens_bin", bin}` virtual parent. Embedded nested id-bearing arrays and FK-as-field are forbidden (`validateContext` flags `nested_id_bearing_array`); containment is a membership edge carrying `ordinal`. `promote-item` is the cross-substrate derivation tool (an item promoted into another registered substrate as a new content-addressed item).
|
|
70
77
|
|
|
78
|
+
**Edge orientation.** 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 declared ONCE as `config.relation_types[].role_direction` (`as_parent` = primary at `edge.parent`, `as_child` = primary at `edge.child`; optional — set only for relations with a per-role consumer). The blocked/ready deriver, the milestone rollup, the derived roadmap, and `promote-item`'s lineage edge all read orientation from this field via the `primaryEndpoint`/`counterEndpoint` helpers rather than hardcoding parent/child. Authoring: `append-relation` / `append-relations` accept EITHER raw `--parent`/`--child` OR role-typed `--primary`/`--counter` (mapped to parent/child via `role_direction`); a bare `--parent`/`--child` append of a relation that is BOTH role-bearing and orientation-ambiguous (its source/target kinds overlap) is rejected in favor of `--primary`/`--counter`. Reading: a `context-walk-descendants` / `walk-ancestors` query on a disjoint-kind relation from the wrong endpoint THROWS naming the correct op instead of returning an ambiguous `[]`. `replace-relation` writes raw endpoints verbatim (bypassing the orientation gate) and is the re-orient affordance — run `context-validate` after.
|
|
79
|
+
|
|
71
80
|
### Schema versioning + migrations
|
|
72
81
|
|
|
73
|
-
`<substrate-dir>/migrations.json` is the per-substrate migration registry. A schema `version` bump requires a companion migration declared via `write-schema-migration` — without one, reading or writing an item with an older `schema_version` throws a version mismatch. Migration kinds are `identity` (shape-compatible, no transform) or `declarative-transform` (a spec of rename/set/delete/coerce on dotted paths). The loaded registry walks items forward at the next read/write without a process restart.
|
|
82
|
+
`<substrate-dir>/migrations.json` is the per-substrate migration registry. A schema `version` bump requires a companion migration declared via `write-schema-migration` — without one, reading or writing an item with an older `schema_version` throws a version mismatch. Migration kinds are `identity` (shape-compatible, no transform) or `declarative-transform` (a spec 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 walks items forward at the next read/write without a process restart. Config loading is migration-aware — a config whose `schema_version` lags the bundled schema is walked forward through the `config` migration chain in memory at load (the on-disk file is never rewritten); every substrate-lifecycle ceremony — `/context init`, `/context accept-all`, `/context install`, `/context update`, `/context check-status`, `/context switch` (the existing-target and switch-back forms; the target substrate is seeded right after the pointer flip), `/context resolve-conflict`, and `/context 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. The bundled config schema itself evolves expand-contract: new fields land optional, and a non-additive change (a removed/renamed key, or a new required field on a pre-existing object) is refused by a build-time gate (`scripts/check-config-schema.ts`, pre-commit + CI) unless the same change advances the schema `version` and declares a packaged `config` migration reaching it.
|
|
74
83
|
|
|
75
84
|
**Tools registered:** the tool surface grows with the package — read the generated `skills/pi-context/SKILL.md` for the current set, or call the `list-tools` tool at runtime (in-pi) / `grep pi.registerTool packages/pi-context/src/index.ts` (source). Families:
|
|
76
85
|
|
|
77
86
|
- **Block CRUD** — `read-block`, `write-block`, `read-block-dir`, `append-block-item`, `update-block-item`, `upsert-block-item` (validated find-or-append), `remove-block-item`, and the nested-array variants (`append/update/remove-block-nested-item`).
|
|
78
|
-
- **Item-level read/query** — `read-block-item`, `read-block-page`, `filter-block-items`, `resolve-item-by-id`, `resolve-items-by-id`, `join-blocks`, `find-references`, `walk-ancestors`, `context-walk-descendants`, `context-edges-for-lens`, `gather-execution-context`.
|
|
87
|
+
- **Item-level read/query** — `read-block-item`, `read-block-page`, `filter-block-items`, `resolve-item-by-id`, `resolve-items-by-id`, `join-blocks`, `find-references`, `walk-ancestors`, `context-walk-descendants`, `context-edges-for-lens`, `context-lens-view`, `gather-execution-context`.
|
|
79
88
|
- **Substrate writes** — `append-relation`, `append-relations` (bulk edge append), `remove-relation`, `replace-relation` (single-write atomic re-orient), `amend-config`, `write-schema`, `write-schema-migration`, `rename-canonical-id`.
|
|
80
89
|
- **Content-addressing lifecycle** — `promote-item` (cross-substrate derivation: promote an item into another registered substrate as a new content-addressed item + `item_derived_from_item` lineage edge; `dryRun` previews the destination write without persisting).
|
|
81
|
-
- **Discovery/introspection** — `read-config`, `read-schema`, `read-samples-catalog`, `list-tools`, `context-current-state`, `context-bootstrap-state`.
|
|
90
|
+
- **Discovery/introspection** — `read-config`, `read-schema`, `read-samples-catalog`, `read-catalog-schema`, `list-tools`, `context-current-state`, `context-bootstrap-state`.
|
|
82
91
|
- **Lifecycle/state** — `context-status`, `context-validate`, `context-validate-relations`, `complete-task` (gates on a passing `verification_verifies_item` edge — verification=parent, task=child).
|
|
83
|
-
- **Substrate management** — `context-init`, `context-accept-all`, `context-switch`, `context-list`, `context-archive`.
|
|
84
|
-
- **Roadmap** — `context-roadmap-load`, `context-roadmap-render`, `context-roadmap-validate
|
|
92
|
+
- **Substrate management** — `context-init`, `context-accept-all`, `context-install`, `context-switch`, `context-list`, `context-archive`.
|
|
93
|
+
- **Roadmap** — `context-roadmap-load`, `context-roadmap-render`, `context-roadmap-validate`: the derived roadmap over the `milestone_precedes_milestone` DAG — milestone-block items topo-ordered by the authored precedes edges, with per-milestone phase/task rollups; adjacency comes strictly from the edges, never inferred from order.
|
|
85
94
|
|
|
86
95
|
The relation byRef ops (`append-relation` / `remove-relation` / `replace-relation` / `append-relations`) and `upsert-block-item` accept a `dryRun` flag: it resolves the operation and validates the prospective whole file under the SAME write-path validation, returning the would-decision (`{ ..., dryRun: true }`) while writing nothing. The same shared library path backs both the op (`--dryRun`) and the orchestrator scripts — one implementation, not a script-only preview.
|
|
87
96
|
|
|
@@ -94,19 +103,26 @@ The relation byRef ops (`append-relation` / `remove-relation` / `replace-relatio
|
|
|
94
103
|
**Commands registered:**
|
|
95
104
|
- `/context init <substrate-dir>` — bootstrap pointer + substrate/schemas dirs + a never-clobber skeleton `config.json` (schema-valid, empty of vocabulary, minted `substrate_id`); onward via accept-all OR amend-config / edit
|
|
96
105
|
- `/context accept-all` — adopt `samples/conception.json` as `config.json` (idempotent; never overwrites an existing config)
|
|
97
|
-
- `/context install [--update]` — reconcile the substrate against `installed_schemas` / `installed_blocks` in `config.json` by copying assets from the samples catalog (skip-if-exists by default;
|
|
106
|
+
- `/context install` (CLI: `pi-context context-install [--update]`) — reconcile the substrate against `installed_schemas` / `installed_blocks` in `config.json` by copying assets from the samples catalog (skip-if-exists by default). Populated block data is never overwritten (reported as `preserved`), and empty or absent blocks get the catalog starter. Install also base-stamps each as-installed schema body into the object store and records an **install baseline** in `config.installed_from` — the catalog source (`name@version` + conception `schema_version`) plus a per-schema fingerprint (content hash + declared version) of the installed schemas — used for installed-vs-catalog drift detection. The baseline covers schemas only (blocks are user data); a re-install on an unchanged substrate is idempotent (byte-identical `config.json`, `at` preserved). On a substrate whose config carries no `substrate_id`, install establishes the identity at entry (mint + persist + register) and reports it under `substrateIdEstablished`. (Bringing the installed schema model current is now `/context update`, below.)
|
|
107
|
+
- `/context check-status` (CLI: `pi-context context-check-status`) — read-only: reports drift between the installed schemas and the catalog, reporting each as `in-sync` / `catalog-ahead` / `locally-modified` / `both-diverged` / `no-baseline` / `missing-catalog` / `missing-installed`, and for each schema behind the catalog (`catalog-ahead` / `both-diverged`) it surfaces `behind` and a `version_delta` — the baseline → catalog version pair (a declared version bump) or a content-only basis when the catalog body moved with the version string unchanged; writes nothing beyond the idempotent ceremony seed of the catalog's `config` migration declarations into `migrations.json` (every substrate-lifecycle ceremony seeds before its first config read, so a version-lagging legacy substrate is diagnosable)
|
|
108
|
+
- `read-catalog-schema` (CLI: `pi-context read-catalog-schema --kind <canonical_id>`) — read-only: fetches and prints the verbatim bundled catalog `*.schema.json` body for a named block kind (the raw JSON Schema — `properties` / `definitions` / `$id`, not the `read-samples-catalog` projection), so the body is diffable locally against the installed `<substrate>/schemas/<name>.schema.json` without hunting through `node_modules`. Package-intrinsic (reads the extension's bundled samples catalog, independent of any project substrate); mutates nothing. An unknown kind is an error
|
|
109
|
+
- `validate-block-items` (CLI: `pi-context validate-block-items --block <name>`) — read-only: validates a block's items against the catalog schema version (forward-migrating in memory when the block lags the catalog version) and returns `{ block, from?, to?, valid, failures[] }`, each failure naming the item id, field, and constraint; writes nothing. An unknown block or a missing installed block file is an error
|
|
110
|
+
- `/context update [--dryRun]` (CLI: `pi-context update [--dryRun]`) — bring the installed schema model current with the catalog, routing each schema by its drift state: `in-sync` no-op; `catalog-ahead` resync (migration-aware); `locally-modified` / `both-diverged` reconciled by a deterministic 3-way merge of base (the as-installed body in the object store, keyed by the baseline `content_hash`) × ours (installed schema) × theirs (catalog schema) — disjoint edits auto-merge (`required` / `enum` / array-`type` nodes merge as sets), and a schema with irreconcilable per-path conflicts is left unmodified — the conflict set is returned in the op output (under `conflicts`) alongside a readable report, and the calling agent reconciles it then commits via `/context resolve-conflict` (below; no subordinate resolver is spawned). Update also additively propagates catalog-new config-registry entries (`relation_types` / `invariants` / `block_kinds` / `lenses`) absent from the config, preserving user-authored entries and any locally-diverged body of an existing entry (reported under `registryAdditions`). A version-bump `catalog-ahead` resync registers the shipped catalog migration chain's declarations into `migrations.json`; these are reported under `migrationsRegistered` (each `{ schema, from, to }`). A `catalog-ahead` schema whose resync is refused (`blocked`) carries its diagnostic under `blockedDetail` (one entry per blocked schema): the refusal reason — `no-migration-chain` (no shipped chain reaches the catalog version) vs `validation-failed` (the forward-migrated items fail the catalog schema) vs `write-failed` (a non-validation throw at the write boundary, e.g. the block writer's duplicate-item-id guard; the failures entry carries the thrown message, the items were NOT flagged invalid, and no markers or pending-blocked record are produced) — the installed→catalog version pair, and for a validation failure the per-item failures naming the failing item id, field, and constraint. A live `validation-failed` block also persists a pending-blocked record (`pending-blocked.json`, pinning the target catalog schema + the chain reaching it) consumable by `/context resolve-blocked` (below): fix the named items (or widen the local schema) then run it to commit the resolution so a subsequent `update` converges instead of re-blocking. A `validation-failed` block additionally gets git-style failure markers written INTO the block file at the offending items (full-line `<<<<<<< BLOCKED …` / `>>>>>>> target: …` sentinels), pinning the pre-marker bytes; the schema and `migrations.json` stay byte-unchanged, and `/context resolve-blocked` strips the markers before re-validating. `--dryRun` predicts the precise per-schema outcome (resync / migrate / block / merge / conflict) by running the forward-migration and re-validation in memory, alongside the per-blocked-schema diagnostic detail, the config-registry entries that would be added, and the migration declarations that would be registered, and writes nothing beyond the idempotent config-migration ceremony seed (no markers). Each auto-merged / resynced schema refreshes its baseline so a follow-up `/context check-status` reports it `in-sync`; a schema left as a `conflict` is NOT brought current by update — it stays unmodified until `/context resolve-conflict` commits the reconciliation. A run that refuses any schema while applying registry additions or other-schema resyncs/migrations/merges additionally reports the partiality under `partialApplication` (`applied`/`notApplied` mirrors of the result channels + a one-line summary naming what was applied alongside what was refused and why), so a blocked run never reads as a no-op; `--dryRun` reports the predicted partiality in the same shape. On a substrate whose config carries no `substrate_id`, a LIVE update establishes the identity at entry — mints (the same `mintSubstrateId` init uses), persists it through the sanctioned config write, and registers it in the project registry — before its first identity-stamping write, so a pre-identity substrate heals on the ceremony instead of refusing; the run reports it under `substrateIdEstablished`, an established identity is never re-minted, and `--dryRun` (no stamping writes) establishes nothing
|
|
111
|
+
- `/context resolve-conflict --schemaName <name> [--schema <reconciled>]` (CLI: `pi-context resolve-conflict --schemaName <name> [--schema <reconciled>]`) — commit the reconciliation of a merge conflict `update` surfaced. Run after reconciling a `both-diverged` conflict: it writes the reconciled schema body (meta-validated, atomic) AND advances the merge base for that schema to the catalog body, so the next `update` sees the schema as `locally-modified` and its deterministic merge takes the reconciled body (base === theirs → ours) — auto-merging with zero conflicts and preserving the resolution. Without advancing the base a bare `write-schema` leaves the baseline on the original pre-conflict body and `update` re-derives the same conflict on every run. If `--schema` is omitted the current on-disk schema is treated as already reconciled and only the base is advanced
|
|
112
|
+
- `/context resolve-blocked --schemaName <name>` (CLI: `pi-context resolve-blocked --schemaName <name>`) — commit the resolution of a schema `update` blocked. Run after fixing the block's failing items (or widening the local schema): when the block file carries git-style failure markers (written by `update`), it strips the full-line marker sentinels first, then re-validates the corrected block against the pinned target schema from the pending-blocked record (forward-migrating in memory through the pinned chain when the block lags the target version); on pass it registers the chain declarations, writes the target schema, advances the block's `schema_version` envelope and the merge base to the target (so a subsequent `update` converges in-sync instead of re-blocking), and clears the pending entry; on fail it returns the remaining per-item failures and writes nothing (the pending record and the marker file stay intact for a retry). The commit itself is all-or-nothing: a throw partway through it restores every touched file byte-exact — `migrations.json`, the installed schema, the block file, `config.json`, and the pending record — and reports the failure, never a partial commit. On a substrate whose config carries no `substrate_id`, resolve-blocked establishes the identity at entry (mint + persist + register) before the commit's stamping write and reports it under `substrateIdEstablished`
|
|
98
113
|
- `/context view <lensId>` — render a configured lens (groupByLens projection) into the conversation as markdown
|
|
99
114
|
- `/context lens-curate <lensId>` — surface bin-assignment suggestions for uncategorized items as a follow-up turn; the LLM persists chosen edges via `append-block-item` against `relations.json`
|
|
100
115
|
- `/context status` — derived project state (source metrics, test counts, block summaries, git state)
|
|
101
116
|
- `/context add-work` — extract structured items from conversation into typed blocks
|
|
102
117
|
- `/context validate` — cross-block referential integrity checks
|
|
103
|
-
- `/context roadmap-
|
|
104
|
-
- `/context roadmap-
|
|
105
|
-
- `/context roadmap-validate [ROADMAP-id]` — validate every roadmap (or one) and surface structured issues
|
|
118
|
+
- `/context roadmap-view` — render the derived roadmap as pure-textual markdown (milestone order over authored `milestone_precedes_milestone` edges, per-milestone phase/task rollups, adjacency strictly from edges; no mermaid)
|
|
119
|
+
- `/context roadmap-validate` — validate the derived roadmap and surface structured issues (error/warning/info codes; info never affects status)
|
|
106
120
|
- `/context switch <existing-dir> | -c <new-dir> | -` — flip the bootstrap pointer (to an existing substrate, bootstrap-new-and-flip, or back to `previous_contextDir`)
|
|
107
121
|
- `/context list` — enumerate top-level dirs containing `config.json` (switchable substrates); marks the active one
|
|
108
122
|
- `/context archive <dir>` — move a non-active substrate dir to `archive/<dir>/`
|
|
109
123
|
|
|
124
|
+
**Constrained pi session.** The standalone `@davidorex/pi-context-cli` also provides `pi-context pi-bound` — a process mode that launches a `pi` coding-agent session restricted to the composed pi-extension tool surface. See the [pi-context-cli README](../pi-context-cli/README.md#pi-context-pi-bound--constrained-pi-session).
|
|
125
|
+
|
|
110
126
|
## Source Files
|
|
111
127
|
|
|
112
128
|
| File | Purpose |
|
|
@@ -207,7 +223,7 @@ edgesForLensByName(cwd: string, lensId: string): Edge[] | { error: string }
|
|
|
207
223
|
walkLensDescendants(cwd: string, parentId: string, relationType: string): string[]
|
|
208
224
|
```
|
|
209
225
|
|
|
210
|
-
Pure functions consumed by the `/context view`, `/context lens-curate`, `context-edges-for-lens`, `context-walk-descendants`, and `context-validate-relations` shells in `index.ts`. Tests call them directly without an `ExtensionCommandContext`.
|
|
226
|
+
Pure functions consumed by the `/context view`, `/context lens-curate`, `context-edges-for-lens`, `context-lens-view`, `context-walk-descendants`, and `context-validate-relations` shells in `index.ts`. Tests call them directly without an `ExtensionCommandContext`.
|
|
211
227
|
|
|
212
228
|
### Substrate Path Surface (`src/context-dir.ts`)
|
|
213
229
|
|
package/dist/block-api.d.ts
CHANGED
|
@@ -183,6 +183,19 @@ export declare function readBlock(cwd: string, blockName: string, filter?: {
|
|
|
183
183
|
* for existing callers.
|
|
184
184
|
*/
|
|
185
185
|
export declare function writeTypedFile(filePath: string, schemaPath: string | null, data: unknown, ctx?: DispatchContext, errorLabel?: string): void;
|
|
186
|
+
/**
|
|
187
|
+
* Pure-data recursive walk over every array-valued property at any depth.
|
|
188
|
+
* For a plain (non-array) object `data`, each `[key, value]` pair whose `value`
|
|
189
|
+
* is an array is reported via `visit(key, value)`, then each element of that
|
|
190
|
+
* array that is itself a plain (non-array) object is recursed into (so an
|
|
191
|
+
* item's own nested id-bearing arrays — e.g. `layer-plans` items'
|
|
192
|
+
* `layers` / `migration_phases` — are visited under their item-local key).
|
|
193
|
+
* No schema is consulted; this matches the block shapes the schemas use
|
|
194
|
+
* (top-level arrays of items, items carrying nested arrays of items). Non-array
|
|
195
|
+
* object-valued properties that are NOT array elements are not descended into —
|
|
196
|
+
* block items live in arrays, so only array elements are recursion frontiers.
|
|
197
|
+
*/
|
|
198
|
+
export declare function forEachBlockArray(data: unknown, visit: (arrayKey: string, array: unknown[]) => void): void;
|
|
186
199
|
export declare function appendToTypedFile(filePath: string, schemaPath: string | null, arrayPath: string | null, item: unknown, ctx?: DispatchContext, errorLabel?: string): void;
|
|
187
200
|
/**
|
|
188
201
|
* Validated atomic bulk append-if-absent to an array within a
|
package/dist/block-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-api.d.ts","sourceRoot":"","sources":["../src/block-api.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA8C7D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAKxD,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAI5D,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAGtD,CAAC;AAoPH;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAM9F;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAqBvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB;AA6ID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGnE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,2BAA2B,CAC1C,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0EzB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC9B,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;CAAE,GAClF,OAAO,CA4CT;AAED,wBAAgB,SAAS,CACxB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;CAAE,GAClF,OAAO,CAMT;AAkCD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,IAAI,EAAE,OAAO,EACb,GAAG,CAAC,EAAE,eAAe,EACrB,UAAU,CAAC,EAAE,MAAM,GACjB,IAAI,
|
|
1
|
+
{"version":3,"file":"block-api.d.ts","sourceRoot":"","sources":["../src/block-api.ts"],"names":[],"mappings":"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"}
|
package/dist/block-api.js
CHANGED
|
@@ -804,6 +804,31 @@ export function writeTypedFile(filePath, schemaPath, data, ctx, errorLabel) {
|
|
|
804
804
|
toWrite = stampItem(data, ctx, "create", declared);
|
|
805
805
|
}
|
|
806
806
|
}
|
|
807
|
+
// Parse the supplied schema once — the version stamp below and the
|
|
808
|
+
// post-validation object persistence both consume it.
|
|
809
|
+
const parsedSchema = schemaPath
|
|
810
|
+
? JSON.parse(fs.readFileSync(schemaPath, "utf-8"))
|
|
811
|
+
: null;
|
|
812
|
+
// Envelope schema_version stamp (TASK-073; the FGAP-105 fold-locus lands
|
|
813
|
+
// generically here so EVERY versioned-document write converges — block
|
|
814
|
+
// wrappers, whole-block writes, config, migrations.json all funnel through
|
|
815
|
+
// writeTypedFile). When the schema declares a top-level `schema_version`
|
|
816
|
+
// property AND carries a `version` string, the envelope is stamped to the
|
|
817
|
+
// schema's current version — overwritten, never passed through, so the
|
|
818
|
+
// persisted version is truthful by construction (an incoming stale version
|
|
819
|
+
// has already been walked forward by the caller's migration gate). Self-
|
|
820
|
+
// gating: a schema that does not declare the property (or has no version)
|
|
821
|
+
// leaves the data untouched, so substrates whose installed schemas predate
|
|
822
|
+
// the property keep writing unchanged until `/context update` lands it.
|
|
823
|
+
if (parsedSchema && toWrite && typeof toWrite === "object" && !Array.isArray(toWrite)) {
|
|
824
|
+
const props = parsedSchema.properties;
|
|
825
|
+
if (props &&
|
|
826
|
+
typeof props === "object" &&
|
|
827
|
+
"schema_version" in props &&
|
|
828
|
+
typeof parsedSchema.version === "string") {
|
|
829
|
+
toWrite = { ...toWrite, schema_version: parsedSchema.version };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
807
832
|
// Validate before write (if a schema is supplied)
|
|
808
833
|
if (schemaPath) {
|
|
809
834
|
validateFromFile(schemaPath, toWrite, label);
|
|
@@ -816,9 +841,9 @@ export function writeTypedFile(filePath, schemaPath, data, ctx, errorLabel) {
|
|
|
816
841
|
// Gated on schemaPath: a schema-less write carries no identity items, and
|
|
817
842
|
// the per-item content_hash check protects the non-stamping config/registry/
|
|
818
843
|
// relations/migrations writers (their items carry no content_hash).
|
|
819
|
-
if (schemaPath) {
|
|
844
|
+
if (schemaPath && parsedSchema) {
|
|
820
845
|
const substrateDir = path.dirname(filePath);
|
|
821
|
-
const schema =
|
|
846
|
+
const schema = parsedSchema;
|
|
822
847
|
forEachBlockArray(toWrite, (arrayKey, arr) => {
|
|
823
848
|
for (const item of arr) {
|
|
824
849
|
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
@@ -925,7 +950,7 @@ function assertNoDuplicateIdsInArray(arr, labelForArray) {
|
|
|
925
950
|
* object-valued properties that are NOT array elements are not descended into —
|
|
926
951
|
* block items live in arrays, so only array elements are recursion frontiers.
|
|
927
952
|
*/
|
|
928
|
-
function forEachBlockArray(data, visit) {
|
|
953
|
+
export function forEachBlockArray(data, visit) {
|
|
929
954
|
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
930
955
|
return;
|
|
931
956
|
for (const [key, value] of Object.entries(data)) {
|