@deftai/directive-core 0.55.2 → 0.56.1
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/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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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/flags.js +5 -1
- package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
- package/dist/release-e2e/legacy-bridge-leg.js +478 -0
- package/dist/release-e2e/main.d.ts +2 -1
- package/dist/release-e2e/main.js +17 -1
- 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/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,478 @@
|
|
|
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
|
+
/**
|
|
273
|
+
* Map a Node `process.platform` value to the published release OS asset token.
|
|
274
|
+
*
|
|
275
|
+
* The release workflow (.github/workflows/release.yml) uploads assets named
|
|
276
|
+
* `install-<os>-<arch>` where <os> is `windows` / `macos` / `linux` -- NOT the Go
|
|
277
|
+
* `GOOS` token. In particular macOS publishes a single `install-macos-universal`
|
|
278
|
+
* binary (the lipo'd fat binary), so a darwin host must look for `macos`, not
|
|
279
|
+
* `darwin`.
|
|
280
|
+
*/
|
|
281
|
+
function goOsToken(platform) {
|
|
282
|
+
if (platform === "win32")
|
|
283
|
+
return "windows";
|
|
284
|
+
if (platform === "darwin")
|
|
285
|
+
return "macos"; // assets are install-macos-universal
|
|
286
|
+
return platform; // "linux" lines up as-is
|
|
287
|
+
}
|
|
288
|
+
/** Map a Node `process.arch` value to the Go release `GOARCH` asset token. */
|
|
289
|
+
function goArchToken(arch) {
|
|
290
|
+
if (arch === "x64")
|
|
291
|
+
return "amd64";
|
|
292
|
+
return arch; // "arm64" lines up with GOARCH as-is
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Match an installer binary asset. The release workflow uploads
|
|
296
|
+
* `install-<os>-<arch>` (e.g. `install-linux-amd64`, `install-windows-amd64.exe`,
|
|
297
|
+
* `install-macos-universal`); the legacy single-binary / `deft-install-` naming is
|
|
298
|
+
* also accepted for back-compat. Anchored at the start of the basename so sibling
|
|
299
|
+
* assets like `checksums.txt` / `README.md` are excluded.
|
|
300
|
+
*/
|
|
301
|
+
const INSTALLER_ASSET_RE = /^(?:deft-)?install(?:-|\.exe$|$)/i;
|
|
302
|
+
/**
|
|
303
|
+
* Pick the installer bridge binary matching the current host platform/arch.
|
|
304
|
+
*
|
|
305
|
+
* The release publishes one asset per OS (`install-<os>-<arch>`, with macOS as a
|
|
306
|
+
* single `install-macos-universal` fat binary). Selecting `assets[0]`
|
|
307
|
+
* alphabetically silently picks the wrong binary on a multi-asset release,
|
|
308
|
+
* producing an exec-format failure on a mismatched runner. This resolves the
|
|
309
|
+
* host-matching asset instead and reports a precise outcome when none matches:
|
|
310
|
+
* - single installer asset -> use it (single-binary releases, back-compat);
|
|
311
|
+
* - prefer an asset whose name carries BOTH the OS and arch tokens;
|
|
312
|
+
* - else fall back to an OS-only match (e.g. the macOS universal binary, which
|
|
313
|
+
* carries no arch suffix, or releases that omit the arch suffix);
|
|
314
|
+
* - else `no-platform-match` so the caller fails loudly rather than guessing.
|
|
315
|
+
*/
|
|
316
|
+
export function selectBridgeAsset(fileNames, platform = process.platform, arch = process.arch) {
|
|
317
|
+
const candidates = fileNames.filter((name) => INSTALLER_ASSET_RE.test(name));
|
|
318
|
+
if (candidates.length === 0) {
|
|
319
|
+
return { kind: "none" };
|
|
320
|
+
}
|
|
321
|
+
if (candidates.length === 1) {
|
|
322
|
+
return { kind: "ok", name: candidates[0] };
|
|
323
|
+
}
|
|
324
|
+
const os = goOsToken(platform).toLowerCase();
|
|
325
|
+
const cpu = goArchToken(arch).toLowerCase();
|
|
326
|
+
const osArch = candidates.find((name) => {
|
|
327
|
+
const lower = name.toLowerCase();
|
|
328
|
+
return lower.includes(os) && lower.includes(cpu);
|
|
329
|
+
});
|
|
330
|
+
if (osArch !== undefined) {
|
|
331
|
+
return { kind: "ok", name: osArch };
|
|
332
|
+
}
|
|
333
|
+
const osOnly = candidates.find((name) => name.toLowerCase().includes(os));
|
|
334
|
+
if (osOnly !== undefined) {
|
|
335
|
+
return { kind: "ok", name: osOnly };
|
|
336
|
+
}
|
|
337
|
+
return { kind: "no-platform-match", candidates: [...candidates] };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The frozen-mode real handoff (reached ONLY once the operator pins the SoT):
|
|
341
|
+
* download the frozen Go bridge binary at the pinned tag and run it against a
|
|
342
|
+
* legacy fixture to normalise it to canonical-vendored. Soft-skips when `gh` is
|
|
343
|
+
* absent. Never builds/cuts a release -- it consumes an already-published one.
|
|
344
|
+
*/
|
|
345
|
+
export function downloadAndRunFrozenBridge(pin, fixtureDir, seams = {}) {
|
|
346
|
+
if (seams.runFrozenBridge) {
|
|
347
|
+
return seams.runFrozenBridge(pin, fixtureDir);
|
|
348
|
+
}
|
|
349
|
+
const which = seams.which ?? seams.whichGh ?? defaultWhich;
|
|
350
|
+
const spawn = seams.spawnText ?? spawnText;
|
|
351
|
+
const gh = which("gh");
|
|
352
|
+
if (gh === null) {
|
|
353
|
+
return [true, `SKIP (gh not on PATH; cannot fetch the frozen bridge ${pin})`];
|
|
354
|
+
}
|
|
355
|
+
const mkdtemp = seams.mkdtemp ?? ((prefix) => mkdtempSync(join(tmpdir(), prefix)));
|
|
356
|
+
const rmTemp = seams.rmTemp ?? ((p) => rmSync(p, { recursive: true, force: true }));
|
|
357
|
+
const assetDir = mkdtemp("deft-frozen-bridge-");
|
|
358
|
+
// Clean up the downloaded-binary temp dir on every exit path (mirrors the
|
|
359
|
+
// workRoot try/finally in runLegacyBridgeLeg). Once the SoT is pinned this
|
|
360
|
+
// runs on every --legacy-bridge CI invocation, so a leaked assetDir would
|
|
361
|
+
// accumulate full Go-bridge binaries.
|
|
362
|
+
try {
|
|
363
|
+
const download = spawn(gh, ["release", "download", pin, "--repo", GO_BRIDGE_REPO, "--dir", assetDir, "--clobber"], { env: { ...process.env }, timeoutMs: 300_000 });
|
|
364
|
+
if (download.status !== 0) {
|
|
365
|
+
return [
|
|
366
|
+
false,
|
|
367
|
+
`gh release download ${pin} from ${GO_BRIDGE_REPO} failed: ${download.stderr.trim()}`,
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
const selection = selectBridgeAsset(readdirSync(assetDir));
|
|
371
|
+
if (selection.kind === "none") {
|
|
372
|
+
return [
|
|
373
|
+
false,
|
|
374
|
+
`frozen bridge ${pin} downloaded but no installer asset (install-*) found in ${assetDir} ` +
|
|
375
|
+
`(see ${GO_BRIDGE_RELEASES_URL})`,
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
if (selection.kind === "no-platform-match") {
|
|
379
|
+
return [
|
|
380
|
+
false,
|
|
381
|
+
`frozen bridge ${pin} downloaded but no installer asset matches ` +
|
|
382
|
+
`${goOsToken(process.platform)}/${goArchToken(process.arch)} ` +
|
|
383
|
+
`(candidates: ${selection.candidates.join(", ")}; see ${GO_BRIDGE_RELEASES_URL})`,
|
|
384
|
+
];
|
|
385
|
+
}
|
|
386
|
+
const binaryPath = join(assetDir, selection.name);
|
|
387
|
+
try {
|
|
388
|
+
chmodSync(binaryPath, 0o755);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// best-effort; non-fatal on platforms that ignore the bit
|
|
392
|
+
}
|
|
393
|
+
const run = spawn(binaryPath, ["--yes", "--upgrade", "--repo-root", fixtureDir], {
|
|
394
|
+
env: { ...process.env },
|
|
395
|
+
timeoutMs: 300_000,
|
|
396
|
+
});
|
|
397
|
+
if (run.status !== 0) {
|
|
398
|
+
return [false, `frozen bridge ${pin} run failed (exit ${run.status}): ${run.stderr.trim()}`];
|
|
399
|
+
}
|
|
400
|
+
if (detectLegacyLayout(fixtureDir).legacy) {
|
|
401
|
+
return [false, `frozen bridge ${pin} ran but the fixture is still a legacy layout`];
|
|
402
|
+
}
|
|
403
|
+
return [true, `frozen bridge ${pin} normalised the legacy fixture to canonical-vendored`];
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
rmTemp(assetDir);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Run the pinned legacy -> bridge -> npm-hybrid migration leg.
|
|
411
|
+
*
|
|
412
|
+
* Returns `[ok, reason]` like the other release-e2e legs. Soft-skips (ok=true)
|
|
413
|
+
* when npm is absent. While the SoT is null (pending-pin) it asserts the
|
|
414
|
+
* fixture-shape + stage-2 npm-hybrid checks and SKIPS the real frozen-binary
|
|
415
|
+
* download; once the SoT is pinned it additionally runs the real bridge handoff.
|
|
416
|
+
*/
|
|
417
|
+
export function runLegacyBridgeLeg(projectRoot, seams = {}) {
|
|
418
|
+
const which = seams.which ?? seams.whichGh ?? defaultWhich;
|
|
419
|
+
if (which("npm") === null) {
|
|
420
|
+
return [
|
|
421
|
+
true,
|
|
422
|
+
"SKIP (npm not on PATH; Node-less operator) -- the npm-hybrid stage needs the npm channel",
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
const pin = (seams.lastGoInstaller ?? lastGoInstaller)();
|
|
426
|
+
const frozen = pin !== null;
|
|
427
|
+
const agentsTemplatePath = seams.agentsTemplatePath !== undefined
|
|
428
|
+
? seams.agentsTemplatePath
|
|
429
|
+
: join(projectRoot, "content", "templates", "agents-entry.md");
|
|
430
|
+
const mkdtemp = seams.mkdtemp ?? ((prefix) => mkdtempSync(join(tmpdir(), prefix)));
|
|
431
|
+
const rmTemp = seams.rmTemp ?? ((p) => rmSync(p, { recursive: true, force: true }));
|
|
432
|
+
const workRoot = mkdtemp("deft-legacy-bridge-");
|
|
433
|
+
try {
|
|
434
|
+
let [ok, reason] = assertLegacyFixturesDetectedAndRefused(workRoot);
|
|
435
|
+
emit(" legacy-bridge step: legacy fixtures + refuse-boundary", `${ok ? "OK" : "FAIL"} (${reason})`);
|
|
436
|
+
if (!ok) {
|
|
437
|
+
return [false, `legacy fixtures: ${reason}`];
|
|
438
|
+
}
|
|
439
|
+
// Stage 1: the frozen Go bridge normalises legacy -> canonical-vendored.
|
|
440
|
+
if (frozen && pin !== null) {
|
|
441
|
+
const fixtureDir = join(workRoot, "frozen-fixture");
|
|
442
|
+
provisionLegacyFixture(fixtureDir, "pre-v0.27-sentinel-agents-md");
|
|
443
|
+
const [bridgeOk, bridgeReason] = downloadAndRunFrozenBridge(pin, fixtureDir, seams);
|
|
444
|
+
emit(" legacy-bridge step: frozen bridge handoff", `${bridgeOk ? "OK" : "FAIL"} (${bridgeReason})`);
|
|
445
|
+
if (!bridgeOk) {
|
|
446
|
+
return [false, `frozen bridge: ${bridgeReason}`];
|
|
447
|
+
}
|
|
448
|
+
if (bridgeReason.startsWith("SKIP")) {
|
|
449
|
+
return [true, `frozen pin ${pin}: ${bridgeReason}; stage-1 deferred (gh absent)`];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
emit(" legacy-bridge step: frozen bridge handoff", "PENDING (SoT unfrozen) -- skipped the real frozen-binary download (pin: null)");
|
|
454
|
+
}
|
|
455
|
+
// Stage 2: directive migrate (+ init/update AGENTS.md render) -> npm-hybrid.
|
|
456
|
+
[ok, reason] = assertNpmHybridMigration(workRoot, agentsTemplatePath, seams);
|
|
457
|
+
emit(" legacy-bridge step: stage-2 npm-hybrid", `${ok ? "OK" : "FAIL"} (${reason})`);
|
|
458
|
+
if (!ok) {
|
|
459
|
+
return [false, `npm-hybrid: ${reason}`];
|
|
460
|
+
}
|
|
461
|
+
if (frozen) {
|
|
462
|
+
return [
|
|
463
|
+
true,
|
|
464
|
+
`frozen pin ${pin}: real legacy -> bridge -> npm-hybrid handoff verified ` +
|
|
465
|
+
"(legacy fixtures detected; frozen bridge normalised; stage-2 migrate npm-hybrid)",
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
return [
|
|
469
|
+
true,
|
|
470
|
+
"PENDING (SoT unfrozen): legacy fixtures + npm refuse-boundary + stage-2 npm-hybrid shape " +
|
|
471
|
+
"asserted; skipped the real frozen-binary download (pin: null) until the operator freezes",
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
finally {
|
|
475
|
+
rmTemp(workRoot);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
//# 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);
|
|
@@ -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
|