@davidorex/pi-context 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +12 -11
- package/dist/block-api.d.ts.map +1 -1
- package/dist/block-api.js +27 -2
- package/dist/block-api.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 +153 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +75 -5
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +71 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +405 -94
- 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 +20 -12
- package/dist/migration-registry-loader.d.ts.map +1 -1
- package/dist/migration-registry-loader.js +46 -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.map +1 -1
- package/dist/ops-registry.js +91 -118
- package/dist/ops-registry.js.map +1 -1
- 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/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 +1 -1
- package/samples/blocks/milestone.json +3 -0
- package/samples/blocks/session-notes.json +1 -0
- package/samples/conception.json +308 -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 +4 -0
- package/samples/schemas/issues.schema.json +28 -0
- package/samples/schemas/layer-plans.schema.json +4 -0
- 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 +4 -0
- 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 +4 -0
- package/schemas/config.schema.json +77 -3
- package/schemas/migrations.schema.json +25 -0
- package/skill-narrative.md +7 -5
- package/skills/pi-context/SKILL.md +44 -49
- package/skills/pi-context/references/bundled-resources.md +5 -1
package/dist/index.js
CHANGED
|
@@ -9,17 +9,18 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { forEachBlockArray, readBlock, readBlockForDir, writeBlockForDir } from "./block-api.js";
|
|
10
10
|
import { computeContentHash, computeFileContentHash } from "./content-hash.js";
|
|
11
11
|
import { adoptConception, installedBlockDestPath, installedSchemaDestPath, loadConfig, loadContext, mergeCatalogRegistries, reconcileActiveSubstrateRegistration, writeConfig, writeSkeletonConfig, } from "./context.js";
|
|
12
|
-
import { BootstrapNotFoundError, flipBootstrapPointer, migrationsPathForDir, resolveContextDir, SCHEMAS_DIR, schemasDir, tryResolveContextDir, writeBootstrapPointer, } from "./context-dir.js";
|
|
12
|
+
import { BootstrapNotFoundError, flipBootstrapPointer, migrationsPathForDir, mintSubstrateId, pendingBlockedPathForDir, resolveContextDir, SCHEMAS_DIR, schemasDir, tryResolveContextDir, writeBootstrapPointer, } from "./context-dir.js";
|
|
13
|
+
import { registerSubstrate } from "./context-registry.js";
|
|
13
14
|
import { contextState, findAppendableBlocks, validateContext } from "./context-sdk.js";
|
|
14
15
|
import { cleanGitEnv } from "./git-env.js";
|
|
15
16
|
import { buildCurationSuggestions, loadLensView, renderLensView } from "./lens-view.js";
|
|
16
17
|
import { buildFreshRegistryWithChain, getProjectMigrationRegistryForDir, invalidateMigrationRegistryForDir, } from "./migration-registry-loader.js";
|
|
17
|
-
import { appendMigrationDeclForDir, loadMigrationsFileForDir } from "./migrations-store.js";
|
|
18
|
+
import { appendMigrationDeclForDir, loadMigrationsFileForDir, seedCatalogConfigMigrationDecls, } from "./migrations-store.js";
|
|
18
19
|
import { getObject, putObject } from "./object-store.js";
|
|
19
20
|
import { registerAll } from "./ops-registry.js";
|
|
20
21
|
import { buildOrientationBlock, skillsDir } from "./orientation.js";
|
|
21
22
|
import { loadPendingBlockedForDir, reconcilePendingBlockedForDir, } from "./pending-blocked-store.js";
|
|
22
|
-
import {
|
|
23
|
+
import { loadRoadmap, renderRoadmap, validateRoadmap } from "./roadmap-plan.js";
|
|
23
24
|
import { mergeSchema } from "./schema-merge.js";
|
|
24
25
|
import { runMigrations } from "./schema-migrations.js";
|
|
25
26
|
import { ValidationError, validate, validateBlockWithMigrationForDir } from "./schema-validator.js";
|
|
@@ -647,8 +648,42 @@ function simulateResyncOutcome(destRoot, samplesRoot, sourceFile, destFile, name
|
|
|
647
648
|
const catalogVersion = readDeclaredVersion(sourceFile);
|
|
648
649
|
const installedVersion = readDeclaredVersion(destFile);
|
|
649
650
|
// Arm 1 — same version (or either unreadable / non-versioned): no transition
|
|
650
|
-
// to migrate across
|
|
651
|
+
// to migrate across. Mirrors the live path 1:1 — a populated block is
|
|
652
|
+
// re-validated in memory against the incoming catalog body, and a failure
|
|
653
|
+
// predicts blocked (validation-failed, per-item failures); otherwise the
|
|
654
|
+
// live path overwrites verbatim → resynced, no decls.
|
|
651
655
|
if (installedVersion === catalogVersion || catalogVersion === undefined || installedVersion === undefined) {
|
|
656
|
+
const sameVersionBlockFile = installedBlockDestPath(destRoot, name);
|
|
657
|
+
if (fs.existsSync(sameVersionBlockFile)) {
|
|
658
|
+
let sameVersionBlockData;
|
|
659
|
+
let sameVersionHasItems = false;
|
|
660
|
+
try {
|
|
661
|
+
sameVersionBlockData = JSON.parse(fs.readFileSync(sameVersionBlockFile, "utf-8"));
|
|
662
|
+
forEachBlockArray(sameVersionBlockData, (_arrayKey, arr) => {
|
|
663
|
+
if (arr.length > 0)
|
|
664
|
+
sameVersionHasItems = true;
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
sameVersionHasItems = true; // unreadable — same safety default as the live path
|
|
669
|
+
}
|
|
670
|
+
if (sameVersionHasItems) {
|
|
671
|
+
try {
|
|
672
|
+
const catalogSchema = JSON.parse(fs.readFileSync(sourceFile, "utf-8"));
|
|
673
|
+
validate(catalogSchema, sameVersionBlockData, name);
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
const failures = err instanceof ValidationError
|
|
677
|
+
? mapValidationFailures(err.errors, sameVersionBlockData)
|
|
678
|
+
: [{ instancePath: "", keyword: "error", message: String(err) }];
|
|
679
|
+
return {
|
|
680
|
+
outcome: "blocked",
|
|
681
|
+
wouldRegister: [],
|
|
682
|
+
detail: { reason: "validation-failed", from: installedVersion, to: catalogVersion, failures },
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
652
687
|
return { outcome: "resynced", wouldRegister: [] };
|
|
653
688
|
}
|
|
654
689
|
// Arm 2 — version bump with NO shipped chain reaching the catalog version: the
|
|
@@ -876,11 +911,67 @@ function resyncSchema(destRoot, samplesRoot, sourceFile, destFile, name) {
|
|
|
876
911
|
return entry;
|
|
877
912
|
};
|
|
878
913
|
// (A) Same version (or either version unreadable / non-versioned): there is no
|
|
879
|
-
// version transition to migrate across
|
|
880
|
-
//
|
|
881
|
-
// version
|
|
914
|
+
// version transition to migrate across. A verbatim overwrite is safe only
|
|
915
|
+
// when the existing block items still conform to the INCOMING catalog body —
|
|
916
|
+
// a same-version change that narrows validity (a dropped property under
|
|
917
|
+
// additionalProperties:false, an added required, a narrowed enum) would
|
|
918
|
+
// otherwise silently invalidate the block. Populated blocks are therefore
|
|
919
|
+
// re-validated against the incoming body IN MEMORY before any write, and a
|
|
920
|
+
// failure refuses the resync (blocked, per-item failures, pending-blocked
|
|
921
|
+
// recorded) — mirroring the version-bump path's validate-or-refuse
|
|
922
|
+
// discipline. Nothing has been written when the refusal fires, so the
|
|
923
|
+
// schema file, block file, and migrations.json are all byte-unchanged. No
|
|
882
924
|
// migration is registered on this arm, so it reports an empty decl list.
|
|
883
925
|
if (installedVersion === catalogVersion || catalogVersion === undefined || installedVersion === undefined) {
|
|
926
|
+
const sameVersionBlockFile = installedBlockDestPath(destRoot, name);
|
|
927
|
+
if (fs.existsSync(sameVersionBlockFile)) {
|
|
928
|
+
let sameVersionBlockData;
|
|
929
|
+
let sameVersionHasItems = false;
|
|
930
|
+
try {
|
|
931
|
+
sameVersionBlockData = JSON.parse(fs.readFileSync(sameVersionBlockFile, "utf-8"));
|
|
932
|
+
forEachBlockArray(sameVersionBlockData, (_arrayKey, arr) => {
|
|
933
|
+
if (arr.length > 0)
|
|
934
|
+
sameVersionHasItems = true;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
// Unreadable block — safety default: treat as populated and route it
|
|
939
|
+
// through the validate path (which throws → blocked).
|
|
940
|
+
sameVersionHasItems = true;
|
|
941
|
+
}
|
|
942
|
+
if (sameVersionHasItems) {
|
|
943
|
+
try {
|
|
944
|
+
const catalogSchema = JSON.parse(fs.readFileSync(sourceFile, "utf-8"));
|
|
945
|
+
validate(catalogSchema, sameVersionBlockData, name);
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
// Classify the refusal (FGAP-115): only an AJV ValidationError is an
|
|
949
|
+
// item-validation failure; any other throw (unreadable catalog body,
|
|
950
|
+
// a write-boundary precondition) is `write-failed` — no pending entry,
|
|
951
|
+
// so the marker/pending pipeline (validation-only consequences) never
|
|
952
|
+
// fires for it.
|
|
953
|
+
if (err instanceof ValidationError) {
|
|
954
|
+
const failures = mapValidationFailures(err.errors, sameVersionBlockData);
|
|
955
|
+
return {
|
|
956
|
+
status: "blocked",
|
|
957
|
+
registeredMigrations: [],
|
|
958
|
+
blockedDetail: { reason: "validation-failed", from: installedVersion, to: catalogVersion, failures },
|
|
959
|
+
pendingEntry: buildPendingEntry("validation-failed", [], failures),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return {
|
|
963
|
+
status: "blocked",
|
|
964
|
+
registeredMigrations: [],
|
|
965
|
+
blockedDetail: {
|
|
966
|
+
reason: "write-failed",
|
|
967
|
+
from: installedVersion,
|
|
968
|
+
to: catalogVersion,
|
|
969
|
+
failures: [{ instancePath: "", keyword: "error", message: String(err) }],
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
884
975
|
fs.copyFileSync(sourceFile, destFile);
|
|
885
976
|
return { status: "resynced", registeredMigrations: [] };
|
|
886
977
|
}
|
|
@@ -910,14 +1001,14 @@ function resyncSchema(destRoot, samplesRoot, sourceFile, destFile, name) {
|
|
|
910
1001
|
// throws on collision). Registration is required BEFORE the validate+migrate
|
|
911
1002
|
// call so the loaded registry carries the forward edge.
|
|
912
1003
|
const existing = loadMigrationsFileForDir(destRoot);
|
|
913
|
-
const present = new Set((existing?.migrations ?? []).map((m) => `${m.schemaName}
|
|
1004
|
+
const present = new Set((existing?.migrations ?? []).map((m) => `${m.schemaName}\u0000${m.fromVersion}`));
|
|
914
1005
|
// Accumulate the decls THIS call actually appends (the not-already-present
|
|
915
1006
|
// subset), reported as the { schema, from, to } shape for the caller to
|
|
916
1007
|
// surface (FGAP-050). On the blocked rollback path below this list is
|
|
917
1008
|
// discarded and [] is returned (post-rollback truth: nothing stuck).
|
|
918
1009
|
const registeredMigrations = [];
|
|
919
1010
|
for (const decl of chain) {
|
|
920
|
-
const key = `${decl.schemaName}
|
|
1011
|
+
const key = `${decl.schemaName}\u0000${decl.fromVersion}`;
|
|
921
1012
|
if (present.has(key))
|
|
922
1013
|
continue;
|
|
923
1014
|
appendMigrationDeclForDir(destRoot, decl);
|
|
@@ -998,14 +1089,30 @@ function resyncSchema(destRoot, samplesRoot, sourceFile, destFile, name) {
|
|
|
998
1089
|
// blocked detail records the validation-failed reason + the version pair + the
|
|
999
1090
|
// per-item failures (a single synthetic failure for a non-AJV throw) so the
|
|
1000
1091
|
// caller surfaces WHY the resync refused (TASK-048).
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1092
|
+
// Classify the refusal (FGAP-115): only an AJV ValidationError means the
|
|
1093
|
+
// migrated items fail the catalog schema. Any other throw — the mandatory
|
|
1094
|
+
// identity stamp refusing a substrate with no substrate_id, an I/O failure —
|
|
1095
|
+
// is `write-failed`: the items were never flagged invalid, so no pending
|
|
1096
|
+
// entry is built and the marker/pending pipeline (validation-only
|
|
1097
|
+
// consequences) never fires.
|
|
1098
|
+
if (err instanceof ValidationError) {
|
|
1099
|
+
const failures = mapValidationFailures(err.errors, blockData);
|
|
1100
|
+
return {
|
|
1101
|
+
status: "blocked",
|
|
1102
|
+
registeredMigrations: [],
|
|
1103
|
+
blockedDetail: { reason: "validation-failed", from: installedVersion, to: catalogVersion, failures },
|
|
1104
|
+
pendingEntry: buildPendingEntry("validation-failed", chain, failures),
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1004
1107
|
return {
|
|
1005
1108
|
status: "blocked",
|
|
1006
1109
|
registeredMigrations: [],
|
|
1007
|
-
blockedDetail: {
|
|
1008
|
-
|
|
1110
|
+
blockedDetail: {
|
|
1111
|
+
reason: "write-failed",
|
|
1112
|
+
from: installedVersion,
|
|
1113
|
+
to: catalogVersion,
|
|
1114
|
+
failures: [{ instancePath: "", keyword: "error", message: String(err) }],
|
|
1115
|
+
},
|
|
1009
1116
|
};
|
|
1010
1117
|
}
|
|
1011
1118
|
}
|
|
@@ -1020,6 +1127,43 @@ function resyncSchema(destRoot, samplesRoot, sourceFile, destFile, name) {
|
|
|
1020
1127
|
* - Sources missing from the samples catalog are reported as "notFound".
|
|
1021
1128
|
* - Empty install lists are not an error — the result is a clean no-op.
|
|
1022
1129
|
*/
|
|
1130
|
+
/**
|
|
1131
|
+
* Ceremony-entry identity establishment (DEC-0020 — FGAP-033). When the
|
|
1132
|
+
* substrate's config.json lacks a `substrate_id`, mint one (the SAME
|
|
1133
|
+
* `mintSubstrateId` init uses), persist it through the sanctioned config write
|
|
1134
|
+
* path, and register it in the project registry (the SAME `registerSubstrate`
|
|
1135
|
+
* call init/accept-all make) — BEFORE the ceremony's first write that stamps
|
|
1136
|
+
* identity, so a pre-identity substrate HEALS on the sanctioned ceremony
|
|
1137
|
+
* instead of refusing at the stamping guard. Called at the entry of every
|
|
1138
|
+
* substrate-lifecycle ceremony that can reach an identity-stamping write
|
|
1139
|
+
* (update, install, resolve-blocked), at the same seam as the config-migration
|
|
1140
|
+
* seeding. Returns the minted id when establishment happened (the ceremony
|
|
1141
|
+
* reports it in its result), undefined when identity was already established
|
|
1142
|
+
* (never re-mints — the on-disk id is immutable) or no config is loadable (the
|
|
1143
|
+
* ceremony's own config handling reports that). This is explicit
|
|
1144
|
+
* ceremony-boundary provisioning, not a lazy mint inside a block write —
|
|
1145
|
+
* `substrateIdForDir`'s loud guard stays load-bearing for any write reaching
|
|
1146
|
+
* it without identity (defense in depth, DEC-0012 preserved).
|
|
1147
|
+
*/
|
|
1148
|
+
function establishSubstrateIdentityAtEntry(cwd, destRoot) {
|
|
1149
|
+
let config;
|
|
1150
|
+
try {
|
|
1151
|
+
config = loadConfig(cwd);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
return undefined; // unloadable config — the ceremony's own path surfaces it
|
|
1155
|
+
}
|
|
1156
|
+
if (!config)
|
|
1157
|
+
return undefined;
|
|
1158
|
+
const existing = config.substrate_id;
|
|
1159
|
+
if (typeof existing === "string" && /^sub-[0-9a-f]{16}$/.test(existing))
|
|
1160
|
+
return undefined;
|
|
1161
|
+
const substrate_id = mintSubstrateId();
|
|
1162
|
+
config.substrate_id = substrate_id;
|
|
1163
|
+
writeConfig(cwd, config);
|
|
1164
|
+
registerSubstrate(cwd, substrate_id, path.relative(cwd, destRoot) || ".", []);
|
|
1165
|
+
return substrate_id;
|
|
1166
|
+
}
|
|
1023
1167
|
export function installContext(cwd, options = {}) {
|
|
1024
1168
|
const result = {
|
|
1025
1169
|
installed: [],
|
|
@@ -1038,6 +1182,16 @@ export function installContext(cwd, options = {}) {
|
|
|
1038
1182
|
"No .pi-context.json bootstrap pointer found. Run /context init <substrate-dir> first to bootstrap the substrate.";
|
|
1039
1183
|
return result;
|
|
1040
1184
|
}
|
|
1185
|
+
// Seed the catalog's `config` migration chain (idempotent) before the config
|
|
1186
|
+
// read below, and before resyncSchema's pre-call migrations.json byte capture
|
|
1187
|
+
// — so a blocked-resync rollback restores to the seeded state, preserving the
|
|
1188
|
+
// seed.
|
|
1189
|
+
seedCatalogConfigMigrationDecls(destRoot);
|
|
1190
|
+
// Ceremony-entry identity establishment (DEC-0020) — before any write that
|
|
1191
|
+
// stamps identity (the --update resync path's writeBlockForDir).
|
|
1192
|
+
const establishedId = establishSubstrateIdentityAtEntry(cwd, destRoot);
|
|
1193
|
+
if (establishedId)
|
|
1194
|
+
result.substrateIdEstablished = establishedId;
|
|
1041
1195
|
const config = loadConfig(cwd);
|
|
1042
1196
|
if (!config) {
|
|
1043
1197
|
result.error = "No config.json found in substrate dir — run /context init <substrate-dir> first.";
|
|
@@ -1228,7 +1382,11 @@ export function installContext(cwd, options = {}) {
|
|
|
1228
1382
|
* re-sync, slice S3). Compares, per installed schema, the S2 install baseline
|
|
1229
1383
|
* against the catalog's current schema file and the currently-installed schema
|
|
1230
1384
|
* file, classifies the drift, and RETURNS the report. Writes NOTHING anywhere —
|
|
1231
|
-
* no config write, no file copy, no mkdir; only reads.
|
|
1385
|
+
* no config write, no file copy, no mkdir; only reads. One designed exception:
|
|
1386
|
+
* like every ceremony entry point it seeds the catalog's `config` migration
|
|
1387
|
+
* chain into `migrations.json` (idempotent) before its first config read — the
|
|
1388
|
+
* heal semantic, consistent with idempotent re-init healing — so a
|
|
1389
|
+
* version-lagging legacy substrate is diagnosable instead of throwing.
|
|
1232
1390
|
*
|
|
1233
1391
|
* For each `config.installed_schemas` entry:
|
|
1234
1392
|
* - baseline = config.installed_from?.assets?.[name]?.content_hash
|
|
@@ -1260,6 +1418,9 @@ export function checkStatus(cwd) {
|
|
|
1260
1418
|
if (destRoot === null) {
|
|
1261
1419
|
return { perAsset, summary: emptySummary() };
|
|
1262
1420
|
}
|
|
1421
|
+
// Ceremony seed (idempotent) before the config read below — the one
|
|
1422
|
+
// sanctioned write in this otherwise pure-read detector (see docstring).
|
|
1423
|
+
seedCatalogConfigMigrationDecls(destRoot);
|
|
1263
1424
|
const config = loadConfig(cwd);
|
|
1264
1425
|
if (!config) {
|
|
1265
1426
|
return { perAsset, summary: emptySummary() };
|
|
@@ -1470,6 +1631,23 @@ export function updateContext(cwd, { dryRun = false } = {}) {
|
|
|
1470
1631
|
"No .pi-context.json bootstrap pointer found. Run /context init <substrate-dir> first to bootstrap the substrate.";
|
|
1471
1632
|
return result;
|
|
1472
1633
|
}
|
|
1634
|
+
// Seed the catalog's `config` migration chain (idempotent) before the config
|
|
1635
|
+
// read below — every ceremony entry point seeds before its first config read,
|
|
1636
|
+
// so a version-lagging legacy substrate heals on update instead of throwing.
|
|
1637
|
+
seedCatalogConfigMigrationDecls(destRoot);
|
|
1638
|
+
// Ceremony-entry identity establishment (DEC-0020) — before any write that
|
|
1639
|
+
// stamps identity (the version-bump resync's writeBlockForDir), so a
|
|
1640
|
+
// pre-identity substrate heals here instead of refusing at the stamping
|
|
1641
|
+
// guard. LIVE runs only: a dryRun performs no stamping write, so there is
|
|
1642
|
+
// nothing to establish ahead of — the preview keeps its writes-nothing
|
|
1643
|
+
// contract (beyond the idempotent ceremony seed), and its predicted
|
|
1644
|
+
// per-schema plan already matches the healed live outcome (the in-memory
|
|
1645
|
+
// simulation never consults the stamping precondition).
|
|
1646
|
+
if (!dryRun) {
|
|
1647
|
+
const establishedId = establishSubstrateIdentityAtEntry(cwd, destRoot);
|
|
1648
|
+
if (establishedId)
|
|
1649
|
+
result.substrateIdEstablished = establishedId;
|
|
1650
|
+
}
|
|
1473
1651
|
const config = loadConfig(cwd);
|
|
1474
1652
|
if (!config) {
|
|
1475
1653
|
result.error = "No config.json found in substrate dir — run /context init <substrate-dir> first.";
|
|
@@ -1774,6 +1952,65 @@ export function updateContext(cwd, { dryRun = false } = {}) {
|
|
|
1774
1952
|
// Catalog read / parse / config-load failure: leave registryAdditions empty
|
|
1775
1953
|
// (its initialized value) and return the schema-update result unchanged.
|
|
1776
1954
|
}
|
|
1955
|
+
// Partial-application legibility (FGAP-076): computed LAST, after every
|
|
1956
|
+
// channel above is final (the registry propagation just ran), so the field is
|
|
1957
|
+
// a pure derivation of the finished result. Populated exactly when the run
|
|
1958
|
+
// both refused something and applied something; a blocked run whose registry
|
|
1959
|
+
// additions (or other-schema resyncs) landed must never read as a no-op.
|
|
1960
|
+
// dryRun runs derive the same shape from the predicted plan.
|
|
1961
|
+
const ra = result.registryAdditions;
|
|
1962
|
+
const registryAdditionCount = ra.relation_types.length + ra.invariants.length + ra.block_kinds.length + ra.lenses.length;
|
|
1963
|
+
const refusedSomething = result.blocked.length > 0 || result.refused.length > 0 || result.conflicts.length > 0;
|
|
1964
|
+
const appliedSomething = result.resynced.length > 0 ||
|
|
1965
|
+
result.migrated.length > 0 ||
|
|
1966
|
+
result.merged.length > 0 ||
|
|
1967
|
+
result.migrationsRegistered.length > 0 ||
|
|
1968
|
+
registryAdditionCount > 0;
|
|
1969
|
+
if (refusedSomething && appliedSomething) {
|
|
1970
|
+
const appliedParts = [];
|
|
1971
|
+
if (registryAdditionCount > 0) {
|
|
1972
|
+
const perRegistry = ["relation_types", "invariants", "block_kinds", "lenses"]
|
|
1973
|
+
.filter((k) => ra[k].length > 0)
|
|
1974
|
+
.map((k) => `${ra[k].length} ${k}`)
|
|
1975
|
+
.join(", ");
|
|
1976
|
+
appliedParts.push(`registry additions (${perRegistry})`);
|
|
1977
|
+
}
|
|
1978
|
+
if (result.resynced.length > 0)
|
|
1979
|
+
appliedParts.push(`resynced: ${result.resynced.join(", ")}`);
|
|
1980
|
+
if (result.migrated.length > 0)
|
|
1981
|
+
appliedParts.push(`migrated: ${result.migrated.join(", ")}`);
|
|
1982
|
+
if (result.merged.length > 0)
|
|
1983
|
+
appliedParts.push(`merged: ${result.merged.join(", ")}`);
|
|
1984
|
+
if (result.migrationsRegistered.length > 0)
|
|
1985
|
+
appliedParts.push(`migration declarations registered: ${result.migrationsRegistered.map((m) => `${m.schema} ${m.from}->${m.to}`).join(", ")}`);
|
|
1986
|
+
const notAppliedParts = [];
|
|
1987
|
+
if (result.blocked.length > 0)
|
|
1988
|
+
notAppliedParts.push(`blocked: ${result.blocked
|
|
1989
|
+
.map((name) => {
|
|
1990
|
+
const detail = result.blockedDetail.find((d) => d.name === name);
|
|
1991
|
+
return detail ? `'${name}' (${detail.reason})` : `'${name}'`;
|
|
1992
|
+
})
|
|
1993
|
+
.join(", ")}`);
|
|
1994
|
+
if (result.refused.length > 0)
|
|
1995
|
+
notAppliedParts.push(`refused (local modifications preserved): ${result.refused.map((n) => `'${n}'`).join(", ")}`);
|
|
1996
|
+
if (result.conflicts.length > 0)
|
|
1997
|
+
notAppliedParts.push(`merge conflicts: ${result.conflicts.map((c) => `'${c.name}'`).join(", ")}`);
|
|
1998
|
+
result.partialApplication = {
|
|
1999
|
+
applied: {
|
|
2000
|
+
resynced: [...result.resynced],
|
|
2001
|
+
migrated: [...result.migrated],
|
|
2002
|
+
merged: [...result.merged],
|
|
2003
|
+
registryAdditions: ra,
|
|
2004
|
+
migrationsRegistered: [...result.migrationsRegistered],
|
|
2005
|
+
},
|
|
2006
|
+
notApplied: {
|
|
2007
|
+
blocked: [...result.blocked],
|
|
2008
|
+
refused: [...result.refused],
|
|
2009
|
+
conflicts: result.conflicts.map((c) => c.name),
|
|
2010
|
+
},
|
|
2011
|
+
summary: `PARTIAL APPLICATION${dryRun ? " (dryRun preview)" : ""}: applied — ${appliedParts.join("; ")}. Not applied — ${notAppliedParts.join("; ")}. The applied portion ${dryRun ? "WOULD change" : "changed"} the substrate even though schemas were refused${dryRun ? " (nothing written in this preview)" : ""}.`,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
1777
2014
|
return result;
|
|
1778
2015
|
}
|
|
1779
2016
|
/**
|
|
@@ -1881,6 +2118,10 @@ export function resolveConflict(cwd, name, schema, ctx) {
|
|
|
1881
2118
|
if (destRoot === null) {
|
|
1882
2119
|
throw new Error(`resolve-conflict: no active substrate resolved for '${cwd}'`);
|
|
1883
2120
|
}
|
|
2121
|
+
// Ceremony seed (idempotent) — update-family class rule: every ceremony
|
|
2122
|
+
// entry point seeds the catalog's `config` migration chain before its first
|
|
2123
|
+
// config read (here reached via stampBaselineFromBody's loadConfig).
|
|
2124
|
+
seedCatalogConfigMigrationDecls(destRoot);
|
|
1884
2125
|
const { samplesRoot, byId } = resolveCatalog();
|
|
1885
2126
|
const kind = byId.get(name);
|
|
1886
2127
|
if (!kind) {
|
|
@@ -1957,6 +2198,15 @@ export function resolveBlocked(cwd, name, ctx) {
|
|
|
1957
2198
|
if (destRoot === null) {
|
|
1958
2199
|
throw new Error(`resolve-blocked: no active substrate resolved for '${cwd}'`);
|
|
1959
2200
|
}
|
|
2201
|
+
// Ceremony seed (idempotent) — update-family class rule: every ceremony
|
|
2202
|
+
// entry point seeds the catalog's `config` migration chain before its first
|
|
2203
|
+
// config read (here reached via stampBaselineFromBody's loadConfig on the
|
|
2204
|
+
// pass path).
|
|
2205
|
+
seedCatalogConfigMigrationDecls(destRoot);
|
|
2206
|
+
// Ceremony-entry identity establishment (DEC-0020) — before the commit's
|
|
2207
|
+
// writeBlockForDir stamping write, so a pre-identity substrate's resolution
|
|
2208
|
+
// heals identity instead of refusing mid-commit.
|
|
2209
|
+
const establishedId = establishSubstrateIdentityAtEntry(cwd, destRoot);
|
|
1960
2210
|
const pending = loadPendingBlockedForDir(destRoot);
|
|
1961
2211
|
const entry = pending?.entries.find((e) => e.name === name);
|
|
1962
2212
|
if (!entry) {
|
|
@@ -2001,6 +2251,7 @@ export function resolveBlocked(cwd, name, ctx) {
|
|
|
2001
2251
|
schemaName: name,
|
|
2002
2252
|
resolved: false,
|
|
2003
2253
|
failures: [{ instancePath: "", keyword: "error", message: String(err) }],
|
|
2254
|
+
...(establishedId ? { substrateIdEstablished: establishedId } : {}),
|
|
2004
2255
|
};
|
|
2005
2256
|
}
|
|
2006
2257
|
const blockVersion = blockData && typeof blockData === "object" && "schema_version" in blockData
|
|
@@ -2034,54 +2285,106 @@ export function resolveBlocked(cwd, name, ctx) {
|
|
|
2034
2285
|
const failures = err instanceof ValidationError
|
|
2035
2286
|
? mapValidationFailures(err.errors, blockData)
|
|
2036
2287
|
: [{ instancePath: "", keyword: "error", message: String(err) }];
|
|
2037
|
-
return {
|
|
2288
|
+
return {
|
|
2289
|
+
schemaName: name,
|
|
2290
|
+
resolved: false,
|
|
2291
|
+
failures,
|
|
2292
|
+
...(establishedId ? { substrateIdEstablished: establishedId } : {}),
|
|
2293
|
+
};
|
|
2038
2294
|
}
|
|
2039
|
-
// PASS — commit the resolution
|
|
2040
|
-
//
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2295
|
+
// PASS — commit the resolution, ALL-OR-NOTHING (FGAP-115). The commit touches
|
|
2296
|
+
// up to five files; a throw partway (e.g. the mandatory identity stamp inside
|
|
2297
|
+
// writeBlockForDir refusing a substrate with no substrate_id) previously
|
|
2298
|
+
// stranded a partial commit — schema advanced, markers stripped, block
|
|
2299
|
+
// unwritten, pending entry stale. Capture the raw pre-commit bytes of every
|
|
2300
|
+
// touched file up front; on any commit-phase throw restore them byte-exact
|
|
2301
|
+
// (per-component byte-exact refuse discipline, DEC-0017/DEC-0018), invalidate
|
|
2302
|
+
// the migration-registry cache warmed by the reverted decl appends, and
|
|
2303
|
+
// return resolved:false carrying the truthful failure. Object-store putObject
|
|
2304
|
+
// entries are content-addressed and harmless to leave behind.
|
|
2305
|
+
const commitFiles = [
|
|
2306
|
+
migrationsPathForDir(destRoot),
|
|
2307
|
+
installedSchemaDestPath(destRoot, name),
|
|
2308
|
+
blockFile,
|
|
2309
|
+
path.join(destRoot, "config.json"),
|
|
2310
|
+
pendingBlockedPathForDir(destRoot),
|
|
2311
|
+
];
|
|
2312
|
+
const preCommitBytes = new Map();
|
|
2313
|
+
for (const f of commitFiles) {
|
|
2314
|
+
preCommitBytes.set(f, fs.existsSync(f) ? fs.readFileSync(f) : null);
|
|
2051
2315
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
fs.writeFileSync(tmpPath, strippedText);
|
|
2066
|
-
fs.renameSync(tmpPath, blockFile);
|
|
2316
|
+
try {
|
|
2317
|
+
// (1) Register the chain decls not already on disk (the resyncSchema dedup),
|
|
2318
|
+
// collecting the registered set.
|
|
2319
|
+
const existing = loadMigrationsFileForDir(destRoot);
|
|
2320
|
+
const present = new Set((existing?.migrations ?? []).map((m) => `${m.schemaName} ${m.fromVersion}`));
|
|
2321
|
+
const registeredMigrations = [];
|
|
2322
|
+
for (const decl of entry.chain) {
|
|
2323
|
+
const key = `${decl.schemaName} ${decl.fromVersion}`;
|
|
2324
|
+
if (present.has(key))
|
|
2325
|
+
continue;
|
|
2326
|
+
appendMigrationDeclForDir(destRoot, decl, ctx);
|
|
2327
|
+
present.add(key);
|
|
2328
|
+
registeredMigrations.push({ schema: decl.schemaName, from: decl.fromVersion, to: decl.toVersion });
|
|
2067
2329
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2330
|
+
// (2) Write the target schema (replace — meta-validated, nested-id-guarded, atomic).
|
|
2331
|
+
writeSchemaCheckedForDir(destRoot, name, targetBody, "replace", ctx);
|
|
2332
|
+
// (3) Advance the migrated block's schema_version envelope to the target + persist
|
|
2333
|
+
// it (skip when the block had no items — schema still written, base still advanced,
|
|
2334
|
+
// mirroring the live no-items handling). Identity stamping re-runs on the write.
|
|
2335
|
+
if (hasItems) {
|
|
2336
|
+
// TASK-052 / FGAP-081 (oid stability): when the on-disk block carried markers,
|
|
2337
|
+
// raw-write the STRIPPED text to the block file (tmp+rename) BEFORE
|
|
2338
|
+
// writeBlockForDir, so the identity-stamp prior-read parses the marker-free
|
|
2339
|
+
// on-disk file and preserves each item's oid (no re-mint; content_parent advances
|
|
2340
|
+
// only on genuinely changed items).
|
|
2341
|
+
if (wasMarked && strippedText !== undefined) {
|
|
2342
|
+
const tmpPath = `${blockFile}.unmark-${process.pid}.tmp`;
|
|
2343
|
+
fs.writeFileSync(tmpPath, strippedText);
|
|
2344
|
+
fs.renameSync(tmpPath, blockFile);
|
|
2345
|
+
}
|
|
2346
|
+
if (migrated &&
|
|
2347
|
+
typeof migrated === "object" &&
|
|
2348
|
+
!Array.isArray(migrated) &&
|
|
2349
|
+
typeof migrated.schema_version === "string" &&
|
|
2350
|
+
typeof targetVersion === "string") {
|
|
2351
|
+
migrated.schema_version = targetVersion;
|
|
2352
|
+
}
|
|
2353
|
+
writeBlockForDir(destRoot, name, migrated);
|
|
2074
2354
|
}
|
|
2075
|
-
|
|
2355
|
+
// (4) Advance the merge base to the target body so a subsequent update converges
|
|
2356
|
+
// (base === catalog) instead of re-deriving drift.
|
|
2357
|
+
const baseAdvancedTo = stampBaselineFromBody(cwd, name, targetBody, targetVersion ?? "");
|
|
2358
|
+
// (5) Clear the resolved entry from pending-blocked.json (remove the file when
|
|
2359
|
+
// it becomes empty — no stale empty sidecar).
|
|
2360
|
+
const remaining = (pending?.entries ?? []).filter((e) => e.name !== name);
|
|
2361
|
+
reconcilePendingBlockedForDir(destRoot, remaining, ctx);
|
|
2362
|
+
return {
|
|
2363
|
+
schemaName: name,
|
|
2364
|
+
resolved: true,
|
|
2365
|
+
registeredMigrations,
|
|
2366
|
+
baseAdvancedTo,
|
|
2367
|
+
...(establishedId ? { substrateIdEstablished: establishedId } : {}),
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
catch (err) {
|
|
2371
|
+
for (const [f, bytes] of preCommitBytes) {
|
|
2372
|
+
if (bytes === null) {
|
|
2373
|
+
if (fs.existsSync(f))
|
|
2374
|
+
fs.unlinkSync(f);
|
|
2375
|
+
}
|
|
2376
|
+
else {
|
|
2377
|
+
fs.writeFileSync(f, bytes);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
invalidateMigrationRegistryForDir(destRoot);
|
|
2381
|
+
return {
|
|
2382
|
+
schemaName: name,
|
|
2383
|
+
resolved: false,
|
|
2384
|
+
failures: [{ instancePath: "", keyword: "error", message: String(err) }],
|
|
2385
|
+
...(establishedId ? { substrateIdEstablished: establishedId } : {}),
|
|
2386
|
+
};
|
|
2076
2387
|
}
|
|
2077
|
-
// (4) Advance the merge base to the target body so a subsequent update converges
|
|
2078
|
-
// (base === catalog) instead of re-deriving drift.
|
|
2079
|
-
const baseAdvancedTo = stampBaselineFromBody(cwd, name, targetBody, targetVersion ?? "");
|
|
2080
|
-
// (5) Clear the resolved entry from pending-blocked.json (remove the file when
|
|
2081
|
-
// it becomes empty — no stale empty sidecar).
|
|
2082
|
-
const remaining = (pending?.entries ?? []).filter((e) => e.name !== name);
|
|
2083
|
-
reconcilePendingBlockedForDir(destRoot, remaining, ctx);
|
|
2084
|
-
return { schemaName: name, resolved: true, registeredMigrations, baseAdvancedTo };
|
|
2085
2388
|
}
|
|
2086
2389
|
/**
|
|
2087
2390
|
* Render an `UpdateResult["conflicts"]` set as a readable conflict report
|
|
@@ -2155,6 +2458,15 @@ export function renderBlocked(blockedDetail) {
|
|
|
2155
2458
|
lines.push(` no migration chain reaches ${to} from ${from}`);
|
|
2156
2459
|
continue;
|
|
2157
2460
|
}
|
|
2461
|
+
if (d.reason === "write-failed") {
|
|
2462
|
+
// FGAP-115: a non-validation refusal at the write boundary. The items were
|
|
2463
|
+
// NOT flagged invalid — do not direct the operator at them.
|
|
2464
|
+
for (const f of d.failures ?? []) {
|
|
2465
|
+
lines.push(` ${f.message}`);
|
|
2466
|
+
}
|
|
2467
|
+
lines.push(" write refused (not an item-validation failure) — the block's items were not flagged invalid; do not edit items. Address the named precondition, then re-run update.");
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2158
2470
|
// validation-failed
|
|
2159
2471
|
for (const f of d.failures ?? []) {
|
|
2160
2472
|
const subject = f.itemId ?? f.instancePath ?? "(item)";
|
|
@@ -2171,7 +2483,12 @@ export function renderBlocked(blockedDetail) {
|
|
|
2171
2483
|
if (anyMarked) {
|
|
2172
2484
|
lines.push("Git-style failure markers were written INTO the block file(s): open each block file, fix the items between the `<<<<<<< BLOCKED …` / `>>>>>>> target: …` markers, then resolve-blocked --schemaName <name> --yes — it strips the markers, re-validates the corrected block against the pinned target, writes the target schema, advances the merge base, and clears the block so update converges.");
|
|
2173
2485
|
}
|
|
2174
|
-
else {
|
|
2486
|
+
else if (blockedDetail.some((d) => d.reason !== "write-failed")) {
|
|
2487
|
+
// The fix-items-then-resolve-blocked flow applies only to entries that
|
|
2488
|
+
// persist a pending record (validation-failed / no-migration-chain). A
|
|
2489
|
+
// report whose entries are ALL write-failed carries its own per-entry
|
|
2490
|
+
// guidance (address the precondition, re-run update) — appending the
|
|
2491
|
+
// item-fixing flow here would contradict it (FGAP-115).
|
|
2175
2492
|
lines.push("No markers were written (preview, or no migration chain to mark against). Resolve a validation-failed block by correcting the offending items in the block file, then resolve-blocked --schemaName <name> --yes — it re-validates the corrected block against the pinned target, writes the target schema, advances the merge base, and clears the block so update converges.");
|
|
2176
2493
|
}
|
|
2177
2494
|
return lines.join("\n");
|
|
@@ -2373,6 +2690,11 @@ export function switchToExisting(cwd, targetDir, writerIdentity) {
|
|
|
2373
2690
|
throw new Error(`/context switch: target dir '${targetDir}' has no config.json at ${targetConfigPath} — refusing to flip the bootstrap pointer to a non-substrate dir. Use '/context switch -c ${targetDir}' to bootstrap a fresh substrate at that dir AND flip the pointer.`);
|
|
2374
2691
|
}
|
|
2375
2692
|
flipBootstrapPointer(cwd, targetDir, writerIdentity);
|
|
2693
|
+
// Seed the TARGET substrate's catalog `config` migration chain (idempotent)
|
|
2694
|
+
// right after the flip — every ceremony entry point seeds before its first
|
|
2695
|
+
// config read, so the first read on the now-active substrate (reconcile's
|
|
2696
|
+
// below, or any later one) cannot throw on a version-lagging legacy config.
|
|
2697
|
+
seedCatalogConfigMigrationDecls(resolveContextDir(cwd));
|
|
2376
2698
|
// Register the now-active substrate's identity if the target carried a
|
|
2377
2699
|
// config-bearing-but-unregistered substrate_id, so the SoT-drift invariant
|
|
2378
2700
|
// does not raise a false substrate_id_unregistered after the flip.
|
|
@@ -2402,6 +2724,11 @@ export function switchToPrevious(cwd, writerIdentity) {
|
|
|
2402
2724
|
throw new Error(`/context switch -: existing pointer at ${bootstrapPath} lacks a string contextDir; refuses to flip an unreadable pointer`);
|
|
2403
2725
|
}
|
|
2404
2726
|
flipBootstrapPointer(cwd, previous, writerIdentity);
|
|
2727
|
+
// Seed the flipped-back-to substrate's catalog `config` migration chain
|
|
2728
|
+
// (idempotent) right after the flip — same switch-family ceremony rule as
|
|
2729
|
+
// switchToExisting: the first config read on the now-active substrate must
|
|
2730
|
+
// not throw on a version-lagging legacy config.
|
|
2731
|
+
seedCatalogConfigMigrationDecls(resolveContextDir(cwd));
|
|
2405
2732
|
// Register the now-active substrate's identity if flipping back landed on a
|
|
2406
2733
|
// config-bearing-but-unregistered substrate_id, so the SoT-drift invariant
|
|
2407
2734
|
// does not raise a false substrate_id_unregistered after the flip. Mirrors
|
|
@@ -2720,49 +3047,30 @@ const extension = (pi) => {
|
|
|
2720
3047
|
});
|
|
2721
3048
|
},
|
|
2722
3049
|
},
|
|
2723
|
-
"roadmap-list": {
|
|
2724
|
-
description: "List every roadmap in <config.root>/roadmap.json with id, title, status, and phase count",
|
|
2725
|
-
handler: (_args, ctx) => {
|
|
2726
|
-
const list = listRoadmaps(ctx.cwd);
|
|
2727
|
-
if (list.length === 0) {
|
|
2728
|
-
ctx.ui.notify("No roadmaps found. Install the roadmap block via the substrate dir's config.json installed_blocks, then author roadmap.json.", "info");
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
|
-
const lines = list.map((r) => `${r.id} [${r.status ?? "(unspecified)"}] ${r.title} (${r.phaseCount} phase${r.phaseCount === 1 ? "" : "s"})`);
|
|
2732
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
2733
|
-
},
|
|
2734
|
-
},
|
|
2735
3050
|
"roadmap-view": {
|
|
2736
|
-
description: "Render
|
|
2737
|
-
handler: (
|
|
2738
|
-
const
|
|
2739
|
-
if (!roadmapId) {
|
|
2740
|
-
ctx.ui.notify("Usage: /context roadmap-view <ROADMAP-id>", "error");
|
|
2741
|
-
return;
|
|
2742
|
-
}
|
|
2743
|
-
const view = loadRoadmap(ctx.cwd, roadmapId);
|
|
3051
|
+
description: "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.",
|
|
3052
|
+
handler: (_args, ctx) => {
|
|
3053
|
+
const view = loadRoadmap(ctx.cwd);
|
|
2744
3054
|
if ("error" in view) {
|
|
2745
3055
|
ctx.ui.notify(view.error, "error");
|
|
2746
3056
|
return;
|
|
2747
3057
|
}
|
|
2748
|
-
|
|
2749
|
-
ctx.ui.notify(renderRoadmap(view, naming), "info");
|
|
3058
|
+
ctx.ui.notify(renderRoadmap(view), "info");
|
|
2750
3059
|
},
|
|
2751
3060
|
},
|
|
2752
3061
|
"roadmap-validate": {
|
|
2753
|
-
description: "Validate
|
|
2754
|
-
handler: (
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
? result.issues.filter((i) => !i.roadmap_id || i.roadmap_id === roadmapId)
|
|
2759
|
-
: result.issues;
|
|
2760
|
-
if (filtered.length === 0) {
|
|
2761
|
-
ctx.ui.notify(`✓ Roadmap validation passed${roadmapId ? ` for ${roadmapId}` : ""}.`, "info");
|
|
3062
|
+
description: "Validate the derived milestone roadmap — surfaces structured issues (error/warning/info codes)",
|
|
3063
|
+
handler: (_args, ctx) => {
|
|
3064
|
+
const result = validateRoadmap(ctx.cwd);
|
|
3065
|
+
if (result.issues.length === 0) {
|
|
3066
|
+
ctx.ui.notify("✓ Roadmap validation passed.", "info");
|
|
2762
3067
|
return;
|
|
2763
3068
|
}
|
|
2764
|
-
const lines =
|
|
2765
|
-
|
|
3069
|
+
const lines = result.issues.map((i) => {
|
|
3070
|
+
const where = `${i.milestone_id ? ` ${i.milestone_id}` : ""}${i.phase_id ? ` ${i.phase_id}` : ""}`;
|
|
3071
|
+
return `✗ [${i.code}]${where}: ${i.message}`;
|
|
3072
|
+
});
|
|
3073
|
+
const level = result.status === "invalid" ? "error" : result.status === "warnings" ? "warning" : "info";
|
|
2766
3074
|
ctx.ui.notify(lines.join("\n"), level);
|
|
2767
3075
|
},
|
|
2768
3076
|
},
|
|
@@ -2855,10 +3163,13 @@ export default extension;
|
|
|
2855
3163
|
// Re-export the config-registry-propagation surface (TASK-038 — FEAT-006 T5) so
|
|
2856
3164
|
// consumers can type `UpdateResult.registryAdditions` and call the pure merge
|
|
2857
3165
|
// helper against the public `@davidorex/pi-context` surface.
|
|
2858
|
-
|
|
3166
|
+
// mergeCatalogRegistries + the FGAP-113 edge-orientation helpers
|
|
3167
|
+
// (counterEndpoint / primaryEndpoint — the single source of truth for reading a
|
|
3168
|
+
// relation's primary/counter endpoint under its config-declared role_direction).
|
|
3169
|
+
export { counterEndpoint, mergeCatalogRegistries, primaryEndpoint } from "./context.js";
|
|
2859
3170
|
export { contextRegistryPath, invalidateRegistry, loadRegistry, REGISTRY_FILE_VERSION, registerSubstrate, resolveAlias, resolveSubstrateDir, writeRegistry, } from "./context-registry.js";
|
|
2860
3171
|
// Re-export for consumers
|
|
2861
3172
|
export { blockStructure, buildIdIndex, CONTEXT_BLOCK_TYPES, completeTask, findAppendableBlocks, resolveItemById, resolveRef, schemaInfo, schemaVocabulary, } from "./context-sdk.js";
|
|
2862
3173
|
export { renameCanonicalId } from "./rename-canonical-id.js";
|
|
2863
|
-
export {
|
|
3174
|
+
export { loadRoadmap, renderRoadmap, resolveStatusVocabulary, rollupPhaseStatus, topoSort, validateRoadmap, } from "./roadmap-plan.js";
|
|
2864
3175
|
//# sourceMappingURL=index.js.map
|