@deftai/directive-core 0.58.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.
Files changed (36) hide show
  1. package/dist/deposit/python-free.d.ts +21 -0
  2. package/dist/deposit/python-free.js +149 -0
  3. package/dist/deposit/resolve-content.d.ts +6 -0
  4. package/dist/deposit/resolve-content.js +7 -1
  5. package/dist/doctor/checks.js +2 -2
  6. package/dist/doctor/constants.d.ts +2 -0
  7. package/dist/doctor/constants.js +2 -0
  8. package/dist/doctor/main.js +22 -8
  9. package/dist/init-deposit/init-deposit.js +2 -0
  10. package/dist/init-deposit/refresh.js +2 -0
  11. package/dist/install-upgrade/index.d.ts +11 -0
  12. package/dist/install-upgrade/index.js +146 -0
  13. package/dist/migrate-preflight/index.d.ts +36 -0
  14. package/dist/migrate-preflight/index.js +193 -0
  15. package/dist/release/index.d.ts +2 -1
  16. package/dist/release/index.js +2 -1
  17. package/dist/release/pipeline.js +8 -3
  18. package/dist/release/preflight.d.ts +27 -0
  19. package/dist/release/preflight.js +27 -0
  20. package/dist/release/{python-bridge.d.ts → python-steps.d.ts} +1 -2
  21. package/dist/release/{python-bridge.js → python-steps.js} +13 -14
  22. package/dist/release-e2e/greenfield-python-free-smoke-cli.d.ts +3 -0
  23. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +10 -0
  24. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +12 -0
  25. package/dist/release-e2e/greenfield-python-free-smoke.js +183 -0
  26. package/dist/render/index.d.ts +1 -1
  27. package/dist/render/index.js +1 -1
  28. package/dist/render/project-render.d.ts +24 -0
  29. package/dist/render/project-render.js +111 -7
  30. package/dist/task-surface/index.d.ts +13 -0
  31. package/dist/task-surface/index.js +126 -0
  32. package/dist/vbrief-validate/precutover.d.ts +28 -0
  33. package/dist/vbrief-validate/precutover.js +90 -3
  34. package/dist/verify-env/toolchain-check.d.ts +9 -2
  35. package/dist/verify-env/toolchain-check.js +15 -4
  36. 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
@@ -5,9 +5,10 @@ export * from "./git.js";
5
5
  export * from "./main.js";
6
6
  export * from "./paths.js";
7
7
  export * from "./pipeline.js";
8
+ export * from "./preflight.js";
8
9
  export * from "./pyproject.js";
9
10
  export * from "./pyproject-sync.js";
10
- export * from "./python-bridge.js";
11
+ export * from "./python-steps.js";
11
12
  export * from "./spawn.js";
12
13
  export * from "./types.js";
13
14
  export * from "./version.js";
@@ -6,9 +6,10 @@ export * from "./git.js";
6
6
  export * from "./main.js";
7
7
  export * from "./paths.js";
8
8
  export * from "./pipeline.js";
9
+ export * from "./preflight.js";
9
10
  export * from "./pyproject.js";
10
11
  export * from "./pyproject-sync.js";
11
- export * from "./python-bridge.js";
12
+ export * from "./python-steps.js";
12
13
  export * from "./spawn.js";
13
14
  export * from "./types.js";
14
15
  export * from "./version.js";
@@ -5,8 +5,9 @@ import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_ARTIFACTS, TOTAL_ST
5
5
  import { checkTagAvailable, createGithubRelease, readTextFile, verifyReleaseDraft } from "./gh.js";
6
6
  import { checkGitClean, commitReleaseArtifacts, createTag, currentBranch, pushRelease, releaseCommitSubject, } from "./git.js";
7
7
  import { resolveScriptsDir, todayIso } from "./paths.js";
8
+ import { runReleaseCheck } from "./preflight.js";
8
9
  import { pyprojectPathFor, syncPyprojectForRelease } from "./pyproject-sync.js";
9
- import { checkVbriefLifecycleSync, refreshRoadmap, runBuild, runCi, runUvLock, } from "./python-bridge.js";
10
+ import { checkVbriefLifecycleSync, refreshRoadmap, runBuild, runUvLock } from "./python-steps.js";
10
11
  import { isPrereleaseTag } from "./version.js";
11
12
  export function emit(step, label, status, target = process.stderr) {
12
13
  target.write(`[${step}/${TOTAL_STEPS}] ${label}... ${status}\n`);
@@ -20,7 +21,7 @@ export function runPipeline(config, seams = {}) {
20
21
  const readFile = seams.readFile ?? ((p) => readFileSync(p, "utf8"));
21
22
  const writeFile = seams.writeFile ?? ((p, c) => writeFileSync(p, c, "utf8"));
22
23
  const fileExists = seams.fileExists ?? ((p) => existsSync(p));
23
- const runCiFn = seams.runCi ?? ((root) => runCi(root, scriptsDir, seams));
24
+ const runCiFn = seams.runCi ?? ((root) => runReleaseCheck(root));
24
25
  const refreshRoadmapFn = seams.refreshRoadmap ?? ((root) => refreshRoadmap(root, scriptsDir, seams));
25
26
  const checkVbriefFn = seams.checkVbriefLifecycleSync ??
26
27
  ((root, repo) => checkVbriefLifecycleSync(root, repo, scriptsDir, seams));
@@ -100,7 +101,11 @@ export function runPipeline(config, seams = {}) {
100
101
  return EXIT_VIOLATION;
101
102
  }
102
103
  }
103
- // Step 5: CI.
104
+ // Step 5: CI pre-flight. The functional path now invokes the native
105
+ // TypeScript `task check` (via `runReleaseCheck`, #2022 Phase 1) instead of
106
+ // the ci_local.py bridge, but the emitted label/dry-run text is kept
107
+ // byte-identical to the Python oracle (scripts/release.py) so the #1729
108
+ // golden-diff release-parity gate stays green until the oracle is retired.
104
109
  label = "Pre-flight CI (task ci:local | fallback task check)";
105
110
  if (config.skipCi) {
106
111
  emit(5, label, "SKIP (--skip-ci)");
@@ -0,0 +1,27 @@
1
+ /**
2
+ * preflight.ts -- Native TypeScript release Step-5 pre-flight (#2022 Phase 1).
3
+ *
4
+ * Promotes the context-aware `task check` orchestrator (check/orchestrator.ts)
5
+ * to the primary release pre-flight, replacing the removed `ci_local.py`
6
+ * python-bridge shim. The maintainer release runs in the framework-source
7
+ * context, so the framework root equals the project root and the orchestrator
8
+ * dispatches `check:framework-source`.
9
+ */
10
+ import { type CheckOrchestratorSeams } from "../check/orchestrator.js";
11
+ /** Seams for test isolation of the native release pre-flight. */
12
+ export interface ReleasePreflightSeams {
13
+ /** Override the check dispatcher (default: dispatchTaskCheck from check/orchestrator). */
14
+ readonly dispatchCheck?: (frameworkRoot: string, projectRoot: string, seams?: CheckOrchestratorSeams) => number;
15
+ /** Seams forwarded to the underlying check orchestrator (e.g. taskBin, spawnFn). */
16
+ readonly checkSeams?: CheckOrchestratorSeams;
17
+ }
18
+ /**
19
+ * Run the native TypeScript `task check` as the release pre-flight.
20
+ *
21
+ * Returns the pipeline's standard `[ok, message]` tuple. The maintainer release
22
+ * cuts the framework itself, so we pass `projectRoot` as both the framework root
23
+ * and the project root -- the orchestrator then resolves the framework-source
24
+ * context and runs `check:framework-source`.
25
+ */
26
+ export declare function runReleaseCheck(projectRoot: string, seams?: ReleasePreflightSeams): [boolean, string];
27
+ //# sourceMappingURL=preflight.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * preflight.ts -- Native TypeScript release Step-5 pre-flight (#2022 Phase 1).
3
+ *
4
+ * Promotes the context-aware `task check` orchestrator (check/orchestrator.ts)
5
+ * to the primary release pre-flight, replacing the removed `ci_local.py`
6
+ * python-bridge shim. The maintainer release runs in the framework-source
7
+ * context, so the framework root equals the project root and the orchestrator
8
+ * dispatches `check:framework-source`.
9
+ */
10
+ import { dispatchTaskCheck } from "../check/orchestrator.js";
11
+ /**
12
+ * Run the native TypeScript `task check` as the release pre-flight.
13
+ *
14
+ * Returns the pipeline's standard `[ok, message]` tuple. The maintainer release
15
+ * cuts the framework itself, so we pass `projectRoot` as both the framework root
16
+ * and the project root -- the orchestrator then resolves the framework-source
17
+ * context and runs `check:framework-source`.
18
+ */
19
+ export function runReleaseCheck(projectRoot, seams = {}) {
20
+ const dispatch = seams.dispatchCheck ?? dispatchTaskCheck;
21
+ const code = dispatch(projectRoot, projectRoot, seams.checkSeams);
22
+ if (code === 0) {
23
+ return [true, "ran native TypeScript task check"];
24
+ }
25
+ return [false, `task check failed (exit ${code})`];
26
+ }
27
+ //# sourceMappingURL=preflight.js.map
@@ -1,7 +1,6 @@
1
1
  import type { ReleaseSeams } from "./types.js";
2
- export declare function runCi(projectRoot: string, scriptsDir: string, seams?: ReleaseSeams): [boolean, string];
3
2
  export declare function refreshRoadmap(projectRoot: string, scriptsDir: string, seams?: ReleaseSeams): [boolean, string];
4
3
  export declare function checkVbriefLifecycleSync(projectRoot: string, repo: string, scriptsDir: string, seams?: ReleaseSeams): [boolean, number, string];
5
4
  export declare function runBuild(projectRoot: string, scriptsDir: string, version: string | null, seams?: ReleaseSeams): [boolean, string];
6
5
  export declare function runUvLock(projectRoot: string, seams?: ReleaseSeams): [boolean, string];
7
- //# sourceMappingURL=python-bridge.d.ts.map
6
+ //# sourceMappingURL=python-steps.d.ts.map
@@ -1,3 +1,15 @@
1
+ /**
2
+ * python-steps.ts -- Residual Python-backed release pipeline steps.
3
+ *
4
+ * #2022 Phase 1 removed the `ci_local.py` Step-5 pre-flight bridge (now native
5
+ * TypeScript via release/preflight.ts), retiring the former `python-bridge.ts`
6
+ * module. These remaining steps still shell into bundled Python and stay here
7
+ * until their own purge phases land:
8
+ * - refreshRoadmap (Step 7) -> scripts/roadmap_render.py
9
+ * - checkVbriefLifecycleSync (Step 3) -> scripts/reconcile_issues.py
10
+ * - runBuild (Step 8) -> scripts/build_dist.py (Bucket B, deleted by #1860)
11
+ * - runUvLock (Step 6) -> `uv lock`
12
+ */
1
13
  import { existsSync, statSync } from "node:fs";
2
14
  import { join } from "node:path";
3
15
  import { defaultWhich, spawnText } from "./spawn.js";
@@ -9,19 +21,6 @@ function runUvPython(_scriptsDir, code, cwd, env = process.env, seams = {}) {
9
21
  timeoutMs: 300_000,
10
22
  });
11
23
  }
12
- export function runCi(projectRoot, scriptsDir, seams = {}) {
13
- const code = [
14
- "import sys",
15
- `sys.path.insert(0, ${JSON.stringify(scriptsDir)})`,
16
- "import ci_local",
17
- `sys.exit(ci_local.main(['--root', ${JSON.stringify(projectRoot)}]))`,
18
- ].join("\n");
19
- const result = runUvPython(scriptsDir, code, scriptsDir, process.env, seams);
20
- if (result.status !== 0) {
21
- return [false, `ci:local failed (exit ${result.status})`];
22
- }
23
- return [true, "ran ci:local"];
24
- }
25
24
  export function refreshRoadmap(projectRoot, scriptsDir, seams = {}) {
26
25
  const pending = join(projectRoot, "vbrief", "pending");
27
26
  const roadmap = join(projectRoot, "ROADMAP.md");
@@ -141,4 +140,4 @@ export function runUvLock(projectRoot, seams = {}) {
141
140
  }
142
141
  return [true, "uv.lock regenerated"];
143
142
  }
144
- //# sourceMappingURL=python-bridge.js.map
143
+ //# sourceMappingURL=python-steps.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=greenfield-python-free-smoke-cli.d.ts.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
@@ -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";
@@ -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 {};