sequant 2.11.0 → 2.13.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +47 -3
- package/dist/dashboard/server.js +4 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +9 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +42 -20
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
- package/dist/src/commands/doctor.js +20 -18
- package/dist/src/commands/ready.js +7 -1
- package/dist/src/commands/status.js +4 -0
- package/dist/src/lib/ac-linter.js +26 -0
- package/dist/src/lib/ac-parser.d.ts +40 -0
- package/dist/src/lib/ac-parser.js +202 -16
- package/dist/src/lib/markdown-fence.d.ts +24 -0
- package/dist/src/lib/markdown-fence.js +51 -0
- package/dist/src/lib/mcp-config.d.ts +24 -0
- package/dist/src/lib/mcp-config.js +51 -0
- package/dist/src/lib/scope/analyzer.d.ts +4 -0
- package/dist/src/lib/scope/analyzer.js +7 -1
- package/dist/src/lib/settings.d.ts +73 -14
- package/dist/src/lib/settings.js +45 -3
- package/dist/src/lib/system.d.ts +7 -3
- package/dist/src/lib/system.js +7 -3
- package/dist/src/lib/test-tautology-detector.js +50 -3
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +81 -6
- package/dist/src/lib/workflow/config-resolver.d.ts +30 -2
- package/dist/src/lib/workflow/config-resolver.js +59 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +36 -4
- package/dist/src/lib/workflow/metrics-schema.d.ts +10 -1
- package/dist/src/lib/workflow/metrics-schema.js +13 -1
- package/dist/src/lib/workflow/metrics-writer.d.ts +3 -1
- package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
- package/dist/src/lib/workflow/mutation-marker.js +97 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
- package/dist/src/lib/workflow/phase-executor.js +60 -6
- package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
- package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +25 -1
- package/dist/src/lib/workflow/ready-gate.js +81 -11
- package/dist/src/lib/workflow/reconcile.js +4 -2
- package/dist/src/lib/workflow/run-log-schema.d.ts +120 -0
- package/dist/src/lib/workflow/run-log-schema.js +40 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +18 -0
- package/dist/src/lib/workflow/run-orchestrator.js +38 -2
- package/dist/src/lib/workflow/state-cleanup.d.ts +4 -4
- package/dist/src/lib/workflow/state-cleanup.js +9 -5
- package/dist/src/lib/workflow/state-schema.d.ts +10 -1
- package/dist/src/lib/workflow/state-schema.js +13 -1
- package/dist/src/lib/workflow/types.d.ts +20 -0
- package/dist/src/mcp/tools/run.js +10 -1
- package/package.json +13 -12
- package/templates/hooks/pre-tool.sh +108 -17
- package/templates/memory/constitution.md +112 -45
- package/templates/skills/exec/SKILL.md +1 -1
- package/templates/skills/fullsolve/SKILL.md +62 -9
- package/templates/skills/loop/SKILL.md +71 -12
- package/templates/skills/merger/SKILL.md +32 -3
- package/templates/skills/qa/SKILL.md +247 -2
- package/templates/skills/spec/SKILL.md +11 -5
- package/templates/skills/test/SKILL.md +1 -1
package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Recommended Workflow Format
|
|
2
2
|
|
|
3
|
-
This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output.
|
|
3
|
+
This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output.
|
|
4
|
+
|
|
5
|
+
## Resolution chain (#921)
|
|
6
|
+
|
|
7
|
+
`sequant run` resolves phases through an ordered chain, not `parseRecommendedWorkflow()` alone:
|
|
8
|
+
|
|
9
|
+
1. **`SEQUANT_SPEC` marker** — a structured HTML comment in the posted plan comment, e.g. `<!-- SEQUANT_SPEC: {"phases":["testgen","exec","qa"],"qualityLoop":true} -->`. This is the primary, durable channel — always emit it alongside the prose section below.
|
|
10
|
+
2. **Comment prose** — `parseRecommendedWorkflow()` applied to the plan comment body (same format as this doc).
|
|
11
|
+
3. **Chat text** — the same parser applied to the spec agent's chat output. Nondeterministic: only present if the agent happens to restate the section in chat rather than posting via a body file (#814).
|
|
12
|
+
4. **Label fallback** — `detectPhasesFromLabels()`. Can never produce `testgen` or `security-review`.
|
|
13
|
+
|
|
14
|
+
The marker's `phases` array excludes `spec` (it already ran) and must name only registered phases — an unknown phase name invalidates the whole marker and falls through to step 2.
|
|
4
15
|
|
|
5
16
|
## Format
|
|
6
17
|
|
|
@@ -10,6 +21,8 @@ This document shows the expected output format for the `## Recommended Workflow`
|
|
|
10
21
|
**Phases:** spec → exec → qa
|
|
11
22
|
**Quality Loop:** disabled
|
|
12
23
|
**Reasoning:** Brief explanation of why this workflow was chosen.
|
|
24
|
+
|
|
25
|
+
<!-- SEQUANT_SPEC: {"phases":["exec","qa"],"qualityLoop":false} -->
|
|
13
26
|
```
|
|
14
27
|
|
|
15
28
|
## Examples
|
|
@@ -582,7 +582,7 @@ Create structured test results:
|
|
|
582
582
|
### 3.2 GitHub Comment
|
|
583
583
|
|
|
584
584
|
**If orchestrated (SEQUANT_ORCHESTRATOR is set):**
|
|
585
|
-
- Skip posting GitHub comment
|
|
585
|
+
- Skip posting this skill's own GitHub comment — no per-phase comment is posted under `sequant run`; test results surface through the run summary and the PR body (#964)
|
|
586
586
|
- Include test summary in output for orchestrator to capture
|
|
587
587
|
- Let orchestrator aggregate results across phases
|
|
588
588
|
|
|
@@ -6,6 +6,7 @@ metadata:
|
|
|
6
6
|
author: sequant
|
|
7
7
|
version: "1.0"
|
|
8
8
|
allowed-tools:
|
|
9
|
+
- Bash(npx sequant worktree:*)
|
|
9
10
|
- Read
|
|
10
11
|
- Write
|
|
11
12
|
- Edit
|
|
@@ -530,17 +531,33 @@ If an AC has verification method "N/A - Trivial", skip test generation and note
|
|
|
530
531
|
|
|
531
532
|
### Step 4: Locate Feature Worktree
|
|
532
533
|
|
|
533
|
-
If generating file-based tests (Unit Test, Integration Test), find the worktree
|
|
534
|
+
If generating file-based tests (Unit Test, Integration Test), find the worktree.
|
|
534
535
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
536
|
+
<!-- BEGIN: worktree-standalone-lookup (#899) -->
|
|
537
|
+
|
|
538
|
+
Resolve it through git, not the filesystem:
|
|
538
539
|
|
|
539
|
-
Or check:
|
|
540
540
|
```bash
|
|
541
|
-
|
|
541
|
+
WORKTREE="$(npx sequant worktree resolve <issue-number>)" || {
|
|
542
|
+
echo "❌ HALT: no worktree for #<issue-number> in this repository."
|
|
543
|
+
exit 1
|
|
544
|
+
}
|
|
545
|
+
cd "$WORKTREE"
|
|
542
546
|
```
|
|
543
547
|
|
|
548
|
+
`sequant worktree resolve` reads `git worktree list` in the current repository
|
|
549
|
+
— which reports only *this* repo's worktrees — and selects on the **branch**
|
|
550
|
+
git reports, not the directory name.
|
|
551
|
+
|
|
552
|
+
**Do not glob `../worktrees/feature/<issue-number>-*`, and do not grep
|
|
553
|
+
`git worktree list` for the issue number.** The first matches across sibling
|
|
554
|
+
repositories, which share that directory; the second matches the printed path,
|
|
555
|
+
so it keys on the directory slug — and a slug can drift from its own branch
|
|
556
|
+
after a rename. Because this skill **writes test files**, landing in the wrong
|
|
557
|
+
tree scatters stubs into an unrelated project.
|
|
558
|
+
|
|
559
|
+
<!-- END: worktree-standalone-lookup (#899) -->
|
|
560
|
+
|
|
544
561
|
Create test directories if needed:
|
|
545
562
|
```bash
|
|
546
563
|
mkdir -p __tests__/integration
|
|
@@ -8,7 +8,8 @@ import { GitHubProvider } from "../lib/workflow/platforms/github.js";
|
|
|
8
8
|
import { fileExists, isExecutable } from "../lib/fs.js";
|
|
9
9
|
import { checkSkillsInstalled } from "../lib/skills-check.js";
|
|
10
10
|
import { getManifest } from "../lib/manifest.js";
|
|
11
|
-
import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers,
|
|
11
|
+
import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
|
|
12
|
+
import { getPhaseMcpServersConfig } from "../lib/mcp-config.js";
|
|
12
13
|
import { getSettings, DEFAULT_AGENT_SETTINGS } from "../lib/settings.js";
|
|
13
14
|
import { checkVersionThorough, getVersionWarning, resolveCliInvocation, } from "../lib/version-check.js";
|
|
14
15
|
import { areSkillsOutdated } from "./sync.js";
|
|
@@ -435,23 +436,24 @@ export async function doctorCommand(options = {}) {
|
|
|
435
436
|
message: "No optional MCPs configured (Sequant works without them, but they enhance functionality)",
|
|
436
437
|
});
|
|
437
438
|
}
|
|
438
|
-
// Check: MCP availability for headless mode (sequant run)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
439
|
+
// Check: MCP availability for headless mode (sequant run) (#936)
|
|
440
|
+
//
|
|
441
|
+
// Phase agents read the project's .mcp.json + settings.run.mcpAllowlist,
|
|
442
|
+
// never Claude Desktop config wholesale — see getPhaseMcpServersConfig.
|
|
443
|
+
// The sequant server is always guaranteed, so this check reports what a
|
|
444
|
+
// phase will actually receive rather than pass/warn on presence.
|
|
445
|
+
const phaseServersConfig = getPhaseMcpServersConfig(process.cwd(), {
|
|
446
|
+
desktopAllowlist: settings.run.mcpAllowlist,
|
|
447
|
+
});
|
|
448
|
+
const phaseServerCount = Object.keys(phaseServersConfig).length;
|
|
449
|
+
const extraServerCount = phaseServerCount - 1; // minus the guaranteed sequant entry
|
|
450
|
+
checks.push({
|
|
451
|
+
name: "MCP Servers (headless)",
|
|
452
|
+
status: "pass",
|
|
453
|
+
message: extraServerCount > 0
|
|
454
|
+
? `Available for sequant run (${phaseServerCount} servers: sequant + ${extraServerCount} from .mcp.json${settings.run.mcpAllowlist?.length ? "/mcpAllowlist" : ""})`
|
|
455
|
+
: "Available for sequant run (sequant only — add servers to .mcp.json, or settings.run.mcpAllowlist for desktop servers, for more)",
|
|
456
|
+
});
|
|
455
457
|
// Check: Sequant MCP server health
|
|
456
458
|
try {
|
|
457
459
|
// Verify MCP server can be created (validates SDK availability)
|
|
@@ -107,7 +107,9 @@ export async function readyCommand(issueArg, options) {
|
|
|
107
107
|
const policy = resolvePolicy(options.policy, settings.ready.policy);
|
|
108
108
|
const { maxIterations, tokenBudget, phaseTimeout } = resolveReadyLimits(options, settings);
|
|
109
109
|
const mcp = options.mcp !== false;
|
|
110
|
-
|
|
110
|
+
// #975: pass modelRoles + active driver so `role:` prefixes resolve,
|
|
111
|
+
// matching the same call in buildExecutionConfig (AC-5 drift guard).
|
|
112
|
+
const phasePolicies = resolvePhasePolicies(options.models, options.efforts, settings.run.phases, getPhaseNames(), settings.run.modelRoles, settings.run.agent ?? "claude-code");
|
|
111
113
|
// #915: CLI > settings > default `false`, same precedence as the `run`
|
|
112
114
|
// path's `buildExecutionConfig` (config-resolver.ts).
|
|
113
115
|
const effortEscalation = options.escalateEffort ?? settings.run.effortEscalation ?? false;
|
|
@@ -200,11 +202,15 @@ export async function readyCommand(issueArg, options) {
|
|
|
200
202
|
nonGoals,
|
|
201
203
|
phaseTimeout,
|
|
202
204
|
mcp,
|
|
205
|
+
mcpAllowlist: settings.run.mcpAllowlist,
|
|
203
206
|
verbose: options.verbose,
|
|
204
207
|
runPhase,
|
|
205
208
|
onProgress,
|
|
206
209
|
phasePolicies,
|
|
207
210
|
effortEscalation,
|
|
211
|
+
// #937 AC-4: persist the final gap report so it survives the terminal
|
|
212
|
+
// closing (previously terminal-scrollback only under `ac` policy).
|
|
213
|
+
postReport: (body) => gh.postComment(String(issueNumber), body),
|
|
208
214
|
});
|
|
209
215
|
}
|
|
210
216
|
catch (error) {
|
|
@@ -69,6 +69,8 @@ function colorStatus(status, resolvedAt) {
|
|
|
69
69
|
return chalk.yellow(status);
|
|
70
70
|
case "waiting_for_human_merge":
|
|
71
71
|
return chalk.green(status);
|
|
72
|
+
case "awaiting_verification":
|
|
73
|
+
return chalk.magenta(status);
|
|
72
74
|
case "ready_for_merge":
|
|
73
75
|
return chalk.green(status);
|
|
74
76
|
case "merged":
|
|
@@ -171,6 +173,7 @@ function displayIssueSummary(issues) {
|
|
|
171
173
|
in_progress: [],
|
|
172
174
|
waiting_for_qa_gate: [],
|
|
173
175
|
waiting_for_human_merge: [],
|
|
176
|
+
awaiting_verification: [],
|
|
174
177
|
ready_for_merge: [],
|
|
175
178
|
blocked: [],
|
|
176
179
|
not_started: [],
|
|
@@ -184,6 +187,7 @@ function displayIssueSummary(issues) {
|
|
|
184
187
|
const statusOrder = [
|
|
185
188
|
"in_progress",
|
|
186
189
|
"waiting_for_qa_gate",
|
|
190
|
+
"awaiting_verification",
|
|
187
191
|
"waiting_for_human_merge",
|
|
188
192
|
"ready_for_merge",
|
|
189
193
|
"blocked",
|
|
@@ -272,6 +272,29 @@ function detectTitleBodyTension(ac) {
|
|
|
272
272
|
suggestion: "Two verification bars detected. Either (a) tighten the title to match the runtime body (e.g., 'Smoke test execution — capture evidence'), or (b) split the runtime requirement into a separate AC.",
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Detect a test-type AC (unit/integration/browser) whose verification
|
|
277
|
+
* method came from keyword inference rather than a declared `Evidence:`
|
|
278
|
+
* clause (#938). `manual` ACs are exempt — docs/decision ACs legitimately
|
|
279
|
+
* have no runnable evidence.
|
|
280
|
+
*
|
|
281
|
+
* Warning-only, same convention as the regex-based DEFAULT_LINT_PATTERNS.
|
|
282
|
+
*
|
|
283
|
+
* @param ac - The acceptance criterion to check
|
|
284
|
+
* @returns A lint issue if evidence is missing, otherwise null
|
|
285
|
+
*/
|
|
286
|
+
function detectMissingEvidence(ac) {
|
|
287
|
+
if (ac.evidence)
|
|
288
|
+
return null;
|
|
289
|
+
if (ac.verificationMethod === "manual")
|
|
290
|
+
return null;
|
|
291
|
+
return {
|
|
292
|
+
type: "incomplete",
|
|
293
|
+
matchedPattern: ac.verificationMethod,
|
|
294
|
+
problem: `Incomplete: verification not named — method "${ac.verificationMethod}" was inferred from keywords, not declared`,
|
|
295
|
+
suggestion: "Add a trailing `Evidence:` clause naming the command or check that verifies this AC (e.g., `Evidence: \\`npm test -- foo\\``).",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
275
298
|
/**
|
|
276
299
|
* Lint a single acceptance criterion against all patterns
|
|
277
300
|
*
|
|
@@ -296,6 +319,9 @@ export function lintAcceptanceCriterion(ac, patterns = DEFAULT_LINT_PATTERNS) {
|
|
|
296
319
|
const tension = detectTitleBodyTension(ac);
|
|
297
320
|
if (tension)
|
|
298
321
|
issues.push(tension);
|
|
322
|
+
const missingEvidence = detectMissingEvidence(ac);
|
|
323
|
+
if (missingEvidence)
|
|
324
|
+
issues.push(missingEvidence);
|
|
299
325
|
return {
|
|
300
326
|
ac,
|
|
301
327
|
issues,
|
|
@@ -30,6 +30,20 @@
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
import { type AcceptanceCriterion, type AcceptanceCriteria, type ACVerificationMethod } from "./workflow/state-schema.js";
|
|
33
|
+
/**
|
|
34
|
+
* Build a word-boundary-anchored, case-insensitive matcher for a keyword.
|
|
35
|
+
* `\b` sits correctly around spaces and hyphens too, so this is safe for
|
|
36
|
+
* both single words ("ui") and phrases ("end-to-end", "unit test") — a
|
|
37
|
+
* phrase already reads as self-anchoring under plain substring matching,
|
|
38
|
+
* and anchoring it doesn't change that (#946).
|
|
39
|
+
*
|
|
40
|
+
* @param allowPlural - Also match a trailing "s" (e.g. `flag` -> `flags`).
|
|
41
|
+
* Only meaningful for single nouns/verbs that commonly appear inflected
|
|
42
|
+
* in prose; multi-word phrases never need it.
|
|
43
|
+
*
|
|
44
|
+
* @internal Exported for testing only — not part of the module's public API.
|
|
45
|
+
*/
|
|
46
|
+
export declare function keywordMatcher(keyword: string, allowPlural?: boolean): RegExp;
|
|
33
47
|
/**
|
|
34
48
|
* Infer verification method from description text
|
|
35
49
|
*
|
|
@@ -37,6 +51,32 @@ import { type AcceptanceCriterion, type AcceptanceCriteria, type ACVerificationM
|
|
|
37
51
|
* @returns The inferred verification method (defaults to 'manual')
|
|
38
52
|
*/
|
|
39
53
|
export declare function inferVerificationMethod(description: string): ACVerificationMethod;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the verification method for an AC, preferring a declared
|
|
56
|
+
* `Evidence:` clause over keyword inference (#938).
|
|
57
|
+
*
|
|
58
|
+
* - Evidence names a backtick-quoted command containing a unit-test
|
|
59
|
+
* token (`test`, `vitest`, `jest`) → `unit_test`.
|
|
60
|
+
* - Evidence names any other backtick-quoted command → `integration_test`.
|
|
61
|
+
* - Evidence is prose with no backtick command (e.g. "human review") →
|
|
62
|
+
* `manual`.
|
|
63
|
+
* - No evidence declared → falls back to {@link inferVerificationMethod}.
|
|
64
|
+
*
|
|
65
|
+
* @param description - The AC description text (evidence clause stripped)
|
|
66
|
+
* @param evidence - The declared evidence clause, if any
|
|
67
|
+
* @returns The resolved verification method
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveVerificationMethod(description: string, evidence?: string): ACVerificationMethod;
|
|
70
|
+
/**
|
|
71
|
+
* Whether a declared `Evidence:` clause describes a CLAUDE.md-style gate
|
|
72
|
+
* test — a fixture-exists / section-present / flag-wired assertion — rather
|
|
73
|
+
* than an ordinary behavioral unit/integration test or a human sign-off.
|
|
74
|
+
*
|
|
75
|
+
* @param evidence - The declared evidence clause text (from {@link splitEvidenceClause})
|
|
76
|
+
* @returns True when the evidence text matches the gate-test keyword set and
|
|
77
|
+
* does not read as a manual-review attestation
|
|
78
|
+
*/
|
|
79
|
+
export declare function isGateTestEvidence(evidence: string): boolean;
|
|
40
80
|
/**
|
|
41
81
|
* Parse acceptance criteria from GitHub issue markdown
|
|
42
82
|
*
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
import { createAcceptanceCriterion, createAcceptanceCriteria, } from "./workflow/state-schema.js";
|
|
33
|
+
import { computeFenceMask } from "./markdown-fence.js";
|
|
33
34
|
/**
|
|
34
35
|
* Regex patterns for AC extraction
|
|
35
36
|
*
|
|
@@ -115,6 +116,39 @@ const VERIFICATION_KEYWORDS = {
|
|
|
115
116
|
"manual test": "manual",
|
|
116
117
|
verify: "manual",
|
|
117
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* Escape regex metacharacters in a literal keyword before embedding it in a
|
|
121
|
+
* constructed RegExp.
|
|
122
|
+
*/
|
|
123
|
+
function escapeRegExp(literal) {
|
|
124
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build a word-boundary-anchored, case-insensitive matcher for a keyword.
|
|
128
|
+
* `\b` sits correctly around spaces and hyphens too, so this is safe for
|
|
129
|
+
* both single words ("ui") and phrases ("end-to-end", "unit test") — a
|
|
130
|
+
* phrase already reads as self-anchoring under plain substring matching,
|
|
131
|
+
* and anchoring it doesn't change that (#946).
|
|
132
|
+
*
|
|
133
|
+
* @param allowPlural - Also match a trailing "s" (e.g. `flag` -> `flags`).
|
|
134
|
+
* Only meaningful for single nouns/verbs that commonly appear inflected
|
|
135
|
+
* in prose; multi-word phrases never need it.
|
|
136
|
+
*
|
|
137
|
+
* @internal Exported for testing only — not part of the module's public API.
|
|
138
|
+
*/
|
|
139
|
+
export function keywordMatcher(keyword, allowPlural = false) {
|
|
140
|
+
const escaped = escapeRegExp(keyword);
|
|
141
|
+
const pattern = allowPlural ? `\\b${escaped}s?\\b` : `\\b${escaped}\\b`;
|
|
142
|
+
return new RegExp(pattern, "i");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Precompiled, longest-keyword-first matchers for {@link VERIFICATION_KEYWORDS}.
|
|
146
|
+
* Longest-first preserves the original precedence (e.g. "unit test" wins
|
|
147
|
+
* over a lone "unit").
|
|
148
|
+
*/
|
|
149
|
+
const VERIFICATION_KEYWORD_MATCHERS = Object.entries(VERIFICATION_KEYWORDS)
|
|
150
|
+
.sort((a, b) => b[0].length - a[0].length)
|
|
151
|
+
.map(([keyword, method]) => ({ regex: keywordMatcher(keyword), method }));
|
|
118
152
|
/**
|
|
119
153
|
* Infer verification method from description text
|
|
120
154
|
*
|
|
@@ -122,16 +156,156 @@ const VERIFICATION_KEYWORDS = {
|
|
|
122
156
|
* @returns The inferred verification method (defaults to 'manual')
|
|
123
157
|
*/
|
|
124
158
|
export function inferVerificationMethod(description) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (const keyword of sortedKeywords) {
|
|
129
|
-
if (lowerDesc.includes(keyword)) {
|
|
130
|
-
return VERIFICATION_KEYWORDS[keyword];
|
|
159
|
+
for (const { regex, method } of VERIFICATION_KEYWORD_MATCHERS) {
|
|
160
|
+
if (regex.test(description)) {
|
|
161
|
+
return method;
|
|
131
162
|
}
|
|
132
163
|
}
|
|
133
164
|
return "manual";
|
|
134
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Matches a trailing `Evidence:` clause on an AC line (#938). Only the
|
|
168
|
+
* LAST occurrence is honored — the clause is defined as trailing, and AC
|
|
169
|
+
* prose can legitimately contain the word "Evidence:" earlier in the
|
|
170
|
+
* sentence (e.g. "the report cites strong Evidence: peer review ..."
|
|
171
|
+
* before the real declaration). Splitting on the first match would corrupt
|
|
172
|
+
* extraction by swallowing the real trailing clause into the description.
|
|
173
|
+
*/
|
|
174
|
+
const EVIDENCE_CLAUSE_RE = /\bEvidence:\s*/gi;
|
|
175
|
+
/**
|
|
176
|
+
* Split a trailing `Evidence:` clause out of an AC description (#938).
|
|
177
|
+
*
|
|
178
|
+
* @param description - The AC description text (post ID-stripping)
|
|
179
|
+
* @returns The description with the clause removed, plus the declared
|
|
180
|
+
* evidence text if present
|
|
181
|
+
*/
|
|
182
|
+
function splitEvidenceClause(description) {
|
|
183
|
+
const matches = [...description.matchAll(EVIDENCE_CLAUSE_RE)];
|
|
184
|
+
if (matches.length === 0)
|
|
185
|
+
return { description };
|
|
186
|
+
const last = matches[matches.length - 1];
|
|
187
|
+
const before = description.slice(0, last.index).trim();
|
|
188
|
+
const evidence = description.slice(last.index + last[0].length).trim();
|
|
189
|
+
if (!before || !evidence)
|
|
190
|
+
return { description };
|
|
191
|
+
return { description: before, evidence };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Matches a backtick-quoted command inside a declared evidence clause.
|
|
195
|
+
* A command (vs. prose like "human review") is what makes evidence
|
|
196
|
+
* runnable/checkable rather than a manual attestation.
|
|
197
|
+
*/
|
|
198
|
+
const EVIDENCE_COMMAND_RE = /`([^`]+)`/;
|
|
199
|
+
/**
|
|
200
|
+
* Command tokens that indicate a unit-test invocation. Anything else
|
|
201
|
+
* backtick-quoted (CLI commands, curl, scripts) is treated as an
|
|
202
|
+
* integration-level check.
|
|
203
|
+
*/
|
|
204
|
+
const UNIT_TEST_COMMAND_RE = /\b(test|vitest|jest)\b/i;
|
|
205
|
+
/**
|
|
206
|
+
* Resolve the verification method for an AC, preferring a declared
|
|
207
|
+
* `Evidence:` clause over keyword inference (#938).
|
|
208
|
+
*
|
|
209
|
+
* - Evidence names a backtick-quoted command containing a unit-test
|
|
210
|
+
* token (`test`, `vitest`, `jest`) → `unit_test`.
|
|
211
|
+
* - Evidence names any other backtick-quoted command → `integration_test`.
|
|
212
|
+
* - Evidence is prose with no backtick command (e.g. "human review") →
|
|
213
|
+
* `manual`.
|
|
214
|
+
* - No evidence declared → falls back to {@link inferVerificationMethod}.
|
|
215
|
+
*
|
|
216
|
+
* @param description - The AC description text (evidence clause stripped)
|
|
217
|
+
* @param evidence - The declared evidence clause, if any
|
|
218
|
+
* @returns The resolved verification method
|
|
219
|
+
*/
|
|
220
|
+
export function resolveVerificationMethod(description, evidence) {
|
|
221
|
+
if (evidence) {
|
|
222
|
+
const commandMatch = EVIDENCE_COMMAND_RE.exec(evidence);
|
|
223
|
+
if (commandMatch) {
|
|
224
|
+
return UNIT_TEST_COMMAND_RE.test(commandMatch[1])
|
|
225
|
+
? "unit_test"
|
|
226
|
+
: "integration_test";
|
|
227
|
+
}
|
|
228
|
+
return "manual";
|
|
229
|
+
}
|
|
230
|
+
return inferVerificationMethod(description);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Keywords matching the CLAUDE.md "gate test" definition — a test whose job
|
|
234
|
+
* is to gate a claim that "a fixture exists, a skill section is present, a
|
|
235
|
+
* flag is wired" (#830, #939). Distinct from {@link VERIFICATION_KEYWORDS}:
|
|
236
|
+
* those classify *how* an AC is checked (unit/integration/browser/manual),
|
|
237
|
+
* this classifies *what kind of claim* the test makes, independent of
|
|
238
|
+
* verification method.
|
|
239
|
+
*
|
|
240
|
+
* A heuristic, not a hard classifier — same caveat {@link inferVerificationMethod}
|
|
241
|
+
* already carries. Over-firing sweeps ordinary tests into the gate-test
|
|
242
|
+
* population (inflating the mutation-verification gate's authoring burden);
|
|
243
|
+
* under-firing lets a real gate test slip through ungated, the exact defect
|
|
244
|
+
* class #830 exists to prevent.
|
|
245
|
+
*/
|
|
246
|
+
/**
|
|
247
|
+
* Single-word gate-test keywords that commonly appear inflected/pluralized
|
|
248
|
+
* in evidence prose ("flags are wired", "fixtures exist", "the section
|
|
249
|
+
* presents..."). Matched with an optional trailing "s" (#946 AC-4) so
|
|
250
|
+
* anchoring to word boundaries doesn't lose those forms.
|
|
251
|
+
*/
|
|
252
|
+
const GATE_TEST_PLURAL_KEYWORDS = ["fixture", "section", "flag", "present"];
|
|
253
|
+
/**
|
|
254
|
+
* Single-word gate-test keywords with no natural plural/inflection needed
|
|
255
|
+
* for this AC's evidence phrasing — matched as exact words only.
|
|
256
|
+
*/
|
|
257
|
+
const GATE_TEST_SINGULAR_KEYWORDS = ["wired", "exists", "registered"];
|
|
258
|
+
/**
|
|
259
|
+
* Multi-word gate-test phrases, matched as substrings (already
|
|
260
|
+
* self-anchoring, same rationale as {@link VERIFICATION_KEYWORDS} phrases).
|
|
261
|
+
*
|
|
262
|
+
* The mutation-verification rule (CLAUDE.md, #830) IS the gate-test
|
|
263
|
+
* definition — an AC that already names its own mutation-verified record
|
|
264
|
+
* is self-identifying, even when it doesn't separately name a
|
|
265
|
+
* fixture/section/flag (e.g. "lint test fails when the §7 entry is
|
|
266
|
+
* deleted (mutation-verified)").
|
|
267
|
+
*/
|
|
268
|
+
const GATE_TEST_PHRASE_KEYWORDS = [
|
|
269
|
+
"skill gate",
|
|
270
|
+
"mutation-verified",
|
|
271
|
+
"mutation test",
|
|
272
|
+
];
|
|
273
|
+
const GATE_TEST_KEYWORD_MATCHERS = [
|
|
274
|
+
...GATE_TEST_PLURAL_KEYWORDS.map((k) => keywordMatcher(k, true)),
|
|
275
|
+
...GATE_TEST_SINGULAR_KEYWORDS.map((k) => keywordMatcher(k)),
|
|
276
|
+
...GATE_TEST_PHRASE_KEYWORDS.map((k) => keywordMatcher(k)),
|
|
277
|
+
];
|
|
278
|
+
/**
|
|
279
|
+
* Phrases that mark evidence as a human-review attestation rather than an
|
|
280
|
+
* automated gate test, even when a {@link GATE_TEST_KEYWORD_MATCHERS} term also
|
|
281
|
+
* appears (#939 QA finding: "reviewed manually, fixture exists in the demo
|
|
282
|
+
* env" false-positived on `fixture` alone). Checked first and short-circuits
|
|
283
|
+
* to `false` — a human sign-off is not the mutation-verifiable claim §6i
|
|
284
|
+
* gates on, regardless of which nouns it happens to mention.
|
|
285
|
+
*/
|
|
286
|
+
const MANUAL_REVIEW_NEGATIVE_SIGNALS = [
|
|
287
|
+
"reviewed manually",
|
|
288
|
+
"manual review",
|
|
289
|
+
"human review",
|
|
290
|
+
"manually verified",
|
|
291
|
+
"manually confirmed",
|
|
292
|
+
];
|
|
293
|
+
/**
|
|
294
|
+
* Whether a declared `Evidence:` clause describes a CLAUDE.md-style gate
|
|
295
|
+
* test — a fixture-exists / section-present / flag-wired assertion — rather
|
|
296
|
+
* than an ordinary behavioral unit/integration test or a human sign-off.
|
|
297
|
+
*
|
|
298
|
+
* @param evidence - The declared evidence clause text (from {@link splitEvidenceClause})
|
|
299
|
+
* @returns True when the evidence text matches the gate-test keyword set and
|
|
300
|
+
* does not read as a manual-review attestation
|
|
301
|
+
*/
|
|
302
|
+
export function isGateTestEvidence(evidence) {
|
|
303
|
+
const lower = evidence.toLowerCase();
|
|
304
|
+
if (MANUAL_REVIEW_NEGATIVE_SIGNALS.some((signal) => lower.includes(signal))) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return GATE_TEST_KEYWORD_MATCHERS.some((regex) => regex.test(evidence));
|
|
308
|
+
}
|
|
135
309
|
/**
|
|
136
310
|
* Parse a single line and extract AC if present
|
|
137
311
|
*
|
|
@@ -146,12 +320,14 @@ function parseACLine(line) {
|
|
|
146
320
|
if (match) {
|
|
147
321
|
// Combine groups 2 and 3 for bold-wrapped format (Pattern 3)
|
|
148
322
|
// where group 3 captures optional text after closing **
|
|
149
|
-
const
|
|
323
|
+
const rawDescription = match[3]
|
|
150
324
|
? `${match[2].trim()} ${match[3].trim()}`.trim()
|
|
151
325
|
: match[2].trim();
|
|
326
|
+
const { description, evidence } = splitEvidenceClause(rawDescription);
|
|
152
327
|
return {
|
|
153
328
|
id: match[1].toUpperCase(),
|
|
154
329
|
description,
|
|
330
|
+
...(evidence !== undefined ? { evidence } : {}),
|
|
155
331
|
};
|
|
156
332
|
}
|
|
157
333
|
}
|
|
@@ -181,11 +357,11 @@ function parseACLine(line) {
|
|
|
181
357
|
export function parseAcceptanceCriteria(issueBody) {
|
|
182
358
|
const criteria = [];
|
|
183
359
|
const seenIds = new Set();
|
|
184
|
-
const push = (id, description) => {
|
|
360
|
+
const push = (id, description, evidence) => {
|
|
185
361
|
if (seenIds.has(id))
|
|
186
362
|
return;
|
|
187
363
|
seenIds.add(id);
|
|
188
|
-
criteria.push(createAcceptanceCriterion(id, description,
|
|
364
|
+
criteria.push(createAcceptanceCriterion(id, description, resolveVerificationMethod(description, evidence), evidence));
|
|
189
365
|
};
|
|
190
366
|
// Split into lines and process each. `inAcSection` tracks whether the
|
|
191
367
|
// current line falls under an `## Acceptance Criteria` heading; it toggles on
|
|
@@ -193,6 +369,10 @@ export function parseAcceptanceCriteria(issueBody) {
|
|
|
193
369
|
// rule as `parseNonGoals` in ready-gate.ts). `bareCount` numbers synthesized
|
|
194
370
|
// IDs for bare checkboxes in appearance order.
|
|
195
371
|
const lines = issueBody.split("\n");
|
|
372
|
+
// Lines inside a fenced code block (#947) — an AC-authoring example shown
|
|
373
|
+
// in a fence must not be scanned as a real AC, or toggle the AC-section
|
|
374
|
+
// heading state, in either pass below.
|
|
375
|
+
const fenceMask = computeFenceMask(lines);
|
|
196
376
|
let inAcSection = false;
|
|
197
377
|
let bareCount = 0;
|
|
198
378
|
// Pre-scan for explicit IDs anywhere in the body. Synthesis must skip IDs
|
|
@@ -201,12 +381,17 @@ export function parseAcceptanceCriteria(issueBody) {
|
|
|
201
381
|
// synthesizes AC-1 first and the author's explicit AC-1 is silently dropped
|
|
202
382
|
// by the first-occurrence dedupe.
|
|
203
383
|
const explicitIds = new Set();
|
|
204
|
-
for (
|
|
205
|
-
|
|
384
|
+
for (let i = 0; i < lines.length; i++) {
|
|
385
|
+
if (fenceMask[i])
|
|
386
|
+
continue;
|
|
387
|
+
const parsed = parseACLine(lines[i]);
|
|
206
388
|
if (parsed)
|
|
207
389
|
explicitIds.add(parsed.id);
|
|
208
390
|
}
|
|
209
|
-
for (
|
|
391
|
+
for (let i = 0; i < lines.length; i++) {
|
|
392
|
+
const line = lines[i];
|
|
393
|
+
if (fenceMask[i])
|
|
394
|
+
continue;
|
|
210
395
|
if (HEADING_RE.test(line)) {
|
|
211
396
|
inAcSection = AC_HEADING_RE.test(line);
|
|
212
397
|
continue;
|
|
@@ -215,14 +400,15 @@ export function parseAcceptanceCriteria(issueBody) {
|
|
|
215
400
|
// so previously-parsable issues are unaffected (AC-4).
|
|
216
401
|
const parsed = parseACLine(line);
|
|
217
402
|
if (parsed) {
|
|
218
|
-
push(parsed.id, parsed.description);
|
|
403
|
+
push(parsed.id, parsed.description, parsed.evidence);
|
|
219
404
|
continue;
|
|
220
405
|
}
|
|
221
406
|
// Bare-checkbox fallback: only inside the AC section (AC-1/AC-2).
|
|
222
407
|
if (inAcSection) {
|
|
223
408
|
const bare = line.match(BARE_CHECKBOX_RE);
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
409
|
+
const bareText = bare?.[1].trim();
|
|
410
|
+
if (bareText) {
|
|
411
|
+
const { description, evidence } = splitEvidenceClause(bareText);
|
|
226
412
|
// Synthesize `AC-<n>`, skipping any ID already taken by an explicit
|
|
227
413
|
// marker — before OR after this line — so a synthesized ID can never
|
|
228
414
|
// collide with (and be silently dropped against) a hand-written one.
|
|
@@ -230,7 +416,7 @@ export function parseAcceptanceCriteria(issueBody) {
|
|
|
230
416
|
do {
|
|
231
417
|
id = `AC-${++bareCount}`;
|
|
232
418
|
} while (seenIds.has(id) || explicitIds.has(id));
|
|
233
|
-
push(id, description);
|
|
419
|
+
push(id, description, evidence);
|
|
234
420
|
}
|
|
235
421
|
}
|
|
236
422
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fenced-code-block tracking for line-oriented markdown scanners.
|
|
3
|
+
*
|
|
4
|
+
* Multiple parsers in this codebase scan an issue/PR body line-by-line
|
|
5
|
+
* looking for patterns (checkbox items, `## Non-Goals` bullets). Without
|
|
6
|
+
* fence awareness, a markdown-authoring example quoted inside a fence
|
|
7
|
+
* — showing what the pattern syntax looks like — gets scanned as if it
|
|
8
|
+
* were real content. See #947 (ac-parser.ts) and its sibling in
|
|
9
|
+
* scope/analyzer.ts's `parseNonGoals`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Compute, for every line of a split markdown body, whether that line falls
|
|
13
|
+
* inside a fenced code block (CommonMark rules: matching delimiter
|
|
14
|
+
* character, closing fence length >= opening fence length; an unclosed
|
|
15
|
+
* fence runs to EOF). The delimiter lines themselves are marked `true` —
|
|
16
|
+
* they're fence syntax, not real content, so patterns should skip them too.
|
|
17
|
+
*/
|
|
18
|
+
export declare function computeFenceMask(lines: string[]): boolean[];
|
|
19
|
+
/**
|
|
20
|
+
* Blank out every line that falls inside a fenced code block, preserving
|
|
21
|
+
* line count (and therefore `\n`-relative offsets) so callers that locate
|
|
22
|
+
* sections via newline-anchored regexes on the full body are unaffected.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stripFencedLines(body: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fenced-code-block tracking for line-oriented markdown scanners.
|
|
3
|
+
*
|
|
4
|
+
* Multiple parsers in this codebase scan an issue/PR body line-by-line
|
|
5
|
+
* looking for patterns (checkbox items, `## Non-Goals` bullets). Without
|
|
6
|
+
* fence awareness, a markdown-authoring example quoted inside a fence
|
|
7
|
+
* — showing what the pattern syntax looks like — gets scanned as if it
|
|
8
|
+
* were real content. See #947 (ac-parser.ts) and its sibling in
|
|
9
|
+
* scope/analyzer.ts's `parseNonGoals`.
|
|
10
|
+
*/
|
|
11
|
+
/** Matches a fenced-code-block delimiter line (` ``` ` or `~~~`, 3+ repeats). */
|
|
12
|
+
const FENCE_DELIMITER_RE = /^\s*(`{3,}|~{3,})/;
|
|
13
|
+
/**
|
|
14
|
+
* Compute, for every line of a split markdown body, whether that line falls
|
|
15
|
+
* inside a fenced code block (CommonMark rules: matching delimiter
|
|
16
|
+
* character, closing fence length >= opening fence length; an unclosed
|
|
17
|
+
* fence runs to EOF). The delimiter lines themselves are marked `true` —
|
|
18
|
+
* they're fence syntax, not real content, so patterns should skip them too.
|
|
19
|
+
*/
|
|
20
|
+
export function computeFenceMask(lines) {
|
|
21
|
+
const mask = new Array(lines.length).fill(false);
|
|
22
|
+
let fenceChar = null;
|
|
23
|
+
let fenceLen = 0;
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
const match = lines[i].match(FENCE_DELIMITER_RE);
|
|
26
|
+
if (fenceChar === null) {
|
|
27
|
+
if (match) {
|
|
28
|
+
fenceChar = match[1][0];
|
|
29
|
+
fenceLen = match[1].length;
|
|
30
|
+
mask[i] = true;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
mask[i] = true;
|
|
35
|
+
if (match && match[1][0] === fenceChar && match[1].length >= fenceLen) {
|
|
36
|
+
fenceChar = null;
|
|
37
|
+
fenceLen = 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return mask;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Blank out every line that falls inside a fenced code block, preserving
|
|
44
|
+
* line count (and therefore `\n`-relative offsets) so callers that locate
|
|
45
|
+
* sections via newline-anchored regexes on the full body are unaffected.
|
|
46
|
+
*/
|
|
47
|
+
export function stripFencedLines(body) {
|
|
48
|
+
const lines = body.split("\n");
|
|
49
|
+
const mask = computeFenceMask(lines);
|
|
50
|
+
return lines.map((line, i) => (mask[i] ? "" : line)).join("\n");
|
|
51
|
+
}
|