@xenonbyte/da-vinci-workflow 0.2.5 → 0.2.7

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.
@@ -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) {
@@ -477,6 +615,8 @@ function analyzeTaskGroup(section) {
477
615
  const placeholderItems = [];
478
616
  const rawFileReferences = [];
479
617
  const executionHints = [];
618
+ const planningAnchorRecords = [];
619
+ const malformedAnchorRecords = [];
480
620
 
481
621
  const lines = Array.isArray(section.lines) ? section.lines : [];
482
622
  for (let index = 0; index < lines.length; index += 1) {
@@ -517,6 +657,31 @@ function analyzeTaskGroup(section) {
517
657
  targetFiles.push(...extractFileReferences(entry));
518
658
  }
519
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
+ }
520
685
  }
521
686
 
522
687
  for (const item of section.checklistItems) {
@@ -532,6 +697,16 @@ function analyzeTaskGroup(section) {
532
697
  if (executionIntent) {
533
698
  executionHints.push(executionIntent);
534
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
+ }
535
710
  }
536
711
 
537
712
  const mergedFileReferences = unique([...targetFiles, ...rawFileReferences]);
@@ -551,7 +726,10 @@ function analyzeTaskGroup(section) {
551
726
  reviewIntent: hasReviewIntent(joinedContent),
552
727
  testingIntent: hasTestingIntent(joinedContent),
553
728
  codeChangeLikely: looksLikeCodeChange(joinedContent),
554
- 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
555
733
  };
556
734
  }
557
735
 
@@ -639,6 +817,9 @@ function parseTasksArtifact(text) {
639
817
  testingIntent: section.testingIntent,
640
818
  codeChangeLikely: section.codeChangeLikely,
641
819
  placeholderItems: section.placeholderItems,
820
+ planningAnchors: section.planningAnchors,
821
+ anchorRecords: section.anchorRecords,
822
+ malformedAnchors: section.malformedAnchors,
642
823
  checklistItems: section.checklistItems
643
824
  };
644
825
  });
@@ -769,6 +950,8 @@ function readChangeArtifacts(projectRoot, changeId) {
769
950
  return {
770
951
  changeDir,
771
952
  proposalPath: path.join(changeDir, "proposal.md"),
953
+ designBriefPath: path.join(changeDir, "design-brief.md"),
954
+ designPath: path.join(changeDir, "design.md"),
772
955
  pageMapPath: pathExists(pageMapPath) ? pageMapPath : fallbackPageMapPath,
773
956
  tasksPath: path.join(changeDir, "tasks.md"),
774
957
  bindingsPath: path.join(changeDir, "pencil-bindings.md"),
@@ -780,6 +963,8 @@ function readChangeArtifacts(projectRoot, changeId) {
780
963
  function readArtifactTexts(paths) {
781
964
  return {
782
965
  proposal: readTextIfExists(paths.proposalPath),
966
+ designBrief: readTextIfExists(paths.designBriefPath),
967
+ design: readTextIfExists(paths.designPath),
783
968
  pageMap: readTextIfExists(paths.pageMapPath),
784
969
  tasks: readTextIfExists(paths.tasksPath),
785
970
  bindings: readTextIfExists(paths.bindingsPath),
@@ -790,7 +975,11 @@ function readArtifactTexts(paths) {
790
975
 
791
976
  module.exports = {
792
977
  normalizeText,
978
+ tokenizeNormalizedWords,
979
+ textContainsAnyNormalizedToken,
793
980
  parseListItems,
981
+ parsePlanningAnchorRef,
982
+ parseAmbiguityRecords,
794
983
  unique,
795
984
  resolveImplementationLanding,
796
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
+ };