@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,96 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { PRODUCT_NARRATIVE_KEYS } from "./constants.js";
|
|
4
|
+
const LIFECYCLE_SCAN = ["proposed", "pending", "active", "completed", "cancelled"];
|
|
5
|
+
function loadJson(path) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function planNarrativeKeys(doc) {
|
|
14
|
+
const keys = new Set();
|
|
15
|
+
const plan = doc.plan;
|
|
16
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan))
|
|
17
|
+
return keys;
|
|
18
|
+
const narratives = plan.narratives;
|
|
19
|
+
if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives))
|
|
20
|
+
return keys;
|
|
21
|
+
for (const [key, val] of Object.entries(narratives)) {
|
|
22
|
+
if (typeof val === "string" && val.trim())
|
|
23
|
+
keys.add(key.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
return keys;
|
|
26
|
+
}
|
|
27
|
+
function collectCanonicalNarrativeKeys(vbriefDir) {
|
|
28
|
+
const keys = new Set();
|
|
29
|
+
const pdPath = join(vbriefDir, "PROJECT-DEFINITION.vbrief.json");
|
|
30
|
+
const pd = loadJson(pdPath);
|
|
31
|
+
if (pd) {
|
|
32
|
+
for (const k of planNarrativeKeys(pd))
|
|
33
|
+
keys.add(k);
|
|
34
|
+
}
|
|
35
|
+
for (const folder of LIFECYCLE_SCAN) {
|
|
36
|
+
const dir = join(vbriefDir, folder);
|
|
37
|
+
if (!existsSync(dir))
|
|
38
|
+
continue;
|
|
39
|
+
let names;
|
|
40
|
+
try {
|
|
41
|
+
names = readdirSync(dir).filter((n) => n.endsWith(".vbrief.json"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const doc = loadJson(join(dir, name));
|
|
48
|
+
if (doc) {
|
|
49
|
+
for (const k of planNarrativeKeys(doc))
|
|
50
|
+
keys.add(k);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return keys;
|
|
55
|
+
}
|
|
56
|
+
function premigrateNarrativeKeys(vbriefDir) {
|
|
57
|
+
const keys = new Set();
|
|
58
|
+
const premigrate = join(vbriefDir, "specification.premigrate.vbrief.json");
|
|
59
|
+
const doc = loadJson(premigrate);
|
|
60
|
+
if (!doc)
|
|
61
|
+
return keys;
|
|
62
|
+
for (const k of planNarrativeKeys(doc))
|
|
63
|
+
keys.add(k);
|
|
64
|
+
return keys;
|
|
65
|
+
}
|
|
66
|
+
function isProductKey(keyLower) {
|
|
67
|
+
if (keyLower === "overview")
|
|
68
|
+
return false;
|
|
69
|
+
return PRODUCT_NARRATIVE_KEYS.some((p) => p.toLowerCase() === keyLower);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* #2005 migration-fidelity: when specification.vbrief.json is absent but a
|
|
73
|
+
* premigrate snapshot exists, require product narratives to land in PD or scopes.
|
|
74
|
+
*/
|
|
75
|
+
export function checkSpecMigrationFidelity(projectRoot) {
|
|
76
|
+
const vbriefDir = join(projectRoot, "vbrief");
|
|
77
|
+
const specPath = join(vbriefDir, "specification.vbrief.json");
|
|
78
|
+
if (existsSync(specPath))
|
|
79
|
+
return [];
|
|
80
|
+
const premigratePath = join(vbriefDir, "specification.premigrate.vbrief.json");
|
|
81
|
+
if (!existsSync(premigratePath))
|
|
82
|
+
return [];
|
|
83
|
+
const premigrateKeys = premigrateNarrativeKeys(vbriefDir);
|
|
84
|
+
const productPremigrate = [...premigrateKeys].filter(isProductKey);
|
|
85
|
+
if (productPremigrate.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const canonicalKeys = collectCanonicalNarrativeKeys(vbriefDir);
|
|
88
|
+
const missing = productPremigrate.filter((k) => !canonicalKeys.has(k));
|
|
89
|
+
if (missing.length === 0)
|
|
90
|
+
return [];
|
|
91
|
+
return [
|
|
92
|
+
`vbrief/specification.vbrief.json is absent but vbrief/specification.premigrate.vbrief.json retains product narratives not found in PROJECT-DEFINITION or lifecycle scope vBRIEFs: ${missing.join(", ")}. ` +
|
|
93
|
+
"Do not delete the legacy spec without migrating content. Run `task migrate:vbrief` or manually merge narratives into PROJECT-DEFINITION / scope vBRIEFs before removing the premigrate snapshot. See #2005.",
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=migration-fidelity.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ResolvedSpecAuthority } from "./resolver.js";
|
|
2
|
+
/** Merge product narratives per Wave 0 locked precedence. */
|
|
3
|
+
export declare function resolveExportNarratives(authority: ResolvedSpecAuthority): Record<string, string>;
|
|
4
|
+
export declare function renderNarrativeSections(narratives: Record<string, string>): string[];
|
|
5
|
+
export declare function greenfieldOverviewNonEmpty(authority: ResolvedSpecAuthority): boolean;
|
|
6
|
+
//# sourceMappingURL=narratives.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { SPECIFICATION_NARRATIVE_KEY_ORDER } from "../render/constants.js";
|
|
3
|
+
import { STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS } from "./constants.js";
|
|
4
|
+
function loadJson(path) {
|
|
5
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
6
|
+
}
|
|
7
|
+
function planNarratives(doc) {
|
|
8
|
+
const plan = doc.plan;
|
|
9
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan))
|
|
10
|
+
return {};
|
|
11
|
+
const narratives = plan.narratives;
|
|
12
|
+
if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives))
|
|
13
|
+
return {};
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const [key, val] of Object.entries(narratives)) {
|
|
16
|
+
if (typeof val === "string")
|
|
17
|
+
out[key] = val;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function isExcludedNarrativeKey(key) {
|
|
22
|
+
return STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS.has(key.toLowerCase().replace(/\s+/g, ""));
|
|
23
|
+
}
|
|
24
|
+
/** Merge product narratives per Wave 0 locked precedence. */
|
|
25
|
+
export function resolveExportNarratives(authority) {
|
|
26
|
+
const pd = loadJson(authority.projectDefPath);
|
|
27
|
+
const pdNarratives = planNarratives(pd);
|
|
28
|
+
if (authority.kind === "greenfield") {
|
|
29
|
+
const filtered = {};
|
|
30
|
+
for (const [key, val] of Object.entries(pdNarratives)) {
|
|
31
|
+
if (!isExcludedNarrativeKey(key) && val.trim())
|
|
32
|
+
filtered[key] = val;
|
|
33
|
+
}
|
|
34
|
+
return filtered;
|
|
35
|
+
}
|
|
36
|
+
const specPath = authority.specPath;
|
|
37
|
+
if (!specPath)
|
|
38
|
+
return pdNarratives;
|
|
39
|
+
const spec = loadJson(specPath);
|
|
40
|
+
const specNarratives = planNarratives(spec);
|
|
41
|
+
const merged = {};
|
|
42
|
+
for (const [key, val] of Object.entries(specNarratives)) {
|
|
43
|
+
if (!isExcludedNarrativeKey(key) && val.trim())
|
|
44
|
+
merged[key] = val;
|
|
45
|
+
}
|
|
46
|
+
// PD identity additive when missing from spec.
|
|
47
|
+
const identityHints = ["overview", "architecture", "risksandunknowns", "risks", "unknowns"];
|
|
48
|
+
for (const [key, val] of Object.entries(pdNarratives)) {
|
|
49
|
+
if (!val.trim() || isExcludedNarrativeKey(key))
|
|
50
|
+
continue;
|
|
51
|
+
const lower = key.toLowerCase().replace(/\s+/g, "");
|
|
52
|
+
const isIdentity = identityHints.some((h) => lower.includes(h)) || lower === "overview";
|
|
53
|
+
if (!isIdentity)
|
|
54
|
+
continue;
|
|
55
|
+
const specHasKey = Object.keys(merged).some((k) => k.toLowerCase() === lower);
|
|
56
|
+
if (!specHasKey)
|
|
57
|
+
merged[key] = val;
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
61
|
+
export function renderNarrativeSections(narratives) {
|
|
62
|
+
const lines = [];
|
|
63
|
+
const rendered = new Set();
|
|
64
|
+
for (const key of SPECIFICATION_NARRATIVE_KEY_ORDER) {
|
|
65
|
+
const val = narratives[key];
|
|
66
|
+
if (val?.trim()) {
|
|
67
|
+
lines.push(`## ${key}\n`, `${val}\n`);
|
|
68
|
+
rendered.add(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const key of Object.keys(narratives).sort()) {
|
|
72
|
+
if (rendered.has(key) || !narratives[key]?.trim())
|
|
73
|
+
continue;
|
|
74
|
+
lines.push(`## ${key}\n`, `${narratives[key]}\n`);
|
|
75
|
+
}
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
export function greenfieldOverviewNonEmpty(authority) {
|
|
79
|
+
const pd = loadJson(authority.projectDefPath);
|
|
80
|
+
const narratives = planNarratives(pd);
|
|
81
|
+
const overview = Object.entries(narratives).find(([k]) => k.toLowerCase() === "overview");
|
|
82
|
+
return overview !== undefined && overview[1].trim().length > 0;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=narratives.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SpecAuthorityKind = "full-spec" | "greenfield";
|
|
2
|
+
export interface ResolvedSpecAuthority {
|
|
3
|
+
readonly kind: SpecAuthorityKind;
|
|
4
|
+
readonly projectRoot: string;
|
|
5
|
+
readonly vbriefDir: string;
|
|
6
|
+
readonly projectDefPath: string;
|
|
7
|
+
readonly specPath: string | null;
|
|
8
|
+
readonly banner: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveSpecAuthority(projectRoot: string): ResolvedSpecAuthority | null;
|
|
11
|
+
export declare function readSpecMarkdown(projectRoot: string): string;
|
|
12
|
+
export declare function isFullSpecState(projectRoot: string): boolean;
|
|
13
|
+
export declare function isGreenfieldSpecExport(projectRoot: string): boolean;
|
|
14
|
+
export declare function isCurrentGeneratedSpecification(projectRoot: string): boolean;
|
|
15
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SPEC_RENDER_BANNER } from "../render/constants.js";
|
|
4
|
+
import { EXPORT_SPEC_PD_BANNER, GENERATED_SPEC_PURPOSE, GENERATED_SPEC_SOURCE_PD, GENERATED_SPEC_SOURCE_SPEC, } from "./constants.js";
|
|
5
|
+
const SPEC_REL = join("vbrief", "specification.vbrief.json");
|
|
6
|
+
const PD_REL = join("vbrief", "PROJECT-DEFINITION.vbrief.json");
|
|
7
|
+
export function resolveSpecAuthority(projectRoot) {
|
|
8
|
+
const root = projectRoot;
|
|
9
|
+
const vbriefDir = join(root, "vbrief");
|
|
10
|
+
const projectDefPath = join(root, PD_REL);
|
|
11
|
+
if (!existsSync(projectDefPath))
|
|
12
|
+
return null;
|
|
13
|
+
const specPath = join(root, SPEC_REL);
|
|
14
|
+
const hasSpec = existsSync(specPath);
|
|
15
|
+
const kind = hasSpec ? "full-spec" : "greenfield";
|
|
16
|
+
const banner = kind === "full-spec" ? SPEC_RENDER_BANNER : EXPORT_SPEC_PD_BANNER;
|
|
17
|
+
return {
|
|
18
|
+
kind,
|
|
19
|
+
projectRoot: root,
|
|
20
|
+
vbriefDir,
|
|
21
|
+
projectDefPath,
|
|
22
|
+
specPath: hasSpec ? specPath : null,
|
|
23
|
+
banner,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function readSpecMarkdown(projectRoot) {
|
|
27
|
+
const path = join(projectRoot, "SPECIFICATION.md");
|
|
28
|
+
try {
|
|
29
|
+
return readFileSync(path, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function isFullSpecState(projectRoot) {
|
|
36
|
+
const authority = resolveSpecAuthority(projectRoot);
|
|
37
|
+
if (authority?.kind !== "full-spec")
|
|
38
|
+
return false;
|
|
39
|
+
const specMd = readSpecMarkdown(projectRoot);
|
|
40
|
+
return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd.includes(GENERATED_SPEC_SOURCE_SPEC);
|
|
41
|
+
}
|
|
42
|
+
export function isGreenfieldSpecExport(projectRoot) {
|
|
43
|
+
const authority = resolveSpecAuthority(projectRoot);
|
|
44
|
+
if (authority?.kind !== "greenfield")
|
|
45
|
+
return false;
|
|
46
|
+
const specMd = readSpecMarkdown(projectRoot);
|
|
47
|
+
return specMd.includes(GENERATED_SPEC_PURPOSE) && specMd.includes(GENERATED_SPEC_SOURCE_PD);
|
|
48
|
+
}
|
|
49
|
+
export function isCurrentGeneratedSpecification(projectRoot) {
|
|
50
|
+
return isFullSpecState(projectRoot) || isGreenfieldSpecExport(projectRoot);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -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
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
const UNRELEASED_RE = /## \[Unreleased\][ \t]*\n([\s\S]*?)(?=\n## \[|$)/;
|
|
5
|
+
const CHANGE_NAME_RE = /^[\w][\w-]*$/;
|
|
6
|
+
const COMMIT_TYPES = "feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert";
|
|
7
|
+
const COMMIT_SUBJECT_RE = new RegExp(`^(${COMMIT_TYPES})(\\(.+\\))?!?: .+`);
|
|
8
|
+
function proposalTemplate(name) {
|
|
9
|
+
return {
|
|
10
|
+
vBRIEFInfo: { version: "0.5" },
|
|
11
|
+
plan: {
|
|
12
|
+
title: name,
|
|
13
|
+
status: "draft",
|
|
14
|
+
narratives: {
|
|
15
|
+
Problem: "What is wrong or missing.",
|
|
16
|
+
Change: "What this proposal does about it.",
|
|
17
|
+
Scope: "In scope: ... Out of scope: ...",
|
|
18
|
+
Impact: "What existing code/specs are affected.",
|
|
19
|
+
Risks: "What could go wrong.",
|
|
20
|
+
Approach: "How to implement the change.",
|
|
21
|
+
Alternatives: "What else was considered and why not.",
|
|
22
|
+
Dependencies: "What must exist before this works.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function tasksTemplate(name) {
|
|
28
|
+
return {
|
|
29
|
+
vBRIEFInfo: { version: "0.5" },
|
|
30
|
+
plan: { title: name, status: "draft", items: [], edges: [] },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Port of ``task change:changelog:check`` inline Python (#2022 Phase 2). */
|
|
34
|
+
export function runChangelogCheck(projectRoot, io) {
|
|
35
|
+
const path = join(resolve(projectRoot), "CHANGELOG.md");
|
|
36
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
37
|
+
io.writeOut("FAIL: CHANGELOG.md not found\n");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
const text = readFileSync(path, "utf8");
|
|
41
|
+
const match = UNRELEASED_RE.exec(text);
|
|
42
|
+
if (match === null) {
|
|
43
|
+
io.writeOut("FAIL: No [Unreleased] section found in CHANGELOG.md\n");
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const body = match[1] ?? "";
|
|
47
|
+
const entries = body.split("\n").filter((line) => line.trimStart().startsWith("- "));
|
|
48
|
+
if (entries.length === 0) {
|
|
49
|
+
io.writeOut('FAIL: [Unreleased] section has no entries (no lines starting with "- ")\n');
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
io.writeOut(`OK: CHANGELOG.md [Unreleased] section has ${entries.length} entries\n`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
/** Port of ``task change:init`` inline Python (#2022 Phase 2). */
|
|
56
|
+
export function runChangeInit(projectRoot, name, io) {
|
|
57
|
+
const trimmed = name.trim();
|
|
58
|
+
if (trimmed.length === 0) {
|
|
59
|
+
io.writeOut("FAIL: Usage: task change:init -- <name>\n");
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
if (!CHANGE_NAME_RE.test(trimmed)) {
|
|
63
|
+
io.writeOut("FAIL: Name must contain only alphanumeric characters, underscores, and hyphens\n");
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const base = join(resolve(projectRoot), "history", "changes", trimmed);
|
|
67
|
+
if (existsSync(base)) {
|
|
68
|
+
io.writeOut(`FAIL: ${base} already exists\n`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
mkdirSync(join(base, "specs"), { recursive: true });
|
|
72
|
+
writeFileSync(join(base, "proposal.vbrief.json"), `${JSON.stringify(proposalTemplate(trimmed), null, 2)}\n`, "utf8");
|
|
73
|
+
writeFileSync(join(base, "tasks.vbrief.json"), `${JSON.stringify(tasksTemplate(trimmed), null, 2)}\n`, "utf8");
|
|
74
|
+
io.writeOut(`OK: Created change proposal at ${base}/\n`);
|
|
75
|
+
for (const file of ["proposal.vbrief.json", "tasks.vbrief.json", "specs/"]) {
|
|
76
|
+
io.writeOut(` - ${file}\n`);
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
/** Port of ``task commit:lint`` inline Python (#2022 Phase 2). */
|
|
81
|
+
export function runCommitLint(projectRoot, io) {
|
|
82
|
+
let stdout;
|
|
83
|
+
try {
|
|
84
|
+
stdout = execFileSync("git", ["log", "--format=%B", "-1"], {
|
|
85
|
+
cwd: resolve(projectRoot),
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
io.writeOut("FAIL: Could not read HEAD commit message\n");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
const msg = stdout.trim();
|
|
94
|
+
const subject = msg.split("\n")[0] ?? "";
|
|
95
|
+
if (!COMMIT_SUBJECT_RE.test(subject)) {
|
|
96
|
+
io.writeOut("FAIL: Commit message does not match conventional commit format\n");
|
|
97
|
+
io.writeOut(` Got: ${subject}\n`);
|
|
98
|
+
io.writeOut(" Expected: type(scope): description\n");
|
|
99
|
+
io.writeOut(" Types: feat, fix, docs, chore, refactor, test, style, perf, ci, build, revert\n");
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
io.writeOut("OK: Commit message is valid conventional commit\n");
|
|
103
|
+
io.writeOut(` Subject: ${subject}\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
/** Port of ``task install:uninstall`` inline Python (#2022 Phase 2). */
|
|
107
|
+
export function runInstallUninstall(projectRoot, io) {
|
|
108
|
+
const path = join(resolve(projectRoot), "AGENTS.md");
|
|
109
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
110
|
+
io.writeOut("No deft entry found in AGENTS.md\n");
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const original = readFileSync(path, "utf8");
|
|
114
|
+
const lines = original.split(/(?<=\n)/);
|
|
115
|
+
const filtered = lines.filter((line) => !line.startsWith("See deft/main.md") && !line.startsWith("Skills: deft/skills/"));
|
|
116
|
+
const next = filtered.join("");
|
|
117
|
+
if (next !== original) {
|
|
118
|
+
writeFileSync(path, next, "utf8");
|
|
119
|
+
io.writeOut("Removed deft entry from AGENTS.md\n");
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
io.writeOut("No deft entry found in AGENTS.md\n");
|
|
123
|
+
}
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { checkSpecMigrationFidelity } from "../spec-authority/migration-fidelity.js";
|
|
4
|
+
import { isFullSpecState } from "../spec-authority/resolver.js";
|
|
3
5
|
import { isDatePrefixedVbriefFilename } from "./filename.js";
|
|
4
|
-
const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
5
|
-
const GENERATED_SPEC_SOURCE = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
|
|
6
6
|
const LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
|
|
7
|
-
function readTextSafe(path) {
|
|
8
|
-
try {
|
|
9
|
-
return readFileSync(path, "utf8");
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return "";
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
7
|
function isDirSafe(path) {
|
|
16
8
|
try {
|
|
17
9
|
return statSync(path).isDirectory();
|
|
@@ -30,20 +22,6 @@ function isDeftFrameworkRoot(projectRoot) {
|
|
|
30
22
|
(isDirSafe(join(projectRoot, "content", "strategies")) ||
|
|
31
23
|
isDirSafe(join(projectRoot, "strategies"))));
|
|
32
24
|
}
|
|
33
|
-
function hasCompleteLifecycle(vbriefDir) {
|
|
34
|
-
return LIFECYCLE_DIRS.every((folder) => {
|
|
35
|
-
const p = join(vbriefDir, folder);
|
|
36
|
-
return existsSync(p) && statSync(p).isDirectory();
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function isPostCutoverFullSpecState(projectRoot) {
|
|
40
|
-
const vbriefDir = join(projectRoot, "vbrief");
|
|
41
|
-
const specMd = readTextSafe(join(projectRoot, "SPECIFICATION.md"));
|
|
42
|
-
return (existsSync(join(vbriefDir, "PROJECT-DEFINITION.vbrief.json")) &&
|
|
43
|
-
hasCompleteLifecycle(vbriefDir) &&
|
|
44
|
-
specMd.includes(GENERATED_SPEC_PURPOSE) &&
|
|
45
|
-
specMd.includes(GENERATED_SPEC_SOURCE));
|
|
46
|
-
}
|
|
47
25
|
/** Pure validator returning error strings (empty == pass). */
|
|
48
26
|
export function validateStrategyOutput(projectRoot, strict = false) {
|
|
49
27
|
const root = resolve(projectRoot);
|
|
@@ -63,7 +41,7 @@ export function validateStrategyOutput(projectRoot, strict = false) {
|
|
|
63
41
|
"(see v0-20-contract.md and task project:render).");
|
|
64
42
|
}
|
|
65
43
|
const specLegacy = join(vbriefDir, "specification.vbrief.json");
|
|
66
|
-
if (existsSync(specLegacy) && !isDeftFrameworkRoot(root) && !
|
|
44
|
+
if (existsSync(specLegacy) && !isDeftFrameworkRoot(root) && !isFullSpecState(root)) {
|
|
67
45
|
errors.push("Legacy artifact vbrief/specification.vbrief.json present. " +
|
|
68
46
|
"v0.20 strategies MUST NOT dual-write the old specification.vbrief.json " +
|
|
69
47
|
"alongside scope vBRIEFs in the lifecycle folders. " +
|
|
@@ -85,6 +63,7 @@ export function validateStrategyOutput(projectRoot, strict = false) {
|
|
|
85
63
|
}
|
|
86
64
|
}
|
|
87
65
|
}
|
|
66
|
+
errors.push(...checkSpecMigrationFidelity(root));
|
|
88
67
|
return errors;
|
|
89
68
|
}
|
|
90
69
|
const FAILURE_HEADER = "❌ Strategy output shape validation FAILED (v0.20 contract gate)";
|
|
@@ -122,6 +122,11 @@ export function cmdVbriefValidate(argv) {
|
|
|
122
122
|
if (argv[0] === "conformance") {
|
|
123
123
|
return runConformance(argv.slice(1));
|
|
124
124
|
}
|
|
125
|
+
if (argv.includes("--staged") ||
|
|
126
|
+
(argv.includes("--all") &&
|
|
127
|
+
!argv.some((a) => a === "--vbrief-dir" || a.startsWith("--vbrief-dir=")))) {
|
|
128
|
+
return runConformance(argv);
|
|
129
|
+
}
|
|
125
130
|
return runValidate(argv);
|
|
126
131
|
}
|
|
127
132
|
//# sourceMappingURL=main.js.map
|
|
@@ -1,31 +1,21 @@
|
|
|
1
|
+
import { isFullSpecState, isGreenfieldSpecExport } from "../spec-authority/resolver.js";
|
|
1
2
|
import { DEPRECATION_SENTINEL } from "../vbrief-build/constants.js";
|
|
2
3
|
export { DEPRECATION_SENTINEL as DEPRECATED_REDIRECT_SENTINEL };
|
|
4
|
+
export declare function missingLifecycleFolders(projectRoot: string): string[];
|
|
3
5
|
/** Return true when markdown content is a migration redirect stub. */
|
|
4
6
|
export declare function isDeprecationRedirect(content: string): boolean;
|
|
5
|
-
/**
|
|
7
|
+
/** Full-spec generated export (specification.vbrief.json source line). */
|
|
8
|
+
export declare function isGeneratedSpecificationExport(projectRoot: string, content: string): boolean;
|
|
9
|
+
/** Return true for a fully current generated spec export (full-spec or greenfield). */
|
|
6
10
|
export declare function isCurrentGeneratedSpecification(projectRoot: string, content: string): boolean;
|
|
11
|
+
/** Return root artifact filenames that are legacy pre-v0.20 inputs (#793 / migrate preflight). */
|
|
12
|
+
export declare function detectPreCutoverLegacy(projectRoot: string): string[];
|
|
7
13
|
/** Structured result of a pre-cutover (pre-v0.20 document model) probe. */
|
|
8
14
|
export interface PrecutoverDetection {
|
|
9
|
-
/** True when any legacy pre-v0.20 artifact still needs migration. */
|
|
10
15
|
preCutover: boolean;
|
|
11
|
-
/** Human-readable reasons; empty when the project is on the current model. */
|
|
12
16
|
reasons: string[];
|
|
13
17
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Detect whether ``projectRoot`` still carries pre-v0.20 (pre-cutover) document-model
|
|
16
|
-
* artifacts that need migration. Mirrors the AGENTS.md "Pre-Cutover Check" rule and the
|
|
17
|
-
* legacy ``scripts/_precutover.py`` helper: a project is pre-cutover when any of the
|
|
18
|
-
* following hold:
|
|
19
|
-
*
|
|
20
|
-
* - ``SPECIFICATION.md`` exists and is neither a deprecation redirect nor a current
|
|
21
|
-
* generated spec export;
|
|
22
|
-
* - ``PROJECT.md`` exists and is not a deprecation redirect;
|
|
23
|
-
* - ``vbrief/`` exists but is missing one or more lifecycle folders.
|
|
24
|
-
*/
|
|
25
18
|
export declare function detectPreCutover(projectRoot: string): PrecutoverDetection;
|
|
26
|
-
/**
|
|
27
|
-
* Render the single-line pre-cutover status string surfaced by ``deft doctor``.
|
|
28
|
-
* Flags the migration-needed state with reasons, or reports a clean current-model state.
|
|
29
|
-
*/
|
|
30
19
|
export declare function renderPrecutoverLine(projectRoot: string): string;
|
|
20
|
+
export { isFullSpecState, isGreenfieldSpecExport };
|
|
31
21
|
//# sourceMappingURL=precutover.d.ts.map
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { GENERATED_SPEC_PURPOSE, GENERATED_SPEC_SOURCE_SPEC } from "../spec-authority/constants.js";
|
|
4
|
+
import { isFullSpecState, isGreenfieldSpecExport } from "../spec-authority/resolver.js";
|
|
3
5
|
import { DEPRECATION_SENTINEL } from "../vbrief-build/constants.js";
|
|
4
6
|
export { DEPRECATION_SENTINEL as DEPRECATED_REDIRECT_SENTINEL };
|
|
5
7
|
const DEPRECATION_REDIRECT_PURPOSE = "<!-- Purpose: deprecation redirect -->";
|
|
6
|
-
const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
7
|
-
const GENERATED_SPEC_SOURCE = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
|
|
8
8
|
const SPEC_SOURCE_RELPATH = join("vbrief", "specification.vbrief.json");
|
|
9
9
|
const LIFECYCLE_FOLDERS = ["proposed", "pending", "active", "completed", "cancelled"];
|
|
10
|
-
function missingLifecycleFolders(projectRoot) {
|
|
10
|
+
export function missingLifecycleFolders(projectRoot) {
|
|
11
11
|
const vbriefRoot = join(projectRoot, "vbrief");
|
|
12
12
|
return LIFECYCLE_FOLDERS.filter((folder) => !existsSync(join(vbriefRoot, folder)));
|
|
13
13
|
}
|
|
@@ -18,14 +18,19 @@ function hasCompleteLifecycle(projectRoot) {
|
|
|
18
18
|
export function isDeprecationRedirect(content) {
|
|
19
19
|
return content.includes(DEPRECATION_SENTINEL) || content.includes(DEPRECATION_REDIRECT_PURPOSE);
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
/** Full-spec generated export (specification.vbrief.json source line). */
|
|
22
|
+
export function isGeneratedSpecificationExport(projectRoot, content) {
|
|
22
23
|
return (content.includes(GENERATED_SPEC_PURPOSE) &&
|
|
23
|
-
content.includes(
|
|
24
|
+
content.includes(GENERATED_SPEC_SOURCE_SPEC) &&
|
|
24
25
|
existsSync(join(projectRoot, SPEC_SOURCE_RELPATH)));
|
|
25
26
|
}
|
|
26
|
-
/** Return true for a fully current
|
|
27
|
+
/** Return true for a fully current generated spec export (full-spec or greenfield). */
|
|
27
28
|
export function isCurrentGeneratedSpecification(projectRoot, content) {
|
|
28
|
-
|
|
29
|
+
if (!hasCompleteLifecycle(projectRoot))
|
|
30
|
+
return false;
|
|
31
|
+
if (isGeneratedSpecificationExport(projectRoot, content))
|
|
32
|
+
return true;
|
|
33
|
+
return isGreenfieldSpecExport(projectRoot);
|
|
29
34
|
}
|
|
30
35
|
function isFile(path) {
|
|
31
36
|
try {
|
|
@@ -43,17 +48,31 @@ function safeReadText(path) {
|
|
|
43
48
|
return "";
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
function rootMarkdownIsLegacy(projectRoot, filename, content) {
|
|
52
|
+
if (isDeprecationRedirect(content))
|
|
53
|
+
return false;
|
|
54
|
+
if (filename === "SPECIFICATION.md") {
|
|
55
|
+
if (isGeneratedSpecificationExport(projectRoot, content))
|
|
56
|
+
return false;
|
|
57
|
+
if (isGreenfieldSpecExport(projectRoot))
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return filename === "SPECIFICATION.md" || filename === "PROJECT.md";
|
|
61
|
+
}
|
|
62
|
+
/** Return root artifact filenames that are legacy pre-v0.20 inputs (#793 / migrate preflight). */
|
|
63
|
+
export function detectPreCutoverLegacy(projectRoot) {
|
|
64
|
+
const legacy = [];
|
|
65
|
+
for (const filename of ["SPECIFICATION.md", "PROJECT.md"]) {
|
|
66
|
+
const candidate = join(projectRoot, filename);
|
|
67
|
+
if (!isFile(candidate))
|
|
68
|
+
continue;
|
|
69
|
+
const content = safeReadText(candidate);
|
|
70
|
+
if (rootMarkdownIsLegacy(projectRoot, filename, content)) {
|
|
71
|
+
legacy.push(filename);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return legacy;
|
|
75
|
+
}
|
|
57
76
|
export function detectPreCutover(projectRoot) {
|
|
58
77
|
const reasons = [];
|
|
59
78
|
const specPath = join(projectRoot, "SPECIFICATION.md");
|
|
@@ -79,17 +98,14 @@ export function detectPreCutover(projectRoot) {
|
|
|
79
98
|
}
|
|
80
99
|
return { preCutover: reasons.length > 0, reasons };
|
|
81
100
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Render the single-line pre-cutover status string surfaced by ``deft doctor``.
|
|
84
|
-
* Flags the migration-needed state with reasons, or reports a clean current-model state.
|
|
85
|
-
*/
|
|
86
101
|
export function renderPrecutoverLine(projectRoot) {
|
|
87
102
|
const { preCutover, reasons } = detectPreCutover(projectRoot);
|
|
88
103
|
if (!preCutover) {
|
|
89
104
|
return "Pre-cutover: none -- project is on the current vBRIEF document model.";
|
|
90
105
|
}
|
|
91
|
-
// Collapse any embedded newlines so the status line stays a single line (CWE-116).
|
|
92
106
|
const summary = reasons.join("; ").replace(/\r?\n/g, " ");
|
|
93
107
|
return `Pre-cutover: migration needed -- ${summary}. Run \`deft migrate:vbrief\` to migrate.`;
|
|
94
108
|
}
|
|
109
|
+
// Re-export for classify callers (#2013).
|
|
110
|
+
export { isFullSpecState, isGreenfieldSpecExport };
|
|
95
111
|
//# sourceMappingURL=precutover.js.map
|
|
@@ -2,6 +2,10 @@ export interface ToolCheck {
|
|
|
2
2
|
readonly name: string;
|
|
3
3
|
readonly command: readonly string[];
|
|
4
4
|
}
|
|
5
|
+
export declare const MAINTAINER_TOOLS: readonly ToolCheck[];
|
|
6
|
+
/** Consumer npm-deposit toolchain (#2022 Phase 3) -- no Python/go/uv maintainer tools. */
|
|
7
|
+
export declare const CONSUMER_TOOLS: readonly ToolCheck[];
|
|
8
|
+
/** @deprecated use MAINTAINER_TOOLS */
|
|
5
9
|
export declare const TOOLS: readonly ToolCheck[];
|
|
6
10
|
export type CommandRunner = (command: readonly string[], timeoutMs: number) => {
|
|
7
11
|
returncode: number;
|
|
@@ -23,6 +27,9 @@ export declare function defaultCommandRunner(command: readonly string[], timeout
|
|
|
23
27
|
error: "not-found" | "exception";
|
|
24
28
|
message: string;
|
|
25
29
|
};
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
export interface ToolchainCheckOptions {
|
|
31
|
+
readonly consumer?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Run maintainer or consumer toolchain probe (mirrors scripts/toolchain-check.py). */
|
|
34
|
+
export declare function runToolchainCheck(runner?: CommandRunner, options?: ToolchainCheckOptions, tools?: readonly ToolCheck[]): ToolchainCheckResult;
|
|
28
35
|
//# sourceMappingURL=toolchain-check.d.ts.map
|