cclaw-cli 5.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter/brainstorm.js +1 -1
- package/dist/artifact-linter/design.d.ts +16 -0
- package/dist/artifact-linter/design.js +83 -19
- package/dist/artifact-linter/scope.js +60 -21
- package/dist/artifact-linter/shared.d.ts +158 -14
- package/dist/artifact-linter/shared.js +319 -109
- package/dist/artifact-linter.js +83 -17
- package/dist/content/skills-elicitation.js +29 -15
- package/dist/content/skills.js +10 -4
- package/dist/content/stage-schema.d.ts +52 -0
- package/dist/content/stage-schema.js +36 -0
- package/dist/content/stages/brainstorm.js +2 -2
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/scope.js +2 -2
- package/dist/content/subagents.js +3 -1
- package/dist/content/templates.d.ts +2 -2
- package/dist/content/templates.js +17 -11
- package/dist/delegation.d.ts +52 -0
- package/dist/delegation.js +128 -3
- package/dist/flow-state.d.ts +14 -0
- package/dist/harness-adapters.js +1 -1
- package/dist/internal/advance-stage/advance.d.ts +38 -0
- package/dist/internal/advance-stage/advance.js +93 -5
- package/dist/internal/advance-stage/review-loop.d.ts +9 -0
- package/dist/internal/advance-stage/review-loop.js +42 -4
- package/package.json +1 -1
|
@@ -86,15 +86,70 @@ function detectStopSignal(rows) {
|
|
|
86
86
|
return false;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
* Validate the kebab-case ASCII shape of a forcing-question topic ID.
|
|
90
|
+
* Wave 24 enforces that IDs are short, language-neutral identifiers
|
|
91
|
+
* authors can paste into a `[topic:<id>]` tag without typos.
|
|
92
|
+
*/
|
|
93
|
+
const TOPIC_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
94
|
+
function isValidTopicId(id) {
|
|
95
|
+
return TOPIC_ID_PATTERN.test(id);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse a single checklist row into the list of forcing-question topic
|
|
99
|
+
* descriptors it declares. Returns `null` when the row is not a
|
|
100
|
+
* forcing-questions header. Throws when the header is found but its
|
|
101
|
+
* body does not match the Wave 24 `id: topic; id: topic; ...` syntax
|
|
102
|
+
* — authors fix the stage definition rather than silently ship
|
|
103
|
+
* un-coverable topics.
|
|
104
|
+
*
|
|
105
|
+
* Exposed for unit tests that exercise the parser without depending on
|
|
106
|
+
* the live stage schema.
|
|
107
|
+
*/
|
|
108
|
+
export function parseForcingQuestionsRow(row, context = "row") {
|
|
109
|
+
const headerMatch = /\*\*\s*[A-Za-z]+\s+forcing\s+questions\s*\([^)]*\)\s*\*\*\s*(?:[—\-–:]+)?\s*(.+)/iu.exec(row);
|
|
110
|
+
if (!headerMatch)
|
|
111
|
+
return null;
|
|
112
|
+
const body = (headerMatch[1] ?? "").trim();
|
|
113
|
+
if (body.length === 0)
|
|
114
|
+
return [];
|
|
115
|
+
// Take everything up to the first sentence-ending `.` followed by a
|
|
116
|
+
// space + capital letter. We split on `;` only; commas are part of
|
|
117
|
+
// human labels. Authors stop the list with `.` so the trailing
|
|
118
|
+
// prose ("Tag the matching ...") is excluded.
|
|
119
|
+
const listSection = body.split(/\.\s+(?=[A-Z])/u)[0] ?? body;
|
|
120
|
+
const segments = listSection
|
|
121
|
+
.split(/;\s*/u)
|
|
122
|
+
.map((segment) => segment.trim())
|
|
123
|
+
.filter((segment) => segment.length > 0);
|
|
124
|
+
const topics = [];
|
|
125
|
+
for (const segment of segments) {
|
|
126
|
+
const match = /^[`*_]?\s*([A-Za-z0-9][A-Za-z0-9-]*)\s*[`*_]?\s*:\s*(.+?)\s*$/u.exec(segment);
|
|
127
|
+
if (!match) {
|
|
128
|
+
throw new Error(`parseForcingQuestionsRow(${context}): segment "${segment}" does not match required \`id: topic\` syntax. Wave 24 (v6.0.0) requires \`id: topic; id: topic; ...\` form.`);
|
|
129
|
+
}
|
|
130
|
+
const id = (match[1] ?? "").toLowerCase();
|
|
131
|
+
const topic = (match[2] ?? "").replace(/[`*_]+$/u, "").trim();
|
|
132
|
+
if (!isValidTopicId(id)) {
|
|
133
|
+
throw new Error(`parseForcingQuestionsRow(${context}): invalid topic id "${id}" in segment "${segment}". IDs must match ${TOPIC_ID_PATTERN.source}.`);
|
|
134
|
+
}
|
|
135
|
+
if (topic.length === 0) {
|
|
136
|
+
throw new Error(`parseForcingQuestionsRow(${context}): empty topic label after id "${id}" in segment "${segment}".`);
|
|
137
|
+
}
|
|
138
|
+
topics.push({ id, topic });
|
|
139
|
+
}
|
|
140
|
+
return topics;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Extract forcing-question topics from a stage's checklist.
|
|
144
|
+
*
|
|
145
|
+
* Wave 24 (v6.0.0): only the new `id: topic; id: topic; ...` syntax is
|
|
146
|
+
* accepted. Throws when the syntax is malformed so authors fix the
|
|
147
|
+
* stage definition rather than silently shipping un-coverable topics.
|
|
94
148
|
*
|
|
95
149
|
* Returns empty array when no forcing-questions row is present (caller
|
|
96
|
-
*
|
|
97
|
-
*
|
|
150
|
+
* treats absence as "no forcing requirement" — convergence falls back
|
|
151
|
+
* to the no-new-decisions / stop-signal detectors). Returning [] when
|
|
152
|
+
* the row exists but lists no segments is also legal.
|
|
98
153
|
*/
|
|
99
154
|
export function extractForcingQuestions(stage) {
|
|
100
155
|
let checklist;
|
|
@@ -105,61 +160,29 @@ export function extractForcingQuestions(stage) {
|
|
|
105
160
|
return [];
|
|
106
161
|
}
|
|
107
162
|
for (const row of checklist) {
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
163
|
+
const parsed = parseForcingQuestionsRow(row, `stage=${stage}`);
|
|
164
|
+
if (parsed === null)
|
|
110
165
|
continue;
|
|
111
|
-
|
|
112
|
-
.replace(/\.$/u, "")
|
|
113
|
-
.trim();
|
|
114
|
-
if (body.length === 0)
|
|
115
|
-
return [];
|
|
116
|
-
return body
|
|
117
|
-
.split(/,\s*(?:and\s+)?|\s+and\s+/iu)
|
|
118
|
-
.map((topic) => topic.trim())
|
|
119
|
-
.filter((topic) => topic.length > 0)
|
|
120
|
-
.map((topic) => topic
|
|
121
|
-
.replace(/^[*_`]+|[*_`]+$/gu, "")
|
|
122
|
-
.replace(/^(?:what|who|where|which|how|is|are|do|does|did|can|will|would|could|should|may|might)\s+/iu, "")
|
|
123
|
-
.replace(/\?+$/u, "")
|
|
124
|
-
.trim())
|
|
125
|
-
.filter((topic) => topic.length > 0);
|
|
166
|
+
return parsed;
|
|
126
167
|
}
|
|
127
168
|
return [];
|
|
128
169
|
}
|
|
129
170
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
171
|
+
* Detect whether a Q&A Log row carries an explicit `[topic:<id>]` tag
|
|
172
|
+
* for the requested forcing-topic id. Matching is case-insensitive on
|
|
173
|
+
* the id, ASCII-only on the tag boundary. NO keyword fallback: the user
|
|
174
|
+
* must stamp the tag in any cell of the row.
|
|
133
175
|
*/
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
"for", "and", "or", "but", "if", "then", "else", "with", "without", "by", "as",
|
|
138
|
-
"we", "us", "our", "they", "them", "their", "you", "your", "i", "me", "my",
|
|
139
|
-
"this", "that", "these", "those", "it", "its", "do", "does", "did", "can",
|
|
140
|
-
"will", "would", "should", "could", "may", "might", "any", "some", "no", "not",
|
|
141
|
-
"from", "into", "onto", "upon", "than", "very", "much", "many", "more", "most",
|
|
142
|
-
"must", "have", "has", "had", "been", "being", "where", "when", "while",
|
|
143
|
-
"what", "which", "who", "whose", "whom", "why", "how", "non"
|
|
144
|
-
]);
|
|
145
|
-
return topic
|
|
146
|
-
.toLowerCase()
|
|
147
|
-
.split(/[\s\-/.,;:()\[\]{}'"`*_]+/u)
|
|
148
|
-
.map((token) => token.replace(/[^\p{L}\p{N}-]/gu, ""))
|
|
149
|
-
.filter((token) => token.length >= 3 && !STOP_WORDS.has(token));
|
|
150
|
-
}
|
|
151
|
-
function isTopicAddressed(topic, rows) {
|
|
152
|
-
const keywords = topicKeywords(topic);
|
|
153
|
-
if (keywords.length === 0)
|
|
154
|
-
return true;
|
|
155
|
-
const minHits = keywords.length === 1 ? 1 : Math.min(2, keywords.length);
|
|
176
|
+
function isTopicAddressed(id, rows) {
|
|
177
|
+
const needle = id.toLowerCase();
|
|
178
|
+
const tagPattern = /\[topic:\s*([A-Za-z0-9][A-Za-z0-9-]*)\s*\]/giu;
|
|
156
179
|
for (const row of rows) {
|
|
157
|
-
const haystack = row.join(" | ")
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
180
|
+
const haystack = row.join(" | ");
|
|
181
|
+
tagPattern.lastIndex = 0;
|
|
182
|
+
let match;
|
|
183
|
+
while ((match = tagPattern.exec(haystack)) !== null) {
|
|
184
|
+
const candidate = (match[1] ?? "").toLowerCase();
|
|
185
|
+
if (candidate === needle)
|
|
163
186
|
return true;
|
|
164
187
|
}
|
|
165
188
|
}
|
|
@@ -210,18 +233,21 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
|
210
233
|
const count = substantiveRows.length;
|
|
211
234
|
const hasStopSignal = detectStopSignal(rows);
|
|
212
235
|
const skipQuestionsAdvisory = options.skipQuestions === true;
|
|
213
|
-
const forcingTopics = options.forcingQuestions ?? extractForcingQuestions(stage);
|
|
236
|
+
const forcingTopics = (options.forcingQuestions ?? extractForcingQuestions(stage)).map((entry) => (typeof entry === "string" ? { id: entry, topic: entry } : entry));
|
|
214
237
|
const forcingCovered = [];
|
|
215
238
|
const forcingPending = [];
|
|
216
239
|
for (const topic of forcingTopics) {
|
|
217
|
-
if (isTopicAddressed(topic, rows))
|
|
218
|
-
forcingCovered.push(topic);
|
|
240
|
+
if (isTopicAddressed(topic.id, rows))
|
|
241
|
+
forcingCovered.push(topic.id);
|
|
219
242
|
else
|
|
220
|
-
forcingPending.push(topic);
|
|
243
|
+
forcingPending.push(topic.id);
|
|
221
244
|
}
|
|
222
245
|
const noNewDecisions = lastTwoRowsAllNoDecision(substantiveRows);
|
|
223
246
|
const allForcingCovered = forcingTopics.length > 0 ? forcingPending.length === 0 : count >= 1;
|
|
224
247
|
const ok = allForcingCovered || noNewDecisions || hasStopSignal;
|
|
248
|
+
const pendingIdsBracket = forcingPending.length > 0
|
|
249
|
+
? `[${forcingPending.join(", ")}]`
|
|
250
|
+
: "[none]";
|
|
225
251
|
let details;
|
|
226
252
|
if (ok) {
|
|
227
253
|
if (allForcingCovered && forcingTopics.length > 0) {
|
|
@@ -232,7 +258,7 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
|
232
258
|
}
|
|
233
259
|
else if (noNewDecisions) {
|
|
234
260
|
const remaining = forcingPending.length > 0
|
|
235
|
-
? ` ${forcingPending.length} forcing topic
|
|
261
|
+
? ` ${forcingPending.length} forcing topic IDs still pending: ${pendingIdsBracket} (Ralph-Loop convergence overrode coverage).`
|
|
236
262
|
: " Ralph-Loop convergence detector says no new decision-changing rows in the last 2 turns.";
|
|
237
263
|
details = `Q&A Log converged via no-new-decisions detector at ${count} row(s).${remaining}`;
|
|
238
264
|
}
|
|
@@ -241,10 +267,10 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
|
|
|
241
267
|
}
|
|
242
268
|
}
|
|
243
269
|
else if (skipQuestionsAdvisory) {
|
|
244
|
-
details = `Q&A Log unconverged at ${count} row(s); --skip-questions flag downgraded the finding to advisory.
|
|
270
|
+
details = `Q&A Log unconverged at ${count} row(s); --skip-questions flag downgraded the finding to advisory. Forcing topic IDs pending: ${pendingIdsBracket}.`;
|
|
245
271
|
}
|
|
246
272
|
else {
|
|
247
|
-
details = `Q&A Log unconverged at ${count} row(s).
|
|
273
|
+
details = `Q&A Log unconverged at ${count} row(s). Forcing topic IDs pending: ${pendingIdsBracket}. Tag each Q&A row with \`[topic:<id>]\` to mark coverage, append a no-new-decisions pair, or record an explicit user stop-signal row.`;
|
|
248
274
|
}
|
|
249
275
|
// Surface advisory budget hint for harness UI without re-introducing a
|
|
250
276
|
// blocking count. `recommended` is the soft budget per track/stage.
|
|
@@ -1019,16 +1045,46 @@ export const INTERACTION_EDGE_CASE_REQUIREMENTS = [
|
|
|
1019
1045
|
},
|
|
1020
1046
|
{ label: "zombie connection", pattern: /\bzombie[\s-]?connection\b/iu }
|
|
1021
1047
|
];
|
|
1022
|
-
|
|
1048
|
+
const INTERACTION_EDGE_CASE_NA_PATTERN = /^\s*n\s*\/\s*a\b/iu;
|
|
1049
|
+
const INTERACTION_EDGE_CASE_NA_WITH_REASON_PATTERN = /^\s*n\s*\/\s*a\s*[—–\-:]\s*\S/iu;
|
|
1050
|
+
const INTERACTION_EDGE_CASE_NETWORK_DEPENDENT_LABELS = new Set([
|
|
1051
|
+
"nav-away-mid-request",
|
|
1052
|
+
"10K-result dataset",
|
|
1053
|
+
"background-job abandonment",
|
|
1054
|
+
"zombie connection"
|
|
1055
|
+
]);
|
|
1056
|
+
function shouldRelaxNetworkDependentEdgeCases(context) {
|
|
1057
|
+
if (!context.liteTier)
|
|
1058
|
+
return false;
|
|
1059
|
+
const sections = context.sections ?? null;
|
|
1060
|
+
if (!sections)
|
|
1061
|
+
return true;
|
|
1062
|
+
const diagramBody = sectionBodyByName(sections, "Architecture Diagram");
|
|
1063
|
+
const failureModeBody = sectionBodyByName(sections, "Failure Mode Table");
|
|
1064
|
+
const failureModeRowCount = failureModeBody !== null ? getMarkdownTableRows(failureModeBody).length : 0;
|
|
1065
|
+
if (failureModeRowCount > 0)
|
|
1066
|
+
return false;
|
|
1067
|
+
if (diagramBody && DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN.test(diagramBody))
|
|
1068
|
+
return false;
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
export function validateInteractionEdgeCaseMatrix(sectionBody, context = {}) {
|
|
1023
1072
|
const rows = getMarkdownTableRows(sectionBody);
|
|
1073
|
+
const relaxNetworkRows = shouldRelaxNetworkDependentEdgeCases(context);
|
|
1024
1074
|
if (rows.length === 0) {
|
|
1075
|
+
if (relaxNetworkRows) {
|
|
1076
|
+
return {
|
|
1077
|
+
ok: true,
|
|
1078
|
+
details: "Data Flow Interaction Edge Case matrix is advisory for lite-tier no-network designs (no Failure Mode Table rows and no external-dependency nodes detected)."
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1025
1081
|
return {
|
|
1026
1082
|
ok: false,
|
|
1027
1083
|
details: "Data Flow must include an Interaction Edge Case matrix table with required rows."
|
|
1028
1084
|
};
|
|
1029
1085
|
}
|
|
1030
1086
|
const seen = new Map();
|
|
1031
|
-
for (const [
|
|
1087
|
+
for (const [, row] of rows.entries()) {
|
|
1032
1088
|
const labelCell = (row[0] ?? "").trim();
|
|
1033
1089
|
if (!labelCell)
|
|
1034
1090
|
continue;
|
|
@@ -1041,15 +1097,31 @@ export function validateInteractionEdgeCaseMatrix(sectionBody) {
|
|
|
1041
1097
|
details: `Interaction Edge Case row "${requirement.label}" must include 4 columns: Edge case | Handled? | Design response | Deferred item.`
|
|
1042
1098
|
};
|
|
1043
1099
|
}
|
|
1044
|
-
const
|
|
1100
|
+
const handledRaw = (row[1] ?? "").trim();
|
|
1101
|
+
const handled = parseBinaryFlag(handledRaw);
|
|
1045
1102
|
const response = (row[2] ?? "").trim();
|
|
1046
1103
|
const deferred = (row[3] ?? "").trim();
|
|
1047
|
-
|
|
1104
|
+
const isNA = INTERACTION_EDGE_CASE_NA_PATTERN.test(handledRaw);
|
|
1105
|
+
if (handled === "unknown" && !isNA) {
|
|
1048
1106
|
return {
|
|
1049
1107
|
ok: false,
|
|
1050
|
-
details: `Interaction Edge Case row "${requirement.label}" must mark Handled? as yes/no.`
|
|
1108
|
+
details: `Interaction Edge Case row "${requirement.label}" must mark Handled? as yes/no, or write \`N/A — <reason>\` (em-dash + free-text reason) when the case does not apply.`
|
|
1051
1109
|
};
|
|
1052
1110
|
}
|
|
1111
|
+
if (isNA) {
|
|
1112
|
+
// Wave 25: `N/A — <reason>` short-circuits both the "must mark
|
|
1113
|
+
// yes/no" rule and the "must reference a deferred item id"
|
|
1114
|
+
// rule. The reason satisfies justification.
|
|
1115
|
+
const hasReason = INTERACTION_EDGE_CASE_NA_WITH_REASON_PATTERN.test(handledRaw) || response.length > 0;
|
|
1116
|
+
if (!hasReason) {
|
|
1117
|
+
return {
|
|
1118
|
+
ok: false,
|
|
1119
|
+
details: `Interaction Edge Case row "${requirement.label}" marked N/A but missing reason. Use \`N/A — <reason>\` (em-dash + free-text reason) in the Handled? cell or fill the Design response cell.`
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
seen.set(requirement.label, true);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1053
1125
|
if (!response) {
|
|
1054
1126
|
return {
|
|
1055
1127
|
ok: false,
|
|
@@ -1059,7 +1131,7 @@ export function validateInteractionEdgeCaseMatrix(sectionBody) {
|
|
|
1059
1131
|
if (handled === "no" && (!deferred || /\bnone\b/iu.test(deferred))) {
|
|
1060
1132
|
return {
|
|
1061
1133
|
ok: false,
|
|
1062
|
-
details: `Interaction Edge Case row "${requirement.label}" is unhandled and must reference a deferred item id (for example D-12)
|
|
1134
|
+
details: `Interaction Edge Case row "${requirement.label}" is unhandled and must reference a deferred item id (for example D-12) or mark Handled? as \`N/A — <reason>\`.`
|
|
1063
1135
|
};
|
|
1064
1136
|
}
|
|
1065
1137
|
seen.set(requirement.label, true);
|
|
@@ -1067,15 +1139,27 @@ export function validateInteractionEdgeCaseMatrix(sectionBody) {
|
|
|
1067
1139
|
const missing = INTERACTION_EDGE_CASE_REQUIREMENTS
|
|
1068
1140
|
.map((requirement) => requirement.label)
|
|
1069
1141
|
.filter((label) => !seen.has(label));
|
|
1070
|
-
|
|
1142
|
+
const stillMissing = relaxNetworkRows
|
|
1143
|
+
? missing.filter((label) => !INTERACTION_EDGE_CASE_NETWORK_DEPENDENT_LABELS.has(label))
|
|
1144
|
+
: missing;
|
|
1145
|
+
const advisoryMissing = relaxNetworkRows
|
|
1146
|
+
? missing.filter((label) => INTERACTION_EDGE_CASE_NETWORK_DEPENDENT_LABELS.has(label))
|
|
1147
|
+
: [];
|
|
1148
|
+
if (stillMissing.length > 0) {
|
|
1149
|
+
const advisoryNote = advisoryMissing.length > 0
|
|
1150
|
+
? ` (${advisoryMissing.length} network-dependent row(s) demoted to advisory by lite-tier no-network detection: ${advisoryMissing.join(", ")})`
|
|
1151
|
+
: "";
|
|
1071
1152
|
return {
|
|
1072
1153
|
ok: false,
|
|
1073
|
-
details: `Interaction Edge Case matrix is missing required row(s): ${
|
|
1154
|
+
details: `Interaction Edge Case matrix is missing required row(s): ${stillMissing.join(", ")}${advisoryNote}.`
|
|
1074
1155
|
};
|
|
1075
1156
|
}
|
|
1157
|
+
const advisoryNote = advisoryMissing.length > 0
|
|
1158
|
+
? ` (${advisoryMissing.length} network-dependent row(s) advisory under lite-tier no-network: ${advisoryMissing.join(", ")})`
|
|
1159
|
+
: "";
|
|
1076
1160
|
return {
|
|
1077
1161
|
ok: true,
|
|
1078
|
-
details:
|
|
1162
|
+
details: `Interaction Edge Case matrix contains all required rows with handled/deferred status${advisoryNote}.`
|
|
1079
1163
|
};
|
|
1080
1164
|
}
|
|
1081
1165
|
export const PRE_SCOPE_AUDIT_SIGNALS = [
|
|
@@ -1102,9 +1186,23 @@ export function validatePreScopeSystemAudit(sectionBody) {
|
|
|
1102
1186
|
details: "Pre-Scope System Audit captures git log/diff/stash/debt-marker checks."
|
|
1103
1187
|
};
|
|
1104
1188
|
}
|
|
1105
|
-
export const DIAGRAM_ARROW_PATTERN = /(
|
|
1189
|
+
export const DIAGRAM_ARROW_PATTERN = /(?:<--?>|<?==?>|--?>|->>|=>|-\.->|→|⟶|↦|={2,}>|-{3,}>|\.{3,}>|-(?:\s-){1,}\s?->)/u;
|
|
1106
1190
|
export const DIAGRAM_FAILURE_EDGE_PATTERN = /\b(fail(?:ed|ure)?|error|timeout|fallback|degrad(?:e|ed|ation)|retry|backoff|circuit|unavailable|recover(?:y)?|rescue|mitigat(?:e|ion)|rollback|exception|abort|dead[\s-]?letter|dlq)\b/iu;
|
|
1107
1191
|
export const DIAGRAM_GENERIC_NODE_PATTERN = /\b(service|component|module|system)\s*(?:[A-Z0-9])?\b/iu;
|
|
1192
|
+
/**
|
|
1193
|
+
* Wave 25 (v6.1.0) — external-dependency keywords that trigger the
|
|
1194
|
+
* failure-edge requirement. The architecture diagram is allowed to
|
|
1195
|
+
* omit failure edges only when ALL of:
|
|
1196
|
+
* - Failure Mode Table has zero rows.
|
|
1197
|
+
* - The diagram body mentions no external-dependency keyword.
|
|
1198
|
+
*
|
|
1199
|
+
* Static landing pages (3 HTML/CSS/JS files, no network) match this:
|
|
1200
|
+
* no failure modes to map, no external systems to fail. The previous
|
|
1201
|
+
* blanket "must include at least one failure-edge" rule produced
|
|
1202
|
+
* ceremony-only failures that the agent worked around with fake
|
|
1203
|
+
* `(timeout)` annotations, defeating the spirit of the rule.
|
|
1204
|
+
*/
|
|
1205
|
+
export const DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN = /\b(http|https|api|rest|grpc|graphql|websocket|socket|tcp|udp|rpc|fetch|request|database|db|sql|postgres|mysql|sqlite|mongo|redis|cache|queue|kafka|rabbitmq|sqs|sns|s3|cdn|external|upstream|downstream|third[\s-]?party|webhook|cloud|service[\s-]?bus|event[\s-]?bus|broker|stream|topic)\b/iu;
|
|
1108
1206
|
export const TEST_COMMAND_MARKER_PATTERN = /\b(?:npm|pnpm|yarn|bun|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)\b/iu;
|
|
1109
1207
|
export const RED_FAILURE_MARKER_PATTERN = /\b(?:fail|failed|failing|assertionerror|cannot find|exception|error|exit code\s*[:=]?\s*[1-9])\b/iu;
|
|
1110
1208
|
export const GREEN_SUCCESS_MARKER_PATTERN = /\b(?:pass|passed|green|ok|0 failed|exit code\s*[:=]?\s*0)\b/iu;
|
|
@@ -1129,18 +1227,158 @@ export function hasFailureEdgeInDiagram(sectionBody) {
|
|
|
1129
1227
|
export function hasLabeledDiagramArrow(lines) {
|
|
1130
1228
|
return lines.some((line) => /\|[^|]+\|/u.test(line) || /:\s*[A-Za-z]/u.test(line));
|
|
1131
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Wave 25 (v6.1.0) — accepted async edge patterns. Returns true when
|
|
1232
|
+
* a line carries any of:
|
|
1233
|
+
*
|
|
1234
|
+
* - `-.->`, `-->>`, `~~>` (mermaid dotted/messaging arrows)
|
|
1235
|
+
* - `- - ->` (loose dotted ASCII arrow with optional spaces)
|
|
1236
|
+
* - `.....>` (3-or-more dots followed by `>`)
|
|
1237
|
+
* - `\basync\b` text token (label-based)
|
|
1238
|
+
* - `[async]` bracketed label, `async:` prefix, `async:` cell content
|
|
1239
|
+
*
|
|
1240
|
+
* The error message printed when this fails (see
|
|
1241
|
+
* `validateArchitectureDiagram`) lists every accepted pattern
|
|
1242
|
+
* verbatim so the agent does not have to guess.
|
|
1243
|
+
*/
|
|
1132
1244
|
export function hasAsyncDiagramEdge(lines) {
|
|
1133
|
-
return lines.some((line) =>
|
|
1245
|
+
return lines.some((line) => {
|
|
1246
|
+
if (/-\.->|-->>|~~>/u.test(line))
|
|
1247
|
+
return true;
|
|
1248
|
+
if (/-(?:\s-){1,}\s?->/u.test(line))
|
|
1249
|
+
return true;
|
|
1250
|
+
if (/\.{3,}\s*>/u.test(line))
|
|
1251
|
+
return true;
|
|
1252
|
+
if (/\basync\b/iu.test(line))
|
|
1253
|
+
return true;
|
|
1254
|
+
if (/\[\s*async\s*\]/iu.test(line))
|
|
1255
|
+
return true;
|
|
1256
|
+
if (/(?:^|[\s|:])async\s*:/iu.test(line))
|
|
1257
|
+
return true;
|
|
1258
|
+
return false;
|
|
1259
|
+
});
|
|
1134
1260
|
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Wave 25 (v6.1.0) — accepted sync edge patterns. Returns true when a
|
|
1263
|
+
* line carries any of:
|
|
1264
|
+
*
|
|
1265
|
+
* - `\bsync\b` text token (label-based)
|
|
1266
|
+
* - `[sync]` bracketed label, `sync:` prefix, `sync:` cell content
|
|
1267
|
+
* - Solid `-->`, `->`, `=>`, `→`, `⟶`, `↦` arrow that is NOT a known
|
|
1268
|
+
* dotted/async variant (`-.->`, `-->>`, `~~>`)
|
|
1269
|
+
* - `===>` (3+ `=` then `>`) and `--->` (3+ `-` then `>`) heavy solid
|
|
1270
|
+
* arrows
|
|
1271
|
+
*/
|
|
1135
1272
|
export function hasSyncDiagramEdge(lines) {
|
|
1136
1273
|
return lines.some((line) => {
|
|
1137
|
-
if (/\bsync\b/iu.test(line))
|
|
1274
|
+
if (/\bsync\b/iu.test(line) && !/\basync\b/iu.test(line))
|
|
1275
|
+
return true;
|
|
1276
|
+
if (/\[\s*sync\s*\]/iu.test(line))
|
|
1277
|
+
return true;
|
|
1278
|
+
if (/(?:^|[\s|:])sync\s*:/iu.test(line))
|
|
1279
|
+
return true;
|
|
1280
|
+
if (/={2,}>/u.test(line))
|
|
1281
|
+
return true;
|
|
1282
|
+
if (/-{3,}>/u.test(line))
|
|
1138
1283
|
return true;
|
|
1139
1284
|
if (!/(-->|->|=>|→|⟶|↦)/u.test(line))
|
|
1140
1285
|
return false;
|
|
1141
|
-
|
|
1286
|
+
if (/-\.->|-->>|~~>/u.test(line))
|
|
1287
|
+
return false;
|
|
1288
|
+
if (/-(?:\s-){1,}\s?->/u.test(line))
|
|
1289
|
+
return false;
|
|
1290
|
+
return true;
|
|
1142
1291
|
});
|
|
1143
1292
|
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Wave 25 (v6.1.0) — exact accepted-pattern list shown in the error
|
|
1295
|
+
* message when sync/async distinction fails. Keep in sync with
|
|
1296
|
+
* `hasAsyncDiagramEdge` / `hasSyncDiagramEdge` above.
|
|
1297
|
+
*/
|
|
1298
|
+
export const DIAGRAM_SYNC_ASYNC_ACCEPTED_PATTERNS = [
|
|
1299
|
+
"Solid arrows: `-->`, `->`, `===>`, `--->`, `=>`, `→`, `⟶`, `↦`",
|
|
1300
|
+
"Dotted/async arrows: `-.->`, `-->>`, `~~>`, `- - ->`, `.....>`",
|
|
1301
|
+
"Text labels on the same line: `sync` / `async`",
|
|
1302
|
+
"Bracket labels: `[sync]` / `[async]`",
|
|
1303
|
+
"Cell-prefix labels: `sync:` / `async:` (e.g. `A -->|sync: persist| B`)"
|
|
1304
|
+
];
|
|
1305
|
+
/**
|
|
1306
|
+
* Wave 25 (v6.1.0) — Architecture Diagram structural check.
|
|
1307
|
+
*
|
|
1308
|
+
* Promoted out of `validateSectionBody` so it can take a `sections`
|
|
1309
|
+
* map and conditionally enforce the failure-edge rule based on
|
|
1310
|
+
* cross-section context (Failure Mode Table presence + diagram body
|
|
1311
|
+
* mentioning external-dependency keywords).
|
|
1312
|
+
*/
|
|
1313
|
+
export function validateArchitectureDiagram(sectionBody, context = {}) {
|
|
1314
|
+
const edgeLines = diagramEdgeLines(sectionBody);
|
|
1315
|
+
if (edgeLines.length === 0) {
|
|
1316
|
+
return {
|
|
1317
|
+
ok: false,
|
|
1318
|
+
details: "Architecture Diagram must include at least one directional edge line (for example `A -->|action| B`)."
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
if (!hasLabeledDiagramArrow(edgeLines)) {
|
|
1322
|
+
return {
|
|
1323
|
+
ok: false,
|
|
1324
|
+
details: "Architecture Diagram must label each edge with an action/message (for example `A -->|sync: persist| B`)."
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const genericLine = edgeLines.find((line) => DIAGRAM_GENERIC_NODE_PATTERN.test(line));
|
|
1328
|
+
if (genericLine) {
|
|
1329
|
+
return {
|
|
1330
|
+
ok: false,
|
|
1331
|
+
details: `Architecture Diagram uses a generic node label in edge "${genericLine}". Use concrete component names instead of placeholders like Service/Component.`
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
if (!hasAsyncDiagramEdge(edgeLines) || !hasSyncDiagramEdge(edgeLines)) {
|
|
1335
|
+
const acceptedList = DIAGRAM_SYNC_ASYNC_ACCEPTED_PATTERNS.map((line) => ` - ${line}`).join("\n");
|
|
1336
|
+
return {
|
|
1337
|
+
ok: false,
|
|
1338
|
+
details: `Architecture Diagram must distinguish sync vs async edges. Accepted patterns:\n${acceptedList}\nExample line that satisfies both: \`Browser -->|sync: render| App\` plus \`App -.->|async: log| Telemetry\`.`
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
if (!shouldEnforceFailureEdge(sectionBody, context)) {
|
|
1342
|
+
return {
|
|
1343
|
+
ok: true,
|
|
1344
|
+
details: "Architecture Diagram includes labeled directional edges with sync/async distinction; failure-edge enforcement skipped (no failure-mode rows and no external-dependency nodes detected)."
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
if (!hasFailureEdgeInDiagram(sectionBody)) {
|
|
1348
|
+
return {
|
|
1349
|
+
ok: false,
|
|
1350
|
+
details: "Architecture Diagram must include at least one failure-edge arrow with a failure keyword (for example: timeout, error, fallback, degraded, retry). Mark a failure path in the diagram (e.g. `App -->|timeout| FallbackCache`)."
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
ok: true,
|
|
1355
|
+
details: "Architecture Diagram contains labeled edges, sync/async distinction, and a failure-edge."
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Wave 25 (v6.1.0) — decide whether the failure-edge enforcement
|
|
1360
|
+
* should fire for the given Architecture Diagram body. Returns
|
|
1361
|
+
* `false` (skip the rule) when BOTH:
|
|
1362
|
+
* - The artifact's `## Failure Mode Table` (if present) has zero
|
|
1363
|
+
* data rows OR is absent entirely.
|
|
1364
|
+
* - The architecture diagram body mentions NO known external-
|
|
1365
|
+
* dependency keyword (network, db, queue, …).
|
|
1366
|
+
*
|
|
1367
|
+
* Static landing pages (no network, no failure modes) hit this
|
|
1368
|
+
* path. Designs with even one Failure Mode row OR one external
|
|
1369
|
+
* dependency keyword in the diagram fall through to the legacy
|
|
1370
|
+
* blanket failure-edge requirement.
|
|
1371
|
+
*/
|
|
1372
|
+
function shouldEnforceFailureEdge(diagramBody, context) {
|
|
1373
|
+
const sections = context.sections ?? null;
|
|
1374
|
+
const failureModeBody = sections ? sectionBodyByName(sections, "Failure Mode Table") : null;
|
|
1375
|
+
const failureModeRowCount = failureModeBody !== null ? getMarkdownTableRows(failureModeBody).length : 0;
|
|
1376
|
+
if (failureModeRowCount > 0)
|
|
1377
|
+
return true;
|
|
1378
|
+
if (DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN.test(diagramBody))
|
|
1379
|
+
return true;
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1144
1382
|
export function validateTddRedEvidence(sectionBody) {
|
|
1145
1383
|
const meaningful = meaningfulLineCount(sectionBody);
|
|
1146
1384
|
if (meaningful < 2) {
|
|
@@ -1543,7 +1781,7 @@ export function collectPatternHits(text, patterns) {
|
|
|
1543
1781
|
}
|
|
1544
1782
|
return hits;
|
|
1545
1783
|
}
|
|
1546
|
-
export function validateSectionBody(sectionBody, rule, sectionName) {
|
|
1784
|
+
export function validateSectionBody(sectionBody, rule, sectionName, context = {}) {
|
|
1547
1785
|
const bodyLines = sectionBody.split(/\r?\n/).map((line) => line.trim());
|
|
1548
1786
|
const meaningful = meaningfulLineCount(sectionBody);
|
|
1549
1787
|
if (meaningful === 0) {
|
|
@@ -1650,41 +1888,13 @@ export function validateSectionBody(sectionBody, rule, sectionName) {
|
|
|
1650
1888
|
return validateRequirementsTaxonomy(sectionBody);
|
|
1651
1889
|
}
|
|
1652
1890
|
if (sectionNameNormalized === "data flow") {
|
|
1653
|
-
return validateInteractionEdgeCaseMatrix(sectionBody
|
|
1891
|
+
return validateInteractionEdgeCaseMatrix(sectionBody, {
|
|
1892
|
+
sections: context.sections ?? null,
|
|
1893
|
+
liteTier: context.liteTier ?? false
|
|
1894
|
+
});
|
|
1654
1895
|
}
|
|
1655
1896
|
if (sectionNameNormalized === "architecture diagram") {
|
|
1656
|
-
|
|
1657
|
-
if (edgeLines.length === 0) {
|
|
1658
|
-
return {
|
|
1659
|
-
ok: false,
|
|
1660
|
-
details: "Architecture Diagram must include at least one directional edge line (for example `A -->|action| B`)."
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
if (!hasLabeledDiagramArrow(edgeLines)) {
|
|
1664
|
-
return {
|
|
1665
|
-
ok: false,
|
|
1666
|
-
details: "Architecture Diagram must label each edge with an action/message (for example `A -->|sync: persist| B`)."
|
|
1667
|
-
};
|
|
1668
|
-
}
|
|
1669
|
-
const genericLine = edgeLines.find((line) => DIAGRAM_GENERIC_NODE_PATTERN.test(line));
|
|
1670
|
-
if (genericLine) {
|
|
1671
|
-
return {
|
|
1672
|
-
ok: false,
|
|
1673
|
-
details: `Architecture Diagram uses a generic node label in edge "${genericLine}". Use concrete component names instead of placeholders like Service/Component.`
|
|
1674
|
-
};
|
|
1675
|
-
}
|
|
1676
|
-
if (!hasAsyncDiagramEdge(edgeLines) || !hasSyncDiagramEdge(edgeLines)) {
|
|
1677
|
-
return {
|
|
1678
|
-
ok: false,
|
|
1679
|
-
details: "Architecture Diagram must distinguish sync vs async edges (for example solid + dotted arrows, or `sync:` and `async:` labels)."
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
if (!hasFailureEdgeInDiagram(sectionBody)) {
|
|
1683
|
-
return {
|
|
1684
|
-
ok: false,
|
|
1685
|
-
details: "Architecture Diagram must include at least one failure-edge arrow with a failure keyword (for example: timeout, error, fallback, degraded, retry)."
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1897
|
+
return validateArchitectureDiagram(sectionBody, { sections: context.sections ?? null });
|
|
1688
1898
|
}
|
|
1689
1899
|
if (sectionNameNormalized === "acceptance criteria" &&
|
|
1690
1900
|
/observable[\s,]*measurable[\s,]+(and )?falsifiable/iu.test(rule)) {
|