pi-gauntlet 7.20.1 → 7.22.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/CHANGELOG.md +18 -0
- package/README.md +3 -3
- package/agents/code-reviewer.md +0 -2
- package/agents/plan-writer.md +7 -6
- package/extensions/lib/plan-check.test.ts +114 -18
- package/extensions/lib/plan-check.ts +113 -35
- package/extensions/phase-tracker.test.ts +35 -1
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +5 -3
- package/skills/brainstorming/gatherer.md +8 -1
- package/skills/brainstorming/reference/amendment-surface.md +1 -1
- package/skills/dispatching-parallel-agents/SKILL.md +8 -2
- package/skills/gauntlet-resume/reference/brief-contract.md +22 -2
- package/skills/requesting-code-review/SKILL.md +5 -6
- package/skills/subagent-driven-development/SKILL.md +49 -51
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +5 -5
- package/skills/subagent-driven-development/implementer-prompt.md +2 -2
- package/skills/subagent-driven-development/stop-note.md +9 -4
- package/skills/verification-before-completion/reference/conformance-check.md +2 -2
- package/skills/writing-plans/SKILL.md +39 -21
- package/skills/writing-plans/reference/plan-contract.md +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v7.22.0 - 2026-10-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Plans carry base-commit test evidence.** The `plan-writer` persona gains `bash`, runs every task's `Tests:` command on the base commit, and records a `- baseline: <sha> pass | pass via <base command> | skipped - <paths> created by this task` bullet; `plan_check` gains the `baseline-line` and `open-questions` findings and refuses to stamp without that evidence or while `## Open Questions` holds a substantive row. A failing base reaches the human once, with the command and output, through the fix-first / narrow / amend menu. A boundary the spec leaves open is settled from the caller or harness contract as a `Decision:` line in the task. `/skill:gauntlet-resume` re-baselines pre-release plans without replacing task statuses. (#72)
|
|
8
|
+
- **Brainstorming traces implicit dependencies.** The scout reports every unnamed file, binary, extension, or fixture the design relies on; a missing one lands in the premise note and the spec as a corrected fact instead of a stop. (#72)
|
|
9
|
+
- **Lookup before asking in the implement loop.** A worker's `NEEDS_CONTEXT`/`BLOCKED` question is classified as a fact ask or a decision; fact asks are looked up in the documented spec, plan, and repo locations before any stop, and the stop note carries `Searched: <where> - <result>` lines. (#72)
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Eval runner accepts unplanned improvements.** An unlisted `fails -> holds` fact is labeled `unplanned improvement` and counts as `improved`; only same-fact regressions are confirmed; cached cells are reclassified locally with zero model calls and the report regenerates even on a zero-call run. (#72)
|
|
14
|
+
|
|
15
|
+
## v7.21.0 - 2026-10-19
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Removed sequential mode; every plan task runs through the wave loop.** Singleton waves use the same implementer/spec-review chain, integration, review, and accepted-wave commit as larger waves. Re-review fixes use a single tree-to-tree fix delta captured through a scratch index, so committed, staged, and unstaged repairs produce the same repair-only patch. Each accepted wave folds repair commits into one wave commit before tracker completion. (#71)
|
|
20
|
+
|
|
3
21
|
## v7.20.1 - 2026-10-19
|
|
4
22
|
|
|
5
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
|
|
|
79
79
|
|
|
80
80
|
- **20 skills** - the workflow logic. Fifteen activate automatically when pi sees the matching kind of task, and the workflow skills chain through their gates: `shape-ticket` (creates or repairs one tracker issue per run against a Context/Problem/Idea/Acceptance-Criteria template, gated by an AC integrity check, a cheap council roast, and a single human-confirmed write; model-invocable so other skills and commands can route "file a ticket" to it, the gate still needs your yes), `gatekeep-pr` (model-invocable on an explicit ask that names the PR and the verb; the gates still need your selection - is consent-gated pre-merge verification of a PR against the scope contract at its head - the approved spec's acceptance-criteria dispositions and Design when the PR carries one, else the PR's stated intent; the ticket is cross-checked, never judged here (`check-delivery` owns it) - read-only gathering, verification evidence resolved CI-first (green checks on the exact assessed head count as evidence; a pending commit status that GitHub does not report as blocking the gating actor (`mergeStateStatus` not `BLOCKED`) is not waited for - external-CI commit statuses included - while a running Actions check, an unknown merge state, and every other merge precondition still withhold; the project's verification command runs only as fallback), a rubric-based review, a compact bottom-up report (what the PR delivers, including rows deferred or deviating per spec, PR comments, nits, blockers, then a `mergeable` / `fixable` verdict) and a verb menu whose rows derive from the actor's permissions on the PR; nothing mutates (fixes, pushes, reviews, merges, tracker comments) until you pick a row, except the opt-in `--rebase [base]` sync that rebases, reviews, and lease-pushes the head before verification, or holds the rebased head locally when a conflict stop's scoped tests fail, so the normal `fix` row resolves it before the lease push; after your own merge two opt-in rows post the `AC coverage` block to the ticket or the PR; every terminal menu offers a recommended `cleanup` row that tears down the gate's worktree and branch and runs the overrides file's `## cleanup` steps - run it with `/skill:gatekeep-pr <pr> [--rebase [base]]`), `check-delivery` (model-invocable on an explicit ask that names the ticket and the verb; the gates still need your selection - is a post-merge detective control: proves an issue actually shipped (default-branch landing, delivery target, per-AC evidence) before its tracker status advances; it never writes a terminal status - run it with `/skill:check-delivery <ref>`), `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `dispatching-parallel-agents`, `verification-before-completion`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `forge-skill`, `linear` (reads/searches/comments on/manages Linear tickets via the `linearis` CLI; owns all linearis mechanics and the `## Issue tracker` overrides schema; tracker-facing skills route to it). Five more require explicit entry (`disable-model-invocation: true`): `brainstorming` starts the gated workflow on explicit consent - use `/skill:brainstorming` for reliable discovery, or opt in through reachable explicit prose or a human-selected handoff; after spec approval, downstream planning and implementation still chain automatically. `chase-bug` is human-only bug triage: read-only root-cause discovery to an evidenced verdict menu (real bug -> ticket/brainstorm/hotfix/respond; five negative verdicts), then a gated response to the reporter for addressable origins (GitHub issue / tracker ticket) and a rendered verdict summary otherwise - it never fixes during triage; the hotfix row hands off to `skills/chase-bug/hotfix.md` after the menu - run it with `/skill:chase-bug`. `gauntlet-performance` reads the committed run telemetry (current repo by default, `--dir <path>` adds others) through the parse-only `gauntlet-performance` CLI and answers with one example-led recommendation, 3-5 cornerstone numbers, a council assessment when available, and a menu of at most three actions (render a report file, open the recommendation as a ticket via `shape-ticket`, drill into one run); it writes nothing unless you pick render - run it with `/skill:gauntlet-performance [--dir <path>]... [--since <version>]`. `gauntlet-handoff` ends a session whose flow a fresh session will continue: it invokes pi-cohort's `handoff` skill (`--out <path>` or `--key <name>`, default key = the run worktree's branch with `/` flattened to `-`, default file `<tmpdir>/pi-handoff/<key>.md`), then appends the gauntlet process-state section (phase/plan tracker status) per the shared contract `skills/gauntlet-resume/reference/brief-contract.md` - run it with `/skill:gauntlet-handoff [--out <path> | --key <name>]`. `gauntlet-resume` is the only way back into an interrupted flow from a fresh session: it takes a `gauntlet-handoff` brief (a file path, pasted text, or - with no arguments - a pick from the briefs under `<tmpdir>/pi-handoff/`), a bare worktree that already holds a spec, or a spec tracked on main under a configured spec dir (a spec seed: it creates or reuses the worktree through `using-git-worktrees` and pins the spec), restores phase/plan tracker state through the legal arming sequence (`start brainstorm`, `skip` with `resume:` reasons, `plan_check` before implement-or-later), and never infers approval from artifacts - run it with `/skill:gauntlet-resume [<brief-file> | <spec>.md | <worktree-name-or-path>]`.
|
|
81
81
|
- **9 subagent personas** - the specialized child agents the skills dispatch via pi-cohort: `implementer`, `code-reviewer`, `spec-reviewer`, `conformance-reviewer`, `spec-summarizer`, `plan-writer`, `spec-council-member`, `spec-council-synthesizer`, `spec-council-applier`. See [doc/personas.md](./doc/personas.md) for what each one does and why its permissions are scoped the way they are.
|
|
82
|
-
- **4 runtime extensions** - the enforcement layer. `plan-tracker` and `phase-tracker` are tools skills call to track progress (with a TUI widget); `verify-before-ship` is a hook that warns if you push or open a PR without a passing test run since your last edit; a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. In a brainstorming-entered flow, phase-tracker rejects `implement` or `verify` completion while tracker tasks remain pending or in progress; see [its configuration reference](./doc/configuration.md#phase-tracker). phase-tracker also registers `plan_check`, which verifies a plan against its spec and against the grammar in [skills/writing-plans/reference/plan-contract.md](./skills/writing-plans/reference/plan-contract.md), including that each task's `Tests:` commands are selective and never the full suite; a pass stamps the plan for implementation. `telemetry` records one YAML record per gauntlet run (phase timing, models, personas, gate/fix rounds, diff at ship) that ships in the squash beside the spec - it is written only inside a brainstorming-entered flow, kept out of history until `finishing-a-development-branch` seals it once with `gauntlet-telemetry-seal` (a `telemetry:` commit under git; the working-copy snapshot in a plain jj workspace) - and blocks a brainstorm `write` into an already-shipped spec; see [its configuration reference](./doc/configuration.md#telemetry). See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
|
|
82
|
+
- **4 runtime extensions** - the enforcement layer. `plan-tracker` and `phase-tracker` are tools skills call to track progress (with a TUI widget); `verify-before-ship` is a hook that warns if you push or open a PR without a passing test run since your last edit; a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. In a brainstorming-entered flow, phase-tracker rejects `implement` or `verify` completion while tracker tasks remain pending or in progress; see [its configuration reference](./doc/configuration.md#phase-tracker). phase-tracker also registers `plan_check`, which verifies a plan against its spec and against the grammar in [skills/writing-plans/reference/plan-contract.md](./skills/writing-plans/reference/plan-contract.md), including that each task's `Tests:` commands are selective and never the full suite, and that every test command carries a passing base-commit result; a pass stamps the plan for implementation. `telemetry` records one YAML record per gauntlet run (phase timing, models, personas, gate/fix rounds, diff at ship) that ships in the squash beside the spec - it is written only inside a brainstorming-entered flow, kept out of history until `finishing-a-development-branch` seals it once with `gauntlet-telemetry-seal` (a `telemetry:` commit under git; the working-copy snapshot in a plain jj workspace) - and blocks a brainstorm `write` into an already-shipped spec; see [its configuration reference](./doc/configuration.md#telemetry). See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
|
|
83
83
|
|
|
84
84
|
pi-gauntlet is **opinionated**: it recommends this pipeline for non-trivial changes, but starts `brainstorming` only after explicit user consent. Enforcement is opt-in by entry, not ambient: once brainstorming starts a flow, the phase-tracker extension mechanically blocks a phase from closing before its gate runs, and warns once if the main loop writes code during implement (subagents own implement-phase edits). A change made *without* entering the flow (a typo, a formatting run, a dependency bump - see "When to use / when NOT to use") is not gated; the discipline of routing real work through the pipeline is a convention the tooling supports, not a trap it springs on every edit.
|
|
85
85
|
|
|
@@ -110,7 +110,7 @@ Run by a human against pi-cohort >= 7.1.0, before a pi-gauntlet release claims t
|
|
|
110
110
|
| Gate | A machine-enforced checkpoint between phases (e.g. within a brainstorming-entered flow, `complete verify` is blocked until conformance review has run). Not a suggestion. |
|
|
111
111
|
| Spec council | Multi-model critique of the spec before you see it (`roasting-the-spec`); members and chair return saved-file references, and a fresh applier inherits the main loop's model and thinking unless overridden, edits only the spec, and returns the audit. The main loop checks structure without ingesting member/chair findings, re-reads the spec, and hands it to the unchanged user gate to ratify or revert. Members flag unrelated extras or sufficient in-scope simplifications (`over-spec`); every member closes with a `lean:` line - "nothing to cut" is a first-class answer - and the chair tallies them (`lean: <k> of <n> members found nothing to cut`). Falls back to a single-model critique if no council is configured. |
|
|
112
112
|
| Conformance gate | The closing check: does the delivered code + docs match your *original prompt*, not the derived plan? Compatible executable recommendations auto-fix first; anything still open renders as a numbered decision brief - one item per decision, the problem in plain words first, lettered options each with a one-line consequence, and `Recommended: <letter> - <reason>` or `No recommendation - pick one`. Reply with one line per item (`1. B`), or bare `1` to take every recommendation (no-recommendation items are re-asked by number); a current `CONFORMS` / no-concerns handoff goes straight to branch options with no extra sign-off. |
|
|
113
|
-
| Wave | A batch of plan tasks
|
|
113
|
+
| Wave | A batch of N >= 1 plan tasks with disjoint files, run through the one wave loop; N > 1 dispatches concurrently |
|
|
114
114
|
| Overrides file | `.pi/gauntlet-overrides.md` - where you put project-specific detail the generic skills don't know (CI command, worktree wrapper, routing rules). |
|
|
115
115
|
|
|
116
116
|
## When to use / when NOT to use
|
|
@@ -122,7 +122,7 @@ Run by a human against pi-cohort >= 7.1.0, before a pi-gauntlet release claims t
|
|
|
122
122
|
## Requirements
|
|
123
123
|
|
|
124
124
|
- [pi-coding-agent](https://github.com/earendil-works/pi) ≥ 0.85.1 - tested minimum for sequential `phase_tracker` tool execution, which persists a tracker update before a later tool call in the same model message.
|
|
125
|
-
- [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.4.1 - required peer package, including binary-safe worktree patches. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `forge-skill`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call. Gauntlet execution dispatches explicitly use `async: false`; do not enable pi-cohort's incompatible `forceTopLevelAsync` setting. See pi-cohort's [dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md) for that setting's owner and semantics.
|
|
125
|
+
- [pi-cohort](https://github.com/jjuraszek/pi-cohort) >= 7.4.1 - required peer package, including binary-safe worktree patches. Skills that dispatch agents (`requesting-code-review`, `subagent-driven-development`, `dispatching-parallel-agents`, `writing-plans`, `forge-skill`, `shape-ticket`, `roasting-the-spec`) call `subagent({})`, which pi-cohort provides. pi-gauntlet does not vendor the dispatch tool; without pi-cohort those skills have nothing to call. Gauntlet execution dispatches explicitly use `async: false`; do not enable pi-cohort's incompatible `forceTopLevelAsync` setting. See pi-cohort's [dispatch configuration](https://github.com/jjuraszek/pi-cohort/blob/main/doc/configuration.md) for that setting's owner and semantics. A wave of N >= 1 tasks dispatches its implementers and first spec reviews as one chain call.
|
|
126
126
|
|
|
127
127
|
Both packages must be listed in your `.pi/settings.json#packages` array (pi adds them automatically when you `pi install`). pi-gauntlet and pi-cohort are versioned independently but release together whenever dispatch semantics change - pin compatible versions of both.
|
|
128
128
|
|
package/agents/code-reviewer.md
CHANGED
|
@@ -77,8 +77,6 @@ with one partition line over the `Fn` IDs assigned above; when a task requires
|
|
|
77
77
|
a trailing `TRAJECTORY:` verdict (re-review), that verdict comes after `Behaviour-change:` as the
|
|
78
78
|
true final line:
|
|
79
79
|
|
|
80
|
-
<!-- writing-plans' plan-time Parallel-safe: line is a deliberately different free-text form; do not unify -->
|
|
81
|
-
|
|
82
80
|
```
|
|
83
81
|
Parallel-safe: <group>[; <group>]*
|
|
84
82
|
<group> = <comma-separated finding-id list> " disjoint"
|
package/agents/plan-writer.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plan-writer
|
|
3
3
|
description: Writes, checks, and repairs the gauntlet implementation plan for one approved spec in a fresh context; the plan file is its only output. Dispatched only by the writing-plans skill; not for direct dispatch.
|
|
4
|
-
tools: read, grep, find, ls, write, plan_check
|
|
4
|
+
tools: read, grep, find, ls, bash, write, plan_check
|
|
5
5
|
inheritProjectContext: false
|
|
6
6
|
inheritSkills: false
|
|
7
7
|
completionGuard: true
|
|
@@ -23,7 +23,7 @@ No plan content, no summary, no commentary.
|
|
|
23
23
|
|
|
24
24
|
The task names five absolute paths: the worktree, the approved spec, the plan path, the plan skill file, and the plan contract file. It may also carry one block after them:
|
|
25
25
|
|
|
26
|
-
- `Findings:` - the verbatim output of the orchestrator's own `plan_check` on the plan at the plan path, or a human's answers to the plan's open questions. Read the existing plan first and keep everything the block does not touch. Treat each answer as the resolution of the matching
|
|
26
|
+
- `Findings:` - the verbatim output of the orchestrator's own `plan_check` on the plan at the plan path, or a human's answers to the plan's open questions. Read the existing plan first and keep everything the block does not touch, except baseline rows, which Baseline owns and regenerates. Treat each answer as the resolution of the matching contract question; remove the section when no questions remain. `Findings: rerun baseline` reruns every task command.
|
|
27
27
|
- `Amend:` - the amended spec sections and the rule for the edit. Read the existing plan first; preserve every task title and wave position the amendment does not touch; edit only the affected anchors and tasks.
|
|
28
28
|
|
|
29
29
|
## Procedure
|
|
@@ -32,12 +32,13 @@ The task names five absolute paths: the worktree, the approved spec, the plan pa
|
|
|
32
32
|
2. Read the plan skill file (`SKILL.md`) from its `## File Structure` heading through the end of its `## Self-Review` section, and the whole plan contract file (`plan-contract.md`). That text is the plan contract; follow it as written.
|
|
33
33
|
3. Read the spec in full. Recon the code the spec touches by reading it directly: exact paths to create, modify, or delete; call sites and tests with line ranges; the scoped command that runs one test file; scoped and repo-wide lint and format commands; the full-suite verification entrypoint and whether it bundles style checks. Dispatch nothing.
|
|
34
34
|
4. Write the plan at the plan path with `write` as a full replacement. The plan path is the only file you write; never create or edit any other file.
|
|
35
|
-
5. Run `plan_check({ planPath })
|
|
36
|
-
6.
|
|
37
|
-
7. Stop when `plan_check` passes and
|
|
35
|
+
5. Run Baseline from the skill's Self-Review before `plan_check({ planPath })`. On Findings/Amend re-dispatch, read provenance once for the current base; retain evidence only for unchanged commands on that same base, rerun missing, invalid, changed-command or changed-SHA evidence, and rerun every command for `Findings: rerun baseline`. Never rewrite SHAs without execution. Baseline owns and regenerates its rows; preserve unresolved contract rows.
|
|
36
|
+
6. Run the deterministic checker and judgment checks in Self-Review, including Contract decisions. Repair `baseline-line` and substantive baseline `open-questions` findings only by Baseline execution, never by authoring evidence or deleting/rewording a row. Normalize malformed Open Questions into rows without changing substantive text. Fix every other finding; with substantive rows remaining, reply `OPEN QUESTIONS <abs plan path>` immediately, without a passing check or repair rounds for those rows. Any ordinary edit returns to step 5.
|
|
37
|
+
7. Stop when `plan_check` passes and judgment checks cause no edit. Apply the 3-round budget only to ordinary findings: convert an exhausted finding to an Open Questions row and stop. Findings blocks continue that finding's count across dispatches. Omit the section when no rows remain and reply per the output contract.
|
|
38
38
|
|
|
39
39
|
## Rules
|
|
40
40
|
|
|
41
|
-
-
|
|
41
|
+
- Use `bash` only for Baseline provenance (`git rev-parse`, `git status --porcelain`, or the contract's jj equivalents) and the plan's own `Tests:` commands in the worktree. Write only the plan. Never edit tracked source, commit, run `stash`, `checkout`, `restore`, `clean`, or mutate jj's working copy or revisions (`new`, `commit`, `abandon`, `restore`, `squash`).
|
|
42
|
+
- Never leave a placeholder in authored plan prose; retain verbatim failure evidence in row continuation fences. Derive decisions from source per Contract decisions; name only an unsettled decision as an open question.
|
|
42
43
|
- Never call `plan_tracker`, `phase_tracker`, or `subagent`; the orchestrator owns them.
|
|
43
44
|
- Never reply with more than the one line.
|
|
@@ -42,6 +42,7 @@ const VALID_PLAN = `# Fixture Plan
|
|
|
42
42
|
|
|
43
43
|
**Tests:**
|
|
44
44
|
- \`node --test extensions/lib/fixture-task1.test.ts\`
|
|
45
|
+
- baseline: abc1234 pass
|
|
45
46
|
- via: \`helperFn()\`
|
|
46
47
|
|
|
47
48
|
This task implements helperFn() for parsing.
|
|
@@ -57,13 +58,12 @@ This task implements helperFn() for parsing.
|
|
|
57
58
|
|
|
58
59
|
**Tests:**
|
|
59
60
|
- \`node --test extensions/lib/fixture-task2.test.ts\`
|
|
61
|
+
- baseline: abc1234 pass
|
|
60
62
|
|
|
61
63
|
This task handles naming details.
|
|
62
64
|
|
|
63
65
|
## Wave 2 — Solo task
|
|
64
66
|
|
|
65
|
-
Solo: lone remaining task
|
|
66
|
-
|
|
67
67
|
### Task 3: Document banned token handling
|
|
68
68
|
|
|
69
69
|
**Spec:** doc/specs/fixture-spec.md § "Testing" L9
|
|
@@ -74,6 +74,7 @@ Solo: lone remaining task
|
|
|
74
74
|
|
|
75
75
|
**Tests:**
|
|
76
76
|
- \`node --test extensions/lib/fixture-task3.test.ts\`
|
|
77
|
+
- baseline: abc1234 pass
|
|
77
78
|
|
|
78
79
|
The literal TODO is intentionally documented here per spec quote-integrity requirement.
|
|
79
80
|
|
|
@@ -520,8 +521,6 @@ const ENTRYPOINT_PLAN = `# Fixture Plan
|
|
|
520
521
|
|
|
521
522
|
## Wave 1 — Solo
|
|
522
523
|
|
|
523
|
-
Solo: lone remaining task
|
|
524
|
-
|
|
525
524
|
### Task 1: Scoped test
|
|
526
525
|
|
|
527
526
|
**Spec:** doc/specs/fixture-spec.md § "Testing" L4
|
|
@@ -532,6 +531,7 @@ Solo: lone remaining task
|
|
|
532
531
|
|
|
533
532
|
**Tests:**
|
|
534
533
|
- \`node --test extensions/lib/plan-check.test.ts\`
|
|
534
|
+
- baseline: abc1234 pass
|
|
535
535
|
|
|
536
536
|
Run node --test extensions/lib/plan-check.test.ts and confirm green.
|
|
537
537
|
|
|
@@ -550,7 +550,7 @@ test("quote-integrity: header entrypoint literal on an anchored line is satisfie
|
|
|
550
550
|
|
|
551
551
|
test("quote-integrity: scoped command on the same anchored line is still required in the task body", () => {
|
|
552
552
|
const mutated = ENTRYPOINT_PLAN.replace(
|
|
553
|
-
"- Test: extensions/lib/plan-check.test.ts\n\n**Tests:**\n- `node --test extensions/lib/plan-check.test.ts`\n\nRun node --test extensions/lib/plan-check.test.ts and confirm green.",
|
|
553
|
+
"- Test: extensions/lib/plan-check.test.ts\n\n**Tests:**\n- `node --test extensions/lib/plan-check.test.ts`\n- baseline: abc1234 pass\n\nRun node --test extensions/lib/plan-check.test.ts and confirm green.",
|
|
554
554
|
"- Test: extensions/lib/other.test.ts\n\n**Tests:**\n- `node --test extensions/lib/other.test.ts`\n\nRun the scoped test and confirm green.",
|
|
555
555
|
);
|
|
556
556
|
const qi = findingsFor(checkPlan(mutated, ENTRYPOINT_SPEC, alwaysTruePort()), "quote-integrity");
|
|
@@ -649,7 +649,7 @@ test("check 4 fail-closed: invalid glob (port throws)", () => {
|
|
|
649
649
|
|
|
650
650
|
test("check 4 fail-closed: task missing **Files:** block", () => {
|
|
651
651
|
const mutated = VALID_PLAN.replace(
|
|
652
|
-
"**Files:**\n- Modify: extensions/lib/fixture-task3.ts\n- Test: extensions/lib/fixture-task3.test.ts\n\n**Tests:**\n- `node --test extensions/lib/fixture-task3.test.ts`\n\n",
|
|
652
|
+
"**Files:**\n- Modify: extensions/lib/fixture-task3.ts\n- Test: extensions/lib/fixture-task3.test.ts\n\n**Tests:**\n- `node --test extensions/lib/fixture-task3.test.ts`\n- baseline: abc1234 pass\n\n",
|
|
653
653
|
"",
|
|
654
654
|
);
|
|
655
655
|
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
@@ -698,13 +698,14 @@ test("check 6 wave-file-disjointness: two tasks in the same wave declare the sam
|
|
|
698
698
|
);
|
|
699
699
|
});
|
|
700
700
|
|
|
701
|
-
test("
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
assert.
|
|
707
|
-
|
|
701
|
+
test("every-wave-singleton plan without Solo: lines passes; legacy Solo: lines are accepted silently", () => {
|
|
702
|
+
const plan = VALID_PLAN.replace(
|
|
703
|
+
"### Task 2: Implement naming",
|
|
704
|
+
"## Wave 2 - Naming\n\n### Task 2: Implement naming",
|
|
705
|
+
).replace("## Wave 2 — Solo task", "## Wave 3 - Documentation");
|
|
706
|
+
assert.deepEqual(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), []);
|
|
707
|
+
const legacy = plan.replace("## Wave 2 - Naming", "## Wave 2 - Naming\n\nSolo: Task 2 depends on Wave 1");
|
|
708
|
+
assert.deepEqual(checkPlan(legacy, SPEC_TEXT, alwaysTruePort()), []);
|
|
708
709
|
});
|
|
709
710
|
|
|
710
711
|
test("check 8 header-entrypoint: verification entrypoint string reused outside the header", () => {
|
|
@@ -746,8 +747,6 @@ const EXEMPTION_PLAN = `# Exemption Plan
|
|
|
746
747
|
|
|
747
748
|
## Wave 1 — Solo task
|
|
748
749
|
|
|
749
|
-
Solo: only task in this wave
|
|
750
|
-
|
|
751
750
|
### Task 1: Document banned token handling
|
|
752
751
|
|
|
753
752
|
**Spec:** doc/specs/exemption-spec.md § "Notes" L4
|
|
@@ -758,6 +757,7 @@ Solo: only task in this wave
|
|
|
758
757
|
|
|
759
758
|
**Tests:**
|
|
760
759
|
- \`node --test extensions/lib/exemption-task1.test.ts\`
|
|
760
|
+
- baseline: abc1234 pass
|
|
761
761
|
|
|
762
762
|
The literal TODO is intentionally documented here per spec quote-integrity requirement.
|
|
763
763
|
|
|
@@ -799,9 +799,8 @@ test("check 2/5 exemption negative: without the task body carrying the literal,
|
|
|
799
799
|
);
|
|
800
800
|
});
|
|
801
801
|
|
|
802
|
-
test("aggregate: independent mutations across
|
|
802
|
+
test("aggregate: independent mutations across two checks are all reported together", () => {
|
|
803
803
|
let mutated = VALID_PLAN;
|
|
804
|
-
mutated = mutated.replace("Solo: lone remaining task\n\n", "");
|
|
805
804
|
mutated = mutated.replace(
|
|
806
805
|
"- via: `helperFn()`\n\nThis task implements helperFn() for parsing.",
|
|
807
806
|
"- via: `parserSeam()`\n\nThis task implements the helper for parsing.",
|
|
@@ -811,7 +810,6 @@ test("aggregate: independent mutations across three checks are all reported toge
|
|
|
811
810
|
"- Modify: extensions/lib/fixture-shared.ts",
|
|
812
811
|
);
|
|
813
812
|
const findings = checkPlan(mutated, SPEC_TEXT, alwaysTruePort());
|
|
814
|
-
assert.ok(findingsFor(findings, "solo-line").length > 0, "expected a solo-line finding");
|
|
815
813
|
assert.ok(findingsFor(findings, "quote-integrity").length > 0, "expected a quote-integrity finding");
|
|
816
814
|
assert.ok(findingsFor(findings, "wave-file-disjointness").length > 0, "expected a wave-file-disjointness finding");
|
|
817
815
|
});
|
|
@@ -1050,6 +1048,104 @@ test("tests-block: no path normalization - ./x and x differ, as in Files:", () =
|
|
|
1050
1048
|
assert.ok(tb(mutated).some((x) => x.reason.includes("not anchored")));
|
|
1051
1049
|
});
|
|
1052
1050
|
|
|
1051
|
+
const BASELINE = "- baseline: abc1234 pass";
|
|
1052
|
+
const T1_COMMAND = "node --test extensions/lib/fixture-task1.test.ts";
|
|
1053
|
+
const T1_PATH = "extensions/lib/fixture-task1.test.ts";
|
|
1054
|
+
const baselineFindings = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()), "baseline-line");
|
|
1055
|
+
|
|
1056
|
+
for (const [name, evidence] of [
|
|
1057
|
+
["pass", BASELINE],
|
|
1058
|
+
["pass artifacts", BASELINE + " - artifacts: coverage.json, report.txt"],
|
|
1059
|
+
["via", BASELINE + " via `" + T1_COMMAND + "`"],
|
|
1060
|
+
["via artifacts", BASELINE + " via `" + T1_COMMAND + "` - artifacts: coverage.json"],
|
|
1061
|
+
] ) {
|
|
1062
|
+
test(`baseline-line accepts ${name}`, () => {
|
|
1063
|
+
assert.deepEqual(baselineFindings(VALID_PLAN.replace(BASELINE, evidence)), []);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
for (const [name, evidence, reason] of [
|
|
1068
|
+
["missing", "", `Task 1: command \`${T1_COMMAND}\` has no baseline: bullet`],
|
|
1069
|
+
["bad sha", "- baseline: ABC1234 pass", "Task 1: baseline: bullet malformed:"],
|
|
1070
|
+
["short sha", "- baseline: abc123 pass", "Task 1: baseline: bullet malformed:"],
|
|
1071
|
+
["long sha", "- baseline: " + "a".repeat(41) + " pass", "Task 1: baseline: bullet malformed:"],
|
|
1072
|
+
["bad result", "- baseline: abc1234 fail", "Task 1: baseline: bullet malformed:"],
|
|
1073
|
+
["empty base command", BASELINE + " via ` `", "Task 1: baseline: bullet malformed:"],
|
|
1074
|
+
["separator-only base command", BASELINE + " via ` ; `", "Task 1: baseline: bullet malformed:"],
|
|
1075
|
+
["misplaced", "- via: seam\n" + BASELINE, "Task 1: baseline: not directly after its command"],
|
|
1076
|
+
["duplicate", BASELINE + "\n" + BASELINE, "Task 1: baseline: not directly after its command"],
|
|
1077
|
+
["sha mismatch", "- baseline: def5678 pass", "baseline SHAs differ: def5678, abc1234"],
|
|
1078
|
+
["skip existing", `- baseline: abc1234 skipped - ${T1_PATH} created by this task`, `Task 1: baseline: skipped but \`${T1_PATH}\` is not a Create: path`],
|
|
1079
|
+
["via unanchored", BASELINE + " via `echo hi`", `Task 1: baseline: pass via \`echo hi\` segment \`echo hi\` anchors no existing Test: path`],
|
|
1080
|
+
["via unanchored compound", BASELINE + " via `" + T1_COMMAND + " && echo hi`", "anchors no existing Test: path"],
|
|
1081
|
+
]) {
|
|
1082
|
+
test(`baseline-line rejects ${name}`, () => {
|
|
1083
|
+
const findings = baselineFindings(VALID_PLAN.replace(BASELINE, evidence));
|
|
1084
|
+
assert.ok(findings.some(f => f.reason.includes(reason)), JSON.stringify(findings));
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
test("baseline-line accepts 40-character SHAs and introduced-selector base commands", () => {
|
|
1089
|
+
assert.deepEqual(baselineFindings(VALID_PLAN.replaceAll("abc1234", "a".repeat(40))), []);
|
|
1090
|
+
const plan = VALID_PLAN.replace(`- \`${T1_COMMAND}\``, `- \`${T1_COMMAND}::new_case\``)
|
|
1091
|
+
.replace(BASELINE, BASELINE + " via `" + T1_COMMAND + "`");
|
|
1092
|
+
assert.deepEqual(baselineFindings(plan), []);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
test("baseline-line pass via rejects created tokens outside this task's Test paths", () => {
|
|
1096
|
+
const plan = VALID_PLAN.replace(BASELINE, BASELINE + " via `" + T1_COMMAND + " extensions/lib/fixture-task2.ts`");
|
|
1097
|
+
assert.ok(baselineFindings(plan).some(f => f.reason.includes("anchors Create: path extensions/lib/fixture-task2.ts")));
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
test("baseline-line validates skipped anchor sets and derived surviving segments", () => {
|
|
1101
|
+
const created = VALID_PLAN.replace("- Create: extensions/lib/fixture-task1.ts", `- Create: ${T1_PATH}`);
|
|
1102
|
+
const skipped = `- baseline: abc1234 skipped - ${T1_PATH} created by this task`;
|
|
1103
|
+
assert.deepEqual(baselineFindings(created.replace(BASELINE, skipped)), []);
|
|
1104
|
+
assert.ok(baselineFindings(created.replace(BASELINE, BASELINE + " via `" + T1_COMMAND + "`" )).some(f => f.reason.includes("anchors Create: path " + T1_PATH)));
|
|
1105
|
+
for (const list of ["other.test.ts", `${T1_PATH}, other.test.ts`]) {
|
|
1106
|
+
assert.ok(baselineFindings(created.replace(BASELINE, `- baseline: abc1234 skipped - ${list} created by this task`)).length);
|
|
1107
|
+
}
|
|
1108
|
+
const compound = created.replace(`- \`${T1_COMMAND}\``, `- \`${T1_COMMAND} && node --test extensions/lib/fixture-task2.test.ts\``)
|
|
1109
|
+
.replace(`- Test: ${T1_PATH}`, `- Test: ${T1_PATH}\n- Test: extensions/lib/fixture-task2.test.ts`);
|
|
1110
|
+
assert.deepEqual(baselineFindings(compound.replace(BASELINE, BASELINE + " via `node --test extensions/lib/fixture-task2.test.ts`")), []);
|
|
1111
|
+
assert.ok(baselineFindings(compound.replace(BASELINE, skipped)).length);
|
|
1112
|
+
const allCreated = compound.replace("- Create: extensions/lib/fixture-task1.ts", "- Create: extensions/lib/fixture-task2.test.ts")
|
|
1113
|
+
.replace("- Create: extensions/lib/fixture-task2.ts", "- Create: extensions/lib/fixture-task2.test.ts");
|
|
1114
|
+
const bothSkipped = `- baseline: abc1234 skipped - ${T1_PATH}, extensions/lib/fixture-task2.test.ts created by this task`;
|
|
1115
|
+
assert.deepEqual(baselineFindings(allCreated.replace(BASELINE, bothSkipped)), []);
|
|
1116
|
+
assert.ok(baselineFindings(allCreated.replace(BASELINE, skipped)).length);
|
|
1117
|
+
const none = VALID_PLAN.replace(`- \`${T1_COMMAND}\``, "- none: docs");
|
|
1118
|
+
assert.ok(baselineFindings(none).some(f => f.reason === "Task 1: baseline: under a none: task"));
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
const FAILURE_ROW = `- Task 1: \`${T1_COMMAND}\` fails on abc1234 (exit 2): assertion failed`;
|
|
1122
|
+
for (const [name, body, count, malformed] of [
|
|
1123
|
+
["absent", "", 0, false],
|
|
1124
|
+
["sentinel", "- none", 0, false],
|
|
1125
|
+
["failure", FAILURE_ROW, 1, false],
|
|
1126
|
+
["global then failure", "- Baseline: worktree dirty on abc1234: source.ts\n" + FAILURE_ROW, 2, false],
|
|
1127
|
+
["contract", "- Task 1: unresolved caller contract", 1, false],
|
|
1128
|
+
["prose", "Not resolved", 1, true],
|
|
1129
|
+
["table", "| question | owner |", 1, true],
|
|
1130
|
+
["continuations", FAILURE_ROW + "\n ran: `node --test original.ts`\n blocked: Task 2 `node --test next.ts`\n ```text\n TODO etc.\n ```", 1, false],
|
|
1131
|
+
] as const) {
|
|
1132
|
+
test(`open-questions ${name}`, () => {
|
|
1133
|
+
const plan = VALID_PLAN + (body ? "\n## Open Questions\n\n" + body + "\n" : "");
|
|
1134
|
+
const findings = checkPlan(plan, SPEC_TEXT, alwaysTruePort());
|
|
1135
|
+
const questions = findingsFor(findings, "open-questions");
|
|
1136
|
+
assert.equal(questions.length, count);
|
|
1137
|
+
if (malformed) assert.equal(questions[0].reason, "## Open Questions malformed: expected `- ` rows or `- none`");
|
|
1138
|
+
if (body.includes(FAILURE_ROW)) assert.ok(questions.some(f => f.reason === "plan has open questions: " + FAILURE_ROW));
|
|
1139
|
+
assert.deepEqual(findingsFor(findings, "placeholder-scan"), []);
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
test("placeholder-scan masks only valid question output, not authored fences or continuations", () => {
|
|
1144
|
+
for (const text of ["```\nTODO\n```", "## Open Questions\n ```\n TODO\n ```", "## Open Questions\n- TODO unresolved", "## Open Questions\n" + FAILURE_ROW + "\n ```\n TODO", "## Open Questions\n" + FAILURE_ROW + "\n ```\nTODO\n ```", "## Open Questions\n" + FAILURE_ROW + "\n ran: `TODO`", "## Open Questions\n" + FAILURE_ROW + "\n blocked: TODO"]) {
|
|
1145
|
+
assert.ok(findingsFor(checkPlan(VALID_PLAN + "\n" + text, SPEC_TEXT, alwaysTruePort()), "placeholder-scan").length);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1053
1149
|
test("tests-block: fenced **Tests:** lines are ignored", () => {
|
|
1054
1150
|
const mutated = VALID_PLAN.replace(
|
|
1055
1151
|
"This task handles naming details.",
|
|
@@ -58,6 +58,7 @@ interface Task {
|
|
|
58
58
|
testsMisplacedLines: number[];
|
|
59
59
|
tests: TestsBullet[];
|
|
60
60
|
testsVia: TestsBullet[];
|
|
61
|
+
testsBaseline: TestsBullet[];
|
|
61
62
|
testsNone: TestsBullet[];
|
|
62
63
|
testsMalformed: TestsBullet[];
|
|
63
64
|
}
|
|
@@ -67,8 +68,6 @@ interface Wave {
|
|
|
67
68
|
line: number;
|
|
68
69
|
text: string;
|
|
69
70
|
label: string;
|
|
70
|
-
soloLine: number | undefined;
|
|
71
|
-
soloReason: string | undefined;
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
interface CoverageRow {
|
|
@@ -103,6 +102,7 @@ interface ParsedPlan {
|
|
|
103
102
|
tasks: Task[];
|
|
104
103
|
coverageTableFound: boolean;
|
|
105
104
|
coverageRows: CoverageRow[];
|
|
105
|
+
openQuestions: { findings: PlanCheckFinding[]; output: Set<number> };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
interface SpecHeading {
|
|
@@ -188,18 +188,7 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
188
188
|
if (mask[i]) continue;
|
|
189
189
|
const m = waveRe.exec(lines[i]);
|
|
190
190
|
if (!m) continue;
|
|
191
|
-
|
|
192
|
-
let soloReason: string | undefined;
|
|
193
|
-
let j = i + 1;
|
|
194
|
-
while (j < lines.length && lines[j].trim() === "") j++;
|
|
195
|
-
if (j < lines.length) {
|
|
196
|
-
const sm = /^Solo: (.+)$/.exec(lines[j]);
|
|
197
|
-
if (sm) {
|
|
198
|
-
soloLine = j + 1;
|
|
199
|
-
soloReason = sm[1];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
waves.push({ number: Number(m[1]), line: i + 1, text: lines[i], label: m[3], soloLine, soloReason });
|
|
191
|
+
waves.push({ number: Number(m[1]), line: i + 1, text: lines[i], label: m[3] });
|
|
203
192
|
}
|
|
204
193
|
|
|
205
194
|
const taskHeaderIdxs: number[] = [];
|
|
@@ -263,6 +252,7 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
263
252
|
const testsMisplacedLines: number[] = [];
|
|
264
253
|
const tests: TestsBullet[] = [];
|
|
265
254
|
const testsVia: TestsBullet[] = [];
|
|
255
|
+
const testsBaseline: TestsBullet[] = [];
|
|
266
256
|
const testsNone: TestsBullet[] = [];
|
|
267
257
|
const testsMalformed: TestsBullet[] = [];
|
|
268
258
|
|
|
@@ -295,10 +285,12 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
295
285
|
const cmd = /^- `([^`]+)`$/.exec(l);
|
|
296
286
|
const via = /^- via: (\S.*)$/.exec(l);
|
|
297
287
|
const none = /^- none: (\S.*)$/.exec(l);
|
|
298
|
-
const
|
|
288
|
+
const baseline = /^- baseline: (\S.*)$/.exec(l);
|
|
289
|
+
const bullet = { line: p + 1, text: l, value: (cmd ?? via ?? none ?? baseline)?.[1] ?? "" };
|
|
299
290
|
if (cmd) tests.push(bullet);
|
|
300
291
|
else if (via) testsVia.push(bullet);
|
|
301
292
|
else if (none) testsNone.push(bullet);
|
|
293
|
+
else if (baseline) testsBaseline.push(bullet);
|
|
302
294
|
else testsMalformed.push(bullet);
|
|
303
295
|
}
|
|
304
296
|
}
|
|
@@ -319,6 +311,7 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
319
311
|
testsMisplacedLines,
|
|
320
312
|
tests,
|
|
321
313
|
testsVia,
|
|
314
|
+
testsBaseline,
|
|
322
315
|
testsNone,
|
|
323
316
|
testsMalformed,
|
|
324
317
|
});
|
|
@@ -380,8 +373,59 @@ function parsePlan(planText: string): ParsedPlan {
|
|
|
380
373
|
break;
|
|
381
374
|
}
|
|
382
375
|
|
|
376
|
+
const questionFindings: PlanCheckFinding[] = [];
|
|
377
|
+
const questionOutput = new Set<number>();
|
|
378
|
+
let inSection = false;
|
|
379
|
+
let row = false;
|
|
380
|
+
let inOutput = false;
|
|
381
|
+
let sentinel: number | undefined;
|
|
382
|
+
let contentCount = 0;
|
|
383
|
+
const malformed = (idx: number) => questionFindings.push({ check: "open-questions", line: idx + 1, text: lines[idx], reason: "## Open Questions malformed: expected `- ` rows or `- none`" });
|
|
384
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
385
|
+
const line = lines[idx];
|
|
386
|
+
if (!mask[idx] && /^## /.test(line)) {
|
|
387
|
+
if (inSection && sentinel !== undefined && contentCount !== 1) malformed(sentinel);
|
|
388
|
+
inSection = line === "## Open Questions";
|
|
389
|
+
row = false;
|
|
390
|
+
inOutput = false;
|
|
391
|
+
sentinel = undefined;
|
|
392
|
+
contentCount = 0;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!inSection) continue;
|
|
396
|
+
if (inOutput) {
|
|
397
|
+
questionOutput.add(idx);
|
|
398
|
+
if (/^\s+```/.test(line)) inOutput = false;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!line.trim()) continue;
|
|
402
|
+
contentCount++;
|
|
403
|
+
if (/^- \S/.test(line)) {
|
|
404
|
+
row = line !== "- none";
|
|
405
|
+
if (row) questionFindings.push({ check: "open-questions", line: idx + 1, text: line, reason: `plan has open questions: ${line}` });
|
|
406
|
+
else sentinel = idx;
|
|
407
|
+
} else if (row && /^\s+\S/.test(line)) {
|
|
408
|
+
if (/^\s+```/.test(line)) {
|
|
409
|
+
let end = idx + 1;
|
|
410
|
+
while (end < lines.length && !/^\s+```/.test(lines[end])) {
|
|
411
|
+
if (lines[end].trim() && !/^\s+/.test(lines[end])) break;
|
|
412
|
+
end++;
|
|
413
|
+
}
|
|
414
|
+
if (end < lines.length && /^\s+```/.test(lines[end])) {
|
|
415
|
+
inOutput = true;
|
|
416
|
+
questionOutput.add(idx);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
malformed(idx);
|
|
421
|
+
row = false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (inSection && sentinel !== undefined && contentCount !== 1) malformed(sentinel);
|
|
425
|
+
|
|
383
426
|
return {
|
|
384
427
|
lines,
|
|
428
|
+
openQuestions: { findings: questionFindings, output: questionOutput },
|
|
385
429
|
header: { specPathLine, specPath, verificationLine, verificationText, happyPathLine, happyPathText, happyPathCommand, separatorLine },
|
|
386
430
|
waves,
|
|
387
431
|
tasks,
|
|
@@ -487,7 +531,7 @@ function checkTestsBlock(parsed: ParsedPlan, fs: FsPort): PlanCheckFinding[] {
|
|
|
487
531
|
|
|
488
532
|
const headingText = parsed.lines[task.testsHeadingLine - 1];
|
|
489
533
|
if (task.tests.length === 0 && task.testsNone.length === 0) push(task, task.testsHeadingLine, headingText, "block empty: no command bullet and no `none:`");
|
|
490
|
-
for (const b of task.testsMalformed) push(task, b.line, b.text, "malformed bullet: expected `- \\`command\\``, `- via: <seam>`, or `- none: <category>`");
|
|
534
|
+
for (const b of task.testsMalformed) push(task, b.line, b.text, "malformed bullet: expected `- \\`command\\``, `- baseline: <evidence>`, `- via: <seam>`, or `- none: <category>`");
|
|
491
535
|
if (task.testsNone.length > 1 || (task.testsNone.length > 0 && (task.tests.length > 0 || task.testsVia.length > 0))) {
|
|
492
536
|
push(task, task.testsNone[0].line, task.testsNone[0].text, "contradictory block: `none:` with a command or `via:`, or more than one `none:`");
|
|
493
537
|
}
|
|
@@ -515,6 +559,55 @@ function checkTestsBlock(parsed: ParsedPlan, fs: FsPort): PlanCheckFinding[] {
|
|
|
515
559
|
return findings;
|
|
516
560
|
}
|
|
517
561
|
|
|
562
|
+
function checkBaselineLines(parsed: ParsedPlan, fs: FsPort): PlanCheckFinding[] {
|
|
563
|
+
const findings: PlanCheckFinding[] = [];
|
|
564
|
+
const shas = new Set<string>();
|
|
565
|
+
const creates = new Set(parsed.tasks.flatMap(t => t.files.filter(f => f.kind === "create").map(f => stripLineSuffix(f.path))));
|
|
566
|
+
for (const task of parsed.tasks) {
|
|
567
|
+
const push = (bullet: TestsBullet, reason: string) => findings.push({ check: "baseline-line", line: bullet.line, text: bullet.text, reason: `Task ${task.number}: ${reason}` });
|
|
568
|
+
const paths = task.files.filter(f => f.kind === "test").map(f => stripLineSuffix(f.path)).filter(p => !isBroadening(p));
|
|
569
|
+
const anchors = (command: string) => commandSegments(command).flatMap(segment => segment.split(" ").flatMap(token => paths.filter(p => token === p || token.startsWith(p + "::") || token.startsWith(p + "#") || token.startsWith(p + ":"))));
|
|
570
|
+
for (const command of task.tests) {
|
|
571
|
+
if (!task.testsBaseline.some(b => b.line === command.line + 1)) push(command, `command \`${command.value}\` has no baseline: bullet`);
|
|
572
|
+
}
|
|
573
|
+
for (const bullet of task.testsBaseline) {
|
|
574
|
+
const command = task.tests.find(c => c.line + 1 === bullet.line);
|
|
575
|
+
if (task.testsNone.length) push(bullet, "baseline: under a none: task");
|
|
576
|
+
else if (!command) push(bullet, "baseline: not directly after its command");
|
|
577
|
+
const match = /^([a-f0-9]{7,40}) (?:pass(?: via `([^`]+)`)?(?: - artifacts: (\S.*))?|skipped - (\S.*) created by this task)$/.exec(bullet.value);
|
|
578
|
+
if (!match || (match[2] !== undefined && !match[2].trim()) || (match[3] !== undefined && !match[3].split(", ").every(p => p.trim().length > 0))) {
|
|
579
|
+
push(bullet, `baseline: bullet malformed: ${bullet.text}`);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
shas.add(match[1]);
|
|
583
|
+
if (match[4] !== undefined && command) {
|
|
584
|
+
const listed = match[4].split(", ");
|
|
585
|
+
const expected = new Set(anchors(command.value));
|
|
586
|
+
for (const path of new Set([...expected, ...listed])) {
|
|
587
|
+
if (!creates.has(path)) push(bullet, `baseline: skipped but \`${path}\` is not a Create: path`);
|
|
588
|
+
else if (!expected.has(path) || !listed.includes(path)) push(bullet, `baseline: bullet malformed: ${bullet.text}`);
|
|
589
|
+
}
|
|
590
|
+
if (!expected.size || listed.length !== new Set(listed).size) push(bullet, `baseline: bullet malformed: ${bullet.text}`);
|
|
591
|
+
}
|
|
592
|
+
if (match[2] !== undefined) {
|
|
593
|
+
const segments = commandSegments(match[2]);
|
|
594
|
+
if (!segments.length) {
|
|
595
|
+
push(bullet, `baseline: bullet malformed: ${bullet.text}`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
for (const segment of segments) {
|
|
599
|
+
const found = anchors(segment);
|
|
600
|
+
const created = [...creates].filter(p => segment.split(" ").some(token => token === p || token.startsWith(p + "::") || token.startsWith(p + "#") || token.startsWith(p + ":")));
|
|
601
|
+
for (const path of created) push(bullet, `baseline: pass via \`${match[2]}\` segment \`${segment}\` anchors Create: path ${path}`);
|
|
602
|
+
if (!found.some(p => !creates.has(p) && fs.exists(p))) push(bullet, `baseline: pass via \`${match[2]}\` segment \`${segment}\` anchors no existing Test: path`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (shas.size > 1) findings.push({ check: "baseline-line", line: 0, text: "", reason: `baseline SHAs differ: ${[...shas].join(", ")}` });
|
|
608
|
+
return findings;
|
|
609
|
+
}
|
|
610
|
+
|
|
518
611
|
function taskBodyText(task: Task, lines: string[]): string {
|
|
519
612
|
return lines.slice(task.bodyStartLine - 1, task.bodyEndLine).join("\n");
|
|
520
613
|
}
|
|
@@ -822,6 +915,7 @@ function checkPlaceholderScan(parsed: ParsedPlan, requiredLiterals: Map<number,
|
|
|
822
915
|
for (let ln = t.bodyStartLine; ln <= t.bodyEndLine; ln++) lineTask.set(ln, t);
|
|
823
916
|
}
|
|
824
917
|
parsed.lines.forEach((line, idx) => {
|
|
918
|
+
if (parsed.openQuestions.output.has(idx)) return;
|
|
825
919
|
const lineNo = idx + 1;
|
|
826
920
|
const lower = line.toLowerCase();
|
|
827
921
|
const task = lineTask.get(lineNo);
|
|
@@ -944,23 +1038,6 @@ function globErrFinding(wave: Wave, pattern: string, err: Error): PlanCheckFindi
|
|
|
944
1038
|
};
|
|
945
1039
|
}
|
|
946
1040
|
|
|
947
|
-
function checkSoloLine(parsed: ParsedPlan): PlanCheckFinding[] {
|
|
948
|
-
const findings: PlanCheckFinding[] = [];
|
|
949
|
-
for (const wave of parsed.waves) {
|
|
950
|
-
const count = parsed.tasks.filter((t) => t.waveNumber === wave.number).length;
|
|
951
|
-
if (count !== 1) continue;
|
|
952
|
-
if (wave.soloLine === undefined || !wave.soloReason || wave.soloReason.trim().length === 0) {
|
|
953
|
-
findings.push({
|
|
954
|
-
check: "solo-line",
|
|
955
|
-
line: wave.line,
|
|
956
|
-
text: wave.text,
|
|
957
|
-
reason: `single-task Wave ${wave.number} is missing a 'Solo: <reason>' line directly under its header`,
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return findings;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
1041
|
function checkHappyPathLine(parsed: ParsedPlan): PlanCheckFinding[] {
|
|
965
1042
|
const { happyPathLine, happyPathText, happyPathCommand } = parsed.header;
|
|
966
1043
|
if (happyPathLine === undefined || happyPathCommand !== undefined) return [];
|
|
@@ -992,7 +1069,7 @@ function checkHeaderEntrypoint(parsed: ParsedPlan): PlanCheckFinding[] {
|
|
|
992
1069
|
const entrypoints = [entrypoint, parsed.header.happyPathCommand].filter((e): e is string => Boolean(e));
|
|
993
1070
|
const executable = new Set<number>();
|
|
994
1071
|
for (const task of parsed.tasks) {
|
|
995
|
-
for (const bullet of [...task.tests, ...task.testsVia, ...task.testsNone, ...task.testsMalformed]) {
|
|
1072
|
+
for (const bullet of [...task.tests, ...task.testsVia, ...task.testsBaseline, ...task.testsNone, ...task.testsMalformed]) {
|
|
996
1073
|
executable.add(bullet.line);
|
|
997
1074
|
}
|
|
998
1075
|
}
|
|
@@ -1087,10 +1164,11 @@ export function checkPlan(planText: string, specText: string, fs: FsPort): PlanC
|
|
|
1087
1164
|
findings.push(...checkAnchorResolution(parsed, specLines));
|
|
1088
1165
|
findings.push(...checkPathsExist(parsed, fs));
|
|
1089
1166
|
findings.push(...checkTestsBlock(parsed, fs));
|
|
1167
|
+
findings.push(...checkBaselineLines(parsed, fs));
|
|
1168
|
+
findings.push(...parsed.openQuestions.findings);
|
|
1090
1169
|
const requiredLiterals = computeRequiredLiteralsPerTask(parsed, specLines);
|
|
1091
1170
|
findings.push(...checkPlaceholderScan(parsed, requiredLiterals));
|
|
1092
1171
|
findings.push(...checkWaveFileDisjointness(parsed, fs));
|
|
1093
|
-
findings.push(...checkSoloLine(parsed));
|
|
1094
1172
|
findings.push(...checkHeaderEntrypoint(parsed));
|
|
1095
1173
|
findings.push(...checkHappyPathLine(parsed));
|
|
1096
1174
|
findings.push(...checkWaiverLiteral(parsed));
|