@deftai/directive-core 0.59.0 → 0.61.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/deposit/python-free.d.ts +21 -0
- package/dist/deposit/python-free.js +149 -0
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +2 -0
- package/dist/doctor/main.js +22 -8
- package/dist/init-deposit/init-deposit.js +2 -0
- package/dist/init-deposit/refresh.js +2 -0
- package/dist/install-upgrade/index.d.ts +11 -0
- package/dist/install-upgrade/index.js +146 -0
- package/dist/migrate-preflight/index.d.ts +36 -0
- package/dist/migrate-preflight/index.js +193 -0
- package/dist/release-e2e/greenfield-python-free-smoke-cli.d.ts +3 -0
- package/dist/release-e2e/greenfield-python-free-smoke-cli.js +10 -0
- package/dist/release-e2e/greenfield-python-free-smoke.d.ts +12 -0
- package/dist/release-e2e/greenfield-python-free-smoke.js +183 -0
- package/dist/render/export-spec.d.ts +17 -0
- package/dist/render/export-spec.js +104 -0
- package/dist/render/index.d.ts +3 -1
- package/dist/render/index.js +3 -1
- package/dist/render/project-render.d.ts +24 -0
- package/dist/render/project-render.js +111 -7
- package/dist/render/scope-outlook.d.ts +9 -0
- package/dist/render/scope-outlook.js +248 -0
- package/dist/render/spec-render.js +3 -192
- package/dist/spec-authority/constants.d.ts +12 -0
- package/dist/spec-authority/constants.js +34 -0
- package/dist/spec-authority/index.d.ts +5 -0
- package/dist/spec-authority/index.js +5 -0
- package/dist/spec-authority/migration-fidelity.d.ts +6 -0
- package/dist/spec-authority/migration-fidelity.js +96 -0
- package/dist/spec-authority/narratives.d.ts +6 -0
- package/dist/spec-authority/narratives.js +84 -0
- package/dist/spec-authority/resolver.d.ts +15 -0
- package/dist/spec-authority/resolver.js +52 -0
- package/dist/task-surface/index.d.ts +13 -0
- package/dist/task-surface/index.js +126 -0
- package/dist/validate-content/validate-strategy-output.js +5 -26
- package/dist/vbrief-validate/main.js +5 -0
- package/dist/vbrief-validate/precutover.d.ts +8 -18
- package/dist/vbrief-validate/precutover.js +39 -23
- package/dist/verify-env/toolchain-check.d.ts +9 -2
- package/dist/verify-env/toolchain-check.js +15 -4
- package/dist/verify-env/verify-hooks-installed.d.ts +3 -3
- package/dist/verify-env/verify-hooks-installed.js +59 -27
- package/package.json +15 -3
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { defaultWhich } from "../scm/binary.js";
|
|
5
|
+
import { detectPreCutoverLegacy, isCurrentGeneratedSpecification, isGeneratedSpecificationExport, missingLifecycleFolders, } from "../vbrief-validate/precutover.js";
|
|
6
|
+
function resolveContentRoot(frameworkRoot) {
|
|
7
|
+
const nested = join(frameworkRoot, "content");
|
|
8
|
+
try {
|
|
9
|
+
if (statSync(nested).isDirectory())
|
|
10
|
+
return nested;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// consumer deposit: content lives directly under framework root
|
|
14
|
+
}
|
|
15
|
+
return frameworkRoot;
|
|
16
|
+
}
|
|
17
|
+
export function checkUv(which = defaultWhich) {
|
|
18
|
+
if (which("uv") !== null) {
|
|
19
|
+
return { name: "uv", status: "PASS", message: "uv is on PATH." };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
name: "uv",
|
|
23
|
+
status: "FAIL",
|
|
24
|
+
message: "uv is not on PATH. Install from https://docs.astral.sh/uv/ and re-run.",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function checkLayout(deftRoot, projectRoot) {
|
|
28
|
+
const migrator = join(deftRoot, "scripts", "migrate_vbrief.py");
|
|
29
|
+
if (!existsSync(migrator) || !statSync(migrator).isFile()) {
|
|
30
|
+
return {
|
|
31
|
+
name: "layout",
|
|
32
|
+
status: "FAIL",
|
|
33
|
+
message: `Migrator script missing at ${migrator}. The framework checkout appears incomplete or pre-v0.20; refresh per deft/QUICK-START.md.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const schemasDir = join(resolveContentRoot(deftRoot), "vbrief", "schemas");
|
|
37
|
+
if (!existsSync(schemasDir) || !statSync(schemasDir).isDirectory()) {
|
|
38
|
+
return {
|
|
39
|
+
name: "layout",
|
|
40
|
+
status: "FAIL",
|
|
41
|
+
message: `Framework schemas dir missing at ${schemasDir}. Refresh the deft checkout (see deft/QUICK-START.md).`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const projectVbrief = join(projectRoot, "vbrief");
|
|
45
|
+
if (!existsSync(projectVbrief)) {
|
|
46
|
+
return {
|
|
47
|
+
name: "layout",
|
|
48
|
+
status: "WARN",
|
|
49
|
+
message: `Project vbrief/ not present at ${projectVbrief} -- migrator will create it on first run; this is expected for greenfield projects.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
name: "layout",
|
|
54
|
+
status: "PASS",
|
|
55
|
+
message: `Framework migrator + schemas present; project vbrief/ at ${projectVbrief}.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function checkGitClean(projectRoot) {
|
|
59
|
+
try {
|
|
60
|
+
const stdout = execFileSync("git", ["status", "--porcelain"], {
|
|
61
|
+
cwd: projectRoot,
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
});
|
|
64
|
+
if (stdout.trim()) {
|
|
65
|
+
return {
|
|
66
|
+
name: "git-clean",
|
|
67
|
+
status: "WARN",
|
|
68
|
+
message: "Working tree is dirty. The migrator will refuse to run without --force; preview with `task migrate:vbrief -- --dry-run` first.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { name: "git-clean", status: "PASS", message: "Working tree is clean." };
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const code = err.code;
|
|
75
|
+
if (code === "ENOENT") {
|
|
76
|
+
return {
|
|
77
|
+
name: "git-clean",
|
|
78
|
+
status: "WARN",
|
|
79
|
+
message: "git executable not on PATH; skipping working-tree check. Migrator's dirty-tree guard will still fire if applicable.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
name: "git-clean",
|
|
84
|
+
status: "WARN",
|
|
85
|
+
message: `Not a git repository at ${projectRoot}; skipping working-tree check.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function checkDocumentModel(projectRoot) {
|
|
90
|
+
const legacy = detectPreCutoverLegacy(projectRoot);
|
|
91
|
+
if (legacy.length > 0) {
|
|
92
|
+
return {
|
|
93
|
+
name: "document-model",
|
|
94
|
+
status: "PASS",
|
|
95
|
+
message: `Legacy root artifact(s) detected: ${legacy.join(", ")}.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const specPath = join(projectRoot, "SPECIFICATION.md");
|
|
99
|
+
if (existsSync(specPath) && statSync(specPath).isFile()) {
|
|
100
|
+
let content = "";
|
|
101
|
+
try {
|
|
102
|
+
content = readFileSync(specPath, "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
content = "";
|
|
106
|
+
}
|
|
107
|
+
if (isGeneratedSpecificationExport(projectRoot, content)) {
|
|
108
|
+
const missing = missingLifecycleFolders(projectRoot);
|
|
109
|
+
if (missing.length > 0) {
|
|
110
|
+
return {
|
|
111
|
+
name: "document-model",
|
|
112
|
+
status: "FAIL",
|
|
113
|
+
message: `Generated SPECIFICATION.md detected (source: vbrief/specification.vbrief.json); repair missing lifecycle folder(s) instead of migrating: ${missing.join(", ")}.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (isCurrentGeneratedSpecification(projectRoot, content)) {
|
|
118
|
+
return {
|
|
119
|
+
name: "document-model",
|
|
120
|
+
status: "FAIL",
|
|
121
|
+
message: "Current generated SPECIFICATION.md detected (source: vbrief/specification.vbrief.json); `task migrate:vbrief` is not needed.",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const vbriefRoot = join(projectRoot, "vbrief");
|
|
126
|
+
if (existsSync(vbriefRoot)) {
|
|
127
|
+
const missing = missingLifecycleFolders(projectRoot);
|
|
128
|
+
if (missing.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
name: "document-model",
|
|
131
|
+
status: "PASS",
|
|
132
|
+
message: `Partial vBRIEF layout detected; missing lifecycle folder(s): ${missing.join(", ")}.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
name: "document-model",
|
|
138
|
+
status: "WARN",
|
|
139
|
+
message: "No legacy root SPECIFICATION.md/PROJECT.md artifacts detected. Migration may have nothing to do.",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function evaluate(deftRoot, projectRoot, which = defaultWhich) {
|
|
143
|
+
const results = [
|
|
144
|
+
checkUv(which),
|
|
145
|
+
checkLayout(deftRoot, projectRoot),
|
|
146
|
+
checkDocumentModel(projectRoot),
|
|
147
|
+
checkGitClean(projectRoot),
|
|
148
|
+
];
|
|
149
|
+
if (results.some((r) => r.status === "FAIL")) {
|
|
150
|
+
return { exitCode: 1, results };
|
|
151
|
+
}
|
|
152
|
+
return { exitCode: 0, results };
|
|
153
|
+
}
|
|
154
|
+
export function formatCheckLine(result) {
|
|
155
|
+
return `CHECK ${result.name}: ${result.status} ${result.message}`;
|
|
156
|
+
}
|
|
157
|
+
export function runMigratePreflight(args) {
|
|
158
|
+
const projectRoot = resolve(args.projectRoot);
|
|
159
|
+
const deftRoot = resolve(args.deftRoot);
|
|
160
|
+
if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
|
|
161
|
+
return {
|
|
162
|
+
kind: "config",
|
|
163
|
+
message: `--project-root must be an existing directory: ${args.projectRoot}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (!existsSync(deftRoot) || !statSync(deftRoot).isDirectory()) {
|
|
167
|
+
return {
|
|
168
|
+
kind: "config",
|
|
169
|
+
message: `--deft-root must be an existing directory: ${args.deftRoot}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const { exitCode, results } = evaluate(deftRoot, projectRoot);
|
|
173
|
+
return { kind: "ready", exitCode, results };
|
|
174
|
+
}
|
|
175
|
+
export function emitMigratePreflight(outcome, io, quiet) {
|
|
176
|
+
for (const result of outcome.results) {
|
|
177
|
+
if (quiet && result.status === "PASS")
|
|
178
|
+
continue;
|
|
179
|
+
const line = `${formatCheckLine(result)}\n`;
|
|
180
|
+
if (result.status === "FAIL")
|
|
181
|
+
io.writeErr(line);
|
|
182
|
+
else
|
|
183
|
+
io.writeOut(line);
|
|
184
|
+
}
|
|
185
|
+
if (outcome.exitCode === 1) {
|
|
186
|
+
io.writeErr("migrate:preflight FAILED -- resolve the FAIL line(s) above before running `task migrate:vbrief`.\n");
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
io.writeOut("migrate:preflight OK -- environment ready for `task migrate:vbrief`.\n");
|
|
190
|
+
}
|
|
191
|
+
return outcome.exitCode;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { rehearseGreenfieldPythonFreeSmoke } from "./greenfield-python-free-smoke.js";
|
|
5
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
6
|
+
const skipWorkspacePrep = process.env.DEFT_GREENFIELD_SKIP_PREP === "1";
|
|
7
|
+
const [ok, reason] = rehearseGreenfieldPythonFreeSmoke(repoRoot, {}, { skipWorkspacePrep });
|
|
8
|
+
process.stdout.write(`${reason}\n`);
|
|
9
|
+
process.exit(ok ? 0 : 1);
|
|
10
|
+
//# sourceMappingURL=greenfield-python-free-smoke-cli.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { E2ESeams } from "./types.js";
|
|
2
|
+
export interface GreenfieldSmokeSeams extends E2ESeams {
|
|
3
|
+
}
|
|
4
|
+
export interface GreenfieldSmokeOptions {
|
|
5
|
+
skipWorkspacePrep?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Greenfield npm smoke (#2022 Phase 3): pack/install directive, run init +
|
|
9
|
+
* task check in a Python-free PATH, and assert the deposit carries no Python.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rehearseGreenfieldPythonFreeSmoke(repoRoot: string, seams?: GreenfieldSmokeSeams, options?: GreenfieldSmokeOptions): [boolean, string];
|
|
12
|
+
//# sourceMappingURL=greenfield-python-free-smoke.d.ts.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { collectPythonArtifacts, isRepoRootPythonRunShim } from "../deposit/python-free.js";
|
|
5
|
+
import { defaultWhich, spawnText } from "../release/spawn.js";
|
|
6
|
+
import { NPM_PUBLISH_PACKAGES } from "./constants.js";
|
|
7
|
+
import { alignNpmPackageVersions, resolvePnpm } from "./npm-ops.js";
|
|
8
|
+
const SMOKE_VERSION = "9.9.9-smoke";
|
|
9
|
+
function pythonFreePathEnv(base = process.env) {
|
|
10
|
+
const pathKey = base.PATH !== undefined ? "PATH" : base.Path !== undefined ? "Path" : "PATH";
|
|
11
|
+
const current = base[pathKey] ?? "";
|
|
12
|
+
const filtered = current
|
|
13
|
+
.split(":")
|
|
14
|
+
.filter((entry) => !/(^|\/)python[0-9]*(?:\.\d+)?$/.test(entry))
|
|
15
|
+
.filter((entry) => !entry.includes("/pyenv/"))
|
|
16
|
+
.join(":");
|
|
17
|
+
return {
|
|
18
|
+
...base,
|
|
19
|
+
[pathKey]: filtered,
|
|
20
|
+
DEFT_PYTHON: "",
|
|
21
|
+
PYTHON: "",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function seedMinimalProjectDefinition(projectDir) {
|
|
25
|
+
const vbriefDir = join(projectDir, "vbrief");
|
|
26
|
+
mkdirSync(vbriefDir, { recursive: true });
|
|
27
|
+
writeFileSync(join(vbriefDir, "PROJECT-DEFINITION.vbrief.json"), `${JSON.stringify({
|
|
28
|
+
vBRIEFInfo: { version: "0.6", description: "greenfield smoke fixture (#2022 Phase 3)" },
|
|
29
|
+
plan: {
|
|
30
|
+
title: "PROJECT-DEFINITION",
|
|
31
|
+
status: "running",
|
|
32
|
+
items: [],
|
|
33
|
+
policy: {},
|
|
34
|
+
narratives: {
|
|
35
|
+
Overview: "Greenfield smoke fixture (#2022 Phase 3).",
|
|
36
|
+
"tech stack": "Node.js",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}, null, 2)}\n`, "utf8");
|
|
40
|
+
}
|
|
41
|
+
function packTarballPath(packDir, pkgDir) {
|
|
42
|
+
const manifest = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8"));
|
|
43
|
+
if (typeof manifest.name !== "string" || typeof manifest.version !== "string") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const scoped = manifest.name.replaceAll("@", "").replaceAll("/", "-");
|
|
47
|
+
return join(packDir, `${scoped}-${manifest.version}.tgz`);
|
|
48
|
+
}
|
|
49
|
+
function runStep(spawn, label, cmd, args, options = {}) {
|
|
50
|
+
const result = spawn(cmd, args, options);
|
|
51
|
+
if (result.status !== 0) {
|
|
52
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
53
|
+
return [false, `${label} failed (exit ${result.status}): ${detail.slice(-800)}`];
|
|
54
|
+
}
|
|
55
|
+
return [true, `${label} OK`];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Greenfield npm smoke (#2022 Phase 3): pack/install directive, run init +
|
|
59
|
+
* task check in a Python-free PATH, and assert the deposit carries no Python.
|
|
60
|
+
*/
|
|
61
|
+
export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options = {}) {
|
|
62
|
+
const which = seams.which ?? seams.whichGh ?? defaultWhich;
|
|
63
|
+
const npm = which("npm");
|
|
64
|
+
if (!npm) {
|
|
65
|
+
return [true, "SKIP greenfield-python-free-smoke: npm not on PATH"];
|
|
66
|
+
}
|
|
67
|
+
const task = which("task");
|
|
68
|
+
if (!task) {
|
|
69
|
+
return [true, "SKIP greenfield-python-free-smoke: task (go-task) not on PATH"];
|
|
70
|
+
}
|
|
71
|
+
const pnpmPrefix = resolvePnpm(seams);
|
|
72
|
+
if (!pnpmPrefix || pnpmPrefix.length === 0) {
|
|
73
|
+
return [false, "greenfield-python-free-smoke FAIL: neither pnpm nor corepack on PATH"];
|
|
74
|
+
}
|
|
75
|
+
const [pnpmCmd, ...pnpmArgs] = pnpmPrefix;
|
|
76
|
+
if (pnpmCmd === undefined) {
|
|
77
|
+
return [false, "greenfield-python-free-smoke FAIL: pnpm command prefix is empty"];
|
|
78
|
+
}
|
|
79
|
+
const spawn = seams.spawnText ?? spawnText;
|
|
80
|
+
const work = mkdtempSync(join(tmpdir(), "deft-greenfield-smoke-"));
|
|
81
|
+
const packDir = join(work, "packs");
|
|
82
|
+
mkdirSync(packDir, { recursive: true });
|
|
83
|
+
const projectDir = join(work, "project");
|
|
84
|
+
const npmPrefix = join(work, "npm-prefix");
|
|
85
|
+
const envBase = { ...process.env, npm_config_prefix: npmPrefix };
|
|
86
|
+
const manifestBackup = new Map();
|
|
87
|
+
for (const pkg of NPM_PUBLISH_PACKAGES) {
|
|
88
|
+
const manifestPath = join(repoRoot, "packages", pkg, "package.json");
|
|
89
|
+
if (existsSync(manifestPath)) {
|
|
90
|
+
manifestBackup.set(manifestPath, readFileSync(manifestPath, "utf8"));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
let ok;
|
|
95
|
+
let reason;
|
|
96
|
+
if (!options.skipWorkspacePrep) {
|
|
97
|
+
[ok, reason] = runStep(spawn, "pnpm install", pnpmCmd, [...pnpmArgs, "install", "--frozen-lockfile"], {
|
|
98
|
+
cwd: repoRoot,
|
|
99
|
+
env: envBase,
|
|
100
|
+
timeoutMs: 120_000,
|
|
101
|
+
});
|
|
102
|
+
if (!ok)
|
|
103
|
+
return [false, `greenfield smoke: ${reason}`];
|
|
104
|
+
[ok, reason] = runStep(spawn, "pnpm build", pnpmCmd, [...pnpmArgs, "run", "build"], {
|
|
105
|
+
cwd: repoRoot,
|
|
106
|
+
env: envBase,
|
|
107
|
+
timeoutMs: 120_000,
|
|
108
|
+
});
|
|
109
|
+
if (!ok)
|
|
110
|
+
return [false, `greenfield smoke: ${reason}`];
|
|
111
|
+
}
|
|
112
|
+
[ok, reason] = alignNpmPackageVersions(repoRoot, SMOKE_VERSION);
|
|
113
|
+
if (!ok)
|
|
114
|
+
return [false, `greenfield smoke: ${reason}`];
|
|
115
|
+
const packed = [];
|
|
116
|
+
for (const pkg of NPM_PUBLISH_PACKAGES) {
|
|
117
|
+
const pkgDir = join(repoRoot, "packages", pkg);
|
|
118
|
+
[ok, reason] = runStep(spawn, `npm pack packages/${pkg}`, npm, ["pack", "--pack-destination", packDir], { cwd: pkgDir, env: envBase, timeoutMs: 120_000 });
|
|
119
|
+
if (!ok)
|
|
120
|
+
return [false, `greenfield smoke: ${reason}`];
|
|
121
|
+
const tgz = packTarballPath(packDir, pkgDir);
|
|
122
|
+
if (!tgz || !existsSync(tgz)) {
|
|
123
|
+
return [false, `greenfield smoke: missing pack tarball for ${pkg}`];
|
|
124
|
+
}
|
|
125
|
+
packed.push(tgz);
|
|
126
|
+
}
|
|
127
|
+
[ok, reason] = runStep(spawn, "npm install -g", npm, ["install", "-g", ...packed], {
|
|
128
|
+
env: envBase,
|
|
129
|
+
timeoutMs: 120_000,
|
|
130
|
+
});
|
|
131
|
+
if (!ok)
|
|
132
|
+
return [false, `greenfield smoke: ${reason}`];
|
|
133
|
+
const deft = join(npmPrefix, "bin", "deft");
|
|
134
|
+
if (!existsSync(deft)) {
|
|
135
|
+
return [false, `greenfield smoke: expected global deft at ${deft}`];
|
|
136
|
+
}
|
|
137
|
+
const pyFree = pythonFreePathEnv(envBase);
|
|
138
|
+
[ok, reason] = runStep(spawn, "directive init", deft, ["init", "--yes", "--repo-root", projectDir], {
|
|
139
|
+
cwd: work,
|
|
140
|
+
env: pyFree,
|
|
141
|
+
timeoutMs: 120_000,
|
|
142
|
+
});
|
|
143
|
+
if (!ok)
|
|
144
|
+
return [false, `greenfield smoke: ${reason}`];
|
|
145
|
+
seedMinimalProjectDefinition(projectDir);
|
|
146
|
+
const depositDir = join(projectDir, ".deft", "core");
|
|
147
|
+
const artifacts = collectPythonArtifacts(depositDir);
|
|
148
|
+
if (artifacts.length > 0) {
|
|
149
|
+
return [
|
|
150
|
+
false,
|
|
151
|
+
`greenfield smoke: deposit still contains Python artifacts: ${artifacts.map((a) => a.path).join(", ")}`,
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
if (isRepoRootPythonRunShim(projectDir)) {
|
|
155
|
+
return [false, "greenfield smoke: repo-root Python run shim present after init"];
|
|
156
|
+
}
|
|
157
|
+
const checkEnv = {
|
|
158
|
+
...pyFree,
|
|
159
|
+
PATH: `${join(npmPrefix, "bin")}:${pyFree.PATH ?? ""}`,
|
|
160
|
+
DEFT_SESSION_RITUAL_SKIP: "1",
|
|
161
|
+
};
|
|
162
|
+
[ok, reason] = runStep(spawn, "task deft:check", task, ["deft:check"], {
|
|
163
|
+
cwd: projectDir,
|
|
164
|
+
env: checkEnv,
|
|
165
|
+
timeoutMs: 180_000,
|
|
166
|
+
});
|
|
167
|
+
if (!ok)
|
|
168
|
+
return [false, `greenfield smoke: ${reason}`];
|
|
169
|
+
return [
|
|
170
|
+
true,
|
|
171
|
+
"greenfield-python-free-smoke: directive init + task deft:check passed with Python absent from PATH",
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
for (const [manifestPath, contents] of manifestBackup) {
|
|
176
|
+
writeFileSync(manifestPath, contents, "utf8");
|
|
177
|
+
}
|
|
178
|
+
if (process.env.DEFT_GREENFIELD_KEEP_WORK !== "1") {
|
|
179
|
+
rmSync(work, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=greenfield-python-free-smoke.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ExportAudience = "stakeholder" | "internal";
|
|
2
|
+
export interface ExportSpecOptions {
|
|
3
|
+
readonly projectRoot?: string;
|
|
4
|
+
readonly outPath?: string;
|
|
5
|
+
readonly audience?: ExportAudience;
|
|
6
|
+
readonly includeScopes?: boolean;
|
|
7
|
+
readonly proposedLimit?: number;
|
|
8
|
+
}
|
|
9
|
+
export type ExportSpecResult = readonly [boolean, string];
|
|
10
|
+
/** Unified spec export (#2013 / #1502). */
|
|
11
|
+
export declare function exportSpec(options?: ExportSpecOptions): ExportSpecResult;
|
|
12
|
+
export declare function parseExportSpecArgv(argv: readonly string[]): {
|
|
13
|
+
options: ExportSpecOptions;
|
|
14
|
+
errors: string[];
|
|
15
|
+
};
|
|
16
|
+
export declare function exportSpecMain(argv: readonly string[]): number;
|
|
17
|
+
//# sourceMappingURL=export-spec.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { greenfieldOverviewNonEmpty, renderNarrativeSections, resolveExportNarratives, } from "../spec-authority/narratives.js";
|
|
4
|
+
import { resolveSpecAuthority } from "../spec-authority/resolver.js";
|
|
5
|
+
import { buildScopeOutlookSection } from "./scope-outlook.js";
|
|
6
|
+
import { validateSpec } from "./spec-validate.js";
|
|
7
|
+
function loadPlanTitle(path, fallback) {
|
|
8
|
+
try {
|
|
9
|
+
const doc = JSON.parse(readFileSync(path, "utf8"));
|
|
10
|
+
const plan = doc.plan;
|
|
11
|
+
if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
|
|
12
|
+
return String(plan.title ?? fallback);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
/* ignore */
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
/** Unified spec export (#2013 / #1502). */
|
|
21
|
+
export function exportSpec(options = {}) {
|
|
22
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
23
|
+
const outPath = options.outPath ?? join(projectRoot, "SPECIFICATION.md");
|
|
24
|
+
const audience = options.audience ?? "stakeholder";
|
|
25
|
+
const includeScopes = options.includeScopes ?? true;
|
|
26
|
+
const includeProposed = audience === "internal";
|
|
27
|
+
const authority = resolveSpecAuthority(projectRoot);
|
|
28
|
+
if (!authority) {
|
|
29
|
+
return [false, "✗ Missing vbrief/PROJECT-DEFINITION.vbrief.json — cannot export spec."];
|
|
30
|
+
}
|
|
31
|
+
if (authority.kind === "full-spec" && authority.specPath) {
|
|
32
|
+
const [ok, msg] = validateSpec(authority.specPath);
|
|
33
|
+
if (!ok)
|
|
34
|
+
return [false, msg];
|
|
35
|
+
}
|
|
36
|
+
else if (!greenfieldOverviewNonEmpty(authority)) {
|
|
37
|
+
return [
|
|
38
|
+
false,
|
|
39
|
+
"⚠ PROJECT-DEFINITION.vbrief.json Overview narrative is empty (D3). Populate Overview before export.",
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
const narratives = resolveExportNarratives(authority);
|
|
43
|
+
const title = authority.kind === "full-spec" && authority.specPath
|
|
44
|
+
? loadPlanTitle(authority.specPath, "Specification")
|
|
45
|
+
: loadPlanTitle(authority.projectDefPath, "Specification");
|
|
46
|
+
const lines = [
|
|
47
|
+
authority.banner,
|
|
48
|
+
`# ${title}\n`,
|
|
49
|
+
...renderNarrativeSections(narratives),
|
|
50
|
+
];
|
|
51
|
+
if (includeScopes) {
|
|
52
|
+
const scopeLines = buildScopeOutlookSection(authority.vbriefDir, {
|
|
53
|
+
includeProposed,
|
|
54
|
+
proposedLimit: options.proposedLimit,
|
|
55
|
+
});
|
|
56
|
+
if (scopeLines.length > 0)
|
|
57
|
+
lines.push(...scopeLines);
|
|
58
|
+
}
|
|
59
|
+
writeFileSync(outPath, lines.join("\n"), "utf8");
|
|
60
|
+
return [true, `✓ Exported spec to ${outPath}`];
|
|
61
|
+
}
|
|
62
|
+
export function parseExportSpecArgv(argv) {
|
|
63
|
+
const options = {};
|
|
64
|
+
const errors = [];
|
|
65
|
+
const positional = [];
|
|
66
|
+
for (const arg of argv) {
|
|
67
|
+
if (arg === "--audience=stakeholder" || arg === "--audience=internal") {
|
|
68
|
+
options.audience = arg.split("=")[1];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg.startsWith("--proposed-limit=")) {
|
|
72
|
+
const n = Number(arg.split("=", 2)[1]);
|
|
73
|
+
if (Number.isFinite(n) && n > 0)
|
|
74
|
+
options.proposedLimit = n;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === "--no-scopes") {
|
|
78
|
+
options.includeScopes = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg.startsWith("--")) {
|
|
82
|
+
errors.push(`Unknown flag: ${arg}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
positional.push(arg);
|
|
86
|
+
}
|
|
87
|
+
if (positional[0])
|
|
88
|
+
options.projectRoot = positional[0];
|
|
89
|
+
if (positional[1])
|
|
90
|
+
options.outPath = positional[1];
|
|
91
|
+
return { options: options, errors };
|
|
92
|
+
}
|
|
93
|
+
export function exportSpecMain(argv) {
|
|
94
|
+
const { options, errors } = parseExportSpecArgv(argv);
|
|
95
|
+
if (errors.length > 0) {
|
|
96
|
+
for (const e of errors)
|
|
97
|
+
console.error(e);
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
const [ok, msg] = exportSpec(options);
|
|
101
|
+
console.log(msg);
|
|
102
|
+
return ok ? 0 : 1;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=export-spec.js.map
|
package/dist/render/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export * from "./constants.js";
|
|
2
|
+
export { type ExportAudience, type ExportSpecOptions, exportSpec, exportSpecMain, parseExportSpecArgv, } from "./export-spec.js";
|
|
2
3
|
export * as frameworkCommands from "./framework-commands.js";
|
|
3
4
|
export { availableCommands, cmdCoreValidate, formatFrameworkCommand, hasCommand, main as frameworkCommandsMain, normalizeTaskSeparator, runFrameworkCommand, } from "./framework-commands.js";
|
|
4
5
|
export * as prdRender from "./prd-render.js";
|
|
5
6
|
export { main as prdRenderMain, parsePrdArgv, renderPrd } from "./prd-render.js";
|
|
6
7
|
export * as projectRender from "./project-render.js";
|
|
7
|
-
export { flagStaleNarratives, main as projectRenderMain, renderProjectDefinition, scanLifecycleFolders, } from "./project-render.js";
|
|
8
|
+
export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
|
|
8
9
|
export * as roadmapRender from "./roadmap-render.js";
|
|
9
10
|
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
|
|
11
|
+
export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
|
|
10
12
|
export * as specRender from "./spec-render.js";
|
|
11
13
|
export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
|
|
12
14
|
export * as specValidate from "./spec-validate.js";
|
package/dist/render/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export * from "./constants.js";
|
|
2
|
+
export { exportSpec, exportSpecMain, parseExportSpecArgv, } from "./export-spec.js";
|
|
2
3
|
export * as frameworkCommands from "./framework-commands.js";
|
|
3
4
|
export { availableCommands, cmdCoreValidate, formatFrameworkCommand, hasCommand, main as frameworkCommandsMain, normalizeTaskSeparator, runFrameworkCommand, } from "./framework-commands.js";
|
|
4
5
|
export * as prdRender from "./prd-render.js";
|
|
5
6
|
export { main as prdRenderMain, parsePrdArgv, renderPrd } from "./prd-render.js";
|
|
6
7
|
export * as projectRender from "./project-render.js";
|
|
7
|
-
export { flagStaleNarratives, main as projectRenderMain, renderProjectDefinition, scanLifecycleFolders, } from "./project-render.js";
|
|
8
|
+
export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
|
|
8
9
|
export * as roadmapRender from "./roadmap-render.js";
|
|
9
10
|
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
|
|
11
|
+
export { aggregateScopeSection, buildScopeOutlookSection } from "./scope-outlook.js";
|
|
10
12
|
export * as specRender from "./spec-render.js";
|
|
11
13
|
export { main as specRenderMain, parseIncludeScopesFlag, renderSpec } from "./spec-render.js";
|
|
12
14
|
export * as specValidate from "./spec-validate.js";
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
type JsonObject = Record<string, unknown>;
|
|
2
|
+
/** Durable review state for PROJECT-DEFINITION narrative staleness (#640). */
|
|
3
|
+
export interface StalenessReviewMetadata {
|
|
4
|
+
/** ISO-8601 UTC when narratives were last reviewed/acknowledged. */
|
|
5
|
+
acknowledged_at: string;
|
|
6
|
+
/** Completed scope IDs incorporated at acknowledgement time (watermark). */
|
|
7
|
+
acknowledged_completed_scope_ids: readonly string[];
|
|
8
|
+
}
|
|
2
9
|
export interface LifecycleItem {
|
|
3
10
|
id: string;
|
|
4
11
|
title: string;
|
|
@@ -7,6 +14,16 @@ export interface LifecycleItem {
|
|
|
7
14
|
}
|
|
8
15
|
export declare function scanLifecycleFolders(vbriefDir: string): LifecycleItem[];
|
|
9
16
|
export declare function flagStaleNarratives(narratives: Record<string, string>, completedItems: LifecycleItem[]): string[];
|
|
17
|
+
/** Parse `plan.metadata.staleness_review` when present and well-formed. */
|
|
18
|
+
export declare function parseStalenessReview(metadata: unknown): StalenessReviewMetadata | null;
|
|
19
|
+
/** Completed scopes not yet covered by the acknowledgement watermark. */
|
|
20
|
+
export declare function unacknowledgedCompletedItems(completedItems: readonly LifecycleItem[], review: StalenessReviewMetadata | null): LifecycleItem[];
|
|
21
|
+
/** Build acknowledgement metadata for the current completed-scope set. */
|
|
22
|
+
export declare function buildStalenessAcknowledgement(completedItems: readonly LifecycleItem[], options?: {
|
|
23
|
+
now?: Date;
|
|
24
|
+
existing?: StalenessReviewMetadata | null;
|
|
25
|
+
}): StalenessReviewMetadata;
|
|
26
|
+
export declare function computeStalenessFlags(narratives: Record<string, string>, completedItems: readonly LifecycleItem[], review?: StalenessReviewMetadata | null): string[];
|
|
10
27
|
export declare function createSkeleton(items: LifecycleItem[], now: string): JsonObject;
|
|
11
28
|
export interface RenderProjectOptions {
|
|
12
29
|
readonly now?: Date;
|
|
@@ -14,6 +31,13 @@ export interface RenderProjectOptions {
|
|
|
14
31
|
export type RenderProjectResult = readonly [boolean, string];
|
|
15
32
|
/** Regenerate PROJECT-DEFINITION.vbrief.json (mirrors ``scripts/project_render.render_project_definition``). */
|
|
16
33
|
export declare function renderProjectDefinition(vbriefDir: string, options?: RenderProjectOptions): RenderProjectResult;
|
|
34
|
+
/**
|
|
35
|
+
* Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
|
|
36
|
+
*
|
|
37
|
+
* Distinct from `task reconcile:issues`, which reconciles origin freshness on scope
|
|
38
|
+
* vBRIEFs — this path only clears narrative staleness heuristics on render.
|
|
39
|
+
*/
|
|
40
|
+
export declare function acknowledgeProjectDefinitionStaleness(vbriefDir: string, options?: RenderProjectOptions): RenderProjectResult;
|
|
17
41
|
/** CLI entry (mirrors ``scripts/project_render.main``). */
|
|
18
42
|
export declare function main(argv: readonly string[]): number;
|
|
19
43
|
export {};
|