mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 02:30:39 +01:00
Make autoplan's rec and all replies apply every recommended plan change (GST-2139) (#4225)
* Make autoplan's rec and all replies apply every recommended plan change (GST-2139) The Phase 4 reply table sent `all` to "approve as written" while gstack-gate parse takes every recommendation, plan changes included, and it let `p1a` approve with a user challenge still pending. The table now matches the gate: `rec`/`all` is the one-word default and routes plan changes through B/B2, a pending item approves nothing until the owner answers it, and p1 recommends b when a recommended option changes the plan. A new test reads the table and checks it against bin/gstack-gate parse for all, rec and p1a. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Review-Finding: p1/red-team#1 reader=red-team Review-Finding: p1/red-team#2 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Rename the changelog fragment to the branch's fragment name Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
e9de3a9d8b
commit
cfedd05bb9
5 files changed
+147
-42
No files matched your search
+19
-15
@@ -625,8 +625,10 @@ in an unattended run, never a warning; interactive runs follow the repair rule a
|
||||
Write `$_AP/gate.json` (shape: `gstack-gate --help`): one item per User Challenge
|
||||
(`uc<n>`, approval; `why` = you said / both recommend / why / blind spots / cost if
|
||||
wrong), per taste decision (`d<n>`, auto; `why` = principle and the alternative's
|
||||
impact) and `p1` (approval: a) approve as-is (recommended) b) with overrides
|
||||
c) interrogate d) revise e) reject). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json`
|
||||
impact) and `p1` (approval: a) approve as-is b) take the recommended changes c) interrogate
|
||||
d) revise e) reject), recommending `b` if a recommended option changes the plan, else `a`.
|
||||
Every `uc<n>`/`d<n>` option sets `changes_plan` (`true` if it changes the plan,
|
||||
else `false`). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json`
|
||||
and append its `decisions` output to `$_AP/decisions.jsonl`. Write the full review
|
||||
to `$_AP/review-record.md` and its Plan Summary alone to `$_AP/summary.md`:
|
||||
|
||||
@@ -641,7 +643,7 @@ findings outside this plan, or `None.`); also written above `## Implementation p
|
||||
[1-3 sentence summary]
|
||||
|
||||
### Decisions Made: [N] total ([M] auto-decided, [K] taste choices, [J] user challenges)
|
||||
Auto-decided: see the plan file's Decision Audit Trail; the rest are the brief's decisions.
|
||||
Auto-decided: see the plan's Decision Audit Trail.
|
||||
|
||||
### Review Scores
|
||||
CEO, Design, DX and Eng: phase summary plus Codex, Claude
|
||||
@@ -649,7 +651,7 @@ and consensus status; say skipped where a phase did not run. Then the CYCLE line
|
||||
from `~/.claude/skills/gstack/bin/gstack-autoplan-timing summary --run <RUN_ID> --out "$_AP"`.
|
||||
|
||||
### Cross-Phase Themes
|
||||
Concerns independently raised in 2+ phases, or "No cross-phase themes — each phase's concerns were distinct."
|
||||
Concerns raised independently in 2+ phases, or "None."
|
||||
|
||||
### Deferred to TODOS.md
|
||||
[Items auto-deferred with reasons]
|
||||
@@ -659,21 +661,23 @@ Concerns independently raised in 2+ phases, or "No cross-phase themes — each p
|
||||
```
|
||||
|
||||
Present `~/.claude/skills/gstack/bin/gstack-owner-brief render --out "$_AP" --write`
|
||||
verbatim (URGENT block, plain summary, numbered decisions seven per page with each
|
||||
auto default, review-record path; `--page N` for later pages), never the full review.
|
||||
verbatim (`--page N` for later pages), never the full review.
|
||||
|
||||
Parse the reply: `~/.claude/skills/gstack/bin/gstack-gate parse $_AP/gate.json --reply "<text>" --gate-rev <rev>`;
|
||||
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `p1a`
|
||||
without overrides = A; `d<n>` overrides = B; `uc<n>` overrides = B2; `p1c`/`p1d`/`p1e`
|
||||
= C/D/E. Under `SESSION_KIND: unattended` Read `sections/unattended.md`, end here
|
||||
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `rec` (alias
|
||||
`all`), the one-word default, takes every recommendation, plan changes included; `as-is` keeps
|
||||
the plan as first written. First match: any `pending` id = approve nothing (bare `p1a` leaves
|
||||
`uc<n>` pending; re-parse the owner's whole reply);
|
||||
`p1c`/`p1d`/`p1e` = C/D/E; a chosen `changes_plan` option (or an unflagged override)
|
||||
= B on a `d<n>`, B2 on a `uc<n>`; else = A. Under `SESSION_KIND: unattended` Read `sections/unattended.md`, end here
|
||||
with its `GSTACK_RESULT` line and never approve.
|
||||
|
||||
**Option handling:**
|
||||
- A: mark APPROVED, run `~/.claude/skills/gstack/bin/gstack-owner-brief close --out "$_AP" --plan <ACTIVE_PLAN>` (draft-direction lines become the decided option and date), write review logs, suggest /ship
|
||||
- B: ask which overrides, apply, then follow D's affected-phase rerun rule (Eng last) before re-presenting the gate; same 3-cycle cap as D.
|
||||
- B2: accept/reject User Challenges one at a time; rejected ones preserve the user's direction. Re-run Eng, then re-present the gate.
|
||||
- A: mark APPROVED, run `~/.claude/skills/gstack/bin/gstack-owner-brief close --out "$_AP" --plan <ACTIVE_PLAN>`, write review logs, suggest /ship
|
||||
- B: apply each chosen change, rerun as in D before re-presenting the gate; same 3-cycle cap as D.
|
||||
- B2: apply each accepted User Challenge (rejected ones keep the user's direction), re-run Eng, re-present the gate.
|
||||
- C: answer freeform, re-present the gate
|
||||
- D: make changes, re-run affected phases (scope→1, design→2, dx→2.5, test plan→3, arch→3; a re-run of any earlier phase re-runs Eng after it — the gate always reviews the final plan). Max 3 cycles.
|
||||
- D: make changes, re-run affected phases (scope→1, design→2, dx→2.5, test plan→3, arch→3), then Eng; max 3 cycles.
|
||||
- E: start over
|
||||
|
||||
**Starting an affected-phase rerun:** Keep the current Implementation plan and all
|
||||
@@ -681,9 +685,9 @@ prior accepted obligations intact. Move that phase's applied `autoplan-baseline-
|
||||
record verbatim into fenced history in Review record, keeping its original source SHA.
|
||||
Create a fresh amendment checkpoint. New baseline edits use `create`'s
|
||||
`baselineEdits.record` and `sourceSha256`; the review projection hash is not baseline
|
||||
identity. Carry forward unchanged accepted requirements. Never replay old replacements
|
||||
identity. Never replay old replacements
|
||||
or rewrite a historical source SHA. This is a new phase invocation; compaction
|
||||
resumes the existing one and its checkpoint. Eng still runs last.
|
||||
resumes the existing one and its checkpoint.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+19
-15
@@ -360,8 +360,10 @@ there with each incomplete item.
|
||||
Write `$_AP/gate.json` (shape: `gstack-gate --help`): one item per User Challenge
|
||||
(`uc<n>`, approval; `why` = you said / both recommend / why / blind spots / cost if
|
||||
wrong), per taste decision (`d<n>`, auto; `why` = principle and the alternative's
|
||||
impact) and `p1` (approval: a) approve as-is (recommended) b) with overrides
|
||||
c) interrogate d) revise e) reject). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json`
|
||||
impact) and `p1` (approval: a) approve as-is b) take the recommended changes c) interrogate
|
||||
d) revise e) reject), recommending `b` if a recommended option changes the plan, else `a`.
|
||||
Every `uc<n>`/`d<n>` option sets `changes_plan` (`true` if it changes the plan,
|
||||
else `false`). Run `~/.claude/skills/gstack/bin/gstack-gate render $_AP/gate.json`
|
||||
and append its `decisions` output to `$_AP/decisions.jsonl`. Write the full review
|
||||
to `$_AP/review-record.md` and its Plan Summary alone to `$_AP/summary.md`:
|
||||
|
||||
@@ -376,7 +378,7 @@ findings outside this plan, or `None.`); also written above `## Implementation p
|
||||
[1-3 sentence summary]
|
||||
|
||||
### Decisions Made: [N] total ([M] auto-decided, [K] taste choices, [J] user challenges)
|
||||
Auto-decided: see the plan file's Decision Audit Trail; the rest are the brief's decisions.
|
||||
Auto-decided: see the plan's Decision Audit Trail.
|
||||
|
||||
### Review Scores
|
||||
CEO, Design, DX and Eng: phase summary plus {{OUTSIDE_LABEL}}, {{NATIVE_LABEL}}
|
||||
@@ -384,7 +386,7 @@ and consensus status; say skipped where a phase did not run. Then the CYCLE line
|
||||
from `~/.claude/skills/gstack/bin/gstack-autoplan-timing summary --run <RUN_ID> --out "$_AP"`.
|
||||
|
||||
### Cross-Phase Themes
|
||||
Concerns independently raised in 2+ phases, or "No cross-phase themes — each phase's concerns were distinct."
|
||||
Concerns raised independently in 2+ phases, or "None."
|
||||
|
||||
### Deferred to TODOS.md
|
||||
[Items auto-deferred with reasons]
|
||||
@@ -394,21 +396,23 @@ Concerns independently raised in 2+ phases, or "No cross-phase themes — each p
|
||||
```
|
||||
|
||||
Present `~/.claude/skills/gstack/bin/gstack-owner-brief render --out "$_AP" --write`
|
||||
verbatim (URGENT block, plain summary, numbered decisions seven per page with each
|
||||
auto default, review-record path; `--page N` for later pages), never the full review.
|
||||
verbatim (`--page N` for later pages), never the full review.
|
||||
|
||||
Parse the reply: `~/.claude/skills/gstack/bin/gstack-gate parse $_AP/gate.json --reply "<text>" --gate-rev <rev>`;
|
||||
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `all` or `p1a`
|
||||
without overrides = A; `d<n>` overrides = B; `uc<n>` overrides = B2; `p1c`/`p1d`/`p1e`
|
||||
= C/D/E. Under `SESSION_KIND: unattended` Read `sections/unattended.md`, end here
|
||||
unparsed or stale replies are re-presented (`gate_rev`+1), never guessed. `rec` (alias
|
||||
`all`), the one-word default, takes every recommendation, plan changes included; `as-is` keeps
|
||||
the plan as first written. First match: any `pending` id = approve nothing (bare `p1a` leaves
|
||||
`uc<n>` pending; re-parse the owner's whole reply);
|
||||
`p1c`/`p1d`/`p1e` = C/D/E; a chosen `changes_plan` option (or an unflagged override)
|
||||
= B on a `d<n>`, B2 on a `uc<n>`; else = A. Under `SESSION_KIND: unattended` Read `sections/unattended.md`, end here
|
||||
with its `GSTACK_RESULT` line and never approve.
|
||||
|
||||
**Option handling:**
|
||||
- A: mark APPROVED, run `~/.claude/skills/gstack/bin/gstack-owner-brief close --out "$_AP" --plan <ACTIVE_PLAN>` (draft-direction lines become the decided option and date), write review logs, suggest /ship
|
||||
- B: ask which overrides, apply, then follow D's affected-phase rerun rule (Eng last) before re-presenting the gate; same 3-cycle cap as D.
|
||||
- B2: accept/reject User Challenges one at a time; rejected ones preserve the user's direction. Re-run Eng, then re-present the gate.
|
||||
- A: mark APPROVED, run `~/.claude/skills/gstack/bin/gstack-owner-brief close --out "$_AP" --plan <ACTIVE_PLAN>`, write review logs, suggest /ship
|
||||
- B: apply each chosen change, rerun as in D before re-presenting the gate; same 3-cycle cap as D.
|
||||
- B2: apply each accepted User Challenge (rejected ones keep the user's direction), re-run Eng, re-present the gate.
|
||||
- C: answer freeform, re-present the gate
|
||||
- D: make changes, re-run affected phases (scope→1, design→2, dx→2.5, test plan→3, arch→3; a re-run of any earlier phase re-runs Eng after it — the gate always reviews the final plan). Max 3 cycles.
|
||||
- D: make changes, re-run affected phases (scope→1, design→2, dx→2.5, test plan→3, arch→3), then Eng; max 3 cycles.
|
||||
- E: start over
|
||||
|
||||
**Starting an affected-phase rerun:** Keep the current Implementation plan and all
|
||||
@@ -416,9 +420,9 @@ prior accepted obligations intact. Move that phase's applied `autoplan-baseline-
|
||||
record verbatim into fenced history in Review record, keeping its original source SHA.
|
||||
Create a fresh amendment checkpoint. New baseline edits use `create`'s
|
||||
`baselineEdits.record` and `sourceSha256`; the review projection hash is not baseline
|
||||
identity. Carry forward unchanged accepted requirements. Never replay old replacements
|
||||
identity. Never replay old replacements
|
||||
or rewrite a historical source SHA. This is a new phase invocation; compaction
|
||||
resumes the existing one and its checkpoint. Eng still runs last.
|
||||
resumes the existing one and its checkpoint.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **/autoplan's `rec` and `all` replies now apply every recommended plan change, as `gstack-gate` already did.** The skill's reply table sent `all` (and left `rec` unmapped) straight to "approve as written", while `gstack-gate parse` resolves both to every item's recommendation, plan changes included, so decisions.jsonl and the approved plan could disagree. `rec` is now the one-word default that takes those changes through the affected-phase rerun before the gate comes back; `as-is` stays the plan as first written. The plan-approval item now recommends `b` (take the recommended changes) whenever a recommended option changes the plan. A bare `p1a` with a user challenge unanswered no longer approves anything: the gate waits for the owner's own answer to that `uc` item and re-parses their whole reply. Every `uc`/`d` option in a hand-written gate now sets `changes_plan`, and an explicit override of an option with no flag still counts as a plan change, as `gstack-autoplan answer` already treats it. A test reads the skill's reply table and checks it against what `gstack-gate parse` returns for `all`, `rec`, `p1a` and an override.
|
||||
+12
-12
@@ -2,53 +2,53 @@
|
||||
"_comment": "Generated SKILL.md files over the limit, pinned at their exact character count (#3199). test/skill-md-size-guard.test.ts fails when a pinned skill grows past its pin or shrinks below it: lower the pin when a skill shrinks and delete it once the skill is within the limit. Raising a pin is an owner decision. The shared-preamble hoist (GSTA-96) moved every tier 2+ skill's shared preamble into sections/preamble.md; the pins below are what remains.",
|
||||
"limit": 100000,
|
||||
"pins": {
|
||||
"agy/gstack-autoplan": 109575,
|
||||
"agy/gstack-autoplan": 109540,
|
||||
"agy/gstack-plan-design-review": 109096,
|
||||
"agy/gstack-plan-devex-review": 114924,
|
||||
"agy/gstack-plan-eng-review": 143580,
|
||||
"agy/gstack-review": 104735,
|
||||
"codex/gstack-autoplan": 108318,
|
||||
"codex/gstack-autoplan": 108283,
|
||||
"codex/gstack-plan-design-review": 110453,
|
||||
"codex/gstack-plan-devex-review": 116432,
|
||||
"codex/gstack-plan-eng-review": 144745,
|
||||
"codex/gstack-review": 104119,
|
||||
"copilot/gstack-autoplan": 109018,
|
||||
"copilot/gstack-autoplan": 108983,
|
||||
"copilot/gstack-plan-design-review": 109362,
|
||||
"copilot/gstack-plan-devex-review": 114386,
|
||||
"copilot/gstack-plan-eng-review": 143745,
|
||||
"copilot/gstack-review": 104316,
|
||||
"cursor/gstack-autoplan": 108250,
|
||||
"cursor/gstack-autoplan": 108215,
|
||||
"cursor/gstack-plan-design-review": 107904,
|
||||
"cursor/gstack-plan-devex-review": 113656,
|
||||
"cursor/gstack-plan-eng-review": 142335,
|
||||
"cursor/gstack-review": 122788,
|
||||
"factory/gstack-autoplan": 110528,
|
||||
"factory/gstack-autoplan": 110493,
|
||||
"factory/gstack-plan-design-review": 111002,
|
||||
"factory/gstack-plan-devex-review": 117019,
|
||||
"factory/gstack-plan-eng-review": 145443,
|
||||
"factory/gstack-review": 126561,
|
||||
"gbrain/gstack-autoplan": 108657,
|
||||
"gbrain/gstack-autoplan": 108622,
|
||||
"gbrain/gstack-plan-eng-review": 124963,
|
||||
"gemini/gstack-autoplan": 108808,
|
||||
"gemini/gstack-autoplan": 108773,
|
||||
"gemini/gstack-plan-design-review": 109190,
|
||||
"gemini/gstack-plan-devex-review": 114214,
|
||||
"gemini/gstack-plan-eng-review": 143578,
|
||||
"gemini/gstack-review": 104138,
|
||||
"hermes/gstack-autoplan": 108611,
|
||||
"hermes/gstack-autoplan": 108576,
|
||||
"hermes/gstack-plan-eng-review": 124999,
|
||||
"kiro/gstack-autoplan": 108024,
|
||||
"kiro/gstack-autoplan": 107989,
|
||||
"kiro/gstack-plan-design-review": 107750,
|
||||
"kiro/gstack-plan-devex-review": 113508,
|
||||
"kiro/gstack-plan-eng-review": 142197,
|
||||
"kiro/gstack-review": 122612,
|
||||
"openclaw/gstack-autoplan": 108480,
|
||||
"openclaw/gstack-autoplan": 108445,
|
||||
"openclaw/gstack-plan-eng-review": 123478,
|
||||
"opencode/gstack-autoplan": 108999,
|
||||
"opencode/gstack-autoplan": 108964,
|
||||
"opencode/gstack-plan-design-review": 109218,
|
||||
"opencode/gstack-plan-devex-review": 114264,
|
||||
"opencode/gstack-plan-eng-review": 143604,
|
||||
"opencode/gstack-review": 123410,
|
||||
"slate/gstack-autoplan": 108137,
|
||||
"slate/gstack-autoplan": 108102,
|
||||
"slate/gstack-plan-design-review": 107827,
|
||||
"slate/gstack-plan-devex-review": 113582,
|
||||
"slate/gstack-plan-eng-review": 142266,
|
||||
|
||||
@@ -279,3 +279,99 @@ describe('the /autoplan Phase 4 gate as documented (#4181)', () => {
|
||||
expect(results.slice(1).map(r => chosen(JSON.parse(r.out))[id])).toEqual(['a', 'c', 'd', 'e']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the /autoplan Phase 4 reply table agrees with gstack-gate (GST-2139)', () => {
|
||||
// Value: protects=the reply-to-option table /autoplan acts on matches what bin/gstack-gate parse resolves for `all`, `rec` and `p1a`;
|
||||
// fails_when=`all`/`rec` send the run to approve-as-written while the gate took every recommended plan change, `p1a` approves while a uc item is pending, or an override of an unflagged option approves (answerGate counts it as a plan change); why_new=the #4181 case proves the replies parse, not what the skill does with them; seam=none
|
||||
const tmpl = fs.readFileSync(path.join(ROOT, 'autoplan', 'SKILL.md.tmpl'), 'utf8').replace(/\s+/g, ' ');
|
||||
const ticks = (s: string) => [...s.matchAll(/`([^`]+)`/g)].map(m => m[1]!);
|
||||
const topLevel = (text: string): string[] => {
|
||||
const out: string[] = []; let depth = 0; let code = false; let start = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i]!;
|
||||
if (ch === '`') code = !code;
|
||||
else if (!code && ch === '(') depth++;
|
||||
else if (!code && ch === ')') depth--;
|
||||
else if (!code && depth === 0 && (ch === ';' || ch === '.') && text[i + 1] === ' ') { out.push(text.slice(start, i).trim()); start = i + 2; }
|
||||
}
|
||||
return [...out, text.slice(start).trim()].filter(Boolean);
|
||||
};
|
||||
const table = topLevel(/Parse the reply:.*?never guessed\. (.*?) Under `SESSION_KIND/.exec(tmpl)?.[1] ?? '');
|
||||
const handling = Object.fromEntries([...tmpl.matchAll(/- ([A-E]2?): (.*?)(?= - [A-E]2?: |\*\*Starting)/g)].map(m => [m[1]!, m[2]!]));
|
||||
const write_ = /Write `\$_AP\/gate\.json`.*?gstack-gate render/.exec(tmpl)?.[0] ?? '';
|
||||
const p1 = /and `p1` \(approval: (a\) .*? e\) \w+)\)/.exec(write_)?.[1] ?? '';
|
||||
const parts = p1.split(/(?:^| )([a-e])\) /).slice(1);
|
||||
const p1Options = parts.flatMap((key, i) => (i % 2 ? [] : [{ key, text: parts[i + 1]!.replace(/ \(recommended\)$/, '') }]));
|
||||
const p1Recommended = parts.flatMap((key, i) => (i % 2 === 0 && / \(recommended\)$/.test(parts[i + 1]!) ? [key] : []))[0]
|
||||
?? /recommending `([a-e])`/.exec(tmpl.slice(tmpl.indexOf(p1)))?.[1];
|
||||
const keep = { key: 'a', text: 'keep your direction', changes_plan: false };
|
||||
const change = { key: 'b', text: 'take the reviewers\' change', changes_plan: true };
|
||||
const gate = (): GateList => ({ schema_version: 1, run: 'autoplan-2139', gate_rev: 1, items: [
|
||||
{ id: 'uc1', title: 'User challenge both voices raised', options: [keep, change], recommended: 'b', kind: 'approval' },
|
||||
{ id: 'd1', title: 'Taste decision', options: [keep, change], recommended: 'b', kind: 'auto' },
|
||||
{ id: 'p1', title: 'Plan approval', options: p1Options, recommended: p1Recommended ?? 'a', kind: 'approval' },
|
||||
] });
|
||||
|
||||
/** The skill's options for a reply: the first table row whose condition the gate's parse meets. */
|
||||
function documented(reply: string, g: GateList, parsed: ReturnType<typeof parseReply>) {
|
||||
const said = reply.toLowerCase().split(/\s+/);
|
||||
const optionOf = (c: { id: string; chosen: string }) => g.items.find(i => i.id === c.id)!.options.find(o => o.key === c.chosen)!;
|
||||
for (const row of table) {
|
||||
const changed = parsed.choices.filter(c => c.id !== 'p1' && (optionOf(c).changes_plan ?? (/unflagged override/.test(row) && c.how === 'override')));
|
||||
const at = row.indexOf(' = ');
|
||||
if (at < 0) continue;
|
||||
const lhs = row.slice(0, at), rhs = row.slice(at + 3), names = ticks(lhs);
|
||||
const hit = /\belse\b/.test(lhs) ? true
|
||||
: names.includes('pending') ? parsed.pending.length > 0
|
||||
: names.includes('changes_plan') ? changed.length > 0
|
||||
: names.some(t => said.includes(t) || (/^[a-z]+<n>$/.test(t) && parsed.choices.some(c => c.how === 'override' && said.includes(c.id + c.chosen) && c.id.startsWith(t.slice(0, -3)))));
|
||||
if (!hit) continue;
|
||||
const perKind = [...rhs.matchAll(/([A-E]2?) on an? `([a-z]+)<n>`/g)];
|
||||
const letters = perKind.length ? perKind.filter(m => changed.some(c => c.id.startsWith(m[2]!) && c.id !== 'p1')).map(m => m[1]!)
|
||||
: /^[A-E](\/[A-E])+$/.test(rhs.trim()) ? [rhs.trim().split('/')[names.findIndex(t => said.includes(t))]!]
|
||||
: [...rhs.replace(/`[^`]*`/g, '').replace(/\([^)]*\)/g, '').matchAll(/(?:^|[\s,])([A-E]2?)(?=$|[\s,.])/g)].map(m => m[1]!);
|
||||
return { row, letters: [...new Set(letters)].sort() };
|
||||
}
|
||||
return { row: '(no row)', letters: [] as string[] };
|
||||
}
|
||||
const approves = (letters: string[]) => letters.filter(l => /mark APPROVED/.test(handling[l] ?? ''));
|
||||
|
||||
test('`all` and `rec` apply every recommended plan change, never the plan as first written', () => {
|
||||
expect(Object.keys(handling)).toEqual(['A', 'B', 'B2', 'C', 'D', 'E']);
|
||||
expect(p1Options.map(o => o.key)).toEqual(['a', 'b', 'c', 'd', 'e']);
|
||||
expect(write_).toContain('Every `uc<n>`/`d<n>` option sets `changes_plan`');
|
||||
const g = gate();
|
||||
const file = write('gate.json', g);
|
||||
const seen = ['all', 'rec'].map(reply => {
|
||||
const r = run(['parse', file, '--reply', reply, '--gate-rev', '1', '--json']);
|
||||
const parsed = JSON.parse(r.out) as ReturnType<typeof parseReply>;
|
||||
const doc = documented(reply, g, parsed);
|
||||
return { reply, code: r.code, gate: chosen(parsed), p1: p1Options.find(o => o.key === chosen(parsed).p1)?.text, letters: doc.letters, approves: approves(doc.letters), row: doc.row };
|
||||
});
|
||||
expect(seen.map(({ row: _row, p1, ...rest }) => ({ ...rest, p1_as_written: /as-is|as written/.test(p1 ?? '') })))
|
||||
.toEqual(['all', 'rec'].map(reply => ({ reply, code: EXIT.ok, gate: { uc1: 'b', d1: 'b', p1: 'b' }, letters: ['B', 'B2'], approves: [], p1_as_written: false })));
|
||||
});
|
||||
|
||||
test('`p1a` with a user challenge unanswered approves nothing', () => {
|
||||
const g = gate();
|
||||
const file = write('gate.json', g);
|
||||
const r = run(['parse', file, '--reply', 'p1a', '--gate-rev', '1', '--json']);
|
||||
const parsed = JSON.parse(r.out) as ReturnType<typeof parseReply>;
|
||||
const doc = documented('p1a', g, parsed);
|
||||
expect({ code: r.code, pending: parsed.pending, p1: chosen(parsed).p1, approves: approves(doc.letters), row_names_pending: ticks(doc.row.split(' = ')[0]!).includes('pending') })
|
||||
.toEqual({ code: EXIT.ok, pending: ['uc1'], p1: 'a', approves: [], row_names_pending: true });
|
||||
});
|
||||
|
||||
test('an override of an unflagged option changes the plan, as answerGate counts it', () => {
|
||||
const unflagged = [{ key: 'a', text: 'keep your direction' }, { key: 'b', text: 'the alternative' }];
|
||||
const g: GateList = { schema_version: 1, run: 'autoplan-2139', gate_rev: 1, items: [
|
||||
{ id: 'd1', title: 'Taste decision', options: unflagged, recommended: 'a', kind: 'auto' },
|
||||
{ id: 'p1', title: 'Plan approval', options: p1Options, recommended: 'a', kind: 'approval' },
|
||||
] };
|
||||
const r = run(['parse', write('gate.json', g), '--reply', 'p1a d1b', '--gate-rev', '1', '--json']);
|
||||
const parsed = JSON.parse(r.out) as ReturnType<typeof parseReply>;
|
||||
const doc = documented('p1a d1b', g, parsed);
|
||||
expect({ code: r.code, d1: parsed.choices.find(c => c.id === 'd1'), letters: doc.letters, approves: approves(doc.letters) })
|
||||
.toEqual({ code: EXIT.ok, d1: { id: 'd1', chosen: 'b', kind: 'auto', how: 'override' }, letters: ['B'], approves: [] });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user