bmad-method-test-architecture-enterprise 1.25.2 → 1.26.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/CHANGELOG.md +18 -0
- package/DESIGN-CRITERIA-REGISTRY.md +12 -6
- package/cli/lib/agent-adapters.js +41 -2
- package/cli/lib/build-prompt.js +46 -9
- package/cli/lib/convention-baseline.js +144 -21
- package/cli/lib/parse-report.js +60 -2
- package/cli/lib/resolve-tea-config.js +39 -3
- package/cli/test-review.js +60 -9
- package/docs/explanation/test-review-cli-architecture.md +1 -1
- package/docs/reference/tea-test-review-cli.md +19 -9
- package/package.json +1 -1
- package/src/workflows/testarch/bmad-testarch-test-review/resources/test-review.example.md +8 -6
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/criteria-registry.md +1 -1
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/step-02-discover-tests.md +36 -17
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/step-03-quality-evaluation.md +44 -2
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/step-03b-subagent-isolation.md +5 -0
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/step-03c-subagent-maintainability.md +5 -0
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/step-03e-subagent-performance.md +5 -0
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/step-03f-aggregate-scores.md +6 -2
- package/src/workflows/testarch/bmad-testarch-test-review/steps-c/step-04-generate-report.md +4 -0
- package/src/workflows/testarch/bmad-testarch-test-review/test-review-template.md +2 -0
- package/test/contracts/test-review.contract.json +3 -1
- package/test/fixtures/test-review-cli/reports/convention-baseline-unavailable.md +2 -0
- package/test/fixtures/test-review-cli/reports/convention-fabricated.md +2 -0
- package/test/fixtures/test-review-cli/reports/convention-honest-absent.md +2 -0
- package/test/fixtures/test-review-cli/stub-agent.js +9 -0
- package/test/test-test-review-cli.js +143 -15
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"name": "bmad-method-test-architecture-enterprise",
|
|
32
32
|
"source": "./",
|
|
33
33
|
"description": "Master Test Architect module for quality strategy, test automation, CI/CD quality gates, and structured testing education. Part of the BMad Method ecosystem.",
|
|
34
|
-
"version": "1.
|
|
34
|
+
"version": "1.26.0",
|
|
35
35
|
"author": {
|
|
36
36
|
"name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
|
|
37
37
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.26.0] - 2026-09-09
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `tea-test-review` spent most of a small review reading files that were not in the pull request. `step-02-discover-tests.md` §2b's convention sample was capped at 40 files, and the agent runs with no shell, so each sampled file cost its own `Read` turn on every run whatever the size of the diff. The agent now reads 8. Measured on a 50-file corpus with a one-file review set: 40 files and ~233 KB before, 8 files and ~47 KB after, five times fewer reads and a fifth of the bytes. Eight clears §2b's `sampled < 4` "corpus too small to infer a house rule" floor with margin and leaves the 0.5 established/emerging ratio a denominator that means something. The sampled count is published in the report and cross-checked by `parse-report.js`, so every report states the corpus it was judged against.
|
|
15
|
+
- The mechanical detector scan keeps the corpus it always had. It is a separate budget from the read set: `cli/lib/convention-baseline.js` opens those files itself and pays no agent turn, so it still scans the closest 40 while the agent reads 8 of them. `mechanicalSignal: false` is what forbids a report from citing any adoption for a key, and a zero observed over 8 files is much weaker evidence of absence than a zero observed over 40. Measured on this repository before the two budgets were split, `networkFirst` went from 1 of 40 to 0 of 8, which turns `emerging` into `absent` and removes the deduction entirely.
|
|
16
|
+
- The 8 are spread evenly across the closest 40. Every file in the reviewed file's own directory ranks at distance 0, so among those the ranking is the alphabetical tie-break and nothing else. At a cap of 40 against a 45-file corpus that never showed; at 8 the tie-break became the selector. Measured on this repository, a review anchored in `test/` sampled `eval-all.js` through `test-contracts.js` and nothing named `test-e*` or later was reachable by any review anchored there, on any run. Taking every `floor(i × 40 / 8)`-th file keeps the count, the determinism and the neighborhood, and the sample now spans `test/`, `test/lib/`, `test/schema/` and `test/fixtures/`.
|
|
17
|
+
- The convention corpus counts a file only when its name says it is a test, and skips `fixtures`, `__fixtures__`, `__mocks__` and `testdata` trees. `isTestFile` is deliberately wider, because the review set has to catch a changed helper under a test directory, and a directory-only match was never right for a baseline: on this repository it admitted `stub-agent.js` and a React component under `trace-eval/clean/src/tokens/`, and it admitted the seeded-defect specs under `test/fixtures/test-review-eval/seeded/` that the evals score against, so the house convention was partly established from deliberate anti-patterns. This repository's own corpus is now empty and its self-review reports the baseline as unavailable, which is the truthful answer for a repository whose only spec files are eval fixtures.
|
|
18
|
+
- The four quality workers in `step-03-quality-evaluation.md` can now actually run in parallel. Three things had to change together. `cli/lib/build-prompt.js` pinned `tea_execution_mode=sequential`; the `claude` adapter's tool list carried no delegation tool, so step-03's capability probe found no launcher; and `tea_capability_probe` was never stated at all, which resolves `auto` to `sequential` on every run whatever the mode says. The prompt now states both orchestration keys, and `cli/lib/agent-adapters.js` grants `Task` from the `scoped-artifact-writes` tier upward in both `--tools` and `--allowedTools`. A launched subagent inherits that list exactly, verified live against claude 2.1.266: the child reported `Agent, Edit, Glob, Grep, Read, Write` and no shell, so "the shell only under command-execution" holds at the workers too. `read-only` runners get no delegation, since the workers' declared outputs cannot exist at that tier, and `codex exec` exposes no subagent launcher, so `auto` resolves to `sequential` there and that adapter's run is unchanged.
|
|
19
|
+
- `tea_execution_mode` and `tea_capability_probe` resolve through the same chain the fragment keys do: `--execution-mode` / `--capability-probe` / `--no-capability-probe`, then `_bmad/tea/config.yaml`, then `src/module.yaml`. Both were previously stated in the prompt as literals, and the prompt declares that stated values beat `config.yaml`, so `tea_execution_mode: sequential` in the file that `docs/reference/troubleshooting.md` tells users to edit was being ignored.
|
|
20
|
+
- The report carries a `**Execution Mode**:` line and the verdict JSON carries `executionMode`, required on any run that measured a convention baseline. step-03 resolves its mode from a runtime capability probe and printed the result only to the agent's stdout, which the CLI reads only when the report is missing, so a run that asked for parallel workers and silently got `sequential` was indistinguishable afterwards from one that got what it asked for. `step-03f-aggregate-scores.md` also stopped hardcoding `subagent_execution: 'PARALLEL (4 quality dimensions)'` and `performance_gain: '~60% faster than sequential'` regardless of what ran.
|
|
21
|
+
- The CLI mints a `tea_run_id` and the workflow uses it as the timestamp in its four worker output paths. step-03 asked the agent to generate one with `new Date().toISOString()`, which an agent with no clock and no shell cannot execute, so it emitted a plausible string; nothing cleans `/tmp/tea-test-review-*` and step-03 checks only that the four files exist, so two runs landing on the same invented value would aggregate each other's scores.
|
|
22
|
+
- The default agent wall-clock timeout scales with the review set: 20 minutes plus 2 minutes per reviewed file, capped at the 30 minutes it used to be flat at, with `--timeout-ms` still overriding it. Nothing bounds how many turns the agent takes and no supported vendor CLI offers a turn cap (`claude` 2.1.266 has `--max-budget-usd` and no `--max-turns`; `codex exec` has neither), so the wall clock is the only vendor-agnostic bound. A one-file review that stopped making progress used to be indistinguishable from a slow one for half an hour.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Three statements of the module defaults that disagreed with `src/module.yaml` and with `cli/lib/resolve-tea-config.js`'s `MODULE_DEFAULTS`, which a test already holds equal to each other. `--use-pactjs-utils`'s help said the default is `false` and `--pact-mcp`'s said `none`; both are `true` and `"mcp"`. `docs/reference/tea-test-review-cli.md` repeated the same two wrong values and then advised the opposite of what the CLI does, telling a contract-testing repo it gets `tea_use_pactjs_utils: false` by default and should pass `--use-pactjs-utils` to change it. A repo on raw `@pact-foundation/pact` needs `--no-use-pactjs-utils`.
|
|
27
|
+
|
|
10
28
|
## [1.25.2] - 2026-09-09
|
|
11
29
|
|
|
12
30
|
### Fixed
|
|
@@ -63,7 +63,7 @@ or label-based locator now satisfies L1 outright.**
|
|
|
63
63
|
### The baseline is measured before it is judged against
|
|
64
64
|
|
|
65
65
|
`step-02-discover-tests` §2b samples the corpus **outside the review set** (a PR must
|
|
66
|
-
not establish or dilute the convention it is judged against), capped at
|
|
66
|
+
not establish or dilute the convention it is judged against), capped at 8 files
|
|
67
67
|
closest-first, and classifies each convention on a pinned threshold: `< 4` files is
|
|
68
68
|
`unknown`, `0` adopted is `absent`, `>= 50%` is `established`, otherwise `emerging`.
|
|
69
69
|
Unmeasurable means every Convention row passes as `n/a` and the report says so.
|
|
@@ -269,9 +269,13 @@ code path that looked for one.
|
|
|
269
269
|
Same fix as the score and the recommendation before it: stop trusting the agent to
|
|
270
270
|
compute a number the CLI can compute itself. `cli/lib/convention-baseline.js` now
|
|
271
271
|
performs §2b's sampling deterministically — `git ls-files`, the review set excluded,
|
|
272
|
-
ranked closest-first by directory distance, capped at
|
|
272
|
+
ranked closest-first by directory distance, capped at 8 — and, for the six keys
|
|
273
273
|
with a literal recognized form (`priorityMarkers`, `testIds`, `networkFirst`,
|
|
274
|
-
`dataFactories`, `fixtures`), scans the real
|
|
274
|
+
`dataFactories`, `fixtures`, `playwrightUtils`), scans the real file content for it.
|
|
275
|
+
The scan reads a wider slice of the same ranking than the agent does, 40 files
|
|
276
|
+
against 8, because the CLI opens them itself and the agent pays a turn per file.
|
|
277
|
+
The zero-signal floor below is only as strong as the corpus it observed nothing in,
|
|
278
|
+
so the read set got cheaper and the scanned corpus stayed where it was. `bddNaming` and
|
|
275
279
|
`assertionStyle` get no mechanical signal (no single token separates "adopted" from
|
|
276
280
|
"not" for a naming style or a dialect choice) and stay agent-judged; the
|
|
277
281
|
sampled/corpusSize grounding still applies to them.
|
|
@@ -282,7 +286,7 @@ already does, and `parse-report.js`'s `verifyConventionBaseline` binds every
|
|
|
282
286
|
`**Convention Baseline**:` line to it, one direction strictly (the sampled/corpusSize
|
|
283
287
|
counts must match exactly — they're 100% mechanical) and one direction only
|
|
284
288
|
downward (a citation claiming nonzero adoption for a key the CLI's own scan found
|
|
285
|
-
zero real occurrences of anywhere in the
|
|
289
|
+
zero real occurrences of anywhere in the scanned corpus is rejected outright; a lower
|
|
286
290
|
or judgment-based count is left alone, because a regex cannot know intent and was
|
|
287
291
|
never used to force a number up). Applied to #106's actual corpus, this scan finds
|
|
288
292
|
zero `priorityMarkers` signal too — the same zero grep found — so the fabricated
|
|
@@ -292,8 +296,10 @@ Suite 8 reproduce this end-to-end against a real temp git repo built to the same
|
|
|
292
296
|
shape (real neighbor test files, zero real priority markers anywhere).
|
|
293
297
|
|
|
294
298
|
Not fixed here, and worth naming: the "seven keys" §2b measures don't map onto the
|
|
295
|
-
registry's Convention gate class evenly.
|
|
296
|
-
Convention-gated (`priorityMarkers` → L2, `testIds` → L3, `bddNaming` → L5
|
|
299
|
+
registry's Convention gate class evenly. Four published criteria are
|
|
300
|
+
Convention-gated (`priorityMarkers` → L2, `testIds` → L3, `bddNaming` → L5, and
|
|
301
|
+
`playwrightUtils` → M9 and L9, added later and the only one whose classification
|
|
302
|
+
moves the score: M9 is MEDIUM at `established` and LOW at `emerging`);
|
|
297
303
|
`networkFirst`/`dataFactories`/`fixtures` deliberately became Applicability-gated
|
|
298
304
|
instead (see "The baseline is measured before it is judged against" above —
|
|
299
305
|
popularity shouldn't demote a navigation race), and `assertionStyle` → L7 has no
|
|
@@ -72,6 +72,25 @@ const DEFAULT_CAPABILITIES = ['scoped-artifact-writes'];
|
|
|
72
72
|
|
|
73
73
|
const WRITE_TOOLS = ['Write', 'Edit'];
|
|
74
74
|
const COMMAND_TOOLS = ['Bash'];
|
|
75
|
+
// Delegation, which claude spells `Task` on the command line and exposes to the
|
|
76
|
+
// model as `Agent`. step-03-quality-evaluation.md dispatches four quality workers
|
|
77
|
+
// and resolves its execution mode from a runtime capability probe, so without this
|
|
78
|
+
// the probe finds no launcher and every headless run collapses to `sequential` no
|
|
79
|
+
// matter what tea_execution_mode says.
|
|
80
|
+
//
|
|
81
|
+
// A launched subagent inherits this exact list, verified live against claude
|
|
82
|
+
// 2.1.266 under the argv below: the child reported `Agent, Edit, Glob, Grep, Read,
|
|
83
|
+
// Write` and no shell. So "the shell only under command-execution" holds at the four
|
|
84
|
+
// workers as well as at the parent, and granting delegation does not widen the tool
|
|
85
|
+
// surface by the back door.
|
|
86
|
+
//
|
|
87
|
+
// It is granted from `scoped-artifact-writes` upward, that being the first tier
|
|
88
|
+
// at which the workers' declared outputs can exist: each writes
|
|
89
|
+
// /tmp/tea-test-review-<dimension>-<timestamp>.json, and step-03 section 5 aborts
|
|
90
|
+
// the workflow when one of them is missing. A `read-only` runner cannot finish
|
|
91
|
+
// that step in any mode, so handing it a launcher would widen the tool surface
|
|
92
|
+
// while enabling nothing.
|
|
93
|
+
const DELEGATE_TOOLS = ['Task'];
|
|
75
94
|
|
|
76
95
|
/** The tier a capability list resolves to: the strongest one named. */
|
|
77
96
|
function strongestCapability(capabilities = DEFAULT_CAPABILITIES) {
|
|
@@ -90,6 +109,7 @@ function claudeTools(capabilities) {
|
|
|
90
109
|
...(tier === 'read-only' ? [] : WRITE_TOOLS),
|
|
91
110
|
'Glob',
|
|
92
111
|
'Grep',
|
|
112
|
+
...(tier === 'read-only' ? [] : DELEGATE_TOOLS),
|
|
93
113
|
...(tier === 'command-execution' ? COMMAND_TOOLS : []),
|
|
94
114
|
].join(',');
|
|
95
115
|
}
|
|
@@ -196,8 +216,19 @@ const AGENT_ADAPTERS = {
|
|
|
196
216
|
modelFlags: ['--model'],
|
|
197
217
|
// --safe-mode strips repo customizations for the review run; --tools/
|
|
198
218
|
// --allowedTools scope the run to the tool surface the caller's declared
|
|
199
|
-
// capabilities allow: search and read always, write
|
|
200
|
-
// the shell only under command-execution.
|
|
219
|
+
// capabilities allow: search and read always, write and delegation only
|
|
220
|
+
// above read-only, the shell only under command-execution. Both flags carry
|
|
221
|
+
// the same list because --tools decides what exists and --allowedTools
|
|
222
|
+
// decides what runs without a prompt; naming a tool in one and not the other
|
|
223
|
+
// either hides it or stops the headless run to ask about it.
|
|
224
|
+
//
|
|
225
|
+
// claude 2.1.266 has no turn cap to pair with these. `--max-budget-usd`
|
|
226
|
+
// is the nearest vendor bound and is reachable through the passthrough
|
|
227
|
+
// (--agent-arg --max-budget-usd --agent-arg 2.00); it stays out of this
|
|
228
|
+
// table for the same reason codex's reasoning effort does, since it is one
|
|
229
|
+
// vendor's flag and the other adapters cannot honor it. The vendor-agnostic
|
|
230
|
+
// bound is the wall-clock timeout in run-agent.js, which the CLI scales to
|
|
231
|
+
// the size of the review set.
|
|
201
232
|
buildArgv: (extra = [], model, capabilities = DEFAULT_CAPABILITIES) => [
|
|
202
233
|
'-p',
|
|
203
234
|
'--output-format',
|
|
@@ -226,6 +257,14 @@ const AGENT_ADAPTERS = {
|
|
|
226
257
|
// --skip-git-repo-check matters under --isolate, where the agent's cwd
|
|
227
258
|
// is a fresh tmpdir with no .git.
|
|
228
259
|
//
|
|
260
|
+
// No parallel-worker argv here, deliberately. `codex exec` exposes no
|
|
261
|
+
// subagent launcher for step-03's four quality workers to run on, so the
|
|
262
|
+
// prompt's tea_execution_mode=auto resolves through the capability probe to
|
|
263
|
+
// sequential on this adapter, which is the same run codex did before. The
|
|
264
|
+
// output contract, the aggregation, and the score are identical in either
|
|
265
|
+
// mode (step-03: "Mode changes orchestration only"), so codex loses the
|
|
266
|
+
// wall-clock gain and nothing else.
|
|
267
|
+
//
|
|
229
268
|
// Reasoning effort is deliberately not pinned here. It is a second
|
|
230
269
|
// unstated input (a local model_reasoning_effort = "max" costs ~10s even
|
|
231
270
|
// on a one-word prompt, measured 2026-08-03), but it is codex-only, so
|
package/cli/lib/build-prompt.js
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* context_files stays an invocation-only wire so PR evidence can never become
|
|
11
11
|
* a persistent user preference.
|
|
12
12
|
*
|
|
13
|
-
* It also states every TEA config key
|
|
14
|
-
* resolve-tea-config
|
|
15
|
-
*
|
|
13
|
+
* It also states every TEA config key the workflow branches on: the fragment-loading
|
|
14
|
+
* keys step-01 reads, resolved by resolve-tea-config, and the orchestration pair
|
|
15
|
+
* step-03 reads (tea_execution_mode, tea_capability_probe). An unstated key is one
|
|
16
|
+
* the agent decides for itself. Two runs over identical files would then load
|
|
17
|
+
* different knowledge, or dispatch a different number of workers.
|
|
16
18
|
*
|
|
17
19
|
* Two file lists travel in the prompt, each as a JSON array inside its own
|
|
18
20
|
* delimiters so paths are unambiguously data: the review set, which is scored,
|
|
@@ -67,20 +69,25 @@ function conventionBaselinePromptLines(conventionBaseline) {
|
|
|
67
69
|
}
|
|
68
70
|
return measured.mechanicalSignal
|
|
69
71
|
? [
|
|
70
|
-
`- ${key}: mechanically scanned; at least one
|
|
71
|
-
|
|
72
|
+
`- ${key}: mechanically scanned; at least one file in the wider scanned corpus contains a recognized form, so`,
|
|
73
|
+
' this key is NOT ruled out. That corpus is larger than the sampled files below, so the form may well live in a',
|
|
74
|
+
` file you were not asked to read. Judge the adopted count (0-${conventionBaseline.sampled}) from the sampled files alone and`,
|
|
75
|
+
' record the observed form; 0 is a legitimate answer here and does not contradict the scan.',
|
|
72
76
|
]
|
|
73
77
|
: [
|
|
74
|
-
`- ${key}: mechanically scanned across all ${conventionBaseline.
|
|
78
|
+
`- ${key}: mechanically scanned across all ${conventionBaseline.scanned} scanned files; zero occurrences of any`,
|
|
75
79
|
' recognized form were found. This convention MUST be reported as absent: adopted = 0. A report claiming ANY',
|
|
76
|
-
` nonzero adoption for ${key} will be rejected — the CLI already read every
|
|
80
|
+
` nonzero adoption for ${key} will be rejected — the CLI already read every scanned file and found nothing.`,
|
|
77
81
|
];
|
|
78
82
|
});
|
|
79
83
|
return [
|
|
80
84
|
"step-02-discover-tests.md §2b's convention baseline has already been computed for this run. Do not sample, glob,",
|
|
81
85
|
'or guess this yourself — the corpus and the counts below came from actually reading the files named, not the',
|
|
82
86
|
'reviewed files themselves (sampling the review set to judge the review set would be circular).',
|
|
83
|
-
`- corpusSize: ${conventionBaseline.corpusSize}, sampled: ${conventionBaseline.sampled}`,
|
|
87
|
+
`- corpusSize: ${conventionBaseline.corpusSize}, sampled: ${conventionBaseline.sampled}, scanned: ${conventionBaseline.scanned}`,
|
|
88
|
+
'sampled is the list below, the files you read. scanned is how many files the CLI ran its own mechanical detectors',
|
|
89
|
+
'over, which is wider because it costs the CLI a file read and costs you nothing. Cite sampled, never scanned:',
|
|
90
|
+
'the scanned corpus only decides which conventions the CLI has already ruled out for you.',
|
|
84
91
|
`The "**Convention Baseline**:" line must read exactly: ${conventionBaseline.sampled} test files sampled outside the review set`,
|
|
85
92
|
'Sampled files (read exactly these; do not substitute, add, or drop any):',
|
|
86
93
|
'---BEGIN CONVENTION CORPUS---',
|
|
@@ -126,6 +133,14 @@ function conventionBaselinePromptLines(conventionBaseline) {
|
|
|
126
133
|
* only --test-glob put there and that no built-in rule recognizes. The CLI
|
|
127
134
|
* cannot know whether a registry row attached, so it names them and the agent
|
|
128
135
|
* applies criteria-registry rule 4 rather than publishing 100 - 0 = 100.
|
|
136
|
+
* @param {string} [options.runId] - Unique id for this run, minted by the CLI. The
|
|
137
|
+
* workflow's step-03 uses it verbatim as the `timestamp` in its worker output
|
|
138
|
+
* paths. step-03 asks the agent to generate one with `new Date().toISOString()`,
|
|
139
|
+
* which a headless agent with no shell cannot execute: it emits a plausible
|
|
140
|
+
* string instead, and two runs on one machine can land on the same one. Nothing
|
|
141
|
+
* cleans /tmp/tea-test-review-*, and step-03 section 5 checks existence only, so
|
|
142
|
+
* a repeat would aggregate a previous run's scores. A real unique value from the
|
|
143
|
+
* caller removes the class.
|
|
129
144
|
* @param {object} [options.conventionBaseline] - step-02-discover-tests.md §2b's
|
|
130
145
|
* "convention baseline", pre-computed by cli/lib/convention-baseline.js instead of
|
|
131
146
|
* left to the agent to sample. `{ baselineUnavailable: true, reason }` or
|
|
@@ -149,6 +164,7 @@ function buildPrompt({
|
|
|
149
164
|
unscorableTestArtifacts = [],
|
|
150
165
|
forcedUnscorableCandidates = [],
|
|
151
166
|
conventionBaseline,
|
|
167
|
+
runId = '',
|
|
152
168
|
}) {
|
|
153
169
|
const absoluteSkillRoot = path.resolve(skillRoot);
|
|
154
170
|
const absoluteOutputPath = path.resolve(outputPath);
|
|
@@ -186,7 +202,9 @@ function buildPrompt({
|
|
|
186
202
|
`review_scope=${reviewScope}`,
|
|
187
203
|
`test_dir=${testDir}`,
|
|
188
204
|
'tea_browser_automation=none',
|
|
189
|
-
|
|
205
|
+
`tea_execution_mode=${teaConfig.tea_execution_mode}`,
|
|
206
|
+
`tea_capability_probe=${teaConfig.tea_capability_probe}`,
|
|
207
|
+
...(runId ? [`tea_run_id=${runId}`] : []),
|
|
190
208
|
`tea_use_playwright_utils=${teaConfig.tea_use_playwright_utils}`,
|
|
191
209
|
`tea_use_pactjs_utils=${teaConfig.tea_use_pactjs_utils}`,
|
|
192
210
|
`tea_pact_mcp=${teaConfig.tea_pact_mcp}`,
|
|
@@ -197,6 +215,25 @@ function buildPrompt({
|
|
|
197
215
|
'fragment set, Pact MCP) instead of inferring the flags.',
|
|
198
216
|
'The two *_installed values above were read from the project manifest by the CLI. Do not re-derive them, and do',
|
|
199
217
|
'not open package.json: they are the second half of each mandate gate, stated here for the same reason the flags are.',
|
|
218
|
+
'tea_execution_mode and tea_capability_probe are the orchestration pair step-03-quality-evaluation.md branches on,',
|
|
219
|
+
'resolved the same way the fragment keys above are: an explicit CLI flag, then _bmad/tea/config.yaml, then',
|
|
220
|
+
'src/module.yaml\'s default. Both are stated because step-03 reads both, and "auto" with probing off resolves to',
|
|
221
|
+
'sequential on every run.',
|
|
222
|
+
'Whichever mode resolves, dispatch every quality worker with the full subagentContext step-03 section 1 assembles,',
|
|
223
|
+
'written out in the launch prompt itself: the review set, the criteria-registry path resolved to an absolute path,',
|
|
224
|
+
'the convention baseline block stated above verbatim, and the playwright_utils_installed / pactjs_utils_installed',
|
|
225
|
+
'values. A worker that has to guess any of those scores rows it cannot see, which silently removes deductions.',
|
|
226
|
+
...(runId
|
|
227
|
+
? [
|
|
228
|
+
`Use tea_run_id (${runId}) verbatim wherever step-03-quality-evaluation.md section 1 says to generate a`,
|
|
229
|
+
'`timestamp`, and pass that same value to every worker and to step 3F. Do not generate one: you have no clock,',
|
|
230
|
+
'nothing cleans /tmp/tea-test-review-*, and step-03 section 5 checks only that the files exist, so an invented',
|
|
231
|
+
"value that collided with an earlier run's would aggregate that run's scores into this report.",
|
|
232
|
+
]
|
|
233
|
+
: []),
|
|
234
|
+
'Report the mode you actually ran in as exactly one "**Execution Mode**: <mode>" line in the Executive Summary,',
|
|
235
|
+
'where <mode> is agent-team, subagent, or sequential. Never write "auto" there: auto is the request, and this line',
|
|
236
|
+
'records what the capability probe resolved it to.',
|
|
200
237
|
'playwrightUtilsActive = tea_use_playwright_utils AND playwright_utils_installed; when true, load',
|
|
201
238
|
'playwright-utils-mandate.md and score registry rows M9 and L9. pactjsUtilsActive = tea_use_pactjs_utils AND',
|
|
202
239
|
'pactjs_utils_installed; when true, load pactjs-utils-mandate.md and score registry row M10.',
|
|
@@ -18,19 +18,24 @@
|
|
|
18
18
|
* is never asked to produce a number this module can produce itself.
|
|
19
19
|
*
|
|
20
20
|
* Two tiers of grounding:
|
|
21
|
-
* - corpusSize / sampled / sampledFiles: 100% mechanical (git ls-files,
|
|
22
|
-
* directory-distance ranking, a
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* and rejected outright.
|
|
21
|
+
* - corpusSize / sampled / sampledFiles: 100% mechanical (git ls-files, a real test
|
|
22
|
+
* filename with fixture trees excluded, directory-distance ranking, then a stride
|
|
23
|
+
* of 8 across the closest 40) — this is exactly step-02 §2b's sampling rules, just
|
|
24
|
+
* executed by code instead of described in prose. A report
|
|
25
|
+
* that cites a different sampled count is provably wrong and rejected outright.
|
|
26
|
+
* (verifyConventionBaseline checks the count; the corpus itself travels in the
|
|
27
|
+
* prompt as a do-not-substitute list and is not re-derivable from the report.)
|
|
26
28
|
* - adopted counts for the six keys with a concrete, literal recognized form
|
|
27
29
|
* (priorityMarkers, testIds, networkFirst, dataFactories, fixtures, playwrightUtils): a generous,
|
|
28
30
|
* high-recall regex scan over the real sampled files' real content. This is not
|
|
29
31
|
* claimed to be a precise semantic judgment — a regex cannot know intent — but it
|
|
30
32
|
* gives a safe one-directional floor: when the scan finds ZERO occurrences of any
|
|
31
|
-
* recognized form anywhere in the entire
|
|
33
|
+
* recognized form anywhere in the entire scanned corpus, the true adopted count is
|
|
32
34
|
* zero with very high confidence (a looser scan finding nothing all but rules out
|
|
33
35
|
* a careful reader finding something), so the report may never claim otherwise.
|
|
36
|
+
* That scan reads up to 40 files, wider than the 8 the agent is asked to read,
|
|
37
|
+
* because this module opens them itself and pays no agent turn for any of them.
|
|
38
|
+
* The floor is only as strong as the corpus it observed nothing in.
|
|
34
39
|
* The two keys with no literal recognized form (bddNaming — a naming *style*, not
|
|
35
40
|
* a token; assertionStyle — dialect consistency) get no mechanical signal and stay
|
|
36
41
|
* fully agent-judged; only their sampled/corpusSize grounding applies. This is an
|
|
@@ -59,7 +64,31 @@ const CONVENTION_KEYS = [
|
|
|
59
64
|
'playwrightUtils',
|
|
60
65
|
];
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
// The sample is the review's single largest input, and it is paid on every run
|
|
68
|
+
// regardless of how small the pull request is. The agent has no shell (see
|
|
69
|
+
// agent-adapters.js's claude tool list), so a sampled file can only be opened one
|
|
70
|
+
// Read at a time: the cap is a turn budget as much as a byte budget. At 40, and
|
|
71
|
+
// measured on this repository against a one-file review set, the sample was
|
|
72
|
+
// ~609 KB of files that were not in the diff; at 8 it is ~289 KB, over five times
|
|
73
|
+
// fewer reads.
|
|
74
|
+
//
|
|
75
|
+
// Eight keeps the two properties the cap has to keep. It stays above
|
|
76
|
+
// step-02-discover-tests.md §2b's `sampled < 4` "corpus too small to infer a
|
|
77
|
+
// house rule" floor with margin, and it leaves enough files for §2b's 0.5
|
|
78
|
+
// established/emerging ratio to mean something. Changing this changes the number
|
|
79
|
+
// every report cites, so step-02 §2b's "Sampling rules" states the same figure.
|
|
80
|
+
const MAX_SAMPLED_FILES = 8;
|
|
81
|
+
|
|
82
|
+
// The mechanical scan is a different budget from the read set, so it gets its own
|
|
83
|
+
// cap. Nothing the agent does bounds it: this module opens the files itself with
|
|
84
|
+
// fs.readFileSync and runs six regexes, which costs milliseconds and no turns at
|
|
85
|
+
// all. Shrinking it alongside the read set would have quietly weakened the one
|
|
86
|
+
// claim the scan exists to make. `mechanicalSignal: false` is what forbids the
|
|
87
|
+
// report from citing any adoption for a key (see conventionBaselinePromptLines
|
|
88
|
+
// and parse-report.js's verifyConventionBaseline), and a zero observed over 8
|
|
89
|
+
// files is much weaker evidence of absence than a zero observed over 40. The
|
|
90
|
+
// floor keeps its old corpus; only the agent's reading got cheaper.
|
|
91
|
+
const MAX_SCANNED_FILES = 40;
|
|
63
92
|
const MIN_CORPUS_TO_ATTEMPT = 1; // 0 eligible files outside the review set: nothing to sample at all.
|
|
64
93
|
|
|
65
94
|
// One regex per mechanically-recognizable key, matched against a sampled file's raw
|
|
@@ -85,6 +114,48 @@ const MECHANICAL_DETECTORS = {
|
|
|
85
114
|
playwrightUtils: /(?:from|require\s*\(|import\s*\()\s*['"`]@seontechnologies\/playwright-utils/i,
|
|
86
115
|
};
|
|
87
116
|
|
|
117
|
+
// §2b says "Sample test files". `isTestFile` is deliberately wider than that,
|
|
118
|
+
// because the review set has to catch a changed helper sitting under a test
|
|
119
|
+
// directory, and a directory-only match is enough there. It is not enough here. A
|
|
120
|
+
// convention baseline built from files that are not tests measures the wrong thing:
|
|
121
|
+
// on this repository the directory rule pulled in `stub-agent.js` and a React
|
|
122
|
+
// component under `trace-eval/clean/src/tokens/`, and the house Playwright
|
|
123
|
+
// convention was being established partly from those.
|
|
124
|
+
const NAMED_TEST_FILE_PATTERNS = [
|
|
125
|
+
/^test_.*\.py$/,
|
|
126
|
+
/_test\.py$/,
|
|
127
|
+
/_test\.go$/,
|
|
128
|
+
/_spec\.rb$/,
|
|
129
|
+
/\.cy\.[^.]+$/,
|
|
130
|
+
/Test\.php$/,
|
|
131
|
+
/Tests?\.cs$/,
|
|
132
|
+
/Tests?\.java$/,
|
|
133
|
+
/\.pacttest\.[^.]+$/,
|
|
134
|
+
/\.test\.[^.]+$/,
|
|
135
|
+
/\.spec\.[^.]+$/,
|
|
136
|
+
/\.test-[^.]+\.[^.]+$/,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// A repository's test-tooling fixtures are not its test suite. A spec under a
|
|
140
|
+
// directory that declares itself fixture data is an input to some other test, and
|
|
141
|
+
// the sharpest case is a seeded-defect corpus: this repository keeps deliberately
|
|
142
|
+
// bad specs under `test/fixtures/test-review-eval/seeded/` for its own evals to
|
|
143
|
+
// score against, and sampling those to establish "what this repo does" would
|
|
144
|
+
// establish the anti-patterns. Costs a consumer repository nothing, since spec
|
|
145
|
+
// files rarely live under a fixtures directory there, and when the exclusion empties
|
|
146
|
+
// the corpus the baseline reports itself unavailable rather than guessing.
|
|
147
|
+
const FIXTURE_TREE_PATTERN = /(^|\/)(fixtures|__fixtures__|__mocks__|testdata)(\/|$)/;
|
|
148
|
+
|
|
149
|
+
/** Whether a path is a real test file for baseline purposes: named like one, and not fixture data. */
|
|
150
|
+
function isConventionCorpusFile(filePath) {
|
|
151
|
+
const normalized = filePath.replaceAll('\\', '/');
|
|
152
|
+
if (FIXTURE_TREE_PATTERN.test(normalized)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const basename = normalized.slice(normalized.lastIndexOf('/') + 1);
|
|
156
|
+
return NAMED_TEST_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
157
|
+
}
|
|
158
|
+
|
|
88
159
|
const MECHANICAL_CONVENTION_KEYS = Object.keys(MECHANICAL_DETECTORS);
|
|
89
160
|
// bddNaming and assertionStyle: no literal token distinguishes "adopted" from "not",
|
|
90
161
|
// so no mechanical signal is offered for them. Grounded on sampled/corpusSize only.
|
|
@@ -138,16 +209,18 @@ function closestDistance(candidateDir, reviewedDirs) {
|
|
|
138
209
|
}
|
|
139
210
|
|
|
140
211
|
/**
|
|
141
|
-
* Mechanically measure adoption of the
|
|
142
|
-
*
|
|
212
|
+
* Mechanically measure adoption of the six literal-form conventions across the
|
|
213
|
+
* scanned files' real content.
|
|
143
214
|
*
|
|
144
215
|
* @param {object} options
|
|
145
216
|
* @param {string} options.projectRoot
|
|
146
217
|
* @param {string[]} options.sampledFiles - Repo-relative paths, already capped/ranked.
|
|
218
|
+
* This is the scanned corpus, which is wider than the agent's read set; see
|
|
219
|
+
* computeConventionBaseline's `scanned` field.
|
|
147
220
|
* @returns {object} One entry per CONVENTION_KEYS, `{ mechanical: false }` for the
|
|
148
221
|
* two judgment-only keys, `{ mechanical: true, adopted, mechanicalSignal }` for
|
|
149
222
|
* the rest. `mechanicalSignal` is `adopted > 0`, i.e. whether the scan found the
|
|
150
|
-
* convention anywhere at all in the
|
|
223
|
+
* convention anywhere at all in the scanned corpus.
|
|
151
224
|
*/
|
|
152
225
|
function measureConventions({ projectRoot, sampledFiles }) {
|
|
153
226
|
const conventions = {};
|
|
@@ -174,36 +247,82 @@ function measureConventions({ projectRoot, sampledFiles }) {
|
|
|
174
247
|
return conventions;
|
|
175
248
|
}
|
|
176
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Take `count` entries spread evenly across `ranked`, always including its head.
|
|
252
|
+
*
|
|
253
|
+
* Taking the first `count` instead would be wrong in a way that hides itself.
|
|
254
|
+
* Every file in the reviewed file's own directory has distance 0, so within that
|
|
255
|
+
* directory the ranking is the alphabetical tie-break and nothing else. At a cap of
|
|
256
|
+
* 40 against a 45-file corpus that never showed, because the sample was almost the
|
|
257
|
+
* whole corpus. At a cap of 8 the tie-break becomes the selector: measured on this
|
|
258
|
+
* repository, a review anchored in `test/` sampled `eval-all.js` through
|
|
259
|
+
* `test-contracts.js` and nothing named `test-e*` or later was reachable by any
|
|
260
|
+
* review, ever. That is a systematic wrong draw rather than a noisy one, and a
|
|
261
|
+
* systematic draw has no error bars to argue about.
|
|
262
|
+
*
|
|
263
|
+
* Striding keeps every property the head had. It is deterministic (same ranking,
|
|
264
|
+
* same indices, same files on every machine), it stays inside the reviewed file's
|
|
265
|
+
* neighborhood (the whole stride runs over the closest `scanCap` files), and it
|
|
266
|
+
* costs the same `count` reads. What it adds is that the sample spans the corpus
|
|
267
|
+
* it claims to describe.
|
|
268
|
+
*
|
|
269
|
+
* @param {string[]} ranked - Ranked file list, closest-first.
|
|
270
|
+
* @param {number} count - How many to take.
|
|
271
|
+
* @returns {string[]} Up to `count` files, in ranked order.
|
|
272
|
+
*/
|
|
273
|
+
function strideSelect(ranked, count) {
|
|
274
|
+
if (count >= ranked.length) {
|
|
275
|
+
return [...ranked];
|
|
276
|
+
}
|
|
277
|
+
const picked = [];
|
|
278
|
+
for (let index = 0; index < count; index++) {
|
|
279
|
+
picked.push(ranked[Math.floor((index * ranked.length) / count)]);
|
|
280
|
+
}
|
|
281
|
+
return picked;
|
|
282
|
+
}
|
|
283
|
+
|
|
177
284
|
function unavailable(reason) {
|
|
178
|
-
return { baselineUnavailable: true, reason, corpusSize: 0, sampled: 0, sampledFiles: [], conventions: {} };
|
|
285
|
+
return { baselineUnavailable: true, reason, corpusSize: 0, sampled: 0, scanned: 0, sampledFiles: [], conventions: {} };
|
|
179
286
|
}
|
|
180
287
|
|
|
181
288
|
/**
|
|
182
289
|
* Compute the convention baseline for a review run: the real corpus outside the
|
|
183
|
-
* review set, ranked closest-first by directory distance
|
|
184
|
-
*
|
|
185
|
-
*
|
|
290
|
+
* review set, ranked closest-first by directory distance, plus a mechanical
|
|
291
|
+
* adoption scan over real file content.
|
|
292
|
+
*
|
|
293
|
+
* Two corpora come out of the one ranking, and they are different sizes on purpose.
|
|
294
|
+
* `scanned` (the closest 40) is how many files this module opened itself for the
|
|
295
|
+
* mechanical detectors. `sampledFiles` (8, per step-02-discover-tests.md §2b's
|
|
296
|
+
* sampling rules) is a stride across those 40, and it is what the agent is told to
|
|
297
|
+
* read and the denominator every report cites. The first is not a turn budget and
|
|
298
|
+
* the second is, so keeping the scan wide costs milliseconds and keeps the
|
|
299
|
+
* zero-signal floor standing on the evidence it always had.
|
|
186
300
|
*
|
|
187
301
|
* @param {object} options
|
|
188
302
|
* @param {string} options.projectRoot - Repo root (git ls-files runs here).
|
|
189
303
|
* @param {string[]} options.reviewFiles - The review set; excluded from sampling and
|
|
190
304
|
* used as the distance anchor.
|
|
191
|
-
* @param {number} [options.cap] -
|
|
305
|
+
* @param {number} [options.cap] - Agent read-set cap (default 8, matching step-02).
|
|
306
|
+
* @param {number} [options.scanCap] - Mechanical scan cap (default 40). Raised to
|
|
307
|
+
* `cap` when it is smaller, since the read set is always drawn from the scanned
|
|
308
|
+
* corpus.
|
|
192
309
|
* @returns {object} `{ baselineUnavailable: true, reason, ... }` when no corpus
|
|
193
310
|
* exists outside the review set (or git ls-files failed), otherwise
|
|
194
|
-
* `{ baselineUnavailable: false, corpusSize, sampled, sampledFiles, conventions }`.
|
|
311
|
+
* `{ baselineUnavailable: false, corpusSize, sampled, scanned, sampledFiles, conventions }`.
|
|
195
312
|
*/
|
|
196
|
-
function computeConventionBaseline({ projectRoot, reviewFiles, cap = MAX_SAMPLED_FILES }) {
|
|
313
|
+
function computeConventionBaseline({ projectRoot, reviewFiles, cap = MAX_SAMPLED_FILES, scanCap = MAX_SCANNED_FILES }) {
|
|
197
314
|
const tracked = listGitTrackedFiles(projectRoot);
|
|
198
315
|
if (tracked === null) {
|
|
199
316
|
return unavailable('could not list repository files (git ls-files failed; not a git repo, or a git error)');
|
|
200
317
|
}
|
|
201
318
|
|
|
202
319
|
const reviewSet = new Set(reviewFiles.map((file) => file.replaceAll('\\', '/')));
|
|
203
|
-
const eligible = tracked.filter((file) => isTestFile(file) && !reviewSet.has(file));
|
|
320
|
+
const eligible = tracked.filter((file) => isTestFile(file) && isConventionCorpusFile(file) && !reviewSet.has(file));
|
|
204
321
|
const corpusSize = eligible.length;
|
|
205
322
|
if (corpusSize < MIN_CORPUS_TO_ATTEMPT) {
|
|
206
|
-
return unavailable(
|
|
323
|
+
return unavailable(
|
|
324
|
+
'no test files exist outside the review set to measure a house convention against (fixture and test-data trees are excluded)',
|
|
325
|
+
);
|
|
207
326
|
}
|
|
208
327
|
|
|
209
328
|
const reviewedDirs = reviewFiles.length > 0 ? reviewFiles.map(directoryOf) : [''];
|
|
@@ -213,15 +332,17 @@ function computeConventionBaseline({ projectRoot, reviewFiles, cap = MAX_SAMPLED
|
|
|
213
332
|
// the same sampled set on every machine regardless of the runtime's ICU/locale
|
|
214
333
|
// data, or "deterministic sampling" would itself be an environment-dependent claim.
|
|
215
334
|
.sort((a, b) => a.distance - b.distance || (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
216
|
-
const
|
|
335
|
+
const scannedFiles = ranked.slice(0, Math.max(cap, scanCap)).map((entry) => entry.file);
|
|
336
|
+
const sampledFiles = strideSelect(scannedFiles, cap);
|
|
217
337
|
|
|
218
338
|
return {
|
|
219
339
|
baselineUnavailable: false,
|
|
220
340
|
reason: null,
|
|
221
341
|
corpusSize,
|
|
222
342
|
sampled: sampledFiles.length,
|
|
343
|
+
scanned: scannedFiles.length,
|
|
223
344
|
sampledFiles,
|
|
224
|
-
conventions: measureConventions({ projectRoot, sampledFiles }),
|
|
345
|
+
conventions: measureConventions({ projectRoot, sampledFiles: scannedFiles }),
|
|
225
346
|
};
|
|
226
347
|
}
|
|
227
348
|
|
|
@@ -234,4 +355,6 @@ module.exports = {
|
|
|
234
355
|
directoryDistance,
|
|
235
356
|
directoryOf,
|
|
236
357
|
measureConventions,
|
|
358
|
+
strideSelect,
|
|
359
|
+
isConventionCorpusFile,
|
|
237
360
|
};
|
package/cli/lib/parse-report.js
CHANGED
|
@@ -71,6 +71,11 @@ const { SEVERITY_ENUM } = require('./registry-rows');
|
|
|
71
71
|
const RECOMMENDATION_ENUM = ['Approve', 'Approve with Comments', 'Request Changes', 'Block'];
|
|
72
72
|
const RECOMMENDATION_LINE = /^[ \t]*(?:\*\*Recommendation\*\*:|\*\*Recommendation:\*\*|Recommendation:)[ \t]*([^\r\n]+?)[ \t]*$/m;
|
|
73
73
|
const CONTEXT_BASIS_ENUM = ['none', 'pr_diff', 'pr_diff_truncated'];
|
|
74
|
+
// step-03-quality-evaluation.md's own three resolved modes. `auto` is a request,
|
|
75
|
+
// never a resolution, so it is deliberately absent: a report that says `auto` never
|
|
76
|
+
// ran the probe.
|
|
77
|
+
const EXECUTION_MODE_ENUM = ['agent-team', 'subagent', 'sequential'];
|
|
78
|
+
const EXECUTION_MODE_LINE_SOURCE = String.raw`^[ \t]*\*\*Execution Mode:?\*\*:?[ \t]*([^\r\n]+?)[ \t]*$`;
|
|
74
79
|
const CONTEXT_BASIS_LINE_SOURCE = String.raw`^[ \t]*\*\*Context Basis:?\*\*:?[ \t]*([^\r\n]+)[ \t]*$`;
|
|
75
80
|
const CONTEXT_WAIVERS_LINE_SOURCE = String.raw`^[ \t]*\*\*Context Waivers Applied:?\*\*:?[ \t]*([^\r\n]+)[ \t]*$`;
|
|
76
81
|
const SCORE_PATTERN = /\*\*Quality Score\*\*:\s*(\d+)\s*\/\s*100(?:[ \t]*\([ \t]*([A-F])(?=[ \t)-]))?/;
|
|
@@ -155,6 +160,7 @@ const PARSED_VERDICT_KEYS = {
|
|
|
155
160
|
},
|
|
156
161
|
conditional: {
|
|
157
162
|
conventionBaseline: 'object',
|
|
163
|
+
executionMode: 'string',
|
|
158
164
|
reportedQualityScore: 'number',
|
|
159
165
|
reportedRecommendation: 'string',
|
|
160
166
|
},
|
|
@@ -696,6 +702,51 @@ function parseConventionCitations(text) {
|
|
|
696
702
|
}));
|
|
697
703
|
}
|
|
698
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Parse the at-most-one "**Execution Mode**:" line, required on any run that
|
|
707
|
+
* measured a convention baseline.
|
|
708
|
+
*
|
|
709
|
+
* `step-03-quality-evaluation.md` resolves its own execution mode from a runtime
|
|
710
|
+
* capability probe and prints the result to the agent's stdout, which
|
|
711
|
+
* `cli/lib/run-agent.js` captures and `cli/test-review.js` reads only when the
|
|
712
|
+
* report is missing. So a run that asked for parallel workers and silently got
|
|
713
|
+
* `sequential` was indistinguishable, after the fact, from one that got what it
|
|
714
|
+
* asked for. That made "this change made the review faster" unfalsifiable: the
|
|
715
|
+
* gain could have come from anywhere. The mode is a run input the same way the
|
|
716
|
+
* model and the convention baseline are, so it travels in the verdict with them.
|
|
717
|
+
*
|
|
718
|
+
* Required exactly when the run supplied a convention baseline, which every real
|
|
719
|
+
* CLI run does. Making it unconditionally optional would have left the hole it
|
|
720
|
+
* exists to close: a report that omits the line is as silent about its mode as one
|
|
721
|
+
* written before the line existed, and the failure gradient would run the wrong way,
|
|
722
|
+
* with an omission passing and a malformed value exiting 3. A bare `parseReport`
|
|
723
|
+
* call in a unit test supplies no baseline and needs no mode.
|
|
724
|
+
*
|
|
725
|
+
* @param {string} text - Full report.
|
|
726
|
+
* @param {boolean} required - Whether this run has to state a mode.
|
|
727
|
+
* @returns {string|null} The resolved mode, or null when none is stated and none is required.
|
|
728
|
+
*/
|
|
729
|
+
function parseExecutionMode(text, required) {
|
|
730
|
+
const matches = [...text.matchAll(new RegExp(EXECUTION_MODE_LINE_SOURCE, 'gm'))];
|
|
731
|
+
if (matches.length === 0) {
|
|
732
|
+
if (required) {
|
|
733
|
+
unparseable(
|
|
734
|
+
'Report is missing the "**Execution Mode**:" line; state the mode step-03 actually resolved ' +
|
|
735
|
+
`(${EXECUTION_MODE_ENUM.join(' | ')}), so a run that fell back to sequential says so in its own artifact`,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
if (matches.length > 1) {
|
|
741
|
+
unparseable(`Report must contain at most one "**Execution Mode**:" line; found ${matches.length}`);
|
|
742
|
+
}
|
|
743
|
+
const cleaned = stripWrappers(matches[0][1].replaceAll(/\s+/g, ' ')).toLowerCase();
|
|
744
|
+
if (!EXECUTION_MODE_ENUM.includes(cleaned)) {
|
|
745
|
+
unparseable(`Report Execution Mode "${cleaned}" is not one of: ${EXECUTION_MODE_ENUM.join(' | ')}`);
|
|
746
|
+
}
|
|
747
|
+
return cleaned;
|
|
748
|
+
}
|
|
749
|
+
|
|
699
750
|
/** Extract the optional, at-most-one "**Convention Baseline**:" line's raw value. */
|
|
700
751
|
function parseConventionBaselineLine(text) {
|
|
701
752
|
const matches = [...text.matchAll(new RegExp(CONVENTION_BASELINE_LINE_SOURCE, 'gm'))];
|
|
@@ -795,8 +846,9 @@ function verifyConventionBaseline(text, conventionBaselineContract) {
|
|
|
795
846
|
if (measured && measured.mechanical && measured.mechanicalSignal === false && citation.adopted > 0) {
|
|
796
847
|
unparseable(
|
|
797
848
|
`Report Convention citation "${citation.key} (${citation.adopted} of ${citation.sampled} sampled)" claims adoption, but this run ` +
|
|
798
|
-
`scanned every one of the ${conventionBaselineContract.sampled}
|
|
799
|
-
'a convention with no real evidence in
|
|
849
|
+
`scanned every one of the ${conventionBaselineContract.scanned ?? conventionBaselineContract.sampled} files in its scanned corpus ` +
|
|
850
|
+
'for the recognized forms and found zero occurrences; a convention with no real evidence anywhere in that corpus must be ' +
|
|
851
|
+
'reported as absent (0 adopted), never a fabricated nonzero count',
|
|
800
852
|
);
|
|
801
853
|
}
|
|
802
854
|
}
|
|
@@ -1465,6 +1517,7 @@ function parseReport(reportText, runContract = {}) {
|
|
|
1465
1517
|
|
|
1466
1518
|
verifyRunContract({ reviewedFiles, contextBasis, contextFiles, excludedFiles }, runContract);
|
|
1467
1519
|
verifyConventionBaseline(text, runContract.conventionBaseline);
|
|
1520
|
+
const executionMode = parseExecutionMode(text, Boolean(runContract.conventionBaseline));
|
|
1468
1521
|
|
|
1469
1522
|
const executiveSection = extractSection(text, 'Executive Summary');
|
|
1470
1523
|
const keyStrengths = extractBullets(extractSubsection(executiveSection, 'Key Strengths'), '✅');
|
|
@@ -1500,6 +1553,9 @@ function parseReport(reportText, runContract = {}) {
|
|
|
1500
1553
|
if (runContract.conventionBaseline) {
|
|
1501
1554
|
setConditionalKey(parsed, 'conventionBaseline', runContract.conventionBaseline);
|
|
1502
1555
|
}
|
|
1556
|
+
if (executionMode !== null) {
|
|
1557
|
+
setConditionalKey(parsed, 'executionMode', executionMode);
|
|
1558
|
+
}
|
|
1503
1559
|
if (
|
|
1504
1560
|
reportedQualityScore !== qualityScore ||
|
|
1505
1561
|
(reportedQualityGrade !== undefined && reportedQualityGrade !== gradeForScore(qualityScore))
|
|
@@ -1575,6 +1631,8 @@ module.exports = {
|
|
|
1575
1631
|
CONTEXT_BASIS_ENUM,
|
|
1576
1632
|
verifyConventionBaseline,
|
|
1577
1633
|
parseConventionCitations,
|
|
1634
|
+
parseExecutionMode,
|
|
1635
|
+
EXECUTION_MODE_ENUM,
|
|
1578
1636
|
verifyFindingSeverityCounts,
|
|
1579
1637
|
extractFindings,
|
|
1580
1638
|
FINDING_KEYS,
|