@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.
- package/dist/deposit/python-free.d.ts +21 -0
- package/dist/deposit/python-free.js +149 -0
- package/dist/deposit/resolve-content.d.ts +6 -0
- package/dist/deposit/resolve-content.js +7 -1
- package/dist/doctor/checks.js +2 -2
- 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/index.d.ts +2 -1
- package/dist/release/index.js +2 -1
- package/dist/release/pipeline.js +8 -3
- package/dist/release/preflight.d.ts +27 -0
- package/dist/release/preflight.js +27 -0
- package/dist/release/{python-bridge.d.ts → python-steps.d.ts} +1 -2
- package/dist/release/{python-bridge.js → python-steps.js} +13 -14
- 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 +28 -0
- package/dist/vbrief-validate/precutover.js +90 -3
- package/dist/verify-env/toolchain-check.d.ts +9 -2
- package/dist/verify-env/toolchain-check.js +15 -4
- package/package.json +15 -3
|
@@ -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
|
|
@@ -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,7 +1,35 @@
|
|
|
1
1
|
import { DEPRECATION_SENTINEL } from "../vbrief-build/constants.js";
|
|
2
2
|
export { DEPRECATION_SENTINEL as DEPRECATED_REDIRECT_SENTINEL };
|
|
3
|
+
export declare function missingLifecycleFolders(projectRoot: string): string[];
|
|
3
4
|
/** Return true when markdown content is a migration redirect stub. */
|
|
4
5
|
export declare function isDeprecationRedirect(content: string): boolean;
|
|
6
|
+
export declare function isGeneratedSpecificationExport(projectRoot: string, content: string): boolean;
|
|
5
7
|
/** Return true for a fully current ``task spec:render`` root export. */
|
|
6
8
|
export declare function isCurrentGeneratedSpecification(projectRoot: string, content: string): boolean;
|
|
9
|
+
/** Return root artifact filenames that are legacy pre-v0.20 inputs (#793 / migrate preflight). */
|
|
10
|
+
export declare function detectPreCutoverLegacy(projectRoot: string): string[];
|
|
11
|
+
/** Structured result of a pre-cutover (pre-v0.20 document model) probe. */
|
|
12
|
+
export interface PrecutoverDetection {
|
|
13
|
+
/** True when any legacy pre-v0.20 artifact still needs migration. */
|
|
14
|
+
preCutover: boolean;
|
|
15
|
+
/** Human-readable reasons; empty when the project is on the current model. */
|
|
16
|
+
reasons: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Detect whether ``projectRoot`` still carries pre-v0.20 (pre-cutover) document-model
|
|
20
|
+
* artifacts that need migration. Mirrors the AGENTS.md "Pre-Cutover Check" rule and the
|
|
21
|
+
* legacy ``scripts/_precutover.py`` helper: a project is pre-cutover when any of the
|
|
22
|
+
* following hold:
|
|
23
|
+
*
|
|
24
|
+
* - ``SPECIFICATION.md`` exists and is neither a deprecation redirect nor a current
|
|
25
|
+
* generated spec export;
|
|
26
|
+
* - ``PROJECT.md`` exists and is not a deprecation redirect;
|
|
27
|
+
* - ``vbrief/`` exists but is missing one or more lifecycle folders.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectPreCutover(projectRoot: string): PrecutoverDetection;
|
|
30
|
+
/**
|
|
31
|
+
* Render the single-line pre-cutover status string surfaced by ``deft doctor``.
|
|
32
|
+
* Flags the migration-needed state with reasons, or reports a clean current-model state.
|
|
33
|
+
*/
|
|
34
|
+
export declare function renderPrecutoverLine(projectRoot: string): string;
|
|
7
35
|
//# sourceMappingURL=precutover.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { DEPRECATION_SENTINEL } from "../vbrief-build/constants.js";
|
|
4
4
|
export { DEPRECATION_SENTINEL as DEPRECATED_REDIRECT_SENTINEL };
|
|
@@ -7,7 +7,7 @@ const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
|
7
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,7 +18,7 @@ function hasCompleteLifecycle(projectRoot) {
|
|
|
18
18
|
export function isDeprecationRedirect(content) {
|
|
19
19
|
return content.includes(DEPRECATION_SENTINEL) || content.includes(DEPRECATION_REDIRECT_PURPOSE);
|
|
20
20
|
}
|
|
21
|
-
function isGeneratedSpecificationExport(projectRoot, content) {
|
|
21
|
+
export function isGeneratedSpecificationExport(projectRoot, content) {
|
|
22
22
|
return (content.includes(GENERATED_SPEC_PURPOSE) &&
|
|
23
23
|
content.includes(GENERATED_SPEC_SOURCE) &&
|
|
24
24
|
existsSync(join(projectRoot, SPEC_SOURCE_RELPATH)));
|
|
@@ -27,4 +27,91 @@ function isGeneratedSpecificationExport(projectRoot, content) {
|
|
|
27
27
|
export function isCurrentGeneratedSpecification(projectRoot, content) {
|
|
28
28
|
return isGeneratedSpecificationExport(projectRoot, content) && hasCompleteLifecycle(projectRoot);
|
|
29
29
|
}
|
|
30
|
+
function isFile(path) {
|
|
31
|
+
try {
|
|
32
|
+
return statSync(path).isFile();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function safeReadText(path) {
|
|
39
|
+
try {
|
|
40
|
+
return readFileSync(path, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function rootMarkdownIsLegacy(projectRoot, filename, content) {
|
|
47
|
+
if (isDeprecationRedirect(content))
|
|
48
|
+
return false;
|
|
49
|
+
if (filename === "SPECIFICATION.md" && isGeneratedSpecificationExport(projectRoot, content)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return filename === "SPECIFICATION.md" || filename === "PROJECT.md";
|
|
53
|
+
}
|
|
54
|
+
/** Return root artifact filenames that are legacy pre-v0.20 inputs (#793 / migrate preflight). */
|
|
55
|
+
export function detectPreCutoverLegacy(projectRoot) {
|
|
56
|
+
const legacy = [];
|
|
57
|
+
for (const filename of ["SPECIFICATION.md", "PROJECT.md"]) {
|
|
58
|
+
const candidate = join(projectRoot, filename);
|
|
59
|
+
if (!isFile(candidate))
|
|
60
|
+
continue;
|
|
61
|
+
const content = safeReadText(candidate);
|
|
62
|
+
if (rootMarkdownIsLegacy(projectRoot, filename, content)) {
|
|
63
|
+
legacy.push(filename);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return legacy;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Detect whether ``projectRoot`` still carries pre-v0.20 (pre-cutover) document-model
|
|
70
|
+
* artifacts that need migration. Mirrors the AGENTS.md "Pre-Cutover Check" rule and the
|
|
71
|
+
* legacy ``scripts/_precutover.py`` helper: a project is pre-cutover when any of the
|
|
72
|
+
* following hold:
|
|
73
|
+
*
|
|
74
|
+
* - ``SPECIFICATION.md`` exists and is neither a deprecation redirect nor a current
|
|
75
|
+
* generated spec export;
|
|
76
|
+
* - ``PROJECT.md`` exists and is not a deprecation redirect;
|
|
77
|
+
* - ``vbrief/`` exists but is missing one or more lifecycle folders.
|
|
78
|
+
*/
|
|
79
|
+
export function detectPreCutover(projectRoot) {
|
|
80
|
+
const reasons = [];
|
|
81
|
+
const specPath = join(projectRoot, "SPECIFICATION.md");
|
|
82
|
+
if (isFile(specPath)) {
|
|
83
|
+
const content = safeReadText(specPath);
|
|
84
|
+
if (!isDeprecationRedirect(content) && !isCurrentGeneratedSpecification(projectRoot, content)) {
|
|
85
|
+
reasons.push("SPECIFICATION.md is a pre-v0.20 hand-authored doc (not a deprecation redirect or current generated export)");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const projectMdPath = join(projectRoot, "PROJECT.md");
|
|
89
|
+
if (isFile(projectMdPath)) {
|
|
90
|
+
const content = safeReadText(projectMdPath);
|
|
91
|
+
if (!isDeprecationRedirect(content)) {
|
|
92
|
+
reasons.push("PROJECT.md is a pre-v0.20 hand-authored doc (not a deprecation redirect)");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const vbriefRoot = join(projectRoot, "vbrief");
|
|
96
|
+
if (existsSync(vbriefRoot)) {
|
|
97
|
+
const missing = missingLifecycleFolders(projectRoot);
|
|
98
|
+
if (missing.length > 0) {
|
|
99
|
+
reasons.push(`vbrief/ is missing lifecycle folder(s): ${missing.join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { preCutover: reasons.length > 0, reasons };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Render the single-line pre-cutover status string surfaced by ``deft doctor``.
|
|
106
|
+
* Flags the migration-needed state with reasons, or reports a clean current-model state.
|
|
107
|
+
*/
|
|
108
|
+
export function renderPrecutoverLine(projectRoot) {
|
|
109
|
+
const { preCutover, reasons } = detectPreCutover(projectRoot);
|
|
110
|
+
if (!preCutover) {
|
|
111
|
+
return "Pre-cutover: none -- project is on the current vBRIEF document model.";
|
|
112
|
+
}
|
|
113
|
+
// Collapse any embedded newlines so the status line stays a single line (CWE-116).
|
|
114
|
+
const summary = reasons.join("; ").replace(/\r?\n/g, " ");
|
|
115
|
+
return `Pre-cutover: migration needed -- ${summary}. Run \`deft migrate:vbrief\` to migrate.`;
|
|
116
|
+
}
|
|
30
117
|
//# 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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as childProcess from "node:child_process";
|
|
2
2
|
import { nodeRuntimeRemediationLines } from "./node-runtime.js";
|
|
3
|
-
export const
|
|
3
|
+
export const MAINTAINER_TOOLS = [
|
|
4
4
|
{ name: "go", command: ["go", "version"] },
|
|
5
5
|
{ name: "uv", command: ["uv", "--version"] },
|
|
6
6
|
{ name: "git", command: ["git", "--version"] },
|
|
@@ -8,6 +8,16 @@ export const TOOLS = [
|
|
|
8
8
|
{ name: "node", command: ["node", "--version"] },
|
|
9
9
|
{ name: "pnpm", command: ["pnpm", "--version"] },
|
|
10
10
|
];
|
|
11
|
+
/** Consumer npm-deposit toolchain (#2022 Phase 3) -- no Python/go/uv maintainer tools. */
|
|
12
|
+
export const CONSUMER_TOOLS = [
|
|
13
|
+
{ name: "git", command: ["git", "--version"] },
|
|
14
|
+
{ name: "gh", command: ["gh", "--version"] },
|
|
15
|
+
{ name: "node", command: ["node", "--version"] },
|
|
16
|
+
{ name: "pnpm", command: ["pnpm", "--version"] },
|
|
17
|
+
{ name: "task", command: ["task", "--version"] },
|
|
18
|
+
];
|
|
19
|
+
/** @deprecated use MAINTAINER_TOOLS */
|
|
20
|
+
export const TOOLS = MAINTAINER_TOOLS;
|
|
11
21
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
12
22
|
export function defaultCommandRunner(command, timeoutMs) {
|
|
13
23
|
try {
|
|
@@ -30,11 +40,12 @@ export function defaultCommandRunner(command, timeoutMs) {
|
|
|
30
40
|
};
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
|
-
/** Run maintainer toolchain probe (mirrors scripts/toolchain-check.py). */
|
|
34
|
-
export function runToolchainCheck(runner = defaultCommandRunner,
|
|
43
|
+
/** Run maintainer or consumer toolchain probe (mirrors scripts/toolchain-check.py). */
|
|
44
|
+
export function runToolchainCheck(runner = defaultCommandRunner, options = {}, tools) {
|
|
45
|
+
const selectedTools = tools ?? (options.consumer ? CONSUMER_TOOLS : MAINTAINER_TOOLS);
|
|
35
46
|
const lines = [];
|
|
36
47
|
const failed = [];
|
|
37
|
-
for (const tool of
|
|
48
|
+
for (const tool of selectedTools) {
|
|
38
49
|
const result = runner(tool.command, DEFAULT_TIMEOUT_MS);
|
|
39
50
|
if ("error" in result) {
|
|
40
51
|
if (result.error === "not-found") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -190,6 +190,18 @@
|
|
|
190
190
|
"types": "./dist/init-deposit/index.d.ts",
|
|
191
191
|
"default": "./dist/init-deposit/index.js"
|
|
192
192
|
},
|
|
193
|
+
"./migrate-preflight": {
|
|
194
|
+
"types": "./dist/migrate-preflight/index.d.ts",
|
|
195
|
+
"default": "./dist/migrate-preflight/index.js"
|
|
196
|
+
},
|
|
197
|
+
"./install-upgrade": {
|
|
198
|
+
"types": "./dist/install-upgrade/index.d.ts",
|
|
199
|
+
"default": "./dist/install-upgrade/index.js"
|
|
200
|
+
},
|
|
201
|
+
"./task-surface": {
|
|
202
|
+
"types": "./dist/task-surface/index.d.ts",
|
|
203
|
+
"default": "./dist/task-surface/index.js"
|
|
204
|
+
},
|
|
193
205
|
"./dist/*.js": {
|
|
194
206
|
"types": "./dist/*.d.ts",
|
|
195
207
|
"default": "./dist/*.js"
|
|
@@ -209,8 +221,8 @@
|
|
|
209
221
|
"provenance": true
|
|
210
222
|
},
|
|
211
223
|
"dependencies": {
|
|
212
|
-
"@deftai/directive-content": "^0.
|
|
213
|
-
"@deftai/directive-types": "^0.
|
|
224
|
+
"@deftai/directive-content": "^0.60.0",
|
|
225
|
+
"@deftai/directive-types": "^0.60.0"
|
|
214
226
|
},
|
|
215
227
|
"scripts": {
|
|
216
228
|
"build": "tsc -b",
|