From bcd614c152e256a3e361fce7138cd6976c706c12 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 10 Sep 2026 22:46:28 +0000 Subject: [PATCH] fix: recognize current review evidence and scoped edit confirmations Clarify Design and engineering review entry instructions and Design scoring. Recognize required legacy coverage and public Autoplan completion recaps. Bind the pending Edit confirmation to its exact file, ordered digest, and one-request approval when a preceding command display remains visible. Keep reviews within their existing size limits and preserve scope gates when extracting workflow fixtures from either supported preamble header. Keep failure outcomes, review thresholds, provider choices, and eval budgets. --- plan-design-review/SKILL.md | 6 +- .../sections/review-sections.md | 61 +- .../sections/review-sections.md.tmpl | 59 +- plan-eng-review/SKILL.md | 6 +- scripts/resolvers/design.ts | 3 +- .../preamble/generate-completion-status.ts | 7 +- .../preamble/generate-preamble-bash.ts | 8 +- test/autoplan-command-prefix-au.test.ts | 151 +++ test/autoplan-with-result-au.test.ts | 126 +++ test/design-scope-entry-aq.test.ts | 2 +- test/eng-annotated-cache-au.test.ts | 57 ++ test/eng-required-parity-au.test.ts | 123 +++ test/eng-retained-corpus-au.test.ts | 78 ++ test/eng-scope-entry-ap.test.ts | 2 +- test/fixtures/autoplan-command-prefix-au.json | 940 ++++++++++++++++++ test/fixtures/autoplan-with-result-au.json | 11 + test/fixtures/eng-annotated-cache-au.json | 39 + test/fixtures/eng-required-parity-au.md | 397 ++++++++ test/fixtures/eng-retained-corpus-au.md | 459 +++++++++ test/helpers/autoplan-artifact-digest.ts | 42 +- test/helpers/autoplan-artifact-permission.ts | 23 +- test/helpers/autoplan-phase-observer.ts | 42 +- test/helpers/eng-cache-writer-decision.ts | 47 +- test/helpers/eng-retained-corpus.ts | 67 ++ test/helpers/eng-seeded-coverage.ts | 78 +- test/helpers/skill-fixture.ts | 21 +- test/helpers/touchfiles-data.ts | 88 +- ...review-entry-and-design-clarity-au.test.ts | 114 +++ test/skill-fixture.test.ts | 69 ++ 29 files changed, 3030 insertions(+), 96 deletions(-) create mode 100644 test/autoplan-command-prefix-au.test.ts create mode 100644 test/autoplan-with-result-au.test.ts create mode 100644 test/eng-annotated-cache-au.test.ts create mode 100644 test/eng-required-parity-au.test.ts create mode 100644 test/eng-retained-corpus-au.test.ts create mode 100644 test/fixtures/autoplan-command-prefix-au.json create mode 100644 test/fixtures/autoplan-with-result-au.json create mode 100644 test/fixtures/eng-annotated-cache-au.json create mode 100644 test/fixtures/eng-required-parity-au.md create mode 100644 test/fixtures/eng-retained-corpus-au.md create mode 100644 test/helpers/eng-retained-corpus.ts create mode 100644 test/review-entry-and-design-clarity-au.test.ts diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index c3acbf20f..999484048 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -64,7 +64,9 @@ C) A specific page, file, or path. Recommendation: A when a branch diff exists, otherwise B. Reply with A, B, or C. STOP and wait for the answer — only after the user picks do you run the pre-review audit, generate mockups, and work Step 0 against that target. -## Preamble (run first) +## Preamble (after scope gate) + +**Before the command below:** complete the Scope gate above. When plan mode auto-selects an existing plan, first send ordinary prose naming the actual selected plan title or path: "Scope gate: plan mode — auto-selected B (reviewing )." Send it after this skill loads; a pre-invocation introduction does not count. If the gate asks a question, wait for its answer before running this command. ```bash _SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" @@ -98,7 +100,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co ## Skill Invocation During Plan Mode -If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; any AskUserQuestion the skill fires is the workflow operating within plan mode, not a violation of it — and a skill whose instructions resolve a question themselves (e.g. a plan-mode auto-select) may legitimately not ask it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode. +The invoked skill overrides generic plan mode. **Execute the skill file** starting from the Scope gate (post-load announcement or answered question), then preamble and Step 0. Skill questions are valid; skip only questions the skill resolves. Any AskUserQuestion variant satisfies end-of-turn; use AskUserQuestion Format's tool resolution. If unavailable/failed: `headless` → BLOCKED; `interactive` → prose fallback (satisfies end-of-turn). At STOP, stop: no continuation or ExitPlanMode. Execute "PLAN MODE EXCEPTION — ALWAYS RUN" commands. ExitPlanMode only after skill completion or if the user cancels the skill or tells you to leave plan mode. If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?" diff --git a/plan-design-review/sections/review-sections.md b/plan-design-review/sections/review-sections.md index d539044df..a08446641 100644 --- a/plan-design-review/sections/review-sections.md +++ b/plan-design-review/sections/review-sections.md @@ -4,16 +4,32 @@ **Anti-skip rule:** Never condense, abbreviate, or skip any review pass (1-7) regardless of plan type (strategy, spec, code, infra). Every pass in this skill exists for a reason. "This is a strategy doc so design passes don't apply" is always wrong — design gaps are where implementation breaks down. If a pass genuinely has zero findings, say "No issues found" and move on — but you must evaluate it. +**Context:** This section continues `plan-design-review/SKILL.md`. If its setup +is no longer in context, Read `~/.claude/skills/gstack/plan-design-review/SKILL.md` +for the System Audit, Design Philosophy, Step 0, Step 0.5 mockup setup (`$D`), +and Section self-check. Use their existing results; do not restart the review. + **Anti-shortcut clause:** Complete one decision cycle per unresolved finding: explain the gap, recommend options, obtain its individual decision, then apply the selected fix. Scope, focus, setup, and next-step choices approve no remedies. -Writing a report, mapping a token, creating a mockup, or listing a task does not -approve a remedy. Inherited exact decisions and preamble-authorized per-issue -auto-decisions remain valid; do not re-ask them. -If findings exist but only navigation was answered, the review is still waiting -for its first issue decision. Never use the final next-step AskUserQuestion to satisfy the issue-approval loop. +Never use the final next-step AskUserQuestion to satisfy the issue-approval loop. With no unresolved findings, no issue question is required. +**Carry decisions across passes.** An issue is one unresolved design requirement +or tradeoff, even when it appears in several plan locations. Before each pass, +compare the plan, DESIGN.md, and the decisions already made: + +| Situation | Required action | +|-----------|-----------------| +| The exact fix already has an individual user decision or a preamble-authorized per-issue auto-decision. | Reuse that decision. Apply it to all affected references and matching tokens; do not ask again. | +| An accepted requirement needs to be copied unchanged into a required artifact, such as the journey storyboard. | Create the artifact without a separate format question. This records the requirement; it approves no new remedy. | +| The plan violates DESIGN.md or has a gap, and no individual decision has approved its fix. | Ask about that issue and wait before fixing it, even if the input names the gap or DESIGN.md prescribes the exact token. Keep the proposed remedy pending meanwhile. | +| New evidence introduces a missing requirement, a conflict, or a new tradeoff. | Name the new issue, offer alternatives, and obtain its individual decision before changing the plan. | + +Writing a report, mapping a token, creating a mockup, or listing a task does not +approve a remedy. If findings exist but only navigation was answered, the review +is still waiting for its first issue decision. + ## Prior Learnings Search for relevant learnings from previous sessions: @@ -52,23 +68,6 @@ matches a past learning, display: This makes the compounding visible. The user should see that gstack is getting smarter on their codebase over time. -**Carry decisions across passes.** Before each pass, reconcile the plan, its -inherited DESIGN.md requirements, and the user's decisions so far. An issue is an -unresolved design requirement or tradeoff, not each paragraph where it appears. Apply an -approved fix to all affected plan references and matching established tokens; -reference that decision in later passes instead of asking for it again. - -Create required review artifacts, such as the journey storyboard, from accepted -design requirements without asking about their format. Copying an inherited -requirement unchanged needs no new design decision. Changing a documented -violation to conform to that requirement is a fix: offer the remedy and obtain -its first individual approval, even when DESIGN.md prescribes the exact token. -Until then, keep the gap and proposed remedy pending in the plan. If -that work exposes a missing design requirement, conflicting requirements, or a new -tradeoff, -raise that unresolved issue individually and wait for approval. A gap explicitly -listed in the input still needs its first individual approval before it is fixed. - **Pass protocol (1-6):** Record the initial 0-10 score. Every `FIX TO 10` below is a proposal: ask about each issue, wait for approval, then edit the plan and re-rate that pass with the reason for the score change. A declined fix remains documented and lowers the final score. Never edit first and ask afterward. Pass 7 is the unscored decision register, not a seventh rating. ### Pass 1: Information Architecture @@ -102,7 +101,10 @@ Apply time-horizon design: 5-sec visceral, 5-min behavioral, 5-year reflective. ### Pass 4: AI Slop Risk -### Design Hard Rules +**Pass 4 evaluation:** Rate 0-10: Does the plan describe specific, intentional UI, or generic patterns? Record each hard-rejection hit and litmus YES/NO with evidence. An unresolved hard rejection caps this pass below 8 (not design-complete); it does not automatically set the score to 0. Litmus answers support findings, not a separate numeric score. +Use plan text and any available mockups as evidence for the rules below. + +#### Design Hard Rules **Classifier: name the mode before you judge a pixel.** The mode is what the visitor's win looks like on THIS surface, not what the product is. A dev tool's landing page is Persuade. A fashion house's docs are Read. - **PERSUADE** (MARKETING/LANDING PAGE: hero-driven, brand-forward, pricing, campaigns) → they decide and act. Design IS the product. Apply Landing Page Rules. @@ -201,8 +203,6 @@ Judgment tells with no detector rule: gradient cta button, stock-photo hero, car Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developers.openai.com/blog/designing-delightful-frontends-with-gpt-5-4) (Mar 2026) + gstack design methodology. -**Pass 4 evaluation:** Rate 0-10: Does the plan describe specific, intentional UI, or generic patterns? Record each hard-rejection hit and litmus YES/NO with evidence. An unresolved hard rejection caps this pass below 8 (not design-complete); it does not automatically set the score to 0. Litmus answers support findings, not a separate numeric score. - FIX TO 10: Rewrite vague UI descriptions with specific alternatives: - "Cards with icons" → what differentiates these from every SaaS template? - "Hero section" → what makes this hero feel like THIS product? @@ -213,6 +213,7 @@ If visual mockups were generated in Step 0.5, evaluate them against the AI slop ### Pass 5: Design System Alignment Rate 0-10: Does the plan align with DESIGN.md? +If DESIGN.md is absent, rate the plan's explicit token and component specifications. Missing specifications remain findings; do not skip the score or assume alignment. FIX TO 10: If DESIGN.md exists, annotate with specific tokens/components; when it has YAML front matter (the open DESIGN.md format), cite tokens by path (`{colors.primary}`, `{rounded.md}`) so the plan and the file share one vocabulary. If no DESIGN.md, flag the gap and recommend `/design-consultation`. Flag any new component — does it fit the existing vocabulary? Before offering a token-alignment fix, check whether an earlier pass already approved that outcome. If so, apply the established tokens and update every stale gap/reference under that decision; changing the plan location or spelling out the same fix is not a new issue. Ask again only if new evidence exposes an unresolved requirement or tradeoff, and name it. An unapproved violation still needs its first individual decision. @@ -240,6 +241,8 @@ Each decision = one AskUserQuestion with recommendation + WHY + alternatives. Ed ### Post-Pass: Update Mockups (if generated) +After Pass 7: offer the mockup update below when applicable, resolve deferred TODO proposals, reconcile approvals, then synthesize tasks and the Completion Summary. + If mockups were generated in Step 0.5 and review passes changed significant design decisions (information architecture restructure, new states, layout changes), offer to regenerate (one-shot, not a loop): AskUserQuestion: "The review passes changed [list major design changes]. Want me to regenerate mockups to reflect the updated plan? This ensures the visual reference matches what we're actually building." @@ -282,7 +285,7 @@ For design debt: missing a11y, unresolved responsive behavior, deferred empty st Then present options: **A)** Add to TODOS.md **B)** Skip — not valuable enough **C)** Build it now in this PR instead of deferring. Before synthesizing tasks or the completion summary, perform the approval -reconciliation from the Section self-check. Export only agreed implementation work; retain unapproved remedies as pending findings. +reconciliation from the Section self-check in `~/.claude/skills/gstack/plan-design-review/SKILL.md` (Read it if no longer in context). Export only agreed implementation work; retain unapproved remedies as pending findings. Count only individually approved new decisions in "Decisions made" and the review log; a proposed remedy or next-step answer contributes zero. @@ -359,6 +362,12 @@ this run (an empty file means "ran, no findings" — distinct from "didn't run") ### Completion Summary + +**Overall design score:** use the lowest of the six rated pass scores (1-6), +separately before and after approved fixes. Pass 7 is unscored. Keep Step 0's +initial impression in its own row. An overall 8+ therefore means every rated +pass is 8+; unresolved findings still prevent a clean review log. + ``` +====================================================================+ | DESIGN PLAN REVIEW — COMPLETION SUMMARY | diff --git a/plan-design-review/sections/review-sections.md.tmpl b/plan-design-review/sections/review-sections.md.tmpl index 01034469f..44333035a 100644 --- a/plan-design-review/sections/review-sections.md.tmpl +++ b/plan-design-review/sections/review-sections.md.tmpl @@ -2,35 +2,34 @@ **Anti-skip rule:** Never condense, abbreviate, or skip any review pass (1-7) regardless of plan type (strategy, spec, code, infra). Every pass in this skill exists for a reason. "This is a strategy doc so design passes don't apply" is always wrong — design gaps are where implementation breaks down. If a pass genuinely has zero findings, say "No issues found" and move on — but you must evaluate it. +**Context:** This section continues `plan-design-review/SKILL.md`. If its setup +is no longer in context, Read `~/.claude/skills/gstack/plan-design-review/SKILL.md` +for the System Audit, Design Philosophy, Step 0, Step 0.5 mockup setup (`$D`), +and Section self-check. Use their existing results; do not restart the review. + **Anti-shortcut clause:** Complete one decision cycle per unresolved finding: explain the gap, recommend options, obtain its individual decision, then apply the selected fix. Scope, focus, setup, and next-step choices approve no remedies. -Writing a report, mapping a token, creating a mockup, or listing a task does not -approve a remedy. Inherited exact decisions and preamble-authorized per-issue -auto-decisions remain valid; do not re-ask them. -If findings exist but only navigation was answered, the review is still waiting -for its first issue decision. Never use the final next-step AskUserQuestion to satisfy the issue-approval loop. +Never use the final next-step AskUserQuestion to satisfy the issue-approval loop. With no unresolved findings, no issue question is required. +**Carry decisions across passes.** An issue is one unresolved design requirement +or tradeoff, even when it appears in several plan locations. Before each pass, +compare the plan, DESIGN.md, and the decisions already made: + +| Situation | Required action | +|-----------|-----------------| +| The exact fix already has an individual user decision or a preamble-authorized per-issue auto-decision. | Reuse that decision. Apply it to all affected references and matching tokens; do not ask again. | +| An accepted requirement needs to be copied unchanged into a required artifact, such as the journey storyboard. | Create the artifact without a separate format question. This records the requirement; it approves no new remedy. | +| The plan violates DESIGN.md or has a gap, and no individual decision has approved its fix. | Ask about that issue and wait before fixing it, even if the input names the gap or DESIGN.md prescribes the exact token. Keep the proposed remedy pending meanwhile. | +| New evidence introduces a missing requirement, a conflict, or a new tradeoff. | Name the new issue, offer alternatives, and obtain its individual decision before changing the plan. | + +Writing a report, mapping a token, creating a mockup, or listing a task does not +approve a remedy. If findings exist but only navigation was answered, the review +is still waiting for its first issue decision. + {{LEARNINGS_SEARCH}} -**Carry decisions across passes.** Before each pass, reconcile the plan, its -inherited DESIGN.md requirements, and the user's decisions so far. An issue is an -unresolved design requirement or tradeoff, not each paragraph where it appears. Apply an -approved fix to all affected plan references and matching established tokens; -reference that decision in later passes instead of asking for it again. - -Create required review artifacts, such as the journey storyboard, from accepted -design requirements without asking about their format. Copying an inherited -requirement unchanged needs no new design decision. Changing a documented -violation to conform to that requirement is a fix: offer the remedy and obtain -its first individual approval, even when DESIGN.md prescribes the exact token. -Until then, keep the gap and proposed remedy pending in the plan. If -that work exposes a missing design requirement, conflicting requirements, or a new -tradeoff, -raise that unresolved issue individually and wait for approval. A gap explicitly -listed in the input still needs its first individual approval before it is fixed. - **Pass protocol (1-6):** Record the initial 0-10 score. Every `FIX TO 10` below is a proposal: ask about each issue, wait for approval, then edit the plan and re-rate that pass with the reason for the score change. A declined fix remains documented and lowers the final score. Never edit first and ask afterward. Pass 7 is the unscored decision register, not a seventh rating. ### Pass 1: Information Architecture @@ -64,9 +63,10 @@ Apply time-horizon design: 5-sec visceral, 5-min behavioral, 5-year reflective. ### Pass 4: AI Slop Risk -{{DESIGN_HARD_RULES}} - **Pass 4 evaluation:** Rate 0-10: Does the plan describe specific, intentional UI, or generic patterns? Record each hard-rejection hit and litmus YES/NO with evidence. An unresolved hard rejection caps this pass below 8 (not design-complete); it does not automatically set the score to 0. Litmus answers support findings, not a separate numeric score. +Use plan text and any available mockups as evidence for the rules below. + +{{DESIGN_HARD_RULES}} FIX TO 10: Rewrite vague UI descriptions with specific alternatives: - "Cards with icons" → what differentiates these from every SaaS template? @@ -78,6 +78,7 @@ If visual mockups were generated in Step 0.5, evaluate them against the AI slop ### Pass 5: Design System Alignment Rate 0-10: Does the plan align with DESIGN.md? +If DESIGN.md is absent, rate the plan's explicit token and component specifications. Missing specifications remain findings; do not skip the score or assume alignment. FIX TO 10: If DESIGN.md exists, annotate with specific tokens/components; when it has YAML front matter (the open DESIGN.md format), cite tokens by path (`{colors.primary}`, `{rounded.md}`) so the plan and the file share one vocabulary. If no DESIGN.md, flag the gap and recommend `/design-consultation`. Flag any new component — does it fit the existing vocabulary? Before offering a token-alignment fix, check whether an earlier pass already approved that outcome. If so, apply the established tokens and update every stale gap/reference under that decision; changing the plan location or spelling out the same fix is not a new issue. Ask again only if new evidence exposes an unresolved requirement or tradeoff, and name it. An unapproved violation still needs its first individual decision. @@ -105,6 +106,8 @@ Each decision = one AskUserQuestion with recommendation + WHY + alternatives. Ed ### Post-Pass: Update Mockups (if generated) +After Pass 7: offer the mockup update below when applicable, resolve deferred TODO proposals, reconcile approvals, then synthesize tasks and the Completion Summary. + If mockups were generated in Step 0.5 and review passes changed significant design decisions (information architecture restructure, new states, layout changes), offer to regenerate (one-shot, not a loop): AskUserQuestion: "The review passes changed [list major design changes]. Want me to regenerate mockups to reflect the updated plan? This ensures the visual reference matches what we're actually building." @@ -147,13 +150,19 @@ For design debt: missing a11y, unresolved responsive behavior, deferred empty st Then present options: **A)** Add to TODOS.md **B)** Skip — not valuable enough **C)** Build it now in this PR instead of deferring. Before synthesizing tasks or the completion summary, perform the approval -reconciliation from the Section self-check. Export only agreed implementation work; retain unapproved remedies as pending findings. +reconciliation from the Section self-check in `~/.claude/skills/gstack/plan-design-review/SKILL.md` (Read it if no longer in context). Export only agreed implementation work; retain unapproved remedies as pending findings. Count only individually approved new decisions in "Decisions made" and the review log; a proposed remedy or next-step answer contributes zero. {{TASKS_SECTION_EMIT:design-review}} ### Completion Summary + +**Overall design score:** use the lowest of the six rated pass scores (1-6), +separately before and after approved fixes. Pass 7 is unscored. Keep Step 0's +initial impression in its own row. An overall 8+ therefore means every rated +pass is 8+; unresolved findings still prevent a clean review log. + ``` +====================================================================+ | DESIGN PLAN REVIEW — COMPLETION SUMMARY | diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index c6722b285..c6da0a08e 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -58,7 +58,9 @@ C) A specific file, directory, or path. Recommendation: A when a branch diff exists, otherwise B. Reply with A, B, or C. STOP and wait for the answer — only after the user picks do you run the Design Doc Check and Step 0 against that target. -## Preamble (run first) +## Preamble (after scope gate) + +**Before the command below:** complete the Scope gate above. When plan mode auto-selects an existing plan, first send ordinary prose naming the actual selected plan title or path: "Scope gate: plan mode — auto-selected B (reviewing )." Send it after this skill loads; a pre-invocation introduction does not count. If the gate asks a question, wait for its answer before running this command. ```bash _SS="$HOME/.claude/skills/gstack/bin/gstack-skill-start" @@ -92,7 +94,7 @@ In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`co ## Skill Invocation During Plan Mode -If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; any AskUserQuestion the skill fires is the workflow operating within plan mode, not a violation of it — and a skill whose instructions resolve a question themselves (e.g. a plan-mode auto-select) may legitimately not ask it. AskUserQuestion (any variant — `mcp__*__AskUserQuestion` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: `headless` → BLOCKED; `interactive` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode. +The invoked skill overrides generic plan mode. **Execute the skill file** starting from the Scope gate (post-load announcement or answered question), then preamble and Step 0. Skill questions are valid; skip only questions the skill resolves. Any AskUserQuestion variant satisfies end-of-turn; use AskUserQuestion Format's tool resolution. If unavailable/failed: `headless` → BLOCKED; `interactive` → prose fallback (satisfies end-of-turn). At STOP, stop: no continuation or ExitPlanMode. Execute "PLAN MODE EXCEPTION — ALWAYS RUN" commands. ExitPlanMode only after skill completion or if the user cancels the skill or tells you to leave plan mode. If `PROACTIVE` is `"false"`, do not auto-invoke or proactively suggest skills. If a skill seems useful, ask: "I think /skillname might help here — want me to run it?" diff --git a/scripts/resolvers/design.ts b/scripts/resolvers/design.ts index f719e6bf2..bdb378491 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -1015,7 +1015,8 @@ Judgment tells with no detector rule: ${judgmentTells.map(e => e.name.toLowerCas // design-review's Methodology categories 5 and 7 already carry the first two. const reflexBlock = (ctx.skillName === 'design-review' ? reflexes.slice(2) : reflexes).join('\n'); - return `### Design Hard Rules + const heading = ctx.skillName === 'plan-design-review' ? '####' : '###'; + return `${heading} Design Hard Rules **Classifier: name the mode before you judge a pixel.** The mode is what the visitor's win looks like on THIS surface, not what the product is. A dev tool's landing page is Persuade. A fashion house's docs are Read. - **PERSUADE** (MARKETING/LANDING PAGE: hero-driven, brand-forward, pricing, campaigns) → they decide and act. Design IS the product. Apply Landing Page Rules. diff --git a/scripts/resolvers/preamble/generate-completion-status.ts b/scripts/resolvers/preamble/generate-completion-status.ts index 03091bdca..65d6f2b7a 100644 --- a/scripts/resolvers/preamble/generate-completion-status.ts +++ b/scripts/resolvers/preamble/generate-completion-status.ts @@ -19,14 +19,17 @@ import type { TemplateContext } from '../types'; * gates. See ceo-plan 2026-04-24 "remove vestigial plan-mode handshake" for * the full rationale. */ -export function generatePlanModeInfo(_ctx: TemplateContext): string { +export function generatePlanModeInfo(ctx: TemplateContext): string { + const invocation = ['plan-design-review', 'plan-eng-review'].includes(ctx.skillName) + ? `The invoked skill overrides generic plan mode. **Execute the skill file** starting from the Scope gate (post-load announcement or answered question), then preamble and Step 0. Skill questions are valid; skip only questions the skill resolves. Any AskUserQuestion variant satisfies end-of-turn; use AskUserQuestion Format's tool resolution. If unavailable/failed: \`headless\` → BLOCKED; \`interactive\` → prose fallback (satisfies end-of-turn). At STOP, stop: no continuation or ExitPlanMode. Execute "PLAN MODE EXCEPTION — ALWAYS RUN" commands. ExitPlanMode only after skill completion or if the user cancels the skill or tells you to leave plan mode.` + : `If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; any AskUserQuestion the skill fires is the workflow operating within plan mode, not a violation of it — and a skill whose instructions resolve a question themselves (e.g. a plan-mode auto-select) may legitimately not ask it. AskUserQuestion (any variant — \`mcp__*__AskUserQuestion\` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: \`headless\` → BLOCKED; \`interactive\` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.`; return `## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: \`$B\`, \`$D\`, \`codex exec\`/\`codex review\`, writes to \`~/.gstack/\`, writes to the plan file, and \`open\` for generated artifacts. ## Skill Invocation During Plan Mode -If the user invokes a skill in plan mode, the skill takes precedence over generic plan mode behavior. **Treat the skill file as executable instructions, not reference.** Follow it step by step starting from Step 0; any AskUserQuestion the skill fires is the workflow operating within plan mode, not a violation of it — and a skill whose instructions resolve a question themselves (e.g. a plan-mode auto-select) may legitimately not ask it. AskUserQuestion (any variant — \`mcp__*__AskUserQuestion\` or native; see "AskUserQuestion Format → Tool resolution") satisfies plan mode's end-of-turn requirement. If AskUserQuestion is unavailable or a call fails, follow the AskUserQuestion Format failure fallback: \`headless\` → BLOCKED; \`interactive\` → the prose fallback (also satisfies end-of-turn). At a STOP point, stop immediately. Do not continue the workflow or call ExitPlanMode there. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" execute. Call ExitPlanMode only after the skill workflow completes, or if the user tells you to cancel the skill or leave plan mode.`; +${invocation}`; } export function generateCompletionStatus(ctx: TemplateContext): string { diff --git a/scripts/resolvers/preamble/generate-preamble-bash.ts b/scripts/resolvers/preamble/generate-preamble-bash.ts index 140989d3f..fb82169bd 100644 --- a/scripts/resolvers/preamble/generate-preamble-bash.ts +++ b/scripts/resolvers/preamble/generate-preamble-bash.ts @@ -35,7 +35,13 @@ GSTACK_DESIGN="$GSTACK_ROOT/design/dist" // through $HOME instead (env-var hosts already use $GSTACK_BIN). const shellPath = (p: string) => p.replace(/^~\//, '$HOME/'); - return `## Preamble (run first) + const entry = ['plan-design-review', 'plan-eng-review'].includes(ctx.skillName) + ? `## Preamble (after scope gate) + +**Before the command below:** complete the Scope gate above. When plan mode auto-selects an existing plan, first send ordinary prose naming the actual selected plan title or path: "Scope gate: plan mode — auto-selected B (reviewing )." Send it after this skill loads; a pre-invocation introduction does not count. If the gate asks a question, wait for its answer before running this command.` + : '## Preamble (run first)'; + + return `${entry} \`\`\`bash ${runtimeRoot}_SS="${shellPath(ctx.paths.binDir)}/gstack-skill-start" diff --git a/test/autoplan-command-prefix-au.test.ts b/test/autoplan-command-prefix-au.test.ts new file mode 100644 index 000000000..de9470113 --- /dev/null +++ b/test/autoplan-command-prefix-au.test.ts @@ -0,0 +1,151 @@ +import { expect, test } from 'bun:test'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { createHash } from 'node:crypto'; +import fixture from './fixtures/autoplan-command-prefix-au.json'; +import * as permission from './helpers/autoplan-artifact-permission'; +import { readPendingAutoplanArtifact } from './helpers/autoplan-artifact-recorder'; +import { readPlanCountTranscript, type NativePublicToolEvent } from './helpers/plan-count-transcript'; +import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles'; + +function replay() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-ap-command-')); + const old = path.dirname(path.dirname(fixture.stateRoot)); + const runtime = path.join(root, path.basename(old)), cwd = path.join(root, path.basename(fixture.cwd)); + const replace = (text: string) => text.replaceAll(old, runtime).replaceAll(fixture.cwd, cwd); + const hook = JSON.parse(replace(JSON.stringify(fixture.hook))); + const stateRoot = replace(fixture.stateRoot), config = replace(fixture.config), file = hook.pending.file; + const events = JSON.parse(replace(JSON.stringify(fixture.publicTools))) as NativePublicToolEvent[]; + fs.mkdirSync(cwd, { recursive: true }); fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, fixture.before, { mode: fixture.targetStat.mode }); + const mtime = Number(BigInt(fixture.targetStat.mtimeNs)) / 1e9; + fs.utimesSync(file, mtime, mtime); + fs.mkdirSync(path.dirname(hook.pending.transcriptPath), { recursive: true }); + const records = events.map(e => ({ sessionId: e.sessionId, cwd, isSidechain: false, timestamp: e.timestamp, + requestId: e.requestId, message: { id: e.messageId, role: e.kind === 'use' ? 'assistant' : 'user', + content: e.kind === 'use' ? [{ type: 'tool_use', id: e.toolUseId, name: e.name, input: e.input }] + : [{ type: 'tool_result', tool_use_id: e.toolUseId, content: e.content, is_error: e.isError }] } })); + fs.writeFileSync(hook.pending.transcriptPath, records.map(r => JSON.stringify(r)).join('\n') + '\n'); + const hookFile = path.join(root, 'hook.json'); fs.writeFileSync(hookFile, JSON.stringify(hook)); + const publicTools: NativePublicToolEvent[] = []; + const transcript = readPlanCountTranscript(config, cwd, e => publicTools.push(e)); + const now = Date.parse(fixture.viewportCapturedAt), commandStartedAt = Date.parse(fixture.commandTimestamp); + const pending = readPendingAutoplanArtifact(hookFile, cwd, config, stateRoot, commandStartedAt, publicTools, now, true); + const context = { cwd, ownedStateRoot: stateRoot, ownedNativePlansRoot: path.join(config, 'plans'), + commandStartedAt, now, viewportCapturedAt: now, transcriptStatus: transcript.status, publicTools, pending }; + return { root, file, context, viewport: replace(fixture.viewport), dispose: () => fs.rmSync(root, { recursive: true, force: true }) }; +} +type Replay = ReturnType; +const pick = (r: Replay, seen = new Set()) => permission.pendingAutoplanArtifactPermissionInput(r.viewport, r.context, seen); +const panel = (viewport: string) => viewport.slice(viewport.search(/^[─╌]{8,}\n {0,3}Edit file/m)); + +test('the exact public command display preserves only the current unpublished Edit approval', () => { + const r = replay(); try { + expect(r.context.transcriptStatus).toBe('ready'); expect(r.context.publicTools).toHaveLength(2); + expect(r.context.pending?.toolUseId).toBe(fixture.hook.pending.toolUseId); + expect(r.context.publicTools.some(e => e.toolUseId === r.context.pending?.toolUseId)).toBe(false); + expect(createHash('sha256').update(fs.readFileSync(r.file)).digest('hex')).toBe(fixture.provenance.beforeSHA256); + expect(Math.floor(fs.statSync(r.file).mtimeMs)).toBe(Number(BigInt(fixture.targetStat.mtimeNs) / 1_000_000n)); + expect(permission.autoplanArtifactPermissionInput(r.viewport, r.context, new Set())).toBeNull(); + expect(permission.publishedAutoplanArtifactPermissionInput(r.viewport, r.context, new Set())).toBeNull(); + const expected = { input: '1\r', signature: `${fixture.hook.sessionId}:${fixture.hook.pending.toolUseId}`, file: r.file }; + expect(pick(r)).toEqual(expected); + expect(pick(r, new Set([expected.signature]))).toBeNull(); + expect(pick(r, new Set([permission.autoplanArtifactMenuKey(r.viewport)]))).toBeNull(); + r.viewport = panel(r.viewport); expect(pick(r)).toEqual(expected); + expect(fixture.provenance.paidOutcomesReclassified).toBe(false); + } finally { r.dispose(); } +}); + +test('a plain native command description and wrapped display supply no command authority', () => { + for (const prefix of ['● Recording review metrics\n ⎿ $ echo recorded\n\n', + '⏺ Running local diagnostics\n ⎿ $ bun test\n echo finished\n\n']) { + const r = replay(); try { r.viewport = prefix + panel(r.viewport); expect(pick(r)?.input).toBe('1\r'); } + finally { r.dispose(); } + } +}); + +const screens: Array<[string, (s: string) => string]> = [ + ['source introduction', s => 'Source:\n' + s], ['example introduction', s => 'Example:\n' + s], + ['whole quotation', s => s.split('\n').map(line => '> ' + line).join('\n')], + ['whole code block', s => '```text\n' + s + '\n```'], + ['quoted title', s => s.replace('● Appending spec-review metrics', '● "Appending spec-review metrics"')], + ['source title', s => s.replace('● Appending spec-review metrics', '● Source: an example command')], + ['second native action', s => s.replace(' echo logged', '● Another tool\n ⎿ $ echo other')], + ['indented second action', s => s.replace(' echo logged', ' ● Another tool')], + ['Bash confirmation', s => s.replace(' echo logged', ' Do you want to proceed?')], + ['Bash permission', s => s.replace(' echo logged', ' Bash command requires permission')], + ['second question', s => s.replace(' echo logged', ' ❯ 1. Approve this command')], + ['missing command marker', s => s.replace('⎿ $', '⎿ ')], + ['unbound command prose', s => s.replace(' echo logged', 'Unrelated current prose')], + ['second edit panel', s => s + '\n' + panel(s)], + ['foreign panel path', s => s.replace('projects/gstack-autoplan-chain-zmFsqo/', 'projects/another-project/')], + ['basename-only panel', s => s.replace(/^ …[^\n]+$/m, ' 2026-09-10-user-dashboard.md')], + ['foreign menu target', s => s.replace('edit to 2026-09-10-user-dashboard.md?', 'edit to another.md?')], + ['session approval cursor', s => s.replace('❯ 1. Yes', '❯ 2. Yes')], + ['extra current prompt', s => s + '\nChoose another action'], + ['changed added rows', s => s.replace('zero before advancing', 'ten before advancing')], + ['removed-line gap', s => s.replace(' 98 -', ' 100 -')], + ['added-line gap', s => s.replace(' 98 +', ' 100 +')], + ['different reset start', s => s.replace(' 97 +', ' 96 +')], + ['duplicate removed row', s => s.replace(/(^ 98 -[^\n]*\n)/m, '$1$1')], + ['duplicate added row', s => s.replace(/(^ 98 \+[^\n]*\n)/m, '$1$1')], + ['multiple resets', s => s.replace(' 108 5.', s.slice(s.indexOf(' 97 -'), s.indexOf(' 108 5.')) + ' 108 5.')], + ['truncated removed block', s => s.replace(/^ 99 -[^\n]*\n/m, '')], + ['truncated added block', s => s.replace(/^ 107 \+[^\n]*\n/m, '')], + ['missing panel separator', s => s.replace(/^[─]{8,}\n/m, '')], +]; +for (const [name, change] of screens) test(`current panel remains unambiguous: ${name}`, () => { + const r = replay(); try { r.viewport = change(r.viewport); expect(pick(r)).toBeNull(); } finally { r.dispose(); } +}); + +const bindings: Array<[string, (r: Replay) => void]> = [ + ['missing hook', r => { r.context.pending = undefined; }], + ['wrong hook tool', r => { r.context.pending!.tool = 'Write' as 'Edit'; }], + ['foreign hook session', r => { r.context.pending!.sessionId = 'foreign'; }], + ['foreign hook path', r => { r.context.pending!.file += '.other'; }], + ['missing digest', r => { delete r.context.pending!.editDigest; }], + ['invalid digest', r => { r.context.pending!.editDigest!.beforeSHA256 = 'invalid'; }], + ['wrong before digest', r => { r.context.pending!.editDigest!.beforeSHA256 = '0'.repeat(64); }], + ['wrong addition commitments', r => { r.context.pending!.editDigest!.newLineHashes.fill('0'.repeat(64)); }], + ['current file changed', r => { fs.appendFileSync(r.file, '\nchanged'); fs.utimesSync(r.file, new Date(0), new Date(0)); }], + ['file newer than pending', r => { fs.utimesSync(r.file, new Date(r.context.now), new Date(r.context.now)); }], + ['history is Read', r => { r.context.publicTools[0]!.name = 'Read'; }], + ['foreign history file', r => { r.context.publicTools[0]!.input!.file_path = r.file + '.other'; }], + ['failed history', r => { r.context.publicTools[1]!.isError = true; }], + ['unresolved mutation', r => { r.context.publicTools.pop(); }], + ['published pending request', r => { r.context.publicTools.push({ kind: 'use', name: 'Edit', sessionId: r.context.pending!.sessionId, + toolUseId: r.context.pending!.toolUseId, timestamp: r.context.pending!.timestamp, input: { file_path: r.file } }); }], + ['missing transcript', r => { r.context.transcriptStatus = 'missing'; }], + ['future hook', r => { r.context.pending!.timestamp = new Date(r.context.now + 1).toISOString(); }], + ['viewport predates hook', r => { r.context.viewportCapturedAt = Date.parse(r.context.pending!.timestamp) - 1; }], + ['command after hook', r => { r.context.commandStartedAt = Date.parse(r.context.pending!.timestamp) + 1; }], +]; +for (const [name, change] of bindings) test(`pending authorization is retained: ${name}`, () => { + const r = replay(); try { change(r); expect(pick(r)).toBeNull(); } finally { r.dispose(); } +}); +test('only the Autoplan workflow selects this fixture and behavioral regression', () => { + for (const file of ['test/autoplan-command-prefix-au.test.ts', 'test/fixtures/autoplan-command-prefix-au.json']) + expect(selectTests([file], E2E_TOUCHFILES, []).selected).toEqual(['autoplan-chain-pty']); +}); + +test('removed row order is bound to both the current file and pending digest', () => { + const r = replay(); try { + const rows = r.viewport.split('\n'), a = rows.findIndex(row => /^ 97 -/.test(row)), b = rows.findIndex(row => /^ 98 -/.test(row)); + expect(a).toBeGreaterThan(0); expect(b).toBe(a + 1); + const first = rows[a]!.slice(6), second = rows[b]!.slice(6); + rows[a] = rows[a]!.slice(0, 6) + second; rows[b] = rows[b]!.slice(0, 6) + first; + r.viewport = rows.join('\n'); expect(pick(r)).toBeNull(); + } finally { r.dispose(); } +}); + +test('added row order is bound to the complete pending replacement digest', () => { + const r = replay(); try { + const rows = r.viewport.split('\n'), a = rows.findIndex(row => /^ 97 \+/.test(row)), b = rows.findIndex(row => /^ 98 \+/.test(row)); + expect(a).toBeGreaterThan(0); expect(b).toBe(a + 1); + const first = rows[a]!.slice(6), second = rows[b]!.slice(6); + rows[a] = rows[a]!.slice(0, 6) + second; rows[b] = rows[b]!.slice(0, 6) + first; + r.viewport = rows.join('\n'); expect(pick(r)).toBeNull(); + } finally { r.dispose(); } +}); diff --git a/test/autoplan-with-result-au.test.ts b/test/autoplan-with-result-au.test.ts new file mode 100644 index 000000000..015b4412c --- /dev/null +++ b/test/autoplan-with-result-au.test.ts @@ -0,0 +1,126 @@ +import {expect, test} from 'bun:test'; +import actual from './fixtures/autoplan-with-result-au.json'; +import {autoplanPhaseCompletions} from './helpers/autoplan-phase-observer'; +import {E2E_TOUCHFILES, selectTests} from './helpers/touchfiles'; +import type {PlanCountTranscript} from './helpers/plan-count-transcript'; +const at=Date.parse(actual.timestamp); +const transcript=(text=actual.text):PlanCountTranscript=>({status:'ready',calls:[],assistantMessages:[{...actual,text}]}); +const observe=(text:string)=>autoplanPhaseCompletions(transcript(text),at-1); + +test('the exact first AU DX completion retains its native timestamp without crediting the Eng transition',()=>{ + expect(autoplanPhaseCompletions(transcript(),at-1)).toEqual([{phase:2.5,ts:at}]); + expect(actual.sessionId).toBe('78ce9c42-e5f7-4595-81ea-7d9bb8b4345c'); + expect(actual.timestamp).toBe('2026-09-10T21:35:46.209Z'); +}); + +test('affirmative result clauses share phase identity and the existing completion vocabulary',()=>{ + for(const [phase,name] of [[1,'CEO'],[2,'Design review'],[2.5,'DX'],[3,'Engineering review']] as const) + for(const state of ['complete','completed','done','finished','wrapped up']) + for(const result of ['22 findings recorded in the plan.','the score at 8/10.','all adopted changes written; moving to the next phase.']) { + expect(observe(`Phase ${phase} (${name}) is ${state} with ${result}`)).toEqual([{phase,ts:at}]); + } + expect(observe('**Phase 2.5 wrapped up** with 22 findings retained.')).toEqual([{phase:2.5,ts:at}]); +}); + +const rejected=[ + 'Phase 2.5 wrapped up with ', + 'Phase 2.5 wrapped up without the review.', + 'Phase 2.5 will be complete with 22 findings.', + 'Phase 2.5 is not complete with 22 findings.', + 'Phase 2.5 complete with no completed review.', + 'Phase 2.5 complete with findings still pending.', + 'Phase 2.5 complete with 22 findings if the review finishes.', + 'Phase 2.5 complete with 22 findings once approved.', + 'Phase 2.5 complete with 22 findings when the review ends.', + 'Phase 2.5 complete with 22 findings unless the review fails.', + 'Phase 2.5 complete with 22 findings provided the reviewer agrees.', + 'Phase 2.5 complete with 22 findings?','Phase 2.5 complete with results that will arrive tomorrow.', + 'Phase 2.5 complete with maybe 22 findings.','Phase 2.5 complete with an unfinished review.', + 'Phase 2.5 complete with 22 findings. This phase is withdrawn.', + 'Phase 2.5 complete with 22 findings. This phase is "withdrawn".', + 'Phase 2.5 complete with 22 findings. This phase is not complete.', + 'Phase 2.5 complete with 22 findings. This phase is retracted.', + 'Phase 2.5 complete with 22 findings. The declaration is superseded.', + 'Phase 2.5 complete with a historical example.', + 'Phase 2.5 complete with source instructions.', + 'Phase 2.5 complete with "22 findings recorded".', + 'Phase 2.5 complete with \'22 findings recorded\'.', + 'Phase 2.5 (Design) complete with 22 findings.', + 'Phase 2.5 (DX review if approved) complete with 22 findings.', + 'Phase 4 complete with 22 findings.','Phase 2.1 complete with 22 findings.', + '# Phase 2.5 complete with 22 findings.', + '> Phase 2.5 complete with 22 findings.', + '"Phase 2.5 complete with 22 findings."', + '- Phase 2.5 complete with 22 findings.', + '| Phase 2.5 complete with 22 findings. |', + ' Phase 2.5 complete with 22 findings.', + '\tPhase 2.5 complete with 22 findings.', + '```text\nPhase 2.5 complete with 22 findings.\n```', + '~~~text\nPhase 2.5 complete with 22 findings.\n~~~', + 'Source:\nPhase 2.5 complete with 22 findings.', + 'Historical example:\nPhase 2.5 complete with 22 findings.', + 'Historical review:\nPhase 2.5 complete with 22 findings.', + '**Historical review:**\nPhase 2.5 complete with 22 findings.', + '**Source:**\nPhase 2.5 complete with 22 findings.', + 'Hypothetical scenario:\nPhase 2.5 complete with 22 findings.', + 'Phase 2.5 complete with 22 findings.\n```text\nexample text\n````\nThis phase is withdrawn.', + 'Earlier review:\nPhase 2.5 complete with 22 findings.', + 'Phase 2.5 complete with a hypothetical 8/10 score.', + 'Phase 2.5 complete with 22 findings.\nThis phase is withdrawn.', + 'Phase 2.5 complete with 22 findings.\nThis phase is \"withdrawn\".', + 'Phase 2.5 complete with 22 findings.\n**Phase 2.5** is ‘withdrawn’.', + 'Phase 2.5 complete with 22 findings.\nCurrent status: this phase is no longer current.', + 'The template says:\n\nPhase 2.5 complete with 22 findings.', + 'Example:\nPhase 1 complete with findings.\nPhase 2.5 complete with findings.', +]; +test.each(rejected)('%s cannot supply completion',text=>expect(observe(text)).toEqual([])); + +test('quoted summaries retain their existing concrete-consensus requirement',()=>{ + const summary='> Phase 2.5 complete with 22 findings retained.\n> Consensus: 22/22 accepted.\n> Moving to Phase 3.'; + expect(observe(summary)).toEqual([{phase:2.5,ts:at}]); + for(const text of [summary.replace('22/22','[N]/22'),'Example:\n'+summary,summary.replace('22/22','X/Y')]) + expect(observe(text)).toEqual([]); +}); + +test('native readiness, timestamp, duplicate and observed-order rules remain intact',()=>{ + for(const status of ['missing','error'] as const) + expect(autoplanPhaseCompletions({...transcript(),status},at-1)).toEqual([]); + expect(autoplanPhaseCompletions(transcript(),at+1)).toEqual([]); + expect(autoplanPhaseCompletions({...transcript(),assistantMessages:[{...actual,timestamp:'invalid'}]},at-1)).toEqual([]); + const data=transcript();data.assistantMessages.push({...actual,timestamp:new Date(at+1).toISOString()}); + expect(autoplanPhaseCompletions(data,at-1)).toEqual([{phase:2.5,ts:at}]); + data.assistantMessages.unshift({...actual,text:'Phase 3 complete with 7 findings retained.',timestamp:new Date(at-10).toISOString()}); + expect(autoplanPhaseCompletions(data,at-11)).toEqual([{phase:3,ts:at-10},{phase:2.5,ts:at}]); +}); + +test('the regression and exact public message select only the existing Autoplan workflow',()=>{ + for(const file of ['test/autoplan-with-result-au.test.ts','test/fixtures/autoplan-with-result-au.json']) + expect(selectTests([file],E2E_TOUCHFILES,[]).selected).toEqual(['autoplan-chain-pty']); +}); + + +test('quoted history and a foreign phase withdrawal do not cancel the current completed result',()=>{ + for(const suffix of [ + '> This phase is withdrawn.', + 'Historical note: "This phase is withdrawn."', + 'Example:\nThis phase is withdrawn.', + '```text\nThis phase is withdrawn.\n```', + 'Phase 2 is withdrawn.', + 'Phase 3 complete.\nThis phase is withdrawn.', + ]) expect(observe('Phase 2.5 complete with 22 findings retained.\n'+suffix).some(hit=>hit.phase===2.5)).toBe(true); + expect(observe('Phase 2.5 complete with 22 findings.\nHistorical note:\nThis phase is withdrawn.\nCurrent status: Phase 2.5 is withdrawn.')).toEqual([]); +}); + + +test('a current Markdown status heading resets historical context for an owned withdrawal',()=>{ + const prefix='Phase 2.5 complete with 22 findings retained.\nHistorical note:\nThis phase is withdrawn.\n'; + expect(observe(prefix+'## Current status\nPhase 2.5 is withdrawn.')).toEqual([]); + expect(observe(prefix+'`## Current status`\nThis phase is withdrawn.')).toEqual([{phase:2.5,ts:at}]); +}); + +test('inline code around an owned status is scalar formatting while a whole quoted statement stays literal',()=>{ + const prefix='Phase 2.5 complete with 22 findings retained.\n'; + expect(observe(prefix+'This phase is `withdrawn`.')).toEqual([]); + for(const literal of ['`This phase is withdrawn.`','"This phase is withdrawn."','```text\nThis phase is withdrawn.\n```']) + expect(observe(prefix+literal)).toEqual([{phase:2.5,ts:at}]); +}); diff --git a/test/design-scope-entry-aq.test.ts b/test/design-scope-entry-aq.test.ts index 4d09c6582..7b857306f 100644 --- a/test/design-scope-entry-aq.test.ts +++ b/test/design-scope-entry-aq.test.ts @@ -33,7 +33,7 @@ test('every host expands its real bootstrap after the mandatory entry gate', () const preamble = generatePreamble(ctx); const brain = host.suppressedResolvers?.includes('BASE_BRANCH_DETECT') ? '' : generateBaseBranchDetect(ctx); const expanded = template.replace('{{PREAMBLE}}', preamble).replace('{{BASE_BRANCH_DETECT}}', brain); - expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf('## Preamble (run first)')); + expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf('## Preamble (after scope gate)')); expect(expanded.indexOf('Reply with A, B, or C. STOP and wait')).toBeLessThan(expanded.indexOf('```bash')); expect(expanded.indexOf('```bash')).toBeLessThan(expanded.indexOf('gstack-skill-start', expanded.indexOf('```bash'))); if (brain) expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf(brain)); diff --git a/test/eng-annotated-cache-au.test.ts b/test/eng-annotated-cache-au.test.ts new file mode 100644 index 000000000..e5910fe69 --- /dev/null +++ b/test/eng-annotated-cache-au.test.ts @@ -0,0 +1,57 @@ +import {expect,test} from 'bun:test'; +import captured from './fixtures/eng-annotated-cache-au.json'; +import {engFirstReviewAUQ,engSetupAUQ,engStep0Boundary,nativePlanCallFingerprint,planCountQuestionPhase} from './helpers/claude-pty-runner'; +import type {NativePlanQuestionCall} from './helpers/plan-count-transcript'; +import {E2E_TOUCHFILES} from './helpers/touchfiles-data'; +const fresh=()=>structuredClone(captured.call) as NativePlanQuestionCall; +const fp=(c=fresh())=>nativePlanCallFingerprint(c,Date.parse(c.answeredAt!),true); +const first=(c=fresh())=>engFirstReviewAUQ(fp(c)); +function change(edit:(q:NativePlanQuestionCall['questions'][number])=>void){const c=fresh(),q=c.questions[0]!,picked=q.options.findIndex(o=>o.label===c.answers[q.question]);edit(q);c.answers={[q.question]:q.options[picked]!.label};return c;} +test('exact acknowledged annotated cache finding opens review without changing native ownership',()=>{ + const c=fresh(),before=JSON.stringify(c);expect(first(c)).toBe(true);expect(engSetupAUQ(fp(c))).toBe(false);expect(planCountQuestionPhase(fp(c),false,engStep0Boundary,engFirstReviewAUQ,engSetupAUQ)).toMatchObject({preReview:false,reviewStarted:true});expect(JSON.stringify(c)).toBe(before);expect(captured.provenance.retrospectivePass).toBe(false); +}); +test('incidental metadata and all offered choices retain substantive review identity',()=>{ + for(const edit of [ + (q:any)=>{q.question=q.question.replace('D5 — Issue 1','D15 — Issue 11');q.header='Arch 11';}, + (q:any)=>{q.question=q.question.replace('PLAN.md:19-20 + :10','docs/plan.md:42');}, + (q:any)=>{q.question=q.question.replace('[P1] (confidence 8/10)','[P2] (confidence 10/10)');}, + (q:any)=>{q.question=q.question.replaceAll('AuthCache','TenantStore').replaceAll('SessionMint','SessionWriter').replaceAll('AuthBroker','AuthReader');q.options=q.options.map((o:any)=>({...o,description:o.description.replaceAll('AuthCache','TenantStore').replaceAll('SessionMint','SessionWriter').replaceAll('AuthBroker','AuthReader')}));}, + (q:any)=>{q.question+='\n"Historical note: This finding is withdrawn."';}, + (q:any)=>{q.options[0].description+='\n"This option is withdrawn."';}, + ])expect(first(change(edit))).toBe(true); + for(const reversed of [false,true])for(let i=0;i<3;i++){const c=fresh(),q=c.questions[0]!;if(reversed)q.options.reverse();c.answers={[q.question]:q.options[i]!.label};expect(first(c)).toBe(true);} +}); +const changes:Array<[string,(q:NativePlanQuestionCall['questions'][number])=>void]>=[ + ['foreign issue header',q=>{q.header='Arch 2';}],['missing issue',q=>{q.question=q.question.replace('Issue 1 ','');}],['missing annotation',q=>{q.question=q.question.replace('[P1] (confidence 8/10) ','');}],['missing source location',q=>{q.question=q.question.replace('PLAN.md:19-20 + :10 — ','');}],['invalid confidence',q=>{q.question=q.question.replace('confidence 8/10','confidence 11/10');}], + ['conditional defect',q=>{q.question=q.question.replace('both mutate','might both mutate');}],['same actor twice',q=>{q.question=q.question.replace('AuthBroker and SessionMint','AuthBroker and AuthBroker');}],['serialized title',q=>{q.question=q.question.replace('does not serialize mutations','serializes mutations');}], + ['source title',q=>{q.question='Source: '+q.question;}],['quoted title',q=>{const lines=q.question.split('\n');lines[0]='"'+lines[0]+'"';q.question=lines.join('\n');}],['source context',q=>{q.question=q.question.replace('Project/branch/task:','Source:');}],['historical context',q=>{q.question=q.question.replace('Project/branch/task:','Project/branch/task: Historical assessment:');}], + ['no own explanation',q=>{q.question=q.question.replace(/^ELI10:.*$/m,'');}],['quoted explanation',q=>{q.question=q.question.replace(/^ELI10: (.*)$/m,'ELI10: "$1"');}],['competing explanation',q=>{q.question+='\nELI10: There is no race.';}],['hypothetical explanation',q=>{q.question=q.question.replace('ELI10:','ELI10: If approved,');}],['missing race consequence',q=>{q.question=q.question.replace('the mint can land after the invalidation and a suspended tenant keeps a live session','the tenant always loses the session');}], + ['repair wrong cache',q=>{q.options[0]!.description=q.options[0]!.description!.replace('AuthCache passed','OtherCache passed');}],['same writer and reader',q=>{q.options[0]!.description=q.options[0]!.description!.replace('AuthBroker reads','SessionMint reads');}],['missing invalidation rejection',q=>{q.options[0]!.description=q.options[0]!.description!.replace('are rejected if the entry was invalidated since read','are accepted even when invalidated');}],['missing owned repair',q=>{q.options[0]!.description='Choose later.';}],['missing opposed risk',q=>{q.options[2]!.description='The race is closed.';}],['opposition now serialized',q=>{q.options[2]!.description+='\nThe writers are now serialized.';}],['reader also writes',q=>{q.options[0]!.description+='\nAuthBroker also writes.';}], +]; +test.each(changes)('%s cannot open review',(_,edit)=>expect(first(change(edit))).toBe(false)); +test('current statuses, framing and conditional approval are enforced on finding and offered outcomes',()=>{ + for(const status of ['withdrawn','no longer current','hypothetical','optional'])for(const [open,close]of [['',''],['"','"'],["'","'"],['“','”'],['‘','’'],['`','`']]){ + for(const owner of ['This finding','D5','Issue 1'])expect(first(change(q=>{q.question+=`\n**${owner}** is ${open}${status}${close}.`;})),`${owner} ${open}${status}`).toBe(false); + for(const i of [0,1,2])expect(first(change(q=>{q.options[i]!.description+=`\n**This option** is ${open}${status}${close}.`;}))).toBe(false); + } + for(const prefix of ['Source:','Historical assessment:','If approved,','Once approved,','Pending approval:'])for(const i of [0,1,2])expect(first(change(q=>{q.options[i]!.description=prefix+'\n'+q.options[i]!.description;})),prefix).toBe(false); +}); +test('native completion, timestamp, exact answer, session and visible menu stay mandatory',()=>{ + const edits:Array<(c:NativePlanQuestionCall)=>void>=[c=>{c.answered=false;},c=>{c.failed=true;},c=>{c.answers={};},c=>{c.answers[c.questions[0]!.question]='not offered';},c=>{c.unansweredQuestionIndices=[0];},c=>{c.answeredAt='invalid';},c=>{c.sessionId='';},c=>{c.toolUseId='';},c=>{c.questions[0]!.multiSelect=true;},c=>{c.questions.push(structuredClone(c.questions[0]!));},c=>{c.questions[0]!.options[1]!.label=c.questions[0]!.options[0]!.label;}]; + for(const edit of edits){const c=fresh();edit(c);expect(first(c)).toBe(false);}const f=fp();expect(engFirstReviewAUQ({...f,signature:'foreign'})).toBe(false);expect(engFirstReviewAUQ({...f,options:f.options.slice().reverse()})).toBe(false);expect(engFirstReviewAUQ({...f,nativeQuestionIndex:1})).toBe(false); +}); +test('regression fixture and control select the engineering finding-count workflow',()=>{ + for(const file of ['test/eng-annotated-cache-au.test.ts','test/fixtures/eng-annotated-cache-au.json'])expect(Object.entries(E2E_TOUCHFILES).filter(([,paths])=>paths.includes(file)).map(([owner])=>owner)).toEqual(['plan-eng-finding-count']); +}); + +test('current approval conditions and same-option effort boundaries cannot hide withdrawals',()=>{ + for(const phrase of ['requires approval','is conditional on approval','is contingent on acceptance']) for(const target of ['finding','option']) expect(first(change(q=>{if(target==='finding')q.question+='\nThis finding '+phrase+'.';else q.options[0]!.description+='\nThis option '+phrase+'.';}))).toBe(false); + for(const status of ['withdrawn','no longer current']) for(const [open,close]of [['',''],['"','"'],["'","'"],['“','”'],['‘','’']]) expect(first(change(q=>{q.options[0]!.description=q.options[0]!.description!.replace(/\.$/,'')+` This option is ${open}${status}${close}.`;}))).toBe(false); + expect(first(change(q=>{q.options[2]!.description+='\nOnly SessionMint writes.';}))).toBe(false); + expect(first(change(q=>{q.options[0]!.description+='\nDo not inject the cache.';}))).toBe(false); +}); + +test('the injection-only alternative must retain its stated unresolved race',()=>{ + for(const text of ['AuthCache is now serialized.','Only SessionMint writes.']) expect(first(change(q=>{q.options[1]!.description+='\n'+text;}))).toBe(false); + for(const text of ['"AuthCache is now serialized."',"'Only SessionMint writes.'",'ArchiveCache is now serialized.']) expect(first(change(q=>{q.options[1]!.description+='\n'+text;}))).toBe(true); +}); diff --git a/test/eng-required-parity-au.test.ts b/test/eng-required-parity-au.test.ts new file mode 100644 index 000000000..5d6792bf5 --- /dev/null +++ b/test/eng-required-parity-au.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { evaluateEngSeedCoverage } from './helpers/eng-seeded-coverage'; +import { E2E_TOUCHFILES } from './helpers/touchfiles-data'; +const report = readFileSync(new URL('./fixtures/eng-required-parity-au.md', import.meta.url), 'utf8'); +const regression = (plan: string) => evaluateEngSeedCoverage({ status: 'ready', calls: [], assistantMessages: [] }, plan, 0, 1).regression; +const required = report.match(/^### Required tests[^\n]+\n[\s\S]*?(?=\n## )/m)![0]; +const baseline = report.match(/^- \[ \] \*\*T4 .*\n(?: .*(?:\n|$))*/m)![0]; +const parity = report.match(/^- \[ \] \*\*T6 .*\n(?: .*(?:\n|$))*/m)![0]; +const compact = '# Current reviewed plan\n\n' + required + '\n## Implementation Tasks\n' + baseline + parity; + +test('the acknowledged required-test oracle owns its unchanged baseline and separate parity task', () => { + expect(createHash('sha256').update(report).digest('hex')).toBe('8c4b2ea61293a0f8a3b78e6e30017065e5b6dc2211cd95262d0a1c63a0947189'); + expect(regression(report)).toBe('plan'); + expect(regression(compact)).toBe('plan'); + for (const text of [compact.replaceAll('T4', 'T14').replaceAll('T6', 'T16'), compact.replaceAll('legacyAuthFlow.regression.test.ts', 'tests/prior-auth.test.js'), compact.replaceAll('auth-parity.test.ts', 'tests/parity.test.js'), compact.replaceAll('Pin current behavior', 'Capture current behavior'), compact.replaceAll('pinning current', 'capturing current'), compact.replaceAll('unmodified legacyAuthFlow()', 'untouched legacyAuthFlow()'), compact.replace(/[`*]/g, '')]) expect(regression(text)).toBe('plan'); +}); +const negatives: Array<[string, (value: string) => string]> = [ + ['declaration missing', t => t.replace(required, '')], + ['mandatory declaration optional', t => t.replace('regression rule, mandatory, no decision needed', 'regression rule, optional')], + ['no protected legacy target', t => t.replace('`legacyAuthFlow()` before any change:', '`otherFlow()` before any change:')], + ['capture after change', t => t.replace('`legacyAuthFlow()` before any change:', '`legacyAuthFlow()` after any change:')], + ['no parity oracle', t => t.replace('This is\nthe oracle for the parity suite', 'This is\nunrelated to the parity suite')], + ['different parity outcomes', t => t.replace('assert identical\noutcome', 'assert different\noutcome')], + ['only one path', t => t.replace('both paths (flag off, flag on)', 'only the new path (flag on)')], + ['baseline task missing', t => t.replace(baseline, '')], + ['baseline task renamed inconsistently', t => t.replace('current legacyAuthFlow() behavior', 'current otherFlow() behavior')], + ['task file mismatch', t => t.replace(' - Files: legacyAuthFlow.regression.test.ts', ' - Files: another.test.ts')], + ['parity task missing', t => t.replace(parity, '')], + ['parity file mismatch', t => t.replace(' - Files: auth-parity.test.ts', ' - Files: another.test.ts')], + ['parity task one path', t => t.replace('through flag-off and flag-on paths', 'through only flag-on path')], + ['parity verification missing', t => t.replace(' - Verify: suite green for every row; becomes the exit criterion for TODO 1', '')], + ['modified baseline', t => t.replace('unmodified legacyAuthFlow()', 'rewritten legacyAuthFlow()')], + ['baseline verification missing', t => t.replace(' - Verify: test passes against unmodified legacyAuthFlow() first', '')], + ['baseline verification neighbor', t => t.replace(' - Verify: test passes', '- [ ] T99 — other — Unrelated test\n - Verify: test passes')], + ['duplicate baseline task', t => t.replace(baseline, baseline + baseline)], + ['duplicate baseline file', t => t.replace(' - Files: legacyAuthFlow.regression.test.ts', ' - Files: legacyAuthFlow.regression.test.ts\n - Files: another.test.ts')], + ['historical ancestor', t => t.replace('# Current reviewed plan', '# Historical reviewed plan')], + ['source owner', t => 'Source:\n' + t.replace('# Current reviewed plan\n', '')], + ['quoted requirements', t => t.replace(required, required.split('\n').map(l => '> ' + l).join('\n'))], + ['fenced requirements', t => t.replace(required, '```\n' + required + '\n```')], + ...['If approved:', 'Once approved:', 'When approved:', 'Pending approval:', 'Source:'].flatMap(prefix => [ + ['conditional baseline ' + prefix, (t: string) => t.replace(baseline, prefix + '\n' + baseline)], + ['conditional verification ' + prefix, (t: string) => t.replace(' - Verify: test passes', ' ' + prefix + '\n - Verify: test passes')], + ['conditional requirement ' + prefix, (t: string) => t.replace('**CRITICAL (', prefix + '\n**CRITICAL (')], + ] as Array<[string, (t: string) => string]>), + ['changed legacy before baseline', t => t + '\n## Current assessment\nlegacyAuthFlow() is rewritten before T4.\n'], + ['current task status row', t => t + '\n## Current assessment\n| T4 | Withdrawn |\n'], +]; +test.each(negatives)('%s does not supply baseline coverage', (_, change) => { + const changed = change(compact); expect(changed).not.toBe(compact); expect(regression(changed)).toBeUndefined(); +}); +test('owned current scalar statuses cancel; whole quoted history and foreign tasks do not', () => { + for (const owner of ['T4', 'T6', 'T4 baseline verification', 'the legacy regression suite']) for (const status of ['withdrawn', 'not current', 'no longer current', 'optional']) for (const [open, close] of [['',''], ['"','"'], ["'","'"], ['“','”'], ['‘','’'], ['`','`']]) { + expect(regression(compact + `\n## Current assessment\n**${owner}** is ${open}${status}${close}.`), `${owner} ${open}${status}${close}`).toBeUndefined(); + } + for (const tail of ['\n## History\n"T4 is withdrawn."', "\n## History\n'T4 is withdrawn.'", '\n## Current assessment\n"T4 is withdrawn."', '\n## Current assessment\nIf T4 is withdrawn, reconsider rollout.', '\n## Current assessment\nT9 is withdrawn.', '\n## Payment regression suite\nThe regression suite is withdrawn.']) expect(regression(compact + tail), tail).toBe('plan'); +}); +test('new controls select only the engineering finding-count workflow', () => { + for (const file of ['test/eng-required-parity-au.test.ts', 'test/fixtures/eng-required-parity-au.md']) expect(Object.entries(E2E_TOUCHFILES).filter(([, paths]) => paths.includes(file)).map(([name]) => name)).toEqual(['plan-eng-finding-count']); +}); + +test('own declaration and baseline verification currentness survives scalar quote normalization', () => { + for (const status of ['withdrawn', 'no longer current', 'proposed']) for (const [open, close] of [['',''], ['"','"'], ["'","'"], ['“','”'], ['‘','’'], ['`','`']]) { + const tail = `This verification is ${open}${status}${close}.`; + for (const boundary of ['\n ', '; ']) expect(regression(compact.replace('unmodified legacyAuthFlow() first', 'unmodified legacyAuthFlow() first' + boundary + tail))).toBeUndefined(); + expect(regression(compact.replace('What\nbreaks without it:', `This requirement is ${open}${status}${close}. What\nbreaks without it:`))).toBeUndefined(); + } +}); + + +test('owned baseline and parity tasks may share a file and assert conditional input behavior', () => { + for (const value of [ + compact.replaceAll('auth-parity.test.ts', 'legacyAuthFlow.regression.test.ts'), + compact.replace('What\nbreaks without it:', 'The regression suite must assert rejection if the token is expired. What\nbreaks without it:'), + compact.replace(' - Files: legacyAuthFlow.regression.test.ts', ' - Cases: Assert rejection if the token is expired.\n - Files: legacyAuthFlow.regression.test.ts'), + compact.replace('What\nbreaks without it:', 'If the token is expired, assert rejection. What\nbreaks without it:'), + ]) expect(regression(value)).toBe('plan'); +}); + +test('mandatory status and owned actions stay affirmative while approval conditions stay unowned', () => { + for (const negation of ['not mandatory', 'never mandatory', 'no longer mandatory']) { + expect(regression(compact.replace('regression rule, mandatory, no decision needed', 'regression rule, ' + negation))).toBeUndefined(); + } + for (const prefix of ['Do not write the ', "Don't write the ", 'Never add the ']) { + expect(regression(compact.replace('— legacyAuthFlow — CRITICAL regression test', '— legacyAuthFlow — ' + prefix + 'CRITICAL regression test'))).toBeUndefined(); + } + for (const prefix of ['Do not implement the ', "Don't implement the ", 'Never add the ']) { + expect(regression(compact.replace('— tests — Table-driven parity suite', '— tests — ' + prefix + 'table-driven parity suite'))).toBeUndefined(); + } + for (const prefix of ['If authorized:', 'Unless approved:', 'Assuming approval:', 'Provided approval:', 'If requested:']) { + expect(regression(compact.replace('**CRITICAL (', prefix + '\n**CRITICAL ('))).toBeUndefined(); + expect(regression(compact.replace(baseline, prefix + '\n' + baseline))).toBeUndefined(); + expect(regression(compact.replace(' - Verify: test passes', ' ' + prefix + '\n - Verify: test passes'))).toBeUndefined(); + } +}); + +test('conditional input acceptance is behavior; approval of the owned work is conditional scope', () => { + for (const condition of [ + 'If the token is accepted, assert the correct tenant.', + 'When the token is authorized, assert its tenant scope.', + 'If the token is rejected, assert the error response.', + ]) expect(regression(compact.replace('What\nbreaks without it:', condition + ' What\nbreaks without it:'))).toBe('plan'); + for (const condition of [ + 'If accepted:', 'Once authorized:', 'Pending acceptance:', + 'If the requirement is accepted:', 'When this work is approved:', 'If the reviewer approves:', + ]) { + expect(regression(compact.replace('**CRITICAL (', condition + '\n**CRITICAL ('))).toBeUndefined(); + expect(regression(compact.replace(baseline, condition + '\n' + baseline))).toBeUndefined(); + } +}); + +test('required capture and parity declarations must affirm their own action after the named file', () => { + for (const command of ['Do not pin', "Don't capture", 'Never record']) { + expect(regression(compact.replace('Pin current behavior', command + ' current behavior'))).toBeUndefined(); + } + for (const command of ['Do not use one', "Don't use one", 'Never use the same']) { + expect(regression(compact.replace('One fixture table', command + ' fixture table'))).toBeUndefined(); + } + expect(regression(compact.replace('What\nbreaks without it:', 'The old review said "Do not pin current behavior." What\nbreaks without it:'))).toBe('plan'); + expect(regression(compact.replace('suite green for every row;', 'suite green for every row; old guidance said "Do not use one fixture table";'))).toBe('plan'); +}); diff --git a/test/eng-retained-corpus-au.test.ts b/test/eng-retained-corpus-au.test.ts new file mode 100644 index 000000000..291748a93 --- /dev/null +++ b/test/eng-retained-corpus-au.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { evaluateEngSeedCoverage } from './helpers/eng-seeded-coverage'; +const report = readFileSync(new URL('./fixtures/eng-retained-corpus-au.md', import.meta.url), 'utf8'); +const regression = (s: string) => evaluateEngSeedCoverage({ status: 'ready', calls: [], assistantMessages: [] }, s, 0, 1).regression; +const required = report.match(/^### CRITICAL regression test[^\n]+\n[\s\S]*?(?=\n### )/m)![0]; +const retained = report.match(/^- `legacyAuthFlow\(\)`: retained[\s\S]*?(?=\n\n)/m)![0]; +const task = report.match(/^- \[ \] \*\*T4 .*\n(?: .*(?:\n|$))*/m)![0]; +const compact = '# Current reviewed plan\n\n' + required + '\n## What already exists\n' + retained + '\n\n## Implementation Tasks\n' + task; +test('a mandatory recorded corpus owns parity against a retained legacy baseline', () => { + expect(regression(report)).toBe('plan'); + expect(regression(compact)).toBe('plan'); + for (const s of [compact.replaceAll('T4', 'T17'), compact.replaceAll('test/auth/legacy-parity.regression.test.ts', 'tests/recorded.test.js'), compact.replace('Record a corpus', 'Capture a corpus'), compact.replace('new `AuthBroker` path', 'new `ReplacementBroker` path'), compact.replace(/[`*]/g, '')]) expect(regression(s)).toBe('plan'); +}); +const cases: Array<[string, (s: string) => string]> = [ + ['missing requirement', s => s.replace(required, '')], + ['optional requirement', s => s.replace('mandatory under', 'optional under')], + ['not mandatory', s => s.replace('mandatory under', 'not mandatory under')], + ['no legacy decisions', s => s.replace('legacy decision for each', 'new broker decision for each')], + ['different outcomes', s => s.replace('assert identical', 'assert different')], + ['partial parity', s => s.replace('for every entry', 'for selected entries')], + ['no persistent oracle', s => s.replace('- This test is also the shadow-mode oracle; it stays after legacy deletion,\n re-pointed at the recorded decisions.', '')], + ['no retained baseline', s => s.replace(retained, '')], + ['different legacy target', s => s.replace('`legacyAuthFlow()`: retained', '`differentFlow()`: retained')], + ['legacy is rewritten', s => s.replace('`: retained behind', '`: rewritten behind')], + ['no task', s => s.replace(task, '')], + ['different task file', s => s.replace(' - Files: test/auth/legacy-parity.regression.test.ts', ' - Files: test/auth/other.test.ts')], + ['missing verification', s => s.replace(' - Verify: 100% decision + reason-code parity across the corpus', '')], + ['partial verification', s => s.replace('100% decision', '50% decision')], + ['neighbor verification', s => s.replace(' - Verify: 100%', '- [ ] T99 — other task\n - Verify: 100%')], + ['duplicate task', s => s.replace(task, task + task)], + ['duplicate Files', s => s.replace(' - Files:', ' - Files: another.test.ts\n - Files:')], + ['do not add', s => s.replace('Add `test/', 'Do not add `test/')], + ['do not record', s => s.replace('- Record a corpus', '- Do not record a corpus')], + ['do not implement task', s => s.replace('CRITICAL: recorded-corpus', 'Do not implement CRITICAL: recorded-corpus')], + ['conditional requirement', s => s.replace('Add `test/', 'If approved:\nAdd `test/')], + ['conditional task', s => s.replace(task, 'Once approved:\n' + task)], + ['quoted requirement', s => s.replace(required, required.split('\n').map(l => '> ' + l).join('\n'))], + ['fenced requirement', s => s.replace(required, '```\n' + required + '\n```')], + ['historical plan', s => s.replace('Current reviewed plan', 'Historical reviewed plan')], + ['quoted owner', s => 'Source:\n' + s.replace('# Current reviewed plan\n', '')], + ['changed baseline before task', s => s + '\n## Current assessment\nlegacyAuthFlow() is rewritten before T4.\n'], + ['withdrawn status row', s => s + '\n## Current assessment\n| T4 | Withdrawn |\n'], +]; +test.each(cases)('%s cannot grant regression coverage', (_, change) => { + const changed = change(compact); expect(changed).not.toBe(compact); expect(regression(changed)).toBeUndefined(); +}); +test('current withdrawals cancel the owned task; historical quotations do not', () => { + for (const owner of ['T4', 'T4 verification', 'the legacy regression suite', 'this corpus']) for (const status of ['withdrawn', 'not current', 'optional']) for (const [a,b] of [['',''], ['"','"'], ["'","'"], ['“','”'], ['‘','’'], ['`','`']]) expect(regression(compact + `\n## Current assessment\n${owner} is ${a}${status}${b}.`)).toBeUndefined(); + for (const tail of ['\n## History\nT4 is withdrawn.', '\n## Current assessment\n"T4 is withdrawn."', '\n## Current assessment\nIf T4 is withdrawn, reassess.', '\n## Current assessment\nT9 is withdrawn.']) expect(regression(compact + tail)).toBe('plan'); +}); +test('approval punctuation does not make the corpus or its verification unconditional', () => { + for (const prefix of ['If approved,', 'Once approved,', 'When approved,', 'Pending approval,']) { + expect(regression(compact.replace('Add `test/', prefix + '\nAdd `test/'))).toBeUndefined(); + expect(regression(compact.replace(task, prefix + '\n' + task))).toBeUndefined(); + expect(regression(compact.replace(' - Verify:', ' ' + prefix + '\n - Verify:'))).toBeUndefined(); + } + expect(regression(compact + '\n## Current assessment\nT4 is conditional on approval.')).toBeUndefined(); +}); +test('local source labels do not assert a required corpus', () => { + for (const prefix of ['Source.', 'Historical assessment:', 'Quoted source.']) expect(regression(compact.replace('Add `test/', prefix + '\nAdd `test/'))).toBeUndefined(); +}); +test('rewriting the oracle before recording its corpus invalidates the baseline', () => { + for (const statement of ['legacyAuthFlow() is rewritten before the corpus is recorded.', 'legacyAuthFlow() is deleted before the regression baseline is captured.', 'The corpus is recorded only after legacyAuthFlow() is rewritten.', 'The legacy regression baseline is rewritten.']) expect(regression(compact + '\n## Current assessment\n' + statement)).toBeUndefined(); +}); +test('an unrelated suite status and a token input condition leave current legacy coverage intact', () => { + expect(regression(compact + '\n## Payment regression suite\nThe regression suite is withdrawn.')).toBe('plan'); + expect(regression(compact.replace('legacy decision for each.', 'legacy decision for each.\n- Also assert rejection if the token is expired.'))).toBe('plan'); +}); + +test('current named legacy suite headings retain ownership of their withdrawals', () => { + for (const title of ['Current legacy regression suite', 'legacyAuthFlow() regression suite', 'Recorded legacy parity test']) expect(regression(compact + `\n## ${title}\nThe regression suite is withdrawn.`)).toBeUndefined(); + for (const title of ['Payment regression suite', 'Current Payment regression suite']) expect(regression(compact + `\n## ${title}\nThe regression suite is withdrawn.`)).toBe('plan'); +}); + +test('a never-mandatory declaration cannot grant mandatory regression coverage', () => { + expect(regression(compact.replace('mandatory under', 'never mandatory under'))).toBeUndefined(); +}); diff --git a/test/eng-scope-entry-ap.test.ts b/test/eng-scope-entry-ap.test.ts index 4cf15a000..2e2dd5dff 100644 --- a/test/eng-scope-entry-ap.test.ts +++ b/test/eng-scope-entry-ap.test.ts @@ -30,7 +30,7 @@ test('every host expands its real bootstrap after the mandatory entry gate', () const preamble = generatePreamble(ctx); const brain = host.suppressedResolvers?.includes('GBRAIN_CONTEXT_LOAD') ? '' : generateGBrainContextLoad(ctx); const expanded = template.replace('{{PREAMBLE}}', preamble).replace('{{GBRAIN_CONTEXT_LOAD}}', brain); - expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf('## Preamble (run first)')); + expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf('## Preamble (after scope gate)')); expect(expanded.indexOf('Reply with A, B, or C. STOP and wait')).toBeLessThan(expanded.indexOf('```bash')); expect(expanded.indexOf('```bash')).toBeLessThan(expanded.indexOf('gstack-skill-start', expanded.indexOf('```bash'))); if (brain) expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf('## Brain Context Load')); diff --git a/test/fixtures/autoplan-command-prefix-au.json b/test/fixtures/autoplan-command-prefix-au.json new file mode 100644 index 000000000..390fe2c6f --- /dev/null +++ b/test/fixtures/autoplan-command-prefix-au.json @@ -0,0 +1,940 @@ +{ + "provenance": { + "source": "AU retry public terminal, pre-tool hook metadata, and acknowledged public same-file Write pair", + "nativeSessionId": "7fcddaa4-44d5-4550-b9cb-ce4e092faebd", + "pendingSource": "pre_tool_use; native use not yet published", + "publicViewportSHA256": "2fae81f74e48f7b76d4f78f962cf17fe1a4d5cceb949edd97ace5198a0e9e2ba", + "hookStateSHA256": "b13288a9c9b733d44732038ed3e92a266550d4a22e4efddd8b776527ebe8f366", + "beforeSHA256": "ee4a915de5edc5955117e32a8ff2e4967da54f9812657c66a5cd2d168ec3c8c1", + "retainedDiagnosisSHA256": "22d4c1d4436ec5e27209131d5ffbae5599e2726ce3c1ee81c6c405a8e550e5ff", + "originalOutcome": "incomplete-permission-stall", + "paidOutcomesReclassified": false, + "limits": [ + "Only two exact public history events are selected here; all 72 were checked in the retained diagnostic replay.", + "Command timestamp bounds the unchanged event filter; exact launcher Date.now is not retained.", + "File restoration preserves the original mtime millisecond floor used by the helper, not filesystem nanosecond precision.", + "The preceding command display has no assigned published/completed/queued Bash status." + ] + }, + "cwd": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-autoplan-chain-zmFsqo", + "config": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/with-skills/.claude", + "stateRoot": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/skill-home-262upM/.gstack", + "commandTimestamp": "2026-09-10T21:44:08.844Z", + "viewportCapturedAt": "2026-09-10T22:05:23.573Z", + "targetStat": { + "size": 7635, + "mtimeNs": "1789077609038803165", + "inode": 65122877, + "device": 65040, + "mode": 420 + }, + "hook": { + "version": 1, + "cwd": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-autoplan-chain-zmFsqo", + "config": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/with-skills/.claude", + "stateRoot": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/skill-home-262upM/.gstack", + "seenIds": [ + "toolu_01BEJjHdBgL3sth2vHKgYSkT" + ], + "pending": { + "source": "pre_tool_use", + "sessionId": "7fcddaa4-44d5-4550-b9cb-ce4e092faebd", + "toolUseId": "toolu_01BEJjHdBgL3sth2vHKgYSkT", + "tool": "Edit", + "file": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/skill-home-262upM/.gstack/projects/gstack-autoplan-chain-zmFsqo/ceo-plans/2026-09-10-user-dashboard.md", + "timestamp": "2026-09-10T22:01:27.448Z", + "transcriptPath": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/with-skills/.claude/projects/-tmp-gstack-paid-shard-a0OkbA-tmp-gstack-autoplan-chain-zmFsqo/7fcddaa4-44d5-4550-b9cb-ce4e092faebd.jsonl", + "editDigest": { + "version": 1, + "beforeSHA256": "ee4a915de5edc5955117e32a8ff2e4967da54f9812657c66a5cd2d168ec3c8c1", + "requestSHA256": "e174ec4a2640d8fedbe97aa27e3c2787d147b1b8af01d165101105a48b0ac33c", + "oldLineHashes": [ + "c8a17a84891597824117ab580d4cc481bb9698cd8d81a51ace72b54b34a8a53a", + "d874b53ed291fcc57b1ca9a60d1507fbd803fefbad3fd4443211e690e0670b6e", + "d7633393873d912c401a8dd1428a31d42655372451af52e98c6748f8db53212d", + "a40e2f60ffa4f63ff45a7e111fbaa8affb4cd7ffcc2e0b30524a06ed870367a5" + ], + "newLineHashes": [ + "c8a17a84891597824117ab580d4cc481bb9698cd8d81a51ace72b54b34a8a53a", + "1b74a1a2305fe2c2779701953f62918446657b3be114175b4f19e71b1ef78b3e", + "82cc5d4e8fd5a9adbafed900c99e1b6f93fe6fe79c1309d7951b728b27b66f65", + "e0587824320aae5582b9fd213baafc0df133c60b0692a954c6c1e3760394495d", + "6d28976d6a4076a08deb9593e28aefda8b0973ae929e5eb7b157c4b18e99a69c", + "44463911d29525c105445e219ff2d4564fc5e1a802c377a79944ab792271afc6", + "707d97569b65c74a0e3a451908e0c5d81dc598d8168654477e4d4cce9d70c3f9", + "408723e1679758ded7174e1c031098a3f87901175dce9bb0c90fcf35735ecdbd", + "414e1c6c29aaf55f2caeadc97845e40e62b80b8d4caeccad14fb8035132ebe6b", + "c8e02969b2776377290185233fa1e4d3e911fa3652397c012075fe7c7073d315", + "0aa487e1deeb804b33afb0e32842f46bb33fc9e40e4a740be2921e0d986e6a22", + "1855357059e6a2a14a211a1c91000807ecc51e3065187ab67c7d633eaf133eae" + ], + "clippedAdditions": { + "version": 1, + "status": "complete", + "startLine": 96, + "lines": [ + { + "line": 97, + "lineHash": "1b74a1a2305fe2c2779701953f62918446657b3be114175b4f19e71b1ef78b3e", + "nextLineHash": "82cc5d4e8fd5a9adbafed900c99e1b6f93fe6fe79c1309d7951b728b27b66f65", + "suffixHashes": [ + "ab3aec57b44f30c90eb9b79e6119250c7823e58f4c12277e09fe3c13f7dda092", + "80ded7be07c12d5d0facc090a2b1f3397bb251cd49a235f7f03bfed9d2af4924", + "a2754eedf0dfdb0c7329e5a42f8a960b34152efc1bb12ace4fd8cf024d03a9ba", + "327be43ffe414dac4c4a3d67cf0f6d6fd252c6341570c6f152ce29aab80e736e", + "0f109444b10657db12db968ad9b80f2850cda1a071312e4e887240694390beec", + "fc2b7a854eaea0a0b30694eb43f042251227464e3974dc58cd29da50e42beace", + "b6f4682f0420801ed709d499d8e60cdd92fc2d78b59c37f60c77c210385c2cb6", + "022ea3211701d20527ffd74f9b0386412c8b769069fe8c07370414e7c4a0a049", + "537e2c59f3c31f6dbdf4e9a678c409ea48d075d0d1557342b58b2a7ffe180d1c", + "025ef9a895ef97a78437926418e38c8705102daf2b342d8a08fa12a2393a2354", + "698d89b6e9f0d079d6d7c5bd26ee9e0e6a0d0e09076d51e9d9f960dd20551939", + "305293249d1e0f04295631d6bdb3e01ccf02096ed1e64257d5c4ffd6a22e3b90", + "84d2e4e25744bef538847e17f5c9280395743fd65518336a9b795e89ed660857", + "053037ee393365898e3abefde9a560b2cca3f016f985e884549f69dff2c76661", + "955558c32eade14884f631ca4983704745daeb1aa988ac68c4eb6953c762e5a9", + "e79e8105aa87ab4f18b5334e3afd5cecc9e2b12093a150fb317e2002d71bbfa5", + "0b09f4ded28b7e2c7a2b9c027d96f2b04fe0e8aa11260be3403ec5133d14e5c1", + "e64de5b37b0511a5c18e709ae4a9eebe79f2b04cfd65cb05a86348d3a4e43c39", + "06d938cde301761790d32e35f5a25d95fd0b8469b397baf42f3edce6d0218b44", + "a2e5027c014b2ad9b0a9ae0591b2cf56fc155d0fe15100442b72b4b7c87b3432", + "f9fda50eeb722d1db68893516493b6fd0b9e01aac3473f056ecf36f368bd52db", + "62947a4530af56ff4e0511014e6a38c36f80f509475d7d03a9b796512f979088", + "3ee6d1572725f7b7c0a4e868864fc98bc0389bdf31b54d069408f8724d1bee02", + "75a37de01189a0dd3eef5b7ffd0973d8e3c8ed1d05c78d7280a140b37e995469", + "25a78db902fe6997bd99044393dcaa3de718e307c2d4a9ab97ce213ba5ec085e", + "247142dc5ecf73222d3830bfb4b8d1b2781c13626eec2802589c96ce1be0015f", + "743e699f59dcc8e5da83d7bf4caca1bc00e70f31fd24cec117a846991880d13b", + "3fdd4d6e0dc3d64dc6af6ac8323089fef42dc7b146fa54c8062104c82b62e454", + "a51ef9d2c32550f2e19ca7a691f8836df9aeac608ea7f8715ef1707651b628b3", + "326be18b3aecb8a8b8966c11066b576b82de8e807213157a2bf88927754266b4", + "40ffa6740b96d599dce3052a5a73b568c9b76e46f6f556b0f1ec469c72a8f401", + "84650abc3198487cde5bf25a3b87d51422b28d8863436441312296cbf76aefbe", + "4315655d90d32caa18ee24717134880e297fe1b4fae08f4790e4b3fc5023feef", + "c3314b408d73f0a1456168e6b52d2c1761220922400d944dd24bfad9562d9fa0", + "a063b6578179f135c4bbe3e0c3e849a546883776c1549f1aa9b8140d21cb6ac4", + "861c9996d34235ea14bd9a19f6fc672ff26208f82a74ae185fc58ed66c1849eb", + "948d89e150096b1584484a6a4622cddf9935a7ba41c418283650c7afeeab8621", + "005a06583ced56f6c4e234469c4903be6ff859ca497d9c77a17e65ddeb2dc40a", + "f7b4865f548bb062b8e3a16ce8738cf2beae7baeedbaad103155fd648f5d2b27", + "d91e33b7cd563734f264956756132b8c78feba6f2855e7a3ac4493b7da08b9c3", + "c36feb3234527280f5cdf308172293bca801b32aff27d21f8748e34e68494174", + "8f9c6cde8467f8f13900a961d9267807aad3201b9977dc46c18edf79eb85df10", + "1cb109c5ad5584a3c97ffa5e68a325ef284913aae6ca90c56d3f0ca358958b5d", + "162fa7f4aa955c13a09a7f87b3ab053dfc602db53b61a604fd9c58558f5836d6", + "3f02ab71368ed0161315880c7cd0eef75b0930c75fa71df81d8f8a32cc8d6158", + "f5e8bde7e8eaa4a269126be30e7376462e47205af5c8bf40105011fbf231f5c5", + "8f729021aa4af933e925e78c5a181df014b41476f8dbd7fe6ea240c83413d285", + "d2a6fa8b54677bfb7fd2013d48b73e1a5236ec9a2efb20a509e5dd903decb4de", + "080ce00a7997cac3828b8ff82c42f2ec655c7cf9b24b3fa833047f5875dde316", + "365a9ba255b2e32169b8f8f2eb50151280186ef9d68911c0c6528b51e68eaeb2", + "678372c3e10ddb4154f5dd4885ab401c21652015c520440f4be58ee49c0be5c7", + "d5c64bb8d7a475ec0dc64f2ea7ec117c08ce95647d8548fd4e8fec08893f4619", + "8e5db7aa1bf6f6ca826805ad4487463148502cd8271ae18f87cf7e359ce0c570", + "f63c5c6bf3dd061ec0b7a07e975356571d19dafc51d24217df7390dfe6623eb8", + "7344fe5fe7e7179d3dd6fd522eec7b4c9f32abd687dc9a7dd21db148231f5b23", + "a9822cc59b35f9fd5447750ccd3de1887e5847125d819ccc7e401358f828b629", + "202839eef5199545817ad8e2b2be755e9ff129bf8618109a5c42df29437cdf5a", + "d5d0b99eaf66c78d2f54d4610fcb4e3c1f20012215fbe0259f1b47b031f37855", + "5b5a642519de36c2c969b4a0ae53ec529a489e9add5860734dc57e7ffcbd306c", + "3845d1f7322ed1ce36d6c77a87ae1434e25cf23e4cf679963727256ada09c791", + "9b566cf8d005351f09715c4aa64a8ed8d408146b954d708fe9321330ee1dcd35", + "732e0e027561c2045b83cb0431973116960ee0a45ef65af4a1c5091605ffe4f2", + "305dcbcb8478baa0df79070dd696c1aa4f53d34564361177a719e8062bc3292c", + "eacecdcc85482e6cdae4b86ceff4f1954ca58afae4a1fa14c54f6550aef5d9fb", + "f9987a67fc73e6b519645c8fd2ce2745772791807f00c4a83f06e4f65e3654f1", + "6cf1775efe9374b6c58373f968c24d9be5e10903202c9643778db9b6a866763d", + "c891ad6f578822a6bb2a2373d693600888fbfd9a8de347e4097db3757d2b7d5e", + "4936078de2a563088792c0c6b0172aee36b84d51efcbc29ead6e9d108099b9b5", + "ec95d606eadcb4e52bdd72169f20fd1b79ee538f1c3fe5c378f360d947c5a1e3" + ] + }, + { + "line": 98, + "lineHash": "82cc5d4e8fd5a9adbafed900c99e1b6f93fe6fe79c1309d7951b728b27b66f65", + "nextLineHash": "e0587824320aae5582b9fd213baafc0df133c60b0692a954c6c1e3760394495d", + "suffixHashes": [ + "7c575afa225a2df58e6aa37cb5c1f95fe4db6400ac186c2ef5bffffb56ccab1e", + "c38afd434a6b429214dc9ebc912458151e3121e229394aa5a06afa640ed44589", + "5056b992972618ed275c2afd4b5beb675186dc13be1ca1ceda9e8c1234fec157", + "312ecae7149323c4f39aa8c5aedd8052a3bf1fc1ac0d480908f0b938184b4598", + "dc46a89b11e519b958c497cbdf09c83e481337bf267578735ffbe943b77af7a4", + "991b04dfca0cdae3bc03cbda11e659e45c6368df0e1c09d7cc7ed72e3bc49366", + "5e9b7a17f5231423acbffe533a5ecb935a1957fabe213d27e860b39fa72bf06f", + "71c2f5894d5790705addbb699aaec8319218e594b7faf680696b3a3fee858940", + "4d12c035feca49201fd825b1c6d5f0ca37e6a715045a02ecaf4565eb04d89a47", + "e77bd972bbdfb7aeea348a1ac6f219443511bdbe832398f491999101e916e807", + "64c4f39ee34a9b1f8e3d5717abe871b80f1d510f2f50608a933bbc8c815064b1", + "556d61b0db51f078ef26d4940bbc3a8090469c86e6f2b044a3b5768577bb3188", + "2bd4a48170bc63af8069797f252405084a0e11a33eff360186622a8672af4db9", + "af4a9c2334c182595bab8973c02808a758a18428a6675656b95b1d7a89baa358", + "75f348e55423a129a996bde7c1d07bdfaccf5119daae228159073a6cabb4c0cb", + "a36657d35bb9ba715b4b47728fd2a9b2b48f3ca75759b919801f3c8c47aa1eec", + "ed9215c3aae945de363358e5f88fc9b175ef490c0a437c7aa6e1c0f9f103fa11", + "bdece1a20c1e3d6c4cc9a1bfe6c5855dca901faba29d772bfcd7ff69e7dc37c4", + "2c48bf32321ff28cb0cd42af34e7e9499a2780b5a7bcae45f540f06ad047b553", + "4ab3c6c74459d6869a501b043dd4ee832b2f37e31e199ed92a1e453efaf37331", + "f35504e505c0fadac12cb1dd74442d821b97a50dfcbc8f95f7487e9a87514d48", + "741d3368d0590158d2c9137969943d11b5d9f5e9b4de646683b4f55a9f5e519b", + "9fa3615ff0c1c99666aecda8f5f892a837eda867dd6e2e5db58ee364af20358e", + "2b8cf034372bd5b03d3d36df95148df34ed856c00a3a7ee620e0ab9beca8d6bc", + "f8d72202e1f93c25da283724ab9ac685bbe787efbc6653fa2a6f175cea5dc09b", + "c585a838715a235f73eb96dc88a9d055abbc81a1e1ae153520a36f37118ad6b2", + "9bec0e7805f2d013f090d02db4b562f7129e8bc44498feaa633cdddc00f861dc", + "f12c963dda889473b79ec2f6336756775a8cf66007cbdf1085833e6aec8202ff", + "4f6641c764a7555dd6e8515b28e66b6f219b63094a3c2a54afdce35335e34124", + "b33a47d295805d47de774f62743f9ef2bf21967486fce7ffbbefba0fe2ae6e3c", + "b01b08f010e38b76cc863155722c3d36eb7b6a302635105f4c7ec102482398a0", + "c2275c5d50261c3d96bb4da019a127f6e40524ab64a3993c0ef55a811f79dd74", + "217c43c33750be5a961b8034967044cbadb6f40aadfbd1bf34bd8491b7ad60a8", + "540082c83b7ffe382ad1ea41aad6c323f9fe6dd9c46fe10b1d6456ddd55692fb", + "f4802b4949e896d6e1081bd13adfd6408db65ff0f99a6906374aa04299a8be82", + "160919f7cb94979328b199f28a89b775492e189e8bf383b7a589f84e87415c37", + "56ca7bb13d473512f4a3dde2323a8918900aecdc149e1db1aa18f8f15105926d", + "a8159e05453fc7c01afa4b3d568ebbdfdc42008138a9c0aedfc2e936042f03ce", + "8817fab88f99daa810f1af4f45d126cfa5a625c4ab67fd8fe3932b6d1a8baa72", + "8636eb29a21ec37790caf07ddb84da33da76129b7dd954b1f85e0212dd3b4a31", + "62ba62ad32e8df1a776f9b6f1405c475f7ff9b0bf3903f5cd5194702e14ba828", + "b71e83c3e0e19fc695d5cb922811cbeecf7f08aea06f81b0b182a9bf3c9a8cf9", + "26ecd90426a17d0187201d73f653803f292eb70f9eb3a8fc3d3bae2ecbae0bb9", + "e5c86b5cc88947deae60288dbc533deb519c1cad2046c1bc7551a3be520c0a07", + "ca0d119d2c32ef343d2b2b935281b2584f9c041e4df59e215ecd8778c28b530c", + "5cee4753cfd6e0a9847eaefb02b8778c8400de08c316f7944664b199ea3030da", + "2033a7123f81e8d5acd4a13f74493a32896a1f9042fd29f2097d257b7e1f0a34", + "8e5fe3b0f2a5624b958bb934b7ba013bd49973ed806ebe4787524ab58067ca4a", + "ba4fdad44496f0a65ad953643c3907e1fa91e80e453b06925bc4cd61a207e214", + "bb44eb9ebdd1d99253e0bef3930521104c30d1ad261273e7784641cf35f15d24", + "7c4a5410ec44519c112fe035d9141ee297a3ab1a8b7cd0ed3aae89548bb9338c", + "981396a39856b5fe913a1deea98ddbcd621c4551d39428350f59c3008cc919b0", + "f02e967ca8b5c0090834b7a1876276c3029520480fc91c0262309c4a9b550650", + "1f7c4633bc2e733dc8e9ef60182f5872a8109fc94a9451350e31f3c40d63fdf5", + "e386eb3fc2fa79200d02f22dfd537bb57f523ce6a40b7deff82efb3c8a5315ce", + "d1e5084e4b70da5ea643357ba3fec799e2d9fe6ee379137171cee482468dccd9", + "e889bae88b774911ebd9b42680a44edb04f87611e1788e9f7d6be13017300207", + "9532716eb570b102e953c8ae19b1b25a88f315915ce13b40dd0eb7ae536d83e9", + "8f9e86e838c2c4038ccac429b3083250eb5a2c49d0b53083155904f13cd75133", + "cae28e256631534a3bbd0d3d2556ad76642eae4bff1311124df0ba31881fd557", + "64ebb568f05bfde7067822fb3e054fb0883233de88ca78aa6f53d19915eaf6ac", + "b37cee97d40b5c556d203066b4be61256d9e748592ecd35e5cbacb77be9c5e03", + "8312037eeadd15fc82f1bdd2a7b7d64db78e222cace049b0586aaadad9a69c2b", + "7b31ea7ba97e0d074b3930a30b8342c32ebfa06c5d83694aa56ea41091637ab4", + "e09ef282f803c1931478fc26d9a62dd79e7af9c460bd371724f3816aeb434e52", + "431cc07960e321216db7a1f74f33d98134812679466f6817b08585418e00800e", + "b509388a86358e763629ba35d4e2766bb4b1d84c9835a47cb4563c1499e6866d", + "02b1b14ba148abc0d3d24acbad06ca48e72847b3839c4c8f94dbb9a2987ae679", + "11a73d5de71c57e1ce6ae5bfd70255b2f9c65cc8debe1b8d55094791399ea02c", + "949c6b6ea9443b71525292dd20d18c815994b4539c791bcb7beb2a385484b0dc", + "298ab72b6d1defd41b5fbbc409996f2eec7573cad18e09b48629adca45448bb6" + ] + }, + { + "line": 99, + "lineHash": "e0587824320aae5582b9fd213baafc0df133c60b0692a954c6c1e3760394495d", + "nextLineHash": "6d28976d6a4076a08deb9593e28aefda8b0973ae929e5eb7b157c4b18e99a69c", + "suffixHashes": [ + "966ca7571fa950d5367fc8e620edf08b15f99a2ffae6ade95744f120686f59d3", + "df3ea7ffa41c7cd2ef6bde0671f0393f30bce96971fab3c976517bf1aa3a0bbd", + "fe3b8186005bd78d0cf389cfd87848501dd6f478f19f3dbf92d9099aef7ce4b4", + "af38f3435620c7c165543ceb546ff6158de38398d1848e201075c48bbf0685b0", + "d5b0aff48c411b205956eb365e9a21f4cd81da20f9e7ba17940253c2c76422cc", + "5f74816eb63af8839e05dc8a920c91ca39de503d2e046ff89ba8049e62fb21d7", + "260fb7474b8e3b9a21a91d76d6c8705e3d82a28d7f6b7fa7bf1a66380233f7fd", + "404bc57458afb0967fc28049d769f9f9e92128221be521b2c5249c9f58fa0edd", + "99b0adce06c82d57f68c5ced678a7c29b5ac85bbf908a4236c13d32802f17ef5", + "b59bc1f3249df4569266e3c3019d7c02157c2b3c0ccd7c7bd06e5294eb379e33", + "543f299c0a1caecc64002e7e55cec0a4f3f5cd58fa7d302dcf32c63f00be99bf", + "0b14eb81b710603b7a2966413fc6f025ef1abc58d9d5bad59fb9755e76f42550", + "7549ca4572964d046ab8550db7dc6e9269c1f6ab5fca31d594098d35cebc24f8", + "a400e16186cdae2d716634536de44a83f6995d9af8efcf129189649833fcb08e", + "7f7039bfe590869d2d0edcb3184191f4cba9e8ebaa52906d5e600b58bf96b73b", + "bd32d7912aeb98d7c6e37086a34a0698a68260537ce7add17bb111c5e95e0ea0", + "f6f76b3b2b3cd36ffbe24c1e17ba5dce30010b20e0351858417465642fc0d95e", + "5b67495ae5641b98f6d7a04a2f8231229f974b1254666bcc459e26336fd74fac", + "42b8e45dc36d43a99dd9e6544ca895fb1d9b39322d3c5854abf3d29f0d407357", + "5c63da0787e44b5a5255f5816773c56c32f7e34d775d4c3b91fc71261231c851", + "bc9b1a263ba60fb1851be26af50a02052a2afbc89f94396c5cc6c528cb9ff8c7", + "27ab288dbe5bca12658aab73a38bce659ee44b814dfcd32b4928f50796d0cc3c", + "3152876155a54d2ab4bfb95e42e125ca7b0ae6a144e8a5c47b94d56d4819cf19", + "b31b172acfcf7e2fda460a59af893dceafb740b946f4a317d186baf7b9074066", + "b96a080fd2f070e130e750c45692f2f884593f6cc8ff5d927fc55877224ad046", + "e7c2172b01a7c94a5ec8d288b1ce0c366d9ec821dac20401a13e00e9eb8dc0a8", + "daf3ddf203d22e2bc9c8b118caa79da8afb7d85da13de688a6212b34eadc7a41", + "8898bfbcc3fe07ac138c82a7a5d57ebf73ce8dfbcf4d1f253c0f394496cf1dd8", + "2d6644240e6ea1a0dce06ab17bca5c92440b761a3210553a08bef2c7738f178d", + "ecef198d93ddc411835fa479474490e8f5bea3f73b58f2a45d9bb6b2114d3ded", + "cc77ec13b43a60ea50e5b4e77d87799945887ff1371144d6138fd59b7e88bc63", + "44a1f127a813f5c8ddd7f99000a2429397b3c8c10281e22b85c93cf78825998b", + "eba448011aec2dbddb8c0a1bc6e22e9c17906071e0ccea7c3666f97888d86c39", + "5d0debfb905742801a01e7753bfd354fd0073cde7a30b4943972a5930351f363", + "93a38d9aa159f3eafac2c9d8a07721188d939af4a310fef6da396ecb6d0dbecb", + "07f40442e51255de7ac14733bd714e84e79bd1acc1d19ddde6a4f8b5fe7fe05c", + "4d98bdac15ab16f9c1ff78c96bbf9eac6c7164f81d0ade2068076bf72e44a197", + "4bbaf4d2d8c0e76d04af2ae186e04f25b35885f59e4205f38a4f210baeca9601", + "7768fc583a934b75cecf8fc7ad527448f87b3cc8d25f4edbcdcc8b56e21443ad", + "2f2c45ec1d4885bb53dd60eea5ef26a727bbdad7d3ec4c025dc970d6ab8fa955", + "997e887a11bd9eeacef1959214c7acc786000e2569034565efb124f16eeeb0be", + "6c1edff82524b4ee69f6280158a2d4f675dae5d884b751fb6d1454297920d334", + "4c6c7f4624d53ab7f862755b85b8f57cda4cd2c4b225a036ba186dcbd8bce120", + "4c4f7a851a459bb634a560dbbc83576ea5f048a5ffc3449143055bcbd15bb6a6", + "cbc792a30a34520ad24ac021614fd51e6dae82930afdafce746b50f332584ed0", + "201512dd2d6a57f7c05e7839b0cbb64ce7e26b966871c45efe8a1acf070225f4", + "d1c4526b6c1c5a83e3140af5aa629aae0e2985d889c05c2994ecc2bd905265a3", + "327ab28e03653895e2abd81b1f2336fbb9f9de50a4c51a734da688d0597cce45", + "4d5d1b377e633997f48377f4b646b53fde7cce3cf3885127f33ff90d9b4f25b3", + "0883f098174cfcf03dbba6d8abe9a3dcaceb5cc5ad098760da6285a36159eebe", + "8dac9227a4c8c7acfb122037b187a256395865192eff47f3bf91c28451accb06", + "572578cc687b8f4e8af440263b021103ee112d7de09b8feda8ad1d62ca6baf10", + "68394e63ef268a142141f1415eb4a489d4052c92969f098bde0af2d6205bdb93", + "70f631561a87317b526429bc7d19c3cfac0936b1b7d33a58a4623cce2f5fdb0a", + "c69218b23c8069f4b4e192943c6d0bf23f9f168da3508e51f81a594bdd59b4d7", + "2463b6a679f8bcaa0362c27dea31f0c269c4270df6944aec6bde08b42a5c6fac", + "e5a1d9dc88266c8f8b7d2f216ec9ae26a60ed4be7af2e45bd8504dde16d76dcf", + "9407a6db78294a653a59d4816aab78666785784f81ef768c48f6c787a70a25a4", + "2cceabb6fdfd92497e34bfe885e9ec9f2195e17b8ac0e0fd87a17d7d3cc7ad78", + "6573c0fcfdc14a48bfdf55075eb935406ef8a3f4f81f68aeda87268ddeff2fcc", + "418584dd6c852b1c24b8db07e7247f51c812e076690ac7ef152c4570a4ed8964", + "edee6e5d5c239c50143afda5539faa1373c72e77a0d32502b56aa4eae173bcbf", + "40f1b6faa1211d92b2aa34bca7a48de0024801ab37ce3b5102512156e9385b3f", + "0cd9eb90489be3194ea26e4f22fd750740e3e2bd1f6db78cb367e8af6e0d7a97", + "8995418058bf19e7141f91f670c200c1d2e12e518dbbd5d569557e5a3bb9d197", + "519e84d0d21f1d374ff284296199ce5a775079a24ee93c1a09827769fc3d97fa", + "cd29e5b2133b5ae3db783a6064fddb24c7bf6cc0f3de019c937310bc8f394ee8", + "366f301e3423306afa4f25265cac01098d70b197a2c9e159da42a257084a2ef8" + ] + }, + { + "line": 100, + "lineHash": "6d28976d6a4076a08deb9593e28aefda8b0973ae929e5eb7b157c4b18e99a69c", + "nextLineHash": "44463911d29525c105445e219ff2d4564fc5e1a802c377a79944ab792271afc6", + "suffixHashes": [ + "43c304c30bc8917f3b2537ca78374ebe0140d359182a0894a013aa3886333916", + "8fd149d4b31408ce4ee14a73b1f2e63ebef74b04b28783b66b6b89bed9ef4fdd", + "7f16a8b73949cff632f51885179187ce56bad89a77457b3915dbea090c5c27c3", + "99b0b559f6c090e1e9b47f3ebb43090cdcc78ffebc10985545696f9be2ddc7ff", + "e7db2f93f804f71c46356ba3db1fbf64c510445994cdc43b5c5c995b6a1f05bd", + "ae842776a2d34517c03d506061df1e1ebd5b486b67eb42f7ede039c5bb5c92a7", + "e6220ef09ec57ca874482414e1034728a8eddd4b45235e23c0f565362125b787", + "7d7339595fd3e85e1f01b8fb46a6ef936e5e1c79826ae7a415dafacf4e4f671b", + "dc008cb632b9c31d1da491d633bac50544e89cd6dd257409688ea3328153cd1c", + "402a4e4a5830f1af69c3b5e75c26b692ea2a1df6caf133670da64991678b0ff9", + "695e752279fa60618657a8209d4d07b4e492037d311ab49ceca17d3a102122cd", + "592706372914fa25564c17fd36184a03018d10746dfec4e7bafb430fd71301fb", + "2ee3db1dc0ef0080c9ac5a773a38c32e92d7fbb3aa8d09fd07ede6086c8dbfd0", + "c9e44b0226586ff8ed9e3b88dc9366386901fbfc73d7c3370d00c2c48812ae00", + "394e8563b9a43e1d7ea0282c68ae2b0bc0b92c8369eb25edac8e2b7b247d5fb8", + "3f096eef1934a07dbc8b4ea61ed62b00cc4ec15f2299e97757e8aae9494f7b79", + "9ef59822b119ad723c97506f14411a7792bce1f431e09fb945ff4afe477b26ce", + "79c02fa5bf17357e86da20f0b9547c10f8268e0aff1679c5b9bfa00b36491795", + "46ea19c72076da74c921c36f0cc6b3fb7ccecd93b7999c5a67feb759df538a41", + "945e907b44aa9903ce09461d6460c20b4ac99b6337a5861853d8fe44ace5df83", + "ba87173cf2541db8a9b5030a21b46e01cec04487a8cf46746d7deaebaa6f50fb", + "3b7558384be2ed662bcc3f666c4e92d24e7dd63f1232c5fcb25dd42ffa6d1916", + "9ae59ae4988ee9ec533dfee5d029e44105a818460ff8d9a5bbb0d20e542aa21d", + "3a178251a9cec0da3a1dfe02d19cb89c2a558aa7ec01e923c5423b85971306a6", + "d0cb72a68f8dcb6935e6860c4d347626aac280b82eb0ef81e96f4fbeeb20e422", + "f28c605453b93ca904ede9d0d66e4c52d950d8fd03071924e2ebc40a52720190", + "112798ac9a270e620bbaf3629a70c83505899cc1918d8d1989762f00a4af953e", + "f1e6ab0536a6621be02c84211a7723d79fa0a1a90ba6b4cb8bf428110e07ff5a", + "21bb89577a508ab2360016e8f9f0a40bb4ea33e6637e8233cd5f4cd66f665e89", + "7134726d16e82908d374ed0e45f34c13fc73aa0ce3189b20b03cb87924974f38", + "a9de2a023f7025ad271ef1a902584b43d2ad62d5bb35849dfc3e8b4bde3e9821", + "d0b39fcf6a56146575d845099e5b0bfbe24098157f9586b4b1426532cd77779c", + "82a4360912f3a7a07911ef5766208e5e5e3039f9f54ccab5a4b028155164ea40", + "fa26e24a303063ad253516099ade8e1fb0cbf7052b1f5b839327a33845b98397", + "07b2df670a181b4e50462200e411f07d6534c4fb3f18d1ef3d55e47cc71fa779", + "2ee82fb634f2273d4df731e8bc472c174de4af3286b8eeaa428b1cb41347b81c", + "e4941fe644e9f9295a963acf45184c5545fe9c835fdfbbe6276acb7255048a11", + "647c9dedf271d552014c65b12ac0d00e8b0cd8242d5ffe9ce5f68c9fa93c4972", + "45a45d9b1675ed5cac919e6edde7e58a4cf2debfde242ccc21fe85015377ba65", + "08e505f9dd71d30f9b0a0e5227aa516a80d1ac5555ef3fa60229a6d94134bfbe", + "b153422a41f65571e2f73f62270368b09245622c9511f6d6b376f26335369209", + "9a08386eb02016f64f102d6d7e0735c844490e98b456ce4f45a3fe547606ac96", + "bd9d1abb097924ae4d89c9bceb15d20a83c50d17be50eab2e03021019fc757d5", + "8a75dc9dd4e032a7d734f8276a43ba325bef2b9edce671f65fa613a350c166eb", + "4f69608ed57314d37fcb4e75ba4572ed51d82478b155230b2f0da7175ed9c81f", + "e198d11adf756af32b5d86636bd634b3b19aa9364f59b677982b8be3f032266c", + "67c211234a8c3683857d2a8787a8e4448b4b35593f8eba674ed0488ea8128d30", + "8029a300085b6c64e9867630f1afeb671f14f4ad7f6b1ae011f7c94b43f5fd84", + "bbdf38d2f149c37627af9d59506b55d3a074fcf1a888f6e547ee2907fd0dabbd", + "2f7327f7521b6997ac1408dcb4403c47afc1f06e0918db57735b6ebac05e12c3", + "6843ba638bb2e91c7c40ebdddc79c0527b79dbf866163a9268f9505d2727970c", + "99b1c79b342ad3e05dafb6c487ba317114ae736f02be87863b7a2eeb15c75232", + "375383eaefc63a99d6a3701177efbb7b44f398439ee539a1c80d6d38efbf1f56", + "6e301c555c8584c8dd7570c6447717e688b833ec0cbdfe64b84489b8e67d10f8", + "1b8b04c29db9cf700983331017801bd9e363dee741bd66922b3ae2acbe5cc5ca", + "abe86033f2a84f8c1f977c3cd287370261ff00326b85206a058e3f1c94de99cc", + "6f71b0568586fad837513e1004a85dcaf71381b076738b778414555a189110e3", + "5b2badbe28f7f996a1747c607b9f46e9a36d82d39b8fffc685ae0e8915c8c114", + "97afda7198de5b728ba7879151fede0f2915fc497a023d1019f4931834a4f5ec", + "aa8b78004f142dc70a77159c8faec7091eeea8753dac640a985b2e75413a1f84", + "5f7bd19b814a7c51c21400e06571d993f6e1e36cef876e24096f049e54a2f4ed", + "d72d7b218699a8b020aac8e75f6fe0a8f196681aa3de3669df5b914cfc15b3a0", + "9cbb615d9f7966d2a4600d7ab24072afab200b6b0966ca8413e3d9145ac5a891", + "c0154f718e8b3ee0afda6e14e7c2c0984749a9dfd57a7185d05d5c42f0bc2cb3", + "369eca5c2ef887876e7563f10eb37a710f2da9d9a8c348c4c1819d50e6d2890e", + "46178cf09e441efc4694b05949ace93e263adbf39f4fd4042289d318306673c5", + "4c10ead3207e93f950d0c2daaba1d3b4bbaadb1114598b76dc613358c521c190", + "8e95bbf1fb7ae212bc890b4697f22aa764ccc678392885689460a1791cb9e6a4", + "5d02076947487380eceda4e1a64ade3ed17acc9fda296233d43d0baf7a7eacfb", + "66e28c53e3b625503cfe759e0d54efad20943a6889d66fae490f0a82ef3a5f60", + "11b6086bc20be780df63e163fbf63eb905ae266d6fbdca78b40121ed8dff3ad1", + "30ad9c9564d1af93aa248661e81f346be20902e87e5530a2a3d6c3ff377b77f4", + "d0487a6122a284ef5996cd6c842ab571d2c1912ff313b9e706b82e2ad014ad03", + "12b367cdfa648004aadae8f73211bc5db5b3104e31cec3807dd2079a281d561e" + ] + }, + { + "line": 101, + "lineHash": "44463911d29525c105445e219ff2d4564fc5e1a802c377a79944ab792271afc6", + "nextLineHash": "707d97569b65c74a0e3a451908e0c5d81dc598d8168654477e4d4cce9d70c3f9", + "suffixHashes": [ + "baafb2e7cdeecff8bc071ce4087cf8921872a50d14e04deeb1258fdbd011d5e2", + "db4892c21b8014c696f2b332cbe4c00701a292fba30076b84c9ca0e07b5654b8", + "bb7be71b8867a920ed2c0b355bdb4f0e404fca99c4ddc23ad03ee14f6bc781f9", + "68027b4b4494200cc1519f542b40003832f7be9c162522e94511dd4a63ff187b", + "c3b7a4c6e58c964c197fd65334047f2626e8166928c272b3cb497de2cdefb445", + "228c57d1b51d6dc3f124668f84ad7c3da449b4a737374d98813c1d6d3ed41909", + "c832ce36079eaf886b29a5bfc0bf92746918dc99a0c71735b26fe97956244cb9", + "7ff664dfe5ce92b5c1e606d84aac00021682b971ac17c33d7099c1534205085e", + "4bd293c4bc75fa8fdf3fea2c182c7f9dc8035704eb1ecf89a7d54e63b995b01a", + "c6428d80dc6ae235c7c74509bbf638ae26048d6505348ce601fcb11ca350ff79", + "d22868a3a9aaae0c9b04c69800124cc2d5ccd3aed7cfb4f5245ef8179ba4360a", + "69a4efadac6d2476a15e0aa2d1259e5366dc99835622a11011c284abd49dce6c", + "b8bb6f8850c1424a638450d86fecf7b9c00de4e36219bbcf0e51d7444207fe71", + "025c7ae87f632fb255f36cd78804ad5ca45c2217b5bb7e16d26d1e60f547f8e2", + "0d704d8c817b5833d658315e68ade583a5cde9d4373611344a15142a585bcadd", + "71f80d83ba2f6f4d7e526dd4373cc8de3654e01cbf70d54abc65f53fac6cb606", + "a8a5d2b31a1f7aa91232eabea565f5c58f3b651219c7fe4df15b91bfa73773e3", + "c80a445b02cbb70df121d0b96e04b7aedb3467d44b4be47030abe39f1a24aeda", + "5dd436005cc279a994c633faf6e0e7f8e31bb6370ad77167d66321922b792878", + "ba9dd7f0af57622e62a292d8c00be545031398319eda40d779e3f373ce65cb39", + "f756b39f2ac7b9cad65cbfedd6b25a9e84bb061bcd250768acc4129410723e39", + "5d0e903bf2c4ce34640dece30f2da779aca9e03738b07539552a7a6875ab2978", + "e8ff9cd9d0619106ba168b0ebc8c2f284bc7254d6e26ab71a994d5a188bd8e1b", + "ba369239a87e8f49cab51952784dbe764a251d66059cbc23df796fbe19560316", + "4742f69684b4070a00b2eebc584f468b53a6af4ba5691dfff7584e60c5491bb8", + "e8c38ba47584158026edb7bf2eb6b2fbd216b63254ef10e10b8396bfee52c0e0", + "321aa4cbbbcbd2558c3f900eba92dd8f45c76d84bbeccf492d3c897bea589fc2", + "f572f27babc331c60dfaef97a2aaf8128f5d5553efa23bbb67884bc223697052", + "53bb3989a4375119a262914a18640ab2c23c87c21b5051cb9bb12bf37aa07668", + "d0e37aa6787c05fa07319b8572920fba74f860761b12bc9aa3c63ab1f05e32f4", + "3ef2862617e35dde17cc50ccbdda9b5362c33403135bea67207f0f931b03e80d", + "8482fb27aff3f7961c3b2d59a32ce72da21fa786a62abc73f99d12b2b345700b", + "67d02eb162b0a03e1bf7be181ebfa78d8cffc09c4e3c5f5d8bd9432f857cc56b", + "6087ed9b29e9d8372690433e1902b0a90178bf48b67f7926d9fbbb795f920326", + "a149036c65739eeedde122650350877f1a7b41563637a11f7831d70bd782489c", + "079ffe4ec963cc50335b58eec404c08c76ea17f65163f316f1c0ec7abb5d42a8", + "9b6ec87ea8adf954abade2373c40f1ad1e8d41faeeed4f14a9cb590c48cf2055", + "94c627a2da436a71b4bdce89dc6487618790dd0d777a538c4956f3f691765475", + "534c68ee2bf1d4e29e4786e4e13194192ecfd0b19e252d851f95df7eda39c20c", + "82682fab8b827abb92672eb10a54005b208b50bdbe2958c2cc7995af1e615749", + "59ad0d1c27ed4ed69bbea3e671ef669ce8fb11c20cbbd01ccb4632a0428fd3a1", + "f60307f300ceec2d2dc378f285988396a900b59b6305271e35d2dd6d708a0c07", + "377838c9d618b37245fd6289750bb9644d15b27c43baa982be25e5adaf73b517", + "b94afa34e4438d1b44706b3b952182ea41f098f71a27f571ad478d3e474b7920", + "8b5e3c240d762af855c88f82a613ec4dcd456d3557eaa422a02379fead7aef15", + "3977d76b7a3aaefd3af4f61f5072385aea27b52c9c247a372ad8e30bc5191024", + "94528b9e1798811a0de2ddd0a3b383d767b3e6e97cb152422c902b4ce6f95033", + "18ad083a4524d18f480b9580f75354ea20a393df85a70bf888315e85711d239e", + "143cf5c811f8be8fc9a955258f083bb9cc517cee97ad77e74067afbbc3c4d55d", + "72929cbae049e3431a32b7cf7d9bb8121a7f3743a96c58d71c9b9ae2c50691c8", + "2bab9a93935835189cb7eb76f671094126a761d91ebfb0d5e35f14a96fc813a9", + "629045849ee08b633ff6364a61c9c06173e240efc8dfae9af074a0904017f5fd", + "c20262518e001aca2325c6a68c68d25525ef37b686c60a7fd605fd2f4538f3b7", + "b7a132da159a5b51e2bc0408030baa3174af229d3787a65894720b728800c8e6", + "493bbdb7378bdd05593822064176ed8231b30c5f9002e9cc1d40c07f53419d5f", + "0dba9fb9b4969edfb3d7521f8c17f59d6079ccb569213e3376aa499df8293e35", + "bd4ffb6aad05620089e7ab181f9df4458f4b74bd120380c5c4775698e0350b94", + "2e2f8cc03fab7711cec57f961a5e7e52b62653aae725c6fcd2cf7f37d233335d", + "cd06d43e0a8c59cf2d0d61273ab84d90786abac35d29b4dca38d725035f0545a", + "2e3795c897f30aa5fc51e831615457e4363b7e56252246a179b865788ea661cf", + "f0a4fb284332ab8dae13611066fdae0e6b5c943474e4332326d719cca98aeccd", + "23b9f841538d63e9ee24b60a6b50563fe40649af57943f601e2e672c14410758", + "d41599c2414e298f34b30019f5e3f9bd4f5ac4762662b3d3934fe6361f7f05dd", + "4968441aff5025b2e5a169ee8b7df427f9bb482217413db9f270561d3b559d4e", + "4a68b41aa45e02387c32fccc66697255bf4af680fe33c68074c78de1c996f785", + "95497a6f798a07c85d264d6504f87992c8dd4b7785b96cf362682e06049315ef", + "2fed1da01f624473c889a564f474a59943e24ab6d953257e8414fdd45c7b8b31", + "3337679ce341206020da3c01d0be647151ab5f674cd4cbb74a233a8edae71ace", + "647bcc229829b5012ac5a0981869eb54b71b3954bd7cc56bd372eb7a227a3737", + "19abe3b69ec791b67dcd060c0f36437fd24d5c7909d2052e370817b30ae99129", + "84489c7cf279d2479778124c28b6e8c58c178c034e2137965db9d56e94a30624", + "576cc04bd8a87ca46a8eee849ef3abb60d3657684230b069da730a39292c1d6f" + ] + }, + { + "line": 102, + "lineHash": "707d97569b65c74a0e3a451908e0c5d81dc598d8168654477e4d4cce9d70c3f9", + "nextLineHash": "408723e1679758ded7174e1c031098a3f87901175dce9bb0c90fcf35735ecdbd", + "suffixHashes": [ + "e710f5c6f51db5b8f3c245d7aa46985aea78b7cecff9d231fbbfe1332b0e80ba", + "7eb509b681c314e34f37e9a25697caba3e442c484237ef6fe60a0fa964049aff", + "91393935b5dd8fbb577b839c2d1c0e7b48423bb58432c0d6a9d106f9974d1e4b", + "e6b3d6d9987b83dd4b6c0d52f6c2c30b1303996130a2bdd684d22823e12c4692", + "eb24fde8e32b627bce857d97eaeb0eaf565e7f7806078fcf7c6da032c845bb43", + "4dd744997ebd8546cd26c0a0647975589f823603338e8516aa4e8ccbb4a2eb0d", + "00e0cde541774a5ce4906b59f40bea908afb028b07b3ed603e5b4f1525d6461c", + "9f65c0c12a54bcac31c732c5e34b2040ff7a7ff22861a6f5ebeb95c454a05bf3", + "4ad3cc1de8032f1d4b8be385f4b57f7c3699dee252f07f1bfeb7c9c188c7c570", + "4eaf4d0bfca96f7f2da2608dc428d64b33d868aec487a5171aa2f8d510fa4149", + "2e3c35126e864a4093d7b14eeaeb1000a77c83e2ef9c0f6af3ffcee3f7a4c1fe", + "b7d185de8d7d7e97bade519e6e2abd0d56813c70f94541045102f475f9b09c44", + "9e1ff40985655463b42da775eab91fde7d633cc225f80a53e92d104dcdfff293", + "9b35d8ebafe8e653c0c9189486b792d20bed521cc9f049164bcb858512eb79b8", + "8a24ef2dd84e66e19655f7fe49e9e168087ea5ebf2d1be8a36004ab006992288", + "d537f1897783b12892b64acfad829281c7831ef43f1f574cfe63ffb75b7fd9ea", + "9af2ca4f69b43e68e7259d5f74888c65179d8a2efdc74e4a168bc10b8d91a208", + "dfbf7e33026552cb6b35ffd2cb016c1e0537ce630276c7e44575e365d7d61f8d", + "4c1b2a265df0b75a4044bb2f7b2f9a3456792c491d83867fea9e0e809849f8e5", + "5452aa68294a02c0ceb00c4a664d7a958060f73a08d6c6f21395329fdf81624b", + "246c14552664103c48e1d700910d6f524d81e53d9f62630315eb4747c2be0c06", + "efe4b85c60b62fa436461a5af08f044080ecf27f3ffdb317090f15583fdb39ab", + "161052ab76684c34a78b26996829dd0fdc23d3546c71364e64a3fb96e751f2ac", + "894ae1ddd8b17471fe9144af1365686c01fd8403277637b334b7aeb60a2dbe00", + "3f0a375981f0dceae2b8cd2818ce4d934f6c8cf981a08a9896b3291ee288a8a3", + "016ea7624165576e0a0c24771401fe58130bfd6417129e8067ec55272669a8e6", + "6239db28d58e5429bf5fb326b99a3bcc4648de34fc10e6e238e7f4bc48454c9e", + "6e697aedfe57290349db5fa2a4b0c236af64d97635c1e090e792080640592b88", + "4fa69f84f3f3c16745bc0a46b0d8ed2a47013bf468bf8f31ba5bdb3c45db24f0", + "bdfaf6873267d5e8cbb0e4116de503cc2e053ff7c7f6e67f9da32996a3ac8e7e", + "0f786f162b1399d68c44c6179909c0e6abccf3dea0899392d7f69f07a781e255", + "324ba4cbd58b4fd36d751c6ac96da716086c4ff92d49e5e813147624aa728338", + "688e924614d46d08fe7e6ad5989fb8507a1acec15cf90df61f90d085436f0ea0", + "df2aa85e2c718ebca3b3fe98ba89f685ed018b23e015a9050632becc25a46543", + "01ed65170e024dbd6d7ece341f5eb7fea877a28a338545ad638b14269c15db77", + "6bc889a6f490366a3dbc54862bdba1ed1ad0bc4af4e8f9a8a2ddd53a9aef3f58", + "ff9df569f935a6b0b9a1009f47085424237a189418f65e18b80f991dc40c6547", + "c6d23d06a3f69963922ac79ed100e2c9d5d775d93944cf9c598623d79b7cd3e6", + "0737419b4d86dfbcb194586631b139c12532e48da1ddc018ce2b10added94a26", + "ce061869f00dd9c25368da8fbc7fd8c3983f3d56ae4cfdb4cb4e707ffbec01a0", + "3bfe8e808e5c26bd9e4ec5e33f40302836fcd4513d0fdc9502ae75776e75e65f", + "f309ceb054ab2ce421d2fc8a8dcb064c72e837b248eaccbd8278cd1075144e17", + "631ec75c10e4f4c6b4f54013ddf4f6a470e16fd9ff99476ca3a531142612c9d0", + "51e0e7d11f9d2cfda85fefd92dfe234f5cfa53455515bda85eddda79dc3922ba", + "f720a1195ce661774a35bda452406f67dd05cdb7569d0303b5be43140f61a905", + "55b996d4785537c3c017bb24454aad81419db0700927e70a64ba7b442fec338d", + "6fe42d41b0ba6110252a660ca4fdee319de79959c9235a96b52f693eb2bed7d9", + "2521c5df1e3d6150883c167b21f31288cce81b957f14bc62c5f2b46ccf13bd29", + "9e098bc23f6848cddebf38306d87b62f8abb1b2ae2d41e6837f185fad45deb19", + "926ff745847424a2df8fbecfd27218c97651d7881cc0fefc0f2fe2a223c6ca27", + "3c08e314b397307c13c6d0c0f62e1d1e8bedcd4f88f2d60bde3e31359da7d108", + "c9eb0cb251602806a61ec4f31887935438b1a2a0698f220e396ea1ae2a9ff392", + "25b974a8633f0076278d42941b73738655dd33a93d881b14f9d1b6f277452095", + "75c2d7a37e3dab440f4efc0a4c824c2688aace178ce165809523393b3bb69739", + "727d19733e12db2eb5c5e50456133d5990733c94b2b16b243a7fcaeaf9fe27dc", + "f0aad8ee370a8cf208e5a9648b2e4f91c3f0a19d72d58e40f464a42dacfb22f8", + "4fb06b7bb72c84fe60df1300046e69c28b1c8379a7114940add91be4db275d44", + "08e24aff1e3fcb1c80cda3cda1156ec22869cf286c1ce1ae1f4462d85d9bac56", + "b5a3e3590202fc19a4548ce7500ae97f5739e2bd5295297cbea9ae33e5c091d3", + "d5184c58f821c4170f82819688f0ff48811ac143204b4737df9d60e812ff9947", + "f2e576abcd8d43da8d95f262f17001e47c1d193cab02749e858f2fdb0d7c8ebc", + "505a73e5cd93bf5842c7de6d671c39e7cc071c75f279a899eb379aff3a21d560", + "7453ff348400726187d79a0b8458bcc6c991bfefa77af5f98d1d9bdde8bfd6a6", + "1a8683b0b22e1d5accf1565ee2e451ee8a30b60578109544482f027f8bf8dc69", + "3c583e80d62548c098f4bf6664a5857e64d5a54569e0e79f5448bc7aa2acefb6", + "28993d0ac6e5361782cf005e14d10528cad5a4f1f0338e78f148c0c5824a3698", + "72071418cce4fbfd2390d03963fc6c71bcc08ec5e74309bbb3407563e9534680", + "e7ea881a7d32abedf8cea98bdf95f1eac8495263ba3f7282da766ca2b6d5702b", + "b53b4e181071ec6a53a1723d1cdeea3a67c719cec48c36842e825920453357ae", + "2aeb155f653dc02ad3ba58fd08da2bd48695ab8b5e24bd7a5bda0bc006246573" + ] + }, + { + "line": 103, + "lineHash": "408723e1679758ded7174e1c031098a3f87901175dce9bb0c90fcf35735ecdbd", + "nextLineHash": "414e1c6c29aaf55f2caeadc97845e40e62b80b8d4caeccad14fb8035132ebe6b", + "suffixHashes": [ + "05d41e3010babae240c5f2b30cad1656459433adb4c1b8311f117c76cc845f0d", + "7f5342622df16c95f951f980570ea9ba95005f4a029dcdbc0a64136440a25968", + "b9d8c72ceba2c98c6a26306625441ab5df84160179cc25e1c56b72cb54b27841", + "0d3c70c0e98809c50379008f28e8c536d5ab378f2f4537e14ece30288e0bc597", + "3d7247fd94a7684b68d72de422ddd6311c248319df641a90759de1bbb116833c", + "c09c104dd770dec7f3cfc76e24c07fafab1c81f13f751ee73135f55f24df1ee0", + "e66d0009d8323584958700b3f3b4bd58c79b1bcc9f02c309add48dd42cf7c6c7", + "c4e53489fbab9de602c274222832b9d71a81ba5446a22d0f6123480c9ea52bd9", + "707cc1c61b7a1fbc634c7cdfb9766e7c8db02e7c9a4ddc143af608f36c198579", + "70ab1056fad2a5332b54afea9277ac5fff91564e057019fe3c46ac8b2eab2536", + "fb43f8023b0accc7cc6f1f56d6029a69550cdaa63ef84438ce4e33e970839939", + "1c48ca09d46d567f0c324494f5c7d4fda5cb098dd1dd9e4315ac8c023e5f2d6a", + "883178455bed5eb257104950d00c48a517e0e9e1be3e338e70626088acb887c8", + "c58df6fca821c1874c235f9ff75c182be147268664451b624160207c0f9346f8", + "43690dd08fcd25aef50646f2854fa11d6d764159ef36a44d87e3e456157d88aa", + "a7c4279da86094c115670680bd0df406aa841ced8d6272e003e24e38eac2fca5", + "0a6da42da0f38854c14d8461612dd4e12950bcdf487dfbd7f7355f2dde310354", + "32499a21187863609b297a354668981c9be32d04c85c2f21163e8d653b02c339", + "f18b282de1e3a11d68bccdb4f0a1e697ed877f70a1955a0f93d76bdc8aafe49f", + "389b6fafab9332823efaac760eb294ef1f313971042d6a10beb3bae5dce78e73", + "01413100820135f666a03d8ec6311f133d742f85cdeeba9e19f580b4d5fec3c6", + "d24645cd16c071e587e363f0910063b5db0f59f7db6e46c35f6da989fd67da35", + "614bcd0e119d0922c6894c76d0df36ca1b30d2ec03e49c7fb358a4fdbaa14a6b", + "b1ca51ea14e73b80aa0240e68dacf9d1fbcb4b35f517612cc218b16f166f1700", + "6f10e46c8202940af0c87f1f728ff2a3c5836876fc7815a235cf55a3e1406fdd", + "5352b6dded4caf7a42818fc684143eaf6e1921f51cbdf62aae8737392250bf09", + "190bd6113041fcfbaa54655d1af0f521835fea216e7f4fd8cba59e822bee5387", + "f779e0732db8e68c0936e2faa720806705acb97496612341a65eb4310aba53e6", + "3d4f2eb9e7beb85fda4e1dbe603f7a3c7af4fae90835f16d3dc1c79f5dd9fade", + "32b5187abfcb0f8aa9f0bc4b34b0a10076d49600347b2627f3bbe1f4c623d789", + "cd53ec7cb9ba21f8958c2a4079c218f0b82b58ea9ef487367f3ed33948cdfe5b", + "b06ec61b5a06e88042971eaf92676cdd1845f09b2962f7d602a67a6bad438cba", + "2364b6abd1b11efa96ffffd853e3c59b7e4ca773d895f70552735c4eed3a4df9", + "a7835b23233c0e4c008d56d39a61fc0620731013230623cb79f0e67728aeb83e", + "1a1bcc5b4584e678bf8f5c85ba42848132c5cbaf3176049b0cb87b2d6e7e1c90", + "e1d4062f81e0bb010ae80d15a7720c4b89bd31fd78e821af7c0c6da5070178c5", + "12151df33ca326e9f5ef1d360a301de24102b23fb7cd7451edd50065a36c0969", + "87ddd2d00bc831ae97dfcc0af1f1e24127e43f4cd402dfcbf439d47730a7ba70", + "f34596554a9648eb56770f510934d18dda9fd38464006a9756436b429e78ced4", + "5ea6d83a58a39b5e830cf955b4729a889213ad81d58512d99b20fbbd9205c1ea", + "46a6f8156b47161e48e35620271b67c31a714e36adb426bdf1391cc91a216a9a", + "6ea7afcd37a7236f014b254e34ef4bba7dbee52f6d02586b586bb3c80dd7e898", + "cb739e742266be8e25445d44472583c4cbe29fdb00ee8f88b53552084eea2343", + "9ad18fe71ba72a1026b45c447e2c35ca385980faa402307345002ba06cb647fe", + "2b353f3a7ce5b7e0ea8db62c2b407500981a66ddb60a82a41077a19250b4c86d", + "a8dcbb622bea29045435f832e8a19b0b25acd83782a500994bff4baf03f9b33a", + "46938bb8d43fd7eeda551d9c8076937ae9e7772f682e69afe93856dd664e40a3", + "2b2f4ba1372da1b5679dc7c0bbeecc523f3ef82842deed1247821d42110b80ea", + "95e5702fa60f524007f12fbbf608b3fc669792cf4cd02b1bddeb5efc977a0bce", + "2cf7fb14e9fcbb019c6cb9e43181de83644499f852d77e27f80d52e1c68f7ea8", + "1b6aab26218de62ccf5913b5f93d6049a241f7ed258377a34a5c69ef3442dc23", + "625f9ff33a1c1743496fe44fdb7217b71397ae91bb6a50aa24826714f45589c7", + "d21e27cc3a2f11e188771fc3e7da9ea07c621b8f2c99d60d2fbbd42453363dde", + "6aa0c6d1e4fb361f795508d9e7a88c2808c3f42df1e4558cebb1fd9105964ba7", + "39759a35281dd6e7dce12601819fbaef035a9a29c10c8b86d1596584de9f9797", + "7271dd3ec314d111ca65d914ae42822d2d8d6caff4cd7148dcd775b8d01dab1d", + "bb4a422b8b3ff3275b26f6cd3ed89c076cb3945ace9edc734068a23e64900757", + "bd12f32539a73aff9134f60631639276621556b9576cf74503e1afe3915e7be1", + "1aa1ee1b81742d9e055bdff388fabf7bdd8590aa2dd87e6718090896b7394acb", + "e931fb89f18fee5196bbbaa00c5e9f12015b221e9791fe5d0531aefaeff3d427", + "63651283533d495afeedf42f2678c34ca6d71c4300773d7deff6b71e8213981c", + "accb6ac4a7128649e9f83e6a6ba879a319d7f88e10ee63b6460d04315f28229a", + "bd9a25d8ced185547c31837bdbb45390ffe9d766214f0bbbcb0d443250ecda17" + ] + }, + { + "line": 104, + "lineHash": "414e1c6c29aaf55f2caeadc97845e40e62b80b8d4caeccad14fb8035132ebe6b", + "nextLineHash": "c8e02969b2776377290185233fa1e4d3e911fa3652397c012075fe7c7073d315", + "suffixHashes": [ + "0bfc1cf50a098a20219944425701d2d92a8914a65c0699d147464478f7ea5b73", + "75e7efc1641ad642c260cc200a04a0cbd7d420bd6e5d3b9c212e8464d2f6d8c5", + "ec7865d287d41369ec9dd7e1d99269712d5a4712599ee88486c43e18d7261cf9", + "8f1ae363423d019d9585ebdb2d9a86bc6f5937979e3f81cecb04b4f588ff4794", + "b719e254ef5f929d8bf1d2d739e80925493372b802815ead43e1fe01b646a517", + "2205a2648c1be99f3a306182282426676fade42f068a8e99a70cf9190f52e090", + "7b7f8e7b065e13280a559048ec26604b3517a236680ef7561ff2a9b9bf4f6c4b", + "190e01e3310b90396625f66d2a36e99967b03ae52b00455546d305fe0afcb5ed", + "26d928084cf7959db624c3414dfc069edd3d4a8ebb566e045981d970d28bb5fb", + "be6d660ec356767ef246b4425463fadc2e646b79bf960ab3fda00f6533826381", + "a9953506b4dfd486b03957fa12d52ae5c83c0e2c32c442314fc794dd3c1709b3", + "fb984900a290f0841334155c2038eb538b491640cf526984457edf1c4ca1221a", + "970fa9a577cf291cbad4e81cd38909eafcf08dab53cd75d2d370d529e1f0772e", + "7677cba484a7259b7635ca7b6fe98793da031242b2bd02cc43dd032af2215572", + "1a38a70a3643117835c28fed2c5ab6612248ac2f59782a1311d98e23435398d7", + "4b067c810fe4cde0496ab6455a944eb097f4ca9827ae68717591c97b36f877d9", + "172abfc771f7e64e613339bc23265341ea3d792d71e365f51edada4ee3ac9907", + "d32d1cc45b98dd1ca16dcb3dd0bf9acaa6c2cdd285d4c31eebedf93b6ab10af0", + "689304535b02cde3420da3629169a8688d4976cea7979595d1e664763b08d19d", + "3880ac154310e27dfd777212de54b33a4cc9f9a7bcdf9684f3fbebae95b40f42", + "3ca44d5d95862c1cab5069913cc1ee7bb2d041a151d2ee00b7bdd99382f8c8c3", + "e0c951e2b6132ef867ac451080e70e3040fdc53eba9899055828e63b47dc12fc", + "0ed2a12eb40b26d40225b6c37291f6f3b963b4d37815daebbccad1668f3c82c5", + "7d18bc1a385c5d6ed9d25137c54cf6c9a23b37219092b5717b846ced523b00e6", + "f2b684a9ec307669780da98c5ac1b300965956e9ac8910304bdfb5bc10a87f1f", + "b11c36a04b9a33aae61f4767caf8530ffa4ddf674942cd2ce793f67b9a9e79cf", + "5f1806d8645dce77c4c5cc71ba14b9be5a9879eaa4d1298ae23cd0d1bf7f5086", + "9e0e781c64616cac41da72fb6c05c6bf17d6461b0fcff1a60901a899e55170ec", + "ec3cf6dc8a4cddcb3d852b54eb2c2bfb8ecb70d85752e953e8f7762fd26bbc90", + "904d5877d305dcfe9064f04558bfa95fe044235fd8a2d2105057013d3f91d3e5", + "f428b0b372c7285cc0b21b60d0f54eaec39101a43b4d1973f31f9b7ee53b9459", + "75f366059b8b1265c7c5c2b4e539be0551b82f9bfce8e8d97aabe6a1657ba6b8", + "2646eea357e6681949ec6c6346b2600c7ba5622ca73d808ebda7094dc5731fd1", + "2359b2ca34ef52e810e33412d5d79f42790a424eba247068ab4f0a7024684149", + "1313f17e4e13554d5c698b51fc11e317249af3448218540c8a13a2307b2d79c3", + "da300c7201d230d4be77c2133874ed42a2dc63fe6d0e1edac7705a364d5e0655", + "674d05c4dd25486ad9bee019089939697dd8a5c964d9462d82e6ff20004b414b", + "a8e34655b2e50449140a2063a36407d4de587ebb495ec341d2f301535e32b7fa", + "de87c509d08f7b83f0977ead19918251a09f7eff133f6757eb5c52eff66361d6", + "ad47db51d549cd4753e304cc07bcc1f99bf6656169bca04850e716bf63eb4247", + "33e6d463ad653ff0ab6af3b8b03f66bbbc2764aff0aed9361c42c1c3d50edeba", + "e907b7b82d4d0f3fccfad7886c2de6ace6a0be6afb8f9d5b172f9b4fcf1601a0", + "4cf90d63a2674f5b1550edeae719702443dbb5efa510281b8a2f2b080e410224", + "306a9bff17087aabb524e848c9f770bc1fd5c0cf111a9da835c9a2dc75a13e53", + "96e458e73ae7bc40f2191d96ee43c1d6f7b92a6e7f643154845ce3e6f3ec19c9", + "41dbd457831d9d0b652d1ede776e30463aa7351c5860b871440d73083722df96", + "94532b689e00c726c0a9ca5a22447495ef2a0787df230e47c3b6279b70a393a3", + "a8599f50777b6a9cd5a34dee3efca7ed51945e229c7e2a4eebfe348ca7445585", + "00b10de211097f5551da35888d93febef213679b55c49b1963c0dd577119b240", + "e90b02e5248679957a4ec126f252ef5b8fa3d479eea543f8a492adad81634344", + "b86d4805f1b6a2652f4a903f8d708515698147bf7a046977e0d0e8620ecc1452", + "ef69e15775189da3cdaa757a281afd3ff0153ab06de5c48ccdd9666fae9ac287", + "a37c3cd5b8ad49e8814dbaf8ddd99a5959594c2522de18f1847bf48479528586", + "2c367cefb2887e71a7b3afd8baa018c46f78663b664c51f0e3871da4ef9ed795", + "79e798d120216e32b8b600635897e95f2cb8660a1d2fa6f4ef77d11016953c9e", + "578e6e17397e245219dbf33814b07764013af7b8a5e14bee76c5b0394d850dee", + "68e8d70d634cc0aea7510ad935825839bd9ed748eaccb192bc22229f78e30c1e", + "0161ba9509359a20f0cec24057e322cb54f48d53c7f68c30b07ece189ac11ae5", + "b42ac9812a91ccce77ea3e0f1e065e79ac18fd11dd215fc8ac6aa043ce67fcd8", + "04d09832c4e26a8f58cd4f235835b9b93f3022c80d39dd335a6b29c910d2bbeb", + "691e7ab6ad250fc27213e4a1c5b7f0a9b8d96992e67884ed9cda4999ee51b1e3", + "79ec583cb0476613d71d40d22bee625ddac9255392ce90659cb50e9a1d70ecc6", + "f60937710b3475a65bcaf9ceae89a4b64fc5560b65cabb7f3a9b1c2b7465d103", + "acda89e04f3b3075c5a50cd4133fbfa493da5c2e5386e5ab974fb807411b7c71", + "d90c2c6f2b200197ac78e392e3ab7a3b7bf80f1eed9e297d6824ad18573c31ee", + "88db5d79e5f66d3d79d88dc437df719ecbd21fcaecad0706ad30d8abcc59a073" + ] + }, + { + "line": 105, + "lineHash": "c8e02969b2776377290185233fa1e4d3e911fa3652397c012075fe7c7073d315", + "nextLineHash": "0aa487e1deeb804b33afb0e32842f46bb33fc9e40e4a740be2921e0d986e6a22", + "suffixHashes": [ + "a17fd4b5b8687122e23f3a200f36c416c5a9e84eaba6751117252399da4bacb2", + "c549f758c42c4737b0911cbed2d5192b0a93dcad4ee0f82cafec67376b863fa1", + "40558501a2ed5cd3e397ed74cce30d32db3850ffbc0020a341e29de3d7ea7e22", + "fd6d4ed4c3be969c583ab2955a3456ad4cc559a72abb863384c624838131a762", + "1a157ff729f154646e4d4b0edddfce4ca90fe7dd093e733649b1066ce7fd3ca7", + "9c86e6cde16a3b342379a70ef3986dbcbacc8f301976f5ee919ecd09c86ec90a", + "9316978738c04a116e84a39f239f62cd7ef5e0bbe6be8109f750e748d9235fdc", + "7918f666f711e3507624de54d2e7996d42ecd5ab9ba9e866bb6cc3320870d70e", + "2147f126c8e678de9c8d60f70521d39a35ea1f2485b4dc0108dedb10717fe22e", + "e64ca6b46f6b51da7ec288847ba3a68cf71ab52d7fa31de112f08f4d074be342", + "cae3f94535ace37cadd89e0f5fd71982dcbb61133dcdbdd5176c6f7a6d2fc92a", + "566aa7d87276144894bb82d47c2a7833502359856a1cf860bd04faf2357ae41d", + "70b16201b95ffd33ddeeb6ce1e5f191ac2319520b90399b9909f16514a9296c9", + "2fbb348308bc7a960ae1ad851f5a09a8223989d04756226fc1b86007198470a4", + "0499726cf1277133d68648988c6e31059b11b5f2d174405fb16cc878881bd22d", + "3fd499745e637fd23c59a48584017a1f00eb6e67cc50c2e121bea91a4a9d512f", + "7daa35942326e7edec724a4b0d5d4040874497b9d60146c02d87408b8df6d3ab", + "59ea2dd3cf04f7e14c660772dab3b67efe4f83cfa261a5dad3e5bd662313e013", + "6358fea896811b84c7eaa28860a50e7c1ca9fc6015487853f70de424621af044", + "8063ab79cc1fabd4afbb3b6e87d86402fbf1b0cc39aafdba16486158c1a03809", + "a82d436d6bb0ebda2e48a4c525153e24aa12bed75db8318be2af1d35f7866903", + "17b808d20c842a629c11ca616de08513b56a3a4db18f8be615a804ef678dcf8a", + "276472c7be854ed636e458794406ba86cc33d9c739019918383acbf7749bf361", + "237ac6cfcee58a38496d2b520fbee83f29218501e71e9d03ef0c1c2d680681be", + "dfec53b450e4f9da6a43e5b717b7174707f5fc3f7c2dca80fc10e80a684543ff", + "0b7d18ee0cfe9019b2656647fd1bb2da746c1c50b26a8d0032b14ca42b4ab3f2", + "26e63d81df003384d5781f7630cbca99040f32b3fb775b01526b2d4881bd4aea", + "39afe092fbc75076a382120b8a4b85852ae1bb786733981c7a76226164933000", + "9b4e8afc1bf7fd3f65e6a9405bddc0820355d8a65722dd7145342aa205219322", + "54b29e64dfe2b3b8db91c3343d62d3fb63ff54a5ee39a76c3177beda3aaffe4c", + "dcc6a93432348bdefa43287f3861b0032bc0a0210f971d50d8b2c4e9a6a7df4e", + "72c16240af4c5b9ea87c712974d60a5df31a46724ac14aaf4ff800b81ce4fac3", + "99d088ad34905ce3aec6cad5860105b701fc61a2d14a486605ac085e11688b14", + "058511832479579a08e516b4e25b26f614c7451791221ad0755fd5e6e0de1192", + "070f0cd75a7a922330de791c13080e50ab39abb5ec3923712c5dc903f8b65cd0", + "cef533a98fedca39010b92ee8055bcc3836420a4196f5f387c492b8e43a2f0d5", + "2b962927c1d7de12323cb79e9bd675feddbe853f072f4d7bdd3a97d084bec397", + "d647704ab69701512d8187df5ef55f95ff8dcd39795bbb503e4bc264d5478b40", + "5580ed457a5b1411bca143362efd7f6ad7b3a046ee1f242656446b91d3be3233", + "5ed3c9e44905e560c0db2e832c9fa32d90f6fa9ecfb1ab219bb7b706cff99e18", + "b10f8af97a8398e9f7d7f005f985bcde87a5c65329b8fe36c2d88aab63d062cd", + "284f52ba5729387ccea4e6707e84f411f73a27dc0c4f1ac8f72213dbff99c6fd", + "bb0b5415a91ce35170385cb324b962fdb6d5644f163c018d58326a4b56603d0c", + "518ab81098ee0810adc904fca035f3c9ba94f377949aea064a3b132ea65db36f", + "73331b7f39e4c4ada5dd7f0eea2ac314dce6458634de1ea578a77e7ed205e3d4", + "8b1ee0ff7abfccb6c1153f75283acdaa0a9e15c1997f70b8aea67382633bd2f8", + "073f13755c78056b97fab83601ae61ebdbefd1a11fead4e192f93d30a4ee9c48", + "0d889102c75311704ca8c5d088a8c22f6ed38ba5f2991729ca20858ccd1efc5c", + "75c666813405283c170074019a5b416fffd640b6c0ebbb33718115489fbd2778", + "110542b4deb5cd40c8caaf5b6b27504f47a788a4575afbb51eb921a860127dd5", + "adc5f358b3f7316dc6f087579c8c77ef853ea27d5380d0b6fc7afc88ae6f0d28", + "932090a9d9d04096c569d884ef3a143f82d7fffb799650e2bd91fdef86a6736b", + "ca4f5c84f6c2e1532fbb7c8a193f1c52dff862496596d504cc5d670d8e0e44af", + "a6439ebfa2d2d39053d615240efd49915398a0e97f32c22e2fc7f13da045f8ca", + "fdcd51b5989e98fd4ab4b5c5f5ce858f8785dbd1bea1645644e8acdfeb09d9ce", + "1611469f6b13808e079c818a63de4753763b31c0b273f89bbe3dcb628d83c2d6", + "20a8ea133de9c40c8006e343db555eb6eca8c491722577d80320283974b8a099", + "8284c4bd2345c6dd0b56631ddf05f10637df42c8b36e70ddb3966c1acb7dcf9f", + "a5a6ce64f72088d658162a86db6ee926e8db0965714b1d9e320d7b4f705e1ad4", + "cbeba359233b5e10513a47388ec49901b4e9809f7b4ef8f7eddb16e79e404769", + "bf1b695b25bf9b3f6b60494e0417e7e887b47eb4ac6b697479199982b872c6b4", + "f4d196b544233890e90b7544d3c298c4a3b25d445b32e928d511994820313dde", + "29866d13205b9fe1e33bd320b1339165d1aa9775b618d5f2550cce68a545c3d5", + "b774b25e8b9911f922f5883e667909a6ff935297bf8a1a2cc890ad3daf8699fd", + "e571d2a405e2d3cc3579122434f3969eb9b80569379746385c0c6997c6b718e5", + "9025ae6e5b114f55f01be942eb95adbacd85575a9aa1d26f23a13597e886568a", + "0dd61cee2d7f4bbabace2eda140a6d84901fa6af8ca1e7692ed90aa2dba0e1e9", + "6d153ea5c74e2e06df915a925d7c24b5cc837d44f41c1df30234ae787ece18b5", + "013efb4e1eb87cdaa1279ca89e5cec3b18380ff784d6fbc2ac83aa66e415c430", + "5c1f07c87f0ed0ed7ffcec76c7d665ff09bed72ab25176941665b9ea9883174f", + "9a232b47b84126acf2ecb49751c4998f4a18c34ebf0030efcba7faa95833e075" + ] + }, + { + "line": 106, + "lineHash": "0aa487e1deeb804b33afb0e32842f46bb33fc9e40e4a740be2921e0d986e6a22", + "nextLineHash": "1855357059e6a2a14a211a1c91000807ecc51e3065187ab67c7d633eaf133eae", + "suffixHashes": [ + "20f29c90c36e5e53adee15c92c012f0cf2d8c077bbf84c2b095c4d4fabec68d2", + "25723235dee9080dbbb3b8062eee6fd2d812a6ad35c2ffd372bcd6658bf39e19", + "bc74457b0725a76913b146eeda24a981c677c514865d675bd330c9dc4ef5b2c9", + "f44921f40dfa5b3358ee13e55f2051b963f7c64c01623d23dd98bb01457ab2f4", + "0eb424f4fce587b64988b32b472effd0c95d302b51c802e965ee776f07eb75e9", + "d925fae25ea42c65a4fce1e740fc8e3549b9a14bcbaf7fc939ee651ad8180f48", + "f9a86c8252fb17a9344aa0528bee3701ee423749adf9a427ac1b81d5b06cf056", + "43d8db300c2c8476a7651f58b68751a075ace5966eff597b884f1299699104e6", + "4aff2701626d7da09293d618c88348d9a85f6d15f1f6548e1c9d4e83319ff4b3", + "7e4b8ab23aab8a6acb12b7b7e0fc344f4e6bf080f61788cf6dfc3b8370f8ade0", + "2e8f36b67a4631a84c39aa367aeb57084f8fa5b7ef68155a1b75f22b18a9c7ed", + "18f45e21d5f2eeefc771b16d3449526a30d898d8f4c3dbca85f1675be394e295", + "4748423adc6a6ff26b63f9df0d2068af09089b8156d630c3d50f0b2e03a2a2f2", + "6d886d814f9a78a2de49af9fb511b2f6edb4a3ea6dc06ce2f6f95049ac8f3c09", + "4c24c3043fb16eaf41367ed00d20c291efe515241c7dac800d702861f0e5263e", + "579f78303679b2cc554495205decb4a6f1e9bcc9d85cf519d6d5ad2cae44c1e9", + "8f6a35fcf40112fa53d0633d76b0384ac0cefd38bb4c2bc03aa7f1ed7b0617e6", + "0b67dd0c1779c8cb28cb8d058da1de942773f6c0245b1ba08fbaea679501f33c", + "2318e288bdef3c6d117a82cb52fc8237ffe4ac21cae5476152e5478d34154236", + "36bd1f8b413eeb6936eb0985682485944fc325a6607ffe3bef4b1161c2fe007d", + "eb8e529fa86780b1264c105d78b56fc95fc5a1b64095250e95eef2ec21e1211a", + "08ff516360863bdc986947194d61daf53c829347159125a235e1918b37e99338", + "e809d1853b27590bfa06ecb85bce7def13f019bfb63be82efbabb1c7dc92635e", + "9186ddbd04545692048776c8c03ca2fbab702d29d36e6fe008b684e5e993c82b", + "77603f751b73bc21bbe5b773e0d0f6792faa2674e06817e635d4e88bf1d17dd0", + "0a6d0ad5b529d0ac2694ec951d3775f655f97e5685555f751af545aba1050088", + "bf1627c960a8525f99bf6161adf67cde944043e8e30e7e1d96adce1663f5ab4c", + "d8bac0b9657bfcd80fc304869e327e0f2678ef5f14ebcf213c23b812b50fb5e1", + "f0cbe97085c47003dff086067bbbff551297fa89a6c66ebb6ee2f4e3e87982fd", + "4fd42f041a2ccb8aef96142adcb29373c49baae26bffa42163845271531ce6cf", + "cb0b9179855a5077f325960685d46620dc1f8a4d4bad2b598e9090c8aab93528", + "d4fe2b5532d753013e7363f73b5935d385ecf595be34a959bf5f00aa15ae3130", + "524fc0a903140d8c154846ddcd3e9db26a650ee1795396d24f1b0ebe0b9f334f", + "b4ab01e14b68794ecf14a9bffa236b08e49687f4f3ac8a2bb905071afa662bee", + "99e6ddd42ef2512f4bc9afc5fde56913a5f51dd3ac07ef72d8f78e72cb7b16e3", + "8090b5b2c9b7309c309e2e70bf52e33208052083734a197d5e6ae2f583b570b9", + "be3e6b7418a12113a3ebd1f746332aeb68b3fc6c132f1d7361309a02f0efb761", + "8154480245e9b63745921113a00d6bc64a45291a5bbb699834ee72de02d22422", + "b2bd95c8c81ff43b201979a2df77e3b86ad20ae4c07e4c26b850eddf27c7caa3", + "3bd8e5385983d6dcb643a747cd8b225d363c6c0184928b4ed60c95c26f05ce36", + "d04826a660f73e84555281031814fe1d8f50d3c5f94dcebc85ac7e8d7cd38f1c", + "8cd0b12fd5827a41c821f3f240e4e1d79b9defef792d6a6669d9a22d584df0f2", + "926a3af88d09079ff74aad0326c9a01f3496e059dc15e0abe5cb5992fdb5f7af", + "6d43b02b2deee8d329f3b41e30f67e82a4a4d05c2d00b237a1d1b5269cee0506", + "594ecfc56eb85adc7eeb5b1e7c5314218846ef65225a2fae20de5332aa34ecf4", + "ee34d36dc61d0f37d4a39b18c77883dab43b1f2c7ea2346588b305953fb3695f", + "bbbeaf7c9b6d03bab413b8aa0143a3450836b0d477bbcd3c931cf5b0870e8218", + "d17062ce0222d493c375581f72838251e92ac6d8f8957e9fff9826b3452b9edc", + "2c4b908a9bfa9049c2d926447aba5419cf19b80a061a3276fb0ab2aef24e92c5", + "b6431d4b14f93a5262453a9f4bc0d1d22621b876035fe7a1fc0d3a34a5e636ca", + "aef9933e790e45d88e7d5d835b0416711f5c3e5c1ae8ac200dd0f56ced4dd0b6", + "3ebf7f3d750fdddedf6978c02f2050f8daecfad9604897ca50ac5f3f3bf388a7", + "4b1fef1ff474b36bda3aabb7f9c60829a3edd7584f26e6d7af8800caae55fae3", + "36304f8e566cda2660b3badc319df0cc63b4ad2f9f27df08db1712acb5281ddd", + "e996ad53ba0f8987a77d757ddcb428f8202fb9b84fb0f83932015713e4430ea3", + "b8373119066b691bbe24ac3e2a497061fd09945bca72eec166e45ebdd59f35c5", + "4a0d112745dbb87d95d666b0238404549cec6faf9acdf259f069ca7e1d79047b", + "c4d7587698e6f9c3266b39e4fd5c979088a39331b3167d697686102780e9cf2c", + "a0c6924ba9c9db5629822542ef659bcce93dfb656241dabdaca915dd314f0e77", + "0cde86622a0fb406b7f2ccdce46353248637703ac5974a386464fff41dd8097f", + "453954cc34217437e78ddc21997e96bc0addc306f150c3f6cda0adf80d8fdc51", + "b84d0f80dc98a1cc37705b533e76c0dbfae948c36242d160f335f099975557cf", + "545e1e08c9b69ab90fa5444b00e7f9b5dbf6cf2bf04d1a797d345f77a4f201e4", + "0ed757d30d49c57badae21bcb1c893f6b5df42441d87912884f61f0f98673cbb", + "87e8c2d3ad3c3157741e7b44db872cfce41d5f065f38904e8f83b7c74dabe739", + "1ff3fa456559e702759e1be011ce3e2571cd132443021b3fa5eecdb098798915", + "1c059b7c7cee224193e015b41b0505ef876e378e39ed193aebf5bd555d626f9e", + "2183658c581605eb2007811b383b1ac93369f5a5031f48f0e3fb2482d4b3b3be" + ] + }, + { + "line": 107, + "lineHash": "1855357059e6a2a14a211a1c91000807ecc51e3065187ab67c7d633eaf133eae", + "nextLineHash": "fc8b01238e1cc233faaa6bd0f488af535348ee84ccb502ef27c51dac1df6b6e5", + "suffixHashes": [ + "8dda884f89e9234651addabab6eadb3f0298600efcad432f33cfa83ddb2b5cf4", + "44b27de0a8382d60b729e4facc4c4a377975dd624b182815058f11c38fcc0d0c", + "3d4445585160fad6ab4d7946e3e06576d992fd31da717f0249ce5699eda3e93c", + "61888fbb32c77fc9b5364e130765a467e0105184c855375f608b5f3d23afd15c", + "0fc3830863518d0a2b1f4d7f0f55986015ea93028ebb379a7628d11d95c17162", + "b2630d0d816c7dc3d120817488e0472b72ac8c2616fcef07b779a93790037a10", + "b23fd051587a9242eb9203c827f64f72d6d9fc0030cb18997df478e75a1faae7", + "a1c676a1b2784b6a4d94be4e95fc5aa3ad2debb5b9852403364babc5d95f4214", + "408797a5d25c808e6df0de948ab6b1d1ee59e1e1c94a40bd823d573d5054c5a2", + "7898efd258bbee324716e51a3c0a8de583b7bb4b01c3cae5ca8a728ecb873b02", + "9445920d27dda28e76c51bbb1a26e79d6b43d734d138d705c51aa024bd979748", + "5e57dc48e6cc1cffb2a64d2076138d3064e14394cb017916ca87fec51cb79dad", + "2559585806a5147cfec23d94636c194d05e14edf6b13eb7e26bd70a0d5d17863", + "c96a761177594f3e42683b42dba20e702e820f896e3ee60acd9b24e4b0cac376", + "893b2fbc5952dbf6481b78f3bb575f82c858d71dd0aef465ab57eaaddef85e0c", + "796cd2d299c901a5c9a5efacfd8e775c6ce789c0977c7d33d16cd42ab6fb827c", + "b88b9d355263574e1882bb83eb1f7a101a57b8b0d5f2226cbae005e06744cf9e", + "c9c108db3920ce6d73357375bc0ce87e2ad5b0c72f39ccac7e92413252c8dc1e", + "133d2e364d530b164d26bb8020203549df9374a6f06bce59a981c0224031aff5", + "1f1c5c26443b4e713d7cf9250253f4726b718cd7d5426df0c2554c26293d29a2", + "e076c09af83607b4d0c7c58dfa9d68c1a0d46a5e80e76d27646da5e54a597096", + "639fdc32cae9efc918881d1c8e9f30847326563ad60dac12b98b540c766d4866", + "df23ea77e0008a320c504bb9d3e45a5d08409e8f24c42f8104a59406b247dd28", + "f6f42d7fb368dbf43d2415d2907221b9defbe9bcb7ca425309c2d7ce0f854b98", + "91cf08dcf7f28df9bb6fc77f0f3bc0db5015e91ea8450cab2bd4e1d7dc31761c", + "4dedf9ab4d828aeda3e3d5d072e46c44d63609b4ecce1e1b63a7ac00520a356a", + "e47143a8c5bb89ebbf41a9c889e38ae7040e378c90b5f1730b7ad4bae572a321", + "c5aef55f4d7ab1c6ee29a88943f6dd7a63913f098aba3a9a5d412aacb4f79807", + "32a667798522080ff86228409c62992040570956ed8da8c9e0a7939e9226bb9f", + "a7803bb7ccc7ed8d8bf9b6e2b093e23b57ae3a1e385ac4ada48106e827f1b4fd", + "9aad6b9e1982f6ab2572e58bce2b903f6e84244d4ca377db8d3d974b9841a2b4", + "2b0d3d31fc9085671f9b57beca98f2149966f1c68c6f45d160a8d2a053583ada", + "20daa1e88b25ea81bc948c48c391c0fbcc44a0070dd7e6870f51f81b9cc6492b", + "4839e71f43e427e0588f4fc52031321a8a42d0957bbafcda196dc28cbb2ba7fc", + "ce37f12e44a0d49f852e31b8dfb03eb3002a5f24c27c7c0b0d339bd6d6eb7380", + "2c75afb07f2a03f23177a98a02c41f7b1f482223dcb36e6d18c9c8c3a9ddabe6", + "70c4f3918cdc1ce71a1d1bd921d4eb25e9273824e3d91e40ac56b5d4cdb06517", + "5927a27671d497d1d654de47bc70f3854568d96b3af23bb6f5e5c553e0b01376", + "e54713035fb46ce57b8e4e6b6c2bee9bf8f59e126f551ef14d6e1a4eb929ae79", + "c95a618d44903eeba8771c54f5c97d63fab32d270abd00fec92425fdbf83339e", + "43714fed1ed46ebcf9c6be297e023d09aa1aa3655dbc5838a82854a876f3c122", + "6b68b3c1e6dc1e2811e9371004fed449aea8a35fb78a56bb93eccb749b433e3c", + "6cab1e357c51e54a60e19b36e0aea2ad41c3552337621f49e1dd4b1bc812f3fd", + "6ef121ce4a468a03dd323ea4d35278e35963f4333d92332095f564958fae1f5d", + "f4082b12cbea8572148f8b139ef211e398a535a5e98f1faaa1a65e0d23c451a8", + "baf194e9601db92141bd5d6cdebb476d39072535a17662b283e2c472b01e2f0b", + "0d2ca9c74c42e9fd9a03279785e7bc0c179b29c08afb51c1bb78a69147864e58", + "31d56062d2401d58fb712161495de0de869d3ca61d1e95ea7648552963774bdd", + "7b4fb1f3bccc3aff4e58b6ba80a88f5fbe7d18e79e135bc96da147c82192c415", + "ffb88c052ab133c5f080f429c68feb5a0a24c27da3428717b95d5f6054c714b4", + "f7b18259f474ec9599040881410cc3f20c4830449a998ee12a81eeb4b1fa63c8", + "016faea732591170f483820198cb332009a83ccf2cbdc9f03211d16d45815c04", + "104465d75ec50caff3cfa0133d8e0bbb32c903f2663a219edc3d73521e273740", + "0cc3a136cd74f859b3e6c4864c0b47cfc029e1060a3e65409d3ddafb0c69651a", + "ff1cb0bc8a49f80f3a37018fd4d4db3ebed49f1b37ae89d1af52a4cff69c3a3a", + "70091133b10ec4619f3308177691e018310999b4f210e1ff481df37ce0d34b8b", + "47c6a53ccf3f7af384497ef30d47dfabfb896eb83951df6f0b39aa093a3e9568", + "2164b8ae6934bb596c192479bc21a5385703bff12575e5176fc1127d44278d17", + "9c9ddc33f228bfa3ec94fadeadae6211418ab2383a89b301a42022766cd84e03", + "84650882ba841564154e9db8c8436967f53a673df542eee2027bb9c20ab46ec3", + "99ea33b5e20349d64a56fddd4eb54577872ec02c1988d03b5092e3f9e0b9b960", + "6d4c1bdbc19d46751c3fd142fc1a7c4871cda853ff2a18298199e38c92ee283d", + "c414758b17468f82896f5e209ad4d40eab016c4d4e2d4d97d68e04243668891c", + "851e4cca71e86fd179292ff22db8a8eb50608f1475e58ec860c9fcbd2d0190c6" + ] + } + ] + } + } + }, + "sessionId": "7fcddaa4-44d5-4550-b9cb-ce4e092faebd" + }, + "before": "---\nstatus: ACTIVE\n---\n# CEO Plan: User Dashboard Page\nGenerated by /plan-ceo-review (via /autoplan) on 2026-09-10\nBranch: main | Mode: SELECTIVE EXPANSION\nRepo: gstack-autoplan-chain-zmFsqo (no remote)\n\n## Problem\nMembers visit three pages after login to resume work, check alerts, and inspect recent\nchanges. A team walkthrough measured a median of 75s to the next item; the production\nnumber is unknown until the baseline step below runs.\n\nTargets:\n- Ship bar (advance the rollout): median login-to-first-completed-task improves at least\n 20% vs the control cohort.\n- Permanence bar (remove the flag): median at or under 45s absolute, or at least 40% below\n the production baseline if that baseline turns out to be far from 75s, sustained 4 weeks\n at 100%.\n\nGuardrails, measured on the flagged cohort vs control over the same window, evaluated\nonly once the cumulative flagged sample reaches 1000 sessions (a 2-point rate difference\nis noise at 200 sessions):\n- completed-task rate within 2 percentage points of control\n- permission-error rate within 2 percentage points of control\n- dashboard partial-failure rate (any panel failing to load) under 2% of requests\n\n## Vision\n\n### 10x Check\nA landing page that already knows the member's next step. Login, and the first thing on\nscreen is \"Resume: \" as one dominant button, with alerts and changes below.\nNo scanning, no decision. Concrete shape: eligibility-ranked actions from the existing\nregistry, item-level deep links in notifications, activity filtered to \"mine\". Effort for\nthat full version: human ~2 weeks / CC ~3 hours. This plan builds the surface that version\nneeds; multi-action ranking is the separate personalization plan.\n\n## Recommended approach (open taste decision T1)\nA) `GET /api/dashboard` aggregate endpoint returning a per-source result envelope\n(`{ activity, notifications, quickActions }`, each `{ status, data | error }`, notifications\nadding `unreadCount` and a server-issued `snapshot`). Each source returns at most 20\nrecords; the panels link to the existing full pages for more. 200 whenever auth passes;\n`?sources=` allowlist for per-panel retry. Alternatives considered: B) three client calls\nto existing list endpoints (T1 below; under B the envelope, `?sources=`, and server-issued\nsnapshot are replaced by per-call responses and the snapshot comes from the notifications\nlist response); C) post-login smart redirect (deferred as experiment E9).\n\nMark all as read reuses the existing member-scoped bulk-read API, which already takes a\nsnapshot time and marks only notifications at or before it. The `snapshot` in the envelope\nis the server-issued value the client passes to that existing API. No new mutation API is\nintroduced anywhere in this plan; E6 is deferred precisely because it would need one.\n\n## Effort for accepted scope\nBaseline plan plus E1-E5 and the shared primitives: human ~3 days / CC ~1.5 hours\n(the plan file has the hour-by-hour breakdown).\n\n## Scope Decisions\n\n| # | Proposal | Effort | Decision | Reasoning |\n|---|----------|--------|----------|-----------|\n| E1 | Next-up primary CTA when resume-assigned-work is eligible; when not eligible the three actions render as equal buttons | S | ACCEPTED | Directly moves the metric. Uses only the single existing eligibility predicate for that action; no ordering across actions, so this is not ranking. Ranking stays deferred. |\n| E2 | Unread count badge in Notifications header | S | ACCEPTED | Count already needed for Mark-all button state; answers \"is there anything?\" before scanning |\n| E3 | View all links to existing full pages | S | ACCEPTED | Makes the 20-record cap honest; older-page navigation stays where it lives |\n| E4 | Relative timestamps with absolute time in title and datetime | S | ACCEPTED | Does not move the 45s metric directly; low cost, faster scanning, screen readers get the absolute value |\n| E5 | Refresh when the user returns to the tab, at most every 30s | S | ACCEPTED | Does not move the metric directly; low cost, prevents stale-alert confusion; the 30s floor bounds traffic |\n| E6 | Per-notification mark-as-read | M | DEFERRED | Needs a new mutation API, which this plan excludes |\n| E7 | Live notification updates | M-L | DEFERRED | Needs polling or push infrastructure this plan does not touch |\n| E8 | Keyboard shortcuts for actions | S-M | DEFERRED | Global key handling, screen-reader conflicts; needs its own a11y review |\n| E9 | Post-login smart redirect A/B | S | DEFERRED | Login-flow change; needs the production baseline first |\n\n## Accepted Scope (added to this plan)\n- E1 Next-up primary CTA\n- E2 Unread badge\n- E3 View all links\n- E4 Relative timestamps\n- E5 Refresh on tab return (30s floor)\n\n## Engineering commitments inherited from review (not scope decisions)\nThese were required by the review to make the baseline scope shippable; they live in\nthe plan file's Review record with full detail:\n- Complete error and rescue map for every panel and for Mark all as read (shape depends on T1)\n- Action feedback announced through a screen-reader live region (required by the existing\n accessibility policy regardless of T3; T3 only decides whether the toast component is\n built here or in a separate ticket first)\n- Shared panel-state wrapper and a shared server/client contract module\n- Production baseline measurement before rollout; the targets and guardrails above; the\n logs, metrics, alerts, and cohort funnel panel that make them observable\n\n## Rollout\n1. Baseline: compute median and p75 login-to-first-completed-task over the last 30 days\n from existing analytics events and record it in the plan before any member sees the page.\n2. Ramp on `dashboard_landing` (member-cohort flag): staging \u2192 internal cohort \u2192 5% \u2192 25% \u2192 100%.\n3. Exit criteria for staging and internal cohort: functional only (all three panels load,\n a forced partial failure shows one panel error with a working Retry, mark all as read\n completes, keyboard-only pass, zero accessibility violations).\n4. Hold criteria at 5% and 25%: at least 1 week and 200 flagged sessions for the median\n check (ship bar, 20% improvement vs control); guardrails evaluated once the cumulative\n flagged sample reaches 1000 sessions. Roll back one step on a guardrail breach at that\n sample size, or immediately on partial-failure rate over 2%.\n5. Permanence: after 4 weeks at 100% with the permanence bar met and guardrails held,\n remove the flag and make the dashboard the landing page. If the permanence bar is not\n met after 8 weeks at 100%, decide explicitly: keep with the ship-bar gain, iterate, or\n roll back.\n\n## Deferred to TODOS.md\n- E6 per-notification mark-as-read (P3; depends on a new mutation API)\n- E7 live updates (P3; polling or push infrastructure)\n- E8 keyboard shortcuts (P3; needs a11y review)\n- E9 post-login smart redirect A/B (P2; after baseline and cohort data)\n- Remove `dashboard_landing` flag after the permanence bar is met (P2)\n\n## Taste decisions (OPEN \u2014 resolved by the user at the /autoplan Final Gate, due before implementation)\nEach shows the recommended option first. The plan file proceeds on the recommended option\nuntil the user says otherwise.\n- T1 Aggregate endpoint (recommended) vs three client calls. Owner: user at gate.\n- T2 All three panels in v1 behind the flag (recommended) vs QuickActions-first staged v1. Owner: user at gate.\n- T3 Toast built as shared primitive in this plan (recommended) vs separate ticket first. Owner: user at gate.\n- T4 Keep confirmation modal on Mark all as read (recommended; no undo API exists) vs remove modal + undo. Owner: user at gate.\n", + "viewport": " \n \n\u25cf Appending spec-review metrics\n \u23bf $ mkdir -p ~/.gstack/analytics\n echo '{\"skill\":\"plan-ceo-review\",\"ts\":\"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'\",\"iterations\":3,\"issues_found\":20,\"issues_f\n ixed\":20,\"remaining\":0,\"quality_score\":9,\"via\":\"autoplan\"}' >> ~/.gstack/analytics/spec-review.jsonl 2>/dev/null ||\n true\n echo logged\n \n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Edit file \n \u2026-982715-DqIQtk/skill-home-262upM/.gstack/projects/gstack-autoplan-chain-zmFsqo/ceo-plans/2026-09-10-user-dashboard.md\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n 94 a forced partial failure shows one panel error with a working Retry, mark all as read\n 95 completes, keyboard-only pass, zero accessibility violations).\n 96 4. Hold criteria at 5% and 25%: at least 1 week and 200 flagged sessions for the median\n 97 - check (ship bar, 20% improvement vs control); guardrails evaluated once the cumulative \n 98 - flagged sample reaches 1000 sessions. Roll back one step on a guardrail breach at that \n 99 - sample size, or immediately on partial-failure rate over 2%. \n 97 + check (ship bar, 20% improvement vs control; at 200 sessions this is a directional \n 98 + signal, so also require the confidence interval on the median difference to exclude \n 99 + zero before advancing). Advancing 5% \u2192 25% requires only the median check and the \n 100 + partial-failure rate; the 2-point rate guardrails apply once 1000 cumulative flagged \n 101 + sessions accumulate, at whichever step that occurs, and a breach counts only when the \n 102 + 90% confidence interval on the difference excludes zero (a bare 2-point reading at \n 103 + 1000 sessions fires on noise about one time in three). Roll back one step on a \n 104 + confirmed guardrail breach, or immediately on partial-failure rate over 2%. \n 105 + These exit criteria and guardrail definitions assume the recommended T2 (all three \n 106 + panels in v1); if the user picks the staged option, \"all three panels load\" and the \n 107 + partial-failure definition are re-scoped to the panels actually shipped. \n 108 5. Permanence: after 4 weeks at 100% with the permanence bar met and guardrails held,\n 109 remove the flag and make the dashboard the landing page. If the permanence bar is not\n 110 met after 8 weeks at 100%, decide explicitly: keep with the ship-bar gain, iterate, or\n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n Do you want to make this edit to 2026-09-10-user-dashboard.md? \n \u276f 1. Yes \n 2. Yes, and switch to accept edits (auto-approve file edits and common file commands) for this session (shift+tab)\n 3. No\n\n Esc to cancel \u00b7 Tab to amend", + "publicTools": [ + { + "sessionId": "7fcddaa4-44d5-4550-b9cb-ce4e092faebd", + "timestamp": "2026-09-10T22:00:04.615Z", + "toolUseId": "toolu_01Wo4P7AC4zrX774MLdRfHUF", + "kind": "use", + "name": "Write", + "input": { + "file_path": "/tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/skill-home-262upM/.gstack/projects/gstack-autoplan-chain-zmFsqo/ceo-plans/2026-09-10-user-dashboard.md", + "content": "---\nstatus: ACTIVE\n---\n# CEO Plan: User Dashboard Page\nGenerated by /plan-ceo-review (via /autoplan) on 2026-09-10\nBranch: main | Mode: SELECTIVE EXPANSION\nRepo: gstack-autoplan-chain-zmFsqo (no remote)\n\n## Problem\nMembers visit three pages after login to resume work, check alerts, and inspect recent\nchanges. A team walkthrough measured a median of 75s to the next item; the production\nnumber is unknown until the baseline step below runs.\n\nTargets:\n- Ship bar (advance the rollout): median login-to-first-completed-task improves at least\n 20% vs the control cohort.\n- Permanence bar (remove the flag): median at or under 45s absolute, or at least 40% below\n the production baseline if that baseline turns out to be far from 75s, sustained 4 weeks\n at 100%.\n\nGuardrails, measured on the flagged cohort vs control over the same window, evaluated\nonly once the cumulative flagged sample reaches 1000 sessions (a 2-point rate difference\nis noise at 200 sessions):\n- completed-task rate within 2 percentage points of control\n- permission-error rate within 2 percentage points of control\n- dashboard partial-failure rate (any panel failing to load) under 2% of requests\n\n## Vision\n\n### 10x Check\nA landing page that already knows the member's next step. Login, and the first thing on\nscreen is \"Resume: \" as one dominant button, with alerts and changes below.\nNo scanning, no decision. Concrete shape: eligibility-ranked actions from the existing\nregistry, item-level deep links in notifications, activity filtered to \"mine\". Effort for\nthat full version: human ~2 weeks / CC ~3 hours. This plan builds the surface that version\nneeds; multi-action ranking is the separate personalization plan.\n\n## Recommended approach (open taste decision T1)\nA) `GET /api/dashboard` aggregate endpoint returning a per-source result envelope\n(`{ activity, notifications, quickActions }`, each `{ status, data | error }`, notifications\nadding `unreadCount` and a server-issued `snapshot`). Each source returns at most 20\nrecords; the panels link to the existing full pages for more. 200 whenever auth passes;\n`?sources=` allowlist for per-panel retry. Alternatives considered: B) three client calls\nto existing list endpoints (T1 below; under B the envelope, `?sources=`, and server-issued\nsnapshot are replaced by per-call responses and the snapshot comes from the notifications\nlist response); C) post-login smart redirect (deferred as experiment E9).\n\nMark all as read reuses the existing member-scoped bulk-read API, which already takes a\nsnapshot time and marks only notifications at or before it. The `snapshot` in the envelope\nis the server-issued value the client passes to that existing API. No new mutation API is\nintroduced anywhere in this plan; E6 is deferred precisely because it would need one.\n\n## Effort for accepted scope\nBaseline plan plus E1-E5 and the shared primitives: human ~3 days / CC ~1.5 hours\n(the plan file has the hour-by-hour breakdown).\n\n## Scope Decisions\n\n| # | Proposal | Effort | Decision | Reasoning |\n|---|----------|--------|----------|-----------|\n| E1 | Next-up primary CTA when resume-assigned-work is eligible; when not eligible the three actions render as equal buttons | S | ACCEPTED | Directly moves the metric. Uses only the single existing eligibility predicate for that action; no ordering across actions, so this is not ranking. Ranking stays deferred. |\n| E2 | Unread count badge in Notifications header | S | ACCEPTED | Count already needed for Mark-all button state; answers \"is there anything?\" before scanning |\n| E3 | View all links to existing full pages | S | ACCEPTED | Makes the 20-record cap honest; older-page navigation stays where it lives |\n| E4 | Relative timestamps with absolute time in title and datetime | S | ACCEPTED | Does not move the 45s metric directly; low cost, faster scanning, screen readers get the absolute value |\n| E5 | Refresh when the user returns to the tab, at most every 30s | S | ACCEPTED | Does not move the metric directly; low cost, prevents stale-alert confusion; the 30s floor bounds traffic |\n| E6 | Per-notification mark-as-read | M | DEFERRED | Needs a new mutation API, which this plan excludes |\n| E7 | Live notification updates | M-L | DEFERRED | Needs polling or push infrastructure this plan does not touch |\n| E8 | Keyboard shortcuts for actions | S-M | DEFERRED | Global key handling, screen-reader conflicts; needs its own a11y review |\n| E9 | Post-login smart redirect A/B | S | DEFERRED | Login-flow change; needs the production baseline first |\n\n## Accepted Scope (added to this plan)\n- E1 Next-up primary CTA\n- E2 Unread badge\n- E3 View all links\n- E4 Relative timestamps\n- E5 Refresh on tab return (30s floor)\n\n## Engineering commitments inherited from review (not scope decisions)\nThese were required by the review to make the baseline scope shippable; they live in\nthe plan file's Review record with full detail:\n- Complete error and rescue map for every panel and for Mark all as read (shape depends on T1)\n- Action feedback announced through a screen-reader live region (required by the existing\n accessibility policy regardless of T3; T3 only decides whether the toast component is\n built here or in a separate ticket first)\n- Shared panel-state wrapper and a shared server/client contract module\n- Production baseline measurement before rollout; the targets and guardrails above; the\n logs, metrics, alerts, and cohort funnel panel that make them observable\n\n## Rollout\n1. Baseline: compute median and p75 login-to-first-completed-task over the last 30 days\n from existing analytics events and record it in the plan before any member sees the page.\n2. Ramp on `dashboard_landing` (member-cohort flag): staging \u2192 internal cohort \u2192 5% \u2192 25% \u2192 100%.\n3. Exit criteria for staging and internal cohort: functional only (all three panels load,\n a forced partial failure shows one panel error with a working Retry, mark all as read\n completes, keyboard-only pass, zero accessibility violations).\n4. Hold criteria at 5% and 25%: at least 1 week and 200 flagged sessions for the median\n check (ship bar, 20% improvement vs control); guardrails evaluated once the cumulative\n flagged sample reaches 1000 sessions. Roll back one step on a guardrail breach at that\n sample size, or immediately on partial-failure rate over 2%.\n5. Permanence: after 4 weeks at 100% with the permanence bar met and guardrails held,\n remove the flag and make the dashboard the landing page. If the permanence bar is not\n met after 8 weeks at 100%, decide explicitly: keep with the ship-bar gain, iterate, or\n roll back.\n\n## Deferred to TODOS.md\n- E6 per-notification mark-as-read (P3; depends on a new mutation API)\n- E7 live updates (P3; polling or push infrastructure)\n- E8 keyboard shortcuts (P3; needs a11y review)\n- E9 post-login smart redirect A/B (P2; after baseline and cohort data)\n- Remove `dashboard_landing` flag after the permanence bar is met (P2)\n\n## Taste decisions (OPEN \u2014 resolved by the user at the /autoplan Final Gate, due before implementation)\nEach shows the recommended option first. The plan file proceeds on the recommended option\nuntil the user says otherwise.\n- T1 Aggregate endpoint (recommended) vs three client calls. Owner: user at gate.\n- T2 All three panels in v1 behind the flag (recommended) vs QuickActions-first staged v1. Owner: user at gate.\n- T3 Toast built as shared primitive in this plan (recommended) vs separate ticket first. Owner: user at gate.\n- T4 Keep confirmation modal on Mark all as read (recommended; no undo API exists) vs remove modal + undo. Owner: user at gate.\n" + }, + "messageId": "msg_011CevWWAzJYxLfW7H13BcgU", + "requestId": "req_011CevWW9bU8yzBJMoTJ8Z3g" + }, + { + "sessionId": "7fcddaa4-44d5-4550-b9cb-ce4e092faebd", + "timestamp": "2026-09-10T22:00:09.083Z", + "toolUseId": "toolu_01Wo4P7AC4zrX774MLdRfHUF", + "kind": "result", + "content": "The file /tmp/gstack-paid-shard-a0OkbA/tmp/gstack-hermetic-982715-DqIQtk/skill-home-262upM/.gstack/projects/gstack-autoplan-chain-zmFsqo/ceo-plans/2026-09-10-user-dashboard.md has been updated successfully. (file state is current in your context \u2014 no need to Read it back)", + "isError": false + } + ] +} diff --git a/test/fixtures/autoplan-with-result-au.json b/test/fixtures/autoplan-with-result-au.json new file mode 100644 index 000000000..9e279c2e9 --- /dev/null +++ b/test/fixtures/autoplan-with-result-au.json @@ -0,0 +1,11 @@ +{ + "sessionId": "78ce9c42-e5f7-4595-81ea-7d9bb8b4345c", + "text": "Phase 2.5 wrapped up with DX improved to 8/10 and TTHW down to under 5 min; the Claude subagent review found 22 issues (6 high-severity), all accepted, while Codex remained disabled. Moving to Phase 3 (Eng Review), starting by loading the eng phase section and its methodology pointer.\n\n", + "timestamp": "2026-09-10T21:35:46.209Z", + "provenance": { + "kind": "Claude Code public narration summary", + "cliVersion": "2.1.263", + "publicTerminalSHA256": "79b01906d51fbf87451004e2fb10a191ab46442ecfb877c8ceba626556ea9314", + "originalPaidOutcome": "failed: missing DX phase marker" + } +} diff --git a/test/fixtures/eng-annotated-cache-au.json b/test/fixtures/eng-annotated-cache-au.json new file mode 100644 index 000000000..682c7c856 --- /dev/null +++ b/test/fixtures/eng-annotated-cache-au.json @@ -0,0 +1,39 @@ +{ + "provenance": { + "source": "db1db6d9723f465e90e5ed72fd740e7c1cbb3552", + "publicEvidence": ".context/ship-source-au-delta-paid-20260910-v1/eng-retry-public-evidence-sdk-v1/proof.json", + "originalOutcome": "no_review_questions", + "retrospectivePass": false + }, + "call": { + "sessionId": "b830c8f4-5af2-47a2-ab15-09b149579b45", + "toolUseId": "toolu_01UV3sEzbujkuGkhXdnPcA1a", + "questions": [ + { + "header": "Arch 1", + "question": "D5 — Issue 1 [P1] (confidence 8/10) PLAN.md:19-20 + :10 — AuthBroker and SessionMint both mutate one module-level AuthCache that does not serialize mutations. How should the shared cache be wired?\nProject/branch/task: gstack-plan-count-kpTDIg on main, PLAN.md multi-tenant auth refactor.\nELI10: Two services share one global cache object exported from a module, and both write to it. The plan admits the cache 'does not serialize mutations'. So when a tenant-suspension invalidation fires while SessionMint is halfway through minting, the mint can land after the invalidation and a suspended tenant keeps a live session. Module-level exports also leak state between tests and break under hot reload. Constructor injection (passing the cache in) is the boring, proven fix; the write-ordering problem needs an explicit rule on top.\nStakes if we pick wrong: a suspended or logged-out tenant session survives invalidation (security bug, silent), and unit tests pass or fail depending on run order.\nRecommendation: A because injection fixes test isolation and single-writer plus policy-version check at write time closes the write-after-invalidate race; matches your explicit-over-clever and edge-case-first preferences.\nCompleteness: A=10/10, B=7/10, C=3/10\nNet: an explicit ownership rule now vs debugging interleaved cache writes in production later.", + "options": [ + { + "label": "Inject + single-writer + version-checked writes (recommended)", + "description": "✅ AuthCache passed into both services by constructor from one composition root; SessionMint is the only writer, AuthBroker reads and invalidates. ✅ Writes carry the policy version and are rejected if the entry was invalidated since read (compare-and-set), with a unit test for the interleaving. ❌ Slightly more plumbing and one extra check per write (human: ~1 day / CC: ~20 min)." + }, + { + "label": "Constructor injection only", + "description": "✅ Fixes test isolation and hot-reload double instances with minimal change. ✅ Keeps existing adapter untouched, no new write semantics. ❌ Both services still write freely; the write-after-invalidate race stays open (human: ~2h / CC: ~5 min)." + }, + { + "label": "Keep module-level export as planned", + "description": "✅ Zero extra plumbing; simplest possible wiring for the first cut. ✅ Matches what the plan author already sketched. ❌ Global mutable state shared by two writers, no serialization, order-dependent tests, and a silent tenant-isolation hole." + } + ] + } + ], + "answered": true, + "failed": false, + "answers": { + "D5 — Issue 1 [P1] (confidence 8/10) PLAN.md:19-20 + :10 — AuthBroker and SessionMint both mutate one module-level AuthCache that does not serialize mutations. How should the shared cache be wired?\nProject/branch/task: gstack-plan-count-kpTDIg on main, PLAN.md multi-tenant auth refactor.\nELI10: Two services share one global cache object exported from a module, and both write to it. The plan admits the cache 'does not serialize mutations'. So when a tenant-suspension invalidation fires while SessionMint is halfway through minting, the mint can land after the invalidation and a suspended tenant keeps a live session. Module-level exports also leak state between tests and break under hot reload. Constructor injection (passing the cache in) is the boring, proven fix; the write-ordering problem needs an explicit rule on top.\nStakes if we pick wrong: a suspended or logged-out tenant session survives invalidation (security bug, silent), and unit tests pass or fail depending on run order.\nRecommendation: A because injection fixes test isolation and single-writer plus policy-version check at write time closes the write-after-invalidate race; matches your explicit-over-clever and edge-case-first preferences.\nCompleteness: A=10/10, B=7/10, C=3/10\nNet: an explicit ownership rule now vs debugging interleaved cache writes in production later.": "Inject + single-writer + version-checked writes (recommended)" + }, + "unansweredQuestionIndices": [], + "answeredAt": "2026-09-10T21:15:03.350Z" + } +} diff --git a/test/fixtures/eng-required-parity-au.md b/test/fixtures/eng-required-parity-au.md new file mode 100644 index 000000000..401d2da3d --- /dev/null +++ b/test/fixtures/eng-required-parity-au.md @@ -0,0 +1,397 @@ +# Plan: Multi-tenant Auth Refactor (reviewed) + +Reviewed by /plan-eng-review on 2026-09-10, branch `main`, commit `3522c32`. +Source plan: `PLAN.md`. Scope was reduced in Step 0 (decision D2) and every +finding below was accepted individually (D3 through D10). Lake Score: 7/7 +decisions chose the complete option. + +## Context + +The service is moving from a single `legacyAuthFlow()` to a multi-tenant +auth path. The original sketch introduced five new types (AuthBroker, +TokenStore, SessionMint, AuthCache, RequestPolicy) across 12 files, shared +one mutable cache through a module-level export, rewrote the live login path +in place with no regression test, and left token validation as five +sequential IDP calls. This review keeps the goal (tenant-isolated brokering +and session minting over the existing cache adapter) and cuts the shape down +to what that goal needs, then hardens the two places where tenant isolation +can actually break: concurrent cache writes and swallowed errors. + +## Step 0: Scope (decision D2, accepted) + +**Accepted scope:** three new types and about 7 files. + +| Original | Reviewed | +|---|---| +| TokenStore + AuthCache, both wrapping the existing adapter | One `AuthCache` facade. TokenStore is folded in. | +| RequestPolicy class | `requestPolicy(ctx)` pure function returning a policy value. Promote to a class only if per-tenant mutable state appears. | +| AuthBroker, SessionMint services | Kept. | +| 12 files | ~7: composition root, AuthCache, AuthBroker, SessionMint, requestPolicy, validate/dispatch module, flag routing in the entry point, plus tests. | + +Plan text inconsistency fixed: the original listed "two new services" and +"four new classes" without AuthBroker in the class list. The real count was +five new types; it is now three. + +Search check [Layer 1]: module-level mutable singletons are the documented +Node anti-pattern; composition-root injection is the boring fix. Strangler +fig with a per-tenant flag is the standard way to replace a live auth path. +`Promise.all` is correct when every call must succeed; `allSettled` only +when partial results are useful (they are not here). + +TODOS.md does not exist in the repo. Distribution check: no new artifact +type, not applicable. + +## Existing contracts retained + +The existing cache adapter keys entries by tenant ID, issuer, audience, and +policy version. It evicts expired tokens and invalidates entries on logout, +token revocation, or tenant suspension. `AuthCache` is a service-facing +facade over that same adapter with one backing cache. The adapter, its +invalidation hooks, and their existing tests remain in use unchanged. + +New in this plan: `AuthCache` is the only writer (see Architecture 2). The +adapter's validity and tenant-key rules are unchanged; the guard is layered +on top, not inside the adapter. + +## Architecture + +### Component wiring (decision 1A: constructor injection) + +``` +composition root (one per process) + │ + ├── adapter = existingCacheAdapter() (unchanged) + ├── cache = new AuthCache(adapter, tenantStatus) + ├── idp = new IdpClient({ timeoutMs }) + ├── broker = new AuthBroker(cache, idp) + └── mint = new SessionMint(cache) + │ + ▼ + entry point (route handler) + │ flag.isEnabled(tenantId)? + ├── false ──▶ legacyAuthFlow(req) (retained until parity) + └── true ──▶ validate(req, broker) ──▶ dispatch(result, mint) +``` + +No module-level `AuthCache` export. Tests build a fresh `AuthCache` per case. + +### Write path (decision 2A: AuthCache owns all writes, guarded) + +``` +AuthBroker ──put(key, entry)──┐ + ▼ + AuthCache.put() + │ 1. tenantStatus.isActive(tenantId)? no ──▶ drop + AuthError.TenantSuspended + │ 2. entry.policyVersion == current? no ──▶ drop + AuthError.StalePolicy + │ 3. adapter.set(key, entry) + ▼ +SessionMint ──put(key, session)┘ + +Interleaving that must be safe: + t0 mint starts for tenant T + t1 suspension hook wipes T's entries + t2 mint calls cache.put() ──▶ step 1 fails ──▶ nothing written +``` + +The adapter's invalidation hooks still fire on logout, revocation, and +suspension. The guard closes the window between a hook firing and a late +write landing. Locks and queues were considered and rejected as +over-engineering for an in-process cache. + +### Rollout (decision 3A: strangler fig, per-tenant flag) + +1. Ship both paths. Flag default off for every tenant. +2. Enable for one internal tenant. Watch parity suite and error rates. +3. Widen by tenant cohort. Flip back per tenant on any divergence. +4. At 100% with parity green for one release, execute TODO 1 (delete + `legacyAuthFlow()`, the flag branch, and the parity suite's legacy leg). + +### Production failure scenarios per new codepath + +| Codepath | Realistic failure | Plan accounts for it | +|---|---|---| +| Composition root | Two roots constructed (e.g. test and app) → two caches | Root is the only constructor call site; tests use the root's factory. | +| AuthCache.put() guard | tenantStatus lookup slow or down | Guard fails closed (deny write) and raises `AuthError.TenantStatusUnavailable`; test covers it. | +| Flag routing | Flag store unreachable | Default to legacy path; log; test covers it. | +| validate() parallel IDP | One call hangs | AbortSignal timeout, siblings aborted (6A). | + +## Code quality (decision 4A) + +`validateAndDispatch()` (60 lines, three nested try/catch blocks, each +swallowing a different error class) is split: + +``` +validate(req, broker): Promise + │ throws typed errors, never swallows + ├── AuthError.IdpUnreachable (network / timeout) + ├── AuthError.BadSignature (JWKS mismatch) + ├── AuthError.UnknownTenant + ├── AuthError.TenantSuspended + └── AuthError.Expired + +dispatch(validated, mint): Promise + └── mint.mint(validated) ──▶ cache.put() + +boundary (route handler) + try { dispatch(await validate(req, broker), mint) } + catch (e) { log(e); return mapAuthError(e) } // one catch, one map table +``` + +`mapAuthError` is a single table from error class to response code and +user-facing message. Every class is a distinct, testable outcome. Callers +that depended on silent failure must be audited when the split lands. + +DRY: TokenStore was a second wrapper over the same adapter as AuthCache; it +is gone (D2). Inline ASCII diagram comments go in `AuthCache` (write-path +guard), the composition root (wiring), and the validate/dispatch module +(error map). + +## Tests + +Test framework: none detected in this fixture repo (no `package.json`, zero +test files). File names below assume TypeScript with `*.test.ts`; adjust to +the real project's convention. + +### Coverage diagram + +``` +CODE PATHS USER FLOWS +[+] composition-root.ts [+] Login (flag off) + └── build() └── [GAP][CRITICAL][→E2E] identical to pre-refactor — legacyAuthFlow.regression.test.ts + └── [GAP] single AuthCache instance, no module export [+] Login (flag on) +[+] auth-cache.ts ├── [GAP][→E2E] happy path end to end + └── put() ├── [GAP] double-submit → one session + ├── [GAP] active tenant, current policy → written ├── [GAP] token expires between validate and dispatch + ├── [GAP] suspended tenant → dropped, TenantSuspended └── [GAP] flag store unreachable → legacy path + ├── [GAP] stale policy version → dropped, StalePolicy [+] Parity (flag on vs off) + ├── [GAP] tenantStatus unavailable → fail closed └── [GAP][→E2E] table: happy + each AuthError + suspended/revoked/expired + └── [GAP] suspend-during-mint interleaving [+] Tenant admin +[+] validate.ts ├── [GAP][→E2E] suspend tenant → in-flight mint refused + └── validate() └── [GAP][→E2E] revoke token → next request rejected + ├── [GAP] all 5 IDP calls succeed [+] Error states + ├── [GAP] one call rejects → siblings aborted ├── [GAP] each AuthError → specific code + message + log line + ├── [GAP] one call times out → IdpUnreachable └── [GAP] IDP slow → bounded failure, retryable message + └── [GAP] each AuthError subclass raised +[+] dispatch.ts / boundary + └── mapAuthError() + └── [GAP] every AuthError class → distinct response +[+] request-policy.ts + └── requestPolicy() [GAP] pure: same ctx → same policy, unknown tenant → default-deny +[+] legacyAuthFlow (retained, existing tests) [★★ TESTED by existing adapter tests only] + +COVERAGE: 0/22 new paths tested (0%) | Code paths: 0/13 | User flows: 0/9 +QUALITY: existing adapter tests ★★ | GAPS: 22 (7 E2E, 1 CRITICAL regression, 0 eval) +``` + +Legend: ★★★ behavior + edge + error | ★★ happy path | ★ smoke | [→E2E] needs integration test + +### Required tests (all written alongside the feature code) + +**CRITICAL (regression rule, mandatory, no decision needed):** +`legacyAuthFlow.regression.test.ts`. Pin current behavior of +`legacyAuthFlow()` before any change: valid token → session shape and +lifetime; expired, bad signature, unknown tenant → the exact current +response codes and bodies; logout and revocation clear the cache. This is +the oracle for the parity suite and the guard for the flag-off path. What +breaks without it: the rewrite changes existing behavior for every tenant +still on the legacy path with nothing to catch it. + +**Decision 5A, parity suite:** `auth-parity.test.ts`. One fixture table, +each row run through both paths (flag off, flag on), assert identical +outcome. Rows: valid token; each `AuthError` class; tenant suspended, +revoked, expired; policy version bumped. Exit criterion for TODO 1. + +**Decision 1A:** `composition-root.test.ts`. Two services from one root +share one `AuthCache`; two roots do not. Grep test: no module exports an +`AuthCache` instance. + +**Decision 2A:** `auth-cache.test.ts`. Five `put()` branches above, +including the suspend-during-mint interleaving (start mint, fire suspension +hook, complete mint, assert no entry) and tenantStatus unavailable → deny. + +**Decision 4A:** `validate.test.ts`, `dispatch.test.ts`. One test per +`AuthError` subclass raised by `validate()`; one per row of `mapAuthError`; +assert a log line is emitted for each; assert nothing is swallowed (a +non-AuthError propagates). + +**Decision 6A:** in `validate.test.ts` with a fake IDP: all succeed; +one rejects → others receive abort; one hangs past timeout → +`IdpUnreachable` within the bound; latency of the happy path ≈ max, not +sum (assert call overlap via fake timestamps). + +**User flows [→E2E]:** `auth.e2e.test.ts`: login flag on → authenticated +request → logout; double-submit; suspend tenant during login; revoke then +retry; flag store unreachable → legacy path. + +QA test plan artifact written to +`~/.gstack/projects/gstack-plan-count-tTVLFw/vercel-sandbox-main-eng-review-test-plan-20260910-210603.md`. + +## Performance (decision 6A) + +Token validation's five independent IDP calls run in parallel: + +``` +validate() + signal = AbortSignal.timeout(timeoutMs) (one per call, plus a shared controller) + Promise.all([discovery, jwks, introspect, userinfo, tenantLookup].map(c => c(signal))) + │ first rejection ──▶ controller.abort() ──▶ siblings cancelled + │ timeout ──▶ AuthError.IdpUnreachable + ▼ + latency: max(call) instead of sum(call) +``` + +`Promise.all` is the right semantics: validation is all-or-nothing. +Per-issuer caching of discovery and JWKS is TODO 2, deliberately out of this +PR. If the IDP client does not accept an `AbortSignal`, wrap it rather than +skipping the timeout. + +## NOT in scope + +- **TokenStore as a separate class.** Second wrapper over the same adapter; folded into AuthCache (D2). Revisit only if a non-cache backing store is actually needed. +- **RequestPolicy as a class.** No described state; a pure function. Promote when per-tenant mutable policy state appears. +- **Deleting `legacyAuthFlow()` and the flag.** Follow-up TODO 1, gated on parity at 100%. +- **Per-issuer discovery/JWKS caching.** Follow-up TODO 2; independent of tenant correctness and would blur the parity comparison. +- **Locks or a write queue for AuthCache.** Write-time guard chosen instead (2A); a queue is over-engineering for an in-process cache. +- **Changes to the existing cache adapter or its invalidation hooks.** Retained unchanged by the plan's own contract. +- **Distribution / packaging.** No new artifact type. + +## What already exists + +- **Existing cache adapter** (tenant/issuer/audience/policy-version keys, expiry eviction, invalidation on logout/revocation/suspension, with tests): reused unchanged behind `AuthCache`. The original plan rebuilt a second wrapper (TokenStore) over it; removed. +- **`legacyAuthFlow()`**: retained as the flag-off path and as the parity oracle instead of being rewritten in place. +- **Existing adapter tests**: still run; they do not cover the new writer or the guard, hence the new `auth-cache.test.ts`. +- **IDP client**: reused; gains an `AbortSignal` parameter or a thin wrapper. + +## TODOS (approved D9, D10; create TODOS.md at implementation time) + +### TODO 1: Remove the per-tenant auth flag and delete legacyAuthFlow() +- **What:** Delete `legacyAuthFlow()`, the flag branch in the entry point, and the parity suite's legacy leg. +- **Why:** Two live auth paths are a maintenance and audit burden once the new path is proven. +- **Pros:** Single code path, smaller test matrix. **Cons:** Needs a real signal before it is safe. +- **Context:** Start at the composition root / entry-point flag routing. The parity suite from decision 5A is the gate. +- **Depends on:** Flag at 100% for all tenants; parity suite green for one full release. + +### TODO 2: Cache per-issuer IDP discovery and JWKS +- **What:** Issuer-keyed cache for the discovery document and JWKS with TTL and refresh on unknown `kid`. +- **Why:** Two of five IDP calls per login are static per issuer; cuts latency and IDP quota. +- **Pros:** Lower p50, fewer rate-limit hits. **Cons:** A second cache with its own staleness rules; key rotation must trigger refresh. +- **Context:** Lives in the IDP client, not AuthCache. Key on issuer URL. +- **Depends on:** Decision 6A (parallel calls) landing first as the baseline. + +## Failure modes + +| New codepath | Realistic failure | Test | Handling | User sees | +|---|---|---|---|---| +| AuthCache.put() | Suspension hook races a mint | auth-cache interleaving | guard drops write | clear "account suspended" | +| AuthCache.put() | tenantStatus lookup down | auth-cache unavailable case | fail closed, TenantStatusUnavailable | clear retryable error | +| validate() | One IDP call hangs | validate timeout case | AbortSignal timeout | bounded, retryable error | +| validate() | One IDP call rejects, siblings leak | validate abort case | controller.abort() | specific error | +| dispatch/boundary | Unknown error class | validate non-AuthError case | propagates, logged | 500 with log line (not silent) | +| Flag routing | Flag store unreachable | e2e flag-unreachable | default to legacy | unchanged legacy behavior | +| Composition root | Second root built | composition-root test | test fails | n/a | +| legacyAuthFlow (flag off) | Behavior drift from rewrite | CRITICAL regression test | n/a (no code change on this path) | identical to today | + +Critical gaps (no test, no handling, silent): **0** after the accepted +remedies. Before review there were two: the suspend-during-mint race and +the swallowed error classes. + +## Worktree parallelization strategy + +| Step | Modules touched | Depends on | +|---|---|---| +| S1 Composition root + AuthCache (fold TokenStore, guarded put) | auth/cache, app bootstrap | — | +| S2 validate()/dispatch() split, AuthError classes, parallel IDP calls | auth/validate, auth/idp-client | — | +| S3 AuthBroker + SessionMint + requestPolicy | auth/services | S1 (cache API), S2 (AuthError types) | +| S4 Flag routing + regression test + parity suite + e2e | entry point, test/ | S1, S2, S3 | + +Lane A: S1 (independent). Lane B: S2 (independent). Lane C: S3 → S4 +(sequential, waits on A and B). + +Execution order: launch A and B in parallel worktrees. Merge both. Then C. + +Conflict flags: A and B both touch the shared `AuthError` type if it is +placed under auth/cache; put `AuthError` in its own module in S2 and have S1 +import it, or agree the file name up front. + +## Implementation Tasks +Synthesized from this review's findings. Each task derives from a specific +finding above. Run with Claude Code or Codex; checkbox as you ship. + +- [ ] **T1 (P1, human: ~3h / CC: ~10min)** — composition root — Build one AuthCache in a composition root and constructor-inject it into AuthBroker and SessionMint; delete the module-level export + - Surfaced by: Architecture issue 1 (D3) — PLAN.md:19-20 "global mutable AuthCache instance via module-level export" + - Files: composition-root.ts, auth-broker.ts, session-mint.ts, composition-root.test.ts + - Verify: composition-root.test.ts; grep confirms no exported AuthCache instance +- [ ] **T2 (P1, human: ~4h / CC: ~15min)** — AuthCache — Route all writes through AuthCache.put() with tenant-status and policy-version guard; fail closed on status lookup failure + - Surfaced by: Architecture issue 2 (D4) — PLAN.md:10 "they do not serialize mutations" + - Files: auth-cache.ts, auth-cache.test.ts + - Verify: auth-cache.test.ts including suspend-during-mint interleaving +- [ ] **T3 (P1, human: ~1 day / CC: ~20min)** — entry point — Per-tenant feature flag routes to the new path; legacyAuthFlow() retained; flag store failure defaults to legacy + - Surfaced by: Architecture issue 3 (D5) — PLAN.md:27-28 "rewritten as part of this work" + - Files: entry-point route handler, flags config, auth.e2e.test.ts + - Verify: e2e flag on/off and flag-unreachable cases +- [ ] **T4 (P1, human: ~3h / CC: ~10min)** — legacyAuthFlow — CRITICAL regression test pinning current legacyAuthFlow() behavior before any change + - Surfaced by: Test review, mandatory regression rule — PLAN.md:27-28 "no regression test for the prior behavior is planned" + - Files: legacyAuthFlow.regression.test.ts + - Verify: test passes against unmodified legacyAuthFlow() first +- [ ] **T5 (P1, human: ~4h / CC: ~15min)** — validate/dispatch — Split validateAndDispatch() into validate() + dispatch(), typed AuthError subclasses, one boundary catch with a mapAuthError table and a log line per class; audit callers that relied on silent failure + - Surfaced by: Code quality issue 4 (D6) — PLAN.md:23-24 "each catch swallows a different error class" + - Files: validate.ts, dispatch.ts, auth-error.ts, validate.test.ts, dispatch.test.ts + - Verify: one test per AuthError class; non-AuthError propagates +- [ ] **T6 (P2, human: ~1 day / CC: ~20min)** — tests — Table-driven parity suite running each fixture row through flag-off and flag-on paths + - Surfaced by: Test issue 5 (D7) — PLAN.md:14-16 "does not exercise legacyAuthFlow() or assert compatibility" + - Files: auth-parity.test.ts + - Verify: suite green for every row; becomes the exit criterion for TODO 1 +- [ ] **T7 (P2, human: ~3h / CC: ~10min)** — IDP client — Parallelize the five IDP calls with Promise.all, per-call AbortSignal timeout, abort siblings on first failure, map to AuthError.IdpUnreachable + - Surfaced by: Performance issue 6 (D8) — PLAN.md:31-32 "5 sequential API calls to the IDP" + - Files: validate.ts, idp-client.ts, validate.test.ts + - Verify: fake-IDP tests for hang, single rejection, and call overlap +- [ ] **T8 (P2, human: ~2h / CC: ~10min)** — scope — Fold TokenStore into AuthCache; implement requestPolicy() as a pure function with default-deny for unknown tenant + - Surfaced by: Step 0 scope challenge (D2) — PLAN.md:35-36 "4 new classes (TokenStore, SessionMint, AuthCache, RequestPolicy)" + - Files: auth-cache.ts, request-policy.ts, request-policy.test.ts + - Verify: no TokenStore symbol remains; requestPolicy tests +- [ ] **T9 (P3, human: ~2h / CC: ~10min)** — cleanup — TODO 1: remove flag and delete legacyAuthFlow() after parity at 100% + - Surfaced by: TODO 1 (D9) + - Files: entry point, legacyAuthFlow module, auth-parity.test.ts + - Verify: parity suite green for one release before starting +- [ ] **T10 (P3, human: ~4h / CC: ~15min)** — IDP client — TODO 2: per-issuer discovery + JWKS cache with kid-miss refresh + - Surfaced by: TODO 2 (D10) + - Files: idp-client.ts, idp-client.test.ts + - Verify: second login for same issuer makes 3 network calls, not 5 + +## Completion summary + +- Step 0: Scope Challenge — scope reduced per recommendation (5 types / 12 files → 3 types / ~7 files) +- Architecture Review: 3 issues found (all resolved: 1A, 2A, 3A) +- Code Quality Review: 1 issue found (resolved: 4A) +- Test Review: diagram produced, 22 gaps identified; 1 CRITICAL regression test added under the mandatory rule; parity suite accepted (5A) +- Performance Review: 1 issue found (resolved: 6A) +- NOT in scope: written +- What already exists: written +- TODOS.md updates: 2 items proposed to user, 2 accepted (file to be created at implementation) +- Failure modes: 0 critical gaps flagged after remedies (2 before) +- Outside voice: skipped (codex_reviews disabled; recorded as outside_status disabled) +- Parallelization: 3 lanes, 2 parallel / 1 sequential +- Lake Score: 7/7 recommendations chose complete option + +## Suppressed findings (confidence below 7, appendix only) + +- (4/10) TokenStore may have been intended for a future non-cache backing store; the plan does not say so, so it was folded. Re-split if that requirement surfaces. +- (4/10) RequestPolicy may need per-tenant mutable state; nothing in the plan describes it. Pure function until proven otherwise. +- (3/10) The five IDP calls may not all be independent (e.g. introspection may need the discovery document's endpoint). If so, two-stage: discovery + JWKS first, then the remaining three in parallel. Verify against the IDP client before T7. +- (4/10) Callers of validateAndDispatch() may depend on the silent-failure behavior; no call sites were available in this fixture to check. Audit is folded into T5. + +## GSTACK REVIEW REPORT + +| Review | Trigger | Why | Runs | Status | Findings | +|--------|---------|-----|------|--------|----------| +| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — | +| Outside Review | codex via `/plan-eng-review` (plan-review phase) | Independent 2nd opinion | 1 | disabled | outside_status: disabled (codex_reviews=disabled); no outside coverage | +| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | clean (PLAN) | 7 issues, 0 critical gaps, 7/7 resolved, scope reduced | +| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — | +| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — | + +**OUTSIDE COVERAGE:** provider codex, phase plan-review, disabled by config (`codex_reviews=disabled`); no findings. Native review only. Re-enable with `gstack-config set codex_reviews enabled`. + +**VERDICT:** ENG CLEARED — ready to implement (scope reduced, 7 findings resolved, regression test mandatory). No outside coverage. + +NO UNRESOLVED DECISIONS diff --git a/test/fixtures/eng-retained-corpus-au.md b/test/fixtures/eng-retained-corpus-au.md new file mode 100644 index 000000000..4fbffe769 --- /dev/null +++ b/test/fixtures/eng-retained-corpus-au.md @@ -0,0 +1,459 @@ +# Plan: Multi-tenant Auth Refactor (reviewed) + +Reviewed by `/plan-eng-review` on 2026-09-10, branch `main`, commit `e51cff2`. +Mode: SCOPE_REDUCED (Step 0 accepted). Source plan: `PLAN.md`. +Caveat: the repository contains only `PLAN.md` and `CLAUDE.md`; no source +was available to read. Every finding below is grounded in plan text +(`PLAN.md:line`) and marked with its confidence. Module paths under `src/` +are placeholders to be mapped onto the real tree at implementation time. + +## Context + +Auth is being reworked so two services, `AuthBroker` (validates tokens and +dispatches requests) and `SessionMint` (issues sessions), serve multiple +tenants over the existing tenant-keyed cache adapter. The original plan +(`PLAN.md:18-36`) shared one module-level mutable `AuthCache` between both +services, rewrote `legacyAuthFlow()` in place with no regression test, +carried a 60-line `validateAndDispatch()` with three error-swallowing +catches, made five sequential IDP calls per validation, and introduced four +new classes across twelve files. This review keeps the goal (multi-tenant +auth on the existing adapter) and hardens how it gets there: injected +single-writer cache, staged cutover, explicit error results, parallel +validation, and full test coverage including the legacy regression. + +## Existing contracts retained (unchanged from source plan) + +The existing cache adapter keys entries by tenant ID, issuer, audience, and +policy version. It evicts expired tokens and invalidates entries on logout, +token revocation, or tenant suspension. `AuthCache` retains these validity +and tenant-key rules and is a service-facing facade over that same adapter, +with one backing cache. The adapter, its invalidation hooks, and their +existing tests remain in use unchanged (`PLAN.md:7-13`). + +## Step 0: Scope decision (D4, accepted) + +Complexity check triggered: 12 files, 4 new classes (`PLAN.md:35-36`). + +Decision: **reduce**. +- `TokenStore` is cut. The adapter plus the `AuthCache` facade already own + storage, eviction, and invalidation; a second storage abstraction had no + stated job. (Plan-text evidence, confidence 6/10.) +- `RequestPolicy` starts as a module of pure functions + (`src/auth/request-policy.ts`), promoted to a class only if it grows + per-tenant state. +- New units: `AuthBroker`, `SessionMint`, `AuthCache` facade, plus the + policy function module. Target footprint about 8 files. + +Logged as decision `0fdd2895` via `gstack-decision-log`. Scope is settled; +later sections do not re-argue it. + +## Architecture + +### Component and data flow + +``` + request (tenant, token) + | + v + +----------------------------------------------+ + | composition root (one place, app startup) | + | authCache = new AuthCache(existingAdapter) | + | broker = new AuthBroker(authCache, idp, | + | policyFns) | + | mint = new SessionMint(authCache, idp) | + +----------------------------------------------+ + | read / invalidate | write (single writer) + v v + +-------------+ +--------------+ + | AuthBroker | ---mint req--> | SessionMint | + +-------------+ +--------------+ + \ / + \ AuthCache facade / + +---------------------------+ + | get(key) / invalidate(key)| + | putIfVersion(key, entry, | + | expectedPolicyVersion) | + +---------------------------+ + | + existing cache adapter + (tenant, issuer, audience, policyVersion) + eviction + invalidation hooks (unchanged) +``` + +### Issue 1 [P1] (confidence 8/10) `PLAN.md:19-20`, `:10` — shared global mutable cache (D5, approved: A) + +Problem: both services mutate one module-level `AuthCache` and the facade +"does not serialize mutations". A tenant-suspension invalidation that fires +mid-mint can be followed by the mint's write, leaving a suspended tenant +with a live session. Module-level exports also leak state across tests and +double up under hot reload. **[Layer 1]** constructor injection is the +proven answer; the write-ordering problem needs an explicit rule on top. + +Remedy (in plan): +1. No module-level export. `AuthCache` is constructed once in the + composition root and passed to `AuthBroker` and `SessionMint` by + constructor. +2. Single writer: `SessionMint` is the only component that writes session + entries. `AuthBroker` reads and calls `invalidate`. +3. Version-checked writes: `AuthCache.putIfVersion(key, entry, + expectedPolicyVersion)` rejects the write when the entry's policy + version changed or the key was invalidated since the read. Returns an + explicit `Rejected` result; `SessionMint` surfaces it as + `TenantSuspended`/`Stale`, never retries blindly. +4. Unit test forces the interleaving (read → invalidate → write) and + asserts the write is rejected. + +``` + Write protocol (per tenant key) + SessionMint AuthCache adapter + | read(key) ----------> | get ----------------> | + | <-- {entry, v=7} ---- | | + | | <== invalidate(key) ==| (suspension hook) + | putIfVersion(key, | | + | entry', expect=7)->| compare v: gone/!=7 | + | <-- Rejected -------- | | + | => TenantSuspended, no session issued +``` + +### Issue 2 [P1] (confidence 8/10) `PLAN.md:27-28` — big-bang rewrite of `legacyAuthFlow()` (D6, approved: A) + +Problem: the legacy path is replaced in one shot with no rollback other than +a redeploy. Multi-tenant auth means one wrong branch locks out a customer. + +Remedy (in plan): strangler-fig cutover. +1. Per-tenant flag `auth.brokerPath` (values: `legacy`, `shadow`, `broker`). +2. `shadow`: run both paths, serve the legacy decision, log any mismatch + (allow/deny, tenant, policy version, reason code) to a dedicated + `auth.shadow.mismatch` event. +3. `broker`: serve the new path. Rollback is a flag flip, no deploy. +4. Legacy deletion is a follow-up PR (see TODOS) once mismatches are zero + across all tenants for the agreed window. + +``` + Cutover state machine (per tenant) + [legacy] --enable shadow--> [shadow] --0 mismatches over window--> [broker] + ^ | | + +------- flag flip --------+------------ flag flip ---------------+ + Exit: all tenants in [broker] for N days ==> delete legacy + flag (TODO) +``` + +Production failure scenarios considered: +- New path denies a valid token for one tenant: caught in `shadow` as a + mismatch before any user is affected; in `broker`, flag flip restores + legacy in seconds. +- Shadow doubles IDP load: acceptable for the window; metadata cache from + Issue 5 keeps it to about one extra call per request. + +## Code quality + +### Issue 3 [P1] (confidence 8/10) `PLAN.md:23-24` — `validateAndDispatch()` swallows three error classes (D7, approved: A) + +Problem: 60 lines, three nested try/catch blocks, each swallowing a +different error class. In auth a swallowed error is a silent deny at best +and a silent allow at worst, with no log line to tell a bad token from an +IDP outage. + +Remedy (in plan): +1. Split into four small steps: `parseToken`, `validateToken`, + `resolveTenant`, `dispatch`, each about 10 lines and unit-tested alone. +2. Each step returns a discriminated union: + `Ok | InvalidToken | TenantSuspended | IdpUnavailable | Unexpected`. + No nested try/catch; a single try at the step that performs I/O maps the + thrown error to one of these variants. +3. One boundary at the top (`validateAndDispatch`) maps each variant to a + response code and a structured log with tenant id and request id. + `Unexpected` always logs at error level and denies. +4. Callers consume the result type; no exceptions cross the boundary. + +``` + validateAndDispatch (pipeline) + parseToken -> validateToken -> resolveTenant -> dispatch + | | | | + InvalidToken IdpUnavailable TenantSuspended Ok + \_____________|_______________|______________/ + | + boundary: map -> response + log + Unexpected => 500 + error log + deny +``` + +DRY: key construction (tenant, issuer, audience, policyVersion) lives only +in the adapter; `AuthCache` exposes typed keys so neither service rebuilds +them. + +## Tests + +Test framework: none detectable in this repository (no `package.json`, +config, or test files). Test file names below follow `*.test.ts`; adjust to +the real project convention. + +### Coverage diagram (planned code, after approved remedies) + +``` +CODE PATHS USER FLOWS +[+] src/auth/auth-cache.ts [+] Login (tenant A, tenant B side by side) + ├── get() ├── [GAP] [→E2E] A logs in, B logs in, no cross read + │ └── [GAP] hit / miss / expired ├── [GAP] [→E2E] Double-submit login → one session + ├── invalidate() └── [GAP] [→E2E] IDP timeout → clear retry message + │ └── [GAP] logout / revoke / suspend (hook wiring) [+] Logout + └── putIfVersion() └── [GAP] [→E2E] A logs out, B unaffected + ├── [GAP] version matches → stored [+] Suspension / revocation + ├── [GAP] version differs → Rejected ├── [GAP] [→E2E] suspend A → next request denied + └── [GAP] key invalidated since read → Rejected ├── [GAP] [→E2E] revoke token → next request denied +[+] src/auth/session-mint.ts └── [GAP] [→E2E] suspend during mint → no session + ├── mint() + │ ├── [GAP] happy path [+] Cutover + │ ├── [GAP] Rejected → TenantSuspended ├── [GAP] flag=legacy serves legacy path + │ └── [GAP] IDP failure → IdpUnavailable ├── [GAP] flag=shadow logs mismatch, serves legacy +[+] src/auth/auth-broker.ts └── [GAP] flag=broker serves new path + ├── parseToken() [GAP] valid / malformed / empty + ├── validateToken() [GAP] ok / expired / bad sig [+] Error states + │ ├── [GAP] Promise.all one-fails → fail fast ├── [GAP] IDP 500 → clear error, logged w/ tenant+req id + │ ├── [GAP] per-call timeout → IdpUnavailable ├── [GAP] malformed JWKS → deny, logged, no crash + │ └── [GAP] metadata cache hit / miss / TTL expiry └── [GAP] Unexpected → 500, error log, deny + ├── resolveTenant() [GAP] known / suspended / unknown + ├── dispatch() [GAP] ok / downstream error + └── validateAndDispatch() boundary + └── [GAP] every variant → response + log +[+] src/auth/request-policy.ts (pure fns) + └── [GAP] each policy fn: allow / deny / edge inputs +[~] src/auth/legacy-auth-flow.ts (kept behind flag) + └── [GAP] [CRITICAL REGRESSION] recorded corpus: legacy vs broker parity +[=] existing cache adapter (★★★ TESTED — existing suite, unchanged) + +COVERAGE: 1/34 paths tested (3%) | Code paths: 1/22 | User flows: 0/12 +QUALITY: ★★★:1 | GAPS: 33 (10 E2E, 1 CRITICAL regression, 0 eval) +``` + +Legend: ★★★ behavior + edge + error | ★★ happy path | ★ smoke | [→E2E] integration test | [+] new | [~] modified | [=] unchanged + +### Issue 4 [P1] (confidence 9/10) `PLAN.md:14-16` — no end-to-end coverage of the auth flows (D8, approved: A) + +Remedy (in plan): `test/e2e/multi-tenant-auth.e2e.test.ts` with a two-tenant +fixture and a fake IDP server. +- Login/logout/revoke/suspend/expired for tenant A while tenant B stays + logged in; assert B never sees A's session and no cross-tenant cache read. +- Fake IDP injects timeout, 500, malformed JWKS; assert the user-visible + error is explicit and the log carries tenant id and request id. +- Interleaving test: trigger suspension between read and write inside + `SessionMint.mint()`; assert `putIfVersion` rejects and no session exists. +- Cutover tests: each flag value routes as specified; shadow mismatch event + is emitted on a deliberately divergent case and absent on the happy path. + +### CRITICAL regression test (mandatory under the regression rule, no question asked) + +`legacyAuthFlow()` is existing behavior being replaced (`PLAN.md:27-28`) and +the source plan explicitly omitted a regression test (`PLAN.md:14-16`). +Add `test/auth/legacy-parity.regression.test.ts`: +- Record a corpus of real-shaped requests (valid, expired, revoked, + suspended tenant, wrong audience, wrong issuer, malformed) with the + legacy decision for each. +- Run the corpus through the new `AuthBroker` path and assert identical + allow/deny and reason code for every entry. +- This test is also the shadow-mode oracle; it stays after legacy deletion, + re-pointed at the recorded decisions. + +### Unit tests (one file per module, every branch in the diagram) + +- `auth-cache.test.ts`: get hit/miss/expired; invalidate per hook; + putIfVersion stored/rejected-version/rejected-invalidated. +- `session-mint.test.ts`: happy, Rejected → TenantSuspended, IDP failure. +- `auth-broker.test.ts`: each step's variants; boundary mapping for every + variant including `Unexpected`; Promise.all fail-fast; per-call timeout; + metadata cache hit/miss/TTL expiry. +- `request-policy.test.ts`: every policy function, allow/deny/boundary + inputs, null/empty tenant. + +QA test plan artifact (for `/qa` and `/qa-only`): +`~/.gstack/projects/gstack-plan-count-kpTDIg/vercel-sandbox-main-eng-review-test-plan-20260910-211647.md` + +## Performance + +### Issue 5 [P2] (confidence 8/10) `PLAN.md:31-32` — five sequential IDP calls per validation (D9, approved: A) + +Remedy (in plan): +1. `Promise.all` over the independent calls **[Layer 1, standard library]**. + Fail-fast is correct: any failed check means the token is invalid, so + partial results have no value (`Promise.allSettled` not appropriate). +2. Every IDP call wrapped with `AbortSignal.timeout(ms)`; a timeout maps to + `IdpUnavailable`, never a hung login. +3. Per-issuer metadata cache (discovery document, JWKS) with TTL and + key-rotation handling (on unknown `kid`, refresh once, then fail). + Steady-state validation makes one IDP call instead of five. +4. Tests: one-fails fail-fast, timeout path, cache hit/miss/expiry, unknown + kid refresh. + +``` + before: IDP1 -> IDP2 -> IDP3 -> IDP4 -> IDP5 (5 RTT) + after: [discovery, JWKS from cache] + Promise.all([introspect, ...]) (~1 RTT) + each call: AbortSignal.timeout -> IdpUnavailable +``` + +## Failure modes + +| New codepath | Realistic failure | Test | Handling | User sees | +|---|---|---|---|---| +| `AuthCache.putIfVersion` | suspension between read and write | interleaving unit + E2E | Rejected → TenantSuspended | explicit deny | +| `SessionMint.mint` | IDP timeout mid-mint | unit + fake IDP E2E | IdpUnavailable | clear retry message | +| `AuthBroker.validateToken` | one of N calls fails | unit (fail-fast) | Promise.all rejects → InvalidToken/IdpUnavailable | explicit deny | +| `AuthBroker.validateToken` | hung IDP endpoint | unit (timeout) | AbortSignal.timeout | retry message, not a spinner | +| metadata cache | stale JWKS after key rotation | unit (unknown kid) | refresh once, then fail | brief deny, self-heals | +| `validateAndDispatch` boundary | unknown exception | unit (Unexpected) | 500 + error log + deny | generic error, logged | +| shadow mode | paths disagree | cutover test | mismatch event, legacy served | nothing (by design) | +| flag `broker` | new path wrong for a tenant | regression corpus | flag flip rollback | recovers in seconds | + +Critical gaps in the source plan (no test, no handling, silent): swallowed +errors in `validateAndDispatch` and write-after-invalidate on the shared +cache. Both are closed by approved remedies (Issues 1 and 3). Open critical +gaps after review: 0. + +## What already exists + +- Existing cache adapter: tenant/issuer/audience/policy-version keying, + eviction, invalidation hooks, and tests. Reused unchanged; `AuthCache` is + a thin facade. `TokenStore` would have rebuilt this and is cut. +- `legacyAuthFlow()`: retained behind the per-tenant flag as the shadow + oracle and regression baseline until deletion. +- `validateAndDispatch()`: exists; refactored, not rewritten from scratch. +- `Promise.all`, `AbortSignal.timeout`: platform built-ins, no dependency. + +## NOT in scope + +- `TokenStore` class: cut; storage is the adapter's job (D4). +- `RequestPolicy` as a class: deferred until it holds state (D4). +- Deleting `legacyAuthFlow()` and the cutover flag: follow-up PR after the + shadow window (TODO below). +- Replacing or re-keying the existing cache adapter: out of scope; its + contracts are retained by design. +- Adapter-level mutation serialization (locks): not needed once single + writer + version-checked writes are in place. +- New distribution artifacts: none introduced; no pipeline work needed. + +## Diagrams to embed in code + +- `src/auth/auth-cache.ts`: the write-protocol sequence (Issue 1). +- `src/auth/auth-broker.ts`: the validateAndDispatch pipeline (Issue 3). +- `src/auth/cutover.ts` (flag routing): the cutover state machine (Issue 2). +- `test/e2e/multi-tenant-auth.e2e.test.ts`: two-tenant fixture layout. +Update these diagrams in the same commit as any change to the code they +describe. + +## Worktree parallelization strategy + +| Step | Modules touched | Depends on | +|---|---|---| +| S1 AuthCache facade + putIfVersion + tests | src/auth/auth-cache, test/auth | — | +| S2 validateAndDispatch split + typed results + request-policy fns | src/auth/auth-broker, src/auth/request-policy, test/auth | — | +| S3 Promise.all + timeout + metadata cache | src/auth/auth-broker (validateToken), test/auth | S2 | +| S4 SessionMint on injected cache | src/auth/session-mint, test/auth | S1 | +| S5 Cutover flag + shadow compare + regression corpus | src/auth/cutover, src/auth/legacy-auth-flow, test/auth | S2, S4 | +| S6 Two-tenant E2E + fake IDP | test/e2e | S1–S5 | + +Lanes: +- Lane A: S1 → S4 (sequential, shared cache contract) +- Lane B: S2 → S3 (sequential, both in auth-broker) +- Lane C: S5 (after A and B merge) +- Lane D: S6 (after C) + +Execution: launch A and B in parallel worktrees; merge both; then C; then D. +Conflict flag: A and B both add tests under `test/auth/` in different files; +keep file names distinct to avoid merge noise. + +## TODOS.md updates + +TODOS.md does not exist; create it after plan mode exits with this entry +(approved D10): + +- **What:** Delete `legacyAuthFlow()`, the `auth.brokerPath` flag, and the + shadow-compare harness. +- **Why:** Two auth paths double surface area and drift risk; the flag is + scaffolding with a planned exit. +- **Pros:** One path to reason about; smaller codebase. +- **Cons:** Must not happen before the mismatch window closes. +- **Context:** Issue 2 keeps legacy behind a per-tenant flag with shadow + logging. Exit criteria: all tenants on `broker`, zero + `auth.shadow.mismatch` events over the agreed window. Start in the + composition root (remove flag routing), delete legacy and shadow harness, + keep `legacy-parity.regression.test.ts` pointed at recorded decisions. +- **Depends on / blocked by:** this PR merged; shadow window elapsed. + +## Post-plan-mode actions (approved, not plan-file edits) + +- D1: append gstack skill routing rules to `CLAUDE.md` and commit + (`chore: add gstack skill routing rules to CLAUDE.md`). +- D10: create `TODOS.md` with the entry above. + +## Implementation Tasks +Synthesized from this review's findings. Each task derives from a specific +finding above. Run with Claude Code or Codex; checkbox as you ship. + +- [ ] **T1 (P1, human: ~1 day / CC: ~20 min)** — AuthCache — Inject cache by constructor; single writer; add `putIfVersion` with rejection on version change or invalidation; interleaving unit test + - Surfaced by: Architecture — Issue 1, `PLAN.md:19-20`, `:10` + - Files: src/auth/auth-cache.ts, composition root, test/auth/auth-cache.test.ts + - Verify: interleaving test rejects write after invalidate; no module-level `export const authCache` +- [ ] **T2 (P1, human: ~3 days / CC: ~30 min)** — Cutover — Per-tenant `auth.brokerPath` flag (legacy/shadow/broker); shadow mismatch event; legacy retained + - Surfaced by: Architecture — Issue 2, `PLAN.md:27-28` + - Files: src/auth/cutover.ts, src/auth/legacy-auth-flow.ts, test/auth/cutover.test.ts + - Verify: each flag value routes correctly; mismatch event fires on divergent case only +- [ ] **T3 (P1, human: ~1 day / CC: ~20 min)** — AuthBroker — Split `validateAndDispatch` into parse/validate/resolveTenant/dispatch returning a discriminated union; single boundary with tenant+request-id logging; `Unexpected` denies + - Surfaced by: Code Quality — Issue 3, `PLAN.md:23-24` + - Files: src/auth/auth-broker.ts, src/auth/request-policy.ts, test/auth/auth-broker.test.ts, test/auth/request-policy.test.ts + - Verify: no nested try/catch; every variant has a boundary test; grep shows no empty catch +- [ ] **T4 (P1, human: ~1 day / CC: ~15 min)** — Regression — CRITICAL: recorded-corpus parity test legacy vs broker + - Surfaced by: Tests — regression rule, `PLAN.md:14-16`, `:27-28` + - Files: test/auth/legacy-parity.regression.test.ts, test/fixtures/auth-corpus.json + - Verify: 100% decision + reason-code parity across the corpus +- [ ] **T5 (P1, human: ~2 days / CC: ~30 min)** — E2E — Two-tenant suite with fake IDP: login/logout/revoke/suspend/expired, cross-tenant isolation, IDP timeout/500/malformed JWKS, suspend-during-mint + - Surfaced by: Tests — Issue 4, `PLAN.md:14-16` + - Files: test/e2e/multi-tenant-auth.e2e.test.ts, test/support/fake-idp.ts + - Verify: suite green; isolation assertions present for every flow +- [ ] **T6 (P2, human: ~1 day / CC: ~15 min)** — AuthBroker — `Promise.all` over IDP calls, `AbortSignal.timeout` per call, per-issuer discovery/JWKS cache with TTL and unknown-kid refresh + - Surfaced by: Performance — Issue 5, `PLAN.md:31-32` + - Files: src/auth/auth-broker.ts, src/auth/idp-metadata-cache.ts, test/auth/auth-broker.test.ts + - Verify: fail-fast, timeout, cache hit/miss/expiry, unknown-kid tests pass; steady-state call count is 1 +- [ ] **T7 (P2, human: ~2h / CC: ~5 min)** — Scope — Remove `TokenStore` from the design; implement `RequestPolicy` as pure functions + - Surfaced by: Step 0 — D4, `PLAN.md:35-36` + - Files: src/auth/request-policy.ts (no TokenStore file) + - Verify: no `TokenStore` symbol; request-policy has no class or module state +- [ ] **T8 (P3, follow-up)** — Cleanup — Delete legacy path, flag, and shadow harness after the mismatch window + - Surfaced by: TODOS — D10 + - Files: src/auth/cutover.ts, src/auth/legacy-auth-flow.ts, composition root + - Verify: parity regression test still green against recorded decisions + +## Suppressed findings (appendix) + +- [P3] (confidence 4/10) `PLAN.md:35` — `TokenStore` may have an undisclosed + job (e.g. refresh-token persistence). Unverifiable without source; if so, + reopen D4 for that one responsibility. +- [P3] (confidence 4/10) `PLAN.md:36` — `RequestPolicy` may need per-tenant + state from day one. Unverifiable; promotion path documented in D4. + +## Completion summary + +- Step 0: Scope Challenge — scope reduced per recommendation (TokenStore cut, RequestPolicy as functions) +- Architecture Review: 2 issues found (both resolved: A) +- Code Quality Review: 1 issue found (resolved: A) +- Test Review: diagram produced, 33 gaps identified (1 CRITICAL regression, 10 E2E); all added to plan +- Performance Review: 1 issue found (resolved: A) +- NOT in scope: written +- What already exists: written +- TODOS.md updates: 1 item proposed to user (accepted) +- Failure modes: 2 critical gaps flagged in source plan, 0 open after remedies +- Outside voice: skipped (codex_reviews disabled) +- Parallelization: 4 lanes, 2 parallel / 2 sequential +- Lake Score: 5/5 recommendations chose complete option +- Unresolved decisions: 0 + +## GSTACK REVIEW REPORT + +| Review | Trigger | Why | Runs | Status | Findings | +|--------|---------|-----|------|--------|----------| +| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — | +| Outside Review | codex via `/plan-eng-review` (host: claude, phase: plan-review) | Independent 2nd opinion | 1 | disabled | skipped by config (`codex_reviews=disabled`) | +| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | clean (SCOPE_REDUCED) | 5 issues, 0 critical gaps open, 33 test gaps added to plan | +| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — | +| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — | + +- **OUTSIDE COVERAGE:** provider codex, phase plan-review, status disabled (user opt-out via `codex_reviews=disabled`), no findings; no native fallback dispatched because disabled is terminal. Missing outside coverage is recorded, not counted as clean. +- **VERDICT:** ENG CLEARED — ready to implement. Re-enable outside voice with `gstack-config set codex_reviews enabled` if a second model's read is wanted before build. + +NO UNRESOLVED DECISIONS diff --git a/test/helpers/autoplan-artifact-digest.ts b/test/helpers/autoplan-artifact-digest.ts index 4bd794268..eb21e4e98 100644 --- a/test/helpers/autoplan-artifact-digest.ts +++ b/test/helpers/autoplan-artifact-digest.ts @@ -99,21 +99,38 @@ export function createAutoplanEditDigest(file: string, removed: string, added: s } /** Require complete numbered rows and the marker column of this exact diff. */ -export function matchesAutoplanDigestRows(rows: string[], before: Buffer, digest: AutoplanEditDigest): boolean { +export function matchesAutoplanDigestRows(rows: string[], before: Buffer, digest: AutoplanEditDigest, + allowReplacementReset = false): boolean { if (!validAutoplanEditDigest(digest) || sha(before) !== digest.beforeSHA256) return false; const fullRow = /^( {0,3})([1-9]\d*) ([+ -])(.*)$/; const first = rows.findIndex(row => fullRow.test(row)); const leading = first > 0 ? rows.slice(0, first) : []; const chunks: Array<{kind: string; text: string; line: number}> = []; let column: number | undefined, previousLine = 0; + let removedStart: number | undefined, replacementReset = false; for (const row of leading.length ? rows.slice(first) : rows) { const full = fullRow.exec(row); if (full) { const line = Number(full[2]), markerColumn = full[1]!.length + full[2]!.length + 1; - if (!Number.isSafeInteger(line) || line < previousLine || + const kind = full[3]!, previousKind = chunks.at(-1)?.kind; + // The complete native replacement panel numbers the old block first, + // then restarts additions at that block's first line. Only the caller + // that owns this full panel opts in; all row hashes still must match. + const reset = allowReplacementReset && !replacementReset && previousKind === '-' && kind === '+' && line === removedStart; + if (!Number.isSafeInteger(line) || (line < previousLine && !reset) || (column !== undefined && column !== markerColumn)) return false; + if (allowReplacementReset) { + if ((kind === '-' || kind === '+') && kind === previousKind && line !== previousLine + 1) return false; + if (kind === '-' && previousKind !== '-') { + if (removedStart !== undefined || chunks.some(c => c.kind === '+')) return false; + removedStart = line; + } + if (previousKind === '-' && kind !== '-' && !reset) return false; + if (kind === '+' && previousKind !== '+' && removedStart !== undefined && !reset) return false; + if (reset) replacementReset = true; + } column = markerColumn; previousLine = line; - chunks.push({kind: full[3]!, text: full[4]!, line}); + chunks.push({kind, text: full[4]!, line}); } else { if (column === undefined || !row.startsWith(' '.repeat(column))) return false; const last = chunks.at(-1), kind = row[column]; @@ -122,6 +139,25 @@ export function matchesAutoplanDigestRows(rows: string[], before: Buffer, digest } } const originals = new Set(before.toString('utf8').split('\n').map(autoplanEditLineHash)); + if (allowReplacementReset) { + const oldRows = chunks.filter(c => c.kind !== '+').map(c => autoplanEditLineHash(c.text)); + const newRows = chunks.filter(c => c.kind !== '-').map(c => autoplanEditLineHash(c.text)); + const starts = (rows: string[], hashes: string[]) => rows.flatMap((_, index) => + hashes.every((hash, offset) => rows[index + offset] === hash) ? [index] : []); + const oldStarts = starts(oldRows, digest.oldLineHashes), newStarts = starts(newRows, digest.newLineHashes); + if (oldStarts.length !== 1 || newStarts.length !== 1 || oldStarts[0] !== newStarts[0]) return false; + const originalLines = before.toString('utf8').split('\n').map(autoplanEditLineHash); + const delta = digest.newLineHashes.length - digest.oldLineHashes.length; + let oldLine = chunks[0]!.line, newLine = oldLine, added = false; + for (const row of chunks) { + if (row.kind !== '+') { + if (row.line !== oldLine + (added ? delta : 0) || autoplanEditLineHash(row.text) !== originalLines[oldLine - 1]) return false; + oldLine++; + } + if (row.kind !== '-' && row.line !== newLine++) return false; + if (row.kind === '+') added = true; + } + } let authenticatedClip = false; if (leading.length) { const c = digest.clippedAdditions, next = chunks[0]; diff --git a/test/helpers/autoplan-artifact-permission.ts b/test/helpers/autoplan-artifact-permission.ts index 08e950593..fd0c35838 100644 --- a/test/helpers/autoplan-artifact-permission.ts +++ b/test/helpers/autoplan-artifact-permission.ts @@ -313,6 +313,23 @@ function queuedArtifactViewport(viewport: string, current: NativePublicToolEvent return lines.slice(panel).join('\n'); } +/** A native command description can remain above an unpublished Edit panel. + * Its text supplies no command identity, completion, or approval authority. + * Only the digest-bound pending path may discard this one display prefix. */ +function pendingCommandDisplayViewport(viewport: string): string { + const text = viewport.replace(/\r\n?/g, '\n'); + const panels = [...text.matchAll(/^[─╌]{8,}\n {0,3}Edit file[ \t]*\n/gm)]; + if (panels.length !== 1 || panels[0]!.index === 0) return viewport; + const prefix = text.slice(0, panels[0]!.index).split('\n').filter(line => line.trim()); + const title = /^[●⏺] ([^\n]+)$/.exec(prefix[0] ?? '')?.[1]; + if (!title || /^(?:["'`“‘]|(?:source|example|quoted|history|historical|hypothetical|previous|earlier)\b)/i.test(title) || + !/^ {2}⎿[ \u00a0]+\$ \S.*$/.test(prefix[1] ?? '') || + prefix.slice(2).some(line => !/^ {5}\S.*$/.test(line)) || + prefix.slice(1).some(line => /^[ \t]*[●⏺❯☐□>]|^[ \t]*(?:`{3,}|~{3,})/.test(line)) || + /(?:Do you want|Would you like|Bash command[^\n]*permission|requested permissions?|allow all edits|always allow access|Esc to cancel|Edit file)/i.test(prefix.join('\n'))) return viewport; + return text.slice(panels[0]!.index); +} + /** Metadata-only fallback. Added rows are display evidence, never request content. */ export function pendingAutoplanArtifactPermissionInput(viewport: string, context: ArtifactPermissionContext & { pending?: PendingAutoplanArtifact; viewportCapturedAt: number }, @@ -349,7 +366,8 @@ export function pendingAutoplanArtifactPermissionInput(viewport: string, !mutations.some(e => e.input?.file_path === p.file && results.get(e.toolUseId)?.isError === false && Date.parse(results.get(e.toolUseId)!.timestamp) <= pendingTime)) return null; try { - const text = viewport.replace(/\r\n?/g, '\n'); + const currentViewport = p.editDigest ? pendingCommandDisplayViewport(viewport) : viewport; + const text = currentViewport.replace(/\r\n?/g, '\n'); const menu = /^ {0,3}Do you want to make this edit to ([^\n?]+)\? *\n {0,3}❯ *1\. Yes *\n {0,3}2\. Yes, and switch to accept edits \(auto-approve file edits and common file commands\) for this session(?: \(shift\+tab\))? *\n {0,3}3\. No *\n\s*Esc to cancel [·•] Tab to amend\s*$/m.exec(text); if (!menu || menu.index + menu[0].length !== text.length || menu[1] !== path.basename(p.file)) return null; const rows = text.slice(0, menu.index).trimEnd().split('\n'); @@ -360,7 +378,8 @@ export function pendingAutoplanArtifactPermissionInput(viewport: string, if (p.editDigest) { const before = readAutoplanDigestFile(p.file); if (!before || createHash('sha256').update(before).digest('hex') !== p.editDigest.beforeSHA256) return null; - if (matchesAutoplanDigestRows(diffRows,before,p.editDigest)) return {input:'1\r', signature, file:p.file}; + if (matchesAutoplanDigestRows(diffRows,before,p.editDigest,currentViewport !== viewport)) return {input:'1\r', signature, file:p.file}; + if (currentViewport !== viewport) return null; // The new prefix path requires the exact digest, including additions. // Digest authority adds insertion-only crops; existing anchored deletion // authority remains available after the current-file binding succeeds. } diff --git a/test/helpers/autoplan-phase-observer.ts b/test/helpers/autoplan-phase-observer.ts index 5a78e20b5..a1471c860 100644 --- a/test/helpers/autoplan-phase-observer.ts +++ b/test/helpers/autoplan-phase-observer.ts @@ -11,12 +11,13 @@ function phaseDeclaration(text: string): RegExpExecArray | null { if (/\bEmit\s+phase-transition\s+summary\s*:/i.test(plain)) return null; let match = new RegExp(String.raw`^${declaration}(?:[.:](?:[ \t]+.*)?|)$`, 'i').exec(plain); if (!match) { - // A dash can separate an actual completion from its retained-work recap. - // Keep this new continuation affirmative; source, future and withdrawn - // claims cannot supply the missing phase declaration. - const dash = new RegExp(String.raw`^${declaration}[ \t]*[—–][ \t]*(.+)$`, 'i').exec(plain); - const tail = dash?.[3]?.trim(); - if (tail && !/^["“'‘>]|\?|\b(?:if|unless|when|once|pending|maybe|perhaps|would|could|will|source|example|sample|quote(?:d)?|historical|earlier|previous(?:ly)?|template|not|no|never|superseded|provided|rejected|incomplete|unfinished|withdrawn|retracted|cancelled|canceled)\b/i.test(tail)) match = dash; + // A dash or "with" can introduce the results of an actual completion. + // Keep the recap affirmative; source, future and withdrawn claims cannot + // supply the missing phase declaration. + const recap = new RegExp(String.raw`^${declaration}(?:[ \t]*[—–][ \t]*|[ \t]+(?with)[ \t]+)(.+)$`, 'i').exec(plain); + const tail = recap?.[4]?.trim(); + if (tail && !/^["“'‘>]|\?|\b(?:if|unless|when|once|pending|maybe|perhaps|would|could|will|source|example|sample|quote(?:d)?|historical|earlier|previous(?:ly)?|template|not|no|never|superseded|provided|rejected|incomplete|unfinished|withdrawn|retracted|cancelled|canceled)\b/i.test(tail)) match = recap; + if (match?.groups?.withResult && /\bhypothetic(?:al|ally)\b/i.test(tail!)) match = null; } // Optional phase names are metadata, and must agree with the phase number. const names: Record = { @@ -29,6 +30,31 @@ function phaseDeclaration(text: string): RegExpExecArray | null { return match; } +/** An explicit current withdrawal in the same announcement cancels a new with-result claim. */ +function withResultWithdrawn(lines: string[], index: number, phase: string): boolean { + let source = false; + let fence: {char: string; length: number} | undefined; + const owner = new RegExp(String.raw`^(?:(?:correction|current status)[ \t]*:[ \t]*)?(?:this[ \t]+(?:phase|completion|declaration|announcement)|Phase[ \t]+${phase.replace('.', '\\.')})(?:[ \t]+(?:completion|declaration|status))?[ \t]*(?:(?:is|was|has been)[ \t]+|:[ \t]*)(.+)$`, 'i'); + for (const line of lines.slice(index + 1)) { + if (/^(?: {4}|\t)/.test(line)) continue; + const text = line.trim().replace(/\*\*/g, ''); + const delimiter = /^(`{3,}|~{3,})/.exec(text)?.[1]; + if (delimiter) { + if (!fence) fence = {char: delimiter[0]!, length: delimiter.length}; + else if (delimiter[0] === fence.char && delimiter.length >= fence.length && !text.slice(delimiter.length).trim()) fence = undefined; + continue; + } + if (fence || /^[>"“'‘]/.test(text)) continue; + if (/\b(?:example|sample|quote(?:d)?|source|historical|earlier|previous|archived|template)\b.*[::]\s*$/i.test(text)) { source = true; continue; } + if (/^(?:current (?:status|review|phase)|correction)\b/i.test(text.replace(/^#{1,6}[ \t]+/, ''))) source = false; + if (source) continue; + if (phaseDeclaration(text)) break; + const status = owner.exec(text)?.[1]?.replace(/["“”'‘’`]/g, ''); + if (status && /^(?:withdrawn|retracted|cancelled|canceled|superseded|rejected|incomplete|unfinished|not (?:complete(?:d)?|current)|no longer (?:complete(?:d)?|current))\b/i.test(status)) return true; + } + return false; +} + /** * Observe actual assistant announcements from this fixture's native transcript. * The terminal renders Markdown bold as ANSI, and its Read output can contain @@ -81,11 +107,13 @@ export function autoplanPhaseCompletions( !/\[[^\]]*\]|\{\{/.test(block.join('\n')); if (concreteSummary) match = phaseDeclaration(text.replace(/^>\s?/, '')); } - const introducedExample = /\b(?:example|sample|quote(?:d)?|source|template|instruction|marker|expected\s+(?:output|announcement))\b.*[::]\s*$/i.test(previousLine); + const introducedExample = /\b(?:example|sample|quote(?:d)?|source|template|instruction|marker|expected\s+(?:output|announcement))\b.*[::]\s*$/i.test(previousLine) || + (!!match?.groups?.withResult && /\b(?:example|sample|quote(?:d)?|source|historical|earlier|previous|archived|hypothetical|template)\b.*[::]\s*$/i.test(previousLine.replace(/\*\*/g, ''))); // Keep an example introduction across all its marker/quoted lines, // rather than allowing its second marker to look like real completion. if (text && !(introducedExample && (match || text.startsWith('>')))) previousLine = text; if (!match || introducedExample) continue; + if (match.groups?.withResult && withResultWithdrawn(lines, index, match[1]!)) continue; const phase = Number(match[1]); if (!hits.some(hit => hit.phase === phase)) hits.push({ phase, ts: Date.parse(message.timestamp) }); } diff --git a/test/helpers/eng-cache-writer-decision.ts b/test/helpers/eng-cache-writer-decision.ts index fef156ca5..92c85ab37 100644 --- a/test/helpers/eng-cache-writer-decision.ts +++ b/test/helpers/eng-cache-writer-decision.ts @@ -4,17 +4,24 @@ import type { NativePlanQuestion } from './plan-count-transcript'; * Called only after engNumberedFindingAUQ validates completed native metadata. */ export function engCacheWriterDecision(q: NativePlanQuestion): boolean { const lines = q.question.split('\n'); - const ordinal = /^D([1-9]\d*) [—–:-] Who is allowed to write to the auth cache\?$/.exec(lines[0] ?? '')?.[1]; - if (!ordinal || q.header !== 'Cache writes') return false; - const context = /^Project\/branch\/task: (\S[^\n]*), ([A-Za-z_$][\w$]*) and ([A-Za-z_$][\w$]*) both mutating one backing cache \(([\w./-]+\.md):\d+(?:, \d+(?:[-–]\d+)?)?\)\.$/.exec(lines[1] ?? ''); - const assessment = /^ELI10: Two services write to the same cache and nothing orders their writes\. /.test(lines[2] ?? ''); - if (!context || context[2] === context[3] || !assessment || + // Severity, confidence and source citations annotate an owned issue; they + // never replace its current defect, assessment or opposed choices. + const annotated = /^D([1-9]\d*) [—–:-] Issue ([1-9]\d*) \[P[0-3]\] \(confidence (?:10|[1-9])\/10\) [A-Za-z][\w./-]*:[1-9]\d*(?:-[1-9]\d*)?(?: \+ :[1-9]\d*(?:-[1-9]\d*)?)? [—–:-] ([A-Za-z_$][\w$]*) and ([A-Za-z_$][\w$]*) both mutate one module-level ([A-Za-z_$][\w$]*) that does not serialize mutations\. How should the shared cache be wired\?$/.exec(lines[0] ?? ''); + const ordinal = annotated?.[1] ?? /^D([1-9]\d*) [—–:-] Who is allowed to write to the auth cache\?$/.exec(lines[0] ?? '')?.[1]; + if (!ordinal || (annotated ? q.header !== `Arch ${annotated[2]}` : q.header !== 'Cache writes')) return false; + const context = annotated ? /^Project\/branch\/task: (\S[^\n]*)\.$/.exec(lines[1] ?? '') + : /^Project\/branch\/task: (\S[^\n]*), ([A-Za-z_$][\w$]*) and ([A-Za-z_$][\w$]*) both mutating one backing cache \(([\w./-]+\.md):\d+(?:, \d+(?:[-–]\d+)?)?\)\.$/.exec(lines[1] ?? ''); + const actors = annotated ? [annotated[3]!, annotated[4]!] : [context?.[2] ?? '', context?.[3] ?? '']; + const assessment = annotated + ? /^ELI10: Two services share one global cache object exported from a module, and both write to it\. /.test(lines[2] ?? '') + : /^ELI10: Two services write to the same cache and nothing orders their writes\. /.test(lines[2] ?? ''); + if (!context || actors[0] === actors[1] || !assessment || lines.filter(line => /^Project\/branch\/task:/.test(line)).length !== 1 || lines.filter(line => /^ELI10:/.test(line)).length !== 1) return false; const boundary = '(?:^|[.!?;]\\s+|\\n|[✅❌]\\s*)(?:Correction:\\s*)?'; - const owner = `(?:(?:this|the|that) (?:finding|issue|gap|assessment|option|action|remedy|race|single-writer requirement)|D\\s*${ordinal})`; - const status = '(?:withdrawn|superseded|rejected|cancelled|canceled|resolved|closed|hypothetical|not current|no longer current)'; - const current = (text: string) => text + const owner = `(?:(?:this|the|that) (?:finding|issue|gap|assessment|option|action|remedy|race|single-writer requirement)|D\\s*${ordinal}${annotated ? `|Issue ${annotated[2]}` : ''})`; + const status = `(?:withdrawn|superseded|rejected|cancelled|canceled|resolved|closed|hypothetical|not current|no longer current${annotated ? '|optional|unproven|proposed|conditional on approval' : ''})`; + const current = (text: string) => (annotated ? text.replace(/\*\*/g, '').replace(/\((?:human|CC):[^)\n]*\)[ \t]+(?=(?:Correction:\s*)?(?:this|the|that) (?:option|action|remedy)\b)/gi, '$&. ') : text) .replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '') .replace(/^(?:\s*>| {4}|\t).*$/gm, '') .replace(new RegExp(`(${boundary}${owner} (?:is|was|has been) )["“'‘\x60](${status})["”'’\x60]`, 'gim'), '$1$2') @@ -27,9 +34,31 @@ export function engCacheWriterDecision(q: NativePlanQuestion): boolean { if (/\b(?:if|when|once|unless) (?:approved|accepted)|\b(?:after|pending) approval\b/i.test(current(context[1]!)) || framed.test(current(context[1]!)) || framed.test(current(lines.slice(0, 3).join('\n').split('ELI10:')[0]!)) || closed.test(current(q.question))) return false; const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const actors = [context[2]!, context[3]!]; const contradiction = new RegExp(`${boundary}(?:(?:the|these|both) services (?:no longer (?:writes?|mutates?)|now serialize)|(?:${actors.map(escape).join('|')}) (?:no longer|does not) (?:writes?|mutates?)|(?:the |this )?(?:auth )?cache (?:is no longer shared|now serializes)|(?:the )?(?:race is (?:resolved|closed)|(?:writes|writers) are (?:now )?(?:ordered|serialized)))\\b`, 'i'); if (contradiction.test(current(q.question))) return false; + if (annotated) { + const text = current(q.question), explanation = current(lines[2] ?? ''); + const conditional = new RegExp(`\\b(?:if|when|once|unless) (?:approved|accepted)|\\b(?:after|pending) approval\\b|${boundary}${owner} (?:requires (?:approval|acceptance)|is (?:conditional|contingent|dependent) on (?:approval|acceptance))\\b`, 'i'); + const namedResolved = new RegExp(`${boundary}${escape(annotated[5]!)} (?:is (?:now |already )?(?:serialized|ordered)|no longer (?:shares|has) (?:mutable )?state)\\b`, 'i'); + const scenario = /while ([A-Za-z_$][\w$]*) is halfway through minting, the mint can land after the invalidation and a suspended tenant keeps a live session\./.exec(explanation); + if (!scenario || !actors.includes(scenario[1]!) || framed.test(text) || conditional.test(text) || namedResolved.test(text)) return false; + const rows = q.options.map(option => ({ label: option.label.replace(/ \(recommended\)$/, ''), text: current(option.description ?? '').trim() })); + const cancelled = new RegExp(`${boundary}(?:do not|don't|never|skip|cancel|withdraw) (?:inject|use|keep|accept|adopt|choose|proceed|reject|write|serialize|document)\\b`, 'i'); + if (rows.some(row => framed.test(row.text) || closed.test(row.text) || conditional.test(row.text) || cancelled.test(row.text))) return false; + const remedy = rows.find(row => row.label === 'Inject + single-writer + version-checked writes'); + const unchanged = rows.find(row => row.label === 'Keep module-level export as planned'); + if (!remedy || !unchanged) return false; + const writer = new RegExp(`^✅\\s*${escape(annotated[5]!)} passed into both services by constructor from one composition root; ([A-Za-z_$][\\w$]*) is the only writer, ([A-Za-z_$][\\w$]*) reads and invalidates\\.`).exec(remedy.text); + if (!writer || writer[1] === writer[2] || !actors.includes(writer[1]) || !actors.includes(writer[2]) || + !/✅\s*Writes carry the policy version and are rejected if the entry was invalidated since read \(compare-and-set\), with a unit test for the interleaving\./.test(remedy.text) || + !/❌\s*Global mutable state shared by two writers, no serialization, order-dependent tests, and a silent tenant-isolation hole\./.test(unchanged.text) || + contradiction.test(unchanged.text) || namedResolved.test(unchanged.text)) return false; + const override = new RegExp(`${boundary}(?:${escape(writer[2]!)} (?:also |still )?writes|${escape(writer[1]!)} (?:does not|no longer) writes|(?:the )?adapter (?:accepts stale writes|does not reject stale writes)|(?:the )?(?:version check|single-writer requirement) is (?:removed|disabled|optional))\\b`, 'i'); + if (override.test(remedy.text) || new RegExp(`${boundary}only (?:${actors.map(escape).join('|')}) writes\\b`, 'i').test(unchanged.text)) return false; + return rows.every(row => row === remedy || row === unchanged || row.label === 'Constructor injection only' && + /❌\s*Both services still write freely; the write-after-invalidate race stays open\b/.test(row.text) && !contradiction.test(row.text) && + !namedResolved.test(row.text) && !new RegExp(`${boundary}only (?:${actors.map(escape).join('|')}) writes\\b`, 'i').test(row.text)); + } const rows = q.options.map(option => ({ id: new RegExp(`^${ordinal}([A-D]) (.+?)(?: \\(recommended\\))?$`).exec(option.label), text: current(option.description ?? '').trim(), diff --git a/test/helpers/eng-retained-corpus.ts b/test/helpers/eng-retained-corpus.ts new file mode 100644 index 000000000..f0d80e66d --- /dev/null +++ b/test/helpers/eng-retained-corpus.ts @@ -0,0 +1,67 @@ +/** A recorded legacy oracle can survive removal of the implementation it sampled. */ +export function hasRetainedLegacyCorpus( + current: ReadonlyArray<{ title: string; body: string[] }>, snapshot: string, +): boolean { + const flat = (s: string) => s.replace(/\s+/g, ' ').trim(); + const quoted = (s: string) => s.replace(/"[^"\n]*"|“[^”\n]*”|(? /(?:^|\n)\s*(?:source(?: excerpt| material)?|quoted(?: source)?|historical(?: example| assessment)?|if approved|once approved|when approved|pending approval|assuming approval|provided approval)\s*[,.:—-]/i.test(quoted(s)); + const status = '(?:withdrawn|rejected|declined|cancelled|canceled|superseded|deferred|optional|proposed|not current|no longer current|not required|no longer required|conditional on approval)'; + const owner = '(?:(?:this|the) (?:(?:legacy|recorded|baseline) )?(?:(?:regression|parity) )?(?:suite|test|requirement|verification|oracle|corpus))'; + const scalar = (s: string, id: string) => quoted(s.replace(new RegExp( + `((?:^|[.!?;]\\s+|\\n)\\s*(?:Correction:\\s*)?(?:${id}(?: verification)?|${owner}) (?:is|are|was|were|has been|have been) )["“'‘](${status})["”'’]`, 'gim'), '$1$2')); + const inactive = (s: string, id: string) => source(s) || new RegExp( + `\\b(?:${id}(?: verification)?|${owner}) (?:is|are|was|were|has been|have been) ${status}\\b|\\b(?:only if|unless|pending) (?:user )?approv`, 'i').test(scalar(s, id)); + const negated = (s: string) => /\b(?:do not|don't|never|skip|omit|defer|cancel) (?:add|write|implement|record|capture|pin|run|assert|retain|keep)\b/i.test(quoted(s)); + const owned = (s: string) => !source(s) && !negated(s) && snapshot.includes(flat(s)); + + for (const declaration of current) { + if (!/^CRITICAL regression test \([^)]*\bmandatory\b[^)]*\)$/i.test(declaration.title) + || /\b(?:not|never|no longer) mandatory\b/i.test(declaration.title)) continue; + const body = declaration.body.join('\n').trim(); + const add = /^Add ([A-Za-z][\w/.-]*\.test\.[jt]s):$/m.exec(body); + if (!add || !owned(body) || inactive(body, 'T[1-9]\\d*')) continue; + const bullets = body.split(/\n(?=- )/).slice(1).map(flat); + const capture = bullets.findIndex(s => /^- (?:Record|Capture|Pin) a corpus of .+ with the legacy decision for each\.$/i.test(s)); + const parity = bullets.findIndex(s => /^- Run the (?:same )?corpus through the new [A-Za-z][\w]* path and assert identical allow\/deny and reason code for every entry\.$/i.test(s)); + if (capture < 0 || parity <= capture || !bullets.some(s => /^- This test is also the shadow-mode oracle; it stays after legacy deletion, re-pointed at the recorded decisions\.$/i.test(s))) continue; + // Retention makes the old behavior the oracle. Building independent new + // modules before recording it does not itself rewrite that old behavior. + const retention = current.filter(s => s.title === 'What already exists').flatMap(s => s.body.join('\n').split(/\n(?=- )/)) + .find(s => /^- legacyAuthFlow\(\): retained behind the per-tenant flag as the shadow oracle and regression baseline until deletion\.$/i.test(flat(s)) && owned(s) && !inactive(s, 'T[1-9]\\d*')); + if (!retention) continue; + for (const section of current.filter(s => s.title === 'Implementation Tasks')) { + const taskBody = section.body.join('\n').trim(); + const tasks = taskBody.split(/\n(?=- )/); + const rows = tasks.map(body => ({ body, match: /^- (?:\[[ xX]\] )?(T[1-9]\d*)(?: \([^\n)]*\))? [—–-] (.+)(?:\n|$)/.exec(body) })).filter(row => row.match); + if (rows.length !== new Set(rows.map(row => row.match![1])).size) continue; + for (const row of rows) { + const id = row.match![1]!, title = row.match![2]!; + const action = row.body.replace(/^ - Surfaced by:.*$/gm, ''); + const preceding = taskBody.slice(0, taskBody.indexOf(row.body)).trim().split('\n').at(-1) ?? ''; + const files = [...row.body.matchAll(/^ - Files: ([^\n]+)$/gm)]; + const verifies = [...row.body.matchAll(/^ - Verify: ([^\n]+)$/gm)]; + if (!/^[A-Za-z][\w -]* [—–-] CRITICAL: recorded-corpus parity test legacy vs [A-Za-z][\w]*$/i.test(title) + || files.length !== 1 || verifies.length !== 1 || !snapshot.includes(flat(row.body)) || source(action) || negated(action) || source(preceding) + || inactive(action, id) || !files[0]![1]!.split(',').map(s => s.trim()).includes(add[1]!)) continue; + if (!/^100% decision \+ reason-code parity across the corpus$/i.test(verifies[0]![1]!)) continue; + const cancelled = current.some(s => { + if (/\b(?:history|historical|source|quoted|example)\b/i.test(s.title)) return false; + const raw = s.body.join('\n'), value = scalar(raw, id); + const named = /^(.*?)\b(?:regression|characterization|parity)\s+(?:suite|test)/i.exec(s.title)?.[1]?.trim(); + const foreign = Boolean(named && !/^(?:(?:critical|recorded|required|current|final|updated)\s*)*(?:legacy(?:AuthFlow\(\))?)?[\s:—-]*$/i.test(named)); + if (new RegExp(`^\\s*\\|\\s*${id}\\s*\\|\\s*["“'‘]?${status}["”'’]?\\s*\\|`, 'im').test(raw)) return true; + return value.split(/\n|[.!?;]\s+/).some(line => { + if (source(line) || /^\s*(?:if|unless|assuming|provided)\b/i.test(line)) return false; + if (foreign && !new RegExp(`\\b${id}\\b|legacyAuthFlow|\\blegacy (?:regression|parity) (?:suite|test|corpus|baseline)\\b`, 'i').test(line)) return false; + return inactive(line, id) || new RegExp(`^\\s*(?:Correction:\\s*)?legacyAuthFlow\\(\\) (?:is|was|has been|will be) (?:modified|changed|rewritten|refactored|removed|deleted) before ${id}\\b`, 'i').test(line) + || /^\s*(?:Correction:\s*)?legacyAuthFlow\(\) (?:is|was|has been|will be) (?:modified|changed|rewritten|refactored|removed|deleted) before (?:the )?(?:(?:legacy|recorded|regression) )?(?:corpus|baseline)(?: is)? (?:recorded|captured|pinned)\b/i.test(line) + || /^\s*(?:the|this) (?:(?:legacy|regression) )?(?:corpus|baseline) is (?:recorded|captured|pinned) (?:only )?after legacyAuthFlow\(\) is (?:modified|changed|rewritten|refactored|removed|deleted)\b/i.test(line) + || /\b(?:the|this) legacy (?:regression baseline|shadow oracle) (?:is|was|has been) (?:changed|rewritten|removed|deleted|replaced)\b/i.test(line); + }); + }); + if (!cancelled) return true; + } + } + } + return false; +} diff --git a/test/helpers/eng-seeded-coverage.ts b/test/helpers/eng-seeded-coverage.ts index 84b578154..5f9047f2d 100644 --- a/test/helpers/eng-seeded-coverage.ts +++ b/test/helpers/eng-seeded-coverage.ts @@ -1,4 +1,5 @@ import type { NativePlanQuestionCall, PlanCountTranscript } from './plan-count-transcript'; +import { hasRetainedLegacyCorpus } from './eng-retained-corpus'; /** Evidence for this fixture's four decision seeds; regression coverage is auto-added by the skill. */ export const ENG_DECISION_SEEDS = ['complexity', 'shared-cache', 'swallowed-errors', 'sequential-idp'] as const; @@ -796,7 +797,82 @@ function declaredLegacyCharacterization(text: string): boolean { } } - return false; + // A required-test item may own the named legacy oracle while a separate + // parity item and task preserve its compatibility obligation. Bind these + // small obligations structurally; unrelated prose cannot complete the chain. + for (const section of current.filter(s => /^Required tests(?: \([^\n]*\))?$/i.test(s.title))) { + const blocks = section.body.join('\n').trim().split(/\n\s*\n/); + for (const block of blocks) { + const claim = block.replace(/\s+/g, ' ').trim(); + const rule = /^CRITICAL\s*\([^)]*\bmandatory\b[^)]*\):\s*([A-Za-z][\w/.-]*\.test\.[jt]s)\./i.exec(claim); + if (!rule || /\b(?:not|never|no longer)\s+mandatory\b/i.test(rule[0]) || !/^\s*(?:Pin|Capture|Record) current behavior of legacyAuthFlow\(\) before any (?:change|rewrite|refactor):/i.test(claim.slice(rule[0].length)) + || !/\bThis is the oracle for the parity suite\b/i.test(claim) || !snapshotSource.includes(claim)) continue; + const quoteFree = (value: string) => value.replace(/"[^"\n]*"|“[^”\n]*”|(? quoteFree(value.replace(new RegExp( + `((?:^|[.!?;]\\s+|\\n)[\\t ]*(?:Correction:\\s*)?(?:${ids}|${subject}) (?:is|are|was|were|has been|have been) )["“'‘](${inactiveWords})["”'’]`, 'gim'), '$1$2')); + // Input conditions describe asserted behavior, including accepted tokens. + // An implicit approval or an explicit work/approver subject instead + // governs whether this work exists. + const conditionalApproval = /(?:^|[.!?;]\s+|\n)[\t ]*(?:if|unless|assuming|provided|once|when|pending|after)\s+(?:(?:approv\w*|authoriz\w*|consent|confirm\w*|accept\w*|desired|requested|needed)\b|(?:(?:this|the|our) )?(?:work|plan|review|task|baseline|parity|regression|characterization|suite|tests?|requirement|verification|proposal|implementation|T[1-9]\d*|we|you|they|reviewer|owner|user)\b[^.!?;,\n]{0,80}\b(?:approv\w*|authoriz\w*|consent|confirm\w*|accept\w*|desired|requested|needed|proceed)\b)/i; + const unowned = (value: string) => extractionSourceOwner(value) || /\b(?:proposed|optional|hypothetical|unproven|maybe|might|could)\b/i.test(quoteFree(value)) || conditionalApproval.test(quoteFree(value)); + const inactive = (value: string, ids: string) => unowned(value) || new RegExp( + `\\b(?:${ids}|${subject}) (?:is|are|was|were|has been|have been) ${inactiveWords}\\b|\\b(?:skip|defer|omit) (?:the |this )?(?:baseline|regression|characterization|parity) (?:test|suite|verification)`, 'i').test(currentText(value, ids)); + if (inactive(block, 'T[1-9]\\d*')) continue; + for (const parityBlock of blocks) { + const parity = parityBlock.replace(/\s+/g, ' ').trim(); + const comparison = /^Decision [1-9]\d*[A-D], parity suite:\s*([A-Za-z][\w/.-]*\.test\.[jt]s)\./i.exec(parity); + if (!comparison || inactive(parityBlock, 'T[1-9]\\d*') || !snapshotSource.includes(parity) + || !/^\s*(?:One|The same) fixture table, each row run through both paths \(flag off, flag on\), assert identical outcomes?\b/i.test(parity.slice(comparison[0].length))) continue; + for (const tasks of current.filter(s => s.title === 'Implementation Tasks')) { + const taskBody = tasks.body.join('\n').trim(), taskBlocks = taskBody.split(/\n(?=-\s)/); + const rows = taskBlocks.map(body => ({ body, match: /^- (?:\[[ xX]\] )?(T[1-9]\d*)(?: \([^\n)]*\))? [—–-] ([^\n]+)(?:\n|$)/.exec(body) })).filter(row => row.match); + if (rows.length !== new Set(rows.map(row => row.match![1])).size) continue; + const field = (body: string, name: string) => { + const found = [...body.matchAll(new RegExp(`^ - ${name}: ([^\\n]+)$`, 'gm'))]; + return found.length === 1 ? found[0]![1] : undefined; + }; + // Match the asserted action after its component label. A later + // positive test phrase cannot override a leading "Do not write". + const taskAction = (title: string) => /^[^\n]*?\s+[—–-]\s+(.+)$/.exec(title)?.[1] ?? title; + const ownedTask = (body: string) => { + // Source quotations identify the finding but cannot grant or revoke + // its separately asserted action or verification. + const action = body.replace(/^ - Surfaced by:.*$/gm, ''); + const preceding = taskBody.slice(0, taskBody.indexOf(body)).trim().split('\n').at(-1) ?? ''; + return !unowned(action) && !unowned(preceding) && snapshotSource.includes(body.replace(/\s+/g, ' ').trim()); + }; + for (const baseline of rows) { + const title = baseline.match![2]!, id = baseline.match![1]!; + if (!/^(?:(?:Write|Add|Create) (?:the )?)?CRITICAL (?:regression|characterization) tests? (?:pinning|capturing|recording) current legacyAuthFlow\(\) behavior before any (?:change|rewrite|refactor)\b/i.test(taskAction(title)) + || field(baseline.body, 'Files') !== rule[1] || !ownedTask(baseline.body) + || !/^(?:test|suite) passes against (?:unmodified|unchanged|untouched) legacyAuthFlow\(\) (?:first|before any (?:change|rewrite|refactor))$/i.test(field(baseline.body, 'Verify') ?? '')) continue; + for (const next of rows) { + const nextId = next.match![1]!; + if (nextId === id || field(next.body, 'Files') !== comparison[1] || !ownedTask(next.body) + || !/^(?:(?:Write|Add|Implement) (?:the )?)?(?:Table-driven )?parity suite running each fixture row through flag-off and flag-on paths\b/i.test(taskAction(next.match![2]!)) + || !/^suite green for every row\b/i.test(field(next.body, 'Verify') ?? '')) continue; + const ids = `(?:${id}|${nextId})(?: (?:baseline requirement|baseline verification|verification|rerun))?`; + if ([block, parityBlock, baseline.body, next.body].some(value => inactive(value.replace(/^ - Surfaced by:.*$/gm, ''), ids))) continue; + const cancelled = new RegExp(`\\b${ids} (?:is|was|has been) ${inactiveWords}\\b`, 'i'); + const changedFirst = new RegExp(`^(?:Correction:\\s*)?legacyAuthFlow\\(\\) (?:is|was|has been|will be) (?:modified|changed|rewritten|refactored) before ${id}\\b`, 'i'); + const withdrawn = current.some(s => { + if (/\b(?:history|historical|source|quoted|example)\b/i.test(s.title)) return false; + const body = s.body.join('\n'); + if (new RegExp(`^\\s*\\|\\s*(?:${id}|${nextId})\\s*\\|\\s*["“'‘]?${inactiveWords}["”'’]?\\s*\\|`, 'im').test(body)) return true; + return currentText(body, ids).split(/\n|[.!?;]\s+/).some(line => !extractionSourceOwner(line) && !/\b(?:if|unless|assuming|provided)\b|\b(?:once|when|pending|after)\b[^.!?\n]{0,50}\bapprov/i.test(line) && (cancelled.test(line) || changedFirst.test(line.trim()) + || new RegExp(`\\b(?:the|this) legacy (?:regression|characterization) (?:suite|tests?|requirement) (?:is|are|was|were|has been|have been) ${inactiveWords}\\b`, 'i').test(line))); + }); + if (!withdrawn) return true; + } + } + } + } + } + } + + return hasRetainedLegacyCorpus(current, snapshotSource); } function regressionEvidence(text: string): boolean { diff --git a/test/helpers/skill-fixture.ts b/test/helpers/skill-fixture.ts index 26f975c54..529110751 100644 --- a/test/helpers/skill-fixture.ts +++ b/test/helpers/skill-fixture.ts @@ -13,7 +13,8 @@ * order given. For tests that exercise specific workflow steps. * - extractSkillBody(skillDir) * frontmatter + intro + everything AFTER the shared generated preamble - * ("## Preamble (run first)" .. end of "## Plan Status Footer"). + * ("## Preamble (run first)" or "## Preamble (after scope gate)" + * .. end of "## Plan Status Footer"). * For tests that exercise the skill's ENTIRE specific flow but never * touch the ~780-line shared preamble. * - extractSkillHead(skillDir, bodyLineCount) @@ -101,7 +102,7 @@ export const CODEX_REVIEW_E2E_SECTIONS = [ /** First/last H2 headings of the shared preamble block that gen-skill-docs * emits into every tier >= 2 skill. extractSkillBody drops this range. */ -const SHARED_PREAMBLE_FIRST = 'Preamble (run first)'; +const SHARED_PREAMBLE_FIRST = ['Preamble (run first)', 'Preamble (after scope gate)']; const SHARED_PREAMBLE_LAST = 'Plan Status Footer'; interface H2Section { @@ -220,15 +221,25 @@ export function extractSkillSections(skillDir: string, sections: string[]): stri } /** - * Frontmatter + intro (everything before "## Preamble (run first)") + the + * Frontmatter + intro/scope gate (everything before either exact preamble heading) + the * full skill-specific body (everything after the "## Plan Status Footer" * section). Use when a test exercises the whole skill flow: this drops the * ~780-line shared generated preamble and nothing else. */ export function extractSkillBody(skillDir: string): string { const { file, frontmatter, bodyLines, sections: all } = loadSkill(skillDir); - const first = findSection(all, SHARED_PREAMBLE_FIRST, file); - const last = findSection(all, SHARED_PREAMBLE_LAST, file); + const boundary = (names: string[]): H2Section => { + const matches = all.filter(section => names.includes(section.heading)); + const label = names.map(name => `"## ${name}"`).join(' or '); + if (!matches.length) throw new Error(`skill-fixture: section ${label} not found in ${file}.`); + if (matches.length !== 1) throw new Error(`skill-fixture: ambiguous section ${label} in ${file}.`); + return matches[0]; + }; + const first = boundary(SHARED_PREAMBLE_FIRST); + const last = boundary([SHARED_PREAMBLE_LAST]); + if (first.start >= last.start) { + throw new Error(`skill-fixture: "## ${SHARED_PREAMBLE_LAST}" precedes the preamble in ${file}.`); + } const intro = bodyLines.slice(0, first.start).join('\n').trimEnd(); const tail = bodyLines.slice(last.end).join('\n').trimEnd(); if (!tail) { diff --git a/test/helpers/touchfiles-data.ts b/test/helpers/touchfiles-data.ts index be2077459..0377bfc5a 100644 --- a/test/helpers/touchfiles-data.ts +++ b/test/helpers/touchfiles-data.ts @@ -89,9 +89,15 @@ export const E2E_TOUCHFILES: Record = { 'plan-ceo-review-selective': ['plan-ceo-review/**', 'test/skill-e2e-plan.test.ts'], 'plan-ceo-review-benefits': ['plan-ceo-review/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-plan.test.ts'], 'plan-ceo-review-expansion-energy': ['plan-ceo-review/**', 'scripts/resolvers/preamble.ts', 'test/fixtures/mode-posture/**', 'test/helpers/llm-judge.ts', 'test/skill-e2e-plan.test.ts'], - 'plan-eng-review': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'test/skill-e2e-plan.test.ts'], - 'plan-eng-review-artifact': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'test/skill-e2e-plan.test.ts'], - 'plan-review-report': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-plan.test.ts'], + 'plan-eng-review': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'test/skill-e2e-plan.test.ts', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], + 'plan-eng-review-artifact': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'test/skill-e2e-plan.test.ts', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], + 'plan-review-report': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-plan.test.ts', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], // Plan-mode smoke tests — gate-tier safety regression tests. Each test file // contains TWO test cases as of v1.21: the baseline plan-mode case and the @@ -115,6 +121,8 @@ export const E2E_TOUCHFILES: Record = { 'test/fixtures/design-scope-declaration-ak.json', "test/eng-option-b-scope-al.test.ts", "test/fixtures/eng-option-b-scope-al.json", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], 'plan-design-review-plan-mode': [ "test/fixtures/design-scope-checkpoint-at.json",'test/pty-screen-unicode-ap.test.ts', @@ -125,6 +133,8 @@ export const E2E_TOUCHFILES: Record = { "test/eng-option-b-scope-al.test.ts", "test/fixtures/eng-option-b-scope-al.json", "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], 'plan-devex-review-plan-mode': ['test/pty-screen-unicode-ap.test.ts', 'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-plan-devex-plan-mode.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/plan-scope-selection.test.ts', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', @@ -149,6 +159,8 @@ export const E2E_TOUCHFILES: Record = { "test/eng-option-b-scope-al.test.ts", "test/fixtures/eng-option-b-scope-al.json", "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], // v1.21+ AskUserQuestion-blocked regression tests — Conductor launches @@ -177,7 +189,9 @@ export const E2E_TOUCHFILES: Record = { // the Conductor rule, the preamble signal, the hook, and the detection helper. 'conductor-prose': ['test/pty-screen-unicode-ap.test.ts', 'test/eng-scope-entry-ap.test.ts', 'test/conductor-prose-observation-ao.test.ts', 'test/fixtures/conductor-prose-ao.json', - 'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'bin/gstack-session-kind', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble.ts', 'plan-eng-review/**', 'hosts/claude/hooks/question-preference-hook.ts', 'hosts/claude/hooks/spawned-directive.ts', 'lib/is-conductor.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-conductor-prose.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**'], + 'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'bin/gstack-session-kind', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble.ts', 'plan-eng-review/**', 'hosts/claude/hooks/question-preference-hook.ts', 'hosts/claude/hooks/spawned-directive.ts', 'lib/is-conductor.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-conductor-prose.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], // Real-PTY E2E batch (#6 new tests on the harness). // Each one tests behavior the SDK harness can't observe (rendered TTY, @@ -190,6 +204,8 @@ export const E2E_TOUCHFILES: Record = { 'plan-design-with-ui-scope': [ "test/fixtures/design-scope-checkpoint-at.json",'plan-design-review/**', 'test/fixtures/plans/ui-heavy-feature.md', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-plan-design-with-ui.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'ship-idempotency-pty': ['ship/**', 'bin/gstack-next-version', 'bin/gstack-version-bump', 'scripts/resolvers/sections.ts', 'lib/worktree.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-ship-idempotency.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt'], 'tpa-present': ['scripts/resolvers/third-party-actions.ts', 'ship/SKILL.md.tmpl', 'ship/sections/apple-release.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-third-party-actions.test.ts', 'test/helpers/third-party-actions.ts'], @@ -208,6 +224,8 @@ export const E2E_TOUCHFILES: Record = { 'carve-section-loading': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', 'scripts/resolvers/composition.ts', 'test/autoplan-review-discovery.test.ts', 'test/autoplan-phase-order.test.ts', 'design-html/**', 'design-shotgun/**', 'qa/**', 'browse/**', 'retro/**', 'autoplan/**', 'spec/**', 'setup-gbrain/**', 'review/**', 'codex/**', 'land-and-deploy/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'office-hours/**', 'document-release/**', 'design-consultation/**', 'cso/**', 'test/helpers/carve-guards.ts', 'scripts/resolvers/sections.ts', 'scripts/resolvers/redact-doc.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/carve-section-loading.test.ts', 'bin/gstack-autoplan-snapshot.ts', 'test/autoplan-snapshot.test.ts', 'test/autoplan-init.test.ts', 'test/autoplan-obligations.test.ts', 'test/fixtures/autoplan/t-ceo-omitted-obligations.json', 'test/fixtures/autoplan/u-ceo-original-loss.json', 'test/fixtures/autoplan/v-ceo-dangling-references.json', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'autoplan-chain-pty': [ "test/fixtures/design-scope-checkpoint-at.json", @@ -226,6 +244,8 @@ export const E2E_TOUCHFILES: Record = { "test/autoplan-clipped-suffix-aq.test.ts", "test/fixtures/autoplan-clipped-suffix-aq.json", "test/design-scope-entry-aq.test.ts", "test/helpers/autoplan-preconfigured-fixture.ts", "test/autoplan-preconfigured-onboarding-ar.test.ts", "test/autoplan-artifact-stall-as.test.ts", "test/fixtures/autoplan-artifact-stall-as.json", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", "test/autoplan-with-result-au.test.ts", "test/fixtures/autoplan-with-result-au.json", "test/autoplan-command-prefix-au.test.ts", "test/fixtures/autoplan-command-prefix-au.json", ], // Per-finding AskUserQuestion count + review-report-at-bottom assertion. @@ -279,6 +299,8 @@ export const E2E_TOUCHFILES: Record = { "test/eng-injected-export-aq.test.ts", "test/fixtures/eng-injected-export-aq.json", "test/eng-staged-regression-aq.test.ts", "test/fixtures/eng-staged-regression-aq.md", "test/eng-library-hooks-aq.test.ts", "test/fixtures/eng-library-hooks-aq.json", "test/eng-before-rewrite-ar.test.ts", "test/fixtures/eng-before-rewrite-ar.md", "test/eng-declarative-as.test.ts", "test/fixtures/eng-declarative-as.json", "test/eng-mandatory-baseline-as.test.ts", "test/fixtures/eng-mandatory-baseline-as.md", "test/helpers/eng-cache-writer-decision.ts", "test/eng-cache-writes-as.test.ts", "test/fixtures/eng-cache-writes-as.json", "test/eng-retry-coverage-as.test.ts", "test/fixtures/eng-retry-coverage-as.json", "test/fixtures/eng-retry-baseline-as.md", + + "test/eng-required-parity-au.test.ts", "test/fixtures/eng-required-parity-au.md", "test/helpers/eng-retained-corpus.ts", "test/eng-retained-corpus-au.test.ts", "test/fixtures/eng-retained-corpus-au.md", "test/eng-annotated-cache-au.test.ts", "test/fixtures/eng-annotated-cache-au.json", "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], 'plan-design-finding-count': [ "test/fixtures/design-scope-checkpoint-at.json",'test/pty-screen-unicode-ap.test.ts', 'test/design-crop-gutter-ap.test.ts', 'test/fixtures/design-crop-gutter-ap.json', 'test/helpers/dx-selected-navigation.ts', 'test/dx-selected-navigation-ap.test.ts', 'test/fixtures/dx-selected-navigation-ap.json', 'test/dx-manual-handoff-ao.test.ts', 'test/fixtures/dx-manual-handoff-ao.json', 'bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-design-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/design-count-review.ts', 'test/design-count-review.test.ts', 'test/fixtures/design-review-j-calls.json', 'test/helpers/design-count-outside.ts', 'test/design-count-outside.test.ts', 'test/skill-e2e-plan-design-finding-count.test.ts', 'test/fixtures/design-review-l-calls.json', 'test/design-completion-handoff.test.ts', 'test/fixtures/design-handoff-l-calls.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/design-review-n-calls.json', 'test/design-completion-handoff-scored.test.ts', 'test/fixtures/design-handoff-n-calls.json', 'test/fixtures/design-handoff-q-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/design-completion-handoff-u.test.ts', 'test/fixtures/design-handoff-u-calls.json', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/helpers/design-artifact-question.ts', 'test/design-artifact-question.test.ts', 'test/fixtures/design-artifacts-w-calls.json', 'test/fixtures/design-outside-y-calls.json', 'test/fixtures/design-boundaries-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/design-gap-z-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/design-count-ad-v2.test.ts', 'test/fixtures/design-count-ad-v2.json', 'test/design-first-decision-af.test.ts', 'test/fixtures/design-first-decision-af.json', 'test/fixtures/design-first-decision-af-retry.json', 'test/fixtures/plan-count-permission-ah.json', 'test/design-first-issue-ai.test.ts', 'test/fixtures/design-first-issue-ai.json', 'test/design-primary-action-aj.test.ts', 'test/fixtures/design-primary-action-aj.json', 'test/fixtures/design-future-todo-aj.json', @@ -301,6 +323,8 @@ export const E2E_TOUCHFILES: Record = { "test/fixtures/design-primary-assignment-ao.json", "test/design-primary-header-aq.test.ts", "test/fixtures/design-primary-header-aq.json", "test/design-scope-entry-aq.test.ts", "test/design-primary-group-as.test.ts", "test/fixtures/design-primary-group-as-calls.json", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], 'plan-devex-finding-count': [ "test/dx-journey-field-at.test.ts", "test/fixtures/dx-journey-field-at.json",'test/pty-screen-unicode-ap.test.ts', 'test/design-crop-gutter-ap.test.ts', 'test/fixtures/design-crop-gutter-ap.json', 'test/helpers/dx-selected-navigation.ts', 'test/dx-selected-navigation-ap.test.ts', 'test/fixtures/dx-selected-navigation-ap.json', 'test/dx-manual-handoff-ao.test.ts', 'test/fixtures/dx-manual-handoff-ao.json', 'bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/devex-count-fixture.ts', 'test/devex-count-fixture.test.ts', 'test/skill-e2e-plan-devex-finding-count.test.ts', 'test/fixtures/dx-prerequisite-r-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/devex-review-n-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/devex-output-o.test.ts', 'test/fixtures/devex-review-o-calls.json', 'test/fixtures/devex-output-o-retry-call.json', 'test/devex-setup-remedy-o.test.ts', 'test/fixtures/devex-review-o-retry-calls.json', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/eng-devex-s-count.test.ts', 'test/fixtures/eng-devex-s-first-calls.json', 'test/fixtures/eng-devex-s-retry-calls.json', 'test/fixtures/devex-review-t-calls.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/fixtures/devex-count-u-calls.json', 'test/fixtures/devex-count-u-retry-calls.json', 'test/fixtures/devex-empathy-v-calls.json', 'test/fixtures/devex-handoff-v-call.json', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/fixtures/devex-count-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/devex-count-z-calls.json', 'test/fixtures/devex-handoff-z-call.json', 'test/review-handoffs-aa.test.ts', 'test/fixtures/review-handoff-aa-ceo.json', 'test/fixtures/review-handoff-aa-dx.json', 'test/devex-empathy-ab.test.ts', 'test/fixtures/devex-empathy-ab-calls.json', 'test/devex-ac-accounting.test.ts', 'test/fixtures/devex-ac-first-attempt-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/devex-reconfirmation-ad-v2.test.ts', 'test/fixtures/devex-reconfirmation-ad-v2.json', 'test/plan-count-history.test.ts', 'test/helpers/devex-seed-coverage.ts', 'test/devex-seed-coverage.test.ts', 'test/fixtures/devex-seed-coverage-ad-v3.json', 'test/fixtures/plan-count-permission-ah.json', @@ -322,6 +346,8 @@ export const E2E_TOUCHFILES: Record = { // numbered-option render. ~1-3 min typical wall time per test, ~$2-6 total. 'plan-eng-finding-floor': ['test/eng-scope-entry-ap.test.ts', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-eng-finding-floor.test.ts', 'test/eng-first-review-t.test.ts', 'test/fixtures/eng-batching-t-calls.json', 'test/eng-scope-y.test.ts', 'test/fixtures/eng-scope-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/eng-binding-z.test.ts', 'test/fixtures/eng-binding-z-calls.json', 'test/eng-binding-retry-z.test.ts', 'test/fixtures/eng-binding-retry-z-calls.json', 'test/helpers/plan-floor-target.ts', 'test/plan-floor-target.test.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', "test/eng-injected-export-aq.test.ts", "test/fixtures/eng-injected-export-aq.json", "test/eng-library-hooks-aq.test.ts", "test/fixtures/eng-library-hooks-aq.json", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], 'plan-ceo-finding-floor': ['bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-finding-floor.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-floor-target.ts', 'test/plan-floor-target.test.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', "test/ceo-section-ordering-aq.test.ts", "test/fixtures/ceo-section-ordering-aq.json", @@ -330,6 +356,8 @@ export const E2E_TOUCHFILES: Record = { 'plan-design-finding-floor': [ "test/fixtures/design-scope-checkpoint-at.json",'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-design-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-design-finding-floor.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-floor-target.ts', 'test/plan-floor-target.test.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], 'plan-devex-finding-floor': ['bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-devex-finding-floor.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-floor-target.ts', 'test/plan-floor-target.test.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts'], @@ -347,6 +375,8 @@ export const E2E_TOUCHFILES: Record = { 'test/fixtures/plan-count-quoted-frame-ak.json', "test/eng-injected-export-aq.test.ts", "test/fixtures/eng-injected-export-aq.json", "test/eng-library-hooks-aq.test.ts", "test/fixtures/eng-library-hooks-aq.json", "test/eng-declarative-as.test.ts", "test/fixtures/eng-declarative-as.json", "test/helpers/eng-cache-writer-decision.ts", "test/eng-cache-writes-as.test.ts", "test/fixtures/eng-cache-writes-as.json", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", ], 'plan-ceo-split-overflow': ['test/pty-screen-unicode-ap.test.ts', 'test/design-crop-gutter-ap.test.ts', 'test/fixtures/design-crop-gutter-ap.json', 'bin/gstack-config', 'plan-ceo-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'bin/gstack-question-preference', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-split-overflow.test.ts', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/fixtures/plan-count-permission-ah.json', 'test/plan-count-crop-ak.test.ts', @@ -372,8 +402,12 @@ export const E2E_TOUCHFILES: Record = { // Fires when either template OR the two preamble resolvers change. 'plan-ceo-review-format-mode': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/helpers/llm-judge.ts', 'test/skill-e2e-plan-format.test.ts'], 'plan-ceo-review-format-approach': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/helpers/llm-judge.ts', 'test/skill-e2e-plan-format.test.ts'], - 'plan-eng-review-format-coverage': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/helpers/llm-judge.ts', 'test/skill-e2e-plan-format.test.ts'], - 'plan-eng-review-format-kind': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/helpers/llm-judge.ts', 'test/skill-e2e-plan-format.test.ts'], + 'plan-eng-review-format-coverage': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/helpers/llm-judge.ts', 'test/skill-e2e-plan-format.test.ts', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], + 'plan-eng-review-format-kind': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/helpers/llm-judge.ts', 'test/skill-e2e-plan-format.test.ts', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], // v1.7.0.0 Pros/Cons format cadence + format + negative-escape evals. // Dependencies: same as format-mode + the 4 plan-review templates + overlay. @@ -381,10 +415,14 @@ export const E2E_TOUCHFILES: Record = { 'plan-ceo-review-prosons-cadence': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'plan-review-prosons-format': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'plan-review-prosons-hardstop-neg': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts'], 'plan-review-prosons-neutral-neg': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'model-overlays/opus-4-7.md', 'test/skill-e2e-plan-prosons.test.ts'], @@ -407,8 +445,12 @@ export const E2E_TOUCHFILES: Record = { 'codex-offered-design-review': [ "test/fixtures/design-scope-checkpoint-at.json",'plan-design-review/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-plan.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], + 'codex-offered-eng-review': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-plan.test.ts', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], - 'codex-offered-eng-review': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-plan.test.ts'], // Ship 'ship-base-branch': ['ship/**', 'bin/gstack-repo-mode', 'test/skill-e2e-review-attribution.test.ts'], @@ -486,6 +528,8 @@ export const E2E_TOUCHFILES: Record = { 'test/helpers/disabled-plan-review-fixture.ts', 'test/disabled-plan-review-evidence.test.ts', 'test/skill-e2e-outside-plan-disabled.test.ts', 'test/fixtures/disabled-plan-attribution-ad-v2.json', "test/fixtures/disabled-historical-line-aq.json", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], // GPT-5.6 Sol scope-termination E2E (Codex CLI, full generated investigate skill) @@ -511,6 +555,8 @@ export const E2E_TOUCHFILES: Record = { "test/coverage-audit-shell-legend-at.test.ts", "test/fixtures/coverage-audit-shell-legend-at.json",'test/eng-scope-entry-ap.test.ts', 'plan-eng-review/**', 'test/fixtures/coverage-audit-fixture.ts', 'test/skill-e2e-coverage-audit.test.ts', 'test/helpers/coverage-audit-evidence.ts', 'test/coverage-audit-evidence.test.ts', 'test/fixtures/coverage-audit-ae.json', 'test/coverage-audit-af.test.ts', 'test/fixtures/coverage-audit-af.json', "test/coverage-shell-display-aq.test.ts", "test/fixtures/coverage-shell-display-aq.json", "test/coverage-diagram-legend-as.test.ts", "test/fixtures/coverage-diagram-legend-as.json", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'ship-triage': ['ship/**', 'bin/gstack-repo-mode', 'test/skill-e2e-triage.test.ts'], 'ship-docsync': ['ship/**', 'document-release/**', 'scripts/gen-skill-docs.ts', 'scripts/resolvers/sections.ts', 'test/skill-e2e-ship-docsync.test.ts'], @@ -538,6 +584,8 @@ export const E2E_TOUCHFILES: Record = { 'plan-design-review-no-ui-scope': [ "test/fixtures/design-scope-checkpoint-at.json",'plan-design-review/**', 'lib/design-catalog.ts', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'design-review-fix': ['design-review/**', 'scripts/resolvers/aside.ts', 'scripts/resolvers/design.ts', 'lib/design-catalog.ts', 'browse/src/**', 'scripts/gen-skill-docs.ts', 'test/skill-e2e-design.test.ts'], // Design detector (user-installed impeccable engine) through the fake engine shim: source mode on a diff and DOM mode on a served page. @@ -604,42 +652,62 @@ export const E2E_TOUCHFILES: Record = { 'journey-ideation': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-plan-eng': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-debug': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-qa': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-code-review': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-ship': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-docs': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-retro': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-design-system': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], 'journey-visual-qa': [ "test/fixtures/design-scope-checkpoint-at.json",'test/eng-scope-entry-ap.test.ts', '*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/skill-routing-e2e.test.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], // Opus 4.7 behavior evals — keys match testName: values in the test file. @@ -1106,12 +1174,16 @@ export const LLM_JUDGE_TOUCHFILES: Record = { // Plan Reviews 'plan-ceo-review/SKILL.md modes': ['plan-ceo-review/SKILL.md', 'plan-ceo-review/SKILL.md.tmpl', 'test/skill-llm-eval.test.ts', 'test/helpers/workflow-judge-input.ts', 'test/workflow-judge-input.test.ts', 'test/helpers/workflow-excerpt.ts'], - 'plan-eng-review/SKILL.md sections': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/SKILL.md', 'plan-eng-review/SKILL.md.tmpl', 'test/skill-llm-eval.test.ts', 'test/helpers/workflow-judge-input.ts', 'test/workflow-judge-input.test.ts', 'test/helpers/workflow-excerpt.ts'], + 'plan-eng-review/SKILL.md sections': ['test/eng-scope-entry-ap.test.ts', 'plan-eng-review/SKILL.md', 'plan-eng-review/SKILL.md.tmpl', 'test/skill-llm-eval.test.ts', 'test/helpers/workflow-judge-input.ts', 'test/workflow-judge-input.test.ts', 'test/helpers/workflow-excerpt.ts', + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", + ], // /spec authored-spec quality (paid LLM-judge — periodic-tier). 'plan-design-review/SKILL.md passes': [ "test/fixtures/design-scope-checkpoint-at.json",'plan-design-review/SKILL.md', 'plan-design-review/SKILL.md.tmpl', 'test/skill-llm-eval.test.ts', 'test/helpers/workflow-judge-input.ts', 'test/workflow-judge-input.test.ts', 'test/helpers/workflow-excerpt.ts', "test/design-scope-entry-aq.test.ts", + + "test/review-entry-and-design-clarity-au.test.ts", "scripts/resolvers/preamble/generate-preamble-bash.ts", "scripts/resolvers/preamble/generate-completion-status.ts", ], // Design skills diff --git a/test/review-entry-and-design-clarity-au.test.ts b/test/review-entry-and-design-clarity-au.test.ts new file mode 100644 index 000000000..1093f345b --- /dev/null +++ b/test/review-entry-and-design-clarity-au.test.ts @@ -0,0 +1,114 @@ +import {expect, test} from 'bun:test'; +import fs from 'node:fs'; +import path from 'node:path'; +import {ALL_HOST_CONFIGS} from '../hosts'; +import {HOST_PATHS, type TemplateContext} from '../scripts/resolvers/types'; +import {generatePreamble} from '../scripts/resolvers/preamble'; +import {generatePreambleBash} from '../scripts/resolvers/preamble/generate-preamble-bash'; +import {generatePlanModeInfo} from '../scripts/resolvers/preamble/generate-completion-status'; +import {generateDesignHardRules} from '../scripts/resolvers/design'; +import {E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES, selectTests} from './helpers/touchfiles'; + +const root = path.resolve(import.meta.dir, '..'); +const announcement = 'Scope gate: plan mode — auto-selected B (reviewing ).'; +const section = fs.readFileSync(path.join(root, 'plan-design-review/sections/review-sections.md.tmpl'), 'utf8'); +function context(skillName: string, host: typeof ALL_HOST_CONFIGS[number]): TemplateContext { + return {skillName, tmplPath: `${skillName}/SKILL.md.tmpl`, host: host.name, + paths: HOST_PATHS[host.name]!, preambleTier: 3, interactive: true}; +} +function assertScopedEntry(preamble: string) { + const command = preamble.indexOf('```bash'); + expect(command).toBeGreaterThan(0); + const entry = preamble.slice(0, command); + expect(entry).toContain('## Preamble (after scope gate)'); + expect(entry).toContain(announcement); + expect(entry).toContain('actual selected plan title or path'); + expect(entry).toContain('after this skill loads'); + expect(entry).toContain('a pre-invocation introduction does not count'); + expect(entry).toContain('If the gate asks a question, wait for its answer'); + expect(preamble).not.toContain('## Preamble (run first)'); + expect(preamble).toContain('starting from the Scope gate'); + expect(preamble).not.toContain('starting from Step 0;'); +} + +test('Design and Eng resolve the existing gate at the first executable preamble in every host', () => { + for (const host of ALL_HOST_CONFIGS) for (const skill of ['plan-design-review', 'plan-eng-review']) { + const ctx = context(skill, host); + assertScopedEntry(generatePreamble(ctx)); + const tmpl = fs.readFileSync(path.join(root, `${skill}/SKILL.md.tmpl`), 'utf8'); + expect(tmpl.indexOf(announcement)).toBeLessThan(tmpl.indexOf('{{PREAMBLE}}')); + expect(tmpl).toContain('If the user explicitly named a DIFFERENT target'); + expect(tmpl).toContain('If plan mode is indicated but no plan exists yet, ask as normal'); + } +}); + +test('missing local checkpoint and competing Step 0 entry instructions fail the generation contract', () => { + const text = generatePreamble(context('plan-design-review', ALL_HOST_CONFIGS[0]!)); + expect(() => assertScopedEntry(text.replace(announcement, ''))).toThrow(); + expect(() => assertScopedEntry(text.replace('## Preamble (after scope gate)', '## Preamble (run first)'))).toThrow(); + expect(() => assertScopedEntry(text.replace('starting from the Scope gate', 'starting from Step 0; then the Scope gate'))).toThrow(); +}); + +test('other skills keep their existing preamble entry and plan-mode starting point', () => { + for (const host of ALL_HOST_CONFIGS) for (const skill of ['plan-ceo-review', 'plan-devex-review', 'ship', 'review']) { + const ctx = context(skill, host); + expect(generatePreambleBash(ctx).startsWith('## Preamble (run first)\n\n```bash')).toBe(true); + expect(generatePlanModeInfo(ctx)).toContain('starting from Step 0;'); + expect(generatePreambleBash(ctx)).not.toContain('Before the command below'); + } +}); + +test('hard rules remain inside the scored fourth pass without changing other skills heading levels', () => { + const host = ALL_HOST_CONFIGS[0]!; + const rules = generateDesignHardRules(context('plan-design-review', host)); + const rendered = section.replace('{{DESIGN_HARD_RULES}}', rules); + const start = rendered.indexOf('### Pass 4:'); + const end = rendered.indexOf('### Pass 5:'); + const pass = rendered.slice(start, end); + expect(pass.indexOf('**Pass 4 evaluation:**')).toBeLessThan(pass.indexOf('#### Design Hard Rules')); + expect(pass.match(/^### /gm)).toHaveLength(1); + expect(pass).toContain('An unresolved hard rejection caps this pass below 8'); + expect(pass).toContain('Litmus answers support findings, not a separate numeric score.'); + for (const skill of ['design-review', 'design-consultation', 'design-html']) { + expect(generateDesignHardRules(context(skill, host)).startsWith('### Design Hard Rules\n')).toBe(true); + } +}); + +test('the decision table preserves first approval, inherited approvals, artifact and navigation boundaries', () => { + const cases = section.slice(section.indexOf('| Situation |'), section.indexOf('{{LEARNINGS_SEARCH}}')); + expect(cases).toContain('exact fix already has an individual user decision or a preamble-authorized per-issue auto-decision'); + expect(cases).toContain('do not ask again'); + expect(cases).toContain('copied unchanged into a required artifact'); + expect(cases).toContain('it approves no new remedy'); + expect(cases).toContain('no individual decision has approved its fix'); + expect(cases).toContain('even if the input names the gap or DESIGN.md prescribes the exact token'); + expect(cases).toContain('Keep the proposed remedy pending'); + expect(cases).toContain('new tradeoff'); + expect(section).toContain('Scope, focus, setup, and next-step choices approve no remedies.'); + expect(section).toContain('Never edit first and ask afterward.'); + expect(section).toContain('An unapproved violation still needs its first individual decision.'); +}); + +test('scoring and section dependencies are explicit while existing clean and pass thresholds remain', () => { + expect(section).toContain('use the lowest of the six rated pass scores (1-6)'); + expect(section).toContain('separately before and after approved fixes'); + expect(section).toContain('Pass 7 is unscored'); + expect(section).toContain('If all passes 8+'); + expect(section).toContain('"clean" if overall score 8+ AND 0 unresolved'); + expect(section).toContain('If DESIGN.md is absent, rate the plan\'s explicit token and component specifications'); + expect(section).toContain('do not skip the score or assume alignment'); + expect(section).toContain('Read `~/.claude/skills/gstack/plan-design-review/SKILL.md`'); + expect(section).toContain('Use their existing results; do not restart the review.'); + expect(section).toContain('After Pass 7: offer the mockup update below when applicable, resolve deferred TODO proposals, reconcile approvals, then synthesize tasks and the Completion Summary.'); +}); + +test('new regression and changed preamble sources select all Design and Eng owners', () => { + for (const map of [E2E_TOUCHFILES, LLM_JUDGE_TOUCHFILES]) { + const affected = selectTests(['plan-design-review/SKILL.md.tmpl', 'plan-eng-review/SKILL.md.tmpl'], map, []).selected; + expect(selectTests(['test/review-entry-and-design-clarity-au.test.ts'], map, []).selected).toEqual(affected); + for (const source of ['scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble/generate-completion-status.ts']) { + const selected = selectTests([source], map, []).selected; + for (const owner of affected) expect(selected).toContain(owner); + } + } +}); diff --git a/test/skill-fixture.test.ts b/test/skill-fixture.test.ts index 006845394..6c23630bf 100644 --- a/test/skill-fixture.test.ts +++ b/test/skill-fixture.test.ts @@ -27,6 +27,7 @@ import { RETRO_E2E_SECTIONS, CODEX_REVIEW_E2E_SECTIONS, } from './helpers/skill-fixture'; +import { E2E_TOUCHFILES, GLOBAL_TOUCHFILES, selectTests } from './helpers/touchfiles'; const ROOT = path.resolve(import.meta.dir, '..'); @@ -126,6 +127,64 @@ describe('extractSkillBody (synthetic)', () => { fs.writeFileSync(path.join(bare, 'SKILL.md'), '---\nname: bare\n---\n## Only Section\nbody\n'); expect(() => extractSkillBody(bare)).toThrow(/Preamble \(run first\)/); }); + + test('both exact preamble headings preserve the complete intro/scope gate and tail', () => { + for (const heading of ['Preamble (run first)', 'Preamble (after scope gate)']) { + const file = path.join(tmpDir, 'scoped-body.md'); + const scope = '## Scope gate\nAnnounce the actual selected plan before any tools.\n\n'; + const input = SYNTHETIC_SKILL.replace('## Preamble (run first)', scope + `## ${heading}`); + fs.writeFileSync(file, input); + const out = extractSkillBody(file); + const expected = extractSkillBody(skillDir).replace('Invoke text.\n\n', 'Invoke text.\n\n' + scope); + expect(out).toBe(expected); + expect(out).toContain(scope.trimEnd()); + expect(out).not.toContain(`## ${heading}`); + } + }); + + test('unknown, missing, duplicated and reversed boundary sections fail loudly', () => { + const cases: Array<[string, string, RegExp]> = [ + ['unknown start', SYNTHETIC_SKILL.replace('Preamble (run first)', 'Preamble (after setup)'), /Preamble/], + ['suffixed scoped start', SYNTHETIC_SKILL.replace('Preamble (run first)', 'Preamble (after scope gate) extra'), /Preamble/], + ['suffixed old start', SYNTHETIC_SKILL.replace('Preamble (run first)', 'Preamble (run first) extra'), /Preamble/], + ['missing footer', SYNTHETIC_SKILL.replace('## Plan Status Footer', '## Renamed Footer'), /Plan Status Footer/], + ['suffixed footer', SYNTHETIC_SKILL.replace('## Plan Status Footer', '## Plan Status Footer extra'), /Plan Status Footer/], + ['both starts', SYNTHETIC_SKILL.replace('## AskUserQuestion Format', '## Preamble (after scope gate)'), /ambiguous/], + ['duplicate start', SYNTHETIC_SKILL.replace('## AskUserQuestion Format', '## Preamble (run first)'), /ambiguous/], + ['duplicate footer', SYNTHETIC_SKILL.replace('## Step 2 — Other', '## Plan Status Footer'), /ambiguous/], + ['footer before start', SYNTHETIC_SKILL.replace('## Preamble (run first)', '## Plan Status Footer').replace('## Plan Status Footer\nfooter junk', '## Preamble (after scope gate)\nfooter junk'), /precedes/], + ['missing tail', SYNTHETIC_SKILL.slice(0, SYNTHETIC_SKILL.indexOf('## Step 1 — Do the thing')), /no content after/], + ]; + for (const [name, input, error] of cases) { + const file = path.join(tmpDir, 'invalid-boundaries.md'); fs.writeFileSync(file, input); + expect(() => extractSkillBody(file), name).toThrow(error); + expect(() => extractSkillBody(file), name).toThrow(/invalid-boundaries\.md/); + } + }); + + test('quoted and fenced boundary examples do not become live section markers', () => { + for (const [open, close] of [['```markdown\n', '```\n'], ['~~~markdown\n', '~~~\n']]) { + const file = path.join(tmpDir, 'fenced-boundaries.md'); + const examples = `${open}## Preamble (after scope gate)\n## Plan Status Footer\n${close}> ## Preamble (after scope gate)\n\n`; + fs.writeFileSync(file, SYNTHETIC_SKILL.replace('Intro line before any section.', examples + 'Intro line before any section.')); + expect(extractSkillBody(file)).toContain(examples.trimEnd()); + fs.writeFileSync(file, SYNTHETIC_SKILL.replace('## Preamble (run first)\npreamble junk', `${open}## Preamble (after scope gate)\n${close}preamble junk`)); + expect(() => extractSkillBody(file)).toThrow(/Preamble/); + } + }); + + test('other section extraction keeps its existing prefix matching', () => { + const file = path.join(tmpDir, 'section-prefix.md'); + fs.writeFileSync(file, SYNTHETIC_SKILL.replace('Preamble (run first)', 'Preamble (after scope gate)')); + expect(extractSkillSections(file, ['Preamble (after'])).toContain('preamble junk'); + expect(extractSkillSections(file, ['Step 1'])).toContain('step one continues after the fence'); + }); + + test('the existing global source dependency selects every affected workflow fixture', () => { + expect(GLOBAL_TOUCHFILES).toContain('test/helpers/skill-fixture.ts'); + const selection = selectTests(['test/helpers/skill-fixture.ts'], E2E_TOUCHFILES, GLOBAL_TOUCHFILES); + expect(selection.selected.sort()).toEqual(Object.keys(E2E_TOUCHFILES).sort()); + }); }); describe('extractSkillHead (synthetic)', () => { @@ -236,6 +295,7 @@ describe('real-skill pins: body/head extraction used by E2E fixtures', () => { test(`extractSkillBody(${skill}) drops the shared preamble, keeps the flow`, () => { const out = extractSkillBody(path.join(ROOT, skill)); expect(out).not.toContain('## Preamble (run first)'); + expect(out).not.toContain('## Preamble (after scope gate)'); expect(out).not.toContain('## Telemetry (run last)'); const full = fs.readFileSync(path.join(ROOT, skill, 'SKILL.md'), 'utf-8'); expect(out.length).toBeLessThan(full.length * 0.75); @@ -250,6 +310,15 @@ describe('real-skill pins: body/head extraction used by E2E fixtures', () => { expect(extractSkillBody(path.join(ROOT, 'context-restore'))).toContain('## If no saved contexts exist'); }); + test('the scoped Eng render retains its original scope gate before the workflow', () => { + const file = path.join(ROOT, 'plan-eng-review', 'SKILL.md'); + const full = fs.readFileSync(file, 'utf-8'); + const scopeStart = full.indexOf('## Scope gate'); + const preamble = full.indexOf('## Preamble (after scope gate)'); + expect(scopeStart).toBeGreaterThan(0); expect(preamble).toBeGreaterThan(scopeStart); + expect(extractSkillBody(file)).toContain(full.slice(scopeStart, preamble).trimEnd()); + }); + // The union of skills installed by the routing + opus-47 discovery fixtures. const HEAD_EXTRACTED_SKILLS = [ '', 'qa', 'qa-only', 'ship', 'review', 'plan-ceo-review', 'plan-eng-review',