@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.
- package/CHANGELOG.md +35 -0
- package/README.md +15 -9
- package/README.zh-CN.md +16 -9
- package/SKILL.md +45 -704
- package/docs/dv-command-reference.md +33 -5
- 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/prompt-entrypoints.md +1 -0
- package/docs/skill-contract-maintenance.md +14 -0
- package/docs/skill-usage.md +31 -0
- package/docs/workflow-overview.md +40 -5
- package/docs/zh-CN/dv-command-reference.md +31 -5
- package/docs/zh-CN/maintainer-bootstrap.md +101 -0
- package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
- package/docs/zh-CN/prompt-entrypoints.md +1 -0
- 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 +104 -0
- package/lib/cli/lint-family.js +56 -0
- package/lib/cli/verify-family.js +79 -0
- package/lib/cli.js +143 -172
- 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 +198 -2
- package/lib/planning-quality-utils.js +81 -0
- package/lib/planning-signal-freshness.js +205 -0
- package/lib/scaffold.js +454 -23
- package/lib/scope-check.js +751 -82
- package/lib/sidecars.js +396 -1
- package/lib/task-review.js +2 -1
- package/lib/utils.js +34 -0
- package/lib/verify.js +1160 -88
- package/lib/workflow-persisted-state.js +52 -32
- package/lib/workflow-state.js +1187 -249
- package/package.json +1 -1
- 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
|
+
};
|
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) {
|
|
@@ -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
|
+
};
|