@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.
Files changed (45) hide show
  1. package/dist/deposit/python-free.d.ts +21 -0
  2. package/dist/deposit/python-free.js +149 -0
  3. package/dist/doctor/constants.d.ts +2 -0
  4. package/dist/doctor/constants.js +2 -0
  5. package/dist/doctor/main.js +22 -8
  6. package/dist/init-deposit/init-deposit.js +2 -0
  7. package/dist/init-deposit/refresh.js +2 -0
  8. package/dist/install-upgrade/index.d.ts +11 -0
  9. package/dist/install-upgrade/index.js +146 -0
  10. package/dist/migrate-preflight/index.d.ts +36 -0
  11. package/dist/migrate-preflight/index.js +193 -0
  12. package/dist/release-e2e/greenfield-python-free-smoke-cli.d.ts +3 -0
  13. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +10 -0
  14. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +12 -0
  15. package/dist/release-e2e/greenfield-python-free-smoke.js +183 -0
  16. package/dist/render/export-spec.d.ts +17 -0
  17. package/dist/render/export-spec.js +104 -0
  18. package/dist/render/index.d.ts +3 -1
  19. package/dist/render/index.js +3 -1
  20. package/dist/render/project-render.d.ts +24 -0
  21. package/dist/render/project-render.js +111 -7
  22. package/dist/render/scope-outlook.d.ts +9 -0
  23. package/dist/render/scope-outlook.js +248 -0
  24. package/dist/render/spec-render.js +3 -192
  25. package/dist/spec-authority/constants.d.ts +12 -0
  26. package/dist/spec-authority/constants.js +34 -0
  27. package/dist/spec-authority/index.d.ts +5 -0
  28. package/dist/spec-authority/index.js +5 -0
  29. package/dist/spec-authority/migration-fidelity.d.ts +6 -0
  30. package/dist/spec-authority/migration-fidelity.js +96 -0
  31. package/dist/spec-authority/narratives.d.ts +6 -0
  32. package/dist/spec-authority/narratives.js +84 -0
  33. package/dist/spec-authority/resolver.d.ts +15 -0
  34. package/dist/spec-authority/resolver.js +52 -0
  35. package/dist/task-surface/index.d.ts +13 -0
  36. package/dist/task-surface/index.js +126 -0
  37. package/dist/validate-content/validate-strategy-output.js +5 -26
  38. package/dist/vbrief-validate/main.js +5 -0
  39. package/dist/vbrief-validate/precutover.d.ts +8 -18
  40. package/dist/vbrief-validate/precutover.js +39 -23
  41. package/dist/verify-env/toolchain-check.d.ts +9 -2
  42. package/dist/verify-env/toolchain-check.js +15 -4
  43. package/dist/verify-env/verify-hooks-installed.d.ts +3 -3
  44. package/dist/verify-env/verify-hooks-installed.js +59 -27
  45. 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 = flagStaleNarratives({ ...SKELETON_NARRATIVES }, completedItems);
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.staleness_flags = flags;
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
- if (argv.length > 1) {
162
- process.stderr.write("Usage: project_render.py [vbrief_dir]\n");
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 = argv[0] ?? "vbrief";
166
- const [ok, message] = renderProjectDefinition(vbriefDir);
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,9 @@
1
+ export interface ScopeOutlookOptions {
2
+ readonly includeProposed?: boolean;
3
+ readonly proposedLimit?: number;
4
+ }
5
+ /** Build Scope outlook section per #2013 Wave 0 §4. */
6
+ export declare function buildScopeOutlookSection(vbriefDir: string, options?: ScopeOutlookOptions): string[];
7
+ /** Legacy wrapper: stakeholder export (no proposed). */
8
+ export declare function aggregateScopeSection(vbriefDir: string): string[];
9
+ //# sourceMappingURL=scope-outlook.d.ts.map
@@ -0,0 +1,248 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { PROPOSED_DISCLAIMER_LINES, PROPOSED_STATUS_FILTER } from "../spec-authority/constants.js";
4
+ import { SCOPE_SUMMARY_NARRATIVES } from "./constants.js";
5
+ function readEdgeEndpoints(edge) {
6
+ if (typeof edge !== "object" || edge === null || Array.isArray(edge))
7
+ return ["", ""];
8
+ const e = edge;
9
+ return [String(e.from ?? e.source ?? "") || "", String(e.to ?? e.target ?? "") || ""];
10
+ }
11
+ function loadScopeVbriefs(folder) {
12
+ if (!existsSync(folder))
13
+ return [];
14
+ let entries;
15
+ try {
16
+ entries = readdirSync(folder)
17
+ .filter((n) => n.endsWith(".vbrief.json"))
18
+ .sort();
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ const out = [];
24
+ for (const name of entries) {
25
+ try {
26
+ const data = JSON.parse(readFileSync(join(folder, name), "utf8"));
27
+ const stem = name.endsWith(".vbrief.json") ? name.slice(0, -".vbrief.json".length) : name;
28
+ out.push([stem, data]);
29
+ }
30
+ catch {
31
+ /* skip */
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ function scopeId(stem, vbrief) {
37
+ const plan = vbrief.plan;
38
+ if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
39
+ const planId = plan.id;
40
+ if (typeof planId === "string" && planId)
41
+ return planId;
42
+ }
43
+ return stem;
44
+ }
45
+ function crossScopeDepMap(scopes) {
46
+ const scopeIds = new Set(scopes.map(([stem, vb]) => scopeId(stem, vb)));
47
+ const depMap = {};
48
+ for (const [, vbrief] of scopes) {
49
+ const plan = vbrief.plan;
50
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan))
51
+ continue;
52
+ const edges = plan.edges;
53
+ if (!Array.isArray(edges))
54
+ continue;
55
+ for (const edge of edges) {
56
+ const [frm, to] = readEdgeEndpoints(edge);
57
+ if (frm && to && scopeIds.has(frm) && scopeIds.has(to)) {
58
+ if (!depMap[to])
59
+ depMap[to] = [];
60
+ depMap[to].push(frm);
61
+ }
62
+ }
63
+ }
64
+ return depMap;
65
+ }
66
+ function topoSortScopes(scopes, depMap) {
67
+ if (scopes.length === 0)
68
+ return [];
69
+ const idByIndex = scopes.map(([stem, vb]) => scopeId(stem, vb));
70
+ const idToIndex = new Map(idByIndex.map((sid, i) => [sid, i]));
71
+ const depths = {};
72
+ const depth = (sid, visited = null) => {
73
+ if (sid in depths)
74
+ return depths[sid] ?? 0;
75
+ const vis = visited ?? new Set();
76
+ if (vis.has(sid))
77
+ return 0;
78
+ vis.add(sid);
79
+ const deps = (depMap[sid] ?? []).filter((d) => idToIndex.has(d));
80
+ if (deps.length === 0) {
81
+ depths[sid] = 0;
82
+ return 0;
83
+ }
84
+ const result = Math.max(...deps.map((d) => depth(d, vis))) + 1;
85
+ depths[sid] = result;
86
+ return result;
87
+ };
88
+ for (const sid of idByIndex)
89
+ depth(sid);
90
+ const orderedIndices = [...idByIndex.keys()].sort((a, b) => (depths[idByIndex[a] ?? ""] ?? 0) - (depths[idByIndex[b] ?? ""] ?? 0) || a - b);
91
+ return orderedIndices.map((i) => scopes[i]);
92
+ }
93
+ function scopeSummaryNarrative(plan) {
94
+ const narratives = plan.narratives;
95
+ if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives))
96
+ return "";
97
+ const narr = narratives;
98
+ for (const key of SCOPE_SUMMARY_NARRATIVES) {
99
+ const val = narr[key];
100
+ if (typeof val === "string" && val.trim())
101
+ return val.trim();
102
+ }
103
+ for (const val of Object.values(narr)) {
104
+ if (typeof val === "string" && val.trim())
105
+ return val.trim();
106
+ }
107
+ return "";
108
+ }
109
+ function splitAcceptance(value) {
110
+ if (Array.isArray(value)) {
111
+ return value.map((item) => String(item).trim()).filter((s) => s.length > 0);
112
+ }
113
+ if (typeof value !== "string")
114
+ return [];
115
+ const parts = [];
116
+ for (const line of value.split("\n")) {
117
+ let cleaned = line.trim();
118
+ if (!cleaned)
119
+ continue;
120
+ if (cleaned.startsWith("- ") || cleaned.startsWith("* "))
121
+ cleaned = cleaned.slice(2).trim();
122
+ if (cleaned)
123
+ parts.push(cleaned);
124
+ }
125
+ return parts;
126
+ }
127
+ function itemAcceptance(item) {
128
+ const narrative = item.narrative;
129
+ if (typeof narrative !== "object" || narrative === null || Array.isArray(narrative))
130
+ return [];
131
+ return splitAcceptance(narrative.Acceptance);
132
+ }
133
+ function renderScopeBlock(stem, vbrief) {
134
+ const plan = vbrief.plan;
135
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan))
136
+ return [];
137
+ const planObj = plan;
138
+ const title = String(planObj.title ?? stem);
139
+ const status = String(planObj.status ?? "");
140
+ let heading = `### ${stem}: ${title}`;
141
+ if (status)
142
+ heading += ` \`[${status}]\``;
143
+ const lines = [`${heading}\n`];
144
+ const summary = scopeSummaryNarrative(planObj);
145
+ if (summary)
146
+ lines.push(`${summary}\n`);
147
+ const narratives = planObj.narratives;
148
+ if (typeof narratives === "object" && narratives !== null && !Array.isArray(narratives)) {
149
+ const scopeAcceptance = splitAcceptance(narratives.Acceptance);
150
+ if (scopeAcceptance.length > 0) {
151
+ lines.push("**Scope Acceptance**:\n");
152
+ for (const criterion of scopeAcceptance)
153
+ lines.push(`- ${criterion}`);
154
+ lines.push("");
155
+ }
156
+ }
157
+ const items = planObj.items;
158
+ if (Array.isArray(items) && items.length > 0) {
159
+ lines.push("**Acceptance**:\n");
160
+ for (const item of items) {
161
+ if (typeof item !== "object" || item === null || Array.isArray(item))
162
+ continue;
163
+ const itemObj = item;
164
+ const itemTitle = String(itemObj.title ?? "Untitled");
165
+ const itemStatus = String(itemObj.status ?? "");
166
+ let bullet = `- ${itemTitle}`;
167
+ if (itemStatus)
168
+ bullet += ` \`[${itemStatus}]\``;
169
+ lines.push(bullet);
170
+ for (const criterion of itemAcceptance(itemObj)) {
171
+ if (criterion !== itemTitle)
172
+ lines.push(` - Acceptance: ${criterion}`);
173
+ }
174
+ }
175
+ lines.push("");
176
+ }
177
+ return lines;
178
+ }
179
+ const COMMITTED_BUCKETS = [
180
+ ["pending", "Accepted backlog (pending)"],
181
+ ["active", "Active"],
182
+ [
183
+ "completed",
184
+ "Completed",
185
+ (vb) => {
186
+ const plan = vb.plan;
187
+ return (typeof plan === "object" &&
188
+ plan !== null &&
189
+ !Array.isArray(plan) &&
190
+ plan.status === "completed");
191
+ },
192
+ ],
193
+ ];
194
+ function filterProposed(vb) {
195
+ const plan = vb.plan;
196
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan))
197
+ return true;
198
+ const status = String(plan.status ?? "proposed").toLowerCase();
199
+ return PROPOSED_STATUS_FILTER.has(status);
200
+ }
201
+ /** Build Scope outlook section per #2013 Wave 0 §4. */
202
+ export function buildScopeOutlookSection(vbriefDir, options = {}) {
203
+ const includeProposed = options.includeProposed ?? false;
204
+ const proposedLimit = options.proposedLimit ?? 0;
205
+ const buckets = [];
206
+ if (includeProposed) {
207
+ let proposed = loadScopeVbriefs(join(vbriefDir, "proposed")).filter(([, vb]) => filterProposed(vb));
208
+ if (proposedLimit > 0 && proposed.length > proposedLimit) {
209
+ const omitted = proposed.length - proposedLimit;
210
+ proposed = proposed.slice(0, proposedLimit);
211
+ buckets.push([
212
+ "proposed",
213
+ `Not yet accepted (proposed) — showing ${proposedLimit} of ${proposedLimit + omitted}`,
214
+ proposed,
215
+ ]);
216
+ }
217
+ else if (proposed.length > 0) {
218
+ buckets.push(["proposed", "Not yet accepted (proposed)", proposed]);
219
+ }
220
+ }
221
+ for (const [folder, heading, filter] of COMMITTED_BUCKETS) {
222
+ let scopes = loadScopeVbriefs(join(vbriefDir, folder));
223
+ if (filter)
224
+ scopes = scopes.filter(([, vb]) => filter(vb));
225
+ if (scopes.length > 0)
226
+ buckets.push([folder, heading, scopes]);
227
+ }
228
+ if (buckets.length === 0)
229
+ return [];
230
+ const lines = ["## Scope outlook\n"];
231
+ for (const [folder, heading, scopes] of buckets) {
232
+ const depMap = crossScopeDepMap(scopes);
233
+ const ordered = topoSortScopes(scopes, depMap);
234
+ lines.push(`### ${heading}\n`);
235
+ if (folder === "proposed") {
236
+ for (const line of PROPOSED_DISCLAIMER_LINES)
237
+ lines.push(line, "");
238
+ }
239
+ for (const [stem, vbrief] of ordered)
240
+ lines.push(...renderScopeBlock(stem, vbrief));
241
+ }
242
+ return lines;
243
+ }
244
+ /** Legacy wrapper: stakeholder export (no proposed). */
245
+ export function aggregateScopeSection(vbriefDir) {
246
+ return buildScopeOutlookSection(vbriefDir, { includeProposed: false });
247
+ }
248
+ //# sourceMappingURL=scope-outlook.js.map
@@ -1,117 +1,8 @@
1
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { LIFECYCLE_BUCKETS, RENDERABLE_SPEC_STATUSES, SCOPE_SUMMARY_NARRATIVES, SPEC_RENDER_BANNER, SPECIFICATION_NARRATIVE_KEY_ORDER, } from "./constants.js";
3
+ import { RENDERABLE_SPEC_STATUSES, SPEC_RENDER_BANNER, SPECIFICATION_NARRATIVE_KEY_ORDER, } from "./constants.js";
4
+ import { aggregateScopeSection } from "./scope-outlook.js";
4
5
  import { validateSpec } from "./spec-validate.js";
5
- function readEdgeEndpoints(edge) {
6
- if (typeof edge !== "object" || edge === null || Array.isArray(edge))
7
- return ["", ""];
8
- const e = edge;
9
- const frm = String(e.from ?? e.source ?? "") || "";
10
- const to = String(e.to ?? e.target ?? "") || "";
11
- return [frm, to];
12
- }
13
- function loadScopeVbriefs(folder) {
14
- if (!existsSync(folder))
15
- return [];
16
- let entries;
17
- try {
18
- entries = readdirSync(folder)
19
- .filter((n) => n.endsWith(".vbrief.json"))
20
- .sort();
21
- }
22
- catch {
23
- return [];
24
- }
25
- const out = [];
26
- for (const name of entries) {
27
- const path = join(folder, name);
28
- try {
29
- const data = JSON.parse(readFileSync(path, "utf8"));
30
- let stem = name;
31
- if (stem.endsWith(".vbrief.json"))
32
- stem = stem.slice(0, -".vbrief.json".length);
33
- out.push([stem, data]);
34
- }
35
- catch {
36
- /* skip malformed */
37
- }
38
- }
39
- return out;
40
- }
41
- function scopeId(stem, vbrief) {
42
- const plan = vbrief.plan;
43
- if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
44
- const planId = plan.id;
45
- if (typeof planId === "string" && planId)
46
- return planId;
47
- }
48
- return stem;
49
- }
50
- function crossScopeDepMap(scopes) {
51
- const scopeIds = new Set(scopes.map(([stem, vb]) => scopeId(stem, vb)));
52
- const depMap = {};
53
- for (const [, vbrief] of scopes) {
54
- const plan = vbrief.plan;
55
- if (typeof plan !== "object" || plan === null || Array.isArray(plan))
56
- continue;
57
- const edges = plan.edges;
58
- if (!Array.isArray(edges))
59
- continue;
60
- for (const edge of edges) {
61
- const [frm, to] = readEdgeEndpoints(edge);
62
- if (frm && to && scopeIds.has(frm) && scopeIds.has(to)) {
63
- if (!depMap[to])
64
- depMap[to] = [];
65
- depMap[to].push(frm);
66
- }
67
- }
68
- }
69
- return depMap;
70
- }
71
- function topoSortScopes(scopes, depMap) {
72
- if (scopes.length === 0)
73
- return [];
74
- const idByIndex = scopes.map(([stem, vb]) => scopeId(stem, vb));
75
- const idToIndex = new Map(idByIndex.map((sid, i) => [sid, i]));
76
- const depths = {};
77
- const depth = (sid, visited = null) => {
78
- if (sid in depths)
79
- return depths[sid] ?? 0;
80
- const vis = visited ?? new Set();
81
- if (vis.has(sid))
82
- return 0;
83
- vis.add(sid);
84
- const deps = (depMap[sid] ?? []).filter((d) => idToIndex.has(d));
85
- if (deps.length === 0) {
86
- depths[sid] = 0;
87
- return 0;
88
- }
89
- const result = Math.max(...deps.map((d) => depth(d, vis))) + 1;
90
- depths[sid] = result;
91
- return result;
92
- };
93
- for (const sid of idByIndex)
94
- depth(sid);
95
- const orderedIndices = [...idByIndex.keys()].sort((a, b) => (depths[idByIndex[a] ?? ""] ?? 0) - (depths[idByIndex[b] ?? ""] ?? 0) || a - b);
96
- return orderedIndices.map((i) => scopes[i]);
97
- }
98
- function scopeSummaryNarrative(plan) {
99
- const narratives = plan.narratives;
100
- if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives)) {
101
- return "";
102
- }
103
- const narr = narratives;
104
- for (const key of SCOPE_SUMMARY_NARRATIVES) {
105
- const val = narr[key];
106
- if (typeof val === "string" && val.trim())
107
- return val.trim();
108
- }
109
- for (const val of Object.values(narr)) {
110
- if (typeof val === "string" && val.trim())
111
- return val.trim();
112
- }
113
- return "";
114
- }
115
6
  function splitAcceptance(value) {
116
7
  if (Array.isArray(value)) {
117
8
  return value.map((item) => String(item).trim()).filter((s) => s.length > 0);
@@ -131,86 +22,6 @@ function splitAcceptance(value) {
131
22
  }
132
23
  return parts;
133
24
  }
134
- function itemAcceptance(item) {
135
- const narrative = item.narrative;
136
- if (typeof narrative !== "object" || narrative === null || Array.isArray(narrative))
137
- return [];
138
- return splitAcceptance(narrative.Acceptance);
139
- }
140
- function renderScopeBlock(stem, vbrief) {
141
- const plan = vbrief.plan;
142
- if (typeof plan !== "object" || plan === null || Array.isArray(plan))
143
- return [];
144
- const planObj = plan;
145
- const title = String(planObj.title ?? stem);
146
- const status = String(planObj.status ?? "");
147
- let heading = `### ${stem}: ${title}`;
148
- if (status)
149
- heading += ` \`[${status}]\``;
150
- const lines = [`${heading}\n`];
151
- const summary = scopeSummaryNarrative(planObj);
152
- if (summary)
153
- lines.push(`${summary}\n`);
154
- const narratives = planObj.narratives;
155
- if (typeof narratives === "object" && narratives !== null && !Array.isArray(narratives)) {
156
- const scopeAcceptance = splitAcceptance(narratives.Acceptance);
157
- if (scopeAcceptance.length > 0) {
158
- lines.push("**Scope Acceptance**:\n");
159
- for (const criterion of scopeAcceptance)
160
- lines.push(`- ${criterion}`);
161
- lines.push("");
162
- }
163
- }
164
- const items = planObj.items;
165
- if (Array.isArray(items) && items.length > 0) {
166
- lines.push("**Acceptance**:\n");
167
- for (const item of items) {
168
- if (typeof item !== "object" || item === null || Array.isArray(item))
169
- continue;
170
- const itemObj = item;
171
- const itemTitle = String(itemObj.title ?? "Untitled");
172
- const itemStatus = String(itemObj.status ?? "");
173
- let bullet = `- ${itemTitle}`;
174
- if (itemStatus)
175
- bullet += ` \`[${itemStatus}]\``;
176
- lines.push(bullet);
177
- for (const criterion of itemAcceptance(itemObj)) {
178
- if (criterion !== itemTitle)
179
- lines.push(` - Acceptance: ${criterion}`);
180
- }
181
- }
182
- lines.push("");
183
- }
184
- return lines;
185
- }
186
- function aggregateScopeSection(vbriefDir) {
187
- const buckets = [];
188
- for (const [folderName, heading] of LIFECYCLE_BUCKETS) {
189
- let scopes = loadScopeVbriefs(join(vbriefDir, folderName));
190
- if (folderName === "completed") {
191
- scopes = scopes.filter(([, vb]) => {
192
- const plan = vb.plan;
193
- return (typeof plan === "object" &&
194
- plan !== null &&
195
- !Array.isArray(plan) &&
196
- plan.status === "completed");
197
- });
198
- }
199
- if (scopes.length > 0)
200
- buckets.push([folderName, heading, scopes]);
201
- }
202
- if (buckets.length === 0)
203
- return [];
204
- const lines = ["## Implementation Plan\n"];
205
- for (const [, heading, scopes] of buckets) {
206
- const depMap = crossScopeDepMap(scopes);
207
- const ordered = topoSortScopes(scopes, depMap);
208
- lines.push(`### ${heading}\n`);
209
- for (const [stem, vbrief] of ordered)
210
- lines.push(...renderScopeBlock(stem, vbrief));
211
- }
212
- return lines;
213
- }
214
25
  /** Render specification JSON to markdown (mirrors ``scripts/spec_render.render_spec``). */
215
26
  export function renderSpec(specPath, outPath, options = {}) {
216
27
  const includeScopes = options.includeScopes ?? true;
@@ -0,0 +1,12 @@
1
+ /** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
2
+ export declare const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
3
+ export declare const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
4
+ export declare const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
5
+ export declare const EXPORT_SPEC_PD_BANNER: string;
6
+ /** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
7
+ export declare const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS: Set<string>;
8
+ /** Product narrative keys used for migration-fidelity checks (#2005). */
9
+ export declare const PRODUCT_NARRATIVE_KEYS: readonly ["ProblemStatement", "Goals", "UserStories", "Requirements", "SuccessMetrics", "FunctionalRequirements", "NonFunctionalRequirements", "AcceptanceCriteria", "Architecture", "EdgeCases"];
10
+ export declare const PROPOSED_DISCLAIMER_LINES: readonly ["_Scopes in this section are ideas, not approved backlog. Promotion to pending is required before work starts._", "_Nothing here is in-flight or committed delivery scope._"];
11
+ export declare const PROPOSED_STATUS_FILTER: Set<string>;
12
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,34 @@
1
+ /** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
2
+ export const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
3
+ export const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
4
+ export const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
5
+ export const EXPORT_SPEC_PD_BANNER = "<!-- AUTO-GENERATED by task project:export-spec -- DO NOT EDIT MANUALLY -->\n" +
6
+ `${GENERATED_SPEC_PURPOSE}\n` +
7
+ `${GENERATED_SPEC_SOURCE_PD}\n` +
8
+ "<!-- Regenerate with: task project:export-spec -->\n";
9
+ /** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
10
+ export const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS = new Set([
11
+ "projectconfig",
12
+ "tech stack",
13
+ "techstack",
14
+ "configuration",
15
+ ]);
16
+ /** Product narrative keys used for migration-fidelity checks (#2005). */
17
+ export const PRODUCT_NARRATIVE_KEYS = [
18
+ "ProblemStatement",
19
+ "Goals",
20
+ "UserStories",
21
+ "Requirements",
22
+ "SuccessMetrics",
23
+ "FunctionalRequirements",
24
+ "NonFunctionalRequirements",
25
+ "AcceptanceCriteria",
26
+ "Architecture",
27
+ "EdgeCases",
28
+ ];
29
+ export const PROPOSED_DISCLAIMER_LINES = [
30
+ "_Scopes in this section are ideas, not approved backlog. Promotion to pending is required before work starts._",
31
+ "_Nothing here is in-flight or committed delivery scope._",
32
+ ];
33
+ export const PROPOSED_STATUS_FILTER = new Set(["draft", "proposed"]);
34
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,5 @@
1
+ export * from "./constants.js";
2
+ export * from "./migration-fidelity.js";
3
+ export * from "./narratives.js";
4
+ export * from "./resolver.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export * from "./constants.js";
2
+ export * from "./migration-fidelity.js";
3
+ export * from "./narratives.js";
4
+ export * from "./resolver.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * #2005 migration-fidelity: when specification.vbrief.json is absent but a
3
+ * premigrate snapshot exists, require product narratives to land in PD or scopes.
4
+ */
5
+ export declare function checkSpecMigrationFidelity(projectRoot: string): string[];
6
+ //# sourceMappingURL=migration-fidelity.d.ts.map