@xenonbyte/da-vinci-workflow 0.2.4 → 0.2.6

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 (42) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -9
  3. package/README.zh-CN.md +16 -9
  4. package/SKILL.md +45 -704
  5. package/docs/dv-command-reference.md +33 -5
  6. package/docs/execution-chain-migration.md +14 -3
  7. package/docs/maintainer-bootstrap.md +102 -0
  8. package/docs/pencil-rendering-workflow.md +1 -1
  9. package/docs/prompt-entrypoints.md +1 -0
  10. package/docs/skill-contract-maintenance.md +14 -0
  11. package/docs/skill-usage.md +31 -0
  12. package/docs/workflow-overview.md +40 -5
  13. package/docs/zh-CN/dv-command-reference.md +31 -5
  14. package/docs/zh-CN/maintainer-bootstrap.md +101 -0
  15. package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
  16. package/docs/zh-CN/prompt-entrypoints.md +1 -0
  17. package/docs/zh-CN/skill-usage.md +30 -0
  18. package/docs/zh-CN/workflow-overview.md +38 -5
  19. package/lib/audit.js +19 -0
  20. package/lib/cli/helpers.js +104 -0
  21. package/lib/cli/lint-family.js +56 -0
  22. package/lib/cli/verify-family.js +79 -0
  23. package/lib/cli.js +143 -172
  24. package/lib/gate-utils.js +56 -0
  25. package/lib/install.js +134 -6
  26. package/lib/lint-bindings.js +41 -28
  27. package/lib/lint-spec.js +403 -109
  28. package/lib/lint-tasks.js +571 -21
  29. package/lib/maintainer-readiness.js +317 -0
  30. package/lib/planning-parsers.js +198 -2
  31. package/lib/planning-quality-utils.js +81 -0
  32. package/lib/planning-signal-freshness.js +205 -0
  33. package/lib/scaffold.js +454 -23
  34. package/lib/scope-check.js +751 -82
  35. package/lib/sidecars.js +396 -1
  36. package/lib/task-review.js +2 -1
  37. package/lib/utils.js +34 -0
  38. package/lib/verify.js +1160 -88
  39. package/lib/workflow-persisted-state.js +52 -32
  40. package/lib/workflow-state.js +1187 -249
  41. package/package.json +1 -1
  42. package/references/skill-workflow-detail.md +66 -0
@@ -0,0 +1,317 @@
1
+ const path = require("path");
2
+ const { spawnSync } = require("child_process");
3
+ const { validateAssets, verifyInstall } = require("./install");
4
+ const { dedupeMessages } = require("./utils");
5
+
6
+ const DEFAULT_FOCUSED_QUALITY_LANE = "quality:ci:contracts";
7
+ const DEEPER_VALIDATION_LANES = ["npm run quality:ci:e2e", "npm run quality:ci"];
8
+
9
+ function buildFocusedLaneCommand(laneScript) {
10
+ return `npm run ${laneScript}`;
11
+ }
12
+
13
+ function normalizeValidateAssetsResult(rawResult, repoRoot) {
14
+ if (!rawResult || typeof rawResult !== "object") {
15
+ return {
16
+ status: "BLOCK",
17
+ details: "asset validation failed",
18
+ failureMessage: "validate-assets failed; inspect command output for details.",
19
+ requiredAssets: null
20
+ };
21
+ }
22
+
23
+ const statusToken = rawResult.status === "PASS" || rawResult.status === "BLOCK"
24
+ ? rawResult.status
25
+ : "BLOCK";
26
+ const details =
27
+ rawResult.details && String(rawResult.details).trim()
28
+ ? String(rawResult.details).trim()
29
+ : statusToken === "PASS"
30
+ ? `required assets: ${Number(rawResult.requiredAssets) || 0}`
31
+ : "asset validation failed";
32
+ const failureMessage =
33
+ rawResult.failureMessage && String(rawResult.failureMessage).trim()
34
+ ? String(rawResult.failureMessage).trim()
35
+ : `validate-assets failed under ${repoRoot}; run \`npm run validate-assets\` in the target repository.`;
36
+
37
+ return {
38
+ status: statusToken,
39
+ details,
40
+ failureMessage,
41
+ requiredAssets:
42
+ Number.isFinite(Number(rawResult.requiredAssets)) && Number(rawResult.requiredAssets) >= 0
43
+ ? Number(rawResult.requiredAssets)
44
+ : null
45
+ };
46
+ }
47
+
48
+ function runValidateAssetsCheck(options = {}) {
49
+ const repoRoot = options.repoRoot || process.cwd();
50
+ if (typeof options.validateAssetsRunner === "function") {
51
+ return normalizeValidateAssetsResult(options.validateAssetsRunner({ repoRoot }), repoRoot);
52
+ }
53
+
54
+ try {
55
+ const assets = validateAssets({ repoRoot });
56
+ return {
57
+ status: "PASS",
58
+ details: `required assets: ${assets.requiredAssets}`,
59
+ requiredAssets: assets.requiredAssets
60
+ };
61
+ } catch (error) {
62
+ return {
63
+ status: "BLOCK",
64
+ details: error && error.message ? error.message : "asset validation failed",
65
+ failureMessage: `validate-assets failed under ${repoRoot}; run \`npm run validate-assets\` in the target repository.`
66
+ };
67
+ }
68
+ }
69
+
70
+ function normalizeFocusedLaneResult(rawResult, laneScript) {
71
+ const laneCommand = buildFocusedLaneCommand(laneScript);
72
+ if (!rawResult || typeof rawResult !== "object") {
73
+ return {
74
+ status: "BLOCK",
75
+ details: `${laneCommand} failed`,
76
+ failureMessage: `${laneCommand} failed; inspect command output for details.`
77
+ };
78
+ }
79
+
80
+ const statusToken =
81
+ rawResult.status === "PASS" || rawResult.status === "SKIP" || rawResult.status === "BLOCK"
82
+ ? rawResult.status
83
+ : "BLOCK";
84
+ const details =
85
+ rawResult.details && String(rawResult.details).trim()
86
+ ? String(rawResult.details).trim()
87
+ : statusToken === "PASS"
88
+ ? `${laneCommand} passed`
89
+ : statusToken === "SKIP"
90
+ ? `${laneCommand} skipped`
91
+ : `${laneCommand} failed`;
92
+ const failureMessage =
93
+ rawResult.failureMessage && String(rawResult.failureMessage).trim()
94
+ ? String(rawResult.failureMessage).trim()
95
+ : `${laneCommand} failed; run ${laneCommand} directly for details.`;
96
+
97
+ return {
98
+ status: statusToken,
99
+ details,
100
+ failureMessage
101
+ };
102
+ }
103
+
104
+ function runFocusedQualityLane(options = {}) {
105
+ const laneScript = String(options.focusedQualityLane || DEFAULT_FOCUSED_QUALITY_LANE).trim();
106
+ const normalizedLaneScript = laneScript || DEFAULT_FOCUSED_QUALITY_LANE;
107
+ const laneCommand = buildFocusedLaneCommand(normalizedLaneScript);
108
+
109
+ if (typeof options.focusedQualityLaneRunner === "function") {
110
+ return normalizeFocusedLaneResult(
111
+ options.focusedQualityLaneRunner({
112
+ laneScript: normalizedLaneScript,
113
+ laneCommand,
114
+ repoRoot: options.repoRoot || process.cwd()
115
+ }),
116
+ normalizedLaneScript
117
+ );
118
+ }
119
+
120
+ const repoRoot = options.repoRoot || process.cwd();
121
+ const result = spawnSync("npm", ["run", normalizedLaneScript], {
122
+ cwd: repoRoot,
123
+ encoding: "utf8",
124
+ env: options.env || process.env
125
+ });
126
+
127
+ if (result.error) {
128
+ return {
129
+ status: "BLOCK",
130
+ details: `${laneCommand} failed to start`,
131
+ failureMessage: `${laneCommand} failed to start: ${result.error.message}`
132
+ };
133
+ }
134
+ if (result.status === 0) {
135
+ return {
136
+ status: "PASS",
137
+ details: `${laneCommand} passed`
138
+ };
139
+ }
140
+
141
+ const stderrText = String(result.stderr || "").trim();
142
+ const stdoutText = String(result.stdout || "").trim();
143
+ const detailSource = stderrText || stdoutText;
144
+ const summaryLine = detailSource
145
+ .split(/\r?\n/)
146
+ .map((line) => line.trim())
147
+ .filter(Boolean)
148
+ .slice(-1)[0];
149
+
150
+ return {
151
+ status: "BLOCK",
152
+ details: `${laneCommand} failed`,
153
+ failureMessage: summaryLine
154
+ ? `${laneCommand} failed: ${summaryLine}`
155
+ : `${laneCommand} failed; inspect output for details.`
156
+ };
157
+ }
158
+
159
+ function runMaintainerReadinessCheck(options = {}) {
160
+ const repoRoot = options.repoRoot ? path.resolve(String(options.repoRoot)) : process.cwd();
161
+ const focusedQualityLane = String(options.focusedQualityLane || DEFAULT_FOCUSED_QUALITY_LANE).trim()
162
+ || DEFAULT_FOCUSED_QUALITY_LANE;
163
+ const focusedQualityLaneCommand = buildFocusedLaneCommand(focusedQualityLane);
164
+ const runFocusedQualityLaneCheck = options.runFocusedQualityLane !== false;
165
+ const checks = [];
166
+ const failures = [];
167
+ const warnings = [];
168
+ const notes = [];
169
+
170
+ const validateAssetsResult = runValidateAssetsCheck({
171
+ repoRoot,
172
+ validateAssetsRunner: options.validateAssetsRunner
173
+ });
174
+ if (validateAssetsResult.status === "PASS") {
175
+ checks.push({
176
+ id: "validate-assets",
177
+ status: "PASS",
178
+ details: validateAssetsResult.details
179
+ });
180
+ } else {
181
+ checks.push({
182
+ id: "validate-assets",
183
+ status: "BLOCK",
184
+ details: validateAssetsResult.details
185
+ });
186
+ failures.push(validateAssetsResult.failureMessage);
187
+ }
188
+
189
+ const installResult = verifyInstall({
190
+ homeDir: options.homeDir,
191
+ platforms: options.platforms
192
+ });
193
+ checks.push({
194
+ id: "verify-install",
195
+ status: installResult.status,
196
+ details: installResult.scope.known
197
+ ? `selected platforms: ${installResult.scope.selectedPlatforms.join(", ")}`
198
+ : "selected platforms: unknown scope"
199
+ });
200
+ if (installResult.status === "BLOCK") {
201
+ failures.push(...installResult.failures);
202
+ } else if (installResult.status === "WARN") {
203
+ warnings.push(...installResult.warnings);
204
+ }
205
+ notes.push(...installResult.notes);
206
+
207
+ if (!runFocusedQualityLaneCheck) {
208
+ checks.push({
209
+ id: focusedQualityLaneCommand,
210
+ status: "SKIP",
211
+ details: "skipped by caller"
212
+ });
213
+ } else if (failures.length > 0) {
214
+ checks.push({
215
+ id: focusedQualityLaneCommand,
216
+ status: "SKIP",
217
+ details: "skipped because blocking prerequisite checks failed"
218
+ });
219
+ } else {
220
+ const focusedLaneResult = runFocusedQualityLane({
221
+ focusedQualityLane,
222
+ focusedQualityLaneRunner: options.focusedQualityLaneRunner,
223
+ repoRoot,
224
+ env: options.env
225
+ });
226
+ checks.push({
227
+ id: focusedQualityLaneCommand,
228
+ status: focusedLaneResult.status,
229
+ details: focusedLaneResult.details
230
+ });
231
+ if (focusedLaneResult.status === "BLOCK") {
232
+ failures.push(focusedLaneResult.failureMessage);
233
+ }
234
+ }
235
+
236
+ const status = failures.length > 0 ? "BLOCK" : warnings.length > 0 ? "WARN" : "PASS";
237
+ const nextSteps = [];
238
+ if (failures.length > 0) {
239
+ nextSteps.push("Resolve failing checks, then rerun `da-vinci maintainer-readiness`.");
240
+ }
241
+ if (warnings.length > 0) {
242
+ nextSteps.push("Resolve warnings or accept degraded coverage before broader changes.");
243
+ }
244
+ nextSteps.push(
245
+ "The canonical readiness surface is diagnosis only; bootstrap/setup still starts with install plus verify-install."
246
+ );
247
+ nextSteps.push(`If focused readiness lane did not pass, run \`${focusedQualityLaneCommand}\` directly for details.`);
248
+ nextSteps.push("Before high-risk changes, run deeper optional lanes such as `npm run quality:ci:e2e` or `npm run quality:ci`.");
249
+
250
+ return {
251
+ status,
252
+ repoRoot,
253
+ canonicalGuide: "docs/maintainer-bootstrap.md",
254
+ focusedQualityLane: focusedQualityLaneCommand,
255
+ componentChecks: ["npm run validate-assets", "da-vinci verify-install", focusedQualityLaneCommand],
256
+ scope: installResult.scope,
257
+ checks,
258
+ failures: dedupeMessages(failures, { normalize: (item) => String(item || "").trim() }),
259
+ warnings: dedupeMessages(warnings, { normalize: (item) => String(item || "").trim() }),
260
+ notes: dedupeMessages(notes, { normalize: (item) => String(item || "").trim() }),
261
+ nextSteps: dedupeMessages(nextSteps, { normalize: (item) => String(item || "").trim() }),
262
+ deeperValidationLanes: DEEPER_VALIDATION_LANES.slice()
263
+ };
264
+ }
265
+
266
+ function formatMaintainerReadinessReport(result) {
267
+ const scopeLabel = result.scope && result.scope.known
268
+ ? result.scope.selectedPlatforms.join(", ")
269
+ : "unknown";
270
+ const lines = [
271
+ "Da Vinci maintainer-readiness",
272
+ `Status: ${result.status}`,
273
+ `Repo root: ${result.repoRoot || "unknown"}`,
274
+ `Guide: ${result.canonicalGuide}`,
275
+ "Surface: diagnosis/readiness (repository health), not bootstrap/setup",
276
+ `Platform scope: ${scopeLabel}`,
277
+ "Checks:"
278
+ ];
279
+
280
+ for (const check of Array.isArray(result.checks) ? result.checks : []) {
281
+ lines.push(`- ${check.id}: ${check.status}${check.details ? ` (${check.details})` : ""}`);
282
+ }
283
+
284
+ if (Array.isArray(result.failures) && result.failures.length > 0) {
285
+ lines.push("Failures:");
286
+ for (const message of result.failures) {
287
+ lines.push(`- ${message}`);
288
+ }
289
+ }
290
+ if (Array.isArray(result.warnings) && result.warnings.length > 0) {
291
+ lines.push("Warnings:");
292
+ for (const message of result.warnings) {
293
+ lines.push(`- ${message}`);
294
+ }
295
+ }
296
+ if (Array.isArray(result.notes) && result.notes.length > 0) {
297
+ lines.push("Notes:");
298
+ for (const message of result.notes) {
299
+ lines.push(`- ${message}`);
300
+ }
301
+ }
302
+ if (Array.isArray(result.nextSteps) && result.nextSteps.length > 0) {
303
+ lines.push("Next steps:");
304
+ for (const step of result.nextSteps) {
305
+ lines.push(`- ${step}`);
306
+ }
307
+ }
308
+
309
+ return lines.join("\n");
310
+ }
311
+
312
+ module.exports = {
313
+ runMaintainerReadinessCheck,
314
+ formatMaintainerReadinessReport,
315
+ runFocusedQualityLane,
316
+ runValidateAssetsCheck
317
+ };
@@ -12,6 +12,125 @@ const { pathExists, readTextIfExists } = require("./utils");
12
12
  const LIST_ITEM_PATTERN = /^\s*(?:[-*+]|\d+[.)])\s+(.+?)\s*$/;
13
13
  const DEFAULT_SCAN_MAX_DEPTH = 32;
14
14
  const DEFAULT_SPEC_SCAN_MAX_DEPTH = 64;
15
+ const PLANNING_ANCHOR_ID_PATTERN = /^(behavior|acceptance|state|mapping)-[a-z0-9._-]+$/i;
16
+ const ARTIFACT_ANCHOR_PATTERN = /^artifact:([^#\s]+)(?:#(.+))?$/i;
17
+ const AMBIGUITY_RECORD_PATTERN =
18
+ /^\s*-\s*\[(blocking|bounded|advisory)\]\s*([a-z][a-z0-9_-]{1,40})\s*:\s*(.+?)\s*$/i;
19
+
20
+ function parsePlanningAnchorRef(value) {
21
+ const raw = String(value || "").trim();
22
+ if (!raw) {
23
+ return {
24
+ raw,
25
+ ref: "",
26
+ valid: false,
27
+ reason: "empty_anchor"
28
+ };
29
+ }
30
+
31
+ const ref = raw.replace(/[`"'(),]/g, "").trim();
32
+ if (!ref) {
33
+ return {
34
+ raw,
35
+ ref,
36
+ valid: false,
37
+ reason: "empty_anchor"
38
+ };
39
+ }
40
+
41
+ if (PLANNING_ANCHOR_ID_PATTERN.test(ref)) {
42
+ const family = String(ref.split("-")[0] || "").toLowerCase();
43
+ return {
44
+ raw,
45
+ ref,
46
+ valid: true,
47
+ type: "sidecar_id",
48
+ family
49
+ };
50
+ }
51
+
52
+ const artifactMatch = ref.match(ARTIFACT_ANCHOR_PATTERN);
53
+ if (artifactMatch) {
54
+ return {
55
+ raw,
56
+ ref,
57
+ valid: true,
58
+ type: "artifact_ref",
59
+ artifactPath: String(artifactMatch[1] || "").trim(),
60
+ artifactToken: String(artifactMatch[2] || "").trim()
61
+ };
62
+ }
63
+
64
+ return {
65
+ raw,
66
+ ref,
67
+ valid: false,
68
+ reason: "unsupported_anchor_format"
69
+ };
70
+ }
71
+
72
+ function parseAmbiguityRecords(text) {
73
+ const source = String(text || "").replace(/\r\n?/g, "\n");
74
+ const lines = source.split("\n");
75
+ const records = [];
76
+ const malformed = [];
77
+
78
+ for (let index = 0; index < lines.length; index += 1) {
79
+ const line = String(lines[index] || "");
80
+ const match = line.match(AMBIGUITY_RECORD_PATTERN);
81
+ if (match) {
82
+ const severity = String(match[1] || "").toLowerCase();
83
+ const ambiguityClass = String(match[2] || "").toLowerCase();
84
+ const payload = String(match[3] || "").trim();
85
+ const segments = payload.split(/\s+\|\s+/).map((item) => item.trim()).filter(Boolean);
86
+ const summary = String(segments.shift() || "").trim();
87
+ const metadata = {};
88
+ for (const segment of segments) {
89
+ const delimiter = segment.indexOf(":");
90
+ if (delimiter === -1) {
91
+ malformed.push({
92
+ line: index + 1,
93
+ text: line,
94
+ reason: "malformed_metadata_segment"
95
+ });
96
+ continue;
97
+ }
98
+ const key = String(segment.slice(0, delimiter) || "").trim().toLowerCase();
99
+ const value = String(segment.slice(delimiter + 1) || "").trim();
100
+ if (!key || !value) {
101
+ malformed.push({
102
+ line: index + 1,
103
+ text: line,
104
+ reason: "empty_metadata_key_or_value"
105
+ });
106
+ continue;
107
+ }
108
+ metadata[key] = value;
109
+ }
110
+ records.push({
111
+ line: index + 1,
112
+ severity,
113
+ ambiguityClass,
114
+ summary,
115
+ metadata
116
+ });
117
+ continue;
118
+ }
119
+
120
+ if (/\[(?:blocking|bounded|advisory)\]/i.test(line) && /:\s*/.test(line)) {
121
+ malformed.push({
122
+ line: index + 1,
123
+ text: line,
124
+ reason: "malformed_ambiguity_record"
125
+ });
126
+ }
127
+ }
128
+
129
+ return {
130
+ records,
131
+ malformed
132
+ };
133
+ }
15
134
 
16
135
  function toPositiveInteger(value, fallback) {
17
136
  const parsed = Number(value);
@@ -33,6 +152,25 @@ function normalizeText(value) {
33
152
  .trim();
34
153
  }
35
154
 
155
+ function tokenizeNormalizedWords(value, options = {}) {
156
+ const normalized = normalizeText(value);
157
+ const pattern = options.pattern instanceof RegExp ? options.pattern : /[a-z0-9_-]{3,}/g;
158
+ const stopWords = options.stopWords instanceof Set ? options.stopWords : null;
159
+ const words = normalized.match(pattern) || [];
160
+ return unique(words.filter((word) => !stopWords || !stopWords.has(word)));
161
+ }
162
+
163
+ function textContainsAnyNormalizedToken(source, tokens) {
164
+ if (!Array.isArray(tokens) || tokens.length === 0) {
165
+ return false;
166
+ }
167
+ const normalizedSource = normalizeText(source);
168
+ return tokens.some((token) => {
169
+ const normalizedToken = normalizeText(token);
170
+ return normalizedToken && normalizedSource.includes(normalizedToken);
171
+ });
172
+ }
173
+
36
174
  function parseListItems(sectionText) {
37
175
  const normalized = String(sectionText || "").replace(/\r\n?/g, "\n").trim();
38
176
  if (!normalized) {
@@ -66,7 +204,7 @@ function unique(values) {
66
204
  }
67
205
 
68
206
  function resolveImplementationLanding(projectRoot, implementationToken) {
69
- const knownExtensions = [".html", ".tsx", ".jsx", ".ts", ".js"];
207
+ const knownExtensions = [".html", ".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte"];
70
208
  const seen = new Set();
71
209
  const candidates = [];
72
210
  const addCandidate = (relativePath) => {
@@ -93,6 +231,9 @@ function resolveImplementationLanding(projectRoot, implementationToken) {
93
231
  addCandidate(path.join(dirPath, `page${extension}`));
94
232
  }
95
233
  };
234
+ const addSvelteRoutePageCandidate = (dirPath) => {
235
+ addCandidate(path.join(dirPath, "+page.svelte"));
236
+ };
96
237
  const resolveFileCandidate = (relativePath) => {
97
238
  const absolutePath = path.join(projectRoot, relativePath);
98
239
  try {
@@ -118,6 +259,8 @@ function resolveImplementationLanding(projectRoot, implementationToken) {
118
259
  addPageCandidates("app");
119
260
  addFileCandidates(path.join("src", "app", "page"));
120
261
  addPageCandidates(path.join("src", "app"));
262
+ addSvelteRoutePageCandidate(path.join("src", "routes"));
263
+ addSvelteRoutePageCandidate("routes");
121
264
  } else {
122
265
  const routePath = normalized.replace(/^\//, "").replace(/\/+$/, "");
123
266
  addFileCandidates(routePath);
@@ -135,6 +278,8 @@ function resolveImplementationLanding(projectRoot, implementationToken) {
135
278
  addPageCandidates(path.join("app", routePath));
136
279
  addFileCandidates(path.join("src", "app", routePath, "page"));
137
280
  addPageCandidates(path.join("src", "app", routePath));
281
+ addSvelteRoutePageCandidate(path.join("src", "routes", routePath));
282
+ addSvelteRoutePageCandidate(path.join("routes", routePath));
138
283
  }
139
284
 
140
285
  for (const candidate of candidates) {
@@ -470,6 +615,8 @@ function analyzeTaskGroup(section) {
470
615
  const placeholderItems = [];
471
616
  const rawFileReferences = [];
472
617
  const executionHints = [];
618
+ const planningAnchorRecords = [];
619
+ const malformedAnchorRecords = [];
473
620
 
474
621
  const lines = Array.isArray(section.lines) ? section.lines : [];
475
622
  for (let index = 0; index < lines.length; index += 1) {
@@ -510,6 +657,31 @@ function analyzeTaskGroup(section) {
510
657
  targetFiles.push(...extractFileReferences(entry));
511
658
  }
512
659
  }
660
+
661
+ if (/^\s*planning anchors\s*:\s*$/i.test(line)) {
662
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
663
+ const followLine = String(lines[cursor] || "");
664
+ if (/^\s*$/.test(followLine)) {
665
+ continue;
666
+ }
667
+ if (/^\s{0,3}##\s+/.test(followLine)) {
668
+ break;
669
+ }
670
+ if (/^\s*[A-Za-z][A-Za-z0-9 _-]{0,80}\s*:\s*$/.test(followLine)) {
671
+ break;
672
+ }
673
+ const itemMatch = followLine.match(/^\s*-\s+(.+)$/);
674
+ if (!itemMatch) {
675
+ break;
676
+ }
677
+ const anchorRecord = parsePlanningAnchorRef(String(itemMatch[1] || "").trim());
678
+ if (anchorRecord.valid) {
679
+ planningAnchorRecords.push(anchorRecord);
680
+ } else {
681
+ malformedAnchorRecords.push(anchorRecord);
682
+ }
683
+ }
684
+ }
513
685
  }
514
686
 
515
687
  for (const item of section.checklistItems) {
@@ -525,6 +697,16 @@ function analyzeTaskGroup(section) {
525
697
  if (executionIntent) {
526
698
  executionHints.push(executionIntent);
527
699
  }
700
+
701
+ const inlineAnchorMatches = String(item.text || "").matchAll(/\banchor\s*:\s*([^\s,;]+)/gi);
702
+ for (const anchorMatch of inlineAnchorMatches) {
703
+ const anchorRecord = parsePlanningAnchorRef(String(anchorMatch[1] || "").trim());
704
+ if (anchorRecord.valid) {
705
+ planningAnchorRecords.push(anchorRecord);
706
+ } else {
707
+ malformedAnchorRecords.push(anchorRecord);
708
+ }
709
+ }
528
710
  }
529
711
 
530
712
  const mergedFileReferences = unique([...targetFiles, ...rawFileReferences]);
@@ -544,7 +726,10 @@ function analyzeTaskGroup(section) {
544
726
  reviewIntent: hasReviewIntent(joinedContent),
545
727
  testingIntent: hasTestingIntent(joinedContent),
546
728
  codeChangeLikely: looksLikeCodeChange(joinedContent),
547
- placeholderItems: unique(placeholderItems.filter(Boolean))
729
+ placeholderItems: unique(placeholderItems.filter(Boolean)),
730
+ planningAnchors: unique(planningAnchorRecords.map((item) => item.ref)),
731
+ anchorRecords: planningAnchorRecords,
732
+ malformedAnchors: malformedAnchorRecords
548
733
  };
549
734
  }
550
735
 
@@ -632,6 +817,9 @@ function parseTasksArtifact(text) {
632
817
  testingIntent: section.testingIntent,
633
818
  codeChangeLikely: section.codeChangeLikely,
634
819
  placeholderItems: section.placeholderItems,
820
+ planningAnchors: section.planningAnchors,
821
+ anchorRecords: section.anchorRecords,
822
+ malformedAnchors: section.malformedAnchors,
635
823
  checklistItems: section.checklistItems
636
824
  };
637
825
  });
@@ -762,6 +950,8 @@ function readChangeArtifacts(projectRoot, changeId) {
762
950
  return {
763
951
  changeDir,
764
952
  proposalPath: path.join(changeDir, "proposal.md"),
953
+ designBriefPath: path.join(changeDir, "design-brief.md"),
954
+ designPath: path.join(changeDir, "design.md"),
765
955
  pageMapPath: pathExists(pageMapPath) ? pageMapPath : fallbackPageMapPath,
766
956
  tasksPath: path.join(changeDir, "tasks.md"),
767
957
  bindingsPath: path.join(changeDir, "pencil-bindings.md"),
@@ -773,6 +963,8 @@ function readChangeArtifacts(projectRoot, changeId) {
773
963
  function readArtifactTexts(paths) {
774
964
  return {
775
965
  proposal: readTextIfExists(paths.proposalPath),
966
+ designBrief: readTextIfExists(paths.designBriefPath),
967
+ design: readTextIfExists(paths.designPath),
776
968
  pageMap: readTextIfExists(paths.pageMapPath),
777
969
  tasks: readTextIfExists(paths.tasksPath),
778
970
  bindings: readTextIfExists(paths.bindingsPath),
@@ -783,7 +975,11 @@ function readArtifactTexts(paths) {
783
975
 
784
976
  module.exports = {
785
977
  normalizeText,
978
+ tokenizeNormalizedWords,
979
+ textContainsAnyNormalizedToken,
786
980
  parseListItems,
981
+ parsePlanningAnchorRef,
982
+ parseAmbiguityRecords,
787
983
  unique,
788
984
  resolveImplementationLanding,
789
985
  listImmediateDirs,
@@ -0,0 +1,81 @@
1
+ const { STATUS } = require("./workflow-contract");
2
+ const { resolveChangeDir } = require("./planning-parsers");
3
+
4
+ const COMMON_STOP_WORDS = Object.freeze([
5
+ "the",
6
+ "and",
7
+ "for",
8
+ "with",
9
+ "from",
10
+ "that",
11
+ "this",
12
+ "then",
13
+ "when",
14
+ "into",
15
+ "only",
16
+ "each",
17
+ "user",
18
+ "users",
19
+ "page",
20
+ "state",
21
+ "states",
22
+ "input",
23
+ "inputs",
24
+ "output",
25
+ "outputs",
26
+ "should",
27
+ "must",
28
+ "shall",
29
+ "able",
30
+ "about",
31
+ "after",
32
+ "before",
33
+ "during",
34
+ "while",
35
+ "where",
36
+ "there",
37
+ "have",
38
+ "has"
39
+ ]);
40
+
41
+ function buildStopWordSet(extraWords = []) {
42
+ return new Set([...COMMON_STOP_WORDS, ...(Array.isArray(extraWords) ? extraWords : [])]);
43
+ }
44
+
45
+ function buildBasePlanningResultEnvelope(projectRoot, strict) {
46
+ return {
47
+ status: STATUS.PASS,
48
+ failures: [],
49
+ warnings: [],
50
+ notes: [],
51
+ projectRoot,
52
+ changeId: null,
53
+ strict
54
+ };
55
+ }
56
+
57
+ function finalizePlanningResult(result) {
58
+ const hasFindings = result.failures.length > 0 || result.warnings.length > 0;
59
+ if (!hasFindings) {
60
+ result.status = STATUS.PASS;
61
+ return result;
62
+ }
63
+
64
+ result.status = result.strict ? STATUS.BLOCK : STATUS.WARN;
65
+ return result;
66
+ }
67
+
68
+ function resolveChangeWithFindings(projectRoot, requestedChangeId, failures, notes) {
69
+ const resolved = resolveChangeDir(projectRoot, requestedChangeId);
70
+ failures.push(...resolved.failures);
71
+ notes.push(...resolved.notes);
72
+ return resolved.changeDir;
73
+ }
74
+
75
+ module.exports = {
76
+ COMMON_STOP_WORDS,
77
+ buildStopWordSet,
78
+ buildBasePlanningResultEnvelope,
79
+ finalizePlanningResult,
80
+ resolveChangeWithFindings
81
+ };