From bc819b0bb28955eae5e660bf66ea8e20b5a07b88 Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 8 Jul 2026 21:44:07 +0200 Subject: [PATCH 1/2] feat: slim plan contract --- agents/executor.md | 12 +- agents/planner.md | 36 +- agents/verifier.md | 2 +- bin/lib/phase.mjs | 428 +++++++++++++++++- bin/lib/plan-constants.mjs | 5 - distilled/DESIGN.md | 60 +-- distilled/EVIDENCE-INDEX.md | 6 +- distilled/references/observation-record.md | 7 +- distilled/references/proof-rules.md | 9 + distilled/templates/delegates/plan-checker.md | 27 +- distilled/templates/ui-proof.md | 254 +++-------- distilled/workflows/audit-milestone.md | 2 +- distilled/workflows/execute.md | 11 +- distilled/workflows/plan.md | 92 ++-- distilled/workflows/quick.md | 15 +- distilled/workflows/verify.md | 12 +- docs/USER-GUIDE.md | 2 +- tests/gsdd.cross-runtime.test.cjs | 19 +- tests/gsdd.guards.test.cjs | 176 +++---- tests/gsdd.invariants.test.cjs | 40 +- tests/gsdd.plan.adapters.test.cjs | 9 +- tests/gsdd.scenarios.test.cjs | 27 +- tests/phase.test.cjs | 386 +++++++++++++++- 23 files changed, 1158 insertions(+), 479 deletions(-) diff --git a/agents/executor.md b/agents/executor.md index 45080fcb..a212f57f 100644 --- a/agents/executor.md +++ b/agents/executor.md @@ -44,7 +44,7 @@ The executor is plan-scoped: ## Core Algorithm -1. **Load plan.** Parse frontmatter (`phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `must_haves`), objective, context references, and tasks. Treat any prompt-provided `` block as task_scoped unless it explicitly labels entries as mandatory_now. +1. **Load plan.** Parse frontmatter (`phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `browser_proof_required`, `browser_proof_rationale`, `must_haves`), objective, context references, and tasks. Treat any prompt-provided `` block as task_scoped unless it explicitly labels entries as mandatory_now. 2. **Run lifecycle preflight.** Before mutating lifecycle artifacts, run `node .work/bin/gsdd.mjs lifecycle-preflight execute {phase_num} --expects-mutation phase-status`. If blocked, stop and surface the blocker. 3. **For each task:** a. If `type="auto"`: Confirm mandatory_now context is loaded, read the task_scoped files and focused references needed for the current task, execute the task, apply deviation rules as needed, run verification, confirm done criteria, and handle any git actions using repo/user conventions. @@ -175,7 +175,7 @@ For each task in the plan, follow this loop: ### Frontmatter And Task Semantics The executor consumes the plan schema defined by the planner: -- frontmatter keys: `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `must_haves` +- frontmatter keys: `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `browser_proof_required`, `browser_proof_rationale`, `must_haves` - task types: - `type="auto"` - proceed without pausing - `type="checkpoint:user"` - stop for a required user decision or human-only step @@ -210,8 +210,8 @@ Before reporting a task complete: - if an API change is involved, hit the endpoint or targeted integration path - A task is not complete because code was written. It is complete when the intended verification path actually passes. -### UI Proof Execution -If the plan defines UI proof slots, record observed proof against the exact claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit before claiming task completion. +### Browser Proof Execution +If the plan sets `browser_proof_required: true`, execute the Browser Proof Plan and record observed proof against the exact route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit before claiming task completion. Use `agent-browser` as the default live UI proof path: - open the planned route/state @@ -220,9 +220,9 @@ Use `agent-browser` as the default live UI proof path: - capture screenshots for the planned viewport(s) - record relevant console/network observations -If `agent-browser` is unavailable, record the availability constraint and closest project-native interactive browser fallback in the proof bundle instead of silently treating the fallback as the default path. Existing Playwright/package-script browser tests remain canonical repeatable regression evidence when present; use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled, structured console, or multi-context verification. +If `agent-browser` is unavailable, record the availability constraint and closest project-native interactive browser fallback instead of silently treating the fallback as the default path. Existing Playwright/package-script browser tests remain canonical repeatable regression evidence when present; use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled, structured console, or multi-context verification. -Artifact metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports are local-only/unsafe by default and cannot back public, tracked, delivery, release, or publication proof claims. Use `gsdd ui-proof validate ` or `gsdd health` when a bundle exists. Artifact count, source comments, AST/cAST findings, semantic search, and Semble-like retrieval are not proof. Missing or weakly linked evidence must be recorded as proof debt, waiver, deferment, or reduced claim language rather than satisfied proof. +Raw screenshots, traces, videos, DOM snapshots, and reports are local-only/unsafe by default and cannot back public, tracked, delivery, release, or publication proof claims unless sanitized. Artifact count, source comments, AST/cAST findings, semantic search, and Semble-like retrieval are not proof. Missing or weakly linked evidence must be recorded as proof debt, waiver, deferment, or reduced claim language rather than satisfied proof, using the failure-cause names in `distilled/references/proof-rules.md` when proof fails or is partial. diff --git a/agents/planner.md b/agents/planner.md index c8e41fa1..09110bc3 100644 --- a/agents/planner.md +++ b/agents/planner.md @@ -153,15 +153,13 @@ Any checkpoint must be justified by the task itself, not by planner caution or h Any plan containing `checkpoint:*` must set `autonomous: false`. - -For UI-sensitive work, plan proof slots that can later be matched exactly to claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit. Use only the stable evidence kinds `code`, `test`, `runtime`, `delivery`, and `human`. + +For UI-sensitive work, set `browser_proof_required: true` and include a `## Browser Proof Plan` that can later be matched exactly to route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit. Use only the stable evidence kinds `code`, `test`, `runtime`, `delivery`, and `human`. -Require observed artifacts to carry `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; when a planned slot is meant to support public, publication, tracked, delivery, or release proof, say to validate the observed bundle with `gsdd ui-proof validate --claim <...>`. `gsdd ui-proof validate`/`gsdd health` must catch invalid bundle metadata when present. +The Browser Proof Plan must name route/state, viewport coverage, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit. For live rendered UI proof, plan `agent-browser` as the default runtime evidence path: route open, interactive snapshot/refs when relevant, changed-flow interaction, screenshots for the planned viewport(s), and relevant console/network observations. If `agent-browser` is unavailable in the runtime, require an explicit availability constraint and the closest project-native interactive browser fallback before narrowing the claim. Existing Playwright/package-script browser tests remain the canonical repeatable regression path when present; do not scaffold new browser infrastructure by default. Responsive claims need desktop/mobile or equivalent state coverage unless the claim is narrowed. -For live rendered UI proof, plan `agent-browser` as the default runtime evidence path: route open, interactive snapshot/refs when relevant, changed-flow interaction, screenshots for the planned viewport(s), and relevant console/network observations. If `agent-browser` is unavailable in the runtime, require an explicit availability constraint and the closest project-native interactive browser fallback before narrowing the claim. Existing Playwright/package-script browser tests remain the canonical repeatable regression path when present; do not scaffold new browser infrastructure by default. The planner chooses viewport coverage, but must explain why the viewport set is sufficient for the claim or narrow the claim limit; responsive claims need desktop/mobile or equivalent state coverage. - -Do not let source annotations, AST/cAST findings, semantic search, comments, or Semble-like retrieval satisfy proof slots; they are discovery hints only. Human acceptance can narrow or waive a claim and record proof debt, but it must not turn missing or mismatched non-human evidence into `satisfied` proof. - +Do not let source annotations, AST/cAST findings, semantic search, comments, or Semble-like retrieval satisfy browser proof; they are discovery hints only. Human acceptance can narrow or waive a claim and record proof debt, but it must not turn missing or mismatched non-human evidence into satisfied proof. + Example dependency graph: @@ -194,8 +192,9 @@ Wave rule: Write one or more `PLAN.md` files to the phase directory. Keep the current GSDD schema exactly: -- frontmatter keys: `phase`, `plan`, `type`, `wave`, `runtime`, `assurance`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `known_unknowns`, `ui_proof_slots`, `no_ui_proof_rationale`, `high_leverage_surfaces`, `second_pass_required`, `closure_claim_limit`, `parallelism_budget`, `leverage`, `must_haves` +- frontmatter keys: `phase`, `plan`, `type`, `wave`, `runtime`, `assurance`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `known_unknowns`, `browser_proof_required`, `browser_proof_rationale`, `must_haves` - typed tasks with `files`, `action`, `verify`, and `done` +- if `browser_proof_required: true`, the plan body must include `## Browser Proof Plan` with route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit Typed frontmatter example: @@ -225,18 +224,8 @@ anti_regression_targets: - Existing session middleware behavior remains unchanged for already-supported routes. known_unknowns: - Exact copy wording for auth errors may still need product confirmation. -ui_proof_slots: [] -no_ui_proof_rationale: Not UI-sensitive; scoped work does not claim a visible UI outcome. -high_leverage_surfaces: [] -second_pass_required: false -closure_claim_limit: Do not claim phase completion until verification satisfies the evidence contract for the scoped truths. -parallelism_budget: - max_concurrent_plans: 1 - safe_parallelism: [] -leverage: - lost: Slightly more planning ceremony for this plan. - kept: Existing auth/session architecture and repo conventions. - gained: Explicit anti-drift boundaries and fail-closed escalation. +browser_proof_required: false +browser_proof_rationale: Not UI-sensitive; scoped work does not claim a visible UI outcome. must_haves: truths: - "User can sign in with email and password" @@ -277,12 +266,7 @@ Before returning, self-check against the checker dimensions: 6. must-have quality 7. context compliance 8. goal achievement -9. scope boundaries -10. anti-regression capture -11. escalation integrity -12. closure honesty -13. high-leverage review -14. approach alignment (when APPROACH.md exists) +9. approach alignment (when APPROACH.md exists) Task completeness rules: - every task has files, action, verify, and done diff --git a/agents/verifier.md b/agents/verifier.md index d9b9d6fc..07d4b4fb 100644 --- a/agents/verifier.md +++ b/agents/verifier.md @@ -123,7 +123,7 @@ Do not return a flat symptom list when the same underlying breakage explains mul Visual correctness, live interaction quality, and some external integrations still need explicit human checks. -For UI proof slots, fail closed unless observed proof is matched to the exact claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit. For live UI runtime proof, expect `agent-browser` as the default captured tool unless the observed bundle explains a project-native equivalent or an availability constraint; do not fail solely because another browser tool was used, but downgrade vague proof that lacks exact route/state, viewport coverage or rationale, interactive steps/refs where relevant, screenshot/report artifacts, or relevant console/network observations. Existing Playwright/package-script browser tests count as canonical repeatable regression evidence, not as a replacement for scoped runtime proof when the slot requires `runtime`. Artifact metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; local-only or unsafe artifacts cannot back public, tracked, delivery, release, or publication proof claims, and `gsdd ui-proof validate`/`gsdd health` metadata failures block the stronger proof claim. Screenshots, traces, reports, Gherkin, a11y scans, E2E outputs, manual notes, source annotations, AST/cAST findings, semantic search, comments, and Semble-like retrieval do not satisfy proof by existence alone. Human acceptance records risk, waiver, deferment, proof debt, or a narrowed claim; it does not upgrade missing or mismatched non-human proof to `satisfied`. +For browser proof, fail closed unless observed proof is matched to the exact route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit. For live UI runtime proof, expect `agent-browser` as the default captured tool unless the observation record explains a project-native equivalent or an availability constraint; do not fail solely because another browser tool was used, but downgrade vague proof that lacks exact route/state, viewport coverage or rationale, interactive steps/refs where relevant, screenshot/report artifacts, relevant console/network observations, or a narrowed claim limit. Existing Playwright/package-script browser tests count as canonical repeatable regression evidence, not as a replacement for scoped runtime proof when browser proof requires runtime observation. Local-only or unsafe artifacts cannot back public, tracked, delivery, release, or publication proof claims unless sanitized. Screenshots, traces, reports, Gherkin, a11y scans, E2E outputs, manual notes, source annotations, AST/cAST findings, semantic search, comments, and Semble-like retrieval do not satisfy proof by existence alone. Human acceptance records risk, waiver, deferment, proof debt, or a narrowed claim; it does not upgrade missing or mismatched non-human proof to satisfied proof. Use the failure-cause names in `distilled/references/proof-rules.md` when proof fails or is partial. ## Step 9: Determine overall status diff --git a/bin/lib/phase.mjs b/bin/lib/phase.mjs index b665ee3c..9603901b 100644 --- a/bin/lib/phase.mjs +++ b/bin/lib/phase.mjs @@ -4,7 +4,7 @@ // evaluate once, so CWD must be computed inside function bodies. import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'; -import { basename, dirname, join, relative } from 'path'; +import { basename, dirname, isAbsolute, join, relative } from 'path'; import { output } from './cli-utils.mjs'; import { resolveWorkspaceContext } from './workspace-root.mjs'; @@ -168,6 +168,397 @@ function extractPlanFileArtifacts(planContent, workspaceRoot) { return artifacts; } +function extractFrontmatter(content) { + const match = String(content || '').replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/); + return match ? match[1] : ''; +} + +function readTopLevelScalar(frontmatter, key) { + const escapedKey = String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = String(frontmatter || '').match(new RegExp(`^${escapedKey}:\\s*(.*)$`, 'm')); + return match ? normalizeScalarValue(match[1]) : null; +} + +function hasTopLevelKey(frontmatter, key) { + const escapedKey = String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`^${escapedKey}:\\s*.*$`, 'm').test(String(frontmatter || '')); +} + +function stripInlineComment(value) { + const text = String(value || ''); + let quote = null; + for (let index = 0; index < text.length; index += 1) { + const char = text[index]; + if (quote) { + if (quote === '"' && char === '\\') { + index += 1; + continue; + } + if (char === quote) quote = null; + continue; + } + if (char === '"' || char === "'") { + quote = char; + continue; + } + if (char === '#') return text.slice(0, index); + } + return text; +} + +function normalizeScalarValue(value) { + let normalized = stripInlineComment(value).trim(); + if ( + (normalized.startsWith('"') && normalized.endsWith('"')) + || (normalized.startsWith("'") && normalized.endsWith("'")) + ) { + normalized = normalized.slice(1, -1).trim(); + } + return normalized; +} + +function extractMarkdownSection(content, heading) { + return extractMarkdownSections(content, heading)[0] || ''; +} + +function extractMarkdownSections(content, heading) { + const normalized = String(content || '').replace(/\r\n/g, '\n'); + const escapedHeading = String(heading).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const headingMatcher = new RegExp(`^##\\s+${escapedHeading}\\s*$`, 'gim'); + const matches = Array.from(normalized.matchAll(headingMatcher)); + return matches.map((headingMatch, index) => { + const start = headingMatch.index + headingMatch[0].length; + const nextStart = matches[index + 1]?.index; + const rest = normalized.slice(start, nextStart); + const nextHeadingIndex = rest.search(/^##\s+/m); + return (nextHeadingIndex === -1 ? rest : rest.slice(0, nextHeadingIndex)).trim(); + }).filter(Boolean); +} + +function sectionFieldValue(section, label) { + const text = String(section || ''); + const escapedLabel = String(label).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = new RegExp(`^\\s*(?:[-*]\\s*)?${escapedLabel}:\\s*(.*)$`, 'im').exec(text); + if (!match) return ''; + + const inlineValue = normalizeScalarValue(match[1]); + if (inlineValue) return inlineValue; + + const nestedValues = []; + const followingLines = text.slice(match.index + match[0].length).split(/\r?\n/); + for (const line of followingLines) { + if (!line.trim()) continue; + if (/^\S/.test(line)) break; + const nestedBullet = line.match(/^\s+[-*]\s*(.+)$/); + if (nestedBullet) { + nestedValues.push(nestedBullet[1].trim()); + continue; + } + const nestedText = line.match(/^\s{2,}(.+)$/); + if (nestedText) { + nestedValues.push(nestedText[1].trim()); + continue; + } + break; + } + return normalizeScalarValue(nestedValues.join(' ')); +} + +function isMeaningfulFieldValue(value) { + const normalized = normalizeScalarValue(value); + if (!normalized) return false; + if (/^n\/?a$/i.test(normalized)) return false; + if (/^(none|null|nil|~|tbd|todo|unknown)$/i.test(normalized)) return false; + if (/^\[.*\]$/.test(normalized)) return false; + if (/^<.*>$/.test(normalized)) return false; + return true; +} + +function nonEmptyField(section, label) { + const value = sectionFieldValue(section, label); + return isMeaningfulFieldValue(value); +} + +function browserProofFixHint(code) { + if (code === 'retired_browser_proof_contract') { + return 'Replace retired ui_proof_slots/no_ui_proof_rationale fields with browser_proof_required and browser_proof_rationale.'; + } + if (code === 'missing_browser_proof_declaration') { + return 'Add browser_proof_required: true|false and browser_proof_rationale to PLAN.md frontmatter.'; + } + if (code === 'invalid_browser_proof_required') { + return 'Set browser_proof_required to true or false in PLAN.md frontmatter.'; + } + if (code === 'missing_browser_proof_rationale') { + return 'Add a nonblank browser_proof_rationale explaining why browser proof is or is not required.'; + } + if (code === 'missing_browser_proof_plan') { + return 'Add a ## Browser Proof Plan section or set browser_proof_required: false with a rationale if the work is not UI-sensitive.'; + } + if (code === 'missing_browser_proof_observation') { + return 'Record a ## Browser Proof Observation in SUMMARY.md or link to an observation record before verifying browser-sensitive work.'; + } + if (code === 'incomplete_browser_proof_observation') { + return 'Complete the Browser Proof Observation fields or narrow the proof claim.'; + } + if (code === 'unmatched_browser_proof_observation') { + return 'When multiple required browser-proof plans exist, add a Plan field to each Browser Proof Observation that names the matching PLAN.md artifact.'; + } + return 'Complete the Browser Proof Plan fields or narrow the proof claim.'; +} + +function evaluateBrowserProofContract(planContent, planPath) { + const frontmatter = extractFrontmatter(planContent); + const requiredRaw = readTopLevelScalar(frontmatter, 'browser_proof_required'); + const rationale = readTopLevelScalar(frontmatter, 'browser_proof_rationale'); + const declarationPresent = requiredRaw !== null || rationale !== null; + const retiredKeys = ['ui_proof_slots', 'no_ui_proof_rationale', 'proof_bundle_version', 'claim_limits'] + .filter((key) => hasTopLevelKey(frontmatter, key)); + const blockers = []; + + if (retiredKeys.length > 0) { + blockers.push({ + code: 'retired_browser_proof_contract', + severity: 'blocker', + path: planPath, + message: `PLAN.md uses retired browser-proof field(s): ${retiredKeys.join(', ')}.`, + fix_hint: browserProofFixHint('retired_browser_proof_contract'), + }); + } + + if (!declarationPresent && retiredKeys.length === 0) { + blockers.push({ + code: 'missing_browser_proof_declaration', + severity: 'blocker', + path: planPath, + message: 'PLAN.md must declare browser_proof_required and browser_proof_rationale before verification.', + fix_hint: browserProofFixHint('missing_browser_proof_declaration'), + }); + return { + path: planPath, + declaration_present: false, + required: null, + rationale: null, + satisfied: false, + blockers, + }; + } + + const normalizedRequired = String(requiredRaw || '').toLowerCase(); + const required = normalizedRequired === 'true' + ? true + : normalizedRequired === 'false' + ? false + : null; + + if (required === null) { + blockers.push({ + code: 'invalid_browser_proof_required', + severity: 'blocker', + path: planPath, + message: 'browser_proof_required must be true or false when the browser-proof contract is declared.', + fix_hint: browserProofFixHint('invalid_browser_proof_required'), + }); + } + + if (!isMeaningfulFieldValue(rationale)) { + blockers.push({ + code: 'missing_browser_proof_rationale', + severity: 'blocker', + path: planPath, + message: 'browser_proof_rationale is required when the browser-proof contract is declared.', + fix_hint: browserProofFixHint('missing_browser_proof_rationale'), + }); + } + + if (required === true) { + const section = extractMarkdownSection(planContent, 'Browser Proof Plan'); + if (!section) { + blockers.push({ + code: 'missing_browser_proof_plan', + severity: 'blocker', + path: planPath, + message: 'browser_proof_required is true but PLAN.md has no ## Browser Proof Plan section.', + fix_hint: browserProofFixHint('missing_browser_proof_plan'), + }); + } else { + const missingFields = ['Routes/states', 'Viewports', 'Runtime path', 'Observations', 'Artifacts', 'Claim limit'] + .filter((field) => !nonEmptyField(section, field)); + const hasEvidenceCommand = nonEmptyField(section, 'Evidence command'); + const hasNoCommandRationale = nonEmptyField(section, 'No-command rationale'); + if (!hasEvidenceCommand && !hasNoCommandRationale) { + missingFields.push('Evidence command or No-command rationale'); + } + if (missingFields.length > 0) { + blockers.push({ + code: 'incomplete_browser_proof_plan', + severity: 'blocker', + path: planPath, + message: `Browser Proof Plan is missing required field(s): ${missingFields.join(', ')}.`, + fix_hint: browserProofFixHint('incomplete_browser_proof_plan'), + }); + } + } + } + + return { + path: planPath, + declaration_present: declarationPresent, + required, + rationale, + satisfied: blockers.length === 0, + blockers, + }; +} + +function sanitizeLinkedObservationPath(value) { + let text = normalizeScalarValue(value); + const markdownLink = text.match(/^\[[^\]]+\]\(([^)]+)\)$/); + if (markdownLink) text = markdownLink[1].trim(); + text = text.replace(/^`|`$/g, '').trim(); + if (/^[a-z]+:\/\//i.test(text)) return ''; + return text; +} + +function readLinkedObservationRecords(summaryContent, summaryFullPath, workspaceRoot) { + const records = []; + const matcher = /^\s*(?:[-*]\s*)?(?:Browser Proof Observation|Browser proof observation|Observation record|Browser proof record):\s*(.+)$/gim; + let match; + while ((match = matcher.exec(String(summaryContent || '')))) { + const linkedPath = sanitizeLinkedObservationPath(match[1]); + if (!linkedPath) continue; + const candidates = isAbsolute(linkedPath) + ? [linkedPath] + : [join(dirname(summaryFullPath), linkedPath), join(workspaceRoot, linkedPath)]; + const existingPath = candidates.find((candidate) => existsSync(candidate)); + if (existingPath) records.push(readFileSync(existingPath, 'utf-8')); + } + return records; +} + +function findBrowserProofObservationSections(summaryContent, summaryFullPath, workspaceRoot) { + return [ + ...extractMarkdownSections(summaryContent, 'Browser Proof Observation'), + ...readLinkedObservationRecords(summaryContent, summaryFullPath, workspaceRoot) + .flatMap((record) => extractMarkdownSections(record, 'Browser Proof Observation')), + ].filter(Boolean); +} + +function isCompleteBrowserProofObservation(section) { + const requiredFieldSets = [ + ['Flow', 'Routes/states'], + ['Viewports'], + ['Runtime path'], + ['Evidence command', 'No-command rationale'], + ['Observed', 'Observations'], + ['Artifacts'], + ['Result'], + ['Claim limit'], + ]; + return requiredFieldSets.every((labels) => ( + labels.some((label) => nonEmptyField(section, label)) + )); +} + +function normalizeArtifactReference(value) { + return String(value || '').replace(/\\/g, '/').toLowerCase(); +} + +function observationReferencesPlan(section, planPath) { + const planReference = sectionFieldValue(section, 'Plan') + || sectionFieldValue(section, 'Plan artifact') + || sectionFieldValue(section, 'PLAN'); + if (!isMeaningfulFieldValue(planReference)) return false; + const normalizedReference = normalizeArtifactReference(planReference); + const normalizedPlanPath = normalizeArtifactReference(planPath); + const normalizedPlanName = normalizeArtifactReference(basename(planPath)); + return normalizedReference === normalizedPlanPath + || normalizedReference === normalizedPlanName + || normalizedReference.includes(normalizedPlanPath) + || normalizedReference.includes(normalizedPlanName); +} + +function evaluateBrowserProofObservation(browserProofPlans, matchingSummaries, phasesDir, workspaceRoot) { + const requiredPlans = browserProofPlans.filter((plan) => plan.required === true); + if (requiredPlans.length === 0 || matchingSummaries.length === 0) { + return { + satisfied: true, + sections: [], + blockers: [], + }; + } + + const sections = matchingSummaries.flatMap((summaryPath) => { + const summaryFullPath = join(phasesDir, summaryPath); + if (!existsSync(summaryFullPath)) return []; + return findBrowserProofObservationSections( + readFileSync(summaryFullPath, 'utf-8'), + summaryFullPath, + workspaceRoot + ); + }); + + if (sections.length === 0) { + return { + satisfied: false, + sections, + blockers: [{ + code: 'missing_browser_proof_observation', + severity: 'blocker', + path: matchingSummaries.join(', '), + message: 'A plan requires browser proof, but no SUMMARY.md or linked record contains ## Browser Proof Observation.', + fix_hint: browserProofFixHint('missing_browser_proof_observation'), + }], + }; + } + + const completeSections = sections.filter((section) => isCompleteBrowserProofObservation(section)); + if (completeSections.length === 0) { + return { + satisfied: false, + sections, + blockers: [{ + code: 'incomplete_browser_proof_observation', + severity: 'blocker', + path: matchingSummaries.join(', '), + message: 'Browser Proof Observation is missing required observed-proof fields.', + fix_hint: browserProofFixHint('incomplete_browser_proof_observation'), + }], + }; + } + + if (requiredPlans.length === 1) { + return { + satisfied: true, + sections, + blockers: [], + }; + } + + const unmatchedPlans = requiredPlans.filter((plan) => ( + !completeSections.some((section) => observationReferencesPlan(section, plan.path)) + )); + if (unmatchedPlans.length === 0) { + return { + satisfied: true, + sections, + blockers: [], + }; + } + + return { + satisfied: false, + sections, + blockers: unmatchedPlans.map((plan) => ({ + code: 'unmatched_browser_proof_observation', + severity: 'blocker', + path: plan.path, + message: `No complete Browser Proof Observation references required plan ${plan.path}.`, + fix_hint: browserProofFixHint('unmatched_browser_proof_observation'), + })), + }; +} + function isPlanArtifactSatisfied(artifact) { if (artifact.operation === 'delete') return !artifact.exists; return artifact.exists; @@ -403,13 +794,40 @@ export function buildPhaseVerificationReport(...args) { ? extractPlanFileArtifacts(readFileSync(fullPath, 'utf-8'), workspaceRoot) : []; }); + const browserProofPlans = matchingPlans.map((planPath) => { + const fullPath = join(phasesDir, planPath); + return existsSync(fullPath) + ? evaluateBrowserProofContract(readFileSync(fullPath, 'utf-8'), planPath) + : { + path: planPath, + declaration_present: false, + required: null, + rationale: null, + satisfied: true, + blockers: [], + }; + }); + const browserProofObservationStatus = evaluateBrowserProofObservation( + browserProofPlans, + matchingSummaries, + phasesDir, + workspaceRoot + ); + const browserProofBlockers = [ + ...browserProofPlans.flatMap((plan) => plan.blockers), + ...browserProofObservationStatus.blockers, + ]; const artifactStatus = evaluatePlanArtifacts(artifacts); const legacyVerified = matchingPlans.length > 0 && matchingSummaries.length > 0; const blockedOn = [ ...(prerequisiteBlockers.length > 0 ? ['prerequisites'] : []), ...(artifactStatus.satisfied ? [] : ['artifacts']), + ...(browserProofBlockers.length > 0 ? ['browser_proof'] : []), ]; - const closureVerified = legacyVerified && prerequisiteBlockers.length === 0 && artifactStatus.satisfied; + const closureVerified = legacyVerified + && prerequisiteBlockers.length === 0 + && artifactStatus.satisfied + && browserProofBlockers.length === 0; const result = { phase: normalizePhaseToken(phaseNum), @@ -419,6 +837,12 @@ export function buildPhaseVerificationReport(...args) { artifacts, allExist: artifacts.every((artifact) => artifact.exists), artifact_status: artifactStatus, + browser_proof_status: { + satisfied: browserProofBlockers.length === 0, + plans: browserProofPlans, + observations: browserProofObservationStatus, + blockers: browserProofBlockers, + }, verified: closureVerified, legacy_verified: legacyVerified, phase_artifacts_present: legacyVerified, diff --git a/bin/lib/plan-constants.mjs b/bin/lib/plan-constants.mjs index 4a5e78f9..d37f3e98 100644 --- a/bin/lib/plan-constants.mjs +++ b/bin/lib/plan-constants.mjs @@ -7,11 +7,6 @@ export const PLAN_CHECK_DIMENSIONS = [ 'must_have_quality', 'context_compliance', 'goal_achievement', - 'scope_boundaries', - 'anti_regression_capture', - 'escalation_integrity', - 'closure_honesty', - 'high_leverage_review', 'approach_alignment', ]; diff --git a/distilled/DESIGN.md b/distilled/DESIGN.md index bea25eb6..55b0acbe 100644 --- a/distilled/DESIGN.md +++ b/distilled/DESIGN.md @@ -72,7 +72,7 @@ 59. [Continuity Authority And Planning-State Drift](#d59---continuity-authority-and-planning-state-drift) 60. [Release Closeout Contract](#d60---release-closeout-contract) 61. [Deliberate Subagent Contract](#d61---deliberate-subagent-contract) -62. [Repo-Native UI Proof Contract](#d62---repo-native-ui-proof-contract) +62. [Repo-Native Browser Proof Contract](#d62---repo-native-browser-proof-contract) 63. [Computed-First Control Map](#d63---computed-first-control-map) 64. [Work-Native Continuity Authority](#d64---work-native-continuity-authority) @@ -961,7 +961,7 @@ Implementation lives under `bin/lib/`: | E7 | ERROR | `.planning/templates/codebase/` missing or empty | | E8 | ERROR | `.planning/templates/` missing critical root files (`spec.md`, `roadmap.md`, `auth-matrix.md`, `ui-proof.md`) | | E9 | ERROR | `.planning/templates/brownfield-change/` missing or missing critical files (`CHANGE.md`, `HANDOFF.md`, `VERIFICATION.md`) | -| E10 | ERROR | Known UI proof bundle metadata is unparseable or fails deterministic privacy/claim validation | +| E10 | RETIRED | Retired UI proof bundle metadata validator; current browser proof uses plan declarations and markdown observation records | | W1 | WARN | `generation-manifest.json` missing | | W2 | WARN | Manifest-tracked installed templates/helpers modified locally (hash mismatch vs manifest) | | W3 | WARN | Manifest-tracked installed templates/helpers missing from disk but listed in manifest | @@ -2372,9 +2372,13 @@ Sub-gap (b) was closed by D28's `` mandate and guarded by G30. Sub- - `escalation_triggers` - `approval_gates` - `anti_regression_targets` - - `closure_claim_limit` - - `parallelism_budget` - - `leverage` (`lost / kept / gained`) + - `known_unknowns` + - `browser_proof_required` + - `browser_proof_rationale` + - `must_haves` +- Keep high-risk review pressure in the plan body through Evidence Contract, + Stop-And-Challenge, Approval Gates, and Second-Pass Review sections instead + of expanding the frontmatter into a control-plane schema. - Upgrade planner checking to block missing anti-drift contract fields instead of treating them as optional prose quality. - Thread the new contract through execution and verification so the fields constrain what execution may do and what closure may claim. @@ -2822,49 +2826,47 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru --- -## D62 - Repo-Native UI Proof Contract +## D62 - Repo-Native Browser Proof Contract -**Decision (2026-04-28; revised 2026-05-09):** UI-sensitive work should carry a compact planned proof-slot contract and, when executed, an observed UI proof bundle that references artifacts by path or link while preserving the existing closure evidence kinds: `code`, `test`, `runtime`, `delivery`, and `human`. For live rendered UI proof, `agent-browser` is the default runtime evidence path for consumers, while existing Playwright tests remain the canonical repeatable browser-regression path when present. The deterministic `ui-proof` validator remains provider-agnostic structural validation, but it now validates planned slot specificity, concise tool provenance, local artifact path existence when validating from files, raw-artifact safety for paths and URLs, and failed/partial proof classification so the workflow cannot degrade back into unstructured "looks good" review. Direct phase verification also treats plan frontmatter as the UI-proof declaration authority and fails closed on missing phase prerequisites, empty `ui_proof_slots: []` without `no_ui_proof_rationale`, and invalid required UI proof. +**Decision (2026-04-28; revised 2026-05-09; revised 2026-07-08):** UI-sensitive work should carry a compact browser-proof declaration in plan frontmatter and, when executed, a plain markdown observation record that references artifacts by path or link while preserving the existing closure evidence kinds: `code`, `test`, `runtime`, `delivery`, and `human`. For live rendered UI proof, `agent-browser` is the default runtime evidence path for consumers, while existing Playwright tests remain the canonical repeatable browser-regression path when present. Direct phase verification treats `browser_proof_required` and `browser_proof_rationale` as the declaration authority and fails closed on missing phase prerequisites, invalid browser-proof frontmatter, a missing `## Browser Proof Plan` when proof is required, or a required browser-proof plan that omits route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit. **Context:** -- UI proof targets the recurring failure mode where agents claim a UI works or looks good without rendered proof, matched observations, or explicit human judgment. -- The contract defines proof slots, proof bundles, comparison statuses, fail-closed agent guardrails, deterministic metadata validation, privacy metadata, and health visibility without adding a browser-provider framework. -- GSD's archived planner, executor, and verifier roles preserve strong lifecycle discipline, but they do not provide this UI-specific planned-vs-observed proof model. GSDD keeps the lifecycle leverage and adds a repo-native UI proof substrate without adding a browser-provider framework. +- Browser proof targets the recurring failure mode where agents claim a UI works or looks good without rendered proof, matched observations, or explicit human judgment. +- The contract defines plan declarations, Browser Proof Plan sections, observation records, fail-closed agent guardrails, privacy/safety notes, and claim limits without adding a browser-provider framework or a separate JSON validator. +- GSD's archived planner, executor, and verifier roles preserve strong lifecycle discipline, but they do not provide this UI-specific browser observation model. GSDD keeps the lifecycle leverage and adds a repo-native browser proof substrate without adding a browser-provider framework. - OneShot's QC guidance and Vercel's `agent-browser` skill converge on an interactive browser loop for snapshots, ref-based interaction, screenshots, and network/console-adjacent inspection. GSDD adapts that as a default workflow instruction, not as a hard validator dependency. **Decision:** -- Planning must classify UI-sensitive work and require either `ui_proof_slots` or an explicit `no_ui_proof_rationale`. -- Direct phase verification must read `ui_proof_slots` and `no_ui_proof_rationale` from plan frontmatter only; body prose, fenced examples, and stale sidecars are not declaration authority. -- Direct phase verification must fail nonzero with structured blockers when no matching plan or summary exists, or when `ui_proof_slots: []` lacks a nonblank `no_ui_proof_rationale`. -- When an explicit no-UI rationale exists, stale UI-proof sidecars are warning-level cleanup signals, not proof and not blockers. -- Planned slots record claim, route/state, required evidence kinds, minimum observations, expected artifact types, runnable validation command, environment/viewport, manual-acceptance requirement, claim limit, and requirement IDs. -- Observed proof bundles record claim, requirement/slot IDs, route/state, environment, viewport, evidence inputs, commands/manual steps, observations, artifacts, privacy metadata, result, and claim limits. -- Planned slots must be tight enough for the plan checker to reject vague proof: specific route/state, viewport rationale or narrowed claim limit, minimum observations, expected artifact types, runnable validation, and matchability back to the exact UI claim. +- Planning must classify UI-sensitive work with `browser_proof_required: true|false` and a nonblank `browser_proof_rationale`. +- Direct phase verification must read browser-proof declaration authority from plan frontmatter only. Body prose and stale sidecars are not declaration authority. +- Direct phase verification must fail nonzero with structured blockers when no matching plan or summary exists, when browser-proof frontmatter is invalid, or when a required Browser Proof Plan is missing or incomplete. +- When browser proof is not required and the rationale is explicit, stale browser-proof observations are warning-level cleanup signals, not proof and not blockers. +- Browser Proof Plan sections record route/state, viewport coverage, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, privacy/safety note, and claim limit. +- Observation records record the actual route/state, viewport, runtime path, command/manual step, rendered observations, artifacts, result, privacy/safety note, stale-after trigger, and claim limit. +- Browser Proof Plan sections must be tight enough for the plan checker to reject vague proof: specific route/state, viewport coverage or narrowed claim, runtime path, rendered observations, artifacts/privacy notes, evidence command or no-command rationale, and claim limit. - The planner chooses viewport coverage, but responsive or layout-sensitive claims require desktop/mobile or equivalent state coverage unless the claim is explicitly narrowed. - Execution defaults to `agent-browser` for live UI runtime proof: open the route/state, capture interactive snapshots/refs where relevant, exercise the changed flow, capture screenshots for planned viewport(s), and record relevant console/network observations. - Existing Playwright tests or package scripts remain the canonical repeatable browser-regression evidence when present. Playwright scripting is reserved for checks `agent-browser` cannot cover cleanly, such as JS-disabled behavior, structured console listeners, or multi-context testing. -- Verification compares planned slots to observed bundles using `satisfied`, `partial`, `missing`, `waived`, `deferred`, and `not_applicable`; waiver and deferral are not proof. -- UI correctness claims fail closed unless rendered proof is matched exactly to claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit, or an explicit waiver/deferment narrows the claim. -- Human acceptance may close a narrowed claim and record proof debt, but it must not convert missing or mismatched non-human evidence into `satisfied` proof. +- UI correctness claims fail closed unless rendered proof is matched exactly to route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit, or an explicit waiver/deferment narrows the claim. +- Human acceptance may close a narrowed claim and record proof debt, but it must not convert missing or mismatched non-human evidence into satisfied proof. - Screenshots, traces, videos, reports, accessibility scans, Gherkin, and visual diffs are artifact types or activities mapped onto the five existing evidence kinds, not new evidence kinds. -- Source annotations, AST/cAST findings, semantic search hits, comments, and Semble-like retrieval may discover proof obligations, but they are discovery hints only and do not satisfy proof slots. +- Source annotations, AST/cAST findings, semantic search hits, comments, and Semble-like retrieval may discover proof obligations, but they are discovery hints only and do not satisfy browser proof. - Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver, and human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. -- Deterministic validation keeps the evidence and comparison-status vocabularies unchanged: planned slots require specific claim, route/state, evidence, expected artifacts, validation, viewport, and claim-limit fields; artifact entries require `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to `local_only` plus `safe_to_publish: false`; `bin/lib/ui-proof.mjs` validates required bundle/observation fields, structured command/manual-step entries, fixed evidence kinds, concise `tools_used` IDs, claim/result statuses, comparison statuses, failure classification for failed/partial proof, claim limits, privacy metadata, safe artifact references, local artifact path existence when validating file-backed bundles, and public/tracked/delivery proof claims backed by local-only, unsafe, unsanitized, or privacy-contradictory artifacts. -- `gsdd health` reports invalid known UI proof bundles as E10 using the same validator, staying read-only and avoiding raw artifact content inspection. -- Failed UI proof is reported through existing GSDD gap/proof-debt language. Product behavior defects, missing or blocked infrastructure, flaky harnesses, and ambiguous specs explain causes, but they do not add new evidence kinds, result statuses, or comparison statuses. +- The current contract is validator-light by design: direct phase verification checks declaration shape and required Browser Proof Plan fields, while substantive rendered proof remains a workflow/role obligation captured in summaries or observation records. +- Failed browser proof is reported through the failure-cause names in `distilled/references/proof-rules.md`: product behavior defects, missing or blocked infrastructure, flaky harnesses, and ambiguous specs explain causes, but they do not add new evidence kinds. **Leverage:** -- Lost: UI-sensitive work now carries a small proof-contract burden, and default live proof guidance adds slightly more specificity for planners/checkers to enforce. -- Kept: repo-native markdown artifacts, optional project tooling, fixed closure evidence kinds, generated-surface freshness, the plan/execute/verify separation, and provider-agnostic deterministic metadata validation. -- Gained: exact claim-to-proof traceability, strict comparison statuses, privacy and claim-limit metadata, fail-closed overclaim guardrails, deterministic metadata validation, a concrete live browser evidence path, and health-visible protection against unsafe public proof claims. +- Lost: UI-sensitive work still carries a small proof-contract burden, and direct verification checks only shape rather than raw screenshot/report contents. +- Kept: repo-native markdown artifacts, optional project tooling, fixed closure evidence kinds, generated-surface freshness, the plan/execute/verify separation, and a concrete live browser evidence path. +- Gained: a much simpler consumer contract, exact claim-to-observation traceability, privacy/safety notes, claim limits, and fail-closed overclaim guardrails without a non-existent validator command. **Evidence:** - `distilled/templates/ui-proof.md` - `distilled/workflows/plan.md`, `distilled/workflows/execute.md`, `distilled/workflows/quick.md`, `distilled/workflows/verify.md` - `agents/planner.md`, `agents/executor.md`, `agents/verifier.md`, `distilled/templates/delegates/plan-checker.md` -- `bin/lib/templates.mjs`, `bin/lib/ui-proof.mjs`, `bin/lib/health.mjs`, `bin/lib/phase.mjs`, `bin/lib/rendering.mjs` +- `bin/lib/templates.mjs`, `bin/lib/health.mjs`, `bin/lib/phase.mjs`, `bin/lib/rendering.mjs` - `tests/phase.test.cjs`, `tests/gsdd.guards.test.cjs`, `tests/gsdd.health.test.cjs`, `tests/gsdd.init.test.cjs` -- GSD comparison: the upstream planner, executor, and verifier role patterns preserve lifecycle rigor, but they do not define UI proof slots or planned-vs-observed UI proof bundles. +- GSD comparison: the upstream planner, executor, and verifier role patterns preserve lifecycle rigor, but they do not define browser-proof plan declarations or observation records. - OneShot QC source: `https://github.com/oneshot-repo/OneShot/tree/main/skills` - Vercel `agent-browser` docs: `https://github.com/vercel-labs/agent-browser/blob/main/skill-data/core/SKILL.md` and `https://agent-browser.dev/snapshots` - Playwright docs: `https://playwright.dev/docs/trace-viewer`, `https://playwright.dev/docs/next/screenshots`, and `https://playwright.dev/mcp/tools/tracing` diff --git a/distilled/EVIDENCE-INDEX.md b/distilled/EVIDENCE-INDEX.md index 4c7b1812..f122408d 100644 --- a/distilled/EVIDENCE-INDEX.md +++ b/distilled/EVIDENCE-INDEX.md @@ -486,11 +486,11 @@ - GSD comparison source: `get-shit-done/workflows/new-project.md` - `tests/gsdd.guards.test.cjs`, `tests/gsdd.invariants.test.cjs` -## D62 — Repo-Native UI Proof Contract +## D62 — Repo-Native Browser Proof Contract - `distilled/templates/ui-proof.md` - `distilled/workflows/plan.md`, `distilled/workflows/execute.md`, `distilled/workflows/quick.md`, `distilled/workflows/verify.md` - `agents/planner.md`, `agents/executor.md`, `agents/verifier.md`, `distilled/templates/delegates/plan-checker.md` -- `bin/lib/templates.mjs`, `bin/lib/ui-proof.mjs`, `bin/lib/health.mjs`, `bin/lib/rendering.mjs` +- `bin/lib/templates.mjs`, `bin/lib/health.mjs`, `bin/lib/phase.mjs`, `bin/lib/rendering.mjs` - `tests/phase.test.cjs`, `tests/gsdd.guards.test.cjs`, `tests/gsdd.health.test.cjs`, `tests/gsdd.init.test.cjs` - OneShot QC/browser policy: https://github.com/oneshot-repo/OneShot/tree/main/skills - Vercel agent-browser docs: https://github.com/vercel-labs/agent-browser/blob/main/skill-data/core/SKILL.md, https://agent-browser.dev/snapshots @@ -503,7 +503,7 @@ - Agent-hostile UI semantics source: https://dev.to/ratikkoka/your-ui-is-invisible-to-ai-agents-heres-how-to-fix-it-1ib3 - Browser-harness efficiency source: https://dev.to/louaiboumediene/the-ai-harness-why-your-ai-coding-agent-is-only-as-smart-as-the-repo-you-put-it-in-cml - Dynamic-UI harness brittleness source: https://tessl.io/blog/webmcp-making-web-apps-faster-and-cheaper-for-ai-agents/ -- Long-term pitfalls carried forward: do not accept screenshot-free "looks good" claims, weak planned slots, unverified artifact paths, stale interactive refs after page mutation, partial/failed proof without failure classification, raw artifact publication without privacy metadata, browser contention in parallel checks, or semantic/selector-poor UI that forces fragile coordinate inspection. +- Long-term pitfalls carried forward: do not accept screenshot-free "looks good" claims, weak browser-proof plans, unverified artifact paths, stale interactive refs after page mutation, partial/failed proof without failure classification, raw artifact publication without privacy/safety notes, browser contention in parallel checks, or semantic/selector-poor UI that forces fragile coordinate inspection. - Supporting spec/runtime docs: https://openspec.dev/, https://www.lean-spec.dev/docs/guide/first-principles, https://help.openai.com/en/articles/11369540-codex-in-chatgpt, https://docs.claude.com/en/docs/agents-and-tools/agent-skills, https://docs.github.com/en/copilot/concepts/prompting/response-customization ## D63 — Computed-First Control Map diff --git a/distilled/references/observation-record.md b/distilled/references/observation-record.md index 40023f1d..13e9e0ea 100644 --- a/distilled/references/observation-record.md +++ b/distilled/references/observation-record.md @@ -13,8 +13,5 @@ actually observed. Plain markdown, one record per checked flow: - **safe to publish**: yes/no — may the artifacts leave the repo (no secrets, no private data)? - **stale after**: what change would invalidate this observation (optional). -- **result**: worked / failed (say why) / partly (say what is missing). - -This replaces the retired machine-validated UI-proof bundle (`ui_proof_slots`, -`gsdd ui-proof validate/compare`). The structure survives; the JSON schema -validator does not. +- **result**: worked / failed (say why) / partly (say what is missing). For + failed or partial proof, use the failure-cause names in proof-rules.md. diff --git a/distilled/references/proof-rules.md b/distilled/references/proof-rules.md index ce2ebb32..b6a4589d 100644 --- a/distilled/references/proof-rules.md +++ b/distilled/references/proof-rules.md @@ -13,3 +13,12 @@ installed), `human` (a person confirmed). - UI work: proof means opening a real browser (agent-browser preferred, Playwright fallback), inspecting the rendered DOM and behavior — not screenshots alone. Record what was observed (see observation-record.md). + +Failure-cause names for failed or partial browser/runtime proof: + +- `product_bug`: the product behavior or rendered UI is wrong. +- `missing_infra`: the required runtime, credentials, fixture data, server, or + browser path is unavailable. +- `flaky_harness`: the proof path is unstable or nondeterministic in a way that + prevents a trustworthy result. +- `ambiguous_spec`: the expected behavior or acceptable result is unclear. diff --git a/distilled/templates/delegates/plan-checker.md b/distilled/templates/delegates/plan-checker.md index 0fd6ca83..5a202dfe 100644 --- a/distilled/templates/delegates/plan-checker.md +++ b/distilled/templates/delegates/plan-checker.md @@ -13,31 +13,30 @@ Read only the explicit inputs provided by the orchestrator: Do NOT inherit the planner's hidden reasoning. Treat the current plans as untrusted drafts that must prove they will achieve the phase goal before execution. Verify these dimensions: -- `requirement_coverage`: every phase requirement is covered by at least one concrete task +- `requirement_coverage`: every phase requirement is covered by at least one concrete task. - `task_completeness`: every executable task has files, action, verify, and done fields. Additionally check verify quality: - **Runnable?** Does `` contain at least one command that an executor can run programmatically (e.g., a shell command, test runner invocation, curl request)? If ALL verify items are observational text with no runnable command -> `blocker`. - **Fast?** Do verify commands complete quickly? Flag full E2E suites (playwright, cypress, selenium) without a faster smoke test -> `warning`. Flag watch-mode flags (`--watchAll`, `--watch`) -> `blocker`. Flag arbitrary delays > 30s -> `warning`. - **Ordered?** If a verify command references a test file, does an earlier task in the plan create that file? If the referenced file has no prior task producing it -> `blocker`. -- `dependency_correctness`: ordering, dependencies, and plan structure are coherent -- `key_link_completeness`: important wiring/integration links are planned, not just isolated artifacts -- `scope_sanity`: plans are sized so an executor can complete them without context collapse -- `must_have_quality`: success criteria and must-haves are specific, observable, and reflected in tasks + - **Browser proof command?** If the plan requires browser proof, the Browser Proof Plan names a runnable evidence command or an explicit narrowed no-command rationale. A rendered-UI claim with neither is a `blocker`. +- `dependency_correctness`: ordering, dependencies, and plan structure are coherent. +- `key_link_completeness`: important wiring/integration links are planned, not just isolated artifacts. +- `scope_sanity`: plans are sized so an executor can complete them without context collapse, and hard boundaries, anti-goals, and explicit out-of-scope items are preserved in task scope. +- `must_have_quality`: success criteria and must-haves are specific, observable, and reflected in tasks. - `context_compliance`: locked decisions are honored and deferred ideas stay out of scope. Additionally check scope consistency: - **Must-have coverage?** Every must-have requirement mapped to this phase in SPEC.md must appear in at least one plan task. A must-have that silently disappears from the plan is a `blocker`. - **Deferred exclusion?** Items marked "Nice to Have", "Deferred", or "Out of Scope" in SPEC.md must not appear as plan tasks. Present → `blocker`. - **Cross-surface consistency?** If SPEC.md marks an item as must-have but APPROACH.md marks it as deferred (or vice versa), surface the contradiction → `blocker`. Include a `fix_hint` asking the planner to resolve the conflict with the user before proceeding. + - **Anti-regression captured?** Known prior failures, compatibility risks, and behavior that must not regress are represented in tasks or verification. + - **Escalation preserved?** Tasks include checkpoints or escalation when evidence, permissions, user decisions, or risky ambiguity are required. + - **Abstraction justified?** Reject a task or refactor whose only rationale is "cleaner code" without reducing risk, removing meaningful duplication, simplifying a consumer path, or matching an existing repo pattern. - `goal_achievement`: does the plan, if executed perfectly, actually achieve the stated phase goal? Check: - **Goal addressed?** Compare the phase goal statement to the plan's collective task outputs. Would successful completion of all tasks deliver the goal? If the goal says "users can authenticate" but tasks only set up database schema → `blocker`. - **Success criteria reachable?** Are the phase success criteria from ROADMAP.md achievable through the planned tasks? Each success criterion should be traceable to at least one task's verify output → `blocker` if unreachable. - **Outcome observable?** After execution, could a human or automated check confirm the goal was met? Plans that produce only internal artifacts with no user-visible or testable outcome → `warning`. -- `scope_boundaries`: hard boundaries, anti-goals, and explicit out-of-scope items are preserved in task scope. -- `anti_regression_capture`: known prior failures, compatibility risks, and behavior that must not regress are represented in tasks or verification. -- `escalation_integrity`: tasks include checkpoints or escalation when evidence, permissions, user decisions, or risky ambiguity are required. -- `closure_honesty`: the plan's done criteria and evidence limits support only claims that execution can actually prove. -- `closure_honesty`: for UI proof, reject agent-only `looks good` closure, artifact-count proof, unsupported evidence kinds, and human acceptance that converts missing/mismatched non-human evidence into `satisfied` proof. Waiver, deferment, proof debt, or narrowed-claim language is acceptable only when the stronger UI claim is not treated as proven. -- `closure_honesty`: for UI proof planning, reject weak slots that omit a specific route/state, viewport rationale or narrowed viewport claim limit, minimum observations, expected artifact types, runnable validation, or a way to compare observed proof back to the planned claim. Treat under-specified viewport coverage as a blocker for responsive or layout-sensitive claims. `agent-browser` is the default live runtime evidence path; do not block a slot solely for using another project-native browser path, but require the plan to explain the `agent-browser` availability constraint and fallback choice. -- `closure_honesty`: for UI proof privacy, require artifact `visibility`, `retention`, `sensitivity`, and `safe_to_publish`, require `gsdd ui-proof validate` or `gsdd health` when bundle metadata exists, and reject public/tracked/delivery/publication proof claims backed by local-only or `safe_to_publish: false` artifacts. -- `high_leverage_review`: high-leverage surfaces have a second-pass review or equivalent contradiction/staleness check before completion. + - **Claim honest?** Done criteria and evidence limits support only claims that execution can actually prove. For browser proof, reject agent-only "looks good" closure, artifact-count proof, unsupported evidence kinds, and human acceptance that replaces required code, test, runtime, or delivery evidence. + - **Browser proof specific?** Required browser proof names exact route/state, viewport coverage or a narrowed viewport claim, runtime path, rendered observations, artifacts/privacy notes, and claim limit. Under-specified viewport coverage is a blocker for responsive or layout-sensitive claims, and fallback browser tooling must state the `agent-browser` availability constraint. + - **Second pass present?** Shared or high-leverage surfaces have a final contradiction/staleness review before completion. - `approach_alignment`: when APPROACH.md is provided, verify that plan tasks implement the chosen approaches from the user's decisions. Check: - **Alignment proof valid?** When `workflow.discuss` is `true`, APPROACH.md must record `alignment_status: user_confirmed` or `alignment_status: approved_skip`. Missing alignment proof, unknown status, or agent-discretion-only proof -> `blocker` with `fix_hint` telling the planner to revise APPROACH.md through real user alignment or an explicit user-approved skip. - **Canonical proof fields present?** APPROACH.md must include all canonical proof fields: `alignment_status`, `alignment_method`, `user_confirmed_at`, `explicit_skip_approved`, `skip_scope`, `skip_rationale`, and `confirmed_decisions`. Missing fields -> `blocker`. @@ -56,7 +55,7 @@ Return JSON only as a single finding summary object with this shape: "summary": "One sentence overall assessment", "issues": [ { - "dimension": "requirement_coverage | task_completeness | dependency_correctness | key_link_completeness | scope_sanity | must_have_quality | context_compliance | goal_achievement | scope_boundaries | anti_regression_capture | escalation_integrity | closure_honesty | high_leverage_review | approach_alignment", + "dimension": "requirement_coverage | task_completeness | dependency_correctness | key_link_completeness | scope_sanity | must_have_quality | context_compliance | goal_achievement | approach_alignment", "severity": "blocker | warning", "description": "What is wrong", "plan": "01-PLAN", diff --git a/distilled/templates/ui-proof.md b/distilled/templates/ui-proof.md index 26ca5480..eff33b41 100644 --- a/distilled/templates/ui-proof.md +++ b/distilled/templates/ui-proof.md @@ -1,200 +1,96 @@ -# UI Proof Bundle Template +# Browser Proof Observation Template -Use this template when work affects rendered UI or when a plan defines `ui_proof_slots`. Keep the bundle compact, claim-specific, and attached to the relevant phase, quick task, or brownfield change. +Use this template when work affects rendered UI or when a plan sets +`browser_proof_required: true`. Keep the record compact, claim-specific, and +attached to the relevant phase, quick task, or brownfield change. -UI proof uses the existing closure evidence kinds only: `code`, `test`, `runtime`, `delivery`, and `human`. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes are artifact types or activities that map onto those evidence kinds. They are not new evidence kinds. +Browser proof uses the existing closure evidence kinds only: `code`, `test`, +`runtime`, `delivery`, and `human`. Screenshots, traces, videos, reports, +accessibility scans, visual diffs, and manual notes are artifacts or activities +that map onto those evidence kinds. They are not new evidence kinds. -For live rendered UI evidence, default to `agent-browser`: open the route, capture an interactive snapshot/refs when interaction is part of the claim, exercise the changed flow, capture screenshots for the planned viewport(s), and record console/network observations when they affect the claim. If the repo already has Playwright tests or a package script wrapping them, those remain the canonical repeatable regression path; use them as `test` evidence and use `agent-browser` for complementary live runtime proof. Do not introduce new Playwright, Cypress, Storybook, CI, browser MCP, or visual-regression infrastructure just to satisfy this template. Use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled behavior, structured console listeners, or multi-context testing. +For live rendered UI evidence, default to `agent-browser`: open the route, +capture interactive refs when interaction is part of the claim, exercise the +changed flow, capture screenshots for planned viewport(s), and record +console/network observations when they affect the claim. If the repo already +has Playwright tests or a package script wrapping them, those remain the +repeatable regression path; use them as `test` evidence and use browser +observation for complementary runtime proof. -Tool availability is part of the proof record. In runtimes where `agent-browser` is not available, first state that availability constraint, then use the closest project-native interactive browser path and record the fallback in `evidence_inputs.tools_used`, `commands_or_manual_steps`, and `claim_limits`. A fallback can support a narrowed local runtime claim, but it must not silently pretend that the default `agent-browser` path ran. +Do not introduce new Playwright, Cypress, Storybook, CI, browser MCP, or visual +regression infrastructure just to satisfy this template. Use Playwright +scripting only for checks `agent-browser` cannot cover cleanly, such as +JS-disabled behavior, structured console listeners, or multi-context testing. -## Planned Proof Slots +Tool availability is part of the proof record. In runtimes where +`agent-browser` is not available, first state that availability constraint, then +use the closest project-native interactive browser path and narrow the claim to +what that fallback actually proves. -Every UI-sensitive plan needs either at least one slot under `ui_proof_slots` or an explicit `no_ui_proof_rationale` explaining why no rendered UI proof is required. +## Plan Declaration + +Every UI-sensitive plan should contain: ```yaml -ui_proof_slots: - - slot_id: ui-01 - requirement_id: REQ-01 - claim: "User can complete the changed flow without a broken rendered UI." - route_state: "/example route, role, data state, and UI state to inspect" - required_evidence_kinds: [test, runtime] - optional_evidence_kinds: [human] - minimum_observations: - - "Changed control is visible and usable in the stated state." - - "Expected interaction completes without console/runtime error." - expected_artifact_types: [screenshot, report] - validation_command: "gsdd ui-proof compare {work_item_dir}/ui-proof-slots.json {work_item_dir}/UI-PROOF.md" - environment: - app_url: "http://localhost:3000" - data_state: "synthetic or seeded data" - viewport: - width: 1280 - height: 720 - notes: "State why this viewport is enough for the claim, or add separate slots/observations for mobile, desktop, or responsive states." - manual_acceptance_required: false - claim_limit: "Does not prove cross-browser layout, full accessibility conformance, production delivery, or unrelated UI states." -no_ui_proof_rationale: null +browser_proof_required: true +browser_proof_rationale: Visible route or interaction changes require rendered browser proof. ``` -Slot rules: -- Keep each slot tied to one exact UI claim. -- Use the lightest proof that can catch a botched rendered experience for that claim. -- Specify the route/state, viewport choice, minimum observations, expected artifact types, and runnable validation path tightly enough that a checker can reject vague proof before execution. -- The planner chooses the viewport set, but the slot must explain the choice. Include desktop and mobile proof when the claim covers responsive layout or when the changed surface is likely to behave differently across those sizes; otherwise narrow the claim limit. -- Source annotations, AST/cAST findings, semantic search hits, comments, and Semble-like retrieval may help discover proof obligations. They are discovery hints only; they do not satisfy proof slots. -- Do not add Playwright, Cypress, Storybook, Cucumber, CI, browser MCP, or visual-regression tooling by default. -- Human approval is required for visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication decisions. -- Human approval does not replace required non-human evidence when the slot requires `code`, `test`, `runtime`, or `delivery` evidence. - -## Observed Proof Bundle - -Create or update this bundle during execution or verification when planned UI proof slots exist. JSON is the canonical machine-readable proof bundle format. Markdown proof files must include fenced JSON for deterministic validation. - -Replace placeholders such as `{work_item_dir}` with the current phase, quick-task, or brownfield-change directory before running commands or validating the bundle. - -```json -{ - "proof_bundle_version": 1, - "scope": { - "work_item": "phase-or-quick-or-brownfield-id", - "requirement_ids": ["REQ-01"], - "slot_ids": ["ui-01"], - "claim": "User can complete the changed flow without a broken rendered UI." - }, - "route_state": { - "route": "/example", - "state": "role, data state, feature flag, loading/error/empty state, or component story" - }, - "environment": { - "app_url": "http://localhost:3000", - "browser": "agent-browser default; record fallback when unavailable", - "browser_version": "record if known", - "os": "record if relevant", - "data_state": "synthetic or seeded data" - }, - "viewport": { - "width": 1280, - "height": 720, - "device_scale_factor": "record if relevant" - }, - "evidence_inputs": { - "kinds": ["test", "runtime"], - "tools_used": ["playwright", "agent-browser"] - }, - "commands_or_manual_steps": [ - { - "command": "npm run test:e2e -- changed-flow.spec.ts", - "exit_code": 0, - "result": "passed", - "attempts": 1 - }, - { - "command": "agent-browser open http://localhost:3000/example && agent-browser snapshot -i && agent-browser screenshot {work_item_dir}/artifacts/example-1280.png --full", - "result": "passed", - "attempts": 1 - }, - { - "manual_step": "Using agent-browser refs, complete the changed interaction as a synthetic user and check for visible breakage or relevant console/network failures.", - "result": "passed" - } - ], - "observations": [ - { - "observation": "Changed control is visible and completes the flow.", - "claim": "User can complete the changed flow without a broken rendered UI.", - "route_state": { - "route": "/example", - "state": "role, data state, feature flag, loading/error/empty state, or component story" - }, - "evidence_kind": "runtime", - "artifact_refs": ["test-results/changed-flow-report/index.html", "{work_item_dir}/artifacts/example-1280.png"], - "privacy": { - "data_classification": "synthetic", - "raw_artifacts_safe_to_publish": false, - "retention": "temporary_review" - }, - "result": "passed", - "claim_limit": "Does not prove Safari/WebKit behavior." - } - ], - "artifacts": [ - { - "path": "test-results/changed-flow-report/index.html", - "type": "report", - "visibility": "local_only", - "retention": "temporary_review", - "sensitivity": "possible", - "safe_to_publish": false, - "notes": "Local report only; not public proof." - }, - { - "path": "{work_item_dir}/artifacts/example-1280.png", - "type": "screenshot", - "visibility": "local_only", - "retention": "temporary_review", - "sensitivity": "possible", - "safe_to_publish": false, - "notes": "Local screenshot only; not public proof unless sanitized and reclassified." - } - ], - "privacy": { - "data_classification": "synthetic", - "redactions": [], - "raw_artifacts_safe_to_publish": false, - "retention": "Keep metadata bundle; keep raw artifacts only while needed for review or failed proof triage." - }, - "manual_acceptance": { - "required": false, - "reviewer": null, - "result": "not_applicable" - }, - "result": { - "claim_status": "passed", - "comparison_status_by_slot": { - "ui-01": "satisfied" - }, - "failure_classification": null - }, - "claim_limits": [ - "Does not prove Safari/WebKit behavior.", - "Does not prove full WCAG conformance.", - "Does not prove deployed production behavior." - ] -} +Plans that do not claim rendered UI behavior should contain: + +```yaml +browser_proof_required: false +browser_proof_rationale: CLI/docs/backend-only work; no visible UI behavior is claimed. ``` -Bundle rules: -- Reference raw screenshots, traces, videos, DOM snapshots, reports, accessibility scans, Gherkin, and visual diffs by path or link. Do not store raw binary or sensitive artifacts inline. -- Each observation must identify the claim, route/state, evidence kind, artifact references behind it, privacy metadata, result, and claim limit it supports. -- Every observation `artifact_refs` value must match an `artifacts[].path` or `artifacts[].url` value. -- Artifact count is never proof. Unsupported or weakly linked artifacts are `partial`, `missing`, `waived`, or `deferred`, not `satisfied`. -- Each artifact must record the locked privacy fields `visibility`, `retention`, `sensitivity`, and `safe_to_publish`. -- Raw screenshots, traces, videos, DOM snapshots, and reports default to `visibility: local_only` plus `safe_to_publish: false` unless explicitly classified as sanitized and safe to publish. -- Local-only or `safe_to_publish: false` artifacts can support local review only; they must not back tracked, public, delivery, release, or publication proof claims. -- Human acceptance may close a narrowed claim only by recording waiver, deferment, or proof debt; it must not upgrade missing or mismatched non-human proof to `satisfied`. -- Quick-mode UI proof should use deterministic synthetic IDs such as `quick-001` and `quick-001-ui-01` when roadmap requirement IDs do not exist. -- Classify failed UI proof using existing GSDD gap/proof-debt language: `product_bug`, `missing_infra`, `flaky_harness`, or `ambiguous_spec`. Do not add new result statuses or evidence kinds for those causes. +When browser proof is required, the plan body should include: + +```markdown +## Browser Proof Plan +Routes/states: /example route, role, data state, and UI state to inspect +Viewports: Desktop 1280px and mobile 390px, or a narrowed viewport claim +Runtime path: agent-browser preferred; explain fallback constraints if unavailable +Evidence command: npm run test:e2e -- --grep "changed flow" +Observations: Changed control is visible, interaction completes, no relevant console/network failures +Artifacts: .work/.../artifacts/example-1280.png (local-only unless sanitized) +Claim limit: Proves only this changed route/state and viewport set +``` -## Deterministic Validation +If no command can reproduce the proof because the runtime path is manual or +interactive only, replace `Evidence command:` with: -Use `gsdd ui-proof validate ` on JSON proof-bundle metadata or markdown fenced JSON before relying on a bundle for closure; add `--claim ` only when validating that stronger proof use. Use `gsdd ui-proof compare [observed-bundle-json ...]` when verifying planned proof slots against observed bundles through the deterministic product-facing path. Required planned-slot fields are `slot_id`, `claim`, `route_state`, `required_evidence_kinds`, `minimum_observations`, `expected_artifact_types`, `validation_command`, `environment`, `viewport`, `manual_acceptance_required`, and `claim_limit`. Required observed-bundle top-level fields are `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits`. The validator checks planned-slot specificity, required bundle and observation fields, structured command/manual-step entries, fixed evidence kinds, concise `tools_used` IDs, `result.claim_status`, observation `result`, comparison statuses, failure classification for failed/partial proof, non-empty claim limits, locked artifact and observation privacy fields, observation-to-artifact references, workspace-relative/http(s) artifact references, existing local artifact paths when validating from files, and explicit public/tracked/delivery proof claims that rely on local-only, unsafe, unsanitized, or privacy-contradictory artifacts. `claim_status`, observation `result`, and command/manual-step `result` use `passed`, `failed`, `partial`, `waived`, `deferred`, or `not_applicable`; failed/partial proof uses `product_bug`, `missing_infra`, `flaky_harness`, or `ambiguous_spec`. It does not inspect raw screenshot, trace, video, DOM, or report contents and does not require any specific browser provider such as `agent-browser`. +```markdown +No-command rationale: agent-browser/manual refs were required for this local runtime; claim is limited to the observed session. +``` + +## Observation Record -## Comparison Statuses +Write one record per checked flow: -Use these statuses when comparing planned slots to observed proof: +```markdown +## Browser Proof Observation -| Status | Meaning | Claim impact | -| --- | --- | --- | -| `satisfied` | Required observations and evidence kinds are present, scoped, and inspectable for the exact claim. | Supports the scoped UI claim. | -| `partial` | Some proof exists, but observations, artifact references, evidence kinds, privacy metadata, or assurance are weaker than planned. | Record a reduced claim or gap. | -| `missing` | Required proof is absent. | Blocks the UI claim unless explicitly waived or deferred. | -| `waived` | A human or approved plan waiver accepts the risk. | Does not prove the UI claim. | -| `deferred` | Proof moved to later work. | Current work must not claim the UI behavior is proven. | -| `not_applicable` | Accepted rationale says no UI proof is required. | No UI proof gap for that claim. | +- Flow: /example route, role, data state, and UI state checked +- Viewports: 1280x720 desktop, 390x844 mobile +- Runtime path: agent-browser +- Evidence command: npm run test:e2e -- --grep "changed flow" +- Observed: Changed control rendered, interaction completed, no relevant console/network failures +- Artifacts: + - .work/.../artifacts/example-1280.png — local-only, not safe to publish + - .work/.../artifacts/example-390.png — local-only, not safe to publish +- Result: passed +- Claim limit: Proves only the scoped route/state, data setup, and viewport set. +- Stale after: route markup, interaction behavior, data fixture, or viewport assumptions change. +``` -Proof debt notes should name the slot, claim, route/state, missing or weak linkage, human acceptance basis, narrowed claim limit, and follow-up trigger. +For failed or partial proof, use the failure-cause names defined in +`distilled/references/proof-rules.md` and record the narrowed claim or follow-up +trigger. ## Claim Boundary -A UI proof bundle proves only the scoped claim, route/state, environment, viewport, observations, and evidence kinds it records. It does not imply broad visual quality, cross-browser coverage, full accessibility conformance, production delivery, release readiness, or public proof unless those dimensions are explicitly planned, evidenced, and classified safe to publish. +Browser proof proves only the scoped route/state, environment, viewport, +observations, artifacts, and evidence kinds it records. It does not imply broad +visual quality, cross-browser coverage, full accessibility conformance, +production delivery, release readiness, or public proof unless those dimensions +are explicitly planned, evidenced, and safe to publish. diff --git a/distilled/workflows/audit-milestone.md b/distilled/workflows/audit-milestone.md index c7ccaacd..ae3bce02 100644 --- a/distilled/workflows/audit-milestone.md +++ b/distilled/workflows/audit-milestone.md @@ -126,7 +126,7 @@ Combine: - Integration checker's report (wiring gaps, auth gaps, broken flows, requirements integration map) - Evidence observations by kind (`code`, `test`, `runtime`, `delivery`, `human`) from phase verifications, summaries, integration findings, and delivery metadata - Release claim posture observations: selected `release_claim_posture`, unsupported claims, waivers, deferrals, and contradiction checks for public, runtime, delivery, planning-drift, and generated-surface claims -- UI proof debt from phase/quick proof bundles or verification gaps, preserving the rule that waiver/deferment/human acceptance narrows claims rather than satisfying missing proof +- Browser-proof debt from phase/quick observation records or verification gaps, preserving the rule that waiver/deferment/human acceptance narrows claims rather than satisfying missing proof ## 5. 3-Source Cross-Reference diff --git a/distilled/workflows/execute.md b/distilled/workflows/execute.md index 625cf584..6da5df7e 100644 --- a/distilled/workflows/execute.md +++ b/distilled/workflows/execute.md @@ -107,7 +107,7 @@ For each task in the plan, follow this loop: ### Frontmatter And Task Semantics The executor consumes the plan schema defined by `/gsdd-plan`: -- frontmatter keys: `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `must_haves`, `tdd` +- frontmatter keys: `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `browser_proof_required`, `browser_proof_rationale`, `must_haves`, `tdd` - task types: - `type="auto"` - proceed without pausing - `type="checkpoint:user"` - stop for a required user decision or human-only step @@ -164,11 +164,10 @@ Before reporting a task complete: - if an API change is involved, hit the endpoint or targeted integration path - A task is not complete because code was written. It is complete when the intended verification path actually passes. -### UI Proof Execution -If the plan defines non-empty `ui_proof_slots`, create or update the observed UI proof bundle before claiming completion; required top-level fields are `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits`. -Use `agent-browser` as the default live UI proof path. Record the planned route/state open, interactive snapshots/refs when interaction is part of the claim, changed-flow interaction, screenshots for planned viewport(s), and relevant console/network observations. If `agent-browser` is unavailable, record the availability constraint and the closest project-native interactive browser fallback in the proof bundle instead of silently treating the fallback as the default path. If the repo already has Playwright tests or a package script wrapping them, run the relevant targeted test as canonical repeatable regression evidence; keep `agent-browser` as complementary runtime proof. Use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled, structured console, or multi-context verification. Do not install Playwright, Cypress, Cucumber, Storybook, browser MCP, CI, or visual-regression tooling by default. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes map onto existing evidence kinds, not new evidence kinds; reference raw artifacts by path/link instead of storing them inline. -Each artifact entry must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to `local_only` and `safe_to_publish: false` unless explicitly sanitized. Use `gsdd ui-proof validate ` when bundle metadata exists, adding `--claim <...>` only when relying on the bundle for public, tracked, delivery, release, or publication proof. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication decisions require human evidence or explicit waiver; artifact count, source comments, AST/cAST findings, semantic search, and Semble-like retrieval are not proof. If evidence does not match the slot claim, route/state, observation, artifact path/manual step, privacy metadata, result, and claim limit, record proof debt, waiver, deferment, or reduced claim language rather than `satisfied` proof. -Classify failed UI proof using existing gap/proof-debt language: `product_bug`, `missing_infra`, `flaky_harness`, or `ambiguous_spec`. Do not add new evidence kinds or result statuses for those causes. +### Browser Proof Execution +If the plan sets `browser_proof_required: true`, execute the `## Browser Proof Plan` before claiming completion. Record the exact route/state, viewport(s), runtime path, evidence command or narrowed no-command rationale, observations, artifact paths, privacy/safety note, result, and claim limit in the summary or a linked observation record. +Use `agent-browser` as the default live browser proof path. Record the planned route/state open, interactive snapshots/refs when interaction is part of the claim, changed-flow interaction, screenshots for planned viewport(s), and relevant console/network observations. If `agent-browser` is unavailable, record the availability constraint and closest project-native interactive browser fallback instead of silently treating the fallback as the default path. If the repo already has Playwright tests or a package script wrapping them, run the relevant targeted test as canonical repeatable regression evidence; keep browser runtime observation as complementary proof. Use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled, structured console, or multi-context verification. Do not install Playwright, Cypress, Cucumber, Storybook, browser MCP, CI, or visual-regression tooling by default. +Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes map onto existing evidence kinds, not new evidence kinds. Raw screenshots, traces, videos, DOM snapshots, and reports default to local-only and unsafe to publish unless explicitly sanitized. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication decisions require human evidence or explicit waiver; artifact count, source comments, AST/cAST findings, semantic search, and Semble-like retrieval are not proof. If evidence does not match the planned route/state, viewport, observation, artifact path/manual step, privacy note, result, and claim limit, record proof debt, waiver, deferment, or reduced claim language rather than satisfied proof. Use the failure-cause names in `distilled/references/proof-rules.md` when proof fails or is partial. ### Git Guidance diff --git a/distilled/workflows/plan.md b/distilled/workflows/plan.md index 47c708e5..cad10871 100644 --- a/distilled/workflows/plan.md +++ b/distilled/workflows/plan.md @@ -131,11 +131,20 @@ If the selected target is `brownfield-change`, do not require ROADMAP phase memb Also verify that `HANDOFF.md` is judgment-only context and does not contradict the operational status, scope, or next action in `CHANGE.md`. If any brownfield contract field is missing or contradictory, STOP and repair the brownfield contract before planning. - -For UI-sensitive work, include compact `ui_proof_slots` with `slot_id`, optional `requirement_id`, `claim`, `route_state`, fixed evidence kinds (`code`, `test`, `runtime`, `delivery`, `human`), `minimum_observations`, `expected_artifact_types`, `validation_command`, `environment`, `viewport`, `manual_acceptance_required`, and `claim_limit`; otherwise set `no_ui_proof_rationale`. -Do not create slots for backend-only, CLI-only, docs-only, or refactor-only work unless the plan claims a visible UI outcome. Evidence must later match claim, route/state, observation, artifact path, evidence kind, privacy metadata, result, and claim limit; local-only or unsafe artifacts cannot support public, publication, tracked, delivery, or release proof claims. Human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. -For live rendered UI proof, plan `agent-browser` as the default runtime evidence path and existing Playwright/package-script browser tests as the repeatable regression path when the repo already has them. If the runtime does not provide `agent-browser`, require the plan to state that availability constraint and name the closest project-native interactive browser fallback before narrowing the claim. The planner chooses the viewport set, but each slot must explain why the chosen viewport(s) are enough for the claim or narrow the claim limit; responsive claims need desktop/mobile or equivalent state coverage. Do not plan new browser infrastructure by default, and use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled, structured console, or multi-context verification. - + +For UI-sensitive work, set `browser_proof_required: true` and add a `## Browser Proof Plan` section. For backend-only, CLI-only, docs-only, or refactor-only work that claims no visible UI outcome, set `browser_proof_required: false` with a short `browser_proof_rationale`. + +The Browser Proof Plan is plain markdown, not a JSON slot schema. It must name: +- `Routes/states:` exact route(s), screen(s), or UI states to inspect +- `Viewports:` desktop and mobile for responsive or layout-sensitive claims, or a narrowed claim explaining why one viewport is enough +- `Runtime path:` `agent-browser` preferred; Playwright or another project-native browser fallback must explain the availability constraint +- `Evidence command:` a runnable command that can reproduce or validate the browser proof, or `No-command rationale:` when the runtime path is manual/interactive only and the claim is narrowed accordingly +- `Observations:` rendered DOM/behavior, interaction steps, and console/network observations when relevant +- `Artifacts:` screenshot/report/log paths if produced, with local-only vs publishable privacy/safety note +- `Claim limit:` the exact claim the observation can support + +Evidence must later match route/state, viewport, observation, artifact path, evidence kind, privacy/safety note, result, and claim limit. Human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. Existing Playwright/package-script browser tests remain canonical repeatable regression evidence when present. Do not plan new browser infrastructure by default; use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled, structured console, or multi-context verification. + Plan backward from success criteria. @@ -195,18 +204,8 @@ anti_regression_targets: - Existing session middleware behavior remains unchanged for already-supported routes. known_unknowns: - Exact copy wording for auth errors may still need product confirmation. -ui_proof_slots: [] -no_ui_proof_rationale: Not UI-sensitive; scoped work does not claim a visible UI outcome. -high_leverage_surfaces: [] -second_pass_required: false -closure_claim_limit: Do not claim phase completion until verification satisfies the evidence contract for the scoped truths. -parallelism_budget: - max_concurrent_plans: 1 - safe_parallelism: [] -leverage: - lost: Slightly more planning ceremony for this plan. - kept: Existing auth/session architecture and repo conventions. - gained: Explicit anti-drift boundaries and fail-closed escalation. +browser_proof_required: false +browser_proof_rationale: Not UI-sensitive; scoped work does not claim a visible UI outcome. must_haves: truths: - User can sign in with email and password. @@ -225,11 +224,9 @@ Schema rules: - `requirements` must not be empty - `files-modified` should list the files this plan is expected to touch - `must_haves` must trace back to roadmap success criteria -- `non_goals`, `hard_boundaries`, `escalation_triggers`, and `closure_claim_limit` must not be empty -- include `ui_proof_slots` for UI-sensitive work or `no_ui_proof_rationale` otherwise -- `leverage.lost`, `leverage.kept`, and `leverage.gained` must all be explicit -- `second_pass_required: true` if `high_leverage_surfaces` is non-empty -- `parallelism_budget.max_concurrent_plans` must stay `1` unless the plan proves disjoint write ownership +- `non_goals`, `hard_boundaries`, `escalation_triggers`, and `approval_gates` must be explicit +- include `browser_proof_required` and `browser_proof_rationale` +- if `browser_proof_required: true`, include a `## Browser Proof Plan` section with route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit @@ -326,16 +323,8 @@ anti_regression_targets: - Existing user route behavior outside the new list view remains unchanged. known_unknowns: - Final empty-state copy may still need product confirmation. -high_leverage_surfaces: [] -second_pass_required: false -closure_claim_limit: Do not claim phase completion until verification confirms the scoped truths with the required evidence. -parallelism_budget: - max_concurrent_plans: 1 - safe_parallelism: [] -leverage: - lost: Slightly more planning ceremony for stronger execution boundaries. - kept: Existing route/component conventions and repo-native workflow. - gained: Better anti-drift enforcement and more honest closure limits. +browser_proof_required: true +browser_proof_rationale: Visible route changes require rendered browser proof. must_haves: truths: - Users can view the list page. @@ -382,6 +371,15 @@ must_haves: ## Approval Gates - [Decision or side-effect boundaries that require explicit user approval] +## Browser Proof Plan +Routes/states: [Exact routes or UI states to inspect, or `N/A` when not required] +Viewports: [Desktop/mobile coverage or narrowed viewport claim] +Runtime path: [agent-browser preferred; explain fallback availability constraints] +Evidence command: [Runnable command, or use `No-command rationale:` for narrowed manual/interactive proof] +Observations: [Rendered behavior, interaction steps, console/network observations] +Artifacts: [Screenshot/report/log paths plus local-only or publishable safety note] +Claim limit: [Exact claim this proof can support] + checker: self | cross_runtime @@ -408,13 +406,8 @@ notes: [What the checker actually validated or why it was skipped] ## Success Criteria - [What must be true when this plan is complete] -## High-Leverage Review -- [Which high-leverage surfaces are touched and whether a second pass is required] - -## Leverage Review -- Lost: [What leverage this plan sacrifices] -- Kept: [What existing leverage or conventions it preserves] -- Gained: [What leverage this plan creates] +## Second-Pass Review +- [Shared/high-risk surfaces to re-check for contradictions, stale assumptions, and overclaims before closure] ## Notes [Gotchas, implementation notes, or explicit assumptions] @@ -505,12 +498,9 @@ After the planner produces a draft plan, an independent checker reviews it in fr 6. `must_have_quality` - success criteria are specific, observable, and reflected in tasks 7. `context_compliance` - locked decisions are honored and deferred ideas stay out of scope 8. `goal_achievement` - the plan, if executed perfectly, actually achieves the stated phase goal: goal addressed (tasks deliver the goal), success criteria reachable (each criterion traceable to a task verify output), and outcome observable (a human or automated check can confirm the goal was met) -9. `scope_boundaries` - anti-goals and hard boundaries are explicit, enforceable, and reflected in the task set -10. `anti_regression_capture` - anti-regression targets are named and mapped to verification -11. `escalation_integrity` - stop-and-challenge triggers and approval gates are present where side effects or ambiguity warrant them -12. `closure_honesty` - closure claim limit prevents the plan from overclaiming what verification can prove -13. `high_leverage_review` - high-leverage surfaces and second-pass obligations are recorded honestly -14. `approach_alignment` - when APPROACH.md exists, plans implement the chosen approaches, not alternatives. Blocker if plan contradicts an explicit user choice. Warning if plan drifts from recommendation without justification. When `workflow.discuss: true`, missing, proofless, agent-discretion-only, or invalid APPROACH.md is a blocker before a plan can be accepted. +9. `approach_alignment` - when APPROACH.md exists, plans implement the chosen approaches, not alternatives. Blocker if plan contradicts an explicit user choice. Warning if plan drifts from recommendation without justification. When `workflow.discuss: true`, missing, proofless, agent-discretion-only, or invalid APPROACH.md is a blocker before a plan can be accepted. + +The smaller dimension set still preserves the old failure coverage: scope boundaries are checked under `scope_sanity`, anti-regression and escalation under `context_compliance`, closure honesty and browser-proof specificity under `goal_achievement`, and second-pass review under the final shared-surface review before closure. ### Invoking the Checker 1. If `.work/config.json` has `workflow.planCheck: false`, skip the independent checker. Perform the planner self-check below and report `reduced_assurance`. This does not skip the earlier alignment-proof gate when `workflow.discuss: true`. 2. If plan checking is enabled, check if your runtime provides a `gsdd-plan-checker` agent. @@ -528,7 +518,7 @@ After the planner produces a draft plan, an independent checker reviews it in fr "summary": "One sentence overall assessment", "issues": [ { - "dimension": "requirement_coverage | task_completeness | dependency_correctness | key_link_completeness | scope_sanity | must_have_quality | context_compliance | goal_achievement | scope_boundaries | anti_regression_capture | escalation_integrity | closure_honesty | high_leverage_review | approach_alignment", + "dimension": "requirement_coverage | task_completeness | dependency_correctness | key_link_completeness | scope_sanity | must_have_quality | context_compliance | goal_achievement | approach_alignment", "severity": "blocker | warning", "description": "What is wrong", "plan": "01-PLAN", @@ -587,9 +577,9 @@ For each task: - [ ] `escalation_triggers` force stop-and-challenge instead of silent interpretation - [ ] `approval_gates` appear anywhere side effects or irreversible choices could happen - [ ] `anti_regression_targets` are concrete enough for verification to check later -- [ ] `closure_claim_limit` prevents the executor from claiming more than the evidence can support -- [ ] `leverage.lost`, `leverage.kept`, and `leverage.gained` are explicit and defensible -- [ ] If `high_leverage_surfaces` is non-empty, `second_pass_required` is true and named in the plan body +- [ ] Evidence contract and claim limits prevent the executor from claiming more than verification can support +- [ ] Browser-proof-required plans include route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit +- [ ] Shared or high-risk surfaces are named for a second-pass contradiction/staleness review ### Red Flags - A success criterion has no task covering it @@ -615,14 +605,14 @@ Planning is done when all of these are true: - [ ] Plan self-check passed - [ ] Success criteria from `ROADMAP.md`, or Done When criteria from `CHANGE.md`, are represented as must-haves - [ ] Goal-backward derivation from criteria to artifacts to key links to tasks is explicit -- [ ] Every plan has frontmatter with `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `ui_proof_slots` or `no_ui_proof_rationale`, `closure_claim_limit`, `parallelism_budget`, `leverage`, and `must_haves` +- [ ] Every plan has frontmatter with `phase`, `plan`, `type`, `wave`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `known_unknowns`, `browser_proof_required`, `browser_proof_rationale`, and `must_haves` - [ ] Every plan frontmatter records `runtime` and `assurance` - [ ] Every plan records checker outcome in a structured `` block - [ ] Every task has XML structure with `id`, `type`, `files`, `action`, `verify`, and `done` - [ ] Every task has at least one runnable verify command - [ ] Plan sizing stays within 2-5 tasks, preferring 2-3 - [ ] Locked decisions from `.work/SPEC.md` and APPROACH.md are honored -- [ ] Plan body includes explicit `## Anti-Goals`, `## Hard Boundaries`, `## Evidence Contract`, `## Common Pitfalls`, `## Stop-And-Challenge`, `## Approval Gates`, and `## Leverage Review` sections +- [ ] Plan body includes explicit `## Anti-Goals`, `## Hard Boundaries`, `## Evidence Contract`, `## Common Pitfalls`, `## Stop-And-Challenge`, `## Approval Gates`, and `## Second-Pass Review` sections; UI-sensitive plans also include `## Browser Proof Plan` - [ ] Any git guidance stays repo-native and follows `.work/config.json` diff --git a/distilled/workflows/quick.md b/distilled/workflows/quick.md index 28d6b851..10f1d3a2 100644 --- a/distilled/workflows/quick.md +++ b/distilled/workflows/quick.md @@ -118,11 +118,10 @@ Delegate to the planner role in quick mode. - No research phase, no ROADMAP requirements - Do NOT extract phase requirement IDs — there is no active phase - Derive must-haves directly from the task description -- If the quick task is UI-sensitive, include proportional `ui_proof_slots` with slot_id, claim, route_state, required_evidence_kinds, minimum_observations, expected_artifact_types, validation_command, environment, viewport, manual_acceptance_required, and claim_limit; otherwise include a short `no_ui_proof_rationale` -- UI proof slots must be matchable to exact observed evidence later: claim, route/state, observation, evidence kind, artifact path or manual step, privacy metadata, result, and claim limit. Discovery hints from source comments, AST/cAST, semantic search, or Semble-like retrieval do not satisfy proof. -- Observed artifact metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports are local-only/unsafe by default. Use `gsdd ui-proof validate ` or `gsdd health` when a bundle exists; add `--claim <...>` only for public, publication, tracked, delivery, or release proof use. +- If the quick task is UI-sensitive, include proportional `browser_proof_required: true`, a short `browser_proof_rationale`, and a plain Browser Proof Plan naming route/state, viewport, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit; otherwise set `browser_proof_required: false` with a short rationale. +- Browser proof must be matchable to exact observed evidence later: route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit. Discovery hints from source comments, AST/cAST, semantic search, or Semble-like retrieval do not satisfy proof. - For live rendered UI proof, default to `agent-browser` snapshots/refs, interactions, screenshots, and relevant console/network observations. If unavailable, state the availability constraint and closest project-native interactive browser fallback before narrowing the claim. Existing Playwright/package-script browser tests remain the canonical repeatable regression path when present. The viewport set is plan-owned, but under-specified viewport coverage is weak proof; explain the chosen viewport(s) or narrow the claim limit. -- Keep UI proof proportional: do not scaffold Playwright, Cypress, Cucumber, Storybook, CI, browser MCP, or visual-regression tooling by default +- Keep browser proof proportional: do not scaffold Playwright, Cypress, Cucumber, Storybook, CI, browser MCP, or visual-regression tooling by default. - Ignore Step 1 requirement extraction; use inline goal-backward planning only - Target minimal context usage @@ -162,8 +161,8 @@ Read `.work/config.json`. **Constraints:** - Check 5 dimensions by default: `requirement_coverage`, `task_completeness`, `dependency_correctness`, `scope_sanity`, `must_have_quality` -- If the quick plan contains `ui_proof_slots` or a rendered UI claim, also check `closure_honesty` so weak UI proof slots block execution -- Skip: `key_link_completeness`, `context_compliance`, `goal_achievement`, `approach_alignment` +- If the quick plan contains a rendered UI claim, check `goal_achievement` so weak browser proof blocks execution. +- Skip: `key_link_completeness`, `context_compliance`, and `approach_alignment`; skip `goal_achievement` only when the quick plan has no rendered UI claim - Maximum 1 revision cycle (if blockers found, send back to planner once, then accept result) - Blocker threshold: only block on `task_completeness` or `scope_sanity` violations - Warnings for other dimensions are noted but do not block @@ -269,8 +268,8 @@ Delegate to the executor role. - Skip the section of your role contract entirely - Do NOT update ROADMAP.md phase status or SPEC.md current state - Create summary at: `.work/quick/$NEXT_NUM-$SLUG/$NEXT_NUM-SUMMARY.md` -- If the quick plan defines `ui_proof_slots`, create or update `.work/quick/$NEXT_NUM-$SLUG/UI-PROOF.md` with fenced JSON containing required top-level fields: `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits` -- For live UI proof, record `agent-browser` in `evidence_inputs.tools_used` when used, the exact commands or manual ref-based steps, screenshot/report artifact paths, and any relevant console/network observations. If `agent-browser` was unavailable, record that availability constraint and fallback tool explicitly. If existing Playwright tests supplied regression evidence, record the package command and result separately from the `agent-browser` runtime observation. +- If the quick plan requires browser proof, create or update `.work/quick/$NEXT_NUM-$SLUG/UI-PROOF.md` as a plain observation record with route/state, viewport, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, privacy/safety note, result, and claim limit. +- For live browser proof, record `agent-browser` when used, the exact commands or manual ref-based steps, screenshot/report artifact paths, and any relevant console/network observations. If `agent-browser` was unavailable, record that availability constraint and fallback tool explicitly. If existing Playwright tests supplied regression evidence, record the package command and result separately from the runtime observation. - Human approval for visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, or privacy publication does not replace required `code`, `test`, `runtime`, or `delivery` evidence **Output:** `.work/quick/$NEXT_NUM-$SLUG/$NEXT_NUM-SUMMARY.md` diff --git a/distilled/workflows/verify.md b/distilled/workflows/verify.md index 3d05ad72..dcbad5ce 100644 --- a/distilled/workflows/verify.md +++ b/distilled/workflows/verify.md @@ -129,12 +129,12 @@ Rules: Note: this step does NOT replace levels 1–3. An artifact can satisfy the evidence-kind requirement and still fail Level 2 (substantive) or Level 3 (wired). Both checks must run. - -Before closure, direct `gsdd verify ` and this workflow must fail closed when the target phase has no matching PLAN.md or SUMMARY.md; report structured prerequisite blockers instead of treating missing artifacts as an empty success. Read UI proof declaration authority from the plan frontmatter contract only: body prose, fenced examples, stale sidecars, and markdown snippets do not declare UI proof intent. If frontmatter defines non-empty `ui_proof_slots`, compare planned UI proof against observed bundles before closure. Prefer `gsdd ui-proof compare [observed-bundle-json ...]` when planned slots are available as JSON or fenced JSON; otherwise perform the same field-by-field comparison and record reduced assurance if no deterministic command could run. If frontmatter records `ui_proof_slots: []`, it must also contain a nonblank `no_ui_proof_rationale`; otherwise verification blocks. If the plan records only `no_ui_proof_rationale`, verify the rationale instead of requiring a bundle, and treat stale planned/observed sidecars as warnings rather than proof or blockers. Each observed bundle must include top-level `proof_bundle_version`, `scope`, `route_state`, `environment`, `viewport`, `evidence_inputs`, `commands_or_manual_steps`, `observations`, `artifacts`, `privacy`, `result`, and `claim_limits`. -Classify each slot as exactly one of: `satisfied`, `partial`, `missing`, `waived`, `deferred`, or `not_applicable`. Deterministic comparison issues include `severity` and `fix_hint`; use those as the normal repair feedback loop before closing verification. Waiver/deferment narrows the claim; it is not proof. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes are artifact types or activities mapped onto existing evidence kinds, not new evidence kinds. Artifact count is never proof; each artifact must tie to the slot claim, route/state, observation, artifact path/link, privacy metadata, and claim limit. -For live UI runtime proof, expect `agent-browser` as the default captured tool unless the observed bundle explains a project-native equivalent or an availability constraint. Do not fail solely because another browser tool was used, but downgrade vague proof that lacks exact route/state, planned viewport coverage or rationale, interactive steps/refs where relevant, screenshot/report artifacts, or relevant console/network observations. Existing Playwright tests count as canonical repeatable regression evidence, not a replacement for scoped runtime evidence when the slot requires `runtime`. -Artifact privacy metadata must include `visibility`, `retention`, `sensitivity`, and `safe_to_publish`; raw screenshots, traces, videos, DOM snapshots, and reports default to local-only and unsafe unless sanitized. Run `gsdd ui-proof validate ` or treat `gsdd health` E10 as blocking; add `--claim <...>` when relying on the bundle for public, tracked, delivery, release, or publication proof. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver; human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. Source annotations, AST/cAST findings, semantic search, comments, and Semble-like retrieval are discovery hints only. - + +Before closure, direct `gsdd verify ` and this workflow must fail closed when the target phase has no matching PLAN.md or SUMMARY.md; report structured prerequisite blockers instead of treating missing artifacts as an empty success. Read browser-proof declaration authority from the plan frontmatter: `browser_proof_required` and `browser_proof_rationale`. Body prose and stale sidecars do not declare proof intent. If `browser_proof_required: false`, verify the rationale is nonblank and treat stale observation records as warnings rather than proof or blockers. If `browser_proof_required: true`, verify the plan contains a `## Browser Proof Plan` with route/state, viewport, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit. +For live UI runtime proof, expect `agent-browser` as the default captured tool unless the observation record explains a project-native equivalent or an availability constraint. Do not fail solely because another browser tool was used, but downgrade vague proof that lacks exact route/state, planned viewport coverage or rationale, interactive steps/refs where relevant, screenshot/report artifacts, relevant console/network observations, privacy/safety note, or a narrowed claim limit. Existing Playwright tests count as canonical repeatable regression evidence, not a replacement for scoped runtime evidence when browser proof requires runtime observation. +Waiver/deferment narrows the claim; it is not proof. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes are artifact types or activities mapped onto existing evidence kinds, not new evidence kinds. Artifact count is never proof; each artifact must tie to the route/state, observation, artifact path/link, privacy note, and claim limit. +Raw screenshots, traces, videos, DOM snapshots, and reports default to local-only and unsafe unless sanitized. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver; human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. Source annotations, AST/cAST findings, semantic search, comments, and Semble-like retrieval are discovery hints only. Use the failure-cause names in `distilled/references/proof-rules.md` when proof fails or is partial. + Check every artifact at three levels. A common failure mode is a file that exists but is still a stub. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index e0f18f36..54bae198 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -259,7 +259,7 @@ Other CLI commands that remain available outside the first-run path: | Command | Purpose | |---------|---------| | `gsdd find-phase [N]` | Show phase info as JSON (for agent consumption) | -| `gsdd verify ` | Run phase artifact and UI-proof closure checks for phase N; exits nonzero when verification is blocked | +| `gsdd verify ` | Run phase artifact and browser-proof closure checks for phase N; exits nonzero when verification is blocked | | `gsdd scaffold phase [name]` | Create a new phase plan file | ### Platform flags for `--tools` diff --git a/tests/gsdd.cross-runtime.test.cjs b/tests/gsdd.cross-runtime.test.cjs index 5878b299..ab3e7887 100644 --- a/tests/gsdd.cross-runtime.test.cjs +++ b/tests/gsdd.cross-runtime.test.cjs @@ -273,39 +273,50 @@ describe('S7 — Adapter Chain Validation (cross-runtime adapter generation comp 'must_have_quality', 'context_compliance', 'goal_achievement', + 'approach_alignment', + ]; + const RETIRED_PLAN_CHECK_DIMENSIONS = [ 'scope_boundaries', 'anti_regression_capture', 'escalation_integrity', 'closure_honesty', 'high_leverage_review', - 'approach_alignment', ]; - test('Claude plan-checker has all 14 dimensions', () => { + test('Claude plan-checker has all 9 dimensions', () => { const checkerPath = path.join(tmpDir, '.claude', 'agents', 'gsdd-plan-checker.md'); assert.ok(fs.existsSync(checkerPath), 'Claude checker must exist'); const content = fs.readFileSync(checkerPath, 'utf-8'); for (const dim of PLAN_CHECK_DIMENSIONS) { assert.ok(content.includes(dim), 'Claude checker must include dimension: ' + dim); } + for (const dim of RETIRED_PLAN_CHECK_DIMENSIONS) { + assert.ok(!content.includes(dim), 'Claude checker must not include retired dimension: ' + dim); + } }); - test('OpenCode plan-checker has all 14 dimensions', () => { + test('OpenCode plan-checker has all 9 dimensions', () => { const checkerPath = path.join(tmpDir, '.opencode', 'agents', 'gsdd-plan-checker.md'); assert.ok(fs.existsSync(checkerPath), 'OpenCode checker must exist'); const content = fs.readFileSync(checkerPath, 'utf-8'); for (const dim of PLAN_CHECK_DIMENSIONS) { assert.ok(content.includes(dim), 'OpenCode checker must include dimension: ' + dim); } + for (const dim of RETIRED_PLAN_CHECK_DIMENSIONS) { + assert.ok(!content.includes(dim), 'OpenCode checker must not include retired dimension: ' + dim); + } }); - test('Codex plan-checker has all 14 dimensions', () => { + test('Codex plan-checker has all 9 dimensions', () => { const checkerPath = path.join(tmpDir, '.codex', 'agents', 'gsdd-plan-checker.toml'); assert.ok(fs.existsSync(checkerPath), 'Codex checker must exist'); const content = fs.readFileSync(checkerPath, 'utf-8'); for (const dim of PLAN_CHECK_DIMENSIONS) { assert.ok(content.includes(dim), 'Codex checker must include dimension: ' + dim); } + for (const dim of RETIRED_PLAN_CHECK_DIMENSIONS) { + assert.ok(!content.includes(dim), 'Codex checker must not include retired dimension: ' + dim); + } }); test('all 3 checkers have dimension parity (same count)', () => { diff --git a/tests/gsdd.guards.test.cjs b/tests/gsdd.guards.test.cjs index 699459b5..225eaa8f 100644 --- a/tests/gsdd.guards.test.cjs +++ b/tests/gsdd.guards.test.cjs @@ -2244,8 +2244,8 @@ describe('G28 - Spec Quality Check and Contradiction Detection', () => { 'plan.md phase_contract_gate must require explicit stop/replan conditions in the roadmap phase contract. FIX: Add stop/replan requirement to .'); }); - test('plan.md anti-drift contract requires boundary and closure fields', () => { - for (const token of ['non_goals', 'hard_boundaries', 'escalation_triggers', 'approval_gates', 'closure_claim_limit', 'leverage']) { + test('plan.md anti-drift contract requires boundary and proof fields', () => { + for (const token of ['non_goals', 'hard_boundaries', 'escalation_triggers', 'approval_gates', 'anti_regression_targets', 'known_unknowns', 'browser_proof_required', 'browser_proof_rationale']) { assert.match(planWorkflow, new RegExp(token), `plan.md must include anti-drift contract field "${token}". FIX: Add ${token} to the plan schema and plan structure.`); } @@ -2253,7 +2253,7 @@ describe('G28 - Spec Quality Check and Contradiction Detection', () => { test('planner role uses the same anti-drift plan schema as plan.md', () => { const plannerRole = fs.readFileSync(path.join(ROOT, 'agents', 'planner.md'), 'utf-8'); - for (const token of ['non_goals', 'hard_boundaries', 'escalation_triggers', 'approval_gates', 'anti_regression_targets', 'known_unknowns', 'high_leverage_surfaces', 'second_pass_required', 'closure_claim_limit', 'parallelism_budget', 'leverage']) { + for (const token of ['non_goals', 'hard_boundaries', 'escalation_triggers', 'approval_gates', 'anti_regression_targets', 'known_unknowns', 'browser_proof_required', 'browser_proof_rationale']) { assert.match(plannerRole, new RegExp(token), `planner role must include anti-drift contract field "${token}". FIX: Keep planner.md schema aligned with plan.md.`); } @@ -3477,93 +3477,77 @@ describe('G53 - Deliberate Subagent Contract', () => { }); }); -describe('G55 - UI Proof Contract', () => { +describe('G55 - Browser Proof Contract', () => { const template = fs.readFileSync(path.join(ROOT, 'distilled', 'templates', 'ui-proof.md'), 'utf-8'); const planContent = fs.readFileSync(path.join(ROOT, 'distilled', 'workflows', 'plan.md'), 'utf-8'); const executeContent = fs.readFileSync(path.join(ROOT, 'distilled', 'workflows', 'execute.md'), 'utf-8'); const quickContent = fs.readFileSync(path.join(ROOT, 'distilled', 'workflows', 'quick.md'), 'utf-8'); const verifyContent = fs.readFileSync(path.join(ROOT, 'distilled', 'workflows', 'verify.md'), 'utf-8'); + const proofRules = fs.readFileSync(path.join(ROOT, 'distilled', 'references', 'proof-rules.md'), 'utf-8'); + const observationRecord = fs.readFileSync(path.join(ROOT, 'distilled', 'references', 'observation-record.md'), 'utf-8'); const plannerRole = fs.readFileSync(path.join(ROOT, 'agents', 'planner.md'), 'utf-8'); const executorRole = fs.readFileSync(path.join(ROOT, 'agents', 'executor.md'), 'utf-8'); const verifierRole = fs.readFileSync(path.join(ROOT, 'agents', 'verifier.md'), 'utf-8'); const planChecker = fs.readFileSync(path.join(ROOT, 'distilled', 'templates', 'delegates', 'plan-checker.md'), 'utf-8'); - function parseObservedBundleExample() { - const match = template.match(/```json\s*\n([\s\S]*?)\n```/); - assert.ok(match, 'ui-proof.md must include a fenced JSON observed-bundle example.'); - return JSON.parse(match[1]); - } - - test('template preserves planned slot and observed bundle fields', () => { + test('template preserves browser-proof declaration and observation fields', () => { for (const token of [ - 'ui_proof_slots', - 'no_ui_proof_rationale', - 'claim', - 'route_state', - 'required_evidence_kinds', - 'minimum_observations', - 'expected_artifact_types', - 'validation_command', - 'environment', - 'viewport', - 'manual_acceptance_required', - 'claim_limit', - 'requirement_ids', - 'slot_ids', - 'evidence_inputs', - 'commands_or_manual_steps', - 'observations', - 'artifacts', - 'privacy', - 'result', - 'claim_status', - 'claim_limits', + 'browser_proof_required', + 'browser_proof_rationale', + 'Browser Proof Plan', + 'Routes/states', + 'Viewports', + 'Runtime path', + 'Evidence command', + 'No-command rationale', + 'Observations', + 'Artifacts', + 'Claim limit', + 'Browser Proof Observation', + 'Stale after', ]) { - assert.match(template, new RegExp(token), `ui-proof.md must include ${token}. FIX: Restore the locked UI proof schema field.`); + assert.match(template, new RegExp(token), `ui-proof.md must include ${token}. FIX: Restore the browser-proof contract field.`); } - assert.match(template, /```json/, 'ui-proof.md must use fenced JSON for observed proof bundle validation. FIX: Keep JSON canonical.'); + assert.doesNotMatch(template, /ui_proof_slots|gsdd ui-proof validate|gsdd ui-proof compare|proof_bundle_version|claim_limits/, + 'ui-proof.md must not reintroduce the retired slot/bundle validator contract.'); }); - test('template defines comparison statuses and unchanged evidence kinds', () => { - assert.match(template, /code`, `test`, `runtime`, `delivery`, and `human`/, 'ui-proof.md must preserve the five stable evidence kinds.'); - for (const status of ['satisfied', 'partial', 'missing', 'waived', 'deferred', 'not_applicable']) { - assert.match(template, new RegExp('`' + status + '`'), `ui-proof.md must define comparison status ${status}.`); + test('template and proof rules preserve evidence kinds and failure causes', () => { + assert.match(template, /code`, `test`,\s*`runtime`, `delivery`, and `human`/, 'ui-proof.md must preserve the five stable evidence kinds.'); + for (const cause of ['product_bug', 'missing_infra', 'flaky_harness', 'ambiguous_spec']) { + assert.match(proofRules, new RegExp('`' + cause + '`'), `proof-rules.md must define failure cause ${cause}.`); } - assert.match(template, /not new evidence kinds/i, 'UI artifacts must not become new evidence kinds.'); - }); - - test('workflow and role sources preserve UI proof planning execution and verification contracts', () => { - assert.match(planContent, //, 'plan.md must include UI proof planning contract.'); - assert.match(planContent, /ui_proof_slots[\s\S]*no_ui_proof_rationale/, 'plan.md must require slots or no-UI-proof rationale.'); - assert.match(plannerRole, //, 'planner role must include UI proof planning guidance.'); - assert.match(executeContent, /UI Proof Execution/, 'execute.md must include UI proof execution guidance.'); - assert.match(executorRole, /UI Proof Execution/, 'executor role must include UI proof execution guidance.'); - assert.match(quickContent, /ui_proof_slots/, 'quick.md must preserve proportional UI proof slots.'); - assert.match(quickContent, /ui_proof_slots[\s\S]*closure_honesty/, 'quick.md must run closure_honesty checking for UI-sensitive quick plans.'); - assert.match(verifyContent, //, 'verify.md must include planned-vs-observed UI proof comparison.'); - assert.match(verifyContent, /gsdd ui-proof compare /, 'verify.md must prefer the deterministic product-facing UI proof comparison command.'); - assert.match(verifyContent, /plan frontmatter contract only/i, 'verify.md must make plan frontmatter the only UI proof declaration authority.'); - assert.match(verifyContent, /body prose, fenced examples, stale sidecars/i, 'verify.md must reject prose/examples/sidecars as UI proof declaration authority.'); - assert.match(verifyContent, /ui_proof_slots: \[\][\s\S]{0,140}no_ui_proof_rationale/i, 'verify.md must require a no-UI rationale for empty UI proof slots.'); + assert.match(template, /not new evidence kinds/i, 'Browser artifacts must not become new evidence kinds.'); + assert.match(observationRecord, /failure-cause names in proof-rules\.md/i, + 'observation-record.md must point failed or partial proof to proof-rules.md.'); + }); + + test('workflow and role sources preserve browser-proof planning execution and verification contracts', () => { + assert.match(planContent, //, 'plan.md must include browser-proof planning contract.'); + assert.match(planContent, /browser_proof_required[\s\S]*browser_proof_rationale/, 'plan.md must require browser-proof declaration fields.'); + assert.match(planContent, /Evidence command[\s\S]*No-command rationale/, 'plan.md must require runnable proof command or narrowed no-command rationale.'); + assert.match(plannerRole, //, 'planner role must include browser-proof planning guidance.'); + assert.match(executeContent, /Browser Proof Execution/, 'execute.md must include browser-proof execution guidance.'); + assert.match(executorRole, /Browser Proof Execution/, 'executor role must include browser-proof execution guidance.'); + assert.match(quickContent, /browser_proof_required/, 'quick.md must preserve proportional browser-proof planning.'); + assert.match(quickContent, /rendered UI claim[\s\S]*goal_achievement/, 'quick.md must check weak browser proof through goal_achievement.'); + assert.match(verifyContent, //, 'verify.md must include browser-proof comparison.'); + assert.match(verifyContent, /browser_proof_required: false[\s\S]*rationale is nonblank/i, 'verify.md must require rationale when browser proof is not required.'); + assert.match(verifyContent, /body prose and stale sidecars do not declare proof intent/i, 'verify.md must reject prose/sidecars as browser-proof declaration authority.'); assert.match(verifyContent, /no matching PLAN\.md or SUMMARY\.md/i, 'verify.md must fail closed on missing phase prerequisites.'); - assert.match(verifierRole, /For UI proof slots, fail closed/i, 'verifier role must fail closed on weak UI proof.'); - }); - - test('template documents product-facing UI proof comparison command', () => { - assert.match(template, /gsdd ui-proof validate /, 'ui-proof.md must document metadata validation.'); - assert.match(template, /gsdd ui-proof compare /, 'ui-proof.md must document planned-vs-observed comparison.'); + assert.match(verifierRole, /For browser proof, fail closed/i, 'verifier role must fail closed on weak browser proof.'); }); test('contract preserves agent-browser default without provider-locking validation', () => { const combined = [template, planContent, executeContent, quickContent, verifyContent, plannerRole, executorRole, verifierRole, planChecker].join('\n'); assert.match(combined, /agent-browser/i, - 'UI proof contract must name agent-browser as the default live UI proof path.'); + 'Browser proof contract must name agent-browser as the default live browser proof path.'); assert.match(combined, /availability constraint/i, - 'UI proof contract must require explicit fallback documentation when agent-browser is unavailable.'); + 'Browser proof contract must require explicit fallback documentation when agent-browser is unavailable.'); assert.match(combined, /Playwright[\s\S]{0,160}(canonical|repeatable).*regression/i, - 'UI proof contract must keep existing Playwright tests as canonical repeatable regression evidence.'); + 'Browser proof contract must keep existing Playwright tests as repeatable regression evidence.'); assert.match(combined, /Playwright scripting[\s\S]{0,220}(JS-disabled|structured console|multi-context)/i, - 'UI proof contract must reserve Playwright scripting for capabilities agent-browser cannot cover cleanly.'); + 'Browser proof contract must reserve Playwright scripting for capabilities agent-browser cannot cover cleanly.'); for (const [surfaceName, source] of [ ['ui-proof template', template], @@ -3576,7 +3560,7 @@ describe('G55 - UI Proof Contract', () => { ['verifier role', verifierRole] ]) { assert.match(source, /agent-browser/i, - `${surfaceName} must preserve agent-browser as the default live UI proof path.`); + `${surfaceName} must preserve agent-browser as the default live browser proof path.`); assert.match(source, /Playwright[\s\S]{0,220}(canonical|repeatable).*regression/i, `${surfaceName} must preserve Playwright/package-script browser tests as repeatable regression evidence.`); } @@ -3595,53 +3579,19 @@ describe('G55 - UI Proof Contract', () => { assert.match(source, /availability constraint/i, `${surfaceName} must require explicit availability-constraint fallback wording.`); } - - assert.match(template, /does not inspect raw screenshot[\s\S]{0,180}does not require any specific browser provider/i, - 'ui-proof.md must keep deterministic validation provider-agnostic.'); - }); - - test('observed bundle example keeps runtime artifacts traceable', () => { - const bundle = parseObservedBundleExample(); - const declaredRefs = new Set(bundle.artifacts.map((artifact) => artifact.path || artifact.url)); - const observationRefs = new Set(bundle.observations.flatMap((observation) => observation.artifact_refs)); - const commandText = bundle.commands_or_manual_steps - .map((step) => step.command || step.manual_step || '') - .join('\n'); - - assert.deepStrictEqual(bundle.evidence_inputs.tools_used, ['playwright', 'agent-browser'], - 'ui-proof.md example must use concise tool identifiers.'); - for (const tool of bundle.evidence_inputs.tools_used) { - assert.match(tool, /^[a-z0-9][a-z0-9_.:-]*$/, - `UI proof tool id must be a concise machine-friendly identifier: ${tool}`); - } - - assert.doesNotMatch(commandText, /\.planning\/quick\/001-example/, - 'ui-proof.md example commands must use a work-item placeholder instead of a hardcoded quick-task directory.'); - - const screenshotCommand = commandText.match(/agent-browser screenshot\s+(\S+)/); - assert.ok(screenshotCommand, 'ui-proof.md example must capture a screenshot through agent-browser.'); - const screenshotRef = screenshotCommand[1]; - assert.ok(declaredRefs.has(screenshotRef), - 'agent-browser screenshot path in the example must be listed in artifacts[].'); - assert.ok(observationRefs.has(screenshotRef), - 'agent-browser screenshot path in the example must be linked from observations[].artifact_refs.'); - - for (const ref of observationRefs) { - assert.ok(declaredRefs.has(ref), - `ui-proof.md example observation references undeclared artifact: ${ref}`); - } }); - test('plan-checker rejects weak UI proof slots before execution', () => { + test('plan-checker rejects weak browser proof before execution', () => { for (const phrase of [ - /specific route\/state/i, - /viewport rationale/i, - /minimum observations/i, - /expected artifact types/i, - /runnable validation/i, + /route\/state/i, + /viewport/i, + /evidence command/i, + /no-command rationale/i, + /artifacts\/privacy notes/i, + /claim limit/i, /under-specified viewport coverage/i, ]) { - assert.match(planChecker, phrase, `plan-checker must reject weak UI proof slots missing ${phrase}.`); + assert.match(planChecker, phrase, `plan-checker must reject weak browser proof missing ${phrase}.`); } }); @@ -3654,15 +3604,13 @@ describe('G55 - UI Proof Contract', () => { /subjective polish\/layout quality/i, /privacy publication/i, /does not replace required `code`, `test`, `runtime`, or `delivery` evidence/i, - /agent-only `looks good` closure/i, + /agent-only "looks good" closure/i, /artifact-count proof|Artifact count is never proof/i, - /visibility[\s\S]*retention[\s\S]*sensitivity[\s\S]*safe_to_publish/, - /observation privacy fields|observation privacy metadata|privacy metadata/i, - /`passed`, `failed`, `partial`, `waived`, `deferred`, or `not_applicable`/, - /local-only or `safe_to_publish: false` artifacts|local-only or unsafe artifacts/i, + /privacy\/safety note|safe to publish/i, + /local-only or unsafe artifacts|local-only and unsafe/i, /Do not install Playwright, Cypress, Cucumber, Storybook, browser MCP, CI, or visual-regression tooling by default|do not scaffold Playwright, Cypress, Cucumber, Storybook, CI, browser MCP, or visual-regression tooling by default/i, ]) { - assert.match(combined, phrase, `UI proof contract must preserve guardrail ${phrase}.`); + assert.match(combined, phrase, `Browser proof contract must preserve guardrail ${phrase}.`); } }); }); diff --git a/tests/gsdd.invariants.test.cjs b/tests/gsdd.invariants.test.cjs index ed679dcb..45fd39f3 100644 --- a/tests/gsdd.invariants.test.cjs +++ b/tests/gsdd.invariants.test.cjs @@ -929,12 +929,14 @@ describe('I7 — Plan-Checker Dimension Integrity', () => { 'must_have_quality', 'context_compliance', 'goal_achievement', + 'approach_alignment', + ]; + const RETIRED_PLAN_CHECKER_DIMENSIONS = [ 'scope_boundaries', 'anti_regression_capture', 'escalation_integrity', 'closure_honesty', 'high_leverage_review', - 'approach_alignment', ]; const planCheckerContent = fs.readFileSync( @@ -947,7 +949,7 @@ describe('I7 — Plan-Checker Dimension Integrity', () => { path.join(AGENTS_DIR, 'planner.md'), 'utf-8' ); - describe('plan-checker.md documents all 14 dimensions', () => { + describe('plan-checker.md documents all 9 dimensions', () => { for (const dim of PLAN_CHECKER_DIMENSIONS) { test(`plan-checker.md documents dimension: ${dim}`, () => { assert.ok( @@ -956,9 +958,17 @@ describe('I7 — Plan-Checker Dimension Integrity', () => { ); }); } + for (const dim of RETIRED_PLAN_CHECKER_DIMENSIONS) { + test(`plan-checker.md omits retired dimension: ${dim}`, () => { + assert.ok( + !planCheckerContent.includes(dim), + `plan-checker.md must not document retired dimension ${dim}` + ); + }); + } }); - describe('plan.md documents all 14 checker dimensions', () => { + describe('plan.md documents all 9 checker dimensions', () => { for (const dim of PLAN_CHECKER_DIMENSIONS) { test(`plan.md documents checker dimension: ${dim}`, () => { assert.ok( @@ -967,9 +977,17 @@ describe('I7 — Plan-Checker Dimension Integrity', () => { ); }); } + for (const dim of RETIRED_PLAN_CHECKER_DIMENSIONS) { + test(`plan.md omits retired checker dimension: ${dim}`, () => { + assert.ok( + !planWorkflowContent.includes(dim), + `plan.md must not document retired checker dimension ${dim}` + ); + }); + } }); - describe('planner.md references all 14 checker dimensions', () => { + describe('planner.md references all 9 checker dimensions', () => { // planner.md uses natural language labels (e.g. "requirement coverage") not underscored JSON keys const PLANNER_DIMENSION_LABELS = [ 'requirement coverage', @@ -980,12 +998,14 @@ describe('I7 — Plan-Checker Dimension Integrity', () => { 'must-have quality', 'context compliance', 'goal achievement', + 'approach alignment', + ]; + const RETIRED_PLANNER_DIMENSION_LABELS = [ 'scope boundaries', 'anti-regression capture', 'escalation integrity', 'closure honesty', 'high-leverage review', - 'approach alignment', ]; for (const label of PLANNER_DIMENSION_LABELS) { @@ -996,6 +1016,14 @@ describe('I7 — Plan-Checker Dimension Integrity', () => { ); }); } + for (const label of RETIRED_PLANNER_DIMENSION_LABELS) { + test(`planner.md omits retired checker label: ${label}`, () => { + assert.ok( + !plannerContent.includes(label), + `planner.md must not reference retired checker label "${label}" in its internal_quality_gate` + ); + }); + } }); describe('plan-checker.md JSON output schema', () => { @@ -1397,7 +1425,7 @@ describe('G1 — Cross-Document Schema Consistency', () => { // PLAN.md frontmatter must appear in both planner.md and plan.md describe('PLAN.md frontmatter in planner.md AND plan.md', () => { - const PLAN_FIELDS = ['phase:', 'depends_on:', 'files-modified:', 'autonomous:', 'requirements:', 'must_haves:']; + const PLAN_FIELDS = ['phase:', 'depends_on:', 'files-modified:', 'autonomous:', 'requirements:', 'browser_proof_required:', 'browser_proof_rationale:', 'must_haves:']; for (const field of PLAN_FIELDS) { test(`${field} appears in both planner.md and plan.md`, () => { assert.ok(plannerContent.includes(field), diff --git a/tests/gsdd.plan.adapters.test.cjs b/tests/gsdd.plan.adapters.test.cjs index 6d22295b..f35958bc 100644 --- a/tests/gsdd.plan.adapters.test.cjs +++ b/tests/gsdd.plan.adapters.test.cjs @@ -331,7 +331,7 @@ describe('specialized plan adapter surfaces', () => { assert.doesNotMatch(content, /^sandbox_mode = "read-only"/m, 'approach-explorer must NOT be read-only (needs write access unlike checker)'); }); - test('all native plan surfaces contain the same 14 dimension names', async () => { + test('all native plan surfaces contain the same 9 dimension names', async () => { const allDimensions = [ 'requirement_coverage', 'task_completeness', @@ -341,12 +341,14 @@ describe('specialized plan adapter surfaces', () => { 'must_have_quality', 'context_compliance', 'goal_achievement', + 'approach_alignment', + ]; + const retiredDimensions = [ 'scope_boundaries', 'anti_regression_capture', 'escalation_integrity', 'closure_honesty', 'high_leverage_review', - 'approach_alignment', ]; // Init claude @@ -404,6 +406,9 @@ describe('specialized plan adapter surfaces', () => { for (const dim of allDimensions) { assert.ok(content.includes(dim), `${label} must contain dimension: ${dim}`); } + for (const dim of retiredDimensions) { + assert.ok(!content.includes(dim), `${label} must not contain retired dimension: ${dim}`); + } } }); diff --git a/tests/gsdd.scenarios.test.cjs b/tests/gsdd.scenarios.test.cjs index be3c4aeb..5c16239d 100644 --- a/tests/gsdd.scenarios.test.cjs +++ b/tests/gsdd.scenarios.test.cjs @@ -515,7 +515,7 @@ describe('S4 — Native Runtime Chain (Claude + Codex adapter completeness)', () ); }); - test('checker content includes all 14 plan-check dimension names', () => { + test('checker content includes all 9 plan-check dimension names', () => { const content = fs.readFileSync( path.join(tmpDir, '.claude', 'agents', 'gsdd-plan-checker.md'), 'utf-8' @@ -529,16 +529,21 @@ describe('S4 — Native Runtime Chain (Claude + Codex adapter completeness)', () 'must_have_quality', 'context_compliance', 'goal_achievement', + 'approach_alignment', + ]; + const retiredDimensions = [ 'scope_boundaries', 'anti_regression_capture', 'escalation_integrity', 'closure_honesty', 'high_leverage_review', - 'approach_alignment', ]; for (const dim of dimensions) { assert.ok(content.includes(dim), `checker must include dimension: ${dim}`); } + for (const dim of retiredDimensions) { + assert.ok(!content.includes(dim), `checker must not include retired dimension: ${dim}`); + } }); test('native approach-explorer exists at .claude/agents/gsdd-approach-explorer.md', () => { @@ -566,7 +571,7 @@ describe('S4 — Native Runtime Chain (Claude + Codex adapter completeness)', () ); }); - test('plan-checker delegate has same 14 dimensions as native checker', () => { + test('plan-checker delegate has same 9 dimensions as native checker', () => { const delegate = fs.readFileSync( path.join(tmpDir, '.work', 'templates', 'delegates', 'plan-checker.md'), 'utf-8' @@ -580,16 +585,21 @@ describe('S4 — Native Runtime Chain (Claude + Codex adapter completeness)', () 'must_have_quality', 'context_compliance', 'goal_achievement', + 'approach_alignment', + ]; + const retiredDimensions = [ 'scope_boundaries', 'anti_regression_capture', 'escalation_integrity', 'closure_honesty', 'high_leverage_review', - 'approach_alignment', ]; for (const dim of dimensions) { assert.ok(delegate.includes(dim), `delegate must include dimension: ${dim}`); } + for (const dim of retiredDimensions) { + assert.ok(!delegate.includes(dim), `delegate must not include retired dimension: ${dim}`); + } }); test('OpenCode generated plan agents preserve alignment proof gate wording', async () => { @@ -706,7 +716,7 @@ describe('S4 — Native Runtime Chain (Claude + Codex adapter completeness)', () ); }); - test('Codex checker contains all 14 dimension names', () => { + test('Codex checker contains all 9 dimension names', () => { const content = fs.readFileSync( path.join(tmpDir, '.codex', 'agents', 'gsdd-plan-checker.toml'), 'utf-8' @@ -720,16 +730,21 @@ describe('S4 — Native Runtime Chain (Claude + Codex adapter completeness)', () 'must_have_quality', 'context_compliance', 'goal_achievement', + 'approach_alignment', + ]; + const retiredDimensions = [ 'scope_boundaries', 'anti_regression_capture', 'escalation_integrity', 'closure_honesty', 'high_leverage_review', - 'approach_alignment', ]; for (const dim of dimensions) { assert.ok(content.includes(dim), `Codex checker must include dimension: ${dim}`); } + for (const dim of retiredDimensions) { + assert.ok(!content.includes(dim), `Codex checker must not include retired dimension: ${dim}`); + } }); test('native approach-explorer exists at .codex/agents/gsdd-approach-explorer.toml', () => { diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 91d07083..a79180ae 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1692,6 +1692,10 @@ describe('verify command nested phase plans', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'phases', '34-identity-and-story-lock', '01-PLAN.md'), [ + '---', + 'browser_proof_required: false', + 'browser_proof_rationale: Artifact-path fixture; no rendered UI behavior is claimed.', + '---', '', ' ', ' - MODIFY: src/example.js', @@ -1721,6 +1725,10 @@ describe('verify command nested phase plans', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'phases', '34-identity-and-story-lock', '01-PLAN.md'), [ + '---', + 'browser_proof_required: false', + 'browser_proof_rationale: Artifact-path fixture; no rendered UI behavior is claimed.', + '---', '', ' ', ' - RENAME: src/old.js -> src/new.js', @@ -1777,7 +1785,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-summary-missing'); fs.mkdirSync(phaseDir, { recursive: true }); - fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '---\nui_proof_slots: []\nno_ui_proof_rationale: CLI-only work.\n---\n# Phase 1 Plan\n'); + fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '---\nbrowser_proof_required: false\nbrowser_proof_rationale: CLI-only work.\n---\n# Phase 1 Plan\n'); const result = await runCliAsMain(tmpDir, ['verify', '1']); assert.strictEqual(result.exitCode, 1, result.output); @@ -1793,7 +1801,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-helper-verify'); fs.mkdirSync(phaseDir, { recursive: true }); - fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '---\nui_proof_slots: []\nno_ui_proof_rationale: CLI-only helper verification.\n---\n# Phase 1 Plan\n'); + fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '---\nbrowser_proof_required: false\nbrowser_proof_rationale: CLI-only helper verification.\n---\n# Phase 1 Plan\n'); fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); const helperPath = path.join(tmpDir, '.planning', 'bin', 'gsdd.mjs'); @@ -1811,7 +1819,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-builder-verify'); fs.mkdirSync(phaseDir, { recursive: true }); - fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '---\nui_proof_slots: []\nno_ui_proof_rationale: CLI-only builder verification.\n---\n# Phase 1 Plan\n'); + fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '---\nbrowser_proof_required: false\nbrowser_proof_rationale: CLI-only builder verification.\n---\n# Phase 1 Plan\n'); fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); const phase = await importPhaseModule(); @@ -1825,11 +1833,381 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { assert.deepStrictEqual(built.result.blocked_on, output.blocked_on); }); + test('phase verify blocks required browser proof when the plan section is missing', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-missing'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.blocked_on.includes('browser_proof')); + assert.strictEqual(output.browser_proof_status.satisfied, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => blocker.code === 'missing_browser_proof_plan')); + }); + + test('phase verify blocks missing browser proof declaration', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-declaration'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '# Phase 1 Plan\n'); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.blocked_on.includes('browser_proof')); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'missing_browser_proof_declaration' + ))); + }); + + test('phase verify blocks retired ui-proof declarations instead of passing legacy plans', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-retired-browser-proof'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'ui_proof_slots: []', + 'no_ui_proof_rationale: none', + '---', + '# Phase 1 Plan', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'retired_browser_proof_contract' + && blocker.message.includes('ui_proof_slots') + ))); + }); + + test('phase verify normalizes scalar comments and rejects placeholder rationale values', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-scalars'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: "false" # CLI-only', + 'browser_proof_rationale: "" # empty placeholder', + '---', + '# Phase 1 Plan', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'missing_browser_proof_rationale' + ))); + assert.ok(!output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'invalid_browser_proof_required' + ))); + }); + + test('phase verify blocks incomplete browser proof sections without evidence command or no-command rationale', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-incomplete'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-incomplete/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'incomplete_browser_proof_plan' + && blocker.message.includes('Evidence command or No-command rationale') + ))); + }); + + test('phase verify blocks placeholder browser proof plan fields', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-placeholders'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: [Exact routes or UI states to inspect]', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence command: [Runnable command]', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-placeholders/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'incomplete_browser_proof_plan' + && blocker.message.includes('Routes/states') + ))); + }); + + test('phase verify blocks required browser proof when no observation is recorded', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-observation-missing'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-observation-missing/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\nNo browser proof yet.\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'missing_browser_proof_observation' + ))); + }); + + test('phase verify passes required browser proof with a complete observation record', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-observed'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: "true" # visible dashboard work', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-observed/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + '## Browser Proof Observation', + '', + '- Flow: /dashboard after loading current account.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence command: npm run test:e2e -- --grep dashboard', + '- Observed: dashboard widgets rendered without console errors.', + '- Artifacts:', + ' - .planning/phases/01-browser-proof-observed/artifacts/dashboard.png - local-only', + '- Result: passed', + '- Claim limit: dashboard render proof only.', + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 0, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, true); + assert.strictEqual(output.browser_proof_status.satisfied, true); + }); + + test('phase verify blocks multi-plan browser proof when observations do not identify each plan', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-multi'); + fs.mkdirSync(phaseDir, { recursive: true }); + const planContent = [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-multi/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n'); + fs.writeFileSync(path.join(phaseDir, '01-1-PLAN.md'), planContent); + fs.writeFileSync(path.join(phaseDir, '01-2-PLAN.md'), planContent); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + '## Browser Proof Observation', + '', + '- Flow: /dashboard after loading current account.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence command: npm run test:e2e -- --grep dashboard', + '- Observed: dashboard widgets rendered without console errors.', + '- Artifacts: .planning/phases/01-browser-proof-multi/artifacts/dashboard.png - local-only', + '- Result: passed', + '- Claim limit: dashboard render proof only.', + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'unmatched_browser_proof_observation' + && blocker.path.endsWith('01-1-PLAN.md') + ))); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'unmatched_browser_proof_observation' + && blocker.path.endsWith('01-2-PLAN.md') + ))); + }); + + test('phase verify passes multi-plan browser proof when observations reference each required plan', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-multi-observed'); + fs.mkdirSync(phaseDir, { recursive: true }); + const planContent = [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-multi-observed/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n'); + fs.writeFileSync(path.join(phaseDir, '01-1-PLAN.md'), planContent); + fs.writeFileSync(path.join(phaseDir, '01-2-PLAN.md'), planContent); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + '## Browser Proof Observation', + '', + '- Plan: 01-browser-proof-multi-observed/01-1-PLAN.md', + '- Flow: /dashboard account A.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence command: npm run test:e2e -- --grep dashboard-a', + '- Observed: dashboard widgets rendered without console errors.', + '- Artifacts: .planning/phases/01-browser-proof-multi-observed/artifacts/dashboard-a.png - local-only', + '- Result: passed', + '- Claim limit: dashboard account A render proof only.', + '', + '## Browser Proof Observation', + '', + '- Plan: 01-browser-proof-multi-observed/01-2-PLAN.md', + '- Flow: /dashboard account B.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence command: npm run test:e2e -- --grep dashboard-b', + '- Observed: dashboard widgets rendered without console errors.', + '- Artifacts: .planning/phases/01-browser-proof-multi-observed/artifacts/dashboard-b.png - local-only', + '- Result: passed', + '- Claim limit: dashboard account B render proof only.', + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 0, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, true); + assert.strictEqual(output.browser_proof_status.satisfied, true); + }); + test('phase verify blocks when planned file artifacts are unsatisfied', async () => { await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-artifact-proof'); fs.mkdirSync(phaseDir, { recursive: true }); - fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '\n \n - CREATE: src/missing.js\n \n\n'); + fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), '---\nbrowser_proof_required: false\nbrowser_proof_rationale: File-artifact verification only.\n---\n\n \n - CREATE: src/missing.js\n \n\n'); fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); const result = await runCliAsMain(tmpDir, ['verify', '1']); From 829f0c83223554d91d67d8eafebb335b398ff4d1 Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 8 Jul 2026 23:05:37 +0200 Subject: [PATCH 2/2] fix: harden browser proof verification --- agents/executor.md | 2 +- agents/planner.md | 6 +- bin/lib/phase.mjs | 425 +++++++++++++++--- distilled/DESIGN.md | 14 +- distilled/references/observation-record.md | 45 +- distilled/templates/delegates/plan-checker.md | 2 +- distilled/templates/ui-proof.md | 8 +- distilled/workflows/execute.md | 2 +- distilled/workflows/plan.md | 6 +- distilled/workflows/quick.md | 2 +- distilled/workflows/verify.md | 4 +- docs/USER-GUIDE.md | 8 + tests/gsdd.init.test.cjs | 35 ++ tests/phase.test.cjs | 421 ++++++++++++++++- 14 files changed, 884 insertions(+), 96 deletions(-) diff --git a/agents/executor.md b/agents/executor.md index a212f57f..d9811985 100644 --- a/agents/executor.md +++ b/agents/executor.md @@ -211,7 +211,7 @@ Before reporting a task complete: - A task is not complete because code was written. It is complete when the intended verification path actually passes. ### Browser Proof Execution -If the plan sets `browser_proof_required: true`, execute the Browser Proof Plan and record observed proof against the exact route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit before claiming task completion. +If the plan sets `browser_proof_required: true`, execute the Browser Proof Plan and record observed proof against the exact `Plan:` artifact path, route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit before claiming task completion. Use `agent-browser` as the default live UI proof path: - open the planned route/state diff --git a/agents/planner.md b/agents/planner.md index 09110bc3..070dc805 100644 --- a/agents/planner.md +++ b/agents/planner.md @@ -154,9 +154,9 @@ Any plan containing `checkpoint:*` must set `autonomous: false`. -For UI-sensitive work, set `browser_proof_required: true` and include a `## Browser Proof Plan` that can later be matched exactly to route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit. Use only the stable evidence kinds `code`, `test`, `runtime`, `delivery`, and `human`. +For UI-sensitive work, set `browser_proof_required: true` and include a `## Browser Proof Plan` that can later be matched exactly to route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit. Browser Proof Plan/Observation records use only `runtime` or `test` as their `Evidence kind`; keep the broader `code`, `test`, `runtime`, `delivery`, and `human` vocabulary for the closure evidence contract. -The Browser Proof Plan must name route/state, viewport coverage, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit. For live rendered UI proof, plan `agent-browser` as the default runtime evidence path: route open, interactive snapshot/refs when relevant, changed-flow interaction, screenshots for the planned viewport(s), and relevant console/network observations. If `agent-browser` is unavailable in the runtime, require an explicit availability constraint and the closest project-native interactive browser fallback before narrowing the claim. Existing Playwright/package-script browser tests remain the canonical repeatable regression path when present; do not scaffold new browser infrastructure by default. Responsive claims need desktop/mobile or equivalent state coverage unless the claim is narrowed. +The Browser Proof Plan must name route/state, viewport coverage, runtime path, evidence kind, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit. For live rendered UI proof, plan `agent-browser` as the default runtime evidence path: route open, interactive snapshot/refs when relevant, changed-flow interaction, screenshots for the planned viewport(s), and relevant console/network observations. If `agent-browser` is unavailable in the runtime, require an explicit availability constraint and the closest project-native interactive browser fallback before narrowing the claim. Existing Playwright/package-script browser tests remain the canonical repeatable regression path when present; do not scaffold new browser infrastructure by default. Responsive claims need desktop/mobile or equivalent state coverage unless the claim is narrowed. Do not let source annotations, AST/cAST findings, semantic search, comments, or Semble-like retrieval satisfy browser proof; they are discovery hints only. Human acceptance can narrow or waive a claim and record proof debt, but it must not turn missing or mismatched non-human evidence into satisfied proof. @@ -194,7 +194,7 @@ Write one or more `PLAN.md` files to the phase directory. Keep the current GSDD schema exactly: - frontmatter keys: `phase`, `plan`, `type`, `wave`, `runtime`, `assurance`, `depends_on`, `files-modified`, `autonomous`, `requirements`, `non_goals`, `hard_boundaries`, `escalation_triggers`, `approval_gates`, `anti_regression_targets`, `known_unknowns`, `browser_proof_required`, `browser_proof_rationale`, `must_haves` - typed tasks with `files`, `action`, `verify`, and `done` -- if `browser_proof_required: true`, the plan body must include `## Browser Proof Plan` with route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit +- if `browser_proof_required: true`, the plan body must include `## Browser Proof Plan` with route/state, viewport, runtime path, evidence kind, evidence command or no-command rationale, observations, artifacts, and claim limit Typed frontmatter example: diff --git a/bin/lib/phase.mjs b/bin/lib/phase.mjs index 9603901b..89975be7 100644 --- a/bin/lib/phase.mjs +++ b/bin/lib/phase.mjs @@ -3,8 +3,8 @@ // IMPORTANT: No module-scope process.cwd() — ESM caching means sub-modules // evaluate once, so CWD must be computed inside function bodies. -import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'; -import { basename, dirname, isAbsolute, join, relative } from 'path'; +import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, realpathSync } from 'fs'; +import { basename, dirname, isAbsolute, join, relative, resolve } from 'path'; import { output } from './cli-utils.mjs'; import { resolveWorkspaceContext } from './workspace-root.mjs'; @@ -184,6 +184,29 @@ function hasTopLevelKey(frontmatter, key) { return new RegExp(`^${escapedKey}:\\s*.*$`, 'm').test(String(frontmatter || '')); } +function readTopLevelBlock(frontmatter, key) { + const escapedKey = String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const lines = String(frontmatter || '').replace(/\r\n/g, '\n').split('\n'); + const startIndex = lines.findIndex((line) => new RegExp(`^${escapedKey}:\\s*(.*)$`).test(line)); + if (startIndex === -1) return null; + + const scalar = normalizeScalarValue(lines[startIndex].replace(new RegExp(`^${escapedKey}:\\s*`), '')); + const nested = []; + for (const line of lines.slice(startIndex + 1)) { + if (/^\S[^:]*:\s*/.test(line)) break; + if (line.trim()) nested.push(line.trim()); + } + return { scalar, nested }; +} + +function legacyUiProofSlotsState(frontmatter) { + const block = readTopLevelBlock(frontmatter, 'ui_proof_slots'); + if (!block) return null; + if (/^\[\s*\]$/.test(block.scalar)) return 'empty'; + if (isMeaningfulFieldValue(block.scalar)) return 'present'; + return block.nested.length > 0 ? 'present' : 'empty'; +} + function stripInlineComment(value) { const text = String(value || ''); let quote = null; @@ -279,10 +302,63 @@ function nonEmptyField(section, label) { return isMeaningfulFieldValue(value); } +function firstFieldValue(section, labels) { + for (const label of labels) { + const value = sectionFieldValue(section, label); + if (isMeaningfulFieldValue(value)) return value; + } + return ''; +} + +function evidenceKindValues(section) { + const value = firstFieldValue(section, ['Evidence kind', 'Evidence kinds']); + return value + .split(/[,/;|]|\band\b/i) + .map((part) => normalizeScalarValue(part).toLowerCase()) + .filter(Boolean); +} + +function hasSupportedBrowserProofEvidenceKind(section) { + return evidenceKindValues(section).some((kind) => kind === 'runtime' || kind === 'test'); +} + +function hasPassingBrowserProofResult(section) { + const value = firstFieldValue(section, ['Result']).toLowerCase(); + if (!isMeaningfulFieldValue(value)) return false; + if (/\b(not\s+passed|fail(?:ed|ing)?|partial|partly|blocked|waived|deferred|missing|not[_ -]?applicable|unknown)\b/i.test(value)) { + return false; + } + return /\b(pass(?:ed|ing)?|satisfied|success(?:ful)?)\b/i.test(value); +} + +function isBoundedBrowserProofClaim(section) { + const value = firstFieldValue(section, ['Claim limit']).toLowerCase(); + if (!isMeaningfulFieldValue(value)) return false; + if (/\b(no limit|unlimited|entire app|whole app|full app|complete app|everything works|all works|all flows|all ui|all screens|works everywhere)\b/i.test(value)) { + return false; + } + return true; +} + +function hasPrivacySafetyNote(section) { + if (firstFieldValue(section, ['Privacy/safety', 'Privacy note', 'Safety note', 'Safe to publish'])) return true; + const artifacts = firstFieldValue(section, ['Artifacts']); + return /\b(local[-_ ]?only|safe to publish|not safe to publish|publishable|private|unsafe|sanitized)\b/i.test(artifacts); +} + function browserProofFixHint(code) { if (code === 'retired_browser_proof_contract') { return 'Replace retired ui_proof_slots/no_ui_proof_rationale fields with browser_proof_required and browser_proof_rationale.'; } + if (code === 'legacy_browser_proof_contract') { + return 'Update PLAN.md frontmatter to browser_proof_required: false and browser_proof_rationale when you next touch this phase.'; + } + if (code === 'legacy_browser_proof_slots_require_migration') { + return 'Migrate non-empty ui_proof_slots to browser_proof_required: true plus a Browser Proof Plan before verifying this phase.'; + } + if (code === 'conflicting_browser_proof_contract') { + return 'Use either the new browser_proof_* frontmatter or the legacy ui_proof_* frontmatter, not both.'; + } if (code === 'missing_browser_proof_declaration') { return 'Add browser_proof_required: true|false and browser_proof_rationale to PLAN.md frontmatter.'; } @@ -301,27 +377,122 @@ function browserProofFixHint(code) { if (code === 'incomplete_browser_proof_observation') { return 'Complete the Browser Proof Observation fields or narrow the proof claim.'; } + if (code === 'failed_browser_proof_observation') { + return 'Record a passing browser-proof observation, or narrow the claim and leave verification blocked.'; + } + if (code === 'unsupported_browser_proof_evidence_kind') { + return 'Use Evidence kind: runtime or Evidence kind: test for browser proof, or narrow the claim.'; + } + if (code === 'overbroad_browser_proof_claim') { + return 'Narrow Claim limit to the exact route, state, viewport, and behavior actually observed.'; + } + if (code === 'invalid_browser_proof_observation_link') { + return 'Link browser-proof observation records as repo-local regular files inside this workspace.'; + } if (code === 'unmatched_browser_proof_observation') { - return 'When multiple required browser-proof plans exist, add a Plan field to each Browser Proof Observation that names the matching PLAN.md artifact.'; + return 'Add a Plan field to each Browser Proof Observation that names the exact required PLAN.md artifact.'; } return 'Complete the Browser Proof Plan fields or narrow the proof claim.'; } function evaluateBrowserProofContract(planContent, planPath) { const frontmatter = extractFrontmatter(planContent); - const requiredRaw = readTopLevelScalar(frontmatter, 'browser_proof_required'); - const rationale = readTopLevelScalar(frontmatter, 'browser_proof_rationale'); - const declarationPresent = requiredRaw !== null || rationale !== null; + let requiredRaw = readTopLevelScalar(frontmatter, 'browser_proof_required'); + let rationale = readTopLevelScalar(frontmatter, 'browser_proof_rationale'); + let declarationPresent = requiredRaw !== null || rationale !== null; + const legacySlots = legacyUiProofSlotsState(frontmatter); + const legacyRationale = readTopLevelScalar(frontmatter, 'no_ui_proof_rationale'); + const legacyNoUiCompatible = !declarationPresent + && legacySlots === 'empty' + && isMeaningfulFieldValue(legacyRationale); const retiredKeys = ['ui_proof_slots', 'no_ui_proof_rationale', 'proof_bundle_version', 'claim_limits'] .filter((key) => hasTopLevelKey(frontmatter, key)); const blockers = []; + const warnings = []; + + if (declarationPresent && retiredKeys.length > 0) { + blockers.push({ + code: 'conflicting_browser_proof_contract', + severity: 'blocker', + path: planPath, + message: `PLAN.md mixes new browser-proof field(s) with retired field(s): ${retiredKeys.join(', ')}.`, + fix_hint: browserProofFixHint('conflicting_browser_proof_contract'), + }); + return { + path: planPath, + declaration_present: declarationPresent, + required: null, + rationale, + legacy_compatible: false, + satisfied: false, + warnings, + blockers, + }; + } + + if (!declarationPresent && legacySlots === 'present') { + blockers.push({ + code: 'legacy_browser_proof_slots_require_migration', + severity: 'blocker', + path: planPath, + message: 'PLAN.md uses legacy non-empty ui_proof_slots; migrate them to browser_proof_required plus a Browser Proof Plan before verification.', + fix_hint: browserProofFixHint('legacy_browser_proof_slots_require_migration'), + }); + return { + path: planPath, + declaration_present: false, + required: null, + rationale: legacyRationale, + legacy_compatible: false, + satisfied: false, + warnings, + blockers, + }; + } + + if (!declarationPresent && legacySlots === 'empty' && !isMeaningfulFieldValue(legacyRationale)) { + blockers.push({ + code: 'missing_browser_proof_rationale', + severity: 'blocker', + path: planPath, + message: 'Legacy ui_proof_slots: [] requires a meaningful no_ui_proof_rationale, or migration to browser_proof_required/browser_proof_rationale.', + fix_hint: browserProofFixHint('missing_browser_proof_rationale'), + }); + return { + path: planPath, + declaration_present: false, + required: null, + rationale: legacyRationale, + legacy_compatible: false, + satisfied: false, + warnings, + blockers, + }; + } - if (retiredKeys.length > 0) { + if (legacyNoUiCompatible) { + requiredRaw = 'false'; + rationale = legacyRationale; + declarationPresent = true; + warnings.push({ + code: 'legacy_browser_proof_contract', + severity: 'warning', + path: planPath, + message: 'PLAN.md uses legacy no-UI proof frontmatter; treating it as browser_proof_required: false for compatibility.', + fix_hint: browserProofFixHint('legacy_browser_proof_contract'), + }); + } + + const blockingRetiredKeys = legacyNoUiCompatible + ? retiredKeys.filter((key) => key !== 'ui_proof_slots' && key !== 'no_ui_proof_rationale') + : retiredKeys; + + if (blockingRetiredKeys.length > 0) { blockers.push({ code: 'retired_browser_proof_contract', severity: 'blocker', path: planPath, - message: `PLAN.md uses retired browser-proof field(s): ${retiredKeys.join(', ')}.`, + message: `PLAN.md uses retired browser-proof field(s): ${blockingRetiredKeys.join(', ')}.`, fix_hint: browserProofFixHint('retired_browser_proof_contract'), }); } @@ -339,7 +510,9 @@ function evaluateBrowserProofContract(planContent, planPath) { declaration_present: false, required: null, rationale: null, + legacy_compatible: false, satisfied: false, + warnings, blockers, }; } @@ -389,6 +562,15 @@ function evaluateBrowserProofContract(planContent, planPath) { if (!hasEvidenceCommand && !hasNoCommandRationale) { missingFields.push('Evidence command or No-command rationale'); } + if (!hasSupportedBrowserProofEvidenceKind(section)) { + missingFields.push('Evidence kind'); + } + if (!hasPrivacySafetyNote(section)) { + missingFields.push('Privacy/safety'); + } + if (!isBoundedBrowserProofClaim(section)) { + missingFields.push('bounded Claim limit'); + } if (missingFields.length > 0) { blockers.push({ code: 'incomplete_browser_proof_plan', @@ -406,7 +588,9 @@ function evaluateBrowserProofContract(planContent, planPath) { declaration_present: declarationPresent, required, rationale, + legacy_compatible: legacyNoUiCompatible, satisfied: blockers.length === 0, + warnings, blockers, }; } @@ -416,48 +600,154 @@ function sanitizeLinkedObservationPath(value) { const markdownLink = text.match(/^\[[^\]]+\]\(([^)]+)\)$/); if (markdownLink) text = markdownLink[1].trim(); text = text.replace(/^`|`$/g, '').trim(); - if (/^[a-z]+:\/\//i.test(text)) return ''; return text; } +function isInsidePath(parent, child) { + const relativePath = relative(resolve(parent), resolve(child)); + return relativePath === '' || (!!relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath)); +} + function readLinkedObservationRecords(summaryContent, summaryFullPath, workspaceRoot) { const records = []; + const blockers = []; const matcher = /^\s*(?:[-*]\s*)?(?:Browser Proof Observation|Browser proof observation|Observation record|Browser proof record):\s*(.+)$/gim; let match; while ((match = matcher.exec(String(summaryContent || '')))) { const linkedPath = sanitizeLinkedObservationPath(match[1]); if (!linkedPath) continue; - const candidates = isAbsolute(linkedPath) - ? [linkedPath] - : [join(dirname(summaryFullPath), linkedPath), join(workspaceRoot, linkedPath)]; + if (/^[a-z]+:\/\//i.test(linkedPath)) { + blockers.push({ + code: 'invalid_browser_proof_observation_link', + severity: 'blocker', + path: linkedPath, + message: 'Browser Proof Observation link must be a repo-local relative file path, not a URL.', + fix_hint: browserProofFixHint('invalid_browser_proof_observation_link'), + }); + continue; + } + if (isAbsolute(linkedPath)) { + blockers.push({ + code: 'invalid_browser_proof_observation_link', + severity: 'blocker', + path: linkedPath, + message: 'Browser Proof Observation link must be a repo-local relative file path.', + fix_hint: browserProofFixHint('invalid_browser_proof_observation_link'), + }); + continue; + } + + const candidates = [resolve(dirname(summaryFullPath), linkedPath), resolve(workspaceRoot, linkedPath)] + .filter((candidate, index, list) => list.indexOf(candidate) === index); const existingPath = candidates.find((candidate) => existsSync(candidate)); - if (existingPath) records.push(readFileSync(existingPath, 'utf-8')); + if (!existingPath) { + blockers.push({ + code: 'invalid_browser_proof_observation_link', + severity: 'blocker', + path: linkedPath, + message: 'Browser Proof Observation link does not resolve to an existing file in this workspace.', + fix_hint: browserProofFixHint('invalid_browser_proof_observation_link'), + }); + continue; + } + try { + const realWorkspaceRoot = realpathSync(workspaceRoot); + const realExistingPath = realpathSync(existingPath); + if (!isInsidePath(realWorkspaceRoot, realExistingPath)) { + blockers.push({ + code: 'invalid_browser_proof_observation_link', + severity: 'blocker', + path: linkedPath, + message: 'Browser Proof Observation link resolves outside this workspace.', + fix_hint: browserProofFixHint('invalid_browser_proof_observation_link'), + }); + continue; + } + const stat = statSync(existingPath); + if (!stat.isFile()) { + blockers.push({ + code: 'invalid_browser_proof_observation_link', + severity: 'blocker', + path: linkedPath, + message: 'Browser Proof Observation link must resolve to a regular file.', + fix_hint: browserProofFixHint('invalid_browser_proof_observation_link'), + }); + continue; + } + records.push(readFileSync(existingPath, 'utf-8')); + } catch (error) { + blockers.push({ + code: 'invalid_browser_proof_observation_link', + severity: 'blocker', + path: linkedPath, + message: `Browser Proof Observation link could not be read: ${error.code || error.message}.`, + fix_hint: browserProofFixHint('invalid_browser_proof_observation_link'), + }); + } } - return records; + return { records, blockers }; } function findBrowserProofObservationSections(summaryContent, summaryFullPath, workspaceRoot) { - return [ - ...extractMarkdownSections(summaryContent, 'Browser Proof Observation'), - ...readLinkedObservationRecords(summaryContent, summaryFullPath, workspaceRoot) - .flatMap((record) => extractMarkdownSections(record, 'Browser Proof Observation')), - ].filter(Boolean); + const linked = readLinkedObservationRecords(summaryContent, summaryFullPath, workspaceRoot); + return { + sections: [ + ...extractMarkdownSections(summaryContent, 'Browser Proof Observation'), + ...linked.records + .flatMap((record) => extractMarkdownSections(record, 'Browser Proof Observation')), + ].filter(Boolean), + blockers: linked.blockers, + }; } -function isCompleteBrowserProofObservation(section) { +function browserProofObservationIssues(section) { const requiredFieldSets = [ ['Flow', 'Routes/states'], ['Viewports'], ['Runtime path'], + ['Evidence kind', 'Evidence kinds'], ['Evidence command', 'No-command rationale'], ['Observed', 'Observations'], ['Artifacts'], ['Result'], ['Claim limit'], ]; - return requiredFieldSets.every((labels) => ( - labels.some((label) => nonEmptyField(section, label)) - )); + const missingFields = requiredFieldSets + .filter((labels) => !labels.some((label) => nonEmptyField(section, label))) + .map((labels) => labels.join(' or ')); + if (!hasPrivacySafetyNote(section)) { + missingFields.push('Privacy/safety'); + } + const issues = missingFields.length > 0 + ? [{ + code: 'incomplete_browser_proof_observation', + missingFields, + message: `Browser Proof Observation is missing required observed-proof field(s): ${missingFields.join(', ')}.`, + }] + : []; + if (!hasSupportedBrowserProofEvidenceKind(section)) { + issues.push({ + code: 'unsupported_browser_proof_evidence_kind', + message: 'Browser Proof Observation evidence kind must include runtime or test.', + }); + } + if (!hasPassingBrowserProofResult(section)) { + issues.push({ + code: 'failed_browser_proof_observation', + message: 'Browser Proof Observation result is not an explicit pass.', + }); + } + if (!isBoundedBrowserProofClaim(section)) { + issues.push({ + code: 'overbroad_browser_proof_claim', + message: 'Browser Proof Observation claim limit is missing or too broad.', + }); + } + return issues; +} + +function isCompleteBrowserProofObservation(section) { + return browserProofObservationIssues(section).length === 0; } function normalizeArtifactReference(value) { @@ -469,13 +759,12 @@ function observationReferencesPlan(section, planPath) { || sectionFieldValue(section, 'Plan artifact') || sectionFieldValue(section, 'PLAN'); if (!isMeaningfulFieldValue(planReference)) return false; - const normalizedReference = normalizeArtifactReference(planReference); + const normalizedReferences = normalizeArtifactReference(planReference) + .split(/[\n,;]+/) + .map((reference) => reference.trim().replace(/^\.\//, '')) + .filter(Boolean); const normalizedPlanPath = normalizeArtifactReference(planPath); - const normalizedPlanName = normalizeArtifactReference(basename(planPath)); - return normalizedReference === normalizedPlanPath - || normalizedReference === normalizedPlanName - || normalizedReference.includes(normalizedPlanPath) - || normalizedReference.includes(normalizedPlanName); + return normalizedReferences.some((reference) => reference === normalizedPlanPath); } function evaluateBrowserProofObservation(browserProofPlans, matchingSummaries, phasesDir, workspaceRoot) { @@ -484,6 +773,7 @@ function evaluateBrowserProofObservation(browserProofPlans, matchingSummaries, p return { satisfied: true, sections: [], + warnings: [], blockers: [], }; } @@ -491,17 +781,28 @@ function evaluateBrowserProofObservation(browserProofPlans, matchingSummaries, p const sections = matchingSummaries.flatMap((summaryPath) => { const summaryFullPath = join(phasesDir, summaryPath); if (!existsSync(summaryFullPath)) return []; - return findBrowserProofObservationSections( + const found = findBrowserProofObservationSections( readFileSync(summaryFullPath, 'utf-8'), summaryFullPath, workspaceRoot ); + return found.sections.map((section) => ({ section, summaryPath })); + }); + const linkBlockers = matchingSummaries.flatMap((summaryPath) => { + const summaryFullPath = join(phasesDir, summaryPath); + if (!existsSync(summaryFullPath)) return []; + return findBrowserProofObservationSections( + readFileSync(summaryFullPath, 'utf-8'), + summaryFullPath, + workspaceRoot + ).blockers.map((blocker) => ({ ...blocker, path: `${summaryPath}: ${blocker.path}` })); }); - if (sections.length === 0) { + if (sections.length === 0 && linkBlockers.length === 0) { return { satisfied: false, sections, + warnings: [], blockers: [{ code: 'missing_browser_proof_observation', severity: 'blocker', @@ -512,50 +813,50 @@ function evaluateBrowserProofObservation(browserProofPlans, matchingSummaries, p }; } - const completeSections = sections.filter((section) => isCompleteBrowserProofObservation(section)); + const issueBlockers = sections.flatMap(({ section, summaryPath }) => ( + browserProofObservationIssues(section).map((issue) => ({ + code: issue.code, + severity: 'blocker', + path: summaryPath, + message: issue.message, + fix_hint: browserProofFixHint(issue.code), + })) + )); + const completeSections = sections.filter(({ section }) => isCompleteBrowserProofObservation(section)); if (completeSections.length === 0) { return { satisfied: false, - sections, - blockers: [{ - code: 'incomplete_browser_proof_observation', - severity: 'blocker', - path: matchingSummaries.join(', '), - message: 'Browser Proof Observation is missing required observed-proof fields.', - fix_hint: browserProofFixHint('incomplete_browser_proof_observation'), - }], - }; - } - - if (requiredPlans.length === 1) { - return { - satisfied: true, - sections, - blockers: [], + sections: sections.map(({ section }) => section), + warnings: [], + blockers: [...linkBlockers, ...issueBlockers], }; } const unmatchedPlans = requiredPlans.filter((plan) => ( - !completeSections.some((section) => observationReferencesPlan(section, plan.path)) + !completeSections.some(({ section }) => observationReferencesPlan(section, plan.path)) )); - if (unmatchedPlans.length === 0) { + const unmatchedBlockers = unmatchedPlans.map((plan) => ({ + code: 'unmatched_browser_proof_observation', + severity: 'blocker', + path: plan.path, + message: `No complete Browser Proof Observation references required plan ${plan.path}.`, + fix_hint: browserProofFixHint('unmatched_browser_proof_observation'), + })); + const blockers = [...linkBlockers, ...issueBlockers, ...unmatchedBlockers]; + if (blockers.length === 0) { return { satisfied: true, - sections, + sections: sections.map(({ section }) => section), + warnings: [], blockers: [], }; } return { satisfied: false, - sections, - blockers: unmatchedPlans.map((plan) => ({ - code: 'unmatched_browser_proof_observation', - severity: 'blocker', - path: plan.path, - message: `No complete Browser Proof Observation references required plan ${plan.path}.`, - fix_hint: browserProofFixHint('unmatched_browser_proof_observation'), - })), + sections: sections.map(({ section }) => section), + warnings: [], + blockers, }; } @@ -804,6 +1105,7 @@ export function buildPhaseVerificationReport(...args) { required: null, rationale: null, satisfied: true, + warnings: [], blockers: [], }; }); @@ -817,6 +1119,10 @@ export function buildPhaseVerificationReport(...args) { ...browserProofPlans.flatMap((plan) => plan.blockers), ...browserProofObservationStatus.blockers, ]; + const browserProofWarnings = [ + ...browserProofPlans.flatMap((plan) => plan.warnings || []), + ...(browserProofObservationStatus.warnings || []), + ]; const artifactStatus = evaluatePlanArtifacts(artifacts); const legacyVerified = matchingPlans.length > 0 && matchingSummaries.length > 0; const blockedOn = [ @@ -841,6 +1147,7 @@ export function buildPhaseVerificationReport(...args) { satisfied: browserProofBlockers.length === 0, plans: browserProofPlans, observations: browserProofObservationStatus, + warnings: browserProofWarnings, blockers: browserProofBlockers, }, verified: closureVerified, diff --git a/distilled/DESIGN.md b/distilled/DESIGN.md index 55b0acbe..38b418b5 100644 --- a/distilled/DESIGN.md +++ b/distilled/DESIGN.md @@ -2828,7 +2828,7 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru ## D62 - Repo-Native Browser Proof Contract -**Decision (2026-04-28; revised 2026-05-09; revised 2026-07-08):** UI-sensitive work should carry a compact browser-proof declaration in plan frontmatter and, when executed, a plain markdown observation record that references artifacts by path or link while preserving the existing closure evidence kinds: `code`, `test`, `runtime`, `delivery`, and `human`. For live rendered UI proof, `agent-browser` is the default runtime evidence path for consumers, while existing Playwright tests remain the canonical repeatable browser-regression path when present. Direct phase verification treats `browser_proof_required` and `browser_proof_rationale` as the declaration authority and fails closed on missing phase prerequisites, invalid browser-proof frontmatter, a missing `## Browser Proof Plan` when proof is required, or a required browser-proof plan that omits route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit. +**Decision (2026-04-28; revised 2026-05-09; revised 2026-07-08):** UI-sensitive work should carry a compact browser-proof declaration in plan frontmatter and, when executed, a plain markdown observation record that references artifacts by path or link while preserving the existing closure evidence kinds: `code`, `test`, `runtime`, `delivery`, and `human`. For live rendered UI proof, `agent-browser` is the default runtime evidence path for consumers, while existing Playwright tests remain the canonical repeatable browser-regression path when present. Direct phase verification treats `browser_proof_required` and `browser_proof_rationale` as the declaration authority and fails closed on missing phase prerequisites, invalid browser-proof frontmatter, a missing `## Browser Proof Plan` when proof is required, or a required browser-proof plan that omits route/state, viewport, runtime path, evidence kind, evidence command or no-command rationale, observations, artifacts with privacy/safety posture, and claim limit. **Context:** - Browser proof targets the recurring failure mode where agents claim a UI works or looks good without rendered proof, matched observations, or explicit human judgment. @@ -2840,19 +2840,19 @@ Posture compatibility is part of that closeout contract: `repo_closeout` and `ru - Planning must classify UI-sensitive work with `browser_proof_required: true|false` and a nonblank `browser_proof_rationale`. - Direct phase verification must read browser-proof declaration authority from plan frontmatter only. Body prose and stale sidecars are not declaration authority. - Direct phase verification must fail nonzero with structured blockers when no matching plan or summary exists, when browser-proof frontmatter is invalid, or when a required Browser Proof Plan is missing or incomplete. -- When browser proof is not required and the rationale is explicit, stale browser-proof observations are warning-level cleanup signals, not proof and not blockers. -- Browser Proof Plan sections record route/state, viewport coverage, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, privacy/safety note, and claim limit. -- Observation records record the actual route/state, viewport, runtime path, command/manual step, rendered observations, artifacts, result, privacy/safety note, stale-after trigger, and claim limit. -- Browser Proof Plan sections must be tight enough for the plan checker to reject vague proof: specific route/state, viewport coverage or narrowed claim, runtime path, rendered observations, artifacts/privacy notes, evidence command or no-command rationale, and claim limit. +- When browser proof is not required and the rationale is explicit, stale browser-proof observations are warning-level cleanup signals, not proof and not blockers. Legacy `ui_proof_slots: []` with a meaningful `no_ui_proof_rationale` is treated as a compatibility warning rather than a blocker; legacy non-empty `ui_proof_slots` still require migration because required rendered proof cannot be inferred losslessly. +- Browser Proof Plan sections record route/state, viewport coverage, runtime path, evidence kind, evidence command or narrowed no-command rationale, observations, artifacts, privacy/safety note, and claim limit. +- Observation records record the exact plan artifact, actual route/state, viewport, runtime path, evidence kind, command/manual step, rendered observations, artifacts, result, privacy/safety note, stale-after trigger, and claim limit. +- Browser Proof Plan sections must be tight enough for the plan checker to reject vague proof: specific route/state, viewport coverage or narrowed claim, runtime path, supported evidence kind, rendered observations, artifacts/privacy notes, evidence command or no-command rationale, and claim limit. - The planner chooses viewport coverage, but responsive or layout-sensitive claims require desktop/mobile or equivalent state coverage unless the claim is explicitly narrowed. - Execution defaults to `agent-browser` for live UI runtime proof: open the route/state, capture interactive snapshots/refs where relevant, exercise the changed flow, capture screenshots for planned viewport(s), and record relevant console/network observations. - Existing Playwright tests or package scripts remain the canonical repeatable browser-regression evidence when present. Playwright scripting is reserved for checks `agent-browser` cannot cover cleanly, such as JS-disabled behavior, structured console listeners, or multi-context testing. -- UI correctness claims fail closed unless rendered proof is matched exactly to route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit, or an explicit waiver/deferment narrows the claim. +- Direct verification fails closed unless each required browser-proof plan has a repo-local, parser-compatible, explicitly passing observation that names the exact plan artifact and carries supported evidence semantics, privacy/safety posture, and bounded claim scope. The workflow verifier still owns substantive judgment about whether the recorded observation actually supports the route/state, viewport, artifact, and claim. - Human acceptance may close a narrowed claim and record proof debt, but it must not convert missing or mismatched non-human evidence into satisfied proof. - Screenshots, traces, videos, reports, accessibility scans, Gherkin, and visual diffs are artifact types or activities mapped onto the five existing evidence kinds, not new evidence kinds. - Source annotations, AST/cAST findings, semantic search hits, comments, and Semble-like retrieval may discover proof obligations, but they are discovery hints only and do not satisfy browser proof. - Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver, and human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. -- The current contract is validator-light by design: direct phase verification checks declaration shape and required Browser Proof Plan fields, while substantive rendered proof remains a workflow/role obligation captured in summaries or observation records. +- The current contract is validator-light by design: direct phase verification checks declaration shape, required Browser Proof Plan fields, observation record shape, safe linked-record boundaries, exact plan references, explicit pass/fail posture, supported evidence-kind labels, and bounded claim wording. It does not inspect raw screenshot pixels, report contents, or visual quality; substantive rendered proof remains a workflow/role obligation captured in summaries or observation records. - Failed browser proof is reported through the failure-cause names in `distilled/references/proof-rules.md`: product behavior defects, missing or blocked infrastructure, flaky harnesses, and ambiguous specs explain causes, but they do not add new evidence kinds. **Leverage:** diff --git a/distilled/references/observation-record.md b/distilled/references/observation-record.md index 13e9e0ea..04d804d3 100644 --- a/distilled/references/observation-record.md +++ b/distilled/references/observation-record.md @@ -1,17 +1,32 @@ -# Observation record +# Browser Proof Observation Record -When a step claims UI or runtime behavior was verified, write down what was -actually observed. Plain markdown, one record per checked flow: +When a step claims rendered UI or browser-observed runtime behavior was +verified, record one parser-compatible markdown section per checked flow. -- **flow**: which screen/route/behavior was checked (plans name these up front: - a plan touching UI lists the flows that need a real-browser look). -- **tool**: what did the looking (`agent-browser`, `playwright`, `manual`, - or a project command). -- **observed**: what actually happened — rendered DOM state, behavior on - interaction, console/network errors seen or absent. -- **artifacts**: paths to screenshot / console log / network log, if captured. -- **safe to publish**: yes/no — may the artifacts leave the repo (no secrets, - no private data)? -- **stale after**: what change would invalidate this observation (optional). -- **result**: worked / failed (say why) / partly (say what is missing). For - failed or partial proof, use the failure-cause names in proof-rules.md. +```markdown +## Browser Proof Observation + +- Plan: 01-example/01-PLAN.md +- Flow: /example route, role, data state, and UI state checked +- Viewports: 1280x720 desktop, 390x844 mobile +- Runtime path: agent-browser +- Evidence kind: runtime +- No-command rationale: agent-browser/manual refs were required for this local runtime; claim is limited to the observed session. +- Observed: Changed control rendered, interaction completed, no relevant console/network failures +- Artifacts: + - .work/.../artifacts/example-1280.png - local-only, not safe to publish + - .work/.../artifacts/example-390.png - local-only, not safe to publish +- Privacy/safety: artifacts are local-only and not safe to publish unless sanitized +- Result: passed +- Claim limit: Proves only the scoped route/state, data setup, and viewport set. +- Stale after: route markup, interaction behavior, data fixture, or viewport assumptions change. +``` + +Direct `gsdd verify ` checks the declaration shape, required fields, +repo-local linked records, explicit passing result, and exact `Plan:` reference. +Supported Browser Proof observation evidence kinds are `runtime` for live +browser observation and `test` for repeatable browser-regression commands. +It does not inspect screenshot pixels, network logs, or visual quality. Failed +or partial proof should use the failure-cause names in proof-rules.md +(`distilled/references/proof-rules.md`) and leave the browser-proof claim +blocked or narrowed. diff --git a/distilled/templates/delegates/plan-checker.md b/distilled/templates/delegates/plan-checker.md index 5a202dfe..4c0820d8 100644 --- a/distilled/templates/delegates/plan-checker.md +++ b/distilled/templates/delegates/plan-checker.md @@ -35,7 +35,7 @@ Verify these dimensions: - **Success criteria reachable?** Are the phase success criteria from ROADMAP.md achievable through the planned tasks? Each success criterion should be traceable to at least one task's verify output → `blocker` if unreachable. - **Outcome observable?** After execution, could a human or automated check confirm the goal was met? Plans that produce only internal artifacts with no user-visible or testable outcome → `warning`. - **Claim honest?** Done criteria and evidence limits support only claims that execution can actually prove. For browser proof, reject agent-only "looks good" closure, artifact-count proof, unsupported evidence kinds, and human acceptance that replaces required code, test, runtime, or delivery evidence. - - **Browser proof specific?** Required browser proof names exact route/state, viewport coverage or a narrowed viewport claim, runtime path, rendered observations, artifacts/privacy notes, and claim limit. Under-specified viewport coverage is a blocker for responsive or layout-sensitive claims, and fallback browser tooling must state the `agent-browser` availability constraint. + - **Browser proof specific?** Required browser proof names exact route/state, viewport coverage or a narrowed viewport claim, runtime path, evidence kind, rendered observations, artifacts/privacy notes, and claim limit. Under-specified viewport coverage is a blocker for responsive or layout-sensitive claims, and fallback browser tooling must state the `agent-browser` availability constraint. - **Second pass present?** Shared or high-leverage surfaces have a final contradiction/staleness review before completion. - `approach_alignment`: when APPROACH.md is provided, verify that plan tasks implement the chosen approaches from the user's decisions. Check: - **Alignment proof valid?** When `workflow.discuss` is `true`, APPROACH.md must record `alignment_status: user_confirmed` or `alignment_status: approved_skip`. Missing alignment proof, unknown status, or agent-discretion-only proof -> `blocker` with `fix_hint` telling the planner to revise APPROACH.md through real user alignment or an explicit user-approved skip. diff --git a/distilled/templates/ui-proof.md b/distilled/templates/ui-proof.md index eff33b41..250f8071 100644 --- a/distilled/templates/ui-proof.md +++ b/distilled/templates/ui-proof.md @@ -50,7 +50,8 @@ When browser proof is required, the plan body should include: Routes/states: /example route, role, data state, and UI state to inspect Viewports: Desktop 1280px and mobile 390px, or a narrowed viewport claim Runtime path: agent-browser preferred; explain fallback constraints if unavailable -Evidence command: npm run test:e2e -- --grep "changed flow" +Evidence kind: runtime +No-command rationale: agent-browser/manual refs were required for this local runtime; claim is limited to the observed session. Observations: Changed control is visible, interaction completes, no relevant console/network failures Artifacts: .work/.../artifacts/example-1280.png (local-only unless sanitized) Claim limit: Proves only this changed route/state and viewport set @@ -70,14 +71,17 @@ Write one record per checked flow: ```markdown ## Browser Proof Observation +- Plan: 01-example/01-PLAN.md - Flow: /example route, role, data state, and UI state checked - Viewports: 1280x720 desktop, 390x844 mobile - Runtime path: agent-browser -- Evidence command: npm run test:e2e -- --grep "changed flow" +- Evidence kind: runtime +- No-command rationale: agent-browser/manual refs were required for this local runtime; claim is limited to the observed session. - Observed: Changed control rendered, interaction completed, no relevant console/network failures - Artifacts: - .work/.../artifacts/example-1280.png — local-only, not safe to publish - .work/.../artifacts/example-390.png — local-only, not safe to publish +- Privacy/safety: artifacts are local-only and not safe to publish unless sanitized - Result: passed - Claim limit: Proves only the scoped route/state, data setup, and viewport set. - Stale after: route markup, interaction behavior, data fixture, or viewport assumptions change. diff --git a/distilled/workflows/execute.md b/distilled/workflows/execute.md index 6da5df7e..acb3da27 100644 --- a/distilled/workflows/execute.md +++ b/distilled/workflows/execute.md @@ -165,7 +165,7 @@ Before reporting a task complete: - A task is not complete because code was written. It is complete when the intended verification path actually passes. ### Browser Proof Execution -If the plan sets `browser_proof_required: true`, execute the `## Browser Proof Plan` before claiming completion. Record the exact route/state, viewport(s), runtime path, evidence command or narrowed no-command rationale, observations, artifact paths, privacy/safety note, result, and claim limit in the summary or a linked observation record. +If the plan sets `browser_proof_required: true`, execute the `## Browser Proof Plan` before claiming completion. Record the exact `Plan:` artifact path plus route/state, viewport(s), runtime path, evidence kind, evidence command or narrowed no-command rationale, observations, artifact paths, privacy/safety note, result, and claim limit in the summary or a linked observation record. Use `agent-browser` as the default live browser proof path. Record the planned route/state open, interactive snapshots/refs when interaction is part of the claim, changed-flow interaction, screenshots for planned viewport(s), and relevant console/network observations. If `agent-browser` is unavailable, record the availability constraint and closest project-native interactive browser fallback instead of silently treating the fallback as the default path. If the repo already has Playwright tests or a package script wrapping them, run the relevant targeted test as canonical repeatable regression evidence; keep browser runtime observation as complementary proof. Use Playwright scripting only for checks `agent-browser` cannot cover cleanly, such as JS-disabled, structured console, or multi-context verification. Do not install Playwright, Cypress, Cucumber, Storybook, browser MCP, CI, or visual-regression tooling by default. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes map onto existing evidence kinds, not new evidence kinds. Raw screenshots, traces, videos, DOM snapshots, and reports default to local-only and unsafe to publish unless explicitly sanitized. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication decisions require human evidence or explicit waiver; artifact count, source comments, AST/cAST findings, semantic search, and Semble-like retrieval are not proof. If evidence does not match the planned route/state, viewport, observation, artifact path/manual step, privacy note, result, and claim limit, record proof debt, waiver, deferment, or reduced claim language rather than satisfied proof. Use the failure-cause names in `distilled/references/proof-rules.md` when proof fails or is partial. diff --git a/distilled/workflows/plan.md b/distilled/workflows/plan.md index cad10871..fd852d79 100644 --- a/distilled/workflows/plan.md +++ b/distilled/workflows/plan.md @@ -138,6 +138,7 @@ The Browser Proof Plan is plain markdown, not a JSON slot schema. It must name: - `Routes/states:` exact route(s), screen(s), or UI states to inspect - `Viewports:` desktop and mobile for responsive or layout-sensitive claims, or a narrowed claim explaining why one viewport is enough - `Runtime path:` `agent-browser` preferred; Playwright or another project-native browser fallback must explain the availability constraint +- `Evidence kind:` `runtime` for live browser observation or `test` for a repeatable browser-regression command - `Evidence command:` a runnable command that can reproduce or validate the browser proof, or `No-command rationale:` when the runtime path is manual/interactive only and the claim is narrowed accordingly - `Observations:` rendered DOM/behavior, interaction steps, and console/network observations when relevant - `Artifacts:` screenshot/report/log paths if produced, with local-only vs publishable privacy/safety note @@ -226,7 +227,7 @@ Schema rules: - `must_haves` must trace back to roadmap success criteria - `non_goals`, `hard_boundaries`, `escalation_triggers`, and `approval_gates` must be explicit - include `browser_proof_required` and `browser_proof_rationale` -- if `browser_proof_required: true`, include a `## Browser Proof Plan` section with route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit +- if `browser_proof_required: true`, include a `## Browser Proof Plan` section with route/state, viewport, runtime path, evidence kind, evidence command or no-command rationale, observations, artifacts, and claim limit @@ -375,6 +376,7 @@ must_haves: Routes/states: [Exact routes or UI states to inspect, or `N/A` when not required] Viewports: [Desktop/mobile coverage or narrowed viewport claim] Runtime path: [agent-browser preferred; explain fallback availability constraints] +Evidence kind: [runtime or test] Evidence command: [Runnable command, or use `No-command rationale:` for narrowed manual/interactive proof] Observations: [Rendered behavior, interaction steps, console/network observations] Artifacts: [Screenshot/report/log paths plus local-only or publishable safety note] @@ -578,7 +580,7 @@ For each task: - [ ] `approval_gates` appear anywhere side effects or irreversible choices could happen - [ ] `anti_regression_targets` are concrete enough for verification to check later - [ ] Evidence contract and claim limits prevent the executor from claiming more than verification can support -- [ ] Browser-proof-required plans include route/state, viewport, runtime path, evidence command or no-command rationale, observations, artifacts, and claim limit +- [ ] Browser-proof-required plans include route/state, viewport, runtime path, evidence kind, evidence command or no-command rationale, observations, artifacts/privacy-safety posture, and claim limit - [ ] Shared or high-risk surfaces are named for a second-pass contradiction/staleness review ### Red Flags diff --git a/distilled/workflows/quick.md b/distilled/workflows/quick.md index 10f1d3a2..4e4c5979 100644 --- a/distilled/workflows/quick.md +++ b/distilled/workflows/quick.md @@ -118,7 +118,7 @@ Delegate to the planner role in quick mode. - No research phase, no ROADMAP requirements - Do NOT extract phase requirement IDs — there is no active phase - Derive must-haves directly from the task description -- If the quick task is UI-sensitive, include proportional `browser_proof_required: true`, a short `browser_proof_rationale`, and a plain Browser Proof Plan naming route/state, viewport, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit; otherwise set `browser_proof_required: false` with a short rationale. +- If the quick task is UI-sensitive, include proportional `browser_proof_required: true`, a short `browser_proof_rationale`, and a plain Browser Proof Plan naming route/state, viewport, runtime path, evidence kind, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit; otherwise set `browser_proof_required: false` with a short rationale. - Browser proof must be matchable to exact observed evidence later: route/state, viewport, observation, evidence kind, artifact path or manual step, privacy/safety note, result, and claim limit. Discovery hints from source comments, AST/cAST, semantic search, or Semble-like retrieval do not satisfy proof. - For live rendered UI proof, default to `agent-browser` snapshots/refs, interactions, screenshots, and relevant console/network observations. If unavailable, state the availability constraint and closest project-native interactive browser fallback before narrowing the claim. Existing Playwright/package-script browser tests remain the canonical repeatable regression path when present. The viewport set is plan-owned, but under-specified viewport coverage is weak proof; explain the chosen viewport(s) or narrow the claim limit. - Keep browser proof proportional: do not scaffold Playwright, Cypress, Cucumber, Storybook, CI, browser MCP, or visual-regression tooling by default. diff --git a/distilled/workflows/verify.md b/distilled/workflows/verify.md index dcbad5ce..1e41c8ec 100644 --- a/distilled/workflows/verify.md +++ b/distilled/workflows/verify.md @@ -130,9 +130,9 @@ Note: this step does NOT replace levels 1–3. An artifact can satisfy the evide -Before closure, direct `gsdd verify ` and this workflow must fail closed when the target phase has no matching PLAN.md or SUMMARY.md; report structured prerequisite blockers instead of treating missing artifacts as an empty success. Read browser-proof declaration authority from the plan frontmatter: `browser_proof_required` and `browser_proof_rationale`. Body prose and stale sidecars do not declare proof intent. If `browser_proof_required: false`, verify the rationale is nonblank and treat stale observation records as warnings rather than proof or blockers. If `browser_proof_required: true`, verify the plan contains a `## Browser Proof Plan` with route/state, viewport, runtime path, evidence command or narrowed no-command rationale, observations, artifacts, and claim limit. +Before closure, direct `gsdd verify ` and this workflow must fail closed when the target phase has no matching PLAN.md or SUMMARY.md; report structured prerequisite blockers instead of treating missing artifacts as an empty success. Read browser-proof declaration authority from the plan frontmatter: `browser_proof_required` and `browser_proof_rationale`. Body prose and stale sidecars do not declare proof intent. If `browser_proof_required: false`, verify the rationale is nonblank; legacy `ui_proof_slots: []` with meaningful `no_ui_proof_rationale` is a compatibility warning, not a blocker. If `browser_proof_required: true`, verify the plan contains a `## Browser Proof Plan` with route/state, viewport, runtime path, evidence kind, evidence command or narrowed no-command rationale, observations, artifacts with privacy/safety posture, and claim limit. Direct verification requires each required browser-proof plan to have a repo-local, parser-compatible `## Browser Proof Observation` that names the exact `Plan:` artifact, uses a supported evidence kind, records an explicit passing result, and keeps the claim limit bounded. For live UI runtime proof, expect `agent-browser` as the default captured tool unless the observation record explains a project-native equivalent or an availability constraint. Do not fail solely because another browser tool was used, but downgrade vague proof that lacks exact route/state, planned viewport coverage or rationale, interactive steps/refs where relevant, screenshot/report artifacts, relevant console/network observations, privacy/safety note, or a narrowed claim limit. Existing Playwright tests count as canonical repeatable regression evidence, not a replacement for scoped runtime evidence when browser proof requires runtime observation. -Waiver/deferment narrows the claim; it is not proof. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes are artifact types or activities mapped onto existing evidence kinds, not new evidence kinds. Artifact count is never proof; each artifact must tie to the route/state, observation, artifact path/link, privacy note, and claim limit. +Waiver/deferment narrows the claim; it is not proof. Screenshots, traces, videos, reports, accessibility scans, Gherkin, visual diffs, and manual notes are artifact types or activities mapped onto existing evidence kinds, not new evidence kinds. Artifact count is never proof; each artifact must tie to the route/state, observation, artifact path/link, privacy note, and claim limit. Direct verification checks the record shape and references; the verifier workflow remains responsible for judging whether the recorded observation substantively supports the claim. Raw screenshots, traces, videos, DOM snapshots, and reports default to local-only and unsafe unless sanitized. Visual taste, accessibility judgment, baseline acceptance, subjective polish/layout quality, and privacy publication require human evidence or explicit waiver; human approval does not replace required `code`, `test`, `runtime`, or `delivery` evidence. Source annotations, AST/cAST findings, semantic search, comments, and Semble-like retrieval are discovery hints only. Use the failure-cause names in `distilled/references/proof-rules.md` when proof fails or is partial. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 54bae198..f51466b8 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -233,6 +233,14 @@ Global install writes Workspine-managed files under selected agent homes and rec If `gsdd-cli` is globally installed, you can use the shorter `gsdd ...` form for the same commands. Generated workflow helper calls do not use the global binary; they run through `node .work/bin/gsdd.mjs ...` from the repo root. +Browser-proof contract migration: `update` refreshes templates, skills, +adapters, and helper code, but it does not rewrite historical phase artifacts. +Old no-UI plans that use `ui_proof_slots: []` with a meaningful +`no_ui_proof_rationale` continue to verify with a compatibility warning. Old +plans with non-empty `ui_proof_slots` must be migrated to +`browser_proof_required: true` plus a `## Browser Proof Plan` before they can +close through direct verification. + Normal user flow: 1. Run `npx -y gsdd-cli init`. diff --git a/tests/gsdd.init.test.cjs b/tests/gsdd.init.test.cjs index 9a818e35..44267b1c 100644 --- a/tests/gsdd.init.test.cjs +++ b/tests/gsdd.init.test.cjs @@ -1270,6 +1270,41 @@ describe('gsdd init and update', () => { assert.match(updatedChecker, /^sandbox_mode = "read-only"/m); }); + test('update --templates refreshes templates without rewriting historical phase artifacts', async () => { + const restoreStdin = setNonInteractiveStdin(); + let gsdd; + try { + gsdd = await loadGsdd(tmpDir); + await gsdd.cmdInit('--tools', 'agents'); + } finally { + restoreStdin(); + } + + const phaseDir = path.join(tmpDir, '.work', 'phases', '01-history'); + fs.mkdirSync(phaseDir, { recursive: true }); + const historicalPlanPath = path.join(phaseDir, '01-PLAN.md'); + const historicalPlan = [ + '---', + 'ui_proof_slots: []', + 'no_ui_proof_rationale: Historical CLI-only phase; no rendered UI claim.', + '---', + '# Phase 1 Plan', + '', + 'This artifact is historical user work and must not be rewritten by update.', + ].join('\n'); + fs.writeFileSync(historicalPlanPath, historicalPlan); + + const templatePath = path.join(tmpDir, '.work', 'templates', 'ui-proof.md'); + fs.writeFileSync(templatePath, 'stale template\n'); + + await gsdd.cmdUpdate('--templates'); + + const updatedTemplate = fs.readFileSync(templatePath, 'utf-8'); + assert.doesNotMatch(updatedTemplate, /^stale template$/m); + assert.match(updatedTemplate, /Browser Proof Observation Template/); + assert.strictEqual(fs.readFileSync(historicalPlanPath, 'utf-8'), historicalPlan); + }); + test('cli entrypoint still runs when invoked through an aliased bin path', async () => { const result = await runCliViaJunction(tmpDir, ['help']); diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index a79180ae..d492ccdc 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1877,7 +1877,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { ))); }); - test('phase verify blocks retired ui-proof declarations instead of passing legacy plans', async () => { + test('phase verify blocks legacy no-UI proof declarations with placeholder rationale', async () => { await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-retired-browser-proof'); fs.mkdirSync(phaseDir, { recursive: true }); @@ -1899,11 +1899,42 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { assert.strictEqual(output.verified, false); assert.ok(output.browser_proof_status.blockers.some((blocker) => ( - blocker.code === 'retired_browser_proof_contract' + blocker.code === 'missing_browser_proof_rationale' && blocker.message.includes('ui_proof_slots') ))); }); + test('phase verify blocks legacy non-empty ui-proof slots pending migration', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-legacy-ui-proof-slots'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'ui_proof_slots:', + ' - slot_id: dashboard-render', + ' claim: dashboard render proof', + 'no_ui_proof_rationale: null', + '---', + '# Phase 1 Plan', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'legacy_browser_proof_slots_require_migration' + ))); + assert.strictEqual(output.browser_proof_status.blockers.filter((blocker) => ( + blocker.code === 'legacy_browser_proof_slots_require_migration' + )).length, 1); + }); + test('phase verify normalizes scalar comments and rejects placeholder rationale values', async () => { await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-scalars'); @@ -2019,6 +2050,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { 'Routes/states: /dashboard after loading current account.', 'Viewports: desktop and mobile.', 'Runtime path: agent-browser.', + 'Evidence kind: runtime', 'Evidence command: npm run test:e2e -- --grep dashboard', 'Observations: dashboard widgets render without console errors.', 'Artifacts: .planning/phases/01-browser-proof-observation-missing/artifacts/dashboard.png, local_only.', @@ -2054,6 +2086,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { 'Routes/states: /dashboard after loading current account.', 'Viewports: desktop and mobile.', 'Runtime path: agent-browser.', + 'Evidence kind: runtime', 'Evidence command: npm run test:e2e -- --grep dashboard', 'Observations: dashboard widgets render without console errors.', 'Artifacts: .planning/phases/01-browser-proof-observed/artifacts/dashboard.png, local_only.', @@ -2067,9 +2100,11 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { '', '## Browser Proof Observation', '', + '- Plan: 01-browser-proof-observed/01-PLAN.md', '- Flow: /dashboard after loading current account.', '- Viewports: desktop and mobile.', '- Runtime path: agent-browser.', + '- Evidence kind: runtime', '- Evidence command: npm run test:e2e -- --grep dashboard', '- Observed: dashboard widgets rendered without console errors.', '- Artifacts:', @@ -2087,6 +2122,383 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { assert.strictEqual(output.browser_proof_status.satisfied, true); }); + test('phase verify blocks failed browser proof observations', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-failed'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence kind: runtime', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-failed/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + '## Browser Proof Observation', + '', + '- Plan: 01-browser-proof-failed/01-PLAN.md', + '- Flow: /dashboard after loading current account.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence kind: runtime', + '- Evidence command: npm run test:e2e -- --grep dashboard', + '- Observed: dashboard widget failed to render.', + '- Artifacts: .planning/phases/01-browser-proof-failed/artifacts/dashboard.png - local-only', + '- Result: failed product_bug', + '- Claim limit: dashboard render proof only.', + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'failed_browser_proof_observation' + ))); + }); + + test('phase verify blocks failed browser proof observations even when another observation passes', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-mixed-result'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence kind: runtime', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-mixed-result/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + '## Browser Proof Observation', + '', + '- Plan: 01-browser-proof-mixed-result/01-PLAN.md', + '- Flow: /dashboard after loading current account.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence kind: runtime', + '- Evidence command: npm run test:e2e -- --grep dashboard', + '- Observed: dashboard widgets rendered without console errors.', + '- Artifacts: .planning/phases/01-browser-proof-mixed-result/artifacts/dashboard.png - local-only', + '- Result: passed', + '- Claim limit: dashboard render proof only.', + '', + '## Browser Proof Observation', + '', + '- Plan: 01-browser-proof-mixed-result/01-PLAN.md', + '- Flow: /dashboard after loading current account.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence kind: runtime', + '- Evidence command: npm run test:e2e -- --grep dashboard', + '- Observed: dashboard widget failed to render on mobile.', + '- Artifacts: .planning/phases/01-browser-proof-mixed-result/artifacts/dashboard-mobile.png - local-only', + '- Result: partial product_bug', + '- Claim limit: dashboard mobile render proof only.', + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'failed_browser_proof_observation' + ))); + }); + + test('phase verify blocks single-plan browser proof when observation references another plan', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-wrong-plan'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence kind: runtime', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-wrong-plan/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + '## Browser Proof Observation', + '', + '- Plan: 99-other-phase/99-PLAN.md', + '- Flow: /dashboard after loading current account.', + '- Viewports: desktop and mobile.', + '- Runtime path: agent-browser.', + '- Evidence kind: runtime', + '- Evidence command: npm run test:e2e -- --grep dashboard', + '- Observed: dashboard widgets rendered without console errors.', + '- Artifacts: .planning/phases/01-browser-proof-wrong-plan/artifacts/dashboard.png - local-only', + '- Result: passed', + '- Claim limit: dashboard render proof only.', + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'unmatched_browser_proof_observation' + && blocker.path.endsWith('01-PLAN.md') + ))); + }); + + test('phase verify blocks linked browser proof observation outside workspace', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-outside-link'); + fs.mkdirSync(phaseDir, { recursive: true }); + const outsideRecord = path.join(path.dirname(tmpDir), `outside-browser-proof-${Date.now()}.md`); + fs.writeFileSync(outsideRecord, '## Browser Proof Observation\n\n- Result: passed\n'); + const outsideLink = path.relative(phaseDir, outsideRecord).replace(/\\/g, '/'); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence kind: runtime', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-outside-link/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + `Browser Proof Observation: ${outsideLink}`, + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'invalid_browser_proof_observation_link' + ))); + }); + + test('phase verify blocks URL browser proof observation links', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-url-link'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence kind: runtime', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-url-link/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync( + path.join(phaseDir, '01-SUMMARY.md'), + [ + '# Phase 1 Summary', + '', + 'Browser Proof Observation: https://example.test/proof.md', + ].join('\n') + ); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'invalid_browser_proof_observation_link' + && blocker.message.includes('not a URL') + ))); + }); + + test('phase verify blocks linked browser proof observation symlink escape', async (t) => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-symlink-link'); + fs.mkdirSync(phaseDir, { recursive: true }); + const outsideRecord = path.join(path.dirname(tmpDir), `outside-browser-proof-${Date.now()}.md`); + const symlinkRecord = path.join(phaseDir, 'linked-observation.md'); + fs.writeFileSync(outsideRecord, '## Browser Proof Observation\n\n- Result: passed\n'); + try { + fs.symlinkSync(outsideRecord, symlinkRecord, 'file'); + } catch (error) { + if (['EPERM', 'ENOTSUP', 'EACCES'].includes(error.code)) { + t.skip(`symlink creation unavailable in this environment: ${error.code}`); + return; + } + throw error; + } + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence kind: runtime', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-symlink-link/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n\nBrowser Proof Observation: linked-observation.md\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'invalid_browser_proof_observation_link' + ))); + }); + + test('phase verify reports linked browser proof observation directories instead of throwing', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-directory-link'); + fs.mkdirSync(path.join(phaseDir, 'observations'), { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'browser_proof_required: true', + 'browser_proof_rationale: Rendered UI work changes the visible dashboard.', + '---', + '# Phase 1 Plan', + '', + '## Browser Proof Plan', + 'Routes/states: /dashboard after loading current account.', + 'Viewports: desktop and mobile.', + 'Runtime path: agent-browser.', + 'Evidence kind: runtime', + 'Evidence command: npm run test:e2e -- --grep dashboard', + 'Observations: dashboard widgets render without console errors.', + 'Artifacts: .planning/phases/01-browser-proof-directory-link/artifacts/dashboard.png, local_only.', + 'Claim limit: dashboard render proof only.', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n\nBrowser Proof Observation: observations\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 1, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, false); + assert.ok(output.browser_proof_status.blockers.some((blocker) => ( + blocker.code === 'invalid_browser_proof_observation_link' + ))); + }); + + test('phase verify keeps legacy no-UI proof plans compatible', async () => { + await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-legacy-no-ui-proof'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync( + path.join(phaseDir, '01-PLAN.md'), + [ + '---', + 'ui_proof_slots: []', + 'no_ui_proof_rationale: CLI-only phase; no rendered UI behavior is claimed.', + '---', + '# Phase 1 Plan', + ].join('\n') + ); + fs.writeFileSync(path.join(phaseDir, '01-SUMMARY.md'), '# Phase 1 Summary\n'); + + const result = await runCliAsMain(tmpDir, ['verify', '1']); + assert.strictEqual(result.exitCode, 0, result.output); + const output = JSON.parse(result.output); + + assert.strictEqual(output.verified, true); + assert.strictEqual(output.browser_proof_status.plans[0].legacy_compatible, true); + assert.ok(output.browser_proof_status.warnings.some((warning) => ( + warning.code === 'legacy_browser_proof_contract' + ))); + }); + test('phase verify blocks multi-plan browser proof when observations do not identify each plan', async () => { await runCliAsMain(tmpDir, ['init', '--auto', '--tools', 'agents']); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-browser-proof-multi'); @@ -2102,6 +2514,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { 'Routes/states: /dashboard after loading current account.', 'Viewports: desktop and mobile.', 'Runtime path: agent-browser.', + 'Evidence kind: runtime', 'Evidence command: npm run test:e2e -- --grep dashboard', 'Observations: dashboard widgets render without console errors.', 'Artifacts: .planning/phases/01-browser-proof-multi/artifacts/dashboard.png, local_only.', @@ -2119,6 +2532,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { '- Flow: /dashboard after loading current account.', '- Viewports: desktop and mobile.', '- Runtime path: agent-browser.', + '- Evidence kind: runtime', '- Evidence command: npm run test:e2e -- --grep dashboard', '- Observed: dashboard widgets rendered without console errors.', '- Artifacts: .planning/phases/01-browser-proof-multi/artifacts/dashboard.png - local-only', @@ -2157,6 +2571,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { 'Routes/states: /dashboard after loading current account.', 'Viewports: desktop and mobile.', 'Runtime path: agent-browser.', + 'Evidence kind: runtime', 'Evidence command: npm run test:e2e -- --grep dashboard', 'Observations: dashboard widgets render without console errors.', 'Artifacts: .planning/phases/01-browser-proof-multi-observed/artifacts/dashboard.png, local_only.', @@ -2175,6 +2590,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { '- Flow: /dashboard account A.', '- Viewports: desktop and mobile.', '- Runtime path: agent-browser.', + '- Evidence kind: runtime', '- Evidence command: npm run test:e2e -- --grep dashboard-a', '- Observed: dashboard widgets rendered without console errors.', '- Artifacts: .planning/phases/01-browser-proof-multi-observed/artifacts/dashboard-a.png - local-only', @@ -2187,6 +2603,7 @@ describe('Phase 58 dogfood and Phase 59 UI proof product comparison', () => { '- Flow: /dashboard account B.', '- Viewports: desktop and mobile.', '- Runtime path: agent-browser.', + '- Evidence kind: runtime', '- Evidence command: npm run test:e2e -- --grep dashboard-b', '- Observed: dashboard widgets rendered without console errors.', '- Artifacts: .planning/phases/01-browser-proof-multi-observed/artifacts/dashboard-b.png - local-only',