cclaw-cli 4.0.0 → 6.0.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 +40 -2
- package/dist/artifact-linter/design.js +2 -2
- package/dist/artifact-linter/review-army.d.ts +25 -0
- package/dist/artifact-linter/review-army.js +155 -0
- package/dist/artifact-linter/review.js +13 -0
- package/dist/artifact-linter/scope.js +9 -17
- package/dist/artifact-linter/shared.d.ts +93 -19
- package/dist/artifact-linter/shared.js +214 -96
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/artifact-linter.js +1 -1
- package/dist/content/core-agents.js +6 -1
- package/dist/content/idea.js +14 -2
- package/dist/content/skills-elicitation.js +35 -18
- package/dist/content/skills.js +10 -4
- package/dist/content/stage-schema.d.ts +29 -0
- package/dist/content/stage-schema.js +17 -0
- package/dist/content/stages/_lint-metadata/index.js +1 -2
- package/dist/content/stages/brainstorm.js +5 -2
- package/dist/content/stages/design.js +13 -12
- package/dist/content/stages/review.js +21 -21
- package/dist/content/stages/scope.js +20 -18
- package/dist/content/stages/spec.js +3 -3
- package/dist/content/stages/tdd.js +1 -0
- package/dist/content/subagents.js +3 -1
- package/dist/content/templates.d.ts +2 -2
- package/dist/content/templates.js +52 -36
- package/dist/delegation.d.ts +16 -0
- package/dist/delegation.js +64 -3
- package/dist/flow-state.d.ts +12 -0
- package/dist/gate-evidence.d.ts +12 -0
- package/dist/gate-evidence.js +4 -1
- package/dist/harness-adapters.js +1 -1
- package/dist/internal/advance-stage/advance.d.ts +2 -0
- package/dist/internal/advance-stage/advance.js +2 -1
- package/dist/internal/advance-stage/parsers.d.ts +8 -0
- package/dist/internal/advance-stage/parsers.js +27 -1
- package/dist/internal/advance-stage/start-flow.js +13 -0
- package/dist/run-persistence.js +14 -2
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { checkCriticPredictionsContract, evaluateQaLogFloor, sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, getMarkdownTableRows, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
|
|
4
|
+
import { readFlowState } from "../run-persistence.js";
|
|
4
5
|
export async function lintBrainstormStage(ctx) {
|
|
5
6
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
6
7
|
const qaLogBody = sectionBodyByName(sections, "Q&A Log");
|
|
@@ -21,9 +22,9 @@ export async function lintBrainstormStage(ctx) {
|
|
|
21
22
|
const skipQuestions = ctx.activeStageFlags.includes("--skip-questions");
|
|
22
23
|
const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { skipQuestions });
|
|
23
24
|
findings.push({
|
|
24
|
-
section: "
|
|
25
|
+
section: "qa_log_unconverged",
|
|
25
26
|
required: !floor.skipQuestionsAdvisory,
|
|
26
|
-
rule: "[P1]
|
|
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.",
|
|
27
28
|
found: floor.ok,
|
|
28
29
|
details: floor.details
|
|
29
30
|
});
|
|
@@ -279,6 +280,43 @@ export async function lintBrainstormStage(ctx) {
|
|
|
279
280
|
: `Outside Voice section is missing field(s): ${missing.join(", ")}.`
|
|
280
281
|
});
|
|
281
282
|
}
|
|
283
|
+
let ideaHint = {};
|
|
284
|
+
try {
|
|
285
|
+
const flowState = await readFlowState(projectRoot);
|
|
286
|
+
const hint = flowState.interactionHints?.brainstorm;
|
|
287
|
+
if (hint) {
|
|
288
|
+
ideaHint = {
|
|
289
|
+
fromIdeaArtifact: hint.fromIdeaArtifact,
|
|
290
|
+
fromIdeaCandidateId: hint.fromIdeaCandidateId
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
ideaHint = {};
|
|
296
|
+
}
|
|
297
|
+
if (ideaHint.fromIdeaArtifact) {
|
|
298
|
+
const carryBody = sectionBodyByName(sections, "Idea Evidence Carry-forward");
|
|
299
|
+
const hasSection = carryBody !== null && meaningfulLineCount(carryBody) > 0;
|
|
300
|
+
const sourceCited = hasSection &&
|
|
301
|
+
carryBody.includes(ideaHint.fromIdeaArtifact);
|
|
302
|
+
const candidateCited = ideaHint.fromIdeaCandidateId
|
|
303
|
+
? hasSection && carryBody.toUpperCase().includes(ideaHint.fromIdeaCandidateId.toUpperCase())
|
|
304
|
+
: true;
|
|
305
|
+
const ok = hasSection && sourceCited && candidateCited;
|
|
306
|
+
findings.push({
|
|
307
|
+
section: "brainstorm.idea_evidence_carry_forward",
|
|
308
|
+
required: true,
|
|
309
|
+
rule: "[P1] brainstorm.idea_evidence_carry_forward — when `flow-state.interactionHints.brainstorm.fromIdeaArtifact` is set (Wave 23 / v5.0.0), the brainstorm artifact MUST include `## Idea Evidence Carry-forward` citing the idea artifact path and chosen `I-#`. Reuse divergent + critique + rank work from `/cc-ideate` as the `baseline` Approach; only newly generate the higher-upside challenger row(s).",
|
|
310
|
+
found: ok,
|
|
311
|
+
details: ok
|
|
312
|
+
? `Idea Evidence Carry-forward cites ${ideaHint.fromIdeaArtifact}${ideaHint.fromIdeaCandidateId ? ` (${ideaHint.fromIdeaCandidateId})` : ""}.`
|
|
313
|
+
: !hasSection
|
|
314
|
+
? `Brainstorm started from /cc-ideate (artifact ${ideaHint.fromIdeaArtifact}${ideaHint.fromIdeaCandidateId ? `, candidate ${ideaHint.fromIdeaCandidateId}` : ""}) but \`## Idea Evidence Carry-forward\` is missing or empty.`
|
|
315
|
+
: !sourceCited
|
|
316
|
+
? `\`## Idea Evidence Carry-forward\` does not cite the source idea artifact path \`${ideaHint.fromIdeaArtifact}\`.`
|
|
317
|
+
: `\`## Idea Evidence Carry-forward\` does not cite the chosen candidate id \`${ideaHint.fromIdeaCandidateId ?? ""}\`.`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
282
320
|
const wavePlansDir = path.join(projectRoot, ".cclaw", "wave-plans");
|
|
283
321
|
let wavePlanEntries = [];
|
|
284
322
|
try {
|
|
@@ -222,9 +222,9 @@ export async function lintDesignStage(ctx) {
|
|
|
222
222
|
const skipQuestions = activeStageFlags.includes("--skip-questions");
|
|
223
223
|
const floor = evaluateQaLogFloor(qaLogBody, track, "design", { skipQuestions });
|
|
224
224
|
findings.push({
|
|
225
|
-
section: "
|
|
225
|
+
section: "qa_log_unconverged",
|
|
226
226
|
required: !floor.skipQuestionsAdvisory,
|
|
227
|
-
rule: "[P1]
|
|
227
|
+
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
228
|
found: floor.ok,
|
|
229
229
|
details: floor.details
|
|
230
230
|
});
|
|
@@ -21,4 +21,29 @@ export interface ReviewSecurityNoChangeAttestationResult {
|
|
|
21
21
|
* APPROVED while open Critical findings or shipBlockers remain.
|
|
22
22
|
*/
|
|
23
23
|
export declare function checkReviewVerdictConsistency(projectRoot: string): Promise<ReviewVerdictConsistencyResult>;
|
|
24
|
+
export interface ReviewTddDuplicationConflict {
|
|
25
|
+
findingId: string;
|
|
26
|
+
tddSeverity: string | null;
|
|
27
|
+
reviewSeverity: string | null;
|
|
28
|
+
tddDisposition: string | null;
|
|
29
|
+
reviewDisposition: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface ReviewTddDuplicationResult {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
errors: string[];
|
|
34
|
+
conflicts: ReviewTddDuplicationConflict[];
|
|
35
|
+
tddArtifactExists: boolean;
|
|
36
|
+
reviewArtifactExists: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Cross-artifact duplication guard (Wave 23 / v5.0.0).
|
|
40
|
+
*
|
|
41
|
+
* When the same finding ID (`F-NN`) appears in both
|
|
42
|
+
* `06-tdd.md > Per-Slice Review` and `07-review-army.json`, the
|
|
43
|
+
* severity and disposition MUST match. Per-slice tdd reviews own
|
|
44
|
+
* single-slice findings; review cites them, never re-classifies.
|
|
45
|
+
*
|
|
46
|
+
* If neither artifact uses `F-NN` IDs, the check is a no-op.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkReviewTddNoCrossArtifactDuplication(projectRoot: string): Promise<ReviewTddDuplicationResult>;
|
|
24
49
|
export declare function checkReviewSecurityNoChangeAttestation(projectRoot: string): Promise<ReviewSecurityNoChangeAttestationResult>;
|
|
@@ -319,6 +319,161 @@ export async function checkReviewVerdictConsistency(projectRoot) {
|
|
|
319
319
|
shipBlockerCount
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
|
+
const FINDING_ID_PATTERN = /\bF-\d+\b/giu;
|
|
323
|
+
const SEVERITY_TOKENS = ["Critical", "Important", "Suggestion"];
|
|
324
|
+
const DISPOSITION_TOKENS = ["open", "accepted", "resolved", "deferred", "won't-fix", "wont-fix"];
|
|
325
|
+
function findFirstToken(text, tokens) {
|
|
326
|
+
for (const token of tokens) {
|
|
327
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
328
|
+
const regex = new RegExp(`\\b${escaped}\\b`, "iu");
|
|
329
|
+
if (regex.test(text))
|
|
330
|
+
return token;
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function normalizeDisposition(value) {
|
|
335
|
+
if (value === null)
|
|
336
|
+
return null;
|
|
337
|
+
const lower = value.toLowerCase();
|
|
338
|
+
if (lower === "wont-fix" || lower === "won't-fix")
|
|
339
|
+
return "won't-fix";
|
|
340
|
+
return lower;
|
|
341
|
+
}
|
|
342
|
+
function extractTddPerSliceFindings(perSliceBody) {
|
|
343
|
+
const rows = new Map();
|
|
344
|
+
const lines = perSliceBody.split(/\r?\n/u);
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
const ids = line.match(FINDING_ID_PATTERN);
|
|
347
|
+
if (!ids || ids.length === 0)
|
|
348
|
+
continue;
|
|
349
|
+
const severity = findFirstToken(line, SEVERITY_TOKENS);
|
|
350
|
+
const disposition = normalizeDisposition(findFirstToken(line, DISPOSITION_TOKENS));
|
|
351
|
+
for (const rawId of ids) {
|
|
352
|
+
const id = rawId.toUpperCase();
|
|
353
|
+
if (rows.has(id))
|
|
354
|
+
continue;
|
|
355
|
+
rows.set(id, { id, severity, disposition });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return rows;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Cross-artifact duplication guard (Wave 23 / v5.0.0).
|
|
362
|
+
*
|
|
363
|
+
* When the same finding ID (`F-NN`) appears in both
|
|
364
|
+
* `06-tdd.md > Per-Slice Review` and `07-review-army.json`, the
|
|
365
|
+
* severity and disposition MUST match. Per-slice tdd reviews own
|
|
366
|
+
* single-slice findings; review cites them, never re-classifies.
|
|
367
|
+
*
|
|
368
|
+
* If neither artifact uses `F-NN` IDs, the check is a no-op.
|
|
369
|
+
*/
|
|
370
|
+
export async function checkReviewTddNoCrossArtifactDuplication(projectRoot) {
|
|
371
|
+
const tddPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "06-tdd.md");
|
|
372
|
+
const armyPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review-army.json");
|
|
373
|
+
const tddArtifactExists = await exists(tddPath);
|
|
374
|
+
const reviewArtifactExists = await exists(armyPath);
|
|
375
|
+
if (!tddArtifactExists || !reviewArtifactExists) {
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
errors: [],
|
|
379
|
+
conflicts: [],
|
|
380
|
+
tddArtifactExists,
|
|
381
|
+
reviewArtifactExists
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const tddRaw = await fs.readFile(tddPath, "utf8");
|
|
385
|
+
const tddSections = extractH2Sections(tddRaw);
|
|
386
|
+
const perSliceBody = sectionBodyByName(tddSections, "Per-Slice Review");
|
|
387
|
+
if (!perSliceBody) {
|
|
388
|
+
return {
|
|
389
|
+
ok: true,
|
|
390
|
+
errors: [],
|
|
391
|
+
conflicts: [],
|
|
392
|
+
tddArtifactExists,
|
|
393
|
+
reviewArtifactExists
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const tddFindings = extractTddPerSliceFindings(perSliceBody);
|
|
397
|
+
if (tddFindings.size === 0) {
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
errors: [],
|
|
401
|
+
conflicts: [],
|
|
402
|
+
tddArtifactExists,
|
|
403
|
+
reviewArtifactExists
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
let parsed;
|
|
407
|
+
try {
|
|
408
|
+
parsed = JSON.parse(await fs.readFile(armyPath, "utf8"));
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
errors: [],
|
|
414
|
+
conflicts: [],
|
|
415
|
+
tddArtifactExists,
|
|
416
|
+
reviewArtifactExists
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
420
|
+
return {
|
|
421
|
+
ok: true,
|
|
422
|
+
errors: [],
|
|
423
|
+
conflicts: [],
|
|
424
|
+
tddArtifactExists,
|
|
425
|
+
reviewArtifactExists
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const root = parsed;
|
|
429
|
+
const findings = Array.isArray(root.findings) ? root.findings : [];
|
|
430
|
+
const conflicts = [];
|
|
431
|
+
for (const f of findings) {
|
|
432
|
+
if (!f || typeof f !== "object" || Array.isArray(f))
|
|
433
|
+
continue;
|
|
434
|
+
const o = f;
|
|
435
|
+
if (typeof o.id !== "string")
|
|
436
|
+
continue;
|
|
437
|
+
const id = o.id.toUpperCase();
|
|
438
|
+
const tddRow = tddFindings.get(id);
|
|
439
|
+
if (!tddRow)
|
|
440
|
+
continue;
|
|
441
|
+
const reviewSeverity = typeof o.severity === "string" ? o.severity : null;
|
|
442
|
+
const reviewDisposition = normalizeDisposition(typeof o.status === "string" ? o.status : null);
|
|
443
|
+
const severityMismatch = tddRow.severity !== null &&
|
|
444
|
+
reviewSeverity !== null &&
|
|
445
|
+
tddRow.severity.toLowerCase() !== reviewSeverity.toLowerCase();
|
|
446
|
+
const dispositionMismatch = tddRow.disposition !== null &&
|
|
447
|
+
reviewDisposition !== null &&
|
|
448
|
+
tddRow.disposition !== reviewDisposition;
|
|
449
|
+
if (severityMismatch || dispositionMismatch) {
|
|
450
|
+
conflicts.push({
|
|
451
|
+
findingId: id,
|
|
452
|
+
tddSeverity: tddRow.severity,
|
|
453
|
+
reviewSeverity,
|
|
454
|
+
tddDisposition: tddRow.disposition,
|
|
455
|
+
reviewDisposition
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const errors = conflicts.map((c) => {
|
|
460
|
+
const parts = [];
|
|
461
|
+
if (c.tddSeverity !== null && c.reviewSeverity !== null && c.tddSeverity.toLowerCase() !== c.reviewSeverity.toLowerCase()) {
|
|
462
|
+
parts.push(`severity tdd=${c.tddSeverity} vs review-army=${c.reviewSeverity}`);
|
|
463
|
+
}
|
|
464
|
+
if (c.tddDisposition !== null && c.reviewDisposition !== null && c.tddDisposition !== c.reviewDisposition) {
|
|
465
|
+
parts.push(`disposition tdd=${c.tddDisposition} vs review-army=${c.reviewDisposition}`);
|
|
466
|
+
}
|
|
467
|
+
return `Finding ${c.findingId} appears in both 06-tdd.md > Per-Slice Review and 07-review-army.json with mismatched ${parts.join(" and ")}. Review must cite, not re-classify.`;
|
|
468
|
+
});
|
|
469
|
+
return {
|
|
470
|
+
ok: errors.length === 0,
|
|
471
|
+
errors,
|
|
472
|
+
conflicts,
|
|
473
|
+
tddArtifactExists,
|
|
474
|
+
reviewArtifactExists
|
|
475
|
+
};
|
|
476
|
+
}
|
|
322
477
|
export async function checkReviewSecurityNoChangeAttestation(projectRoot) {
|
|
323
478
|
const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
|
|
324
479
|
if (!(await exists(reviewMdPath))) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { markdownFieldRegex, sectionBodyByName } from "./shared.js";
|
|
2
|
+
import { checkReviewTddNoCrossArtifactDuplication } from "./review-army.js";
|
|
2
3
|
export async function lintReviewStage(ctx) {
|
|
3
4
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
4
5
|
// Universal Layer 2.7 structural checks (superpowers requesting + receiving).
|
|
@@ -62,6 +63,18 @@ export async function lintReviewStage(ctx) {
|
|
|
62
63
|
: "Receiving Posture is missing the anti-sycophancy acknowledgement line."
|
|
63
64
|
});
|
|
64
65
|
}
|
|
66
|
+
const dupResult = await checkReviewTddNoCrossArtifactDuplication(projectRoot);
|
|
67
|
+
findings.push({
|
|
68
|
+
section: "review.no_cross_artifact_duplication",
|
|
69
|
+
required: true,
|
|
70
|
+
rule: "[P1] review.no_cross_artifact_duplication — when a finding ID appears in both `06-tdd.md > Per-Slice Review` and `07-review-army.json`, severity and disposition must match (review cites tdd; never re-classifies).",
|
|
71
|
+
found: dupResult.ok,
|
|
72
|
+
details: dupResult.ok
|
|
73
|
+
? dupResult.tddArtifactExists && dupResult.reviewArtifactExists
|
|
74
|
+
? "No cross-artifact severity/disposition conflicts between tdd Per-Slice Review and review-army findings."
|
|
75
|
+
: "Skipped: tdd Per-Slice Review or review-army artifact not present."
|
|
76
|
+
: dupResult.errors.join(" ")
|
|
77
|
+
});
|
|
65
78
|
const lensCoverageBody = sectionBodyByName(sections, "Lens Coverage");
|
|
66
79
|
if (lensCoverageBody === null) {
|
|
67
80
|
findings.push({
|
|
@@ -23,9 +23,9 @@ export async function lintScopeStage(ctx) {
|
|
|
23
23
|
const skipQuestions = activeStageFlags.includes("--skip-questions");
|
|
24
24
|
const floor = evaluateQaLogFloor(qaLogBody, track, "scope", { skipQuestions });
|
|
25
25
|
findings.push({
|
|
26
|
-
section: "
|
|
26
|
+
section: "qa_log_unconverged",
|
|
27
27
|
required: !floor.skipQuestionsAdvisory,
|
|
28
|
-
rule: "[P1]
|
|
28
|
+
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
29
|
found: floor.ok,
|
|
30
30
|
details: floor.details
|
|
31
31
|
});
|
|
@@ -108,19 +108,11 @@ export async function lintScopeStage(ctx) {
|
|
|
108
108
|
: issues.join("; ")
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
required: true,
|
|
119
|
-
rule: "Implementation Alternatives must conclude with a `RECOMMENDATION:` line citing the chosen option and rationale.",
|
|
120
|
-
found: recommendation,
|
|
121
|
-
details: recommendation
|
|
122
|
-
? "Recommendation marker present."
|
|
123
|
-
: "Missing or empty `RECOMMENDATION:` line under Implementation Alternatives."
|
|
124
|
-
});
|
|
125
|
-
}
|
|
111
|
+
// Wave 23 (v5.0.0): scope no longer owns architecture-tier alternatives
|
|
112
|
+
// (`## Implementation Alternatives` was removed from the scope template
|
|
113
|
+
// and stage schema). Design OWNS the architecture-tier decision via
|
|
114
|
+
// `## Architecture Decision Record (ADR)` and `## Engineering Lock`.
|
|
115
|
+
// The legacy linter rule `Implementation Alternatives Recommendation`
|
|
116
|
+
// was removed in Wave 23 — if a legacy artifact still has the section,
|
|
117
|
+
// it is now treated as informational only.
|
|
126
118
|
}
|
|
@@ -1,43 +1,121 @@
|
|
|
1
1
|
import { type FlowStage, type FlowTrack } from "../types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Stages that run adaptive elicitation. The `
|
|
4
|
-
* fires for these. Other stages may still record a Q&A Log but no
|
|
5
|
-
* enforced.
|
|
3
|
+
* Stages that run adaptive elicitation. The `qa_log_unconverged` rule
|
|
4
|
+
* only fires for these. Other stages may still record a Q&A Log but no
|
|
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
|
-
* When true, downgrades
|
|
24
|
+
* When true, downgrades the finding to advisory (`required: false`).
|
|
11
25
|
* Set when `--skip-questions` was persisted to the active stage flags.
|
|
12
26
|
*/
|
|
13
27
|
skipQuestions?: boolean;
|
|
28
|
+
/**
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
forcingQuestions?: ReadonlyArray<ForcingQuestionTopic | string>;
|
|
14
35
|
}
|
|
15
36
|
export interface QaLogFloorResult {
|
|
16
|
-
/** Whether
|
|
37
|
+
/** Whether convergence is satisfied (passes the gate). */
|
|
17
38
|
ok: boolean;
|
|
18
39
|
/** Substantive Q&A Log row count (excludes `skipped`/`waived` only rows). */
|
|
19
40
|
count: number;
|
|
20
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Legacy field, retained for harness UI compatibility. Always 0 in
|
|
43
|
+
* Wave 23 — the convergence floor no longer enforces a fixed count.
|
|
44
|
+
* Harness can still surface `questionBudgetHint(track, stage).recommended`
|
|
45
|
+
* as a soft hint, but it is NOT tied to gate blocking.
|
|
46
|
+
*/
|
|
21
47
|
min: number;
|
|
22
48
|
/** Whether a stop-signal row was detected. */
|
|
23
49
|
hasStopSignal: boolean;
|
|
24
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Legacy field, retained for harness UI compatibility. Always false in
|
|
52
|
+
* Wave 23 — convergence semantics replaced the lite-tier short-circuit.
|
|
53
|
+
*/
|
|
25
54
|
liteShortCircuit: boolean;
|
|
26
55
|
/** Whether `--skip-questions` flag downgraded the finding to advisory. */
|
|
27
56
|
skipQuestionsAdvisory: boolean;
|
|
57
|
+
/** Forcing-question topics deemed addressed (substring match in Q&A). */
|
|
58
|
+
forcingCovered: string[];
|
|
59
|
+
/** Forcing-question topics still pending (no matching Q&A row). */
|
|
60
|
+
forcingPending: string[];
|
|
61
|
+
/**
|
|
62
|
+
* True when the last 2 substantive rows have decision_impact marking
|
|
63
|
+
* `skip`/`continue`/`no-change`/`done`/etc. — i.e. Q&A is no longer
|
|
64
|
+
* surfacing decision-changing answers (Ralph-Loop convergence detector).
|
|
65
|
+
*/
|
|
66
|
+
noNewDecisions: boolean;
|
|
28
67
|
/** Human-readable details for the linter finding. */
|
|
29
68
|
details: string;
|
|
30
69
|
}
|
|
31
70
|
/**
|
|
32
|
-
*
|
|
33
|
-
* Returns
|
|
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.
|
|
34
77
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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.
|
|
88
|
+
*
|
|
89
|
+
* Returns empty array when no forcing-questions row is present (caller
|
|
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.
|
|
93
|
+
*/
|
|
94
|
+
export declare function extractForcingQuestions(stage: FlowStage): ForcingQuestionTopic[];
|
|
95
|
+
/**
|
|
96
|
+
* Evaluate the Q&A Log convergence floor for a brainstorm / scope /
|
|
97
|
+
* design artifact. Returns ok=true when convergence is reached or any
|
|
98
|
+
* escape hatch fires.
|
|
99
|
+
*
|
|
100
|
+
* Convergence sources (any one is sufficient):
|
|
101
|
+
* - All forcing-question topics from the stage checklist appear addressed
|
|
102
|
+
* in `## Q&A Log` (substring keyword match in question/answer columns).
|
|
103
|
+
* - The Ralph-Loop convergence detector reports the last 2 substantive
|
|
104
|
+
* rows have decision_impact marking `skip`/`continue`/`no-change`/`done`
|
|
105
|
+
* (i.e. the dialogue is no longer producing decision-changing rows).
|
|
106
|
+
* - Q&A Log contains a stop-signal row (existing
|
|
107
|
+
* `QA_LOG_STOP_SIGNAL_PATTERNS` keep working).
|
|
37
108
|
* - `--skip-questions` flag was persisted to the active stage flags
|
|
38
|
-
* (
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
109
|
+
* (`options.skipQuestions=true`); finding downgrades to advisory.
|
|
110
|
+
* - The stage checklist exposes no forcing-questions row (e.g. simple
|
|
111
|
+
* refactor) AND the artifact has at least one substantive row — treat
|
|
112
|
+
* as converged because there is nothing left to force.
|
|
113
|
+
*
|
|
114
|
+
* Wave 23 (v5.0.0) replaces the count-based `qa_log_below_min` rule with
|
|
115
|
+
* `qa_log_unconverged`. The fixed count constant (10 for standard) and
|
|
116
|
+
* the `CCLAW_ELICITATION_FLOOR=advisory` env override were removed. The
|
|
117
|
+
* `min` and `liteShortCircuit` fields on the result are retained for
|
|
118
|
+
* harness UI compatibility but are always 0/false.
|
|
41
119
|
*/
|
|
42
120
|
export declare function evaluateQaLogFloor(qaLogBody: string | null, track: FlowTrack, stage: FlowStage, options?: QaLogFloorOptions): QaLogFloorResult;
|
|
43
121
|
export interface LintFinding {
|
|
@@ -130,10 +208,6 @@ export declare function canonicalModesInText(text: string): CanonicalScopeMode[]
|
|
|
130
208
|
export declare function shortModeToCanonical(text: string): CanonicalScopeMode | null;
|
|
131
209
|
export declare function canonicalModeFromCandidate(candidate: string): CanonicalScopeMode | null;
|
|
132
210
|
export declare function extractCanonicalScopeMode(body: string): CanonicalScopeMode | null;
|
|
133
|
-
export declare function validatePremiseChallenge(sectionBody: string): {
|
|
134
|
-
ok: boolean;
|
|
135
|
-
details: string;
|
|
136
|
-
};
|
|
137
211
|
export declare function validateScopeSummary(sectionBody: string): {
|
|
138
212
|
ok: boolean;
|
|
139
213
|
details: string;
|
|
@@ -282,7 +356,7 @@ export interface StageLintContext {
|
|
|
282
356
|
/**
|
|
283
357
|
* Stage-level flags persisted to flow-state.json `activeRun.currentStage.flags`
|
|
284
358
|
* (or equivalent). Used as escape-hatch signal for the Q&A floor rule
|
|
285
|
-
* (e.g. `--skip-questions` downgrades `
|
|
359
|
+
* (e.g. `--skip-questions` downgrades `qa_log_unconverged` to advisory).
|
|
286
360
|
* When orchestrator cannot read flow-state, defaults to an empty array.
|
|
287
361
|
*/
|
|
288
362
|
activeStageFlags: string[];
|