@xenonbyte/da-vinci-workflow 0.2.5 → 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.
- package/CHANGELOG.md +16 -0
- package/README.md +15 -9
- package/README.zh-CN.md +16 -9
- package/docs/dv-command-reference.md +18 -2
- package/docs/execution-chain-migration.md +14 -3
- package/docs/maintainer-bootstrap.md +102 -0
- package/docs/pencil-rendering-workflow.md +1 -1
- package/docs/skill-usage.md +31 -0
- package/docs/workflow-overview.md +40 -5
- package/docs/zh-CN/dv-command-reference.md +16 -2
- package/docs/zh-CN/maintainer-bootstrap.md +101 -0
- package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
- package/docs/zh-CN/skill-usage.md +30 -0
- package/docs/zh-CN/workflow-overview.md +38 -5
- package/lib/audit.js +19 -0
- package/lib/cli/helpers.js +63 -2
- package/lib/cli.js +98 -0
- package/lib/gate-utils.js +56 -0
- package/lib/install.js +134 -6
- package/lib/lint-bindings.js +41 -28
- package/lib/lint-spec.js +403 -109
- package/lib/lint-tasks.js +571 -21
- package/lib/maintainer-readiness.js +317 -0
- package/lib/planning-parsers.js +190 -1
- package/lib/planning-quality-utils.js +81 -0
- package/lib/planning-signal-freshness.js +205 -0
- package/lib/scope-check.js +751 -82
- package/lib/sidecars.js +396 -1
- package/lib/task-review.js +2 -1
- package/lib/utils.js +15 -0
- package/lib/workflow-persisted-state.js +52 -32
- package/lib/workflow-state.js +1187 -249
- package/package.json +1 -1
|
@@ -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
|
+
};
|
package/lib/planning-parsers.js
CHANGED
|
@@ -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
|
+
};
|