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.
@@ -24,7 +24,7 @@ export async function lintBrainstormStage(ctx) {
24
24
  findings.push({
25
25
  section: "qa_log_unconverged",
26
26
  required: !floor.skipQuestionsAdvisory,
27
- rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
27
+ rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until every forcing-question topic id is tagged with `[topic:<id>]` on at least one row, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
28
28
  found: floor.ok,
29
29
  details: floor.details
30
30
  });
@@ -1,2 +1,18 @@
1
1
  import { type StageLintContext } from "./shared.js";
2
+ export interface CodebaseInvestigationFileRef {
3
+ /** Filename to stat (parenthetical suffix already stripped). */
4
+ filename: string;
5
+ /** Raw cell content, useful for diagnostic messages. */
6
+ raw: string;
7
+ /** When true, the audit treats this row as a "new file, no baseline". */
8
+ newFile: boolean;
9
+ /**
10
+ * When true, the audit skips this row entirely (suffix `(skip)`,
11
+ * `(deleted)`, `(stub)`, leading `#`, or a `skip:` token in the
12
+ * Notes column).
13
+ */
14
+ skip: boolean;
15
+ }
16
+ export declare function normalizeCodebaseInvestigationFileRef(value: string, notesCell: string): CodebaseInvestigationFileRef | null;
17
+ export declare function collectCodebaseInvestigationFiles(sectionBody: string): CodebaseInvestigationFileRef[];
2
18
  export declare function lintDesignStage(ctx: StageLintContext): Promise<void>;
@@ -110,25 +110,73 @@ async function resolveDesignDiagramTier(projectRoot, track, designRaw) {
110
110
  }
111
111
  return { tier: "standard", source: "default:standard" };
112
112
  }
113
- function normalizeCodebaseInvestigationFileRef(value) {
114
- const cleaned = value
113
+ /**
114
+ * Wave 25 (v6.1.0) — parenthetical suffixes that the audit strips
115
+ * from a Codebase Investigation filename cell BEFORE attempting
116
+ * `fs.stat`. The user's quick-tier test wrote `index.html (new)` in
117
+ * the table, and the linter then tried to stat the literal string
118
+ * `index.html (new)` (with the suffix) and failed with "could not
119
+ * read blast-radius file(s): index.html (new)". Authors used these
120
+ * markers as informational labels, not as part of the filename.
121
+ *
122
+ * Stripping happens for ANY parenthetical suffix on the same line as
123
+ * the filename cell so we don't have to enumerate every author
124
+ * convention. For new files (suffix "new"), the audit records
125
+ * "new file, no stale diagrams to detect" instead of trying to stat.
126
+ */
127
+ const STALE_DIAGRAM_NEW_FILE_SUFFIX_PATTERN = /\(\s*new(?:[\s-]?file)?\s*\)/iu;
128
+ const STALE_DIAGRAM_SKIP_FILE_SUFFIX_PATTERN = /\(\s*(?:n\/a|skip|skipped|deleted|removed|stub|placeholder|tbd)\s*\)/iu;
129
+ export function normalizeCodebaseInvestigationFileRef(value, notesCell) {
130
+ const cleanedFull = value
115
131
  .replace(/`/gu, "")
116
132
  .replace(/^\s*[-*]\s*/u, "")
117
133
  .trim();
118
- if (!cleaned)
134
+ if (!cleanedFull)
119
135
  return null;
120
- if (/^(?:file|n\/a|none|\(none\)|tbd|\?)$/iu.test(cleaned))
136
+ if (/^#/u.test(cleanedFull)) {
137
+ return { filename: cleanedFull.replace(/^#\s*/u, ""), raw: cleanedFull, newFile: false, skip: true };
138
+ }
139
+ // Strip ANY trailing parenthetical suffix(es) so the audit operates
140
+ // on the raw filename. We loop because authors sometimes stack
141
+ // multiple suffixes (`index.html (new) (stub)`).
142
+ let stripped = cleanedFull;
143
+ let newFile = false;
144
+ let skip = false;
145
+ for (let safety = 0; safety < 4; safety += 1) {
146
+ const trailingParen = /\s*\([^)]*\)\s*$/u.exec(stripped);
147
+ if (!trailingParen)
148
+ break;
149
+ const parenText = trailingParen[0];
150
+ if (STALE_DIAGRAM_NEW_FILE_SUFFIX_PATTERN.test(parenText))
151
+ newFile = true;
152
+ if (STALE_DIAGRAM_SKIP_FILE_SUFFIX_PATTERN.test(parenText))
153
+ skip = true;
154
+ stripped = stripped.slice(0, trailingParen.index).trim();
155
+ }
156
+ if (!stripped)
157
+ return null;
158
+ if (/^(?:file|n\/a|none|\(none\)|tbd|\?)$/iu.test(stripped))
121
159
  return null;
122
- return cleaned;
160
+ // Notes column may carry an explicit `skip:` marker (Wave 25).
161
+ if (/(?:^|\s|\|)skip\s*:/iu.test(notesCell))
162
+ skip = true;
163
+ return { filename: stripped, raw: cleanedFull, newFile, skip };
123
164
  }
124
- function collectCodebaseInvestigationFiles(sectionBody) {
165
+ export function collectCodebaseInvestigationFiles(sectionBody) {
125
166
  const refs = [];
167
+ const seen = new Set();
126
168
  for (const row of getMarkdownTableRows(sectionBody)) {
127
- const fileCell = normalizeCodebaseInvestigationFileRef(row[0] ?? "");
128
- if (fileCell)
129
- refs.push(fileCell);
169
+ const notesCell = row[row.length - 1] ?? "";
170
+ const fileCell = normalizeCodebaseInvestigationFileRef(row[0] ?? "", notesCell);
171
+ if (!fileCell)
172
+ continue;
173
+ const key = `${fileCell.filename}|${fileCell.skip}|${fileCell.newFile}`;
174
+ if (seen.has(key))
175
+ continue;
176
+ seen.add(key);
177
+ refs.push(fileCell);
130
178
  }
131
- return [...new Set(refs)];
179
+ return refs;
132
180
  }
133
181
  async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, codebaseInvestigationBody) {
134
182
  const markerCount = (artifactRaw.match(/<!--\s*diagram:\s*[a-z0-9-]+\s*-->/giu) ?? []).length;
@@ -157,11 +205,21 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
157
205
  }
158
206
  const stale = [];
159
207
  const missing = [];
208
+ const newFiles = [];
209
+ const skipped = [];
160
210
  let scanned = 0;
161
211
  for (const ref of refs) {
162
- const absPath = path.isAbsolute(ref) ? ref : path.join(projectRoot, ref);
212
+ if (ref.skip) {
213
+ skipped.push(ref.filename);
214
+ continue;
215
+ }
216
+ if (ref.newFile) {
217
+ newFiles.push(ref.filename);
218
+ continue;
219
+ }
220
+ const absPath = path.isAbsolute(ref.filename) ? ref.filename : path.join(projectRoot, ref.filename);
163
221
  if (!(await exists(absPath))) {
164
- missing.push(ref);
222
+ missing.push(ref.filename);
165
223
  continue;
166
224
  }
167
225
  let fileStat;
@@ -169,23 +227,29 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
169
227
  fileStat = await fs.stat(absPath);
170
228
  }
171
229
  catch {
172
- missing.push(ref);
230
+ missing.push(ref.filename);
173
231
  continue;
174
232
  }
175
233
  if (!fileStat.isFile())
176
234
  continue;
177
235
  scanned += 1;
178
236
  if (fileStat.mtimeMs > artifactStat.mtimeMs) {
179
- stale.push(ref);
237
+ stale.push(ref.filename);
180
238
  }
181
239
  }
182
240
  if (missing.length > 0) {
183
241
  return {
184
242
  ok: false,
185
- details: `Stale Diagram Audit could not read blast-radius file(s): ${missing.join(", ")}.`
243
+ details: `Stale Diagram Audit could not read blast-radius file(s): ${missing.join(", ")}. Strip parenthetical suffixes like \` (new)\`, \` (deleted)\`, \` (stub)\` from the filename column, mark new files as \`<path> (new)\`, or add a leading \`#\` to the filename to skip the row.`
186
244
  };
187
245
  }
188
- if (scanned === 0) {
246
+ const noteParts = [];
247
+ if (skipped.length > 0)
248
+ noteParts.push(`${skipped.length} skipped (${skipped.join(", ")})`);
249
+ if (newFiles.length > 0)
250
+ noteParts.push(`${newFiles.length} new file(s) with no stale diagrams to detect (${newFiles.join(", ")})`);
251
+ const notes = noteParts.length > 0 ? `; ${noteParts.join("; ")}` : "";
252
+ if (scanned === 0 && newFiles.length === 0 && skipped.length === 0) {
189
253
  return {
190
254
  ok: false,
191
255
  details: "Stale Diagram Audit found no readable blast-radius files in Codebase Investigation."
@@ -194,12 +258,12 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
194
258
  if (stale.length > 0) {
195
259
  return {
196
260
  ok: false,
197
- details: `Stale Diagram Audit flagged stale file(s) newer than diagram baseline: ${stale.join(", ")}.`
261
+ details: `Stale Diagram Audit flagged stale file(s) newer than diagram baseline: ${stale.join(", ")}${notes}.`
198
262
  };
199
263
  }
200
264
  return {
201
265
  ok: true,
202
- details: `Stale Diagram Audit clear: ${scanned} blast-radius file(s) are not newer than diagram baseline.`
266
+ details: `Stale Diagram Audit clear: ${scanned} blast-radius file(s) are not newer than diagram baseline${notes}.`
203
267
  };
204
268
  }
205
269
  export async function lintDesignStage(ctx) {
@@ -224,7 +288,7 @@ export async function lintDesignStage(ctx) {
224
288
  findings.push({
225
289
  section: "qa_log_unconverged",
226
290
  required: !floor.skipQuestionsAdvisory,
227
- rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
291
+ rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until every forcing-question topic id is tagged with `[topic:<id>]` on at least one row, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
228
292
  found: floor.ok,
229
293
  details: floor.details
230
294
  });
@@ -1,7 +1,9 @@
1
1
  import { checkCriticPredictionsContract, evaluateQaLogFloor, sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode, getMarkdownTableRows } from "./shared.js";
2
- import { readDelegationLedger } from "../delegation.js";
2
+ import { readDelegationLedger, recordExpansionStrategistSkippedByTrack } from "../delegation.js";
3
+ import { shouldDemoteArtifactValidationByTrack } from "../content/stage-schema.js";
4
+ import { readFlowState } from "../run-persistence.js";
3
5
  export async function lintScopeStage(ctx) {
4
- const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, activeStageFlags } = ctx;
6
+ const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, activeStageFlags, taskClass } = ctx;
5
7
  const lockedDecisionsBody = sectionBodyByHeadingPrefix(sections, "Locked Decisions") ?? "";
6
8
  const scopeSummaryBody = sectionBodyByName(sections, "Scope Summary") ?? "";
7
9
  const selectedScopeMode = extractCanonicalScopeMode(scopeSummaryBody);
@@ -25,31 +27,68 @@ export async function lintScopeStage(ctx) {
25
27
  findings.push({
26
28
  section: "qa_log_unconverged",
27
29
  required: !floor.skipQuestionsAdvisory,
28
- rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
30
+ rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until every forcing-question topic id is tagged with `[topic:<id>]` on at least one row, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
29
31
  found: floor.ok,
30
32
  details: floor.details
31
33
  });
32
34
  }
33
35
  const strategistRequired = selectedScopeMode === "SCOPE EXPANSION" || selectedScopeMode === "SELECTIVE EXPANSION";
34
36
  if (strategistRequired) {
35
- const delegationLedger = await readDelegationLedger(projectRoot);
36
- const discoveryRows = delegationLedger.entries.filter((entry) => entry.stage === "scope" &&
37
- entry.agent === "product-discovery" &&
38
- entry.runId === delegationLedger.runId &&
39
- entry.status === "completed");
40
- const hasCompleted = discoveryRows.length > 0;
41
- const hasEvidence = discoveryRows.some((entry) => Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0);
42
- findings.push({
43
- section: "Expansion Strategist Delegation",
44
- required: true,
45
- rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-discovery` delegation for the active run with non-empty evidenceRefs is required.",
46
- found: hasCompleted && hasEvidence,
47
- details: !hasCompleted
48
- ? `Scope mode ${selectedScopeMode} requires a completed product-discovery delegation row for active run ${delegationLedger.runId}.`
49
- : hasEvidence
50
- ? `product-discovery delegation satisfied for mode ${selectedScopeMode}.`
51
- : "product-discovery delegation exists but evidenceRefs is empty; add at least one artifact/code evidence reference."
52
- });
37
+ // Wave 25 (v6.1.0) — for `track === "quick"` (lite-tier) or
38
+ // `taskClass === "software-bugfix"`, the Expansion Strategist
39
+ // delegation requirement is dropped entirely. The user's
40
+ // 3-file static landing page hit this gate without any
41
+ // discovery scope — pure ceremony for trivial work. Standard
42
+ // tracks remain unchanged.
43
+ const skipByTrack = shouldDemoteArtifactValidationByTrack(track, taskClass);
44
+ if (skipByTrack) {
45
+ findings.push({
46
+ section: "Expansion Strategist Delegation",
47
+ required: false,
48
+ rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-discovery` delegation for the active run with non-empty evidenceRefs is required.",
49
+ found: true,
50
+ details: `Expansion Strategist delegation requirement skipped for track="${track}"` +
51
+ (taskClass ? `, taskClass="${taskClass}"` : "") +
52
+ ` (Wave 25: lite-tier escape; selectedMode=${selectedScopeMode}).`
53
+ });
54
+ // Best-effort audit; we read the flow-state runId here
55
+ // because StageLintContext does not surface it directly.
56
+ try {
57
+ const flowState = await readFlowState(projectRoot);
58
+ const runId = flowState.activeRunId ?? null;
59
+ if (runId) {
60
+ await recordExpansionStrategistSkippedByTrack(projectRoot, {
61
+ track,
62
+ taskClass: taskClass ?? null,
63
+ runId,
64
+ selectedScopeMode
65
+ }).catch(() => { });
66
+ }
67
+ }
68
+ catch {
69
+ // Audit is best-effort; never block scope linting.
70
+ }
71
+ }
72
+ else {
73
+ const delegationLedger = await readDelegationLedger(projectRoot);
74
+ const discoveryRows = delegationLedger.entries.filter((entry) => entry.stage === "scope" &&
75
+ entry.agent === "product-discovery" &&
76
+ entry.runId === delegationLedger.runId &&
77
+ entry.status === "completed");
78
+ const hasCompleted = discoveryRows.length > 0;
79
+ const hasEvidence = discoveryRows.some((entry) => Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0);
80
+ findings.push({
81
+ section: "Expansion Strategist Delegation",
82
+ required: true,
83
+ rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-discovery` delegation for the active run with non-empty evidenceRefs is required.",
84
+ found: hasCompleted && hasEvidence,
85
+ details: !hasCompleted
86
+ ? `Scope mode ${selectedScopeMode} requires a completed product-discovery delegation row for active run ${delegationLedger.runId}.`
87
+ : hasEvidence
88
+ ? `product-discovery delegation satisfied for mode ${selectedScopeMode}.`
89
+ : "product-discovery delegation exists but evidenceRefs is empty; add at least one artifact/code evidence reference."
90
+ });
91
+ }
53
92
  }
54
93
  const criticPredictions = checkCriticPredictionsContract(sections);
55
94
  if (criticPredictions !== null) {
@@ -5,6 +5,20 @@ import { type FlowStage, type FlowTrack } from "../types.js";
5
5
  * convergence floor is enforced.
6
6
  */
7
7
  export declare const ELICITATION_STAGES: ReadonlySet<FlowStage>;
8
+ /**
9
+ * Wave 24 (v6.0.0) — language-neutral forcing-question topic descriptor.
10
+ *
11
+ * Each forcing-question row in a stage's checklist now declares topics as
12
+ * `id: human-readable label` pairs (e.g. `pain: what pain are we solving`).
13
+ * The `id` (kebab-case ASCII) is the machine-key authors stamp on Q&A Log
14
+ * rows via `[topic:<id>]` so the linter can verify coverage in ANY natural
15
+ * language (RU/EN/UA/etc.). Wave 23's English keyword fallback was removed
16
+ * because it silently mis-reported convergence on RU/UA Q&A.
17
+ */
18
+ export interface ForcingQuestionTopic {
19
+ id: string;
20
+ topic: string;
21
+ }
8
22
  export interface QaLogFloorOptions {
9
23
  /**
10
24
  * When true, downgrades the finding to advisory (`required: false`).
@@ -12,11 +26,12 @@ export interface QaLogFloorOptions {
12
26
  */
13
27
  skipQuestions?: boolean;
14
28
  /**
15
- * Optional pre-extracted forcing-question topics. When omitted, the
16
- * evaluator calls `extractForcingQuestions(stage)` which scans the
17
- * stage's checklist row.
29
+ * Optional pre-extracted forcing-question topic descriptors. When
30
+ * omitted, the evaluator calls `extractForcingQuestions(stage)` which
31
+ * scans the stage's checklist row. Strings are accepted as topic IDs
32
+ * (label = id) for callers that build their own list.
18
33
  */
19
- forcingQuestions?: string[];
34
+ forcingQuestions?: ReadonlyArray<ForcingQuestionTopic | string>;
20
35
  }
21
36
  export interface QaLogFloorResult {
22
37
  /** Whether convergence is satisfied (passes the gate). */
@@ -53,17 +68,30 @@ export interface QaLogFloorResult {
53
68
  details: string;
54
69
  }
55
70
  /**
56
- * Extract forcing-question topics from a stage's checklist. Looks for
57
- * the canonical `**<Stage> forcing questions (must be covered or
58
- * explicitly waived)** <topic1>, <topic2>, ...` row and tokenizes the
59
- * comma-separated topic list. Returns trimmed topic strings stripped of
60
- * leading question words (`what`/`who`/`where`/`which`/`how`/`is`/`do`/`does`).
71
+ * Parse a single checklist row into the list of forcing-question topic
72
+ * descriptors it declares. Returns `null` when the row is not a
73
+ * forcing-questions header. Throws when the header is found but its
74
+ * body does not match the Wave 24 `id: topic; id: topic; ...` syntax
75
+ * authors fix the stage definition rather than silently ship
76
+ * un-coverable topics.
77
+ *
78
+ * Exposed for unit tests that exercise the parser without depending on
79
+ * the live stage schema.
80
+ */
81
+ export declare function parseForcingQuestionsRow(row: string, context?: string): ForcingQuestionTopic[] | null;
82
+ /**
83
+ * Extract forcing-question topics from a stage's checklist.
84
+ *
85
+ * Wave 24 (v6.0.0): only the new `id: topic; id: topic; ...` syntax is
86
+ * accepted. Throws when the syntax is malformed so authors fix the
87
+ * stage definition rather than silently shipping un-coverable topics.
61
88
  *
62
89
  * Returns empty array when no forcing-questions row is present (caller
63
- * should treat absence as "no forcing requirement" — convergence falls
64
- * back to the no-new-decisions / stop-signal detectors).
90
+ * treats absence as "no forcing requirement" — convergence falls back
91
+ * to the no-new-decisions / stop-signal detectors). Returning [] when
92
+ * the row exists but lists no segments is also legal.
65
93
  */
66
- export declare function extractForcingQuestions(stage: FlowStage): string[];
94
+ export declare function extractForcingQuestions(stage: FlowStage): ForcingQuestionTopic[];
67
95
  /**
68
96
  * Evaluate the Q&A Log convergence floor for a brainstorm / scope /
69
97
  * design artifact. Returns ok=true when convergence is reached or any
@@ -208,7 +236,34 @@ export interface InteractionEdgeCaseRequirement {
208
236
  pattern: RegExp;
209
237
  }
210
238
  export declare const INTERACTION_EDGE_CASE_REQUIREMENTS: readonly InteractionEdgeCaseRequirement[];
211
- export declare function validateInteractionEdgeCaseMatrix(sectionBody: string): {
239
+ /**
240
+ * Wave 25 (v6.1.0) — context for `validateInteractionEdgeCaseMatrix`.
241
+ *
242
+ * The user's quick-tier test of a 3-file static landing page hit
243
+ * "Interaction Edge Case row \"nav-away-mid-request\" must mark
244
+ * Handled? as yes/no" because they wrote `N/A` (no network at all).
245
+ * Then `unhandled must reference a deferred item id (for example
246
+ * D-12)`. Wave 25 introduces:
247
+ *
248
+ * 1. `N/A — <reason>` (em-dash + free-text reason) is now an
249
+ * accepted Handled? value. The reason replaces the D-XX
250
+ * requirement.
251
+ * 2. When the caller signals lite-tier and the design has no
252
+ * network/external dependencies (detected via the Architecture
253
+ * Diagram body or a missing Failure Mode Table), the standard
254
+ * mandatory rows (`nav-away-mid-request`, `10K-result dataset`,
255
+ * `background-job abandonment`, `zombie connection`) are
256
+ * treated as advisory rather than required. The `double-click`
257
+ * row stays mandatory because UI duplicate-action handling is
258
+ * relevant even for static pages.
259
+ */
260
+ export interface InteractionEdgeCaseValidationContext {
261
+ /** Optional H2 sections map for cross-section "no network" detection. */
262
+ sections?: H2SectionMap | null;
263
+ /** When true, network-dependent mandatory rows become advisory. */
264
+ liteTier?: boolean;
265
+ }
266
+ export declare function validateInteractionEdgeCaseMatrix(sectionBody: string, context?: InteractionEdgeCaseValidationContext): {
212
267
  ok: boolean;
213
268
  details: string;
214
269
  };
@@ -223,14 +278,76 @@ export declare function validatePreScopeSystemAudit(sectionBody: string): {
223
278
  export declare const DIAGRAM_ARROW_PATTERN: RegExp;
224
279
  export declare const DIAGRAM_FAILURE_EDGE_PATTERN: RegExp;
225
280
  export declare const DIAGRAM_GENERIC_NODE_PATTERN: RegExp;
281
+ /**
282
+ * Wave 25 (v6.1.0) — external-dependency keywords that trigger the
283
+ * failure-edge requirement. The architecture diagram is allowed to
284
+ * omit failure edges only when ALL of:
285
+ * - Failure Mode Table has zero rows.
286
+ * - The diagram body mentions no external-dependency keyword.
287
+ *
288
+ * Static landing pages (3 HTML/CSS/JS files, no network) match this:
289
+ * no failure modes to map, no external systems to fail. The previous
290
+ * blanket "must include at least one failure-edge" rule produced
291
+ * ceremony-only failures that the agent worked around with fake
292
+ * `(timeout)` annotations, defeating the spirit of the rule.
293
+ */
294
+ export declare const DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN: RegExp;
226
295
  export declare const TEST_COMMAND_MARKER_PATTERN: RegExp;
227
296
  export declare const RED_FAILURE_MARKER_PATTERN: RegExp;
228
297
  export declare const GREEN_SUCCESS_MARKER_PATTERN: RegExp;
229
298
  export declare function diagramEdgeLines(sectionBody: string): string[];
230
299
  export declare function hasFailureEdgeInDiagram(sectionBody: string): boolean;
231
300
  export declare function hasLabeledDiagramArrow(lines: string[]): boolean;
301
+ /**
302
+ * Wave 25 (v6.1.0) — accepted async edge patterns. Returns true when
303
+ * a line carries any of:
304
+ *
305
+ * - `-.->`, `-->>`, `~~>` (mermaid dotted/messaging arrows)
306
+ * - `- - ->` (loose dotted ASCII arrow with optional spaces)
307
+ * - `.....>` (3-or-more dots followed by `>`)
308
+ * - `\basync\b` text token (label-based)
309
+ * - `[async]` bracketed label, `async:` prefix, `async:` cell content
310
+ *
311
+ * The error message printed when this fails (see
312
+ * `validateArchitectureDiagram`) lists every accepted pattern
313
+ * verbatim so the agent does not have to guess.
314
+ */
232
315
  export declare function hasAsyncDiagramEdge(lines: string[]): boolean;
316
+ /**
317
+ * Wave 25 (v6.1.0) — accepted sync edge patterns. Returns true when a
318
+ * line carries any of:
319
+ *
320
+ * - `\bsync\b` text token (label-based)
321
+ * - `[sync]` bracketed label, `sync:` prefix, `sync:` cell content
322
+ * - Solid `-->`, `->`, `=>`, `→`, `⟶`, `↦` arrow that is NOT a known
323
+ * dotted/async variant (`-.->`, `-->>`, `~~>`)
324
+ * - `===>` (3+ `=` then `>`) and `--->` (3+ `-` then `>`) heavy solid
325
+ * arrows
326
+ */
233
327
  export declare function hasSyncDiagramEdge(lines: string[]): boolean;
328
+ /**
329
+ * Wave 25 (v6.1.0) — exact accepted-pattern list shown in the error
330
+ * message when sync/async distinction fails. Keep in sync with
331
+ * `hasAsyncDiagramEdge` / `hasSyncDiagramEdge` above.
332
+ */
333
+ export declare const DIAGRAM_SYNC_ASYNC_ACCEPTED_PATTERNS: readonly ["Solid arrows: `-->`, `->`, `===>`, `--->`, `=>`, `→`, `⟶`, `↦`", "Dotted/async arrows: `-.->`, `-->>`, `~~>`, `- - ->`, `.....>`", "Text labels on the same line: `sync` / `async`", "Bracket labels: `[sync]` / `[async]`", "Cell-prefix labels: `sync:` / `async:` (e.g. `A -->|sync: persist| B`)"];
334
+ export interface ArchitectureDiagramValidationContext {
335
+ /** Optional H2 sections map for cross-section checks (e.g. Failure Mode Table presence). */
336
+ sections?: H2SectionMap | null;
337
+ }
338
+ export interface ArchitectureDiagramValidationResult {
339
+ ok: boolean;
340
+ details: string;
341
+ }
342
+ /**
343
+ * Wave 25 (v6.1.0) — Architecture Diagram structural check.
344
+ *
345
+ * Promoted out of `validateSectionBody` so it can take a `sections`
346
+ * map and conditionally enforce the failure-edge rule based on
347
+ * cross-section context (Failure Mode Table presence + diagram body
348
+ * mentioning external-dependency keywords).
349
+ */
350
+ export declare function validateArchitectureDiagram(sectionBody: string, context?: ArchitectureDiagramValidationContext): ArchitectureDiagramValidationResult;
234
351
  export declare function validateTddRedEvidence(sectionBody: string): {
235
352
  ok: boolean;
236
353
  details: string;
@@ -306,7 +423,24 @@ export declare function collectPatternHits(text: string, patterns: Array<{
306
423
  label: string;
307
424
  regex: RegExp;
308
425
  }>): string[];
309
- export declare function validateSectionBody(sectionBody: string, rule: string, sectionName: string): {
426
+ export interface ValidateSectionBodyContext {
427
+ /**
428
+ * Wave 25 (v6.1.0) — optional H2 sections map for cross-section
429
+ * checks (e.g. Architecture Diagram failure-edge enforcement gates
430
+ * on Failure Mode Table presence). When omitted, cross-section
431
+ * checks fall back to legacy blanket enforcement.
432
+ */
433
+ sections?: H2SectionMap | null;
434
+ /**
435
+ * Wave 25 (v6.1.0) — when true, lite-tier-only relaxations apply.
436
+ * Currently used by the Interaction Edge Case matrix to demote
437
+ * network-dependent mandatory rows to advisory when the design has
438
+ * no Failure Mode Table rows and no external-dependency keywords
439
+ * in the Architecture Diagram body.
440
+ */
441
+ liteTier?: boolean;
442
+ }
443
+ export declare function validateSectionBody(sectionBody: string, rule: string, sectionName: string, context?: ValidateSectionBodyContext): {
310
444
  ok: boolean;
311
445
  details: string;
312
446
  };
@@ -332,4 +466,14 @@ export interface StageLintContext {
332
466
  * When orchestrator cannot read flow-state, defaults to an empty array.
333
467
  */
334
468
  activeStageFlags: string[];
469
+ /**
470
+ * Wave 25 (v6.1.0) — task class for the active run, mirrored from
471
+ * `flow-state.json::taskClass`. `null` when not classified. Stage
472
+ * linters read this together with `track` via
473
+ * `shouldDemoteArtifactValidationByTrack` to demote advanced
474
+ * artifact-level checks (architecture diagram async/failure edges,
475
+ * interaction edge-case mandatory rows, stale-diagram drift,
476
+ * expansion-strategist delegation) from required → advisory.
477
+ */
478
+ taskClass: "software-standard" | "software-trivial" | "software-bugfix" | null;
335
479
  }