@deftai/directive-core 0.59.0 → 0.60.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/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/project-render.d.ts +24 -0
- package/dist/render/project-render.js +111 -7
- package/dist/task-surface/index.d.ts +13 -0
- package/dist/task-surface/index.js +126 -0
- package/dist/vbrief-validate/precutover.d.ts +4 -0
- package/dist/vbrief-validate/precutover.js +24 -2
- package/dist/verify-env/toolchain-check.d.ts +9 -2
- package/dist/verify-env/toolchain-check.js +15 -4
- 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
|
package/dist/render/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { availableCommands, cmdCoreValidate, formatFrameworkCommand, hasCommand,
|
|
|
4
4
|
export * as prdRender from "./prd-render.js";
|
|
5
5
|
export { main as prdRenderMain, parsePrdArgv, renderPrd } from "./prd-render.js";
|
|
6
6
|
export * as projectRender from "./project-render.js";
|
|
7
|
-
export { flagStaleNarratives, main as projectRenderMain, renderProjectDefinition, scanLifecycleFolders, } from "./project-render.js";
|
|
7
|
+
export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
|
|
8
8
|
export * as roadmapRender from "./roadmap-render.js";
|
|
9
9
|
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
|
|
10
10
|
export * as specRender from "./spec-render.js";
|
package/dist/render/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { availableCommands, cmdCoreValidate, formatFrameworkCommand, hasCommand,
|
|
|
4
4
|
export * as prdRender from "./prd-render.js";
|
|
5
5
|
export { main as prdRenderMain, parsePrdArgv, renderPrd } from "./prd-render.js";
|
|
6
6
|
export * as projectRender from "./project-render.js";
|
|
7
|
-
export { flagStaleNarratives, main as projectRenderMain, renderProjectDefinition, scanLifecycleFolders, } from "./project-render.js";
|
|
7
|
+
export { acknowledgeProjectDefinitionStaleness, buildStalenessAcknowledgement, computeStalenessFlags, flagStaleNarratives, main as projectRenderMain, parseStalenessReview, renderProjectDefinition, scanLifecycleFolders, unacknowledgedCompletedItems, } from "./project-render.js";
|
|
8
8
|
export * as roadmapRender from "./roadmap-render.js";
|
|
9
9
|
export { checkDrift, generateRoadmapContent, main as roadmapRenderMain, renderRoadmap, renderRoadmapToBuffer, } from "./roadmap-render.js";
|
|
10
10
|
export * as specRender from "./spec-render.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 {};
|
|
@@ -87,9 +87,57 @@ export function flagStaleNarratives(narratives, completedItems) {
|
|
|
87
87
|
}
|
|
88
88
|
return flags.sort();
|
|
89
89
|
}
|
|
90
|
+
function isoTimestamp(date) {
|
|
91
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
92
|
+
}
|
|
93
|
+
/** Parse `plan.metadata.staleness_review` when present and well-formed. */
|
|
94
|
+
export function parseStalenessReview(metadata) {
|
|
95
|
+
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const review = metadata.staleness_review;
|
|
99
|
+
if (typeof review !== "object" || review === null || Array.isArray(review)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const acknowledgedAt = review.acknowledged_at;
|
|
103
|
+
const scopeIds = review.acknowledged_completed_scope_ids;
|
|
104
|
+
if (typeof acknowledgedAt !== "string" || acknowledgedAt.length === 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(scopeIds) || !scopeIds.every((id) => typeof id === "string")) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
acknowledged_at: acknowledgedAt,
|
|
112
|
+
acknowledged_completed_scope_ids: [...scopeIds].sort(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Completed scopes not yet covered by the acknowledgement watermark. */
|
|
116
|
+
export function unacknowledgedCompletedItems(completedItems, review) {
|
|
117
|
+
if (!review)
|
|
118
|
+
return [...completedItems];
|
|
119
|
+
const acknowledged = new Set(review.acknowledged_completed_scope_ids);
|
|
120
|
+
return completedItems.filter((item) => !acknowledged.has(item.id));
|
|
121
|
+
}
|
|
122
|
+
/** Build acknowledgement metadata for the current completed-scope set. */
|
|
123
|
+
export function buildStalenessAcknowledgement(completedItems, options = {}) {
|
|
124
|
+
const now = isoTimestamp(options.now ?? new Date());
|
|
125
|
+
const mergedIds = new Set([
|
|
126
|
+
...(options.existing?.acknowledged_completed_scope_ids ?? []),
|
|
127
|
+
...completedItems.map((item) => item.id),
|
|
128
|
+
]);
|
|
129
|
+
return {
|
|
130
|
+
acknowledged_at: now,
|
|
131
|
+
acknowledged_completed_scope_ids: [...mergedIds].sort(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function computeStalenessFlags(narratives, completedItems, review = null) {
|
|
135
|
+
const pending = unacknowledgedCompletedItems(completedItems, review);
|
|
136
|
+
return flagStaleNarratives(narratives, pending);
|
|
137
|
+
}
|
|
90
138
|
export function createSkeleton(items, now) {
|
|
91
139
|
const completedItems = items.filter((i) => i.status === "completed");
|
|
92
|
-
const stalenessFlags =
|
|
140
|
+
const stalenessFlags = computeStalenessFlags({ ...SKELETON_NARRATIVES }, completedItems);
|
|
93
141
|
return {
|
|
94
142
|
vBRIEFInfo: {
|
|
95
143
|
version: EMITTED_VBRIEF_VERSION,
|
|
@@ -133,13 +181,15 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
|
133
181
|
? plan.narratives
|
|
134
182
|
: {};
|
|
135
183
|
const completedItems = items.filter((i) => i.status === "completed");
|
|
136
|
-
const flags = flagStaleNarratives(narratives, completedItems);
|
|
137
184
|
if (typeof plan.metadata !== "object" ||
|
|
138
185
|
plan.metadata === null ||
|
|
139
186
|
Array.isArray(plan.metadata)) {
|
|
140
187
|
plan.metadata = {};
|
|
141
188
|
}
|
|
142
|
-
plan.metadata
|
|
189
|
+
const planMetadata = plan.metadata;
|
|
190
|
+
const review = parseStalenessReview(planMetadata);
|
|
191
|
+
const flags = computeStalenessFlags(narratives, completedItems, review);
|
|
192
|
+
planMetadata.staleness_flags = flags;
|
|
143
193
|
projectDef.plan = plan;
|
|
144
194
|
}
|
|
145
195
|
else {
|
|
@@ -156,14 +206,68 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
|
156
206
|
parts.push(`⚠ ${flagCount} staleness flag(s) -- agent review recommended`);
|
|
157
207
|
return [true, parts.join("\n")];
|
|
158
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
|
|
211
|
+
*
|
|
212
|
+
* Distinct from `task reconcile:issues`, which reconciles origin freshness on scope
|
|
213
|
+
* vBRIEFs — this path only clears narrative staleness heuristics on render.
|
|
214
|
+
*/
|
|
215
|
+
export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
|
|
216
|
+
const nowDate = options.now ?? new Date();
|
|
217
|
+
const now = isoTimestamp(nowDate);
|
|
218
|
+
const projectDefPath = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
|
|
219
|
+
if (!existsSync(projectDefPath)) {
|
|
220
|
+
return [false, `✗ ${projectDefPath} not found — run project:render first`];
|
|
221
|
+
}
|
|
222
|
+
let projectDef;
|
|
223
|
+
try {
|
|
224
|
+
projectDef = JSON.parse(readFileSync(projectDefPath, "utf8"));
|
|
225
|
+
}
|
|
226
|
+
catch (exc) {
|
|
227
|
+
return [false, `✗ Failed to read ${projectDefPath}: ${String(exc)}`];
|
|
228
|
+
}
|
|
229
|
+
const plan = (projectDef.plan ?? {});
|
|
230
|
+
if (typeof plan.metadata !== "object" || plan.metadata === null || Array.isArray(plan.metadata)) {
|
|
231
|
+
plan.metadata = {};
|
|
232
|
+
}
|
|
233
|
+
const planMetadata = plan.metadata;
|
|
234
|
+
const items = scanLifecycleFolders(vbriefDir);
|
|
235
|
+
const completedItems = items.filter((i) => i.status === "completed");
|
|
236
|
+
const existing = parseStalenessReview(planMetadata);
|
|
237
|
+
planMetadata.staleness_review = buildStalenessAcknowledgement(completedItems, {
|
|
238
|
+
now: nowDate,
|
|
239
|
+
existing,
|
|
240
|
+
});
|
|
241
|
+
const narratives = typeof plan.narratives === "object" &&
|
|
242
|
+
plan.narratives !== null &&
|
|
243
|
+
!Array.isArray(plan.narratives)
|
|
244
|
+
? plan.narratives
|
|
245
|
+
: {};
|
|
246
|
+
planMetadata.staleness_flags = computeStalenessFlags(narratives, completedItems, parseStalenessReview(planMetadata));
|
|
247
|
+
if (typeof projectDef.vBRIEFInfo !== "object" || projectDef.vBRIEFInfo === null) {
|
|
248
|
+
projectDef.vBRIEFInfo = {};
|
|
249
|
+
}
|
|
250
|
+
projectDef.vBRIEFInfo.updated = now;
|
|
251
|
+
projectDef.plan = plan;
|
|
252
|
+
writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
|
|
253
|
+
const ackCount = completedItems.length;
|
|
254
|
+
return [
|
|
255
|
+
true,
|
|
256
|
+
`✓ PROJECT-DEFINITION staleness acknowledged (${ackCount} completed scope(s) watermarked)`,
|
|
257
|
+
];
|
|
258
|
+
}
|
|
159
259
|
/** CLI entry (mirrors ``scripts/project_render.main``). */
|
|
160
260
|
export function main(argv) {
|
|
161
|
-
|
|
162
|
-
|
|
261
|
+
const acknowledge = argv[0] === "--acknowledge-staleness";
|
|
262
|
+
const rest = acknowledge ? argv.slice(1) : argv;
|
|
263
|
+
if (rest.length > 1) {
|
|
264
|
+
process.stderr.write("Usage: project_render.py [--acknowledge-staleness] [vbrief_dir]\n");
|
|
163
265
|
return 2;
|
|
164
266
|
}
|
|
165
|
-
const vbriefDir =
|
|
166
|
-
const [ok, message] =
|
|
267
|
+
const vbriefDir = rest[0] ?? "vbrief";
|
|
268
|
+
const [ok, message] = acknowledge
|
|
269
|
+
? acknowledgeProjectDefinitionStaleness(vbriefDir)
|
|
270
|
+
: renderProjectDefinition(vbriefDir);
|
|
167
271
|
process.stdout.write(`${message}\n`);
|
|
168
272
|
return ok ? 0 : 1;
|
|
169
273
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TaskSurfaceIo {
|
|
2
|
+
writeOut: (text: string) => void;
|
|
3
|
+
writeErr: (text: string) => void;
|
|
4
|
+
}
|
|
5
|
+
/** Port of ``task change:changelog:check`` inline Python (#2022 Phase 2). */
|
|
6
|
+
export declare function runChangelogCheck(projectRoot: string, io: TaskSurfaceIo): number;
|
|
7
|
+
/** Port of ``task change:init`` inline Python (#2022 Phase 2). */
|
|
8
|
+
export declare function runChangeInit(projectRoot: string, name: string, io: TaskSurfaceIo): number;
|
|
9
|
+
/** Port of ``task commit:lint`` inline Python (#2022 Phase 2). */
|
|
10
|
+
export declare function runCommitLint(projectRoot: string, io: TaskSurfaceIo): number;
|
|
11
|
+
/** Port of ``task install:uninstall`` inline Python (#2022 Phase 2). */
|
|
12
|
+
export declare function runInstallUninstall(projectRoot: string, io: TaskSurfaceIo): number;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|