@deftai/directive-core 0.55.1 → 0.56.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/dist/cache/errors.d.ts +4 -1
- package/dist/cache/errors.js +8 -2
- package/dist/cache/fetch.d.ts +50 -0
- package/dist/cache/fetch.js +224 -3
- package/dist/cache/main.js +10 -5
- package/dist/cache/operations.js +6 -4
- package/dist/codebase/map-fresh.js +7 -1
- package/dist/deposit/copy-tree.d.ts +16 -0
- package/dist/deposit/copy-tree.js +58 -0
- package/dist/deposit/resolve-content.d.ts +28 -0
- package/dist/deposit/resolve-content.js +70 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +37 -0
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +8 -0
- package/dist/engine-version.d.ts +3 -0
- package/dist/engine-version.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/init-deposit/gitignore.d.ts +51 -0
- package/dist/init-deposit/gitignore.js +200 -0
- package/dist/init-deposit/index.d.ts +6 -0
- package/dist/init-deposit/index.js +6 -0
- package/dist/init-deposit/init-deposit.d.ts +44 -0
- package/dist/init-deposit/init-deposit.js +197 -0
- package/dist/init-deposit/legacy-detect.d.ts +61 -0
- package/dist/init-deposit/legacy-detect.js +223 -0
- package/dist/init-deposit/migrate.d.ts +88 -0
- package/dist/init-deposit/migrate.js +196 -0
- package/dist/init-deposit/refresh.d.ts +50 -0
- package/dist/init-deposit/refresh.js +292 -0
- package/dist/init-deposit/scaffold.d.ts +41 -0
- package/dist/init-deposit/scaffold.js +753 -0
- package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
- package/dist/legacy-bridge/bridge-drift.js +171 -0
- package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
- package/dist/legacy-bridge/freeze-gate.js +168 -0
- package/dist/legacy-bridge/index.d.ts +4 -0
- package/dist/legacy-bridge/index.js +4 -0
- package/dist/legacy-bridge/sot.d.ts +45 -0
- package/dist/legacy-bridge/sot.js +49 -0
- package/dist/preflight-cache/evaluate.d.ts +11 -0
- package/dist/preflight-cache/evaluate.js +63 -13
- package/dist/release-e2e/constants.d.ts +2 -0
- package/dist/release-e2e/constants.js +2 -0
- package/dist/release-e2e/flags.js +5 -1
- package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
- package/dist/release-e2e/legacy-bridge-leg.js +460 -0
- package/dist/release-e2e/main.d.ts +2 -1
- package/dist/release-e2e/main.js +17 -1
- package/dist/release-e2e/npm-ops.d.ts +3 -1
- package/dist/release-e2e/npm-ops.js +5 -3
- package/dist/release-e2e/types.d.ts +9 -0
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/project-render.js +4 -3
- package/dist/render/roadmap-render.d.ts +4 -2
- package/dist/render/roadmap-render.js +14 -7
- package/dist/swarm/routing-verify.js +2 -2
- package/dist/swarm/subagent-backend.d.ts +16 -0
- package/dist/swarm/subagent-backend.js +14 -0
- package/dist/triage/actions/candidates-log.d.ts +12 -1
- package/dist/triage/actions/candidates-log.js +44 -7
- package/dist/triage/actions/index.d.ts +25 -1
- package/dist/triage/actions/index.js +94 -3
- package/dist/triage/summary/index.d.ts +1 -3
- package/dist/triage/summary/index.js +6 -57
- package/dist/triage/summary/reconcilable.js +4 -33
- package/dist/triage/welcome/default-mode.d.ts +1 -0
- package/dist/triage/welcome/default-mode.js +6 -0
- package/dist/vbrief-validate/project-definition.js +4 -69
- package/dist/vbrief-validate/registry-status.d.ts +20 -0
- package/dist/vbrief-validate/registry-status.js +85 -0
- package/package.json +11 -2
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { GO_BRIDGE_RELEASES_URL } from "../doctor/constants.js";
|
|
5
|
+
import { parseInstallManifest } from "../doctor/manifest.js";
|
|
6
|
+
import { buildLegacyRefusalMessage, detectLegacyLayout, } from "../init-deposit/legacy-detect.js";
|
|
7
|
+
import { isNpmManaged, NPM_MANAGED_SENTINEL_VALUE, runMigrate } from "../init-deposit/migrate.js";
|
|
8
|
+
import { buildInstallManifestText, CANONICAL_INSTALL_ROOT, writeAgentsMd, writeInstallManifest, } from "../init-deposit/scaffold.js";
|
|
9
|
+
// NOTE: the Tier-0 SoT lives at packages/core/src/legacy-bridge/sot.ts and is
|
|
10
|
+
// deliberately NOT edited here (operator-only freeze step). This leg only READS
|
|
11
|
+
// it via lastGoInstaller(). deft:last-go-installer -- the bridge tag is read
|
|
12
|
+
// from the SoT (lastGoInstaller()), never restated as a literal on this line.
|
|
13
|
+
import { lastGoInstaller } from "../legacy-bridge/sot.js";
|
|
14
|
+
import { defaultWhich, spawnText } from "../release/spawn.js";
|
|
15
|
+
import { emit } from "./flags.js";
|
|
16
|
+
/**
|
|
17
|
+
* legacy-bridge-leg.ts -- the opt-in Tier-1 pinned migration e2e leg (#1912).
|
|
18
|
+
*
|
|
19
|
+
* Exercises the two-stage legacy migration end-to-end:
|
|
20
|
+
* - Stage 1 (the frozen Go bridge installer): normalises a LEGACY on-disk
|
|
21
|
+
* layout (pre-v0.27 sentinel AGENTS.md / git-clone / submodule / orphan
|
|
22
|
+
* `.deft/VERSION` / legacy `deft/`-prefixed root) to the canonical-vendored
|
|
23
|
+
* `.deft/core` layout.
|
|
24
|
+
* - Stage 2 (the npm CLI): `directive migrate` stamps the deposit npm-managed,
|
|
25
|
+
* then `directive init`/`update` own the canonical-vendored payload, yielding
|
|
26
|
+
* a valid npm-hybrid deposit (canonical `.deft/core` + AGENTS.md
|
|
27
|
+
* managed-section + a `managed_by: 'npm'` install manifest).
|
|
28
|
+
*
|
|
29
|
+
* SEMANTICS -- pending-pin (the key #1912 contract):
|
|
30
|
+
* The bridge tag is the Tier-0 SoT `lastGoInstaller()` (story G). While it is
|
|
31
|
+
* `null` (unfrozen -- today), the leg cannot download/run a real frozen bridge
|
|
32
|
+
* binary, so it runs every fixture-shape + stage-2 assertion it can WITHOUT the
|
|
33
|
+
* network and SKIPS the real frozen-binary download with a clear
|
|
34
|
+
* `PENDING (SoT unfrozen)` advisory. The moment the operator freezes (pins the
|
|
35
|
+
* SoT), the same leg runs the real pinned bridge -> npm handoff.
|
|
36
|
+
*
|
|
37
|
+
* The leg mirrors the npm-ops leg (npm-ops.ts): a self-contained function
|
|
38
|
+
* returning `[ok, reason]`, invoked from main.ts behind the opt-in
|
|
39
|
+
* `--legacy-bridge` flag (default OFF -- the default `task release:e2e` budget is
|
|
40
|
+
* unaffected). It soft-skips (ok=true) when the npm channel is absent, symmetric
|
|
41
|
+
* to `rehearseNpmPublish`'s npm soft-skip.
|
|
42
|
+
*
|
|
43
|
+
* Refs #1912 (capstone), #1942 (deposit journey), #1941 (migrate), #1669 (Wave 5).
|
|
44
|
+
*/
|
|
45
|
+
const NOOP_IO = { printf: () => { } };
|
|
46
|
+
/** The frozen Go bridge repo (the published release lives under deftai/directive). */
|
|
47
|
+
export const GO_BRIDGE_REPO = "deftai/directive";
|
|
48
|
+
/**
|
|
49
|
+
* The legacy layouts the leg provisions + asserts. Covers the four shapes the
|
|
50
|
+
* detector recognises (legacy-detect.ts): orphan `.deft/VERSION`, legacy
|
|
51
|
+
* `deft/`-prefixed root, git-clone / submodule, and a pre-v0.27 sentinel-only
|
|
52
|
+
* AGENTS.md. Each fixture lands in its own clean throwaway dir.
|
|
53
|
+
*/
|
|
54
|
+
export const LEGACY_FIXTURES = [
|
|
55
|
+
{
|
|
56
|
+
kind: "orphan-deft-version",
|
|
57
|
+
provision: (dir) => {
|
|
58
|
+
mkdirSync(join(dir, ".deft"), { recursive: true });
|
|
59
|
+
writeFileSync(join(dir, ".deft", "VERSION"), "ref: 'v0.1.0'\nsha: 'legacy'\ntag: 'v0.1.0'\n", "utf8");
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
kind: "legacy-deft-prefixed",
|
|
64
|
+
provision: (dir) => {
|
|
65
|
+
mkdirSync(join(dir, "deft"), { recursive: true });
|
|
66
|
+
writeFileSync(join(dir, "deft", "VERSION"), "ref: 'v0.1.0'\n", "utf8");
|
|
67
|
+
writeFileSync(join(dir, "deft", "main.md"), "# Deft (legacy prefixed)\n", "utf8");
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
kind: "git-clone-or-submodule",
|
|
72
|
+
provision: (dir) => {
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
writeFileSync(join(dir, ".gitmodules"), '[submodule "deft"]\n\tpath = .deft\n\turl = https://github.com/deftai/directive.git\n', "utf8");
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: "pre-v0.27-sentinel-agents-md",
|
|
79
|
+
provision: (dir) => {
|
|
80
|
+
mkdirSync(dir, { recursive: true });
|
|
81
|
+
writeFileSync(join(dir, "AGENTS.md"), "# AGENTS\n\n<!-- deft:managed-section -->\nLegacy pre-v0.27 sentinel-only body (no v2/v3 markers).\n", "utf8");
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
/** Provision the named legacy fixture under `dir`. */
|
|
86
|
+
export function provisionLegacyFixture(dir, kind) {
|
|
87
|
+
const spec = LEGACY_FIXTURES.find((f) => f.kind === kind);
|
|
88
|
+
if (!spec) {
|
|
89
|
+
throw new Error(`unknown legacy fixture kind: ${kind}`);
|
|
90
|
+
}
|
|
91
|
+
spec.provision(dir);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Stage-0 fixture-shape assertions: every legacy fixture is classified as legacy
|
|
95
|
+
* with its expected kind, and the npm refuse-preflight surface (story P) would
|
|
96
|
+
* refuse it (we exercise the version-neutral two-step refusal message). This is
|
|
97
|
+
* the boundary that makes the two-stage migration load-bearing: the npm path
|
|
98
|
+
* NEVER migrates a legacy layout -- the frozen Go bridge does (stage 1) first.
|
|
99
|
+
*/
|
|
100
|
+
export function assertLegacyFixturesDetectedAndRefused(workRoot) {
|
|
101
|
+
for (const spec of LEGACY_FIXTURES) {
|
|
102
|
+
const dir = join(workRoot, `legacy-${spec.kind}`);
|
|
103
|
+
provisionLegacyFixture(dir, spec.kind);
|
|
104
|
+
const detection = detectLegacyLayout(dir);
|
|
105
|
+
if (!detection.legacy) {
|
|
106
|
+
return [false, `legacy fixture ${spec.kind} was NOT detected as legacy`];
|
|
107
|
+
}
|
|
108
|
+
if (detection.kind !== spec.kind) {
|
|
109
|
+
return [
|
|
110
|
+
false,
|
|
111
|
+
`legacy fixture ${spec.kind} misclassified: detector returned ${detection.kind}`,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
for (const command of ["init", "update"]) {
|
|
115
|
+
const refusal = buildLegacyRefusalMessage(command, detection);
|
|
116
|
+
if (!refusal.includes("frozen final Go bridge")) {
|
|
117
|
+
return [
|
|
118
|
+
false,
|
|
119
|
+
`npm ${command} refusal for ${spec.kind} did not signpost the frozen Go bridge`,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
true,
|
|
126
|
+
`${LEGACY_FIXTURES.length} legacy fixtures detected + npm init/update refuse-boundary asserted ` +
|
|
127
|
+
`(${LEGACY_FIXTURES.map((f) => f.kind).join(", ")})`,
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The canonical installer manifest fields the frozen Go bridge writes into
|
|
132
|
+
* `.deft/core/VERSION`. Field names + order come from the installer's
|
|
133
|
+
* `buildInstallManifestText` (the TS port of the Go `BuildInstallManifestText`),
|
|
134
|
+
* so this fixture is the same shape a real frozen-bridge run deposits.
|
|
135
|
+
*/
|
|
136
|
+
export const CANONICAL_INSTALLER_MANIFEST_FIELDS = {
|
|
137
|
+
ref: "v0.0.1",
|
|
138
|
+
sha: "content-package",
|
|
139
|
+
tag: "v0.0.1",
|
|
140
|
+
installRoot: CANONICAL_INSTALL_ROOT,
|
|
141
|
+
fetchedAt: "2026-06-24T00:00:00Z",
|
|
142
|
+
fetchedBy: "frozen-go-bridge",
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* The install-manifest field keys a canonical installer VERSION carries, derived
|
|
146
|
+
* from the installer's own `buildInstallManifestText` output so the handshake
|
|
147
|
+
* expectation tracks the real installer field shape rather than restating it.
|
|
148
|
+
* `directive migrate` consumes this VERSION; a drift away from these keys is the
|
|
149
|
+
* handshake break this leg now asserts against.
|
|
150
|
+
*/
|
|
151
|
+
export function installerManifestFieldKeys() {
|
|
152
|
+
return Object.keys(parseInstallManifest(buildInstallManifestText(CANONICAL_INSTALLER_MANIFEST_FIELDS)));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Assert an installer-shaped VERSION is migrate-acceptable: `parseInstallManifest`
|
|
156
|
+
* accepts it AND it carries every canonical installer manifest field
|
|
157
|
+
* ({@link installerManifestFieldKeys}) with a non-empty value. A drifted field
|
|
158
|
+
* shape (missing or renamed key, empty value) returns a handshake mismatch
|
|
159
|
+
* reason so the leg fails loudly rather than letting `directive migrate` stamp a
|
|
160
|
+
* non-installer VERSION npm-managed. This is the stage-2 handshake the #1912
|
|
161
|
+
* freeze hinges on (the installer VERSION the npm CLI accepts as canonical-ready).
|
|
162
|
+
*/
|
|
163
|
+
export function assertInstallerVersionMigrateAcceptable(versionText) {
|
|
164
|
+
const manifest = parseInstallManifest(versionText);
|
|
165
|
+
if (Object.keys(manifest).length === 0) {
|
|
166
|
+
return [false, "handshake mismatch: installer VERSION parsed to an empty manifest"];
|
|
167
|
+
}
|
|
168
|
+
const required = installerManifestFieldKeys();
|
|
169
|
+
const missing = required.filter((key) => {
|
|
170
|
+
const value = manifest[key];
|
|
171
|
+
return value === undefined || value.trim() === "";
|
|
172
|
+
});
|
|
173
|
+
if (missing.length > 0) {
|
|
174
|
+
return [
|
|
175
|
+
false,
|
|
176
|
+
`handshake mismatch: installer VERSION missing migrate-required field(s): ${missing.join(", ")}`,
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return [true, `installer VERSION migrate-acceptable (${required.join(", ")})`];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Provision a canonical-vendored `.deft/core` deposit -- the shape the stage-1
|
|
183
|
+
* bridge produces from a legacy layout. Writes an installer-shaped VERSION (via
|
|
184
|
+
* the installer's own `writeInstallManifest`) unless `versionTextOverride` is
|
|
185
|
+
* supplied, in which case the raw override text is written verbatim so a test can
|
|
186
|
+
* drive a drifted VERSION through the stage-2 handshake. Optionally renders
|
|
187
|
+
* AGENTS.md when the agents-entry template is available (so the npm-hybrid
|
|
188
|
+
* end-state check can assert the managed-section the way `directive init`/`update`
|
|
189
|
+
* do). Returns the VERSION manifest path so the caller can assert the handshake.
|
|
190
|
+
*/
|
|
191
|
+
export function provisionCanonicalVendoredDeposit(projectDir, agentsTemplatePath, versionTextOverride = null) {
|
|
192
|
+
const deftDir = join(projectDir, CANONICAL_INSTALL_ROOT);
|
|
193
|
+
mkdirSync(join(deftDir, "templates"), { recursive: true });
|
|
194
|
+
writeFileSync(join(deftDir, "main.md"), "# Deft\n", "utf8");
|
|
195
|
+
let manifestPath;
|
|
196
|
+
if (versionTextOverride !== null) {
|
|
197
|
+
// deftDir already exists: the `mkdirSync(join(deftDir, "templates"))` above
|
|
198
|
+
// created it as an ancestor (recursive). Just write the override VERSION.
|
|
199
|
+
manifestPath = join(deftDir, "VERSION");
|
|
200
|
+
writeFileSync(manifestPath, versionTextOverride, "utf8");
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
manifestPath = writeInstallManifest(projectDir, deftDir, CANONICAL_INSTALLER_MANIFEST_FIELDS);
|
|
204
|
+
}
|
|
205
|
+
let agentsRendered = false;
|
|
206
|
+
if (agentsTemplatePath !== null && existsSync(agentsTemplatePath)) {
|
|
207
|
+
copyFileSync(agentsTemplatePath, join(deftDir, "templates", "agents-entry.md"));
|
|
208
|
+
writeAgentsMd(projectDir, deftDir, NOOP_IO);
|
|
209
|
+
agentsRendered = true;
|
|
210
|
+
}
|
|
211
|
+
return { deftDir, agentsRendered, manifestPath };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Stage-2 assertion: drive a canonical-vendored deposit through `directive
|
|
215
|
+
* migrate` (and the init/update AGENTS.md render) to a valid npm-hybrid deposit.
|
|
216
|
+
* Asserts migrate stamps the deposit npm-managed, is idempotent (already-hybrid
|
|
217
|
+
* on the second run), and leaves a `managed_by: 'npm'` manifest.
|
|
218
|
+
*/
|
|
219
|
+
export function assertNpmHybridMigration(workRoot, agentsTemplatePath, seams = {}) {
|
|
220
|
+
const projectDir = join(workRoot, "post-bridge");
|
|
221
|
+
mkdirSync(projectDir, { recursive: true });
|
|
222
|
+
const { agentsRendered, manifestPath } = provisionCanonicalVendoredDeposit(projectDir, agentsTemplatePath, seams.versionTextOverride ?? null);
|
|
223
|
+
// Stage-2 handshake: the installer-shaped VERSION must be migrate-acceptable
|
|
224
|
+
// (parseInstallManifest accepts it AND it carries the canonical installer
|
|
225
|
+
// fields) BEFORE `directive migrate` stamps managed_by. migrate itself only
|
|
226
|
+
// appends the sentinel and would happily stamp a drifted VERSION, so asserting
|
|
227
|
+
// the installer field shape here is what makes the freeze handshake end-to-end
|
|
228
|
+
// rather than assumed. Runs regardless of the SoT pin (the pending-pin path
|
|
229
|
+
// still validates the handshake without any binary download).
|
|
230
|
+
const versionText = readFileSync(manifestPath, "utf8");
|
|
231
|
+
const [handshakeOk, handshakeReason] = assertInstallerVersionMigrateAcceptable(versionText);
|
|
232
|
+
if (!handshakeOk) {
|
|
233
|
+
return [false, `npm-hybrid FAIL: ${handshakeReason}`];
|
|
234
|
+
}
|
|
235
|
+
if (agentsRendered) {
|
|
236
|
+
const agents = readFileSync(join(projectDir, "AGENTS.md"), "utf8");
|
|
237
|
+
if (!agents.includes("deft:managed-section")) {
|
|
238
|
+
return [false, "npm-hybrid FAIL: rendered AGENTS.md missing the managed-section"];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const resolveEngine = seams.resolveEngine ?? (() => "npm-channel");
|
|
242
|
+
const nowIso = seams.nowIso;
|
|
243
|
+
const first = runMigrate(projectDir, { resolveEngine, nowIso });
|
|
244
|
+
if (first.outcome !== "migrated") {
|
|
245
|
+
return [
|
|
246
|
+
false,
|
|
247
|
+
`npm-hybrid FAIL: directive migrate expected 'migrated', got '${first.outcome}'`,
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
const second = runMigrate(projectDir, { resolveEngine, nowIso });
|
|
251
|
+
if (second.outcome !== "already-hybrid") {
|
|
252
|
+
return [
|
|
253
|
+
false,
|
|
254
|
+
`npm-hybrid FAIL: directive migrate not idempotent (second run: '${second.outcome}')`,
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
if (first.manifestPath === null) {
|
|
258
|
+
return [false, "npm-hybrid FAIL: migrate reported no manifest path"];
|
|
259
|
+
}
|
|
260
|
+
const manifest = parseInstallManifest(readFileSync(first.manifestPath, "utf8"));
|
|
261
|
+
if (!isNpmManaged(manifest)) {
|
|
262
|
+
return [false, "npm-hybrid FAIL: manifest is not stamped npm-managed after migrate"];
|
|
263
|
+
}
|
|
264
|
+
const agentsNote = agentsRendered ? " + AGENTS.md managed-section" : "";
|
|
265
|
+
return [
|
|
266
|
+
true,
|
|
267
|
+
`npm-hybrid deposit valid (canonical ${CANONICAL_INSTALL_ROOT}${agentsNote}; ` +
|
|
268
|
+
`installer VERSION migrate-acceptable; ` +
|
|
269
|
+
`directive migrate -> managed_by: '${NPM_MANAGED_SENTINEL_VALUE}', idempotent)`,
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
/** Map a Node `process.platform` value to the Go release `GOOS` asset token. */
|
|
273
|
+
function goOsToken(platform) {
|
|
274
|
+
if (platform === "win32")
|
|
275
|
+
return "windows";
|
|
276
|
+
return platform; // "linux" / "darwin" line up with GOOS as-is
|
|
277
|
+
}
|
|
278
|
+
/** Map a Node `process.arch` value to the Go release `GOARCH` asset token. */
|
|
279
|
+
function goArchToken(arch) {
|
|
280
|
+
if (arch === "x64")
|
|
281
|
+
return "amd64";
|
|
282
|
+
return arch; // "arm64" lines up with GOARCH as-is
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Pick the deft-install bridge binary matching the current host platform/arch.
|
|
286
|
+
*
|
|
287
|
+
* Go releases publish one asset per OS/arch (e.g. `deft-install-linux-amd64`,
|
|
288
|
+
* `deft-install-darwin-arm64`, `deft-install-windows-amd64.exe`). Selecting
|
|
289
|
+
* `assets[0]` alphabetically silently picks the wrong binary on a multi-asset
|
|
290
|
+
* release (darwin sorts before linux), producing an exec-format failure on a
|
|
291
|
+
* non-Darwin runner. This resolves the host-matching asset instead and reports
|
|
292
|
+
* a precise outcome when none matches:
|
|
293
|
+
* - single deft-install asset -> use it (single-binary releases, back-compat);
|
|
294
|
+
* - prefer an asset whose name carries BOTH the GOOS and GOARCH tokens;
|
|
295
|
+
* - else fall back to a GOOS-only match (releases that omit the arch suffix);
|
|
296
|
+
* - else `no-platform-match` so the caller fails loudly rather than guessing.
|
|
297
|
+
*/
|
|
298
|
+
export function selectBridgeAsset(fileNames, platform = process.platform, arch = process.arch) {
|
|
299
|
+
const candidates = fileNames.filter((name) => /deft-install/i.test(name));
|
|
300
|
+
if (candidates.length === 0) {
|
|
301
|
+
return { kind: "none" };
|
|
302
|
+
}
|
|
303
|
+
if (candidates.length === 1) {
|
|
304
|
+
return { kind: "ok", name: candidates[0] };
|
|
305
|
+
}
|
|
306
|
+
const os = goOsToken(platform).toLowerCase();
|
|
307
|
+
const cpu = goArchToken(arch).toLowerCase();
|
|
308
|
+
const osArch = candidates.find((name) => {
|
|
309
|
+
const lower = name.toLowerCase();
|
|
310
|
+
return lower.includes(os) && lower.includes(cpu);
|
|
311
|
+
});
|
|
312
|
+
if (osArch !== undefined) {
|
|
313
|
+
return { kind: "ok", name: osArch };
|
|
314
|
+
}
|
|
315
|
+
const osOnly = candidates.find((name) => name.toLowerCase().includes(os));
|
|
316
|
+
if (osOnly !== undefined) {
|
|
317
|
+
return { kind: "ok", name: osOnly };
|
|
318
|
+
}
|
|
319
|
+
return { kind: "no-platform-match", candidates: [...candidates] };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* The frozen-mode real handoff (reached ONLY once the operator pins the SoT):
|
|
323
|
+
* download the frozen Go bridge binary at the pinned tag and run it against a
|
|
324
|
+
* legacy fixture to normalise it to canonical-vendored. Soft-skips when `gh` is
|
|
325
|
+
* absent. Never builds/cuts a release -- it consumes an already-published one.
|
|
326
|
+
*/
|
|
327
|
+
export function downloadAndRunFrozenBridge(pin, fixtureDir, seams = {}) {
|
|
328
|
+
if (seams.runFrozenBridge) {
|
|
329
|
+
return seams.runFrozenBridge(pin, fixtureDir);
|
|
330
|
+
}
|
|
331
|
+
const which = seams.which ?? seams.whichGh ?? defaultWhich;
|
|
332
|
+
const spawn = seams.spawnText ?? spawnText;
|
|
333
|
+
const gh = which("gh");
|
|
334
|
+
if (gh === null) {
|
|
335
|
+
return [true, `SKIP (gh not on PATH; cannot fetch the frozen bridge ${pin})`];
|
|
336
|
+
}
|
|
337
|
+
const mkdtemp = seams.mkdtemp ?? ((prefix) => mkdtempSync(join(tmpdir(), prefix)));
|
|
338
|
+
const rmTemp = seams.rmTemp ?? ((p) => rmSync(p, { recursive: true, force: true }));
|
|
339
|
+
const assetDir = mkdtemp("deft-frozen-bridge-");
|
|
340
|
+
// Clean up the downloaded-binary temp dir on every exit path (mirrors the
|
|
341
|
+
// workRoot try/finally in runLegacyBridgeLeg). Once the SoT is pinned this
|
|
342
|
+
// runs on every --legacy-bridge CI invocation, so a leaked assetDir would
|
|
343
|
+
// accumulate full Go-bridge binaries.
|
|
344
|
+
try {
|
|
345
|
+
const download = spawn(gh, ["release", "download", pin, "--repo", GO_BRIDGE_REPO, "--dir", assetDir, "--clobber"], { env: { ...process.env }, timeoutMs: 300_000 });
|
|
346
|
+
if (download.status !== 0) {
|
|
347
|
+
return [
|
|
348
|
+
false,
|
|
349
|
+
`gh release download ${pin} from ${GO_BRIDGE_REPO} failed: ${download.stderr.trim()}`,
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
const selection = selectBridgeAsset(readdirSync(assetDir));
|
|
353
|
+
if (selection.kind === "none") {
|
|
354
|
+
return [
|
|
355
|
+
false,
|
|
356
|
+
`frozen bridge ${pin} downloaded but no deft-install asset found in ${assetDir} ` +
|
|
357
|
+
`(see ${GO_BRIDGE_RELEASES_URL})`,
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
if (selection.kind === "no-platform-match") {
|
|
361
|
+
return [
|
|
362
|
+
false,
|
|
363
|
+
`frozen bridge ${pin} downloaded but no deft-install asset matches ` +
|
|
364
|
+
`${goOsToken(process.platform)}/${goArchToken(process.arch)} ` +
|
|
365
|
+
`(candidates: ${selection.candidates.join(", ")}; see ${GO_BRIDGE_RELEASES_URL})`,
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
const binaryPath = join(assetDir, selection.name);
|
|
369
|
+
try {
|
|
370
|
+
chmodSync(binaryPath, 0o755);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// best-effort; non-fatal on platforms that ignore the bit
|
|
374
|
+
}
|
|
375
|
+
const run = spawn(binaryPath, ["--yes", "--upgrade", "--repo-root", fixtureDir], {
|
|
376
|
+
env: { ...process.env },
|
|
377
|
+
timeoutMs: 300_000,
|
|
378
|
+
});
|
|
379
|
+
if (run.status !== 0) {
|
|
380
|
+
return [false, `frozen bridge ${pin} run failed (exit ${run.status}): ${run.stderr.trim()}`];
|
|
381
|
+
}
|
|
382
|
+
if (detectLegacyLayout(fixtureDir).legacy) {
|
|
383
|
+
return [false, `frozen bridge ${pin} ran but the fixture is still a legacy layout`];
|
|
384
|
+
}
|
|
385
|
+
return [true, `frozen bridge ${pin} normalised the legacy fixture to canonical-vendored`];
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
rmTemp(assetDir);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Run the pinned legacy -> bridge -> npm-hybrid migration leg.
|
|
393
|
+
*
|
|
394
|
+
* Returns `[ok, reason]` like the other release-e2e legs. Soft-skips (ok=true)
|
|
395
|
+
* when npm is absent. While the SoT is null (pending-pin) it asserts the
|
|
396
|
+
* fixture-shape + stage-2 npm-hybrid checks and SKIPS the real frozen-binary
|
|
397
|
+
* download; once the SoT is pinned it additionally runs the real bridge handoff.
|
|
398
|
+
*/
|
|
399
|
+
export function runLegacyBridgeLeg(projectRoot, seams = {}) {
|
|
400
|
+
const which = seams.which ?? seams.whichGh ?? defaultWhich;
|
|
401
|
+
if (which("npm") === null) {
|
|
402
|
+
return [
|
|
403
|
+
true,
|
|
404
|
+
"SKIP (npm not on PATH; Node-less operator) -- the npm-hybrid stage needs the npm channel",
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
const pin = (seams.lastGoInstaller ?? lastGoInstaller)();
|
|
408
|
+
const frozen = pin !== null;
|
|
409
|
+
const agentsTemplatePath = seams.agentsTemplatePath !== undefined
|
|
410
|
+
? seams.agentsTemplatePath
|
|
411
|
+
: join(projectRoot, "content", "templates", "agents-entry.md");
|
|
412
|
+
const mkdtemp = seams.mkdtemp ?? ((prefix) => mkdtempSync(join(tmpdir(), prefix)));
|
|
413
|
+
const rmTemp = seams.rmTemp ?? ((p) => rmSync(p, { recursive: true, force: true }));
|
|
414
|
+
const workRoot = mkdtemp("deft-legacy-bridge-");
|
|
415
|
+
try {
|
|
416
|
+
let [ok, reason] = assertLegacyFixturesDetectedAndRefused(workRoot);
|
|
417
|
+
emit(" legacy-bridge step: legacy fixtures + refuse-boundary", `${ok ? "OK" : "FAIL"} (${reason})`);
|
|
418
|
+
if (!ok) {
|
|
419
|
+
return [false, `legacy fixtures: ${reason}`];
|
|
420
|
+
}
|
|
421
|
+
// Stage 1: the frozen Go bridge normalises legacy -> canonical-vendored.
|
|
422
|
+
if (frozen && pin !== null) {
|
|
423
|
+
const fixtureDir = join(workRoot, "frozen-fixture");
|
|
424
|
+
provisionLegacyFixture(fixtureDir, "pre-v0.27-sentinel-agents-md");
|
|
425
|
+
const [bridgeOk, bridgeReason] = downloadAndRunFrozenBridge(pin, fixtureDir, seams);
|
|
426
|
+
emit(" legacy-bridge step: frozen bridge handoff", `${bridgeOk ? "OK" : "FAIL"} (${bridgeReason})`);
|
|
427
|
+
if (!bridgeOk) {
|
|
428
|
+
return [false, `frozen bridge: ${bridgeReason}`];
|
|
429
|
+
}
|
|
430
|
+
if (bridgeReason.startsWith("SKIP")) {
|
|
431
|
+
return [true, `frozen pin ${pin}: ${bridgeReason}; stage-1 deferred (gh absent)`];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
emit(" legacy-bridge step: frozen bridge handoff", "PENDING (SoT unfrozen) -- skipped the real frozen-binary download (pin: null)");
|
|
436
|
+
}
|
|
437
|
+
// Stage 2: directive migrate (+ init/update AGENTS.md render) -> npm-hybrid.
|
|
438
|
+
[ok, reason] = assertNpmHybridMigration(workRoot, agentsTemplatePath, seams);
|
|
439
|
+
emit(" legacy-bridge step: stage-2 npm-hybrid", `${ok ? "OK" : "FAIL"} (${reason})`);
|
|
440
|
+
if (!ok) {
|
|
441
|
+
return [false, `npm-hybrid: ${reason}`];
|
|
442
|
+
}
|
|
443
|
+
if (frozen) {
|
|
444
|
+
return [
|
|
445
|
+
true,
|
|
446
|
+
`frozen pin ${pin}: real legacy -> bridge -> npm-hybrid handoff verified ` +
|
|
447
|
+
"(legacy fixtures detected; frozen bridge normalised; stage-2 migrate npm-hybrid)",
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
return [
|
|
451
|
+
true,
|
|
452
|
+
"PENDING (SoT unfrozen): legacy fixtures + npm refuse-boundary + stage-2 npm-hybrid shape " +
|
|
453
|
+
"asserted; skipped the real frozen-binary download (pin: null) until the operator freezes",
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
rmTemp(workRoot);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
//# sourceMappingURL=legacy-bridge-leg.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { type LegacyBridgeLegSeams } from "./legacy-bridge-leg.js";
|
|
1
2
|
import type { E2EConfig, E2ESeams } from "./types.js";
|
|
2
|
-
export declare function runE2e(config: E2EConfig, seams?: E2ESeams): number;
|
|
3
|
+
export declare function runE2e(config: E2EConfig, seams?: E2ESeams & LegacyBridgeLegSeams): number;
|
|
3
4
|
export declare function cmdReleaseE2e(args: readonly string[], seams?: E2ESeams): number;
|
|
4
5
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/release-e2e/main.js
CHANGED
|
@@ -2,6 +2,7 @@ import { resolveProjectRoot } from "../release/paths.js";
|
|
|
2
2
|
import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_E2E_HELP } from "./constants.js";
|
|
3
3
|
import { emit, generateRepoSlug, parseE2EFlags } from "./flags.js";
|
|
4
4
|
import { destroyTempRepo, provisionTempRepo } from "./gh-ops.js";
|
|
5
|
+
import { runLegacyBridgeLeg } from "./legacy-bridge-leg.js";
|
|
5
6
|
import { runRehearsal } from "./rehearsal.js";
|
|
6
7
|
export function runE2e(config, seams = {}) {
|
|
7
8
|
const slug = config.repoSlug ?? generateRepoSlug(seams);
|
|
@@ -13,9 +14,23 @@ export function runE2e(config, seams = {}) {
|
|
|
13
14
|
: "npm publish dry-run (4 packages) -> task release:rollback";
|
|
14
15
|
emit("Rehearsal", "DRYRUN (would run pipeline-mirror rehearsal: clone -> push heads+tags -> task release -> " +
|
|
15
16
|
`verify draft + tag -> ${npmPlan} against temp repo)`);
|
|
17
|
+
if (config.legacyBridge) {
|
|
18
|
+
emit("Legacy-bridge leg", "DRYRUN (would run the pinned legacy -> bridge -> npm-hybrid migration leg; reads the " +
|
|
19
|
+
"Tier-0 SoT lastGoInstaller(), pending-pin while null)");
|
|
20
|
+
}
|
|
16
21
|
emit("Destroy temp repo", `DRYRUN (would run \`gh repo delete ${owner}/${slug} --yes\`)`);
|
|
17
22
|
return EXIT_OK;
|
|
18
23
|
}
|
|
24
|
+
// Opt-in #1912 leg: local + self-contained (no temp GitHub repo), so run it
|
|
25
|
+
// independently of the pipeline-mirror rehearsal and fold its result into rc.
|
|
26
|
+
let legacyBridgeRc = EXIT_OK;
|
|
27
|
+
if (config.legacyBridge) {
|
|
28
|
+
const [ok, reason] = runLegacyBridgeLeg(config.projectRoot, seams);
|
|
29
|
+
emit("Legacy-bridge leg", `${ok ? "OK" : "FAIL"} (${reason})`);
|
|
30
|
+
if (!ok) {
|
|
31
|
+
legacyBridgeRc = EXIT_VIOLATION;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
19
34
|
const [provisionOk, provisionReason] = provisionTempRepo(owner, slug, seams);
|
|
20
35
|
if (!provisionOk) {
|
|
21
36
|
emit(`Provision ${owner}/${slug}`, `FAIL (${provisionReason})`);
|
|
@@ -48,7 +63,7 @@ export function runE2e(config, seams = {}) {
|
|
|
48
63
|
}
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
|
-
return rehearsalRc;
|
|
66
|
+
return rehearsalRc !== EXIT_OK ? rehearsalRc : legacyBridgeRc;
|
|
52
67
|
}
|
|
53
68
|
export function cmdReleaseE2e(args, seams = {}) {
|
|
54
69
|
const flags = parseE2EFlags(args);
|
|
@@ -71,6 +86,7 @@ export function cmdReleaseE2e(args, seams = {}) {
|
|
|
71
86
|
dryRun: flags.dryRun,
|
|
72
87
|
keepRepo: flags.keepRepo,
|
|
73
88
|
skipNpm: flags.skipNpm,
|
|
89
|
+
legacyBridge: flags.legacyBridge,
|
|
74
90
|
repoSlug: null,
|
|
75
91
|
};
|
|
76
92
|
return runE2e(config, seams);
|
|
@@ -21,7 +21,9 @@ export declare function alignNpmPackageVersions(cloneDir: string, version: strin
|
|
|
21
21
|
* 2. Resolve pnpm (or `corepack pnpm`) and `pnpm install --frozen-lockfile`.
|
|
22
22
|
* 3. `pnpm -w run build`; dist/ must exist for the dist-only files allowlist.
|
|
23
23
|
* 4. Align the four package.json versions + resolve the workspace protocol.
|
|
24
|
-
* 5. `npm publish --dry-run --access public` per package
|
|
24
|
+
* 5. `npm publish --dry-run --access public --tag e2e-rehearsal` per package
|
|
25
|
+
* in dependency order (#1925 bypasses implicit-`latest` when the rehearsal
|
|
26
|
+
* sentinel is below the highest published version).
|
|
25
27
|
*
|
|
26
28
|
* Returns [ok, reason] like verifyDraftRelease / verifyTag.
|
|
27
29
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { defaultWhich, spawnText } from "../release/spawn.js";
|
|
4
|
-
import { NPM_BUILD_TIMEOUT_SECONDS, NPM_INSTALL_TIMEOUT_SECONDS, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, NPM_PUBLISH_PACKAGES, } from "./constants.js";
|
|
4
|
+
import { NPM_BUILD_TIMEOUT_SECONDS, NPM_E2E_REHEARSAL_TAG, NPM_INSTALL_TIMEOUT_SECONDS, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, NPM_PUBLISH_PACKAGES, } from "./constants.js";
|
|
5
5
|
/**
|
|
6
6
|
* npm publish dry-run rehearsal (#1910) -- the TS port of the
|
|
7
7
|
* scripts/release_e2e.py helpers of the same name. Mirrors
|
|
@@ -102,7 +102,9 @@ export function alignNpmPackageVersions(cloneDir, version) {
|
|
|
102
102
|
* 2. Resolve pnpm (or `corepack pnpm`) and `pnpm install --frozen-lockfile`.
|
|
103
103
|
* 3. `pnpm -w run build`; dist/ must exist for the dist-only files allowlist.
|
|
104
104
|
* 4. Align the four package.json versions + resolve the workspace protocol.
|
|
105
|
-
* 5. `npm publish --dry-run --access public` per package
|
|
105
|
+
* 5. `npm publish --dry-run --access public --tag e2e-rehearsal` per package
|
|
106
|
+
* in dependency order (#1925 bypasses implicit-`latest` when the rehearsal
|
|
107
|
+
* sentinel is below the highest published version).
|
|
106
108
|
*
|
|
107
109
|
* Returns [ok, reason] like verifyDraftRelease / verifyTag.
|
|
108
110
|
*/
|
|
@@ -135,7 +137,7 @@ export function rehearseNpmPublish(cloneDir, version, seams = {}) {
|
|
|
135
137
|
}
|
|
136
138
|
for (const pkg of NPM_PUBLISH_PACKAGES) {
|
|
137
139
|
const pkgDir = join(cloneDir, "packages", pkg);
|
|
138
|
-
[ok, reason] = runNpmStep([npmPath, "publish", "--dry-run", "--access", "public"], pkgDir, env, `npm publish --dry-run packages/${pkg}`, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, seams);
|
|
140
|
+
[ok, reason] = runNpmStep([npmPath, "publish", "--dry-run", "--access", "public", "--tag", NPM_E2E_REHEARSAL_TAG], pkgDir, env, `npm publish --dry-run --tag ${NPM_E2E_REHEARSAL_TAG} packages/${pkg}`, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, seams);
|
|
139
141
|
if (!ok) {
|
|
140
142
|
return [false, reason];
|
|
141
143
|
}
|
|
@@ -6,6 +6,13 @@ export interface E2EConfig {
|
|
|
6
6
|
keepRepo: boolean;
|
|
7
7
|
/** When true, skip the npm publish dry-run rehearsal step (#1910). */
|
|
8
8
|
skipNpm: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* When true, run the opt-in pinned legacy->bridge->npm-hybrid migration leg
|
|
11
|
+
* (#1912). Default OFF (optional) so the field is absent on existing callers
|
|
12
|
+
* and the default `task release:e2e` budget is unaffected; mirrors `skipNpm`
|
|
13
|
+
* as an opt-in knob rather than a default-on step.
|
|
14
|
+
*/
|
|
15
|
+
legacyBridge?: boolean;
|
|
9
16
|
/** Optional override slug (test injection). If null, a fresh slug is generated per run. */
|
|
10
17
|
repoSlug: string | null;
|
|
11
18
|
}
|
|
@@ -16,6 +23,8 @@ export interface ParsedE2EFlags {
|
|
|
16
23
|
keepRepo: boolean;
|
|
17
24
|
projectRoot: string | null;
|
|
18
25
|
skipNpm: boolean;
|
|
26
|
+
/** Opt-in: run the pinned legacy->bridge->npm-hybrid migration leg (#1912). */
|
|
27
|
+
legacyBridge: boolean;
|
|
19
28
|
unknown: string[];
|
|
20
29
|
}
|
|
21
30
|
export type EntrypointFn = (argv: string[]) => number;
|
package/dist/render/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { main as prdRenderMain, parsePrdArgv, renderPrd } from "./prd-render.js"
|
|
|
6
6
|
export * as projectRender from "./project-render.js";
|
|
7
7
|
export { flagStaleNarratives, main as projectRenderMain, renderProjectDefinition, scanLifecycleFolders, } from "./project-render.js";
|
|
8
8
|
export * as roadmapRender from "./roadmap-render.js";
|
|
9
|
-
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, } from "./roadmap-render.js";
|
|
9
|
+
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
|
|
10
10
|
export * as specRender from "./spec-render.js";
|
|
11
11
|
export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
|
|
12
12
|
export * as specValidate from "./spec-validate.js";
|
package/dist/render/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { main as prdRenderMain, parsePrdArgv, renderPrd } from "./prd-render.js"
|
|
|
6
6
|
export * as projectRender from "./project-render.js";
|
|
7
7
|
export { flagStaleNarratives, main as projectRenderMain, renderProjectDefinition, scanLifecycleFolders, } from "./project-render.js";
|
|
8
8
|
export * as roadmapRender from "./roadmap-render.js";
|
|
9
|
-
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, } from "./roadmap-render.js";
|
|
9
|
+
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
|
|
10
10
|
export * as specRender from "./spec-render.js";
|
|
11
11
|
export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
|
|
12
12
|
export * as specValidate from "./spec-validate.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
4
|
+
import { deriveRegistryItemStatus, registryMetadataReferencesFromScope, } from "../vbrief-validate/registry-status.js";
|
|
4
5
|
import { PROJECT_LIFECYCLE_FOLDERS, SKELETON_NARRATIVES } from "./constants.js";
|
|
5
6
|
import { splitCamel, splitWords } from "./text-utils.js";
|
|
6
7
|
export function scanLifecycleFolders(vbriefDir) {
|
|
@@ -24,8 +25,8 @@ export function scanLifecycleFolders(vbriefDir) {
|
|
|
24
25
|
const data = JSON.parse(readFileSync(full, "utf8"));
|
|
25
26
|
const plan = (data.plan ?? {});
|
|
26
27
|
const title = String(plan.title ?? vbriefFile.replace(/\.vbrief\.json$/, ""));
|
|
27
|
-
const status =
|
|
28
|
-
const references = plan.references;
|
|
28
|
+
const status = deriveRegistryItemStatus(plan.status, folderName);
|
|
29
|
+
const references = registryMetadataReferencesFromScope(plan.references);
|
|
29
30
|
const item = {
|
|
30
31
|
id: vbriefFile.replace(/\.vbrief\.json$/, "").replace(/\.vbrief$/, ""),
|
|
31
32
|
title,
|
|
@@ -35,7 +36,7 @@ export function scanLifecycleFolders(vbriefDir) {
|
|
|
35
36
|
lifecycle_folder: folderName,
|
|
36
37
|
},
|
|
37
38
|
};
|
|
38
|
-
if (
|
|
39
|
+
if (references.length > 0) {
|
|
39
40
|
item.metadata.references = references;
|
|
40
41
|
}
|
|
41
42
|
items.push(item);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Single render-to-buffer entry used by both write and --check (mirrors ``scripts/roadmap_render.generate_roadmap_content``). */
|
|
2
|
+
export declare function renderRoadmapToBuffer(pendingDir: string, completedDir?: string): string;
|
|
3
|
+
/** @deprecated Prefer ``renderRoadmapToBuffer`` — kept for existing imports and parity harnesses. */
|
|
2
4
|
export declare function generateRoadmapContent(pendingDir: string, completedDir?: string): string;
|
|
3
5
|
export type RenderRoadmapResult = readonly [boolean, string];
|
|
4
6
|
export declare function renderRoadmap(pendingDir: string, outPath: string, completedDir?: string): RenderRoadmapResult;
|
|
5
|
-
export declare function checkDrift(pendingDir: string, roadmapPath: string): RenderRoadmapResult;
|
|
7
|
+
export declare function checkDrift(pendingDir: string, roadmapPath: string, completedDir?: string): RenderRoadmapResult;
|
|
6
8
|
/** CLI entry (mirrors ``scripts/roadmap_render.main``). */
|
|
7
9
|
export declare function main(argv: readonly string[]): number;
|
|
8
10
|
//# sourceMappingURL=roadmap-render.d.ts.map
|