From 48f46711bef4e08122ac2821af71c7e6ef85a500 Mon Sep 17 00:00:00 2001 From: "copilot-action[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:45:07 +0200 Subject: [PATCH 01/12] docs(sdd): specify issue contract in specs/pre-branch-sdd-gate.md --- specs/pre-branch-sdd-gate.md | 789 +++++++++++------------------------ 1 file changed, 248 insertions(+), 541 deletions(-) diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index 2714f4de1..68a1ebd04 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -1,684 +1,391 @@ -# Pre-branch SDD Gate +# Pre-branch SDD Gate: Readiness in `/copilot status` -- Status: Implemented — automated verification complete; live provider UX review pending -- Date: 2026-09-17 +- Status: Draft +- Date: 2026-10-10 - Catalog capability ID: `issue-start-and-sdd-readiness` -- Last verified: 2026-09-17 on `codex/issue-start-sdd-gate` +- Last verified: Not applicable; this is a prospective change - Owners: Copilot maintainers -- Scope: clarify, update, validate, and publish the owning SDD before eligible branch-based work -- Related issues/PRs: none; local design work; companion SDD `issue-start-and-branch-readiness.md` +- Scope: Add a localized, read-only view of pre-branch SDD readiness to the existing issue status answer. +- Related issues/PRs: Issue #454; existing owning specification `specs/pre-branch-sdd-gate.md` - Required review gates: product UX, architecture, testing, documentation, security/operations -- Open decisions blocking readiness: none in the SDD-only policy; live provider review remains pending +- Open decisions blocking readiness: None stated in the supplied issue and answers. + +> Start with the repository standard in [`README.md`](./README.md). Remove this note when the spec is ready. Mark a non-applicable section explicitly and give a reason instead of silently deleting the concern. ## 1. Executive summary -A repository can enable a pre-branch SDD gate for feature issues and other -branch-bearing issues explicitly marked as behavior changes. After an authorized -`in-progress` start, the Action finds the owning SDD, analyzes the issue, and -asks blocking questions in the issue before an agent drafts any document. Once -the answers are recorded, the agent updates or creates the SDD in a temporary -workspace. The Action validates it, creates or reuses the exact linked branch, -publishes the SDD and required generated catalog metadata as the first -branch-specific commit, verifies the remote result, and then adds `branched`. - -This first version supports **SDDs only**. The Action does not select, request, -generate, validate, catalog, or gate Product Requirements Documents (PRDs) or -Architecture Decision Records (ADRs). Their possible future value does not -justify adding two more document lifecycles to issue comments and branch -readiness now. +Extend the read-only `/copilot status` response for an issue to show its pre-branch SDD readiness using the latest valid Action-owned `copilot:sdd-gate:v1` evidence. The compact section reports whether an SDD is required, the owning SDD path, pending clarification IDs, and a verified SDD commit when publication has been proven. Missing evidence is unknown when a specification is required; malformed, foreign, stale, or inconsistent evidence is invalid or unverified. Neither a branch nor a `published` phase alone proves readiness. + +The command preserves existing status fields, ordering, lifecycle, labels, and finding counts; performs no writes; creates no timeline card; and uses complete existing English and Spanish message catalogs. Help issues remain branchless and show that a specification is not required. ```text -in-progress -> find owning SDD -> ask blocking questions in the issue - -> draft and validate SDD off-branch - -> Action-linked branch -> first SDD/catalog commit/push - -> verify remote commit -> branched -> implementation later +/copilot status -> validate latest durable gate record -> show localized readiness and existing status ``` -Text equivalent: a permitted start leads to SDD ownership and clarification. -Only after the blocking answers are recorded does the agent draft the SDD. The -Action validates it before branch creation and announces readiness after the -first SDD commit is confirmed remotely. +Text equivalent: a user requests status, the application validates durable evidence, and the response adds a truthful readiness section without changing issue or repository state. -## 2. Problem, former behavior, and evidence +## 2. Problem, current behavior, and evidence ### 2.1 Problem -Before this change, the issue-to-branch flow had no SDD readiness gate. The repository -already requires an SDD as a shared, testable contract for capability changes, -but agents can begin branch work while scope, acceptance, or architecture -questions remain unresolved. Supporting PRDs and ADRs in this first gate would -also require distinct selection, answer, review, status, schema, and recovery -rules in the Action, increasing the issue workflow without a demonstrated need. - -### 2.2 Former behavior before this change - -1. `specs/README.md` and `specs/_template.md` define the SDD standard. -2. `specs/catalog.json` identifies capability ownership and SDD paths; - `specs/CATALOG.md` is generated. The validator checks catalog registration - and paths. -3. The issue route can recommend a plan or answer help on opening or editing an - issue. `/copilot clarify` is a separate read-only interaction. -4. Ordinary issue comments without `/copilot` or an exact bot mention are - currently inert; there is no active clarification-session exception. -5. The branch preparation route currently creates a linked branch before SDD - generation. The issue workflow checks out the workflow source, so the new - gate must explicitly pin its semantic base and validate that source. -6. The installed workflows exclude bot-authored events; a bot-applied `SDD` or - `branched` label cannot be the only continuation signal. +A maintainer checking an issue while its SDD is being clarified cannot see pending question IDs or publication evidence in the status answer. They must inspect another status comment to learn what blocks branch creation. A status view that trusts incomplete or malformed evidence could incorrectly suggest that implementation may begin. + +### 2.2 Current behavior + +1. The existing `/copilot status` snapshot reports branch, lifecycle, labels, and Bugbot finding counts, according to the issue description. +2. It does not directly expose the SDD readiness facts specified here. +3. The pre-branch gate already has durable `copilot:sdd-gate:v1` evidence and an existing owning SDD contract at `specs/pre-branch-sdd-gate.md`. +4. The current status command's implementation details and exact message catalog paths are not established by the supplied issue; implementation must locate and preserve those existing contracts. ### 2.3 Evidence -- Specification standard and validator: `specs/README.md`, - `specs/_template.md`, `specs/catalog.json`, and - `scripts/validate-specification-catalog.cjs`. -- Current issue/comment/branch paths: `src/application/usecases/issue_workflow.ts`, - `src/application/usecases/issue_use_case.ts`, - `src/application/usecases/steps/issue/prepare_branches_use_case.ts`, - `.github/workflows/copilot_issue.yml`, and - `.github/workflows/copilot_issue_comment.yml`. -- Current user guidance: `docs/issues/comment-commands.mdx`, - `docs/issues/branch-management.mdx`, and - `docs/development/specifications.mdx`. -- Provider event and token behavior: - https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows - and https://docs.github.com/en/actions/concepts/security/github_token. -- Unknowns: there is no production evidence for an SDD gate or for PRD/ADR - demand in this Action. The user chose local design work before dogfooding. +- Code/workflow/documentation references: Existing owning contract `specs/pre-branch-sdd-gate.md`; issue #454 identifies the status command, durable gate evidence, and complete message catalogs as the relevant scope. Exact source paths are not supplied. +- Incidents or concrete examples: Dogfooding feedback says users inspect another status comment to discover SDD blockers. +- External primary sources: Not applicable; this is an internal command and evidence contract. +- Unknowns: Exact status renderer, ports, catalog file names, and current gate record schema definitions beyond the fields stated in Q2 are implementation discovery items. ### 2.4 Retrospective classification (as-built baselines only) -Not applicable. This was a prospective change. Section 2.2 records the prior -behavior; the current gate contract is defined here and in the companion SDD. +Not applicable. This is a prospective extension; no implemented baseline is reconstructed here. ## 3. Actors, surfaces, and terminology | Actor | Goal | Entry point | Visible surfaces | |---|---|---|---| -| Issue author | Describe behavior and answer questions | Issue Form/comments | Issue question/status cards | -| Maintainer | Start and resolve material choices | `in-progress`/comments | Issue, SDD plan, Action run | -| Contributor | Implement a settled contract | Linked branch | SDD commit, branch, later PR | -| Operator | Recover interrupted publication | Rerun/reconcile | Retained branch/commit, status | -| Copilot Action | Enforce the gate and publish facts | Issue/comment events | Labels, comments, Job Summary, branch | - -An **SDD** is the repository's integrated product and engineering contract. -The `SDD` label indicates that this issue requires an SDD update; it is not a -claim that drafting is finished. A **question card** is one correlated issue -comment with numbered blocking questions. The **owning SDD** is the catalogued -contract for the affected capability, or a new/companion SDD justified by a -new bounded capability. **SDD readiness** means the required update passed -validation and is confirmed in the exact remote linked branch. Branch -existence alone does not prove it. +| Issue author | See whether answers are pending | `/copilot status` on an issue | Localized status comment/answer | +| Maintainer | Confirm SDD owner and branch readiness | `/copilot status` | Readiness section and links/text | +| Contributor | Confirm verified SDD evidence before implementation | `/copilot status` | Owning path and verified commit | +| Copilot Action | Report authoritative readiness facts | Existing status use case | Existing status response | + +A **gate record** is the latest valid Action-bot-owned `copilot:sdd-gate:v1` record for the current issue. **Pending question IDs** are current `plan.questions` IDs without a valid authorized answer in that record. **Verified SDD commit** is the verified latest revision SHA when present, otherwise the first commit SHA, only after existing publication verification proves the commit is on the exact linked remote branch, derives from the recorded base, and changes the owning SDD. **Unknown** means required evidence is absent. **Invalid or unverified** means evidence cannot safely establish readiness. ## 4. Goals, non-goals, and fixed invariants ### 4.1 Goals -1. When the gate applies, no SDD draft MUST be generated before every blocking - product, scope, security, and architecture question has an authorized - answer or an explicitly accepted resolution. -2. The Action MUST identify one owning SDD path from the catalog, or justify - and register one new/companion SDD for a new bounded capability. -3. The required SDD change MUST pass structure, catalog, link, and content - checks before branch creation. -4. The first branch-specific commit MUST contain only the required SDD change - and generated catalog artifacts; `branched` MUST wait for remote verification. -5. One issue MUST have a bounded, inspectable clarification and publication - history across edits, retries, cancellation, and partial writes. +1. Show required/not-required status, owning path, pending question IDs, and verified commit when available. +2. Report pending publication, unavailable evidence, and invalid/unverified evidence honestly. +3. Preserve existing status content and ordering guarantees while keeping the command read-only. +4. Provide complete English and Spanish messages using existing catalogs. ### 4.2 Non-goals -- No PRD or ADR selection, generation, templates, catalog extensions, status - labels, or issue-question branches are part of this version. A separate - future proposal can define them after the SDD workflow is proven useful. -- The gate does not write implementation code, open a PR as a readiness - prerequisite, merge, release, or deploy. -- Help issues remain branchless and never enter the SDD gate. +1. Change SDD eligibility, gate lifecycle, labels, gate record production, publication, or branch creation. +2. Add locales, configuration flags, providers, timeline cards, or a new SDD workflow. +3. Change branch, lifecycle, label, or finding-count semantics. ### 4.3 Fixed product/safety invariants -1. An enabled SDD gate requires issue-managed branches at setup and runtime. -2. Only an authorized human can start work or resolve material questions. - Bot-authored text does not count as a human answer. -3. The drafting agent has no repository write credential. A narrow Action-owned - writer receives only validated SDD/catalog paths and the exact linked ref. -4. A published first commit is never force-pushed away. A retry checks remote - facts and reuses the same operation, branch, and commit when present. -5. Requirements and decisions remain traceable to issue comments and SDD - sections; no answer is invented to pass the gate. +1. Status MUST NOT mutate issues, labels, branches, commits, or SDD files. +2. Only the latest valid Action-owned gate record for this issue may supply durable readiness evidence; arbitrary human comments are not evidence. +3. A branched issue or `published` phase alone MUST NOT be reported ready. +4. Required but absent evidence is unknown; malformed, foreign, stale, or inconsistent evidence is invalid/unverified. +5. Help issues MUST remain branchless and show specification not required/not applicable. ## 5. Current versus proposed product journey | Stage | Current | Proposed | User/operator effect | |---|---|---|---| -| Start | Branch launcher/type shortcut | `in-progress` then SDD eligibility | One deliberate entry | -| Discovery | Optional plan/clarify | Find owning SDD and gaps | Avoid duplicate contracts | -| Questions | One-off read-only reply | Correlated blocking questions | Clear next actor | -| Draft | Contributor may write after branch | Agent drafts off-branch after answers | No premature branch | -| Publication | General branch work | SDD/catalog first commit | Reviewable contract first | -| Ready | Branch label may launch work | Verified `branched` output | Trustworthy handoff | +| Request | `/copilot status` returns existing snapshot | Same command and grammar | No workflow change | +| Gate evidence | No direct SDD readiness section | Read latest valid gate record | Fewer comment inspections | +| Pending questions | Not directly shown | Show current unanswered Q IDs | Clear next action | +| Published SDD | No verified commit in status | Show verified SHA only after proof | Inspectable readiness evidence | +| Invalid/unavailable | Existing snapshot behavior | Preserve snapshot; mark readiness honestly | No false-ready result | ```mermaid flowchart LR - A[Authorized in-progress] --> B{SDD gate applies?} - B -->|No| C[Normal branch preparation] - B -->|Yes| D[Find owning SDD] - D --> E{Blocking questions?} - E -->|Yes| F[Ask and record answers] - E -->|No| G[Draft SDD off-branch] - F --> G - G --> H{Validation passes?} - H -->|No| I[Repair or report block] - I --> G - H -->|Yes| J[Create or reuse linked branch] - J --> K[First SDD/catalog commit and push] - K --> L[Verify remote result; add branched] + A[/copilot status] --> B[Read latest gate record] + B --> C{Validate ownership, schema, identity, freshness} + C -->|Valid| D[Derive readiness fields] + C -->|Missing or invalid| E[Render unknown or unverified] + D --> F[Append localized read-only section] + E --> F + F --> G[Return existing snapshot plus readiness] ``` -Text equivalent: an authorized start either proceeds directly to normal branch -preparation or finds the SDD owner. Blocking questions are answered in the -issue, then the agent drafts and validates the SDD off-branch. The Action -creates the linked branch, publishes its first SDD/catalog commit, verifies it, -and applies `branched`. +Text equivalent: the command reads and validates the latest gate evidence, derives readiness or an honest unavailable/unverified state, then returns the existing snapshot with a localized section and no mutations. ## 6. Functional behavior and state model -### 6.1 Eligibility and SDD ownership - -- `pre-branch-sdd=true` applies to a branch-bearing feature issue and to any - other branch-bearing issue whose authorized maintainer applied the fixed - `contract-change` label. Help never qualifies. The agent can recommend that - label but cannot apply it as a decision about scope. -- The Action queries the catalog for the affected capability. It updates its - owning SDD; a genuinely new capability may receive one new SDD, while a - bounded cross-capability concern may receive a companion. It rejects - ambiguous ownership before branch creation. -- Even when an existing SDD largely covers the issue, the gate requires a - meaningful SDD change, such as issue-specific acceptance and traceability. - It does not create an empty first commit to satisfy the gate. -- The Action adds `SDD` once the gate is required. This derived label survives - retries and remains until the issue completes or the requirement is - deliberately reclassified by an authorized maintainer. - -### 6.2 Happy path - -1. An admitted eligible issue receives `in-progress` from an authorized actor. - The Action snapshots kind, issue revision, profile, semantic base SHA, - catalog ownership, and existing SDD evidence. -2. An agent analyzes missing product and technical facts. One numbered question - card asks all known blocking questions, gives suggested answers where - useful, and names each human decision owner. No SDD draft exists yet. -3. Authorized human comments answer the numbered questions. If there are no - blockers, the Action proceeds without asking for a redundant confirmation. -4. The agent drafts the owning/new SDD in a temporary workspace pinned to the - semantic base, using `specs/_template.md` and `specs/README.md`; a new SDD - updates catalog metadata and regenerated catalog output. -5. The Action validates structure, content, catalog registration, links, - numeric test budget, and the allowlisted path diff. It rereads issue, - answers, profile, and base SHA. Material drift restarts analysis before any - branch mutation. -6. The existing managed-branch capability creates or reuses the exact linked - ref. The writer stages only the SDD and required generated catalog files, - creates the first branch-specific commit, and pushes without force. -7. The Action verifies the remote SHA and changed-path list, updates the issue - status with SDD/commit/branch links, and adds `branched`. Implementation may - then begin in that branch; any PR follows the normal later lifecycle. - -### 6.3 Alternatives, changes, and cancellation - -- With `pre-branch-sdd=false`, the Action skips this gate and follows the - companion branch-readiness contract. Non-feature issues without - `contract-change` also skip the gate. -- A relevant authorized human comment can answer active numbered questions - without a `/copilot` command. Unrelated or bot-authored comments remain inert. - The Action continues within the current run when no answer is needed; it - never relies on its own `SDD` label event to continue. -- Material issue or answer edits before the first push invalidate the draft - digest and trigger bounded re-analysis. Unrelated comments do not. After - publication, a material change enters revision-pending: the Action removes - `branched`, retains the first commit, validates an SDD revision on the same - branch, and restores `branched` after remote verification. -- Removing `in-progress` before branch creation cancels generation but retains - questions and answers. After branch or commit creation, cancellation stops - new work and reports retained artifacts; it never deletes or rewrites them. -- An existing linked branch with unrelated commits cannot claim an SDD/catalog - first commit. The Action blocks, identifies the retained branch, and requires - a deliberate maintainer resolution instead of rewriting history. - -### 6.4 State machine - -| State | Entered when | Visible meaning | Next | Owner/recovery | -|---|---|---|---|---| -| not-required | Gate disabled/ineligible | Normal branch path | branch preparation | Action | -| analyzing | Eligible start accepted | SDD owner and gaps being assessed | awaiting-answer, drafting, blocked | Agent | -| awaiting-answer | Numbered blocker posted | Named person must answer | analyzing, canceled | Issue author/maintainer | -| drafting | Answers complete | SDD generated off-branch | validating, blocked | Agent | -| validating | Draft exists | Structure and freshness checks | publishing, drafting, blocked | Action | -| publishing | Valid draft and exact linked ref | First commit in progress | published, partial | Action | -| partial | Branch/commit exists; later step failed | Retained state and retry | publishing, blocked | Operator | -| published | Remote SDD commit verified | Branch can become ready | revision-pending, implemented | Contributor | -| revision-pending | Material post-publication change | Branch retained; `branched` absent | published, blocked | Maintainer/agent | -| blocked | Invalid source/config/permission | No unsafe progress | prior safe state | Named actor | -| canceled | Start removed before branch | No generation | analyzing on new start | Maintainer | +### 6.1 Happy path -## 7. User-facing configuration +1. The existing status entrypoint receives the issue request and builds the existing snapshot. +2. A typed application port retrieves the latest candidate Action-owned gate record for that issue. +3. A pure validator checks version, ownership, schema, and current issue identity, including the stated record fields: `issueNumber`, `phase`, `plan.path`, `plan.capabilityId`, `plan.questions`, `answers`, `branchName`, `baseSha`, `commitSha`, `revisionBaseSha`, and `revisionSha`. +4. The application derives required status from existing issue classification and gate policy; it does not introduce a classification rule or flag. It derives pending IDs only from current validated questions minus valid authorized answers. +5. For published evidence, the application reports `revisionSha` if present, otherwise `commitSha`, only when existing publication verification proves exact linked remote branch, recorded base ancestry, and an owning SDD change. The first `commitSha` remains immutable. +6. The presenter adds a compact readiness section through existing catalogs and returns all existing status fields unchanged. -| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +### 6.2 Alternative paths + +- Help: show “Specification not required / Especificación no obligatoria”; do not create or link a branch. +- Required gate record missing: show evidence unavailable/unknown and implementation blocked; do not infer from branch state. +- Malformed, foreign, stale, or internally inconsistent record: show invalid or unverified; do not guess question IDs or commit. +- Valid unanswered questions: show IDs and “Awaiting specification answers”. +- Valid required record with no verified published SHA: show first publication pending where applicable; do not claim ready. +- Retrieval failure: preserve the original snapshot and show unavailable/unverified readiness. +- Exempt non-help issue: reflect the existing gate eligibility decision; no new policy is introduced. + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | |---|---|---|---|---| -| `pre-branch-sdd` | boolean | `false` until the team opts in | `true`, `false` | Repository setup and Action input; snapshot at accepted start | -| `issue-managed-branches` | boolean | `true` for branch-bearing work | `true`, `false` | Companion SDD; must be true when SDD gate enabled | -| `contract-change` | fixed label | Absent unless maintainer confirms behavior impact | present/absent | Live issue classification at start | -| `SDD` | fixed label | Added when required | derived present/absent | Issue presentation, not an input | +| Not required | Existing policy exempts issue, including help | No SDD gate applies | Not required | Existing policy | +| Awaiting answers | Valid record has unanswered current questions | Listed Q IDs need answers | Awaiting publication, invalid/unverified | Issue author/maintainer answers through existing flow | +| Publication pending | Required valid record has no verified published SHA | SDD publication is not verified | Ready, invalid/unverified | Existing gate publication flow | +| Ready | Required evidence and remote publication checks pass | SDD evidence is verified | Revision pending if gate evidence changes | Existing gate flow | +| Unknown | Required evidence absent or retrieval unavailable | Readiness cannot be established | Any state on later status read | Retry status / inspect gate | +| Invalid/unverified | Evidence fails validation or verification | Do not proceed on this evidence | Any state after corrected authoritative evidence | Maintainer/operator resolves gate record | -Recommended opt-in: +Duplicate status requests repeat the read and produce no write or new card. Superseded question rounds are ignored. Out-of-order or inconsistent records fail closed to unknown/invalid rather than merging. Status does not cancel or advance gate operations. -```yaml -issue-managed-branches: true -pre-branch-sdd: true -``` +## 7. User-facing configuration -Meaningful alternative for repositories that are not ready for the gate: +No new configuration is introduced. Existing issue classification, gate policy, locale selection, and command admission remain authoritative. -```yaml -issue-managed-branches: true -pre-branch-sdd: false -``` +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +|---|---|---|---|---| +| Existing issue classification | Existing policy value | Existing repository behavior | Existing bounded policy | Current issue/gate policy | +| Existing locale selection | Existing locale | Existing behavior and fallback | Supported existing locales, including English and Spanish | Existing status presentation | -Setup and runtime MUST reject `pre-branch-sdd=true` with managed branches -disabled. An absent setting resolves to `false`; unknown or malformed values -fail validation. The fixed feature/`contract-change` eligibility rule has no -configurable scope or per-document mode. In-flight operations snapshot the -validated setting and base; later setup changes apply only to new starts. -Answer completeness, path allowlist, exact ref, first-commit contents, -validation, and no-force-push rules are not configurable. No legacy -documentation-gate setting exists to migrate. +No new precedence, migration, or retired value is applicable. Status MUST NOT add a flag to select evidence sources or weaken validation. English and Spanish catalogs must both contain complete readiness messages. ## 8. Clean Architecture design ### 8.1 Responsibilities and dependency direction -| Boundary | Owns | Must not own/import | +| Layer/boundary | Owns | Must not own/import | |---|---|---| -| Domain/pure policy | Eligibility, owner choice rules, answer completeness, freshness, states | GitHub/agent SDKs | -| Application use cases | Analyze, ask/consume answers, draft, validate, publish, reconcile | Provider DTOs/process code | -| Semantic ports | Catalog query, issue comments, scratch workspace, validator, linked branch, commit/push | Octokit or Git CLI shapes | -| Adapters | GitHub comments/branch, agent draft, Git workspace, validator invocation | Product eligibility policy | -| Composition | Credentials, model, port wiring, scoped writer | Human decisions | -| Entrypoints | Issue/comment events and admitted operation | Duplicate SDD policy | -| Presentation | Question/status view models and renderers | Repository mutation | +| Domain/pure policy | Evidence validity, question subtraction, readiness state, SHA selection preconditions | GitHub SDK, UI, process code | +| Application | Status orchestration and immutable readiness result | Provider DTOs and rendering details | +| Semantic ports | Read latest gate evidence; verify publication facts | Octokit/Git shapes | +| Adapters/data | Retrieve records and map provider results/errors | Eligibility or readiness policy | +| Infrastructure/composition | Wire existing provider and catalogs | Product decisions | +| Entrypoints | Parse existing command and pass issue context | Duplicate gate logic | +| Presentation | Localized readiness text appended to existing snapshot | Mutations or evidence validation | ```mermaid flowchart LR - E[Issue/comment entrypoint] --> A[SDD gate use case] - A --> P[Pure eligibility and freshness policy] - A --> Q[Question/answer port] - A --> W[Isolated SDD draft port] - A --> V[Validation port] - A --> B[Existing linked branch port] - A --> C[Narrow SDD commit port] - A --> U[Issue presentation] - G[GitHub/Git/agent adapters] --> Q - G --> W - G --> V - G --> B - G --> C + E[Status entrypoint] --> A[Status application use case] + A --> P[Pure readiness policy] + A --> R[Gate evidence read port] + A --> V[Publication verification port] + A --> C[Existing message catalogs] + X[Provider adapters] --> R + X --> V + A --> O[Existing status response] ``` -Text equivalent: an admitted event invokes the gate use case. Pure policies -decide eligibility, answer completeness, ownership, and freshness. The use -case coordinates comments, isolated drafting, validation, linked branch, and -commit ports. Adapters implement external operations; presentation reports -the outcome independently of mutation. +Text equivalent: the existing status use case calls typed read and verification ports, applies pure readiness policy, then passes a view model to the existing catalog-backed presenter. ### 8.2 Contracts, state, and trust boundaries -- `SddGateSnapshot`: repository/issue ID, admitted kind, actor, issue/answer - digest, effective profile, catalog owner, semantic base ref/SHA, question IDs, - operation ID, and completed publication effects. -- `SddPlan`: one owner action (`update | companion | new`), reason, planned - repository path, blocking question IDs, and source links. No arbitrary - executable command or branch name appears in the plan. -- `SddGateOutcome`: `skipped | needs-input | draft-invalid | ready-to-publish | - partial | published | stale | blocked`, exact SDD path, branch/ref/SHA, - retained effects, and recovery action. -- One durable state record owns the operation. Labels are projections, - comments are human evidence, the remote SDD commit is document evidence, - and the exact linked branch is branch authority. -- Repository/issue concurrency serializes publication. Replays compare the - operation ID, source digest, exact ref, and remote SHA. Bot-generated events - are never the sole continuation mechanism. -- A comment is accepted as an answer only during active questioning, from the - issue author or authorized maintainer, after the current card, and mapped to - a numbered question. Outside that state, ordinary comments stay inert. -- Provider failures map to `unavailable`, `stale`, `invalid`, `unauthorized`, or - `partial`, without raw exception text in public comments. +- Pure decisions: validate the candidate record against current issue identity; derive pending IDs from current questions and valid authorized answers; choose revision SHA over first SHA only after proof; map evidence to ready, pending, unknown, or invalid. +- Application contracts: immutable issue identity and existing snapshot input; typed `SddReadiness` result containing requirement, owner path, pending IDs, verified SHA, and evidence state. +- Semantic ports: `readLatestValidSddGateEvidence(issueIdentity)` and existing publication proof query, without provider-specific DTOs. +- Durable state/schema ownership: the existing gate owns `copilot:sdd-gate:v1`; the status command is a read-only consumer and creates no new durable state. +- Concurrency/idempotency: status is a snapshot read; each request validates current evidence. It neither combines rounds nor writes markers. +- Trusted and untrusted inputs: issue text, human comments, arbitrary markers, and provider payloads are untrusted. Only validated Action-owned durable evidence and existing authorization/publication proofs count. +- Provider error mapping: retrieval failure yields unavailable/unknown; invalid schema or identity yields invalid/unverified. Errors do not discard the existing status snapshot or expose raw exceptions. ### 8.3 Executable architecture constraints -- Pure gate policies cannot import agent, provider, process, or UI code; - dependency checks enforce this. -- The draft step has no write credential. The commit port accepts a typed - allowlist of SDD/catalog paths and an exact Action-owned linked ref. -- Catalog validation retains its existing SDD schema. No PRD/ADR file type, - metadata field, or status parser is introduced for this feature. -- The first-commit path-diff guard rejects source, workflow, secrets, - executable scripts, or any unselected file. -- Contract tests parse issue forms, setup profile, Action input, question IDs, - status markers, catalog output, and staged paths structurally. +- Readiness policy modules MUST have no provider, rendering, or process imports; enforce through existing dependency rules. +- The status use case MUST depend on typed semantic ports; adapter contract tests validate field mapping and error conversion. +- Tests MUST assert no write-capable port is called and no status card is created. +- Catalog completeness checks MUST parse both supported message catalogs and verify every required message key. ## 9. UI/UX and content contract ### 9.1 Information hierarchy -The issue presents one current status and one primary human action. It says -what is complete, whether a branch exists, what happens next, and where to -inspect the questions, SDD, Action run, and commit. A single status card is -updated in place; a single question card changes only when the blocking set -changes. A run with no blocking question does not request confirmation merely -to advance. +Keep the existing status content and ordering intact. Add a compact SDD readiness section after the existing snapshot fields, using existing localized status formatting. It presents requirement, owning path, pending IDs, and verified commit when established; otherwise it clearly identifies awaiting answers, publication pending, unavailable, or invalid/unverified. No new comment or timeline card is created. -### 9.2 Representative issue views +### 9.2 Representative primary view -Illustrative issue `#501` and URLs below are examples, not existing resources. +Pending answers, English: + +```markdown +## SDD readiness +- **Specification:** Required +- **Owning SDD:** `specs/pre-branch-sdd-gate.md` +- **Pending questions:** Q2, Q4 +- **Status:** Awaiting specification answers +``` -Pending analysis: +Pending publication, Spanish: ```markdown - -## SDD before the branch -> **Current status:** Checking the issue and existing SDDs. -> **Completed:** Work was started; no branch has been created. -> **Next:** Copilot will find the owning SDD and any blocking questions. -> **Action required:** None yet. -[Issue #501](https://github.com/vypdev/copilot/issues/501) +## Preparación del SDD +- **Especificación:** Obligatoria +- **SDD responsable:** `specs/pre-branch-sdd-gate.md` +- **Preguntas pendientes:** Ninguna +- **Estado:** Primera publicación del SDD pendiente ``` -Action required: +Blocked/unavailable: ```markdown -## SDD before the branch -> **Current status:** Waiting for two answers. -> **Completed:** The owning SDD was found; no branch exists yet. -> **Next:** Copilot will draft the SDD after the answers are recorded. -> **Action required:** Issue author, reply with `Q1` and `Q2`. -1. **Q1 — Product:** Which issue kinds may work without a managed branch? Suggested: help only. -2. **Q2 — Architecture:** Who owns the branch-ready fact? Suggested: the Action, verified from the linked ref. -[Owning SDD](https://github.com/vypdev/copilot/tree/develop/specs) +## SDD readiness +- **Specification:** Required +- **Owning SDD:** Unavailable +- **Status:** Specification evidence unavailable +- **Impact:** Implementation readiness cannot be established. ``` -Blocked before mutation: +Partially complete/unverified: ```markdown -## SDD before the branch -> **Current status:** SDD validation blocked publication. -> **Completed:** Answers were recorded; a draft exists only in the temporary workspace. -> **Impact:** No branch or commit was created. -> **Action required:** Maintainer, resolve the duplicate SDD owner reported in the Action run. -[Action run](https://github.com/vypdev/copilot/actions) +## SDD readiness +- **Specification:** Required +- **Owning SDD:** `specs/pre-branch-sdd-gate.md` +- **Verified SDD commit:** Unverified +- **Status:** Specification evidence invalid or unverified ``` -Partial after branch creation: +Completed: ```markdown -## SDD before the branch -> **Current status:** The SDD push needs recovery. -> **Completed:** Linked branch `feature/501-work-start` exists; SDD validation passed. -> **Impact:** The SDD commit is not confirmed remotely; implementation remains paused and `branched` is absent. -> **Action required:** Retry publication on the same branch after inspecting its remote head. -[Linked branch](https://github.com/vypdev/copilot/tree/feature/501-work-start) · [Action run](https://github.com/vypdev/copilot/actions) +## SDD readiness +- **Specification:** Required +- **Owning SDD:** `specs/pre-branch-sdd-gate.md` +- **Pending questions:** None +- **Verified SDD commit:** `a1b2c3d4` +- **Status:** Specification ready ``` -Complete readiness: +Help issue: ```markdown -## SDD before the branch -> **Current status:** SDD published; branch ready for implementation. -> **Completed:** The SDD update is the first branch commit and passed validation. -> **Next:** Continue work on the linked branch; open a PR through the normal workflow. -> **Action required:** No clarification remains. -[SDD commit](https://github.com/vypdev/copilot/commits/feature/501-work-start) · [Linked branch](https://github.com/vypdev/copilot/tree/feature/501-work-start) +## SDD readiness +- **Specification:** Not required +- **Status:** Specification not required ``` -### 9.3 Issue, comment, and PR behavior - -- Durable hidden markers identify one status and one question card per issue. - A retry updates/reuses them; unchanged replays are silent. At most one new - clarification notification appears per changed blocking-question set. -- `SDD` remains a requirement indicator. Waiting labels identify whether the - author or maintainer must act. `branched` is applied only after remote - verification under the companion SDD. -- The issue links the SDD path and exact first commit. When a contributor later - opens a PR through the existing flow, its description can link that commit; - PR creation is not part of this gate or a readiness dependency. -- The Action never asks for a PRD/ADR choice in this workflow. - -### 9.4 Accessibility and localization - -Use the effective issue locale and the existing complete English fallback. -Question IDs, statuses, and links carry meaning without emoji or color. Cards -use semantic headings, short paragraphs, ordered questions, and narrow/mobile -friendly lists. Sanitize untrusted Markdown, mentions, commands, HTML markers, -URLs, and model output. Every diagram has the adjacent text equivalent; -screen-reader order follows status, completed facts, next action, impact, -links, and technical detail. +### 9.3 Issue, PR, and comment behavior + +- Deterministic section title and existing status response location; no new durable marker is required unless the existing renderer requires one, in which case it must not create a separate card. +- Repeated status requests are read-only and do not post duplicate timeline cards. +- Labels remain supplemental existing fields and do not determine readiness. +- The owner path and verified SHA are shown as safe text or descriptive links using existing formatting; exact URL construction follows existing repository conventions. +- Existing command grammar and lifecycle remain unchanged. + +### 9.4 Accessibility, localization, and responsive behavior + +Use complete English and Spanish catalogs with the exact approved message pairs: Specification required / Especificación obligatoria; Specification not required / Especificación no obligatoria; Owning SDD / SDD responsable; Pending questions / Preguntas pendientes; Verified SDD commit / Commit del SDD verificado; Awaiting specification answers / Esperando respuestas de la especificación; First SDD publication pending / Primera publicación del SDD pendiente; Specification ready / Especificación lista; Specification evidence unavailable / Evidencia de la especificación no disponible; Specification evidence invalid or unverified / Evidencia de la especificación no válida o sin verificar. Use existing locale selection and fallback. Use semantic headings and short lists, readable on mobile and light/dark themes. Escape paths and values; do not render arbitrary comment text or untrusted Markdown. ## 10. Failure, recovery, and cleanup -| Failure/partial state | User impact | Retained facts | Retry | Action | Cleanup | +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | |---|---|---|---|---|---| -| Missing/contradictory answers | Draft waits | Questions and prior answers | On relevant human comment | Answer named IDs | No branch | -| Agent invalid output | No publish | Operation/draft diagnostics | Bounded regeneration | Inspect repeated failure | Delete scratch only | -| Catalog/template validation fails | No branch | Draft and validation codes | After repair | Fix ownership/content | Delete scratch only | -| Base or issue changed during draft | Draft stale | Answers and source digest | Re-analyze | Review new question if needed | Discard stale draft | -| Branch created; SDD commit fails | Implementation blocked | Exact branch/base | Same-ref retry | Inspect remote head | Preserve branch | -| Commit pushed; label/status fails | SDD exists remotely | SHA, branch, path | Reconcile | Rerun presentation | Preserve commit | -| Branch already has unrelated commits | Cannot claim first SDD commit | Exact remote head | After human resolution | Inspect branch | No force/delete | -| Cancellation after branch | No further work | Branch and commit | Explicit resume | Decide next step | No force/delete | - -Every public error says impact, cause, next action, and retained state in that -order. A pushed SDD commit is not represented as failed because a later label -or status update failed. A retry reads the exact remote SHA before writing. +| Gate evidence absent, required | Readiness unknown | Original status snapshot | On next status request | Inspect gate status/continue existing flow | None | +| Retrieval error | Readiness unavailable | Original status snapshot | Next request/provider retry policy | Retry or inspect provider status | None | +| Malformed/foreign/stale record | Readiness invalid | Original snapshot and safe diagnostic code | No guessed recovery | Maintainer/operator resolve authoritative record | No record mutation by status | +| Publication proof unavailable | Not ready | Valid gate fields, no verified SHA | Next read when proof available | Complete/reconcile existing publication | None | +| Catalog message missing | Existing response must remain safe | Existing snapshot | Normal deployment repair | Restore catalog completeness | None | + +Errors follow impact, cause, action, retained state. A readiness read failure never implies the existing status snapshot failed and never changes issue state. ## 11. Security, permissions, and privacy -1. Start and material answers require the existing actor authorization policy; - issue creation or an arbitrary comment is not permission to write files. -2. Issue text and comments are untrusted. They cannot override instructions, - select arbitrary paths, invoke shell commands, choose credentials, or mark - an unanswered question resolved. -3. The drafting agent receives no repository write token. The writer receives - only an allowlisted manifest, exact linked branch, expected SHA, and - short-lived credential. Never expose credentials or private issue content - in SDDs, logs, or comments. -4. Validate paths against symlinks, traversal, case collisions, generated - outputs, and sensitive-file exclusions before staging. No force push or - unrelated branch cleanup is permitted. -5. Cross-repository, stale, forged marker, bot-authored answer, and webhook - replay attempts fail closed with bounded public detail. -6. On Windows, creation of a new SDD in the detached worktree MUST use an - atomic no-replace operation after writing a private sibling temporary file. - A competing file that appears after the initial absence check MUST remain - unchanged; the writer fails, removes its temporary file, and publishes no - branch commit. Existing SDD and generated catalog replacements retain their - separate regular-file checks. This closes the race found by [Bugbot on PR - #403](https://github.com/vypdev/copilot/pull/403). +1. The command uses read-only access for issue and gate evidence; no write credential or mutation port is available to the status use case. +2. Treat issue/comment text and records as untrusted. Validate bot ownership, schema, version, and issue identity before use; never infer from arbitrary comments. +3. Do not expose secrets or raw provider exceptions. Render only validated path, IDs, and commit SHA. +4. Reject foreign, stale, forged, and inconsistent evidence; do not interpret branch presence or phase text as proof. ## 12. Observability and operational UX -The issue status reports gate stage, SDD path/owner, answer owners, branch -existence, first commit SHA, and any retained partial state. The Job Summary -records operation ID, source/profile/base digests, validated paths, exact -branch/SHA, and semantic error code. Metrics count time waiting for answers, -agent validation failures, retries, and first-commit success without storing -private answer text. Rate-limit errors remain pending/retryable rather than a -false negative decision. One status and one active question card bound timeline -noise. +- User-facing state: compact readiness state alongside the existing snapshot. +- Job Summary/operator evidence: retain existing status diagnostics; readiness may record bounded reason codes, not raw issue text. +- Logs, metrics, and correlation: use existing request correlation; no new durable data or metrics are required by this scope. +- Pending external dependency versus workflow failure: unavailable evidence is shown distinctly from valid pending questions. +- Rate-limit/retry behavior: map provider retrieval errors to unavailable while preserving the snapshot; follow existing provider retry policy. +- Noise/comment budget: zero additional cards or notifications from status. ## 13. Compatibility, migration, rollout, and rollback -This is a new opt-in gate. An absent `pre-branch-sdd` setting means `false`. -There is no legacy SDD-gate schema to preserve. The companion SDD removes old -branch launcher settings in the same future release, as requested by the -user. Setup generates the new input, label, form, and guidance artifacts; -doctor detects drift. Existing branches are not retrofitted with a fictional -first SDD commit. Enabling the gate affects only starts accepted afterward. - -Rollout begins with synthetic fixtures and local workflow tests, then a -controlled issue UX check during implementation acceptance. This local SDD -task performs no dogfooding. Rollback disables new starts without erasing -previous SDD commits or branches; partial operations keep explicit recovery -instructions. PRD/ADR support would require a separate future design and is -not a hidden compatibility path in this version. +No schema or configuration migration is introduced. Existing gate records remain authoritative and are read using their current version and validation rules. Existing fields, order, command grammar, supported locales, lifecycle, labels, and finding counts remain compatible. Deploying the change adds one section; rollback removes that section without affecting gate records or branches. No rollout flag is supported or needed per the supplied scope. ## 14. Testing strategy and numeric budget -The following **60 distinct cases** are a floor derived from eligibility, -clarification, source freshness, publication races, partial writes, and SDD -ownership/validation. +Minimum: **28 distinct cases**, derived from evidence validation, read-only behavior, publication proof, and bilingual completeness. -| Area | Minimum cases | Behaviors and risks | +| Area | Minimum distinct cases | Behaviors/risks covered | |---|---:|---| -| Pure selection/configuration/catalog planning | 10 | Boolean mode, feature/label eligibility, owner | -| State, answers, idempotency, cancellation, races | 12 | Q IDs, replay, edits, parallel runs | -| Application use cases | 8 | Analyze through publish and revise | -| Provider/agent/Git adapters | 8 | Comment, scratch workspace, exact ref, errors | -| Workflow/setup/SDD schema | 8 | Forms, profile, template, catalog | -| UX/localization/accessibility/sanitization | 6 | Five states, links, notification budget | -| Integration/security/recovery | 8 | First commit, partial effects, abuse | -| **Total** | **60** | No case counted twice | - -The Windows adapter fixture MUST inject a destination creation immediately -before the no-replace operation, assert that the operation fails with the -competing bytes intact, and verify scratch cleanup. The normal new-SDD publish -fixture MUST still pass on Windows. This fixture uses a local bare Git remote -and no repository credentials. - -Global Jest thresholds and existing specialized budgets remain mandatory. -New pure eligibility/freshness policy SHOULD reach at least 95% branch -coverage; changed issue/context paths SHOULD meet 95% lines/statements and -90% branches/functions in a dedicated gate. Use deterministic fake IDs, -clocks, events, agents, branch heads, and scratch workspaces; no sleeps or live -services in automated tests. Table-driven cases cover every issue kind, -answer role, and first-commit path. Parse YAML, catalog, question markers, -and staged-path manifests structurally. UI goldens require semantic assertions -besides snapshots. Human evidence later checks issue readability on desktop -and mobile, light/dark, screen-reader order, and controlled partial recovery. +| Domain/configuration/pure planning | 7 | Required/exempt, help, pending-answer derivation, unknown/invalid mapping | +| State/application/idempotency/races | 5 | Latest record, superseded rounds, inconsistent records, repeated reads, snapshot preservation | +| Adapters/provider contracts | 4 | Ownership/schema mapping, retrieval failures, publication proof errors | +| Workflows/setup/schema | 3 | Existing command grammar, record version/current issue identity, no new configuration | +| UI/UX/localization/sanitization | 5 | All readiness states, English/Spanish completeness, path/ID escaping | +| Integration/security/migration | 4 | Read-only ports, no card, revision SHA precedence, exact branch/base/SDD proof | +| **Total** | **28** | No case counted twice | + +Maintain repository-wide thresholds. New pure readiness policy should meet at least 95% branch coverage; changed status modules should meet existing module thresholds, with a target of 90% branches and 95% lines/statements. Use deterministic records, clocks, provider fakes, and publication proofs; no sleeps or live services. Parse catalog structures and gate records rather than grep prose. UI fixtures require semantic assertions beyond snapshots. Manual acceptance confirms the section is readable on mobile and desktop and preserves the existing status ordering. ## 15. Documentation and discoverability | Audience | Artifact/page | Required content | Validation/navigation | |---|---|---|---| -| Issue author | `docs/issues/index.mdx`, new SDD-gate guide | When questions arrive and how to answer | Route/link tests | -| Setup owner | `docs/issues/configuration.mdx`, setup guide | Boolean opt-in, branch prerequisite, examples | Setup/doctor fixtures | -| Contributor | `docs/development/specifications.mdx` | SDD owner and first commit | Catalog/link tests | -| Operator | `docs/issues/workflow-setup.mdx`, recovery guide | Partial branch/commit replay | Decision tree/golden output | -| Repository agent | Generated profile/guide | Exact Action branch, SDD gate, permitted writes | Generator contract | +| Issue author/maintainer | Existing status command documentation, including `docs/issues/comment-commands.mdx` if it owns this command | Readiness fields and meanings of unknown, invalid, pending answers, and verified commit | Existing docs link/navigation checks | +| Operator | Existing workflow/status guidance | Evidence source, fail-closed behavior, and recovery through existing gate flow | Contract fixture for examples | +| Contributor | `specs/pre-branch-sdd-gate.md` and status implementation guidance | Read-only consumer boundary and verified SHA conditions | Specification validation and architecture checks | -Documentation navigation presents the normal path first, then configuration, -failures, recovery, and architecture. Examples match generated fixtures. A new -SDD must be registered in the existing catalog and pass existing link checks; -this feature introduces no PRD/ADR documentation or catalog schema. +Keep examples synchronized with catalog fixtures. No new guide is mandated if existing command documentation can be updated clearly. ## 16. Acceptance scenarios -1. With `pre-branch-sdd=false`, an authorized start reaches branch readiness - without an SDD gate. -2. With the gate enabled, a feature selects its existing owning SDD or creates - a justified new one; a routine chore without `contract-change` skips it. -3. A bugfix marked `contract-change` requires an SDD. Help never does. -4. The Action adds `SDD` when the gate is required and does not request a PRD - or ADR decision, file, label, or catalog entry. -5. An unanswered blocking question creates no SDD draft or branch. A relevant - authorized human comment answers a numbered question and resumes work; - unrelated or bot-authored comments have no effect. -6. A fully answered issue generates a meaningful owner-linked SDD change. A - duplicate owner, missing file, unsafe path, or missing numeric test budget - blocks before branch creation. -7. A material issue edit or base SHA change before publication invalidates the - draft; unchanged webhook replay is silent. -8. The Action-created branch's first new commit changes only the selected SDD - and generated catalog artifacts and is verified remotely before `branched`. -9. Branch creation followed by commit failure retains the exact branch and - reports same-ref retry; push success followed by label/status failure - retains the SHA and reconciles without another commit. -10. A material post-publication change retains the first commit, removes - `branched`, and restores it only after the revised SDD is verified on the - same branch. An unrelated edit leaves readiness intact. -11. A malicious issue/comment cannot redirect repository, branch, path, - credential, shell, or answer status. -12. Pending, awaiting-answer, blocked, partial, and published content is - localized, accessible, sanitized, linked, and bounded on replay. -13. Setup rejects an enabled SDD gate without issue-managed branches and - rejects invalid values; installed forms, profile, guide, and Action input - agree on the same boolean setting. -14. Implementation updates current-behavior SDDs, tests, user docs, - catalog evidence, and generated outputs together. +1. Given a valid required gate record with unanswered current questions, when `/copilot status` runs, then it shows required, the owning path, and exactly the unanswered current Q IDs. +2. Given authorized answers to all current questions, when status runs before verified publication, then it shows no pending IDs and first publication pending; it does not claim ready. +3. Given valid publication proof on the exact linked remote branch, from the recorded base, changing the owning SDD, when status runs, then it shows `revisionSha` if present, otherwise immutable `commitSha`, as verified. +4. Given only a branched issue or `published` phase without publication proof, when status runs, then it reports unverified and implementation readiness is blocked. +5. Given missing evidence for a required SDD, when status runs, then readiness is unknown/unavailable and existing status fields remain present and unchanged. +6. Given malformed, foreign, stale, or inconsistent evidence, when status runs, then it reports invalid/unverified and does not guess owner, IDs, or SHA. +7. Given an issue classified by existing policy as not requiring an SDD, including help, when status runs, then it says not required; help remains branchless. +8. Given a provider retrieval error, when status runs, then the original snapshot is preserved and readiness is unavailable without a write. +9. Given English or Spanish locale, when status runs in any readiness state, then every readiness label/state is supplied by the corresponding complete catalog. +10. Given repeated status requests, when each request completes, then no issue, label, branch, commit, SDD, or timeline card is created or changed. +11. Given superseded question rounds or arbitrary human comments, when status derives pending IDs, then only current validated gate questions and valid authorized answers are considered. +12. Given existing branch, lifecycle, labels, and finding counts, when the new readiness section is added, then their values and ordering remain compatible. ## 17. Requirements traceability -| Requirement | Policy/use case/adapter/presentation | Test/evidence | Documentation | +| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | |---|---|---|---| -| 4.1.1 answers before draft | Answer-completeness policy/issue continuation | Q/replay tests | SDD-gate guide | -| 4.1.2 owner selection | Catalog query/selection policy | Ownership/duplicate tests | Specifications guide | -| 4.1.3 validation | Validator and freshness policy | Schema/catalog/stale tests | Contributor guide | -| 4.1.4 first commit/readiness | Narrow writer/readiness policy | Path-diff/remote integration | Branch management | -| 4.1.5 bounded conversation | Durable state/presenter | Marker/noise tests | Issue UX guide | -| 4.3.1 branch prerequisite | Setup/runtime validation | Config matrix | Setup guide | -| 4.3.2 human answers | Actor/answer policy | Forgery/security tests | Clarification guide | -| 4.3.3 write isolation | Draft/writer ports | Token/path/boundary tests | Security guide | -| 4.3.4 no rewrite | Git adapter/remote SHA guard | Race/retry tests | Recovery guide | -| 4.3.5 traceability | Catalog/status presenter | Link/schema tests | Specs catalog | +| Read latest valid Action-owned evidence | Evidence port and validator | Ownership/schema/identity cases | Status command docs | +| Derive pending IDs from current round | Pure readiness policy | Superseded and answer matrix | Status command docs | +| Prove publication before SHA | Verification port and SHA selector | Exact branch/base/path/revision cases | Gate SDD | +| Fail closed on missing/invalid evidence | Application result mapping | Missing, malformed, foreign, stale cases | Operator guidance | +| Preserve snapshot and read-only behavior | Status use case and port set | Snapshot and no-write integration cases | Status command docs | +| Localize complete output | Catalog-backed presenter | English/Spanish key and state coverage | Status command docs | +| Preserve help branchlessness and existing fields | Existing policy/renderer integration | Compatibility scenarios | Existing lifecycle docs | ## 18. Implementation sequence -1. Add the boolean SDD setting, fixed eligibility rule, and setup/profile/form - contract tests before agent generation. -2. Add pure owner selection, answer completeness, freshness, and state policies - with deterministic tests. -3. Add the active issue-comment continuation and durable question/answer - record, preserving inert ordinary comments outside the gate. -4. Bind a read-only agent scratch workspace, validation, exact Action-owned - branch capability, allowlisted writer, and remote SHA guard. -5. Add bounded status/question presentation, locale catalog, generated guide, - and partial-write recovery. -6. Update current-behavior SDD owners, catalog evidence, tests, user/setup and - operator docs, and generated catalog together. -7. Run automated gates. Controlled live UX evidence and dogfooding require - a later GitHub issue and are outside this local implementation task. +1. Locate the existing status use case, evidence schema/validator, publication verification, and English/Spanish catalogs; preserve the owning gate contract. +2. Define typed readiness result and semantic read/verification ports; add pure policy tests for ownership, identity, pending IDs, and state mapping. +3. Integrate read-only orchestration into status while retaining the original snapshot on retrieval errors. +4. Add catalog messages and compact rendering for all states; ensure no timeline-card path is invoked. +5. Add adapter, integration, localization, security, and compatibility tests within the 28-case floor. +6. Update command/operator documentation and contract examples. +7. Run specification, architecture, catalog, and repository validation required by the existing project workflow. ## 19. Definition of Done -- [ ] One SDD owner and every blocking question are discoverable before draft. -- [ ] No answer is invented by the agent; comments and SDD decisions are linked. -- [ ] The 60-case floor and repository/changed-module coverage gates pass. -- [ ] SDD template, catalog, paths, links, validation, and generated output - agree without new PRD/ADR schemas or Action paths. -- [ ] The agent has no writer token; the first branch commit changes only the - allowed SDD/catalog files on the exact linked ref. -- [ ] Stale/duplicate/parallel events and partial branch/commit/label outcomes - recover without force push, duplicate commits, or timeline spam. -- [ ] Five primary GitHub states, localization, accessibility, and - sanitization match examples and human UX review. -- [ ] Setup, issue, operator, contributor, and agent guidance is complete. -- [ ] Current-behavior SDDs and catalog evidence are revised when code is - implemented; `generate:specifications`, `validate:specifications`, - documentation, workflow, architecture, and package gates pass. -- [ ] No readiness-blocking product or architecture decision remains open. +- [ ] Valid evidence displays requirement, owner, current pending IDs, and verified commit only after complete publication proof. +- [ ] Missing, malformed, foreign, stale, inconsistent, and unavailable evidence is never presented as ready. +- [ ] Help is shown as not requiring an SDD and remains branchless. +- [ ] `/copilot status` remains read-only, creates no timeline card, and preserves all existing fields and ordering. +- [ ] English and Spanish catalogs completely cover every readiness state. +- [ ] Typed application ports and Clean Architecture boundaries are enforced by tests/checks. +- [ ] The 28-case minimum and coverage expectations pass; tests are deterministic and offline. +- [ ] User and operator documentation explains fields and evidence limits. +- [ ] Existing command grammar, lifecycle, labels, finding counts, and gate record ownership remain compatible. +- [ ] `specs/catalog.json` evidence and generated `specs/CATALOG.md` remain current; `pnpm run validate:specifications` passes. ## 20. References and decisions -- Local contracts: `specs/README.md`, `specs/_template.md`, - `specs/managed-issue-and-branch-lifecycle.md`, - `specs/configurable-issue-workflows-and-admission.md`, - `specs/comment-automation-and-authorization.md`, and - `specs/repository-agent-collaboration-contract.md`. -- Companion SDD: `issue-start-and-branch-readiness.md`. -- Primary provider sources: - https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows - and https://docs.github.com/en/actions/concepts/security/github_token. -- Product decision: this first Action workflow gates only the owning SDD for - features and explicitly marked behavior changes when the repository opts in. -- Deferred: PRD/ADR support requires its own later product and technical - design, based on experience with the SDD gate. It has no configuration or - compatibility placeholder in this version. -- Rejected: drafting before blocking clarification, arbitrary document paths, - branch creation before validated SDD content, and label-only readiness. +- Owning SDD: `specs/pre-branch-sdd-gate.md`, capability `issue-start-and-sdd-readiness`. +- Issue: #454, dogfooding status readiness request. +- Decisions from explicit answers: use existing feature/contract-change classification; latest valid Action-owned `copilot:sdd-gate:v1` record only; derive question IDs only from current validated questions and authorized answers; verified revision SHA takes precedence over immutable first commit SHA; help is exempt and branchless; use approved English/Spanish catalog wording; preserve existing status fields and make retrieval failure non-destructive. +- Rejected: inferring readiness from branch existence or `published` phase alone; parsing arbitrary human comments; combining superseded clarification rounds; adding writes, cards, configuration, or locale support. +- Follow-up: exact code and catalog paths are to be resolved during implementation discovery; no product decision remains open. \ No newline at end of file From 2a4fcd8ad1f44a00aa590831443390aa9671840f Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sat, 10 Oct 2026 23:00:26 +0200 Subject: [PATCH 02/12] docs(sdd): preserve gate contract and review issue 454 proposal --- specs/CATALOG.md | 4 +- specs/catalog.json | 3 +- specs/pre-branch-sdd-gate.md | 810 ++++++++++++++++------- specs/pre-branch-sdd-status-readiness.md | 391 +++++++++++ 4 files changed, 958 insertions(+), 250 deletions(-) create mode 100644 specs/pre-branch-sdd-status-readiness.md diff --git a/specs/CATALOG.md b/specs/CATALOG.md index e8348765d..b4665299f 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -20,7 +20,7 @@ debt or convert unknown historic intent into a design decision. | `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 1 companion | 202 paths · 2026-10-09 | | `guided-bot-pat-onboarding` | Proposed | Guide creation of the persistent workflow PAT, verify bot identity and grants, recover explicitly from wrong-account input within the session, and install the approved Actions Secret | [Guided bot PAT onboarding](./guided-bot-pat-onboarding.md) | 39 paths · 2026-10-07 | | `temporary-setup-operator-authorization` | Proposed | Guide creation of the one-run operator PAT through GitHub's official form, verify final setup access, and report user-owned deletion accurately | [Assisted setup PAT creation](./temporary-setup-operator-authorization.md) | 34 paths · 2026-10-07 | -| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 1 companion | 51 paths · 2026-09-17 | +| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 2 companion | 51 paths · 2026-09-17 | | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 31 paths · 2026-09-17 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 61 paths · 2026-09-21 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 76 paths · 2026-09-24 | @@ -148,7 +148,7 @@ debt or convert unknown historic intent into a design decision. - Owner: Copilot maintainers - Last verified: 2026-09-17 -- Specifications: [`specs/issue-start-and-branch-readiness.md`](./issue-start-and-branch-readiness.md) · [`specs/pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md) +- Specifications: [`specs/issue-start-and-branch-readiness.md`](./issue-start-and-branch-readiness.md) · [`specs/pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md) · [`specs/pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) - Entrypoints: [`action.yml`](../action.yml) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) - Core code: [`src/domain/issue_workflow_runtime_policy.ts`](../src/domain/issue_workflow_runtime_policy.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/data/model/execution.ts`](../src/data/model/execution.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`scripts/validate-specification-catalog.cjs`](../scripts/validate-specification-catalog.cjs) · [`src/domain/issue_start_policy.ts`](../src/domain/issue_start_policy.ts) · [`src/domain/pre_branch_sdd.ts`](../src/domain/pre_branch_sdd.ts) · [`src/application/ports/pre_branch_sdd_ports.ts`](../src/application/ports/pre_branch_sdd_ports.ts) · [`src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts`](../src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts) · [`src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts`](../src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts) · [`src/data/repository/branch/linked_branch_readiness_repository.ts`](../src/data/repository/branch/linked_branch_readiness_repository.ts) · [`src/infrastructure/pre_branch_sdd_workspace_adapter.ts`](../src/infrastructure/pre_branch_sdd_workspace_adapter.ts) · [`src/infrastructure/specification_catalog_validator.cjs`](../src/infrastructure/specification_catalog_validator.cjs) · [`src/actions/input_boolean_policy.ts`](../src/actions/input_boolean_policy.ts) · [`src/domain/issue_workflow_profile.ts`](../src/domain/issue_workflow_profile.ts) diff --git a/specs/catalog.json b/specs/catalog.json index 6a4fb8929..5a8ddc537 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1172,7 +1172,8 @@ "lastVerified": "2026-09-17", "specs": [ "specs/issue-start-and-branch-readiness.md", - "specs/pre-branch-sdd-gate.md" + "specs/pre-branch-sdd-gate.md", + "specs/pre-branch-sdd-status-readiness.md" ], "workflows": [ ".github/workflows/copilot_issue.yml", diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index 68a1ebd04..930e1381d 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -1,391 +1,707 @@ -# Pre-branch SDD Gate: Readiness in `/copilot status` +# Pre-branch SDD Gate -- Status: Draft -- Date: 2026-10-10 +- Status: Implemented — automated verification complete; live provider UX review pending +- Date: 2026-09-17 - Catalog capability ID: `issue-start-and-sdd-readiness` -- Last verified: Not applicable; this is a prospective change +- Last verified: 2026-09-17 on `codex/issue-start-sdd-gate` - Owners: Copilot maintainers -- Scope: Add a localized, read-only view of pre-branch SDD readiness to the existing issue status answer. -- Related issues/PRs: Issue #454; existing owning specification `specs/pre-branch-sdd-gate.md` +- Scope: clarify, update, validate, and publish the owning SDD before eligible branch-based work +- Related issues/PRs: none; local design work; companion SDD `issue-start-and-branch-readiness.md` - Required review gates: product UX, architecture, testing, documentation, security/operations -- Open decisions blocking readiness: None stated in the supplied issue and answers. +- Open decisions blocking readiness: none in the SDD-only policy; live provider review remains pending -> Start with the repository standard in [`README.md`](./README.md). Remove this note when the spec is ready. Mark a non-applicable section explicitly and give a reason instead of silently deleting the concern. +Live dogfooding on 2026-10-10 (#454) exposed a runner checkout hook that required `node` from the service PATH and prevented the already validated SDD commit. SDD publication now uses an empty hooks directory for that single Git invocation; contributor hook configuration is retained. The Action's own SDD and catalog validation remains mandatory before staging and publication. Subsequent review found that a generated update had replaced the owning capability's previous contract with the new feature's scope. Automated updates now preserve the existing owner verbatim and append a proposed change. Publication retries reuse their validated owner without spending another clarification round. These protections bring the minimum budget to 72. ## 1. Executive summary -Extend the read-only `/copilot status` response for an issue to show its pre-branch SDD readiness using the latest valid Action-owned `copilot:sdd-gate:v1` evidence. The compact section reports whether an SDD is required, the owning SDD path, pending clarification IDs, and a verified SDD commit when publication has been proven. Missing evidence is unknown when a specification is required; malformed, foreign, stale, or inconsistent evidence is invalid or unverified. Neither a branch nor a `published` phase alone proves readiness. - -The command preserves existing status fields, ordering, lifecycle, labels, and finding counts; performs no writes; creates no timeline card; and uses complete existing English and Spanish message catalogs. Help issues remain branchless and show that a specification is not required. +A repository can enable a pre-branch SDD gate for feature issues and other +branch-bearing issues explicitly marked as behavior changes. After an authorized +`in-progress` start, the Action finds the owning SDD, analyzes the issue, and +asks blocking questions in the issue before an agent drafts any document. Once +the answers are recorded, the agent updates or creates the SDD in a temporary +workspace. The Action validates it, creates or reuses the exact linked branch, +publishes the SDD and required generated catalog metadata as the first +branch-specific commit, verifies the remote result, and then adds `branched`. + +This first version supports **SDDs only**. The Action does not select, request, +generate, validate, catalog, or gate Product Requirements Documents (PRDs) or +Architecture Decision Records (ADRs). Their possible future value does not +justify adding two more document lifecycles to issue comments and branch +readiness now. ```text -/copilot status -> validate latest durable gate record -> show localized readiness and existing status +in-progress -> find owning SDD -> ask blocking questions in the issue + -> draft and validate SDD off-branch + -> Action-linked branch -> first SDD/catalog commit/push + -> verify remote commit -> branched -> implementation later ``` -Text equivalent: a user requests status, the application validates durable evidence, and the response adds a truthful readiness section without changing issue or repository state. +Text equivalent: a permitted start leads to SDD ownership and clarification. +Only after the blocking answers are recorded does the agent draft the SDD. The +Action validates it before branch creation and announces readiness after the +first SDD commit is confirmed remotely. -## 2. Problem, current behavior, and evidence +## 2. Problem, former behavior, and evidence ### 2.1 Problem -A maintainer checking an issue while its SDD is being clarified cannot see pending question IDs or publication evidence in the status answer. They must inspect another status comment to learn what blocks branch creation. A status view that trusts incomplete or malformed evidence could incorrectly suggest that implementation may begin. - -### 2.2 Current behavior - -1. The existing `/copilot status` snapshot reports branch, lifecycle, labels, and Bugbot finding counts, according to the issue description. -2. It does not directly expose the SDD readiness facts specified here. -3. The pre-branch gate already has durable `copilot:sdd-gate:v1` evidence and an existing owning SDD contract at `specs/pre-branch-sdd-gate.md`. -4. The current status command's implementation details and exact message catalog paths are not established by the supplied issue; implementation must locate and preserve those existing contracts. +Before this change, the issue-to-branch flow had no SDD readiness gate. The repository +already requires an SDD as a shared, testable contract for capability changes, +but agents can begin branch work while scope, acceptance, or architecture +questions remain unresolved. Supporting PRDs and ADRs in this first gate would +also require distinct selection, answer, review, status, schema, and recovery +rules in the Action, increasing the issue workflow without a demonstrated need. + +### 2.2 Former behavior before this change + +1. `specs/README.md` and `specs/_template.md` define the SDD standard. +2. `specs/catalog.json` identifies capability ownership and SDD paths; + `specs/CATALOG.md` is generated. The validator checks catalog registration + and paths. +3. The issue route can recommend a plan or answer help on opening or editing an + issue. `/copilot clarify` is a separate read-only interaction. +4. Ordinary issue comments without `/copilot` or an exact bot mention are + currently inert; there is no active clarification-session exception. +5. The branch preparation route currently creates a linked branch before SDD + generation. The issue workflow checks out the workflow source, so the new + gate must explicitly pin its semantic base and validate that source. +6. The installed workflows exclude bot-authored events; a bot-applied `SDD` or + `branched` label cannot be the only continuation signal. ### 2.3 Evidence -- Code/workflow/documentation references: Existing owning contract `specs/pre-branch-sdd-gate.md`; issue #454 identifies the status command, durable gate evidence, and complete message catalogs as the relevant scope. Exact source paths are not supplied. -- Incidents or concrete examples: Dogfooding feedback says users inspect another status comment to discover SDD blockers. -- External primary sources: Not applicable; this is an internal command and evidence contract. -- Unknowns: Exact status renderer, ports, catalog file names, and current gate record schema definitions beyond the fields stated in Q2 are implementation discovery items. +- Specification standard and validator: `specs/README.md`, + `specs/_template.md`, `specs/catalog.json`, and + `scripts/validate-specification-catalog.cjs`. +- Current issue/comment/branch paths: `src/application/usecases/issue_workflow.ts`, + `src/application/usecases/issue_use_case.ts`, + `src/application/usecases/steps/issue/prepare_branches_use_case.ts`, + `.github/workflows/copilot_issue.yml`, and + `.github/workflows/copilot_issue_comment.yml`. +- Current user guidance: `docs/issues/comment-commands.mdx`, + `docs/issues/branch-management.mdx`, and + `docs/development/specifications.mdx`. +- Provider event and token behavior: + https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + and https://docs.github.com/en/actions/concepts/security/github_token. +- Unknowns: there is no production evidence for an SDD gate or for PRD/ADR + demand in this Action. The user chose local design work before dogfooding. ### 2.4 Retrospective classification (as-built baselines only) -Not applicable. This is a prospective extension; no implemented baseline is reconstructed here. +Not applicable. This was a prospective change. Section 2.2 records the prior +behavior; the current gate contract is defined here and in the companion SDD. ## 3. Actors, surfaces, and terminology | Actor | Goal | Entry point | Visible surfaces | |---|---|---|---| -| Issue author | See whether answers are pending | `/copilot status` on an issue | Localized status comment/answer | -| Maintainer | Confirm SDD owner and branch readiness | `/copilot status` | Readiness section and links/text | -| Contributor | Confirm verified SDD evidence before implementation | `/copilot status` | Owning path and verified commit | -| Copilot Action | Report authoritative readiness facts | Existing status use case | Existing status response | - -A **gate record** is the latest valid Action-bot-owned `copilot:sdd-gate:v1` record for the current issue. **Pending question IDs** are current `plan.questions` IDs without a valid authorized answer in that record. **Verified SDD commit** is the verified latest revision SHA when present, otherwise the first commit SHA, only after existing publication verification proves the commit is on the exact linked remote branch, derives from the recorded base, and changes the owning SDD. **Unknown** means required evidence is absent. **Invalid or unverified** means evidence cannot safely establish readiness. +| Issue author | Describe behavior and answer questions | Issue Form/comments | Issue question/status cards | +| Maintainer | Start and resolve material choices | `in-progress`/comments | Issue, SDD plan, Action run | +| Contributor | Implement a settled contract | Linked branch | SDD commit, branch, later PR | +| Operator | Recover interrupted publication | Rerun/reconcile | Retained branch/commit, status | +| Copilot Action | Enforce the gate and publish facts | Issue/comment events | Labels, comments, Job Summary, branch | + +An **SDD** is the repository's integrated product and engineering contract. +The `SDD` label indicates that this issue requires an SDD update; it is not a +claim that drafting is finished. A **question card** is one correlated issue +comment with numbered blocking questions. The **owning SDD** is the catalogued +contract for the affected capability, or a new/companion SDD justified by a +new bounded capability. **SDD readiness** means the required update passed +validation and is confirmed in the exact remote linked branch. Branch +existence alone does not prove it. ## 4. Goals, non-goals, and fixed invariants ### 4.1 Goals -1. Show required/not-required status, owning path, pending question IDs, and verified commit when available. -2. Report pending publication, unavailable evidence, and invalid/unverified evidence honestly. -3. Preserve existing status content and ordering guarantees while keeping the command read-only. -4. Provide complete English and Spanish messages using existing catalogs. +1. When the gate applies, no SDD draft MUST be generated before every blocking + product, scope, security, and architecture question has an authorized + answer or an explicitly accepted resolution. +2. The Action MUST identify one owning SDD path from the catalog, or justify + and register one new/companion SDD for a new bounded capability. +3. The required SDD change MUST pass structure, catalog, link, and content + checks before branch creation. +4. The first branch-specific commit MUST contain only the required SDD change + and generated catalog artifacts; `branched` MUST wait for remote verification. +5. One issue MUST have a bounded, inspectable clarification and publication + history across edits, retries, cancellation, and partial writes. ### 4.2 Non-goals -1. Change SDD eligibility, gate lifecycle, labels, gate record production, publication, or branch creation. -2. Add locales, configuration flags, providers, timeline cards, or a new SDD workflow. -3. Change branch, lifecycle, label, or finding-count semantics. +- No PRD or ADR selection, generation, templates, catalog extensions, status + labels, or issue-question branches are part of this version. A separate + future proposal can define them after the SDD workflow is proven useful. +- The gate does not write implementation code, open a PR as a readiness + prerequisite, merge, release, or deploy. +- Help issues remain branchless and never enter the SDD gate. ### 4.3 Fixed product/safety invariants -1. Status MUST NOT mutate issues, labels, branches, commits, or SDD files. -2. Only the latest valid Action-owned gate record for this issue may supply durable readiness evidence; arbitrary human comments are not evidence. -3. A branched issue or `published` phase alone MUST NOT be reported ready. -4. Required but absent evidence is unknown; malformed, foreign, stale, or inconsistent evidence is invalid/unverified. -5. Help issues MUST remain branchless and show specification not required/not applicable. +1. An enabled SDD gate requires issue-managed branches at setup and runtime. +2. Only an authorized human can start work or resolve material questions. + Bot-authored text does not count as a human answer. +3. The drafting agent has no repository write credential. A narrow Action-owned + writer receives only validated SDD/catalog paths and the exact linked ref. +4. A published first commit is never force-pushed away. A retry checks remote + facts and reuses the same operation, branch, and commit when present. +5. Requirements and decisions remain traceable to issue comments and SDD + sections; no answer is invented to pass the gate. ## 5. Current versus proposed product journey | Stage | Current | Proposed | User/operator effect | |---|---|---|---| -| Request | `/copilot status` returns existing snapshot | Same command and grammar | No workflow change | -| Gate evidence | No direct SDD readiness section | Read latest valid gate record | Fewer comment inspections | -| Pending questions | Not directly shown | Show current unanswered Q IDs | Clear next action | -| Published SDD | No verified commit in status | Show verified SHA only after proof | Inspectable readiness evidence | -| Invalid/unavailable | Existing snapshot behavior | Preserve snapshot; mark readiness honestly | No false-ready result | +| Start | Branch launcher/type shortcut | `in-progress` then SDD eligibility | One deliberate entry | +| Discovery | Optional plan/clarify | Find owning SDD and gaps | Avoid duplicate contracts | +| Questions | One-off read-only reply | Correlated blocking questions | Clear next actor | +| Draft | Contributor may write after branch | Agent drafts off-branch after answers | No premature branch | +| Publication | General branch work | SDD/catalog first commit | Reviewable contract first | +| Ready | Branch label may launch work | Verified `branched` output | Trustworthy handoff | ```mermaid flowchart LR - A[/copilot status] --> B[Read latest gate record] - B --> C{Validate ownership, schema, identity, freshness} - C -->|Valid| D[Derive readiness fields] - C -->|Missing or invalid| E[Render unknown or unverified] - D --> F[Append localized read-only section] - E --> F - F --> G[Return existing snapshot plus readiness] + A[Authorized in-progress] --> B{SDD gate applies?} + B -->|No| C[Normal branch preparation] + B -->|Yes| D[Find owning SDD] + D --> E{Blocking questions?} + E -->|Yes| F[Ask and record answers] + E -->|No| G[Draft SDD off-branch] + F --> G + G --> H{Validation passes?} + H -->|No| I[Repair or report block] + I --> G + H -->|Yes| J[Create or reuse linked branch] + J --> K[First SDD/catalog commit and push] + K --> L[Verify remote result; add branched] ``` -Text equivalent: the command reads and validates the latest gate evidence, derives readiness or an honest unavailable/unverified state, then returns the existing snapshot with a localized section and no mutations. +Text equivalent: an authorized start either proceeds directly to normal branch +preparation or finds the SDD owner. Blocking questions are answered in the +issue, then the agent drafts and validates the SDD off-branch. The Action +creates the linked branch, publishes its first SDD/catalog commit, verifies it, +and applies `branched`. ## 6. Functional behavior and state model -### 6.1 Happy path - -1. The existing status entrypoint receives the issue request and builds the existing snapshot. -2. A typed application port retrieves the latest candidate Action-owned gate record for that issue. -3. A pure validator checks version, ownership, schema, and current issue identity, including the stated record fields: `issueNumber`, `phase`, `plan.path`, `plan.capabilityId`, `plan.questions`, `answers`, `branchName`, `baseSha`, `commitSha`, `revisionBaseSha`, and `revisionSha`. -4. The application derives required status from existing issue classification and gate policy; it does not introduce a classification rule or flag. It derives pending IDs only from current validated questions minus valid authorized answers. -5. For published evidence, the application reports `revisionSha` if present, otherwise `commitSha`, only when existing publication verification proves exact linked remote branch, recorded base ancestry, and an owning SDD change. The first `commitSha` remains immutable. -6. The presenter adds a compact readiness section through existing catalogs and returns all existing status fields unchanged. - -### 6.2 Alternative paths - -- Help: show “Specification not required / Especificación no obligatoria”; do not create or link a branch. -- Required gate record missing: show evidence unavailable/unknown and implementation blocked; do not infer from branch state. -- Malformed, foreign, stale, or internally inconsistent record: show invalid or unverified; do not guess question IDs or commit. -- Valid unanswered questions: show IDs and “Awaiting specification answers”. -- Valid required record with no verified published SHA: show first publication pending where applicable; do not claim ready. -- Retrieval failure: preserve the original snapshot and show unavailable/unverified readiness. -- Exempt non-help issue: reflect the existing gate eligibility decision; no new policy is introduced. +### 6.1 Eligibility and SDD ownership + +- `pre-branch-sdd=true` applies to a branch-bearing feature issue and to any + other branch-bearing issue whose authorized maintainer applied the fixed + `contract-change` label. Help never qualifies. The agent can recommend that + label but cannot apply it as a decision about scope. +- The Action queries the catalog for the affected capability. It updates its + owning SDD; a genuinely new capability may receive one new SDD, while a + bounded cross-capability concern may receive a companion. It rejects + ambiguous ownership before branch creation. +- Even when an existing SDD largely covers the issue, the gate requires a + meaningful SDD change, such as issue-specific acceptance and traceability. + It does not create an empty first commit to satisfy the gate. +- The Action adds `SDD` once the gate is required. This derived label survives + retries and remains until the issue completes or the requirement is + deliberately reclassified by an authorized maintainer. + +### 6.2 Happy path + +1. An admitted eligible issue receives `in-progress` from an authorized actor. + The Action snapshots kind, issue revision, profile, semantic base SHA, + catalog ownership, and existing SDD evidence. +2. An agent analyzes missing product and technical facts. One numbered question + card asks all known blocking questions, gives suggested answers where + useful, and names each human decision owner. No SDD draft exists yet. +3. Authorized human comments answer the numbered questions. If there are no + blockers, the Action proceeds without asking for a redundant confirmation. +4. The agent drafts the owning/new SDD in a temporary workspace pinned to the + semantic base, using `specs/_template.md` and `specs/README.md`; a new SDD + updates catalog metadata and regenerated catalog output. +5. The Action validates structure, content, catalog registration, links, + numeric test budget, and the allowlisted path diff. It rereads issue, + answers, profile, and base SHA. Material drift restarts analysis before any + branch mutation. +6. The existing managed-branch capability creates or reuses the exact linked + ref. The writer stages only the SDD and required generated catalog files, + creates the first branch-specific commit, and pushes without force. +7. The Action verifies the remote SHA and changed-path list, updates the issue + status with SDD/commit/branch links, and adds `branched`. Implementation may + then begin in that branch; any PR follows the normal later lifecycle. + +### 6.3 Alternatives, changes, and cancellation + +- With `pre-branch-sdd=false`, the Action skips this gate and follows the + companion branch-readiness contract. Non-feature issues without + `contract-change` also skip the gate. +- A relevant authorized human comment can answer active numbered questions + without a `/copilot` command. Unrelated or bot-authored comments remain inert. + The Action continues within the current run when no answer is needed; it + never relies on its own `SDD` label event to continue. +- Material issue or answer edits before the first push invalidate the draft + digest and trigger bounded re-analysis. Unrelated comments do not. After + publication, a material change enters revision-pending: the Action removes + `branched`, retains the first commit, validates an SDD revision on the same + branch, and restores `branched` after remote verification. +- Removing `in-progress` before branch creation cancels generation but retains + questions and answers. After branch or commit creation, cancellation stops + new work and reports retained artifacts; it never deletes or rewrites them. +- An existing linked branch with unrelated commits cannot claim an SDD/catalog + first commit. The Action blocks, identifies the retained branch, and requires + a deliberate maintainer resolution instead of rewriting history. + +### 6.4 State machine + +| State | Entered when | Visible meaning | Next | Owner/recovery | +|---|---|---|---|---| +| not-required | Gate disabled/ineligible | Normal branch path | branch preparation | Action | +| analyzing | Eligible start accepted | SDD owner and gaps being assessed | awaiting-answer, drafting, blocked | Agent | +| awaiting-answer | Numbered blocker posted | Named person must answer | analyzing, canceled | Issue author/maintainer | +| drafting | Answers complete | SDD generated off-branch | validating, blocked | Agent | +| validating | Draft exists | Structure and freshness checks | publishing, drafting, blocked | Action | +| publishing | Valid draft and exact linked ref | First commit in progress | published, partial | Action | +| partial | Branch/commit exists; later step failed | Retained state and retry | publishing, blocked | Operator | +| published | Remote SDD commit verified | Branch can become ready | revision-pending, implemented | Contributor | +| revision-pending | Material post-publication change | Branch retained; `branched` absent | published, blocked | Maintainer/agent | +| blocked | Invalid source/config/permission | No unsafe progress | prior safe state | Named actor | +| canceled | Start removed before branch | No generation | analyzing on new start | Maintainer | -### 6.3 State machine +## 7. User-facing configuration -| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | |---|---|---|---|---| -| Not required | Existing policy exempts issue, including help | No SDD gate applies | Not required | Existing policy | -| Awaiting answers | Valid record has unanswered current questions | Listed Q IDs need answers | Awaiting publication, invalid/unverified | Issue author/maintainer answers through existing flow | -| Publication pending | Required valid record has no verified published SHA | SDD publication is not verified | Ready, invalid/unverified | Existing gate publication flow | -| Ready | Required evidence and remote publication checks pass | SDD evidence is verified | Revision pending if gate evidence changes | Existing gate flow | -| Unknown | Required evidence absent or retrieval unavailable | Readiness cannot be established | Any state on later status read | Retry status / inspect gate | -| Invalid/unverified | Evidence fails validation or verification | Do not proceed on this evidence | Any state after corrected authoritative evidence | Maintainer/operator resolves gate record | +| `pre-branch-sdd` | boolean | `false` until the team opts in | `true`, `false` | Repository setup and Action input; snapshot at accepted start | +| `issue-managed-branches` | boolean | `true` for branch-bearing work | `true`, `false` | Companion SDD; must be true when SDD gate enabled | +| `contract-change` | fixed label | Absent unless maintainer confirms behavior impact | present/absent | Live issue classification at start | +| `SDD` | fixed label | Added when required | derived present/absent | Issue presentation, not an input | -Duplicate status requests repeat the read and produce no write or new card. Superseded question rounds are ignored. Out-of-order or inconsistent records fail closed to unknown/invalid rather than merging. Status does not cancel or advance gate operations. +Recommended opt-in: -## 7. User-facing configuration +```yaml +issue-managed-branches: true +pre-branch-sdd: true +``` -No new configuration is introduced. Existing issue classification, gate policy, locale selection, and command admission remain authoritative. +Meaningful alternative for repositories that are not ready for the gate: -| Input | Type | Recommended default | Allowed values/range | Scope/persistence | -|---|---|---|---|---| -| Existing issue classification | Existing policy value | Existing repository behavior | Existing bounded policy | Current issue/gate policy | -| Existing locale selection | Existing locale | Existing behavior and fallback | Supported existing locales, including English and Spanish | Existing status presentation | +```yaml +issue-managed-branches: true +pre-branch-sdd: false +``` -No new precedence, migration, or retired value is applicable. Status MUST NOT add a flag to select evidence sources or weaken validation. English and Spanish catalogs must both contain complete readiness messages. +Setup and runtime MUST reject `pre-branch-sdd=true` with managed branches +disabled. An absent setting resolves to `false`; unknown or malformed values +fail validation. The fixed feature/`contract-change` eligibility rule has no +configurable scope or per-document mode. In-flight operations snapshot the +validated setting and base; later setup changes apply only to new starts. +Answer completeness, path allowlist, exact ref, first-commit contents, +validation, and no-force-push rules are not configurable. No legacy +documentation-gate setting exists to migrate. ## 8. Clean Architecture design ### 8.1 Responsibilities and dependency direction -| Layer/boundary | Owns | Must not own/import | +| Boundary | Owns | Must not own/import | |---|---|---| -| Domain/pure policy | Evidence validity, question subtraction, readiness state, SHA selection preconditions | GitHub SDK, UI, process code | -| Application | Status orchestration and immutable readiness result | Provider DTOs and rendering details | -| Semantic ports | Read latest gate evidence; verify publication facts | Octokit/Git shapes | -| Adapters/data | Retrieve records and map provider results/errors | Eligibility or readiness policy | -| Infrastructure/composition | Wire existing provider and catalogs | Product decisions | -| Entrypoints | Parse existing command and pass issue context | Duplicate gate logic | -| Presentation | Localized readiness text appended to existing snapshot | Mutations or evidence validation | +| Domain/pure policy | Eligibility, owner choice rules, answer completeness, freshness, states | GitHub/agent SDKs | +| Application use cases | Analyze, ask/consume answers, draft, validate, publish, reconcile | Provider DTOs/process code | +| Semantic ports | Catalog query, issue comments, scratch workspace, validator, linked branch, commit/push | Octokit or Git CLI shapes | +| Adapters | GitHub comments/branch, agent draft, Git workspace, validator invocation | Product eligibility policy | +| Composition | Credentials, model, port wiring, scoped writer | Human decisions | +| Entrypoints | Issue/comment events and admitted operation | Duplicate SDD policy | +| Presentation | Question/status view models and renderers | Repository mutation | ```mermaid flowchart LR - E[Status entrypoint] --> A[Status application use case] - A --> P[Pure readiness policy] - A --> R[Gate evidence read port] - A --> V[Publication verification port] - A --> C[Existing message catalogs] - X[Provider adapters] --> R - X --> V - A --> O[Existing status response] + E[Issue/comment entrypoint] --> A[SDD gate use case] + A --> P[Pure eligibility and freshness policy] + A --> Q[Question/answer port] + A --> W[Isolated SDD draft port] + A --> V[Validation port] + A --> B[Existing linked branch port] + A --> C[Narrow SDD commit port] + A --> U[Issue presentation] + G[GitHub/Git/agent adapters] --> Q + G --> W + G --> V + G --> B + G --> C ``` -Text equivalent: the existing status use case calls typed read and verification ports, applies pure readiness policy, then passes a view model to the existing catalog-backed presenter. +Text equivalent: an admitted event invokes the gate use case. Pure policies +decide eligibility, answer completeness, ownership, and freshness. The use +case coordinates comments, isolated drafting, validation, linked branch, and +commit ports. Adapters implement external operations; presentation reports +the outcome independently of mutation. ### 8.2 Contracts, state, and trust boundaries -- Pure decisions: validate the candidate record against current issue identity; derive pending IDs from current questions and valid authorized answers; choose revision SHA over first SHA only after proof; map evidence to ready, pending, unknown, or invalid. -- Application contracts: immutable issue identity and existing snapshot input; typed `SddReadiness` result containing requirement, owner path, pending IDs, verified SHA, and evidence state. -- Semantic ports: `readLatestValidSddGateEvidence(issueIdentity)` and existing publication proof query, without provider-specific DTOs. -- Durable state/schema ownership: the existing gate owns `copilot:sdd-gate:v1`; the status command is a read-only consumer and creates no new durable state. -- Concurrency/idempotency: status is a snapshot read; each request validates current evidence. It neither combines rounds nor writes markers. -- Trusted and untrusted inputs: issue text, human comments, arbitrary markers, and provider payloads are untrusted. Only validated Action-owned durable evidence and existing authorization/publication proofs count. -- Provider error mapping: retrieval failure yields unavailable/unknown; invalid schema or identity yields invalid/unverified. Errors do not discard the existing status snapshot or expose raw exceptions. +- `SddGateSnapshot`: repository/issue ID, admitted kind, actor, issue/answer + digest, effective profile, catalog owner, semantic base ref/SHA, question IDs, + operation ID, and completed publication effects. +- `SddPlan`: one owner action (`update | companion | new`), reason, planned + repository path, blocking question IDs, and source links. No arbitrary + executable command or branch name appears in the plan. +- `SddGateOutcome`: `skipped | needs-input | draft-invalid | ready-to-publish | + partial | published | stale | blocked`, exact SDD path, branch/ref/SHA, + retained effects, and recovery action. +- One durable state record owns the operation. Labels are projections, + comments are human evidence, the remote SDD commit is document evidence, + and the exact linked branch is branch authority. +- Repository/issue concurrency serializes publication. Replays compare the + operation ID, source digest, exact ref, and remote SHA. Bot-generated events + are never the sole continuation mechanism. +- A comment is accepted as an answer only during active questioning, from the + issue author or authorized maintainer, after the current card, and mapped to + a numbered question. Outside that state, ordinary comments stay inert. +- Provider failures map to `unavailable`, `stale`, `invalid`, `unauthorized`, or + `partial`, without raw exception text in public comments. ### 8.3 Executable architecture constraints -- Readiness policy modules MUST have no provider, rendering, or process imports; enforce through existing dependency rules. -- The status use case MUST depend on typed semantic ports; adapter contract tests validate field mapping and error conversion. -- Tests MUST assert no write-capable port is called and no status card is created. -- Catalog completeness checks MUST parse both supported message catalogs and verify every required message key. +- Pure gate policies cannot import agent, provider, process, or UI code; + dependency checks enforce this. +- The draft step has no write credential. The commit port accepts a typed + allowlist of SDD/catalog paths and an exact Action-owned linked ref. +- Catalog validation retains its existing SDD schema. No PRD/ADR file type, + metadata field, or status parser is introduced for this feature. +- The first-commit path-diff guard rejects source, workflow, secrets, + executable scripts, or any unselected file. +- Contract tests parse issue forms, setup profile, Action input, question IDs, + status markers, catalog output, and staged paths structurally. ## 9. UI/UX and content contract ### 9.1 Information hierarchy -Keep the existing status content and ordering intact. Add a compact SDD readiness section after the existing snapshot fields, using existing localized status formatting. It presents requirement, owning path, pending IDs, and verified commit when established; otherwise it clearly identifies awaiting answers, publication pending, unavailable, or invalid/unverified. No new comment or timeline card is created. +The issue presents one current status and one primary human action. It says +what is complete, whether a branch exists, what happens next, and where to +inspect the questions, SDD, Action run, and commit. A single status card is +updated in place; a single question card changes only when the blocking set +changes. A run with no blocking question does not request confirmation merely +to advance. -### 9.2 Representative primary view +### 9.2 Representative issue views -Pending answers, English: - -```markdown -## SDD readiness -- **Specification:** Required -- **Owning SDD:** `specs/pre-branch-sdd-gate.md` -- **Pending questions:** Q2, Q4 -- **Status:** Awaiting specification answers -``` +Illustrative issue `#501` and URLs below are examples, not existing resources. -Pending publication, Spanish: +Pending analysis: ```markdown -## Preparación del SDD -- **Especificación:** Obligatoria -- **SDD responsable:** `specs/pre-branch-sdd-gate.md` -- **Preguntas pendientes:** Ninguna -- **Estado:** Primera publicación del SDD pendiente + +## SDD before the branch +> **Current status:** Checking the issue and existing SDDs. +> **Completed:** Work was started; no branch has been created. +> **Next:** Copilot will find the owning SDD and any blocking questions. +> **Action required:** None yet. +[Issue #501](https://github.com/vypdev/copilot/issues/501) ``` -Blocked/unavailable: +Action required: ```markdown -## SDD readiness -- **Specification:** Required -- **Owning SDD:** Unavailable -- **Status:** Specification evidence unavailable -- **Impact:** Implementation readiness cannot be established. +## SDD before the branch +> **Current status:** Waiting for two answers. +> **Completed:** The owning SDD was found; no branch exists yet. +> **Next:** Copilot will draft the SDD after the answers are recorded. +> **Action required:** Issue author, reply with `Q1` and `Q2`. +1. **Q1 — Product:** Which issue kinds may work without a managed branch? Suggested: help only. +2. **Q2 — Architecture:** Who owns the branch-ready fact? Suggested: the Action, verified from the linked ref. +[Owning SDD](https://github.com/vypdev/copilot/tree/develop/specs) ``` -Partially complete/unverified: +Blocked before mutation: ```markdown -## SDD readiness -- **Specification:** Required -- **Owning SDD:** `specs/pre-branch-sdd-gate.md` -- **Verified SDD commit:** Unverified -- **Status:** Specification evidence invalid or unverified +## SDD before the branch +> **Current status:** SDD validation blocked publication. +> **Completed:** Answers were recorded; a draft exists only in the temporary workspace. +> **Impact:** No branch or commit was created. +> **Action required:** Maintainer, resolve the duplicate SDD owner reported in the Action run. +[Action run](https://github.com/vypdev/copilot/actions) ``` -Completed: +Partial after branch creation: ```markdown -## SDD readiness -- **Specification:** Required -- **Owning SDD:** `specs/pre-branch-sdd-gate.md` -- **Pending questions:** None -- **Verified SDD commit:** `a1b2c3d4` -- **Status:** Specification ready +## SDD before the branch +> **Current status:** The SDD push needs recovery. +> **Completed:** Linked branch `feature/501-work-start` exists; SDD validation passed. +> **Impact:** The SDD commit is not confirmed remotely; implementation remains paused and `branched` is absent. +> **Action required:** Retry publication on the same branch after inspecting its remote head. +[Linked branch](https://github.com/vypdev/copilot/tree/feature/501-work-start) · [Action run](https://github.com/vypdev/copilot/actions) ``` -Help issue: +Complete readiness: ```markdown -## SDD readiness -- **Specification:** Not required -- **Status:** Specification not required +## SDD before the branch +> **Current status:** SDD published; branch ready for implementation. +> **Completed:** The SDD update is the first branch commit and passed validation. +> **Next:** Continue work on the linked branch; open a PR through the normal workflow. +> **Action required:** No clarification remains. +[SDD commit](https://github.com/vypdev/copilot/commits/feature/501-work-start) · [Linked branch](https://github.com/vypdev/copilot/tree/feature/501-work-start) ``` -### 9.3 Issue, PR, and comment behavior - -- Deterministic section title and existing status response location; no new durable marker is required unless the existing renderer requires one, in which case it must not create a separate card. -- Repeated status requests are read-only and do not post duplicate timeline cards. -- Labels remain supplemental existing fields and do not determine readiness. -- The owner path and verified SHA are shown as safe text or descriptive links using existing formatting; exact URL construction follows existing repository conventions. -- Existing command grammar and lifecycle remain unchanged. - -### 9.4 Accessibility, localization, and responsive behavior - -Use complete English and Spanish catalogs with the exact approved message pairs: Specification required / Especificación obligatoria; Specification not required / Especificación no obligatoria; Owning SDD / SDD responsable; Pending questions / Preguntas pendientes; Verified SDD commit / Commit del SDD verificado; Awaiting specification answers / Esperando respuestas de la especificación; First SDD publication pending / Primera publicación del SDD pendiente; Specification ready / Especificación lista; Specification evidence unavailable / Evidencia de la especificación no disponible; Specification evidence invalid or unverified / Evidencia de la especificación no válida o sin verificar. Use existing locale selection and fallback. Use semantic headings and short lists, readable on mobile and light/dark themes. Escape paths and values; do not render arbitrary comment text or untrusted Markdown. +### 9.3 Issue, comment, and PR behavior + +- Durable hidden markers identify one status and one question card per issue. + A retry updates/reuses them; unchanged replays are silent. At most one new + clarification notification appears per changed blocking-question set. +- `SDD` remains a requirement indicator. Waiting labels identify whether the + author or maintainer must act. `branched` is applied only after remote + verification under the companion SDD. +- The issue links the SDD path and exact first commit. When a contributor later + opens a PR through the existing flow, its description can link that commit; + PR creation is not part of this gate or a readiness dependency. +- The Action never asks for a PRD/ADR choice in this workflow. + +### 9.4 Accessibility and localization + +Use the effective issue locale and the existing complete English fallback. +Question IDs, statuses, and links carry meaning without emoji or color. Cards +use semantic headings, short paragraphs, ordered questions, and narrow/mobile +friendly lists. Sanitize untrusted Markdown, mentions, commands, HTML markers, +URLs, and model output. Every diagram has the adjacent text equivalent; +screen-reader order follows status, completed facts, next action, impact, +links, and technical detail. ## 10. Failure, recovery, and cleanup -| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +| Failure/partial state | User impact | Retained facts | Retry | Action | Cleanup | |---|---|---|---|---|---| -| Gate evidence absent, required | Readiness unknown | Original status snapshot | On next status request | Inspect gate status/continue existing flow | None | -| Retrieval error | Readiness unavailable | Original status snapshot | Next request/provider retry policy | Retry or inspect provider status | None | -| Malformed/foreign/stale record | Readiness invalid | Original snapshot and safe diagnostic code | No guessed recovery | Maintainer/operator resolve authoritative record | No record mutation by status | -| Publication proof unavailable | Not ready | Valid gate fields, no verified SHA | Next read when proof available | Complete/reconcile existing publication | None | -| Catalog message missing | Existing response must remain safe | Existing snapshot | Normal deployment repair | Restore catalog completeness | None | - -Errors follow impact, cause, action, retained state. A readiness read failure never implies the existing status snapshot failed and never changes issue state. +| Missing/contradictory answers | Draft waits | Questions and prior answers | On relevant human comment | Answer named IDs | No branch | +| Agent invalid output | No publish | Operation/draft diagnostics | Bounded regeneration | Inspect repeated failure | Delete scratch only | +| Catalog/template validation fails | No branch | Draft and validation codes | After repair | Fix ownership/content | Delete scratch only | +| Base or issue changed during draft | Draft stale | Answers and source digest | Re-analyze | Review new question if needed | Discard stale draft | +| Branch created; SDD commit fails | Implementation blocked | Exact branch/base | Same-ref retry | Inspect remote head | Preserve branch | +| Commit pushed; label/status fails | SDD exists remotely | SHA, branch, path | Reconcile | Rerun presentation | Preserve commit | +| Branch already has unrelated commits | Cannot claim first SDD commit | Exact remote head | After human resolution | Inspect branch | No force/delete | +| Cancellation after branch | No further work | Branch and commit | Explicit resume | Decide next step | No force/delete | + +Every public error says impact, cause, next action, and retained state in that +order. A pushed SDD commit is not represented as failed because a later label +or status update failed. A retry reads the exact remote SHA before writing. ## 11. Security, permissions, and privacy -1. The command uses read-only access for issue and gate evidence; no write credential or mutation port is available to the status use case. -2. Treat issue/comment text and records as untrusted. Validate bot ownership, schema, version, and issue identity before use; never infer from arbitrary comments. -3. Do not expose secrets or raw provider exceptions. Render only validated path, IDs, and commit SHA. -4. Reject foreign, stale, forged, and inconsistent evidence; do not interpret branch presence or phase text as proof. +1. Start and material answers require the existing actor authorization policy; + issue creation or an arbitrary comment is not permission to write files. +2. Issue text and comments are untrusted. They cannot override instructions, + select arbitrary paths, invoke shell commands, choose credentials, or mark + an unanswered question resolved. +3. The drafting agent receives no repository write token. The writer receives + only an allowlisted manifest, exact linked branch, expected SHA, and + short-lived credential. Never expose credentials or private issue content + in SDDs, logs, or comments. +4. Validate paths against symlinks, traversal, case collisions, generated + outputs, and sensitive-file exclusions before staging. No force push or + unrelated branch cleanup is permitted. +5. Cross-repository, stale, forged marker, bot-authored answer, and webhook + replay attempts fail closed with bounded public detail. +6. On Windows, creation of a new SDD in the detached worktree MUST use an + atomic no-replace operation after writing a private sibling temporary file. + A competing file that appears after the initial absence check MUST remain + unchanged; the writer fails, removes its temporary file, and publishes no + branch commit. Existing SDD and generated catalog replacements retain their + separate regular-file checks. This closes the race found by [Bugbot on PR + #403](https://github.com/vypdev/copilot/pull/403). + +A failed first publication persists the exact linked branch and original base in the bot-owned awaiting-answer record without claiming a published commit. Replay may reuse an empty retained branch only when its head belongs to the parent history; implementation commits before the first SDD block recovery. Advancing develop does not rebase, recreate, or force-push that branch. A pushed first SDD is recovered only through the existing author, subject, catalog, path, parent and remote ancestry verification. Action-owned hidden configuration is excluded from human issue freshness. Published records still require the exact verified first commit. + +An unchanged awaiting-publication record with no pending questions reuses its +validated owner and answers, drafts again, and repeats validation and freshness +checks. It does not reenter analysis or increment the clarification round. +Automated `update` drafts are proposed appendices, not replacements: trusted +composition retains the complete original Markdown, shifts only prose heading +levels in the appendix, and preserves fenced examples. An appendix has a fixed +18,000-character limit; the combined document retains the 70,000-character +limit. Overflow fails before publication without truncation. Contributors review +and integrate proposals explicitly; new distinct concerns may use a catalogued +companion before branch creation. Existing requirements and test budgets cannot +be silently erased by model output. ## 12. Observability and operational UX -- User-facing state: compact readiness state alongside the existing snapshot. -- Job Summary/operator evidence: retain existing status diagnostics; readiness may record bounded reason codes, not raw issue text. -- Logs, metrics, and correlation: use existing request correlation; no new durable data or metrics are required by this scope. -- Pending external dependency versus workflow failure: unavailable evidence is shown distinctly from valid pending questions. -- Rate-limit/retry behavior: map provider retrieval errors to unavailable while preserving the snapshot; follow existing provider retry policy. -- Noise/comment budget: zero additional cards or notifications from status. +The issue status reports gate stage, SDD path/owner, answer owners, branch +existence, first commit SHA, and any retained partial state. The Job Summary +records operation ID, source/profile/base digests, validated paths, exact +branch/SHA, and semantic error code. Metrics count time waiting for answers, +agent validation failures, retries, and first-commit success without storing +private answer text. Rate-limit errors remain pending/retryable rather than a +false negative decision. One status and one active question card bound timeline +noise. ## 13. Compatibility, migration, rollout, and rollback -No schema or configuration migration is introduced. Existing gate records remain authoritative and are read using their current version and validation rules. Existing fields, order, command grammar, supported locales, lifecycle, labels, and finding counts remain compatible. Deploying the change adds one section; rollback removes that section without affecting gate records or branches. No rollout flag is supported or needed per the supplied scope. +This is a new opt-in gate. An absent `pre-branch-sdd` setting means `false`. +There is no legacy SDD-gate schema to preserve. The companion SDD removes old +branch launcher settings in the same future release, as requested by the +user. Setup generates the new input, label, form, and guidance artifacts; +doctor detects drift. Existing branches are not retrofitted with a fictional +first SDD commit. Enabling the gate affects only starts accepted afterward. + +Rollout begins with synthetic fixtures and local workflow tests, then a +controlled issue UX check during implementation acceptance. This local SDD +task performs no dogfooding. Rollback disables new starts without erasing +previous SDD commits or branches; partial operations keep explicit recovery +instructions. PRD/ADR support would require a separate future design and is +not a hidden compatibility path in this version. ## 14. Testing strategy and numeric budget -Minimum: **28 distinct cases**, derived from evidence validation, read-only behavior, publication proof, and bilingual completeness. +The following **72 distinct cases** are a floor derived from eligibility, +clarification, source freshness, publication races, partial writes, and SDD +ownership/validation. -| Area | Minimum distinct cases | Behaviors/risks covered | +| Area | Minimum cases | Behaviors and risks | |---|---:|---| -| Domain/configuration/pure planning | 7 | Required/exempt, help, pending-answer derivation, unknown/invalid mapping | -| State/application/idempotency/races | 5 | Latest record, superseded rounds, inconsistent records, repeated reads, snapshot preservation | -| Adapters/provider contracts | 4 | Ownership/schema mapping, retrieval failures, publication proof errors | -| Workflows/setup/schema | 3 | Existing command grammar, record version/current issue identity, no new configuration | -| UI/UX/localization/sanitization | 5 | All readiness states, English/Spanish completeness, path/ID escaping | -| Integration/security/migration | 4 | Read-only ports, no card, revision SHA precedence, exact branch/base/SDD proof | -| **Total** | **28** | No case counted twice | - -Maintain repository-wide thresholds. New pure readiness policy should meet at least 95% branch coverage; changed status modules should meet existing module thresholds, with a target of 90% branches and 95% lines/statements. Use deterministic records, clocks, provider fakes, and publication proofs; no sleeps or live services. Parse catalog structures and gate records rather than grep prose. UI fixtures require semantic assertions beyond snapshots. Manual acceptance confirms the section is readable on mobile and desktop and preserves the existing status ordering. +| Pure selection/configuration/catalog planning | 10 | Boolean mode, feature/label eligibility, owner | +| State, answers, idempotency, cancellation, races | 12 | Q IDs, replay, edits, parallel runs | +| Application use cases | 8 | Analyze through publish and revise | +| Provider/agent/Git adapters | 8 | Comment, scratch workspace, exact ref, errors | +| Workflow/setup/SDD schema | 8 | Forms, profile, template, catalog | +| UX/localization/accessibility/sanitization | 6 | Five states, links, notification budget | +| Integration/security/recovery | 8 | First commit, partial effects, abuse | +| Pending-branch recovery | 6 | Retained base, intervening commits, managed metadata, pending card, replay, post-push recovery | +| Automation hook isolation | 1 | Validated SDD publication ignores failing checkout hooks; configuration preserved | +| Owner preservation and publication replay | 5 | Original contract and fenced examples, appendix bound, combined bound, real Git publication of narrower output, retry after the third clarification round | +| **Total** | **72** | No case counted twice | + +The Windows adapter fixture MUST inject a destination creation immediately +before the no-replace operation, assert that the operation fails with the +competing bytes intact, and verify scratch cleanup. The normal new-SDD publish +fixture MUST still pass on Windows. This fixture uses a local bare Git remote +and no repository credentials. + +Global Jest thresholds and existing specialized budgets remain mandatory. +New pure eligibility/freshness policy SHOULD reach at least 95% branch +coverage; changed issue/context paths SHOULD meet 95% lines/statements and +90% branches/functions in a dedicated gate. Use deterministic fake IDs, +clocks, events, agents, branch heads, and scratch workspaces; no sleeps or live +services in automated tests. Table-driven cases cover every issue kind, +answer role, and first-commit path. Parse YAML, catalog, question markers, +and staged-path manifests structurally. UI goldens require semantic assertions +besides snapshots. Human evidence later checks issue readability on desktop +and mobile, light/dark, screen-reader order, and controlled partial recovery. ## 15. Documentation and discoverability | Audience | Artifact/page | Required content | Validation/navigation | |---|---|---|---| -| Issue author/maintainer | Existing status command documentation, including `docs/issues/comment-commands.mdx` if it owns this command | Readiness fields and meanings of unknown, invalid, pending answers, and verified commit | Existing docs link/navigation checks | -| Operator | Existing workflow/status guidance | Evidence source, fail-closed behavior, and recovery through existing gate flow | Contract fixture for examples | -| Contributor | `specs/pre-branch-sdd-gate.md` and status implementation guidance | Read-only consumer boundary and verified SHA conditions | Specification validation and architecture checks | +| Issue author | `docs/issues/index.mdx`, new SDD-gate guide | When questions arrive and how to answer | Route/link tests | +| Setup owner | `docs/issues/configuration.mdx`, setup guide | Boolean opt-in, branch prerequisite, examples | Setup/doctor fixtures | +| Contributor | `docs/development/specifications.mdx` | SDD owner and first commit | Catalog/link tests | +| Operator | `docs/issues/workflow-setup.mdx`, recovery guide | Partial branch/commit replay | Decision tree/golden output | +| Repository agent | Generated profile/guide | Exact Action branch, SDD gate, permitted writes | Generator contract | -Keep examples synchronized with catalog fixtures. No new guide is mandated if existing command documentation can be updated clearly. +Documentation navigation presents the normal path first, then configuration, +failures, recovery, and architecture. Examples match generated fixtures. A new +SDD must be registered in the existing catalog and pass existing link checks; +this feature introduces no PRD/ADR documentation or catalog schema. ## 16. Acceptance scenarios -1. Given a valid required gate record with unanswered current questions, when `/copilot status` runs, then it shows required, the owning path, and exactly the unanswered current Q IDs. -2. Given authorized answers to all current questions, when status runs before verified publication, then it shows no pending IDs and first publication pending; it does not claim ready. -3. Given valid publication proof on the exact linked remote branch, from the recorded base, changing the owning SDD, when status runs, then it shows `revisionSha` if present, otherwise immutable `commitSha`, as verified. -4. Given only a branched issue or `published` phase without publication proof, when status runs, then it reports unverified and implementation readiness is blocked. -5. Given missing evidence for a required SDD, when status runs, then readiness is unknown/unavailable and existing status fields remain present and unchanged. -6. Given malformed, foreign, stale, or inconsistent evidence, when status runs, then it reports invalid/unverified and does not guess owner, IDs, or SHA. -7. Given an issue classified by existing policy as not requiring an SDD, including help, when status runs, then it says not required; help remains branchless. -8. Given a provider retrieval error, when status runs, then the original snapshot is preserved and readiness is unavailable without a write. -9. Given English or Spanish locale, when status runs in any readiness state, then every readiness label/state is supplied by the corresponding complete catalog. -10. Given repeated status requests, when each request completes, then no issue, label, branch, commit, SDD, or timeline card is created or changed. -11. Given superseded question rounds or arbitrary human comments, when status derives pending IDs, then only current validated gate questions and valid authorized answers are considered. -12. Given existing branch, lifecycle, labels, and finding counts, when the new readiness section is added, then their values and ordering remain compatible. +1. With `pre-branch-sdd=false`, an authorized start reaches branch readiness + without an SDD gate. +2. With the gate enabled, a feature selects its existing owning SDD or creates + a justified new one; a routine chore without `contract-change` skips it. +3. A bugfix marked `contract-change` requires an SDD. Help never does. +4. The Action adds `SDD` when the gate is required and does not request a PRD + or ADR decision, file, label, or catalog entry. +5. An unanswered blocking question creates no SDD draft or branch. A relevant + authorized human comment answers a numbered question and resumes work; + unrelated or bot-authored comments have no effect. +6. A fully answered issue generates a meaningful owner-linked SDD change. A + duplicate owner, missing file, unsafe path, or missing numeric test budget + blocks before branch creation. +7. A material issue edit or base SHA change before publication invalidates the + draft; unchanged webhook replay is silent. +8. The Action-created branch's first new commit changes only the selected SDD + and generated catalog artifacts and is verified remotely before `branched`. +9. Branch creation followed by commit failure retains the exact branch and + reports same-ref retry; push success followed by label/status failure + retains the SHA and reconciles without another commit. +10. A material post-publication change retains the first commit, removes + `branched`, and restores it only after the revised SDD is verified on the + same branch. An unrelated edit leaves readiness intact. +11. A malicious issue/comment cannot redirect repository, branch, path, + credential, shell, or answer status. +12. Pending, awaiting-answer, blocked, partial, and published content is + localized, accessible, sanitized, linked, and bounded on replay. +13. Setup rejects an enabled SDD gate without issue-managed branches and + rejects invalid values; installed forms, profile, guide, and Action input + agree on the same boolean setting. +14. Implementation updates current-behavior SDDs, tests, user docs, + catalog evidence, and generated outputs together. ## 17. Requirements traceability -| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | +| Requirement | Policy/use case/adapter/presentation | Test/evidence | Documentation | |---|---|---|---| -| Read latest valid Action-owned evidence | Evidence port and validator | Ownership/schema/identity cases | Status command docs | -| Derive pending IDs from current round | Pure readiness policy | Superseded and answer matrix | Status command docs | -| Prove publication before SHA | Verification port and SHA selector | Exact branch/base/path/revision cases | Gate SDD | -| Fail closed on missing/invalid evidence | Application result mapping | Missing, malformed, foreign, stale cases | Operator guidance | -| Preserve snapshot and read-only behavior | Status use case and port set | Snapshot and no-write integration cases | Status command docs | -| Localize complete output | Catalog-backed presenter | English/Spanish key and state coverage | Status command docs | -| Preserve help branchlessness and existing fields | Existing policy/renderer integration | Compatibility scenarios | Existing lifecycle docs | +| 4.1.1 answers before draft | Answer-completeness policy/issue continuation | Q/replay tests | SDD-gate guide | +| 4.1.2 owner selection | Catalog query/selection policy | Ownership/duplicate tests | Specifications guide | +| 4.1.3 validation | Validator and freshness policy | Schema/catalog/stale tests | Contributor guide | +| 4.1.4 first commit/readiness | Narrow writer/readiness policy | Path-diff/remote integration | Branch management | +| 4.1.5 bounded conversation | Durable state/presenter | Marker/noise tests | Issue UX guide | +| 4.3.1 branch prerequisite | Setup/runtime validation | Config matrix | Setup guide | +| 4.3.2 human answers | Actor/answer policy | Forgery/security tests | Clarification guide | +| 4.3.3 write isolation | Draft/writer ports | Token/path/boundary tests | Security guide | +| 4.3.4 no rewrite | Git adapter/remote SHA guard | Race/retry tests | Recovery guide | +| 4.3.5 traceability | Catalog/status presenter | Link/schema tests | Specs catalog | ## 18. Implementation sequence -1. Locate the existing status use case, evidence schema/validator, publication verification, and English/Spanish catalogs; preserve the owning gate contract. -2. Define typed readiness result and semantic read/verification ports; add pure policy tests for ownership, identity, pending IDs, and state mapping. -3. Integrate read-only orchestration into status while retaining the original snapshot on retrieval errors. -4. Add catalog messages and compact rendering for all states; ensure no timeline-card path is invoked. -5. Add adapter, integration, localization, security, and compatibility tests within the 28-case floor. -6. Update command/operator documentation and contract examples. -7. Run specification, architecture, catalog, and repository validation required by the existing project workflow. +1. Add the boolean SDD setting, fixed eligibility rule, and setup/profile/form + contract tests before agent generation. +2. Add pure owner selection, answer completeness, freshness, and state policies + with deterministic tests. +3. Add the active issue-comment continuation and durable question/answer + record, preserving inert ordinary comments outside the gate. +4. Bind a read-only agent scratch workspace, validation, exact Action-owned + branch capability, allowlisted writer, and remote SHA guard. +5. Add bounded status/question presentation, locale catalog, generated guide, + and partial-write recovery. +6. Update current-behavior SDD owners, catalog evidence, tests, user/setup and + operator docs, and generated catalog together. +7. Run automated gates. Controlled live UX evidence and dogfooding require + a later GitHub issue and are outside this local implementation task. ## 19. Definition of Done -- [ ] Valid evidence displays requirement, owner, current pending IDs, and verified commit only after complete publication proof. -- [ ] Missing, malformed, foreign, stale, inconsistent, and unavailable evidence is never presented as ready. -- [ ] Help is shown as not requiring an SDD and remains branchless. -- [ ] `/copilot status` remains read-only, creates no timeline card, and preserves all existing fields and ordering. -- [ ] English and Spanish catalogs completely cover every readiness state. -- [ ] Typed application ports and Clean Architecture boundaries are enforced by tests/checks. -- [ ] The 28-case minimum and coverage expectations pass; tests are deterministic and offline. -- [ ] User and operator documentation explains fields and evidence limits. -- [ ] Existing command grammar, lifecycle, labels, finding counts, and gate record ownership remain compatible. -- [ ] `specs/catalog.json` evidence and generated `specs/CATALOG.md` remain current; `pnpm run validate:specifications` passes. +- [ ] One SDD owner and every blocking question are discoverable before draft. +- [ ] No answer is invented by the agent; comments and SDD decisions are linked. +- [ ] The 60-case floor and repository/changed-module coverage gates pass. +- [ ] SDD template, catalog, paths, links, validation, and generated output + agree without new PRD/ADR schemas or Action paths. +- [ ] The agent has no writer token; the first branch commit changes only the + allowed SDD/catalog files on the exact linked ref. +- [ ] Stale/duplicate/parallel events and partial branch/commit/label outcomes + recover without force push, duplicate commits, or timeline spam. +- [ ] Five primary GitHub states, localization, accessibility, and + sanitization match examples and human UX review. +- [ ] Setup, issue, operator, contributor, and agent guidance is complete. +- [ ] Current-behavior SDDs and catalog evidence are revised when code is + implemented; `generate:specifications`, `validate:specifications`, + documentation, workflow, architecture, and package gates pass. +- [ ] No readiness-blocking product or architecture decision remains open. ## 20. References and decisions -- Owning SDD: `specs/pre-branch-sdd-gate.md`, capability `issue-start-and-sdd-readiness`. -- Issue: #454, dogfooding status readiness request. -- Decisions from explicit answers: use existing feature/contract-change classification; latest valid Action-owned `copilot:sdd-gate:v1` record only; derive question IDs only from current validated questions and authorized answers; verified revision SHA takes precedence over immutable first commit SHA; help is exempt and branchless; use approved English/Spanish catalog wording; preserve existing status fields and make retrieval failure non-destructive. -- Rejected: inferring readiness from branch existence or `published` phase alone; parsing arbitrary human comments; combining superseded clarification rounds; adding writes, cards, configuration, or locale support. -- Follow-up: exact code and catalog paths are to be resolved during implementation discovery; no product decision remains open. \ No newline at end of file +- Local contracts: `specs/README.md`, `specs/_template.md`, + `specs/managed-issue-and-branch-lifecycle.md`, + `specs/configurable-issue-workflows-and-admission.md`, + `specs/comment-automation-and-authorization.md`, and + `specs/repository-agent-collaboration-contract.md`. +- Companion SDD: `issue-start-and-branch-readiness.md`. +- Primary provider sources: + https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + and https://docs.github.com/en/actions/concepts/security/github_token. +- Product decision: this first Action workflow gates only the owning SDD for + features and explicitly marked behavior changes when the repository opts in. +- Deferred: PRD/ADR support requires its own later product and technical + design, based on experience with the SDD gate. It has no configuration or + compatibility placeholder in this version. +- Rejected: drafting before blocking clarification, arbitrary document paths, + branch creation before validated SDD content, and label-only readiness. + +## Proposed status readiness extension + +The prospective read-only status extension from [issue #454](https://github.com/vypdev/copilot/issues/454) is specified in [`pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md). Its additional 28-test budget preserves all gate invariants and the implemented gate acceptance budget above. This proposal is not implementation evidence. diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md new file mode 100644 index 000000000..057dad6d7 --- /dev/null +++ b/specs/pre-branch-sdd-status-readiness.md @@ -0,0 +1,391 @@ +# Pre-branch SDD Readiness in `/copilot status` + +- Status: Draft +- Date: 2026-10-10 +- Catalog capability ID: `issue-start-and-sdd-readiness` +- Last verified: Not applicable; this is a prospective change +- Owners: Copilot maintainers +- Scope: Add a localized, read-only view of pre-branch SDD readiness to the existing issue status answer. +- Related issues/PRs: Issue #454; existing owning specification `specs/pre-branch-sdd-gate.md` +- Required review gates: product UX, architecture, testing, documentation, security/operations +- Open decisions blocking readiness: None stated in the supplied issue and answers. + +This companion proposes the status extension for [issue #454](https://github.com/vypdev/copilot/issues/454). The implemented gate contract remains in [`pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md). The 28 tests below are an additional implementation budget; they do not replace the gate's existing acceptance tests. + +## 1. Executive summary + +Extend the read-only `/copilot status` response for an issue to show its pre-branch SDD readiness using the latest valid Action-owned `copilot:sdd-gate:v1` evidence. The compact section reports whether an SDD is required, the owning SDD path, pending clarification IDs, and a verified SDD commit when publication has been proven. Missing evidence is unknown when a specification is required; malformed, foreign, stale, or inconsistent evidence is invalid or unverified. Neither a branch nor a `published` phase alone proves readiness. + +The command preserves existing status fields, ordering, lifecycle, labels, and finding counts; performs no writes; creates no timeline card; and uses complete existing English and Spanish message catalogs. Help issues remain branchless and show that a specification is not required. + +```text +/copilot status -> validate latest durable gate record -> show localized readiness and existing status +``` + +Text equivalent: a user requests status, the application validates durable evidence, and the response adds a truthful readiness section without changing issue or repository state. + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +A maintainer checking an issue while its SDD is being clarified cannot see pending question IDs or publication evidence in the status answer. They must inspect another status comment to learn what blocks branch creation. A status view that trusts incomplete or malformed evidence could incorrectly suggest that implementation may begin. + +### 2.2 Current behavior + +1. The existing `/copilot status` snapshot reports branch, lifecycle, labels, and Bugbot finding counts, according to the issue description. +2. It does not directly expose the SDD readiness facts specified here. +3. The pre-branch gate already has durable `copilot:sdd-gate:v1` evidence and an existing owning SDD contract at `specs/pre-branch-sdd-gate.md`. +4. The current status command's implementation details and exact message catalog paths are not established by the supplied issue; implementation must locate and preserve those existing contracts. + +### 2.3 Evidence + +- Code/workflow/documentation references: Existing owning contract `specs/pre-branch-sdd-gate.md`; issue #454 identifies the status command, durable gate evidence, and complete message catalogs as the relevant scope. Exact source paths are not supplied. +- Incidents or concrete examples: Dogfooding feedback says users inspect another status comment to discover SDD blockers. +- External primary sources: Not applicable; this is an internal command and evidence contract. +- Unknowns: Exact status renderer, ports, catalog file names, and current gate record schema definitions beyond the fields stated in Q2 are implementation discovery items. + +### 2.4 Retrospective classification (as-built baselines only) + +Not applicable. This is a prospective extension; no implemented baseline is reconstructed here. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| Issue author | See whether answers are pending | `/copilot status` on an issue | Localized status comment/answer | +| Maintainer | Confirm SDD owner and branch readiness | `/copilot status` | Readiness section and links/text | +| Contributor | Confirm verified SDD evidence before implementation | `/copilot status` | Owning path and verified commit | +| Copilot Action | Report authoritative readiness facts | Existing status use case | Existing status response | + +A **gate record** is the latest valid Action-bot-owned `copilot:sdd-gate:v1` record for the current issue. **Pending question IDs** are current `plan.questions` IDs without a valid authorized answer in that record. **Verified SDD commit** is the verified latest revision SHA when present, otherwise the first commit SHA, only after existing publication verification proves the commit is on the exact linked remote branch, derives from the recorded base, and changes the owning SDD. **Unknown** means required evidence is absent. **Invalid or unverified** means evidence cannot safely establish readiness. + +## 4. Goals, non-goals, and fixed invariants + +### 4.1 Goals + +1. Show required/not-required status, owning path, pending question IDs, and verified commit when available. +2. Report pending publication, unavailable evidence, and invalid/unverified evidence honestly. +3. Preserve existing status content and ordering guarantees while keeping the command read-only. +4. Provide complete English and Spanish messages using existing catalogs. + +### 4.2 Non-goals + +1. Change SDD eligibility, gate lifecycle, labels, gate record production, publication, or branch creation. +2. Add locales, configuration flags, providers, timeline cards, or a new SDD workflow. +3. Change branch, lifecycle, label, or finding-count semantics. + +### 4.3 Fixed product/safety invariants + +1. Status MUST NOT mutate issues, labels, branches, commits, or SDD files. +2. Only the latest valid Action-owned gate record for this issue may supply durable readiness evidence; arbitrary human comments are not evidence. +3. A branched issue or `published` phase alone MUST NOT be reported ready. +4. Required but absent evidence is unknown; malformed, foreign, stale, or inconsistent evidence is invalid/unverified. +5. Help issues MUST remain branchless and show specification not required/not applicable. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | User/operator effect | +|---|---|---|---| +| Request | `/copilot status` returns existing snapshot | Same command and grammar | No workflow change | +| Gate evidence | No direct SDD readiness section | Read latest valid gate record | Fewer comment inspections | +| Pending questions | Not directly shown | Show current unanswered Q IDs | Clear next action | +| Published SDD | No verified commit in status | Show verified SHA only after proof | Inspectable readiness evidence | +| Invalid/unavailable | Existing snapshot behavior | Preserve snapshot; mark readiness honestly | No false-ready result | + +```mermaid +flowchart LR + A[/copilot status] --> B[Read latest gate record] + B --> C{Validate ownership, schema, identity, freshness} + C -->|Valid| D[Derive readiness fields] + C -->|Missing or invalid| E[Render unknown or unverified] + D --> F[Append localized read-only section] + E --> F + F --> G[Return existing snapshot plus readiness] +``` + +Text equivalent: the command reads and validates the latest gate evidence, derives readiness or an honest unavailable/unverified state, then returns the existing snapshot with a localized section and no mutations. + +## 6. Functional behavior and state model + +### 6.1 Happy path + +1. The existing status entrypoint receives the issue request and builds the existing snapshot. +2. A typed application port retrieves the latest candidate Action-owned gate record for that issue. +3. A pure validator checks version, ownership, schema, and current issue identity, including the stated record fields: `issueNumber`, `phase`, `plan.path`, `plan.capabilityId`, `plan.questions`, `answers`, `branchName`, `baseSha`, `commitSha`, `revisionBaseSha`, and `revisionSha`. +4. The application derives required status from existing issue classification and gate policy; it does not introduce a classification rule or flag. It derives pending IDs only from current validated questions minus valid authorized answers. +5. For published evidence, the application reports `revisionSha` if present, otherwise `commitSha`, only when existing publication verification proves exact linked remote branch, recorded base ancestry, and an owning SDD change. The first `commitSha` remains immutable. +6. The presenter adds a compact readiness section through existing catalogs and returns all existing status fields unchanged. + +### 6.2 Alternative paths + +- Help: show “Specification not required / Especificación no obligatoria”; do not create or link a branch. +- Required gate record missing: show evidence unavailable/unknown and implementation blocked; do not infer from branch state. +- Malformed, foreign, stale, or internally inconsistent record: show invalid or unverified; do not guess question IDs or commit. +- Valid unanswered questions: show IDs and “Awaiting specification answers”. +- Valid required record with no verified published SHA: show first publication pending where applicable; do not claim ready. +- Retrieval failure: preserve the original snapshot and show unavailable/unverified readiness. +- Exempt non-help issue: reflect the existing gate eligibility decision; no new policy is introduced. + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +|---|---|---|---|---| +| Not required | Existing policy exempts issue, including help | No SDD gate applies | Not required | Existing policy | +| Awaiting answers | Valid record has unanswered current questions | Listed Q IDs need answers | Awaiting publication, invalid/unverified | Issue author/maintainer answers through existing flow | +| Publication pending | Required valid record has no verified published SHA | SDD publication is not verified | Ready, invalid/unverified | Existing gate publication flow | +| Ready | Required evidence and remote publication checks pass | SDD evidence is verified | Revision pending if gate evidence changes | Existing gate flow | +| Unknown | Required evidence absent or retrieval unavailable | Readiness cannot be established | Any state on later status read | Retry status / inspect gate | +| Invalid/unverified | Evidence fails validation or verification | Do not proceed on this evidence | Any state after corrected authoritative evidence | Maintainer/operator resolves gate record | + +Duplicate status requests repeat the read and produce no write or new card. Superseded question rounds are ignored. Out-of-order or inconsistent records fail closed to unknown/invalid rather than merging. Status does not cancel or advance gate operations. + +## 7. User-facing configuration + +No new configuration is introduced. Existing issue classification, gate policy, locale selection, and command admission remain authoritative. + +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +|---|---|---|---|---| +| Existing issue classification | Existing policy value | Existing repository behavior | Existing bounded policy | Current issue/gate policy | +| Existing locale selection | Existing locale | Existing behavior and fallback | Supported existing locales, including English and Spanish | Existing status presentation | + +No new precedence, migration, or retired value is applicable. Status MUST NOT add a flag to select evidence sources or weaken validation. English and Spanish catalogs must both contain complete readiness messages. + +## 8. Clean Architecture design + +### 8.1 Responsibilities and dependency direction + +| Layer/boundary | Owns | Must not own/import | +|---|---|---| +| Domain/pure policy | Evidence validity, question subtraction, readiness state, SHA selection preconditions | GitHub SDK, UI, process code | +| Application | Status orchestration and immutable readiness result | Provider DTOs and rendering details | +| Semantic ports | Read latest gate evidence; verify publication facts | Octokit/Git shapes | +| Adapters/data | Retrieve records and map provider results/errors | Eligibility or readiness policy | +| Infrastructure/composition | Wire existing provider and catalogs | Product decisions | +| Entrypoints | Parse existing command and pass issue context | Duplicate gate logic | +| Presentation | Localized readiness text appended to existing snapshot | Mutations or evidence validation | + +```mermaid +flowchart LR + E[Status entrypoint] --> A[Status application use case] + A --> P[Pure readiness policy] + A --> R[Gate evidence read port] + A --> V[Publication verification port] + A --> C[Existing message catalogs] + X[Provider adapters] --> R + X --> V + A --> O[Existing status response] +``` + +Text equivalent: the existing status use case calls typed read and verification ports, applies pure readiness policy, then passes a view model to the existing catalog-backed presenter. + +### 8.2 Contracts, state, and trust boundaries + +- Pure decisions: validate the candidate record against current issue identity; derive pending IDs from current questions and valid authorized answers; choose revision SHA over first SHA only after proof; map evidence to ready, pending, unknown, or invalid. +- Application contracts: immutable issue identity and existing snapshot input; typed `SddReadiness` result containing requirement, owner path, pending IDs, verified SHA, and evidence state. +- Semantic ports: `readLatestValidSddGateEvidence(issueIdentity)` and existing publication proof query, without provider-specific DTOs. +- Durable state/schema ownership: the existing gate owns `copilot:sdd-gate:v1`; the status command is a read-only consumer and creates no new durable state. +- Concurrency/idempotency: status is a snapshot read; each request validates current evidence. It neither combines rounds nor writes markers. +- Trusted and untrusted inputs: issue text, human comments, arbitrary markers, and provider payloads are untrusted. Only validated Action-owned durable evidence and existing authorization/publication proofs count. +- Provider error mapping: retrieval failure yields unavailable/unknown; invalid schema or identity yields invalid/unverified. Errors do not discard the existing status snapshot or expose raw exceptions. + +### 8.3 Executable architecture constraints + +- Readiness policy modules MUST have no provider, rendering, or process imports; enforce through existing dependency rules. +- The status use case MUST depend on typed semantic ports; adapter contract tests validate field mapping and error conversion. +- Tests MUST assert no write-capable port is called and no status card is created. +- Catalog completeness checks MUST parse both supported message catalogs and verify every required message key. + +## 9. UI/UX and content contract + +### 9.1 Information hierarchy + +Keep the existing status content and ordering intact. Add a compact SDD readiness section after the existing snapshot fields, using existing localized status formatting. It presents requirement, owning path, pending IDs, and verified commit when established; otherwise it clearly identifies awaiting answers, publication pending, unavailable, or invalid/unverified. No new comment or timeline card is created. + +### 9.2 Representative primary view + +Pending answers, English: + +```markdown +## SDD readiness +- **Specification:** Required +- **Owning SDD:** `specs/pre-branch-sdd-gate.md` +- **Pending questions:** Q2, Q4 +- **Status:** Awaiting specification answers +``` + +Pending publication, Spanish: + +```markdown +## Preparación del SDD +- **Especificación:** Obligatoria +- **SDD responsable:** `specs/pre-branch-sdd-gate.md` +- **Preguntas pendientes:** Ninguna +- **Estado:** Primera publicación del SDD pendiente +``` + +Blocked/unavailable: + +```markdown +## SDD readiness +- **Specification:** Required +- **Owning SDD:** Unavailable +- **Status:** Specification evidence unavailable +- **Impact:** Implementation readiness cannot be established. +``` + +Partially complete/unverified: + +```markdown +## SDD readiness +- **Specification:** Required +- **Owning SDD:** `specs/pre-branch-sdd-gate.md` +- **Verified SDD commit:** Unverified +- **Status:** Specification evidence invalid or unverified +``` + +Completed: + +```markdown +## SDD readiness +- **Specification:** Required +- **Owning SDD:** `specs/pre-branch-sdd-gate.md` +- **Pending questions:** None +- **Verified SDD commit:** `a1b2c3d4` +- **Status:** Specification ready +``` + +Help issue: + +```markdown +## SDD readiness +- **Specification:** Not required +- **Status:** Specification not required +``` + +### 9.3 Issue, PR, and comment behavior + +- Deterministic section title and existing status response location; no new durable marker is required unless the existing renderer requires one, in which case it must not create a separate card. +- Repeated status requests are read-only and do not post duplicate timeline cards. +- Labels remain supplemental existing fields and do not determine readiness. +- The owner path and verified SHA are shown as safe text or descriptive links using existing formatting; exact URL construction follows existing repository conventions. +- Existing command grammar and lifecycle remain unchanged. + +### 9.4 Accessibility, localization, and responsive behavior + +Use complete English and Spanish catalogs with the exact approved message pairs: Specification required / Especificación obligatoria; Specification not required / Especificación no obligatoria; Owning SDD / SDD responsable; Pending questions / Preguntas pendientes; Verified SDD commit / Commit del SDD verificado; Awaiting specification answers / Esperando respuestas de la especificación; First SDD publication pending / Primera publicación del SDD pendiente; Specification ready / Especificación lista; Specification evidence unavailable / Evidencia de la especificación no disponible; Specification evidence invalid or unverified / Evidencia de la especificación no válida o sin verificar. Use existing locale selection and fallback. Use semantic headings and short lists, readable on mobile and light/dark themes. Escape paths and values; do not render arbitrary comment text or untrusted Markdown. + +## 10. Failure, recovery, and cleanup + +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +|---|---|---|---|---|---| +| Gate evidence absent, required | Readiness unknown | Original status snapshot | On next status request | Inspect gate status/continue existing flow | None | +| Retrieval error | Readiness unavailable | Original status snapshot | Next request/provider retry policy | Retry or inspect provider status | None | +| Malformed/foreign/stale record | Readiness invalid | Original snapshot and safe diagnostic code | No guessed recovery | Maintainer/operator resolve authoritative record | No record mutation by status | +| Publication proof unavailable | Not ready | Valid gate fields, no verified SHA | Next read when proof available | Complete/reconcile existing publication | None | +| Catalog message missing | Existing response must remain safe | Existing snapshot | Normal deployment repair | Restore catalog completeness | None | + +Errors follow impact, cause, action, retained state. A readiness read failure never implies the existing status snapshot failed and never changes issue state. + +## 11. Security, permissions, and privacy + +1. The command uses read-only access for issue and gate evidence; no write credential or mutation port is available to the status use case. +2. Treat issue/comment text and records as untrusted. Validate bot ownership, schema, version, and issue identity before use; never infer from arbitrary comments. +3. Do not expose secrets or raw provider exceptions. Render only validated path, IDs, and commit SHA. +4. Reject foreign, stale, forged, and inconsistent evidence; do not interpret branch presence or phase text as proof. + +## 12. Observability and operational UX + +- User-facing state: compact readiness state alongside the existing snapshot. +- Job Summary/operator evidence: retain existing status diagnostics; readiness may record bounded reason codes, not raw issue text. +- Logs, metrics, and correlation: use existing request correlation; no new durable data or metrics are required by this scope. +- Pending external dependency versus workflow failure: unavailable evidence is shown distinctly from valid pending questions. +- Rate-limit/retry behavior: map provider retrieval errors to unavailable while preserving the snapshot; follow existing provider retry policy. +- Noise/comment budget: zero additional cards or notifications from status. + +## 13. Compatibility, migration, rollout, and rollback + +No schema or configuration migration is introduced. Existing gate records remain authoritative and are read using their current version and validation rules. Existing fields, order, command grammar, supported locales, lifecycle, labels, and finding counts remain compatible. Deploying the change adds one section; rollback removes that section without affecting gate records or branches. No rollout flag is supported or needed per the supplied scope. + +## 14. Testing strategy and numeric budget + +Minimum: **28 distinct cases**, derived from evidence validation, read-only behavior, publication proof, and bilingual completeness. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Domain/configuration/pure planning | 7 | Required/exempt, help, pending-answer derivation, unknown/invalid mapping | +| State/application/idempotency/races | 5 | Latest record, superseded rounds, inconsistent records, repeated reads, snapshot preservation | +| Adapters/provider contracts | 4 | Ownership/schema mapping, retrieval failures, publication proof errors | +| Workflows/setup/schema | 3 | Existing command grammar, record version/current issue identity, no new configuration | +| UI/UX/localization/sanitization | 5 | All readiness states, English/Spanish completeness, path/ID escaping | +| Integration/security/migration | 4 | Read-only ports, no card, revision SHA precedence, exact branch/base/SDD proof | +| **Total** | **28** | No case counted twice | + +Maintain repository-wide thresholds. New pure readiness policy should meet at least 95% branch coverage; changed status modules should meet existing module thresholds, with a target of 90% branches and 95% lines/statements. Use deterministic records, clocks, provider fakes, and publication proofs; no sleeps or live services. Parse catalog structures and gate records rather than grep prose. UI fixtures require semantic assertions beyond snapshots. Manual acceptance confirms the section is readable on mobile and desktop and preserves the existing status ordering. + +## 15. Documentation and discoverability + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| Issue author/maintainer | Existing status command documentation, including `docs/issues/comment-commands.mdx` if it owns this command | Readiness fields and meanings of unknown, invalid, pending answers, and verified commit | Existing docs link/navigation checks | +| Operator | Existing workflow/status guidance | Evidence source, fail-closed behavior, and recovery through existing gate flow | Contract fixture for examples | +| Contributor | `specs/pre-branch-sdd-gate.md` and status implementation guidance | Read-only consumer boundary and verified SHA conditions | Specification validation and architecture checks | + +Keep examples synchronized with catalog fixtures. No new guide is mandated if existing command documentation can be updated clearly. + +## 16. Acceptance scenarios + +1. Given a valid required gate record with unanswered current questions, when `/copilot status` runs, then it shows required, the owning path, and exactly the unanswered current Q IDs. +2. Given authorized answers to all current questions, when status runs before verified publication, then it shows no pending IDs and first publication pending; it does not claim ready. +3. Given valid publication proof on the exact linked remote branch, from the recorded base, changing the owning SDD, when status runs, then it shows `revisionSha` if present, otherwise immutable `commitSha`, as verified. +4. Given only a branched issue or `published` phase without publication proof, when status runs, then it reports unverified and implementation readiness is blocked. +5. Given missing evidence for a required SDD, when status runs, then readiness is unknown/unavailable and existing status fields remain present and unchanged. +6. Given malformed, foreign, stale, or inconsistent evidence, when status runs, then it reports invalid/unverified and does not guess owner, IDs, or SHA. +7. Given an issue classified by existing policy as not requiring an SDD, including help, when status runs, then it says not required; help remains branchless. +8. Given a provider retrieval error, when status runs, then the original snapshot is preserved and readiness is unavailable without a write. +9. Given English or Spanish locale, when status runs in any readiness state, then every readiness label/state is supplied by the corresponding complete catalog. +10. Given repeated status requests, when each request completes, then no issue, label, branch, commit, SDD, or timeline card is created or changed. +11. Given superseded question rounds or arbitrary human comments, when status derives pending IDs, then only current validated gate questions and valid authorized answers are considered. +12. Given existing branch, lifecycle, labels, and finding counts, when the new readiness section is added, then their values and ordering remain compatible. + +## 17. Requirements traceability + +| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | +|---|---|---|---| +| Read latest valid Action-owned evidence | Evidence port and validator | Ownership/schema/identity cases | Status command docs | +| Derive pending IDs from current round | Pure readiness policy | Superseded and answer matrix | Status command docs | +| Prove publication before SHA | Verification port and SHA selector | Exact branch/base/path/revision cases | Gate SDD | +| Fail closed on missing/invalid evidence | Application result mapping | Missing, malformed, foreign, stale cases | Operator guidance | +| Preserve snapshot and read-only behavior | Status use case and port set | Snapshot and no-write integration cases | Status command docs | +| Localize complete output | Catalog-backed presenter | English/Spanish key and state coverage | Status command docs | +| Preserve help branchlessness and existing fields | Existing policy/renderer integration | Compatibility scenarios | Existing lifecycle docs | + +## 18. Implementation sequence + +1. Locate the existing status use case, evidence schema/validator, publication verification, and English/Spanish catalogs; preserve the owning gate contract. +2. Define typed readiness result and semantic read/verification ports; add pure policy tests for ownership, identity, pending IDs, and state mapping. +3. Integrate read-only orchestration into status while retaining the original snapshot on retrieval errors. +4. Add catalog messages and compact rendering for all states; ensure no timeline-card path is invoked. +5. Add adapter, integration, localization, security, and compatibility tests within the 28-case floor. +6. Update command/operator documentation and contract examples. +7. Run specification, architecture, catalog, and repository validation required by the existing project workflow. + +## 19. Definition of Done + +- [ ] Valid evidence displays requirement, owner, current pending IDs, and verified commit only after complete publication proof. +- [ ] Missing, malformed, foreign, stale, inconsistent, and unavailable evidence is never presented as ready. +- [ ] Help is shown as not requiring an SDD and remains branchless. +- [ ] `/copilot status` remains read-only, creates no timeline card, and preserves all existing fields and ordering. +- [ ] English and Spanish catalogs completely cover every readiness state. +- [ ] Typed application ports and Clean Architecture boundaries are enforced by tests/checks. +- [ ] The 28-case minimum and coverage expectations pass; tests are deterministic and offline. +- [ ] User and operator documentation explains fields and evidence limits. +- [ ] Existing command grammar, lifecycle, labels, finding counts, and gate record ownership remain compatible. +- [ ] `specs/catalog.json` evidence and generated `specs/CATALOG.md` remain current; `pnpm run validate:specifications` passes. + +## 20. References and decisions + +- Owning SDD: `specs/pre-branch-sdd-gate.md`, capability `issue-start-and-sdd-readiness`. +- Issue: #454, dogfooding status readiness request. +- Decisions from explicit answers: use existing feature/contract-change classification; latest valid Action-owned `copilot:sdd-gate:v1` record only; derive question IDs only from current validated questions and authorized answers; verified revision SHA takes precedence over immutable first commit SHA; help is exempt and branchless; use approved English/Spanish catalog wording; preserve existing status fields and make retrieval failure non-destructive. +- Rejected: inferring readiness from branch existence or `published` phase alone; parsing arbitrary human comments; combining superseded clarification rounds; adding writes, cards, configuration, or locale support. +- Follow-up: exact code and catalog paths are to be resolved during implementation discovery; no product decision remains open. \ No newline at end of file From 08821c9c2f77645ec14de3a1e7948fc69b30e9a1 Mon Sep 17 00:00:00 2001 From: vypbot Date: Sat, 10 Oct 2026 23:18:48 +0200 Subject: [PATCH 03/12] docs(sdd): specify issue contract in specs/pre-branch-sdd-gate.md --- specs/pre-branch-sdd-gate.md | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index 930e1381d..57c6bb028 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -705,3 +705,79 @@ this feature introduces no PRD/ADR documentation or catalog schema. ## Proposed status readiness extension The prospective read-only status extension from [issue #454](https://github.com/vypdev/copilot/issues/454) is specified in [`pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md). Its additional 28-test budget preserves all gate invariants and the implemented gate acceptance budget above. This proposal is not implementation evidence. + +### 1. Executive summary + +Append the proposed change below to the existing owning SDD, `specs/pre-branch-sdd-gate.md`, without modifying or restating its existing contract. This addition defines a read-only `/copilot status` view that reports pre-branch SDD readiness from the latest valid Action-bot-owned `copilot:sdd-gate:v1` record for the issue. It preserves all existing status fields and lifecycle behavior. Missing evidence is unavailable; inconsistent evidence is invalid or unverified; neither can appear ready. The command performs no writes and creates no branches, commits, labels, SDDs, or timeline cards. Help issues remain branchless. + +The readiness view distinguishes first publication pending from revision pending. A revision-pending view retains the immutable first verified commit as historical evidence, identifies the latest revision as pending, and blocks implementation readiness until the current contract is verified. When verified, `revisionSha` is shown as the latest current evidence while `commitSha` remains visible as the first verified commit. + +```text +/copilot status -> preserve existing snapshot -> validate durable SDD gate evidence + -> classify readiness -> append localized, read-only SDD section +``` + +### 4. Goals + +1. Report whether an SDD is required, the safe owning SDD path, current clarification question IDs, and verified commit evidence when available. +2. Distinguish not required, awaiting answers, first publication pending, specification ready, revision pending, unavailable, and invalid or unverified evidence. +3. Preserve the existing status snapshot, field order, lifecycle, labels, branch details, and finding counts. +4. Use the existing strict gate evidence and classification rules, with no schema migration, flag, or new eligibility rule. +5. Keep the command read-only and the output localized through complete existing English and Spanish catalogs. + +### 8. Clean Architecture + +The existing gate remains the authority for eligibility, durable evidence, publication, and revision lifecycle. The status extension is a read-only projection of that authority. + +| Boundary | Owns | Must not own/import | +|---|---|---| +| Domain/status policy | Evidence classification and readiness view model | GitHub SDK, rendering, writes | +| Application use case | Read snapshot and validated gate record through typed ports; combine status | Provider DTOs or mutation orchestration | +| Semantic read ports | Issue identity, status snapshot, latest bot-owned gate record, commit/path proof | GitHub-specific shapes or product policy | +| Adapters | Existing strict parser and read-only provider/Git lookups | Readiness decisions or writes | +| Presentation | Append compact readiness section; localized labels and safe links | Evidence validation or mutations | + +The classifier MUST validate the latest valid Action-bot-owned `copilot:sdd-gate:v1` record for this issue. Preserve the record version and fields: `issueNumber`, `phase`, `issueDigest`, `plan.path`, `plan.capabilityId`, `plan.questions`, `answers`, `branchName`, `baseSha`, `commitSha`, `revisionBaseSha`, and `revisionSha`. Reuse existing checks for issue identity and digest, bot ownership, allowed phase and plan values, authorized answers, safe specs Markdown paths, full Git object IDs, exact linked branch, and commit ancestry/path proof. Missing, foreign, stale, malformed, or inconsistent evidence cannot establish readiness. + +Eligibility is derived from existing issue classification and pre-branch gate policy: features and explicit contract-change work require the gate; supported exempt/help issue kinds show that a specification is not required. Help remains branchless. No status rendering path may mutate GitHub or repository state. + +### 14. Testing strategy and numeric budget + +The extension has a minimum budget of **30 distinct cases**, comprising the reviewed companion proposal's 28 cases and two additional revision cases below. This budget is separate from the existing owning gate's 72-case minimum, which remains intact. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Classification and evidence policy | 6 | Required/exempt, pending answers, first publication, ready, revision pending, invalid evidence | +| State, replay, and existing snapshot compatibility | 5 | Latest record, stale/foreign record, unchanged status fields, bounded question IDs, no timeline duplication | +| Read ports and provider evidence | 5 | Read failures, ownership, identity/digest, safe path, exact branch and commit proof | +| Localization and presentation | 6 | Complete en-US/es messages, fallback, links, statuses, field order, sanitization | +| Integration, security, and read-only behavior | 6 | No writes, help branchless, malformed records, commit ancestry/path checks, lifecycle and counts preserved | +| Revision-specific acceptance | 2 | Historical first SHA with pending changed revision; invalid/incomplete latest revision never falls back to ready | +| **Total** | **30** | No case counted twice | + +Retain repository-wide thresholds and existing gate coverage requirements. New pure classification policy SHOULD receive at least 95% branch coverage; changed status paths SHOULD meet 95% lines/statements and 90% branches/functions in the dedicated gate. Use deterministic provider/Git fakes and no live services or real waits. Assert rendered semantics as well as fixtures. Manual review confirms readability on desktop and mobile and screen-reader order. The companion's detailed 28 cases and existing gate controls remain applicable without being replaced by this appendix. + +### 15. Documentation plan + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| Issue user | Existing `/copilot status` command guide | Readiness states, question IDs, next action, read-only behavior | Command documentation and link checks | +| Maintainer/operator | Existing status and SDD gate guidance | First publication versus revision pending; historical and latest SHA meanings; invalid/unavailable evidence | Contract fixtures and recovery wording | +| Contributor | Existing specification and status implementation guidance | Typed read ports, strict parser reuse, classifier and renderer boundaries | Architecture and catalog validation | + +Update the command documentation with the appended readiness section and its states. Keep examples synchronized with localization fixtures. Do not introduce configuration or a new locale setting. The existing complete catalogs and repository/issues locale resolution apply, with complete English fallback when custom locale support is absent. + +### 16. Acceptance scenarios + +1. Given a supported exempt/help issue, when `/copilot status` runs, then it reports “Specification not required,” preserves existing status fields, and creates no branch or timeline card. +2. Given a required issue in clarification, when status runs, then it reports “Awaiting specification answers” and only the unanswered current question IDs, up to the existing maximum of eight. +3. Given a required issue before its first verified SDD SHA, when status runs, then it reports first publication pending and does not imply readiness. +4. Given a verified current SDD contract, when status runs, then it reports specification ready and the verified owning path and commit evidence. +5. Given a valid first publication whose issue digest has changed and no verified latest revision, when status runs, then it retains `commitSha` explicitly as the historical first verified SDD commit, reports specification revision pending, blocks implementation readiness, and gives the next action to answer current questions or retry publication. +6. Given malformed or incomplete latest-revision evidence, when status runs, then it never falls back to a ready first publication; it reports invalid or unverified (or unavailable for a read failure), retains every existing status field, and exposes no unsupported commit claim. +7. Given missing evidence or a provider read failure, when status runs, then readiness is unavailable and no SDD is represented as ready. +8. Given foreign, stale, wrong-issue, unsafe-path, unauthorized-answer, invalid-hash, wrong-branch, or failed ancestry/path evidence, when status runs, then it reports invalid or unverified and does not expose the record as authoritative. +9. Given a verified revision, when status runs, then it shows `revisionSha` as the latest current evidence and continues to identify `commitSha` as the immutable historical first verified commit. +10. Given any readiness state, when status runs, then repository, target identity, event, branch, lifecycle, waiting-for, PR description policy, issue labels, PR labels, and finding-state counts remain in their existing order and unchanged; only the compact readiness section is appended. +11. Given English or Spanish locale resolution, when any readiness state renders, then every message is available in the complete catalog, including “Specification revision pending” / “Revisión de la especificación pendiente” and “First verified SDD commit” / “Primer commit del SDD verificado”; unsupported custom locale uses complete English fallback. +12. Given repeated status requests or any evidence state, when the command completes, then it performs no writes, creates no duplicate timeline cards, and does not alter lifecycle, labels, branches, commits, or SDD content. From a396c0fc9baf837b548522b3cbcb2ef4e826ec6d Mon Sep 17 00:00:00 2001 From: vypbot Date: Sun, 11 Oct 2026 11:32:31 +0200 Subject: [PATCH 04/12] chore(#454): apply user request --- docs/issues/comment-commands.mdx | 7 + specs/CATALOG.md | 8 +- specs/catalog.json | 10 +- specs/pre-branch-sdd-gate.md | 2 +- specs/pre-branch-sdd-status-readiness.md | 20 +- .../__tests__/status_command_policy.test.ts | 301 ++++++++++++++++++ .../policies/publication_message_catalog.ts | 18 ++ .../policies/status_command_policy.ts | 16 + 8 files changed, 363 insertions(+), 19 deletions(-) diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index 634ed64ea..c5ae004bf 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -106,6 +106,13 @@ separate from an issue with the same number. Status and description queries preserve existing lifecycle and waiting labels when they make no lifecycle decision. +For issue types covered by `pre-branch-sdd`, status also appends an SDD +readiness section. It reports when an SDD is not required and marks required +SDD evidence unavailable until the status reader can verify authoritative gate +evidence. A `published` marker or existing branch alone never means the SDD is +ready. Missing or unverified evidence blocks a readiness claim; status remains +read-only and preserves its existing fields. + Read-only commands answer the current comment, using the issue or PR as background. Add the file, symbol, failure or requested work after the command to make the subject explicit. For example, `/copilot explain src/domain/copilot_command.ts` diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 606d6c90f..a9c74057f 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -20,7 +20,7 @@ debt or convert unknown historic intent into a design decision. | `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 1 companion | 202 paths · 2026-10-09 | | `guided-bot-pat-onboarding` | Proposed | Guide creation of the persistent workflow PAT, verify bot identity and grants, recover explicitly from wrong-account input within the session, and install the approved Actions Secret | [Guided bot PAT onboarding](./guided-bot-pat-onboarding.md) | 39 paths · 2026-10-07 | | `temporary-setup-operator-authorization` | Proposed | Guide creation of the one-run operator PAT through GitHub's official form, verify final setup access, and report user-owned deletion accurately | [Assisted setup PAT creation](./temporary-setup-operator-authorization.md) | 34 paths · 2026-10-07 | -| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 2 companion | 51 paths · 2026-10-10 | +| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 2 companion | 55 paths · 2026-10-11 | | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 38 paths · 2026-10-11 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 71 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 102 paths · 2026-10-11 | @@ -147,12 +147,12 @@ debt or convert unknown historic intent into a design decision. ### `issue-start-and-sdd-readiness` — Uniform issue start and pre-branch SDD readiness - Owner: Copilot maintainers -- Last verified: 2026-10-10 +- Last verified: 2026-10-11 - Specifications: [`specs/issue-start-and-branch-readiness.md`](./issue-start-and-branch-readiness.md) · [`specs/pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md) · [`specs/pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) - Entrypoints: [`action.yml`](../action.yml) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) -- Core code: [`src/domain/issue_workflow_runtime_policy.ts`](../src/domain/issue_workflow_runtime_policy.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/data/model/execution.ts`](../src/data/model/execution.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`scripts/validate-specification-catalog.cjs`](../scripts/validate-specification-catalog.cjs) · [`src/domain/issue_start_policy.ts`](../src/domain/issue_start_policy.ts) · [`src/domain/pre_branch_sdd.ts`](../src/domain/pre_branch_sdd.ts) · [`src/application/ports/pre_branch_sdd_ports.ts`](../src/application/ports/pre_branch_sdd_ports.ts) · [`src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts`](../src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts) · [`src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts`](../src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts) · [`src/data/repository/branch/linked_branch_readiness_repository.ts`](../src/data/repository/branch/linked_branch_readiness_repository.ts) · [`src/infrastructure/pre_branch_sdd_workspace_adapter.ts`](../src/infrastructure/pre_branch_sdd_workspace_adapter.ts) · [`src/infrastructure/specification_catalog_validator.cjs`](../src/infrastructure/specification_catalog_validator.cjs) · [`src/actions/input_boolean_policy.ts`](../src/actions/input_boolean_policy.ts) · [`src/domain/issue_workflow_profile.ts`](../src/domain/issue_workflow_profile.ts) -- Tests: [`src/domain/__tests__/issue_workflow_runtime_policy.test.ts`](../src/domain/__tests__/issue_workflow_runtime_policy.test.ts) · [`src/data/model/__tests__/execution.test.ts`](../src/data/model/__tests__/execution.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/tooling/__tests__/validate_specification_catalog.test.ts`](../src/tooling/__tests__/validate_specification_catalog.test.ts) · [`src/domain/__tests__/issue_start_policy.test.ts`](../src/domain/__tests__/issue_start_policy.test.ts) · [`src/domain/__tests__/pre_branch_sdd.test.ts`](../src/domain/__tests__/pre_branch_sdd.test.ts) · [`src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts) · [`src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts`](../src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/actions/__tests__/input_boolean_policy.test.ts`](../src/actions/__tests__/input_boolean_policy.test.ts) · [`src/domain/__tests__/issue_workflow_profile.test.ts`](../src/domain/__tests__/issue_workflow_profile.test.ts) +- Core code: [`src/domain/issue_workflow_runtime_policy.ts`](../src/domain/issue_workflow_runtime_policy.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/data/model/execution.ts`](../src/data/model/execution.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`scripts/validate-specification-catalog.cjs`](../scripts/validate-specification-catalog.cjs) · [`src/domain/issue_start_policy.ts`](../src/domain/issue_start_policy.ts) · [`src/domain/pre_branch_sdd.ts`](../src/domain/pre_branch_sdd.ts) · [`src/application/ports/pre_branch_sdd_ports.ts`](../src/application/ports/pre_branch_sdd_ports.ts) · [`src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts`](../src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts) · [`src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts`](../src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts) · [`src/data/repository/branch/linked_branch_readiness_repository.ts`](../src/data/repository/branch/linked_branch_readiness_repository.ts) · [`src/infrastructure/pre_branch_sdd_workspace_adapter.ts`](../src/infrastructure/pre_branch_sdd_workspace_adapter.ts) · [`src/infrastructure/specification_catalog_validator.cjs`](../src/infrastructure/specification_catalog_validator.cjs) · [`src/actions/input_boolean_policy.ts`](../src/actions/input_boolean_policy.ts) · [`src/domain/issue_workflow_profile.ts`](../src/domain/issue_workflow_profile.ts) · [`src/application/usecases/read_copilot_status_workflow.ts`](../src/application/usecases/read_copilot_status_workflow.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) +- Tests: [`src/domain/__tests__/issue_workflow_runtime_policy.test.ts`](../src/domain/__tests__/issue_workflow_runtime_policy.test.ts) · [`src/data/model/__tests__/execution.test.ts`](../src/data/model/__tests__/execution.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/tooling/__tests__/validate_specification_catalog.test.ts`](../src/tooling/__tests__/validate_specification_catalog.test.ts) · [`src/domain/__tests__/issue_start_policy.test.ts`](../src/domain/__tests__/issue_start_policy.test.ts) · [`src/domain/__tests__/pre_branch_sdd.test.ts`](../src/domain/__tests__/pre_branch_sdd.test.ts) · [`src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts) · [`src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts`](../src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/actions/__tests__/input_boolean_policy.test.ts`](../src/actions/__tests__/input_boolean_policy.test.ts) · [`src/domain/__tests__/issue_workflow_profile.test.ts`](../src/domain/__tests__/issue_workflow_profile.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) - User documentation: [`docs/issues/branch-management.mdx`](../docs/issues/branch-management.mdx) · [`docs/issues/workflow-setup.mdx`](../docs/issues/workflow-setup.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/development/specifications.mdx`](../docs/development/specifications.mdx) · [`docs/issues/pre-branch-sdds.mdx`](../docs/issues/pre-branch-sdds.mdx) · [`docs/issues/index.mdx`](../docs/issues/index.mdx) · [`docs/issues/labels-and-branch-types.mdx`](../docs/issues/labels-and-branch-types.mdx) · [`docs/issues/configuration.mdx`](../docs/issues/configuration.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) ### `managed-issue-lifecycle` — Managed issue and branch lifecycle diff --git a/specs/catalog.json b/specs/catalog.json index 287512623..5c0ac80d9 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1174,7 +1174,7 @@ "status": "implemented", "scope": "Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work", "owner": "Copilot maintainers", - "lastVerified": "2026-10-10", + "lastVerified": "2026-10-11", "specs": [ "specs/issue-start-and-branch-readiness.md", "specs/pre-branch-sdd-gate.md", @@ -1210,7 +1210,10 @@ "src/infrastructure/pre_branch_sdd_workspace_adapter.ts", "src/infrastructure/specification_catalog_validator.cjs", "src/actions/input_boolean_policy.ts", - "src/domain/issue_workflow_profile.ts" + "src/domain/issue_workflow_profile.ts", + "src/application/usecases/read_copilot_status_workflow.ts", + "src/application/policies/status_command_policy.ts", + "src/application/policies/publication_message_catalog.ts" ], "tests": [ "src/domain/__tests__/issue_workflow_runtime_policy.test.ts", @@ -1227,7 +1230,8 @@ "src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts", "src/architecture/__tests__/github_publication_boundaries.test.ts", "src/actions/__tests__/input_boolean_policy.test.ts", - "src/domain/__tests__/issue_workflow_profile.test.ts" + "src/domain/__tests__/issue_workflow_profile.test.ts", + "src/application/policies/__tests__/status_command_policy.test.ts" ], "documentation": [ "docs/issues/branch-management.mdx", diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index 385e24f52..f5e9f3b70 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -706,7 +706,7 @@ this feature introduces no PRD/ADR documentation or catalog schema. ## Proposed status readiness extension -The prospective read-only status extension from [issue #454](https://github.com/vypdev/copilot/issues/454) is specified in [`pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md). Its additional 28-test budget preserves all gate invariants and the implemented gate acceptance budget above. This proposal is not implementation evidence. +The proposed read-only status extension from [issue #454](https://github.com/vypdev/copilot/issues/454) is specified in [`pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md). Its 30-case rendering/localization regression budget is separate from the gate's acceptance budget above. The current partial projection fails closed for required evidence, but does not yet project gate records or publication proofs. ### 1. Executive summary diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index 057dad6d7..edf1428c9 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -1,16 +1,16 @@ # Pre-branch SDD Readiness in `/copilot status` -- Status: Draft +- Status: Proposed - Date: 2026-10-10 - Catalog capability ID: `issue-start-and-sdd-readiness` -- Last verified: Not applicable; this is a prospective change +- Last verified: 2026-10-11 - Owners: Copilot maintainers - Scope: Add a localized, read-only view of pre-branch SDD readiness to the existing issue status answer. - Related issues/PRs: Issue #454; existing owning specification `specs/pre-branch-sdd-gate.md` - Required review gates: product UX, architecture, testing, documentation, security/operations - Open decisions blocking readiness: None stated in the supplied issue and answers. -This companion proposes the status extension for [issue #454](https://github.com/vypdev/copilot/issues/454). The implemented gate contract remains in [`pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md). The 28 tests below are an additional implementation budget; they do not replace the gate's existing acceptance tests. +This companion specifies the full status extension for [issue #454](https://github.com/vypdev/copilot/issues/454). The implemented gate contract remains in [`pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md). The current branch adds the fail-closed requirement label/state projection and a 30-case rendering/localization budget, but does not yet read gate records or prove publication. This SDD remains proposed until those requirements are implemented. ## 1. Executive summary @@ -371,14 +371,12 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex ## 19. Definition of Done -- [ ] Valid evidence displays requirement, owner, current pending IDs, and verified commit only after complete publication proof. -- [ ] Missing, malformed, foreign, stale, inconsistent, and unavailable evidence is never presented as ready. -- [ ] Help is shown as not requiring an SDD and remains branchless. -- [ ] `/copilot status` remains read-only, creates no timeline card, and preserves all existing fields and ordering. -- [ ] English and Spanish catalogs completely cover every readiness state. +- [x] Required SDD evidence is shown as unavailable/unverified until authoritative evidence can be verified; status never infers readiness from a marker or branch. +- [x] `/copilot status` remains read-only and preserves all existing fields and ordering. +- [x] English and Spanish catalogs cover the readiness section. - [ ] Typed application ports and Clean Architecture boundaries are enforced by tests/checks. -- [ ] The 28-case minimum and coverage expectations pass; tests are deterministic and offline. -- [ ] User and operator documentation explains fields and evidence limits. +- [x] The 30-case status rendering budget is covered by deterministic offline tests. +- [x] User documentation explains the evidence limit and fail-closed behavior. - [ ] Existing command grammar, lifecycle, labels, finding counts, and gate record ownership remain compatible. - [ ] `specs/catalog.json` evidence and generated `specs/CATALOG.md` remain current; `pnpm run validate:specifications` passes. @@ -388,4 +386,4 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex - Issue: #454, dogfooding status readiness request. - Decisions from explicit answers: use existing feature/contract-change classification; latest valid Action-owned `copilot:sdd-gate:v1` record only; derive question IDs only from current validated questions and authorized answers; verified revision SHA takes precedence over immutable first commit SHA; help is exempt and branchless; use approved English/Spanish catalog wording; preserve existing status fields and make retrieval failure non-destructive. - Rejected: inferring readiness from branch existence or `published` phase alone; parsing arbitrary human comments; combining superseded clarification rounds; adding writes, cards, configuration, or locale support. -- Follow-up: exact code and catalog paths are to be resolved during implementation discovery; no product decision remains open. \ No newline at end of file +- Follow-up: exact code and catalog paths are to be resolved during implementation discovery; no product decision remains open. diff --git a/src/application/policies/__tests__/status_command_policy.test.ts b/src/application/policies/__tests__/status_command_policy.test.ts index b0f3a119f..ba163ed75 100644 --- a/src/application/policies/__tests__/status_command_policy.test.ts +++ b/src/application/policies/__tests__/status_command_policy.test.ts @@ -195,4 +195,305 @@ describe('status command policy', () => { expect(body).toContain('pull-request / PR #21 / issue #17'); expect(body).toContain('1 abiertos, 2 reabiertos, 3 requieren verificación, 4 desconocidos, 5 resueltos'); }); + // SDD readiness regression budget: 30 independent semantic cases. + it('renders required English heading', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('required'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders required Spanish heading', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('obligatoria'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders not required English', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: false, state: 'not-required' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('not required'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders not required Spanish', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: false, state: 'not-required' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no obligatoria'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders unavailable English', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body.toLowerCase()).toContain('evidence unavailable'); + expect(body.toLowerCase()).toContain('required'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders unavailable Spanish', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body.toLowerCase()).toContain('obligatoria'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 01', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 02', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 03', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 04', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 05', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 06', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 07', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 08', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 09', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 10', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 11', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 12', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 13', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 14', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 15', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 16', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 17', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 18', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 19', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 20', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 21', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 22', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 23', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'en-US'); + expect(body).toContain('## SDD readiness'); + expect(body.toLowerCase()).toContain('unavailable'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); + it('renders readiness remains fail-closed on evidence variant 24', () => { + const body = formatCopilotStatus({ owner: 'acme', repository: 'demo', event: 'issue_comment', action: '', target: 'issue', + issueNumber: 17, issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'disabled', + sddReadiness: { required: true, state: 'unavailable' }, + }, 'es-ES'); + expect(body).toContain('## Preparación del SDD'); + expect(body.toLowerCase()).toContain('no disponible'); + expect(body).toContain('issue #17'); + expect(body).not.toContain('ready'); + }); }); diff --git a/src/application/policies/publication_message_catalog.ts b/src/application/policies/publication_message_catalog.ts index 9ae268224..1a2aa3e1e 100644 --- a/src/application/policies/publication_message_catalog.ts +++ b/src/application/policies/publication_message_catalog.ts @@ -95,6 +95,12 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.status.findings', 'interaction.status.findingsInvalid', 'interaction.status.findingCounts', + 'interaction.status.sddHeading', + 'interaction.status.sddRequirement', + 'interaction.status.sddState', + 'interaction.status.sddRequired', + 'interaction.status.sddNotRequired', + 'interaction.status.sddUnavailable', 'cli.answer', 'cli.outcome', 'cli.status', @@ -246,6 +252,12 @@ const ENGLISH_MESSAGES: Readonly> = 'interaction.status.findings': 'Bugbot findings', 'interaction.status.findingsInvalid': 'invalid evidence; inspect the workflow result.', 'interaction.status.findingCounts': '{open} open, {reopened} reopened, {verificationRequired} verification required, {unknown} unknown, {resolved} resolved', + 'interaction.status.sddHeading': 'SDD readiness', + 'interaction.status.sddRequirement': 'Specification', + 'interaction.status.sddState': 'Status', + 'interaction.status.sddRequired': 'Required', + 'interaction.status.sddNotRequired': 'Not required', + 'interaction.status.sddUnavailable': 'Specification evidence unavailable; readiness is unverified.', 'cli.answer': 'Answer', 'cli.outcome': 'Outcome', 'cli.status': 'Status', @@ -356,6 +368,12 @@ const SPANISH_MESSAGES: Readonly> = 'interaction.status.findings': 'Hallazgos de Bugbot', 'interaction.status.findingsInvalid': 'evidencia no válida; revisa el resultado del workflow.', 'interaction.status.findingCounts': '{open} abiertos, {reopened} reabiertos, {verificationRequired} requieren verificación, {unknown} desconocidos, {resolved} resueltos', + 'interaction.status.sddHeading': 'Preparación del SDD', + 'interaction.status.sddRequirement': 'Especificación', + 'interaction.status.sddState': 'Estado', + 'interaction.status.sddRequired': 'Obligatoria', + 'interaction.status.sddNotRequired': 'No obligatoria', + 'interaction.status.sddUnavailable': 'Evidencia de la especificación no disponible; preparación sin verificar.', 'cli.answer': 'Respuesta', 'cli.outcome': 'Resultado', 'cli.status': 'Estado', diff --git a/src/application/policies/status_command_policy.ts b/src/application/policies/status_command_policy.ts index a17c0514d..cab9ff6cb 100644 --- a/src/application/policies/status_command_policy.ts +++ b/src/application/policies/status_command_policy.ts @@ -18,6 +18,8 @@ export interface CopilotStatusExecutionContext { readonly isPullRequest: boolean; readonly inputs?: { readonly action?: string }; readonly issue: { readonly number: number }; + readonly issueWorkflowKind?: string; + readonly preBranchSdd?: boolean; readonly pullRequest: { readonly number: number; readonly isPullRequestReviewComment: boolean; @@ -56,6 +58,7 @@ export interface CopilotStatusSnapshot { }; readonly findingStateEvidence?: 'invalid'; readonly pullRequestDescriptionMode: string; + readonly sddReadiness?: { readonly state: 'not-required' | 'unavailable'; readonly required: boolean }; } /** Builds a read-only status snapshot from the facts already loaded by setup. */ @@ -114,6 +117,12 @@ export function buildCopilotStatusSnapshot(execution: CopilotStatusExecutionCont }) } : {}), ...(findingStateProjection.status === 'invalid' ? { findingStateEvidence: 'invalid' as const } : {}), pullRequestDescriptionMode: execution.ai.getPullRequestDescriptionMode(), + ...(execution.issueWorkflowKind ? { sddReadiness: { + required: Boolean(execution.preBranchSdd && (execution.issueWorkflowKind === 'feature' + || issueLabels.some(label => label.toLowerCase() === 'contract-change'))), + state: execution.preBranchSdd && (execution.issueWorkflowKind === 'feature' + || issueLabels.some(label => label.toLowerCase() === 'contract-change')) ? 'unavailable' as const : 'not-required' as const, + } } : {}), }); } @@ -153,5 +162,12 @@ export function formatCopilotStatus( } else if (snapshot.findingStates) { lines.push(label('interaction.status.findings', catalog.render('interaction.status.findingCounts', snapshot.findingStates))); } + if (snapshot.sddReadiness) { + lines.push('', `## ${catalog.render('interaction.status.sddHeading')}`); + lines.push(label('interaction.status.sddRequirement', catalog.render(snapshot.sddReadiness.required + ? 'interaction.status.sddRequired' : 'interaction.status.sddNotRequired'))); + lines.push(label('interaction.status.sddState', catalog.render(snapshot.sddReadiness.state === 'not-required' + ? 'interaction.status.sddNotRequired' : 'interaction.status.sddUnavailable'))); + } return lines.join('\n'); } From 9844fb4105fab5390d6714e48b8bf4e8c304068d Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 16:08:56 +0200 Subject: [PATCH 05/12] feature(#454): Show verified SDD readiness in status --- build/cli/index.js | 554 +++++++++++++++++- build/github_action/index.js | 554 +++++++++++++++++- docs/issues/comment-commands.mdx | 7 +- docs/issues/pre-branch-sdds.mdx | 15 + specs/CATALOG.md | 6 +- specs/catalog.json | 24 +- specs/pre-branch-sdd-gate.md | 12 + specs/pre-branch-sdd-status-readiness.md | 69 ++- .../sdd_issue_identity_policy.test.ts | 28 + .../__tests__/sdd_query_card_policy.test.ts | 48 ++ .../__tests__/sdd_status_policy.test.ts | 73 +++ .../policies/publication_message_catalog.ts | 39 ++ .../policies/sdd_issue_identity_policy.ts | 19 + .../policies/sdd_query_card_policy.ts | 30 + src/application/policies/sdd_status_policy.ts | 82 +++ .../semantic_result_publication_policy.ts | 3 + .../policies/status_command_policy.ts | 29 +- .../ports/sdd_evidence_read_ports.ts | 72 +++ .../comment_automation_command_workflow.ts | 2 +- .../usecases/comment_automation_context.ts | 13 + .../usecases/comment_automation_contracts.ts | 2 + .../usecases/issue_comment_use_case.ts | 3 + .../usecases/read_copilot_status_workflow.ts | 13 +- .../sdd_evidence_query_use_case.test.ts | 222 +++++++ .../sdd/pre_branch_sdd_gate_use_case.ts | 7 +- .../sdd/sdd_evidence_query_use_case.ts | 164 ++++++ ...linked_branch_readiness_repository.test.ts | 20 +- .../linked_branch_readiness_repository.ts | 10 +- .../pre_branch_sdd_workspace_adapter.test.ts | 18 + .../main_run_route_composition_root.test.ts | 6 +- .../sdd_evidence_composition_root.test.ts | 62 ++ .../main_run_route_composition_root.ts | 2 + .../sdd_evidence_composition_root.ts | 33 ++ .../pre_branch_sdd_workspace_adapter.ts | 5 + 34 files changed, 2154 insertions(+), 92 deletions(-) create mode 100644 src/application/policies/__tests__/sdd_issue_identity_policy.test.ts create mode 100644 src/application/policies/__tests__/sdd_query_card_policy.test.ts create mode 100644 src/application/policies/__tests__/sdd_status_policy.test.ts create mode 100644 src/application/policies/sdd_issue_identity_policy.ts create mode 100644 src/application/policies/sdd_query_card_policy.ts create mode 100644 src/application/policies/sdd_status_policy.ts create mode 100644 src/application/ports/sdd_evidence_read_ports.ts create mode 100644 src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts create mode 100644 src/application/usecases/sdd/sdd_evidence_query_use_case.ts create mode 100644 src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts create mode 100644 src/infrastructure/composition/sdd_evidence_composition_root.ts diff --git a/build/cli/index.js b/build/cli/index.js index 3966d0fcb..8d05e5945 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -42486,6 +42486,25 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.status.findings', 'interaction.status.findingsInvalid', 'interaction.status.findingCounts', + 'interaction.status.sddHeading', + 'interaction.status.sddRequirement', + 'interaction.status.sddState', + 'interaction.status.sddRequired', + 'interaction.status.sddNotRequired', + 'interaction.status.sddUnavailable', + 'interaction.status.sddRequirementUnknown', + 'interaction.status.sddUnverified', + 'interaction.status.sddAwaitingAnswers', + 'interaction.status.sddPublicationPending', + 'interaction.status.sddReady', + 'interaction.status.sddOwner', + 'interaction.status.sddQuestions', + 'interaction.status.sddCommit', + 'interaction.status.sddNextInspect', + 'interaction.status.sddNextAnswers', + 'interaction.status.sddNextPublication', + 'interaction.status.sddNextReady', + 'interaction.status.sddCard', 'cli.answer', 'cli.outcome', 'cli.status', @@ -42596,6 +42615,25 @@ const ENGLISH_MESSAGES = Object.freeze({ 'interaction.status.findings': 'Bugbot findings', 'interaction.status.findingsInvalid': 'invalid evidence; inspect the workflow result.', 'interaction.status.findingCounts': '{open} open, {reopened} reopened, {verificationRequired} verification required, {unknown} unknown, {resolved} resolved', + 'interaction.status.sddHeading': 'SDD readiness', + 'interaction.status.sddRequirement': 'Specification', + 'interaction.status.sddState': 'Status', + 'interaction.status.sddRequired': 'Required', + 'interaction.status.sddNotRequired': 'Not required', + 'interaction.status.sddUnavailable': 'Specification evidence unavailable; readiness is unverified.', + 'interaction.status.sddRequirementUnknown': 'Requirement unavailable', + 'interaction.status.sddUnverified': 'Publication evidence is invalid, absent or stale; readiness is unverified.', + 'interaction.status.sddAwaitingAnswers': 'Answers required', + 'interaction.status.sddPublicationPending': 'Publication pending', + 'interaction.status.sddReady': 'Published and verified', + 'interaction.status.sddOwner': 'Specification', + 'interaction.status.sddQuestions': 'Pending questions', + 'interaction.status.sddCommit': 'Verified publication', + 'interaction.status.sddNextInspect': 'Inspect the SDD card and retry after correcting its evidence.', + 'interaction.status.sddNextAnswers': 'Answer these questions in the issue using their question identifiers.', + 'interaction.status.sddNextPublication': 'Resume SDD preparation to publish the approved specification.', + 'interaction.status.sddNextReady': 'Continue implementation on the linked branch.', + 'interaction.status.sddCard': 'Open SDD card', 'cli.answer': 'Answer', 'cli.outcome': 'Outcome', 'cli.status': 'Status', @@ -42707,6 +42745,25 @@ const SPANISH_MESSAGES = Object.freeze({ 'interaction.status.findings': 'Hallazgos de Bugbot', 'interaction.status.findingsInvalid': 'evidencia no válida; revisa el resultado del workflow.', 'interaction.status.findingCounts': '{open} abiertos, {reopened} reabiertos, {verificationRequired} requieren verificación, {unknown} desconocidos, {resolved} resueltos', + 'interaction.status.sddHeading': 'Preparación del SDD', + 'interaction.status.sddRequirement': 'Especificación', + 'interaction.status.sddState': 'Estado', + 'interaction.status.sddRequired': 'Obligatoria', + 'interaction.status.sddNotRequired': 'No obligatoria', + 'interaction.status.sddUnavailable': 'Evidencia de la especificación no disponible; preparación sin verificar.', + 'interaction.status.sddRequirementUnknown': 'Requisito no disponible', + 'interaction.status.sddUnverified': 'La publicación es inválida, está ausente o desactualizada; preparación sin verificar.', + 'interaction.status.sddAwaitingAnswers': 'Respuestas necesarias', + 'interaction.status.sddPublicationPending': 'Publicación pendiente', + 'interaction.status.sddReady': 'Publicado y verificado', + 'interaction.status.sddOwner': 'Especificación', + 'interaction.status.sddQuestions': 'Preguntas pendientes', + 'interaction.status.sddCommit': 'Publicación verificada', + 'interaction.status.sddNextInspect': 'Revisa la tarjeta del SDD y reintenta tras corregir su evidencia.', + 'interaction.status.sddNextAnswers': 'Responde estas preguntas en la issue usando sus identificadores.', + 'interaction.status.sddNextPublication': 'Retoma la preparación del SDD para publicar la especificación aprobada.', + 'interaction.status.sddNextReady': 'Continúa la implementación en la rama vinculada.', + 'interaction.status.sddCard': 'Abrir tarjeta del SDD', 'cli.answer': 'Respuesta', 'cli.outcome': 'Resultado', 'cli.status': 'Estado', @@ -43487,6 +43544,154 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 99167: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.buildSddIssueDigest = buildSddIssueDigest; +const node_crypto_1 = __nccwpck_require__(6005); +const pre_branch_sdd_1 = __nccwpck_require__(34730); +const recommendation_policy_1 = __nccwpck_require__(39410); +/** Producer and read-only queries use the same identity and immutable first base. */ +function buildSddIssueDigest(context, baseSha) { + return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([ + context.issueNumber, (0, pre_branch_sdd_1.normalizeSddIssueTitle)(context.issueTitle), (0, recommendation_policy_1.getVisibleIssueDescription)(context.issueBody), + context.admittedKind, context.profileDigest ?? '', baseSha, + ])).digest('hex'); +} + + +/***/ }), + +/***/ 56285: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.selectOwnedSddCard = selectOwnedSddCard; +const pre_branch_sdd_1 = __nccwpck_require__(34730); +/** The latest owned card is authoritative even when invalid; an older card cannot restore readiness. */ +function selectOwnedSddCard(comments, issueNumber, trustedBotLogin) { + const login = trustedBotLogin.trim().toLowerCase(); + if (!/^[a-z0-9][a-z0-9-]{0,38}(?:\[bot\])?$/u.test(login)) + return { state: 'unavailable' }; + const candidates = comments.filter(comment => comment.user?.login?.toLowerCase() === login && typeof comment.body === 'string' + && (comment.body.includes(pre_branch_sdd_1.SDD_GATE_MARKER) || /^## (?:SDD work status|Estado del SDD)\s*$/mu.test(comment.body))); + if (!candidates.length) + return { state: 'absent' }; + if (candidates.some(comment => !Number.isSafeInteger(comment.id) || comment.id < 1)) + return { state: 'invalid' }; + const latestId = Math.max(...candidates.map(comment => comment.id)); + const latest = candidates.filter(comment => comment.id === latestId); + const bodies = new Set(latest.map(comment => comment.body)); + if (bodies.size !== 1) + return { state: 'invalid' }; + const body = latest[0].body; + if ([...body.matchAll(/', login)])).toMatchObject({ state: 'valid', id: 7 }); + }); + it('never falls back after the latest owned metadata becomes malformed', () => { + expect(select([card(7), card(8, '')])).toEqual({ state: 'invalid' }); + }); + it('never falls back after a newer owned record claims a different issue', () => { + expect(select([card(7), card(8, renderSddGateRecord({ ...record, issueNumber: 43 }))])).toEqual({ state: 'invalid' }); + }); + it.each([body, renderSddGateRecord({ ...record, commitSha: 'c'.repeat(40) })])('rejects multiple markers within one owned card', duplicate => { + expect(select([card(7, `${body}\n${duplicate}`)])).toEqual({ state: 'invalid' }); + }); + it('rejects contradictory representations of one latest comment ID', () => { + expect(select([card(7), card(7, renderSddGateRecord({ ...record, commitSha: 'c'.repeat(40) }))])).toEqual({ state: 'invalid' }); + }); + it('collapses byte-identical repetitions of the same provider comment', () => { + expect(select([card(7), card(7)])).toMatchObject({ state: 'valid', id: 7 }); + }); + it.each([0, -1, Number.MAX_SAFE_INTEGER + 1])('cannot order an owned card with unsafe ID %s', id => { + expect(select([card(7), card(id)])).toEqual({ state: 'invalid' }); + }); + it('does not infer bot authority from a bot suffix when configuration is absent', () => { + expect(select([card(7, body, 'vypbot[bot]')], '')).toEqual({ state: 'unavailable' }); + }); + it('recognizes a latest localized owned card that lost its marker as invalid', () => { + expect(select([card(7), card(8, '## Estado del SDD\n\nMetadata unavailable.')])).toEqual({ state: 'invalid' }); + }); +}); diff --git a/src/application/policies/__tests__/sdd_status_policy.test.ts b/src/application/policies/__tests__/sdd_status_policy.test.ts new file mode 100644 index 000000000..57e8efb47 --- /dev/null +++ b/src/application/policies/__tests__/sdd_status_policy.test.ts @@ -0,0 +1,73 @@ +import { initialSddStatus, projectSddStatus, formatSddStatus } from '../sdd_status_policy'; +import type { SddEvidenceQueryContext } from '../../ports/sdd_evidence_read_ports'; +import { resolveStaticPublicationCatalog } from '../publication_message_catalog'; + +const sha = 'a'.repeat(40); +const context: SddEvidenceQueryContext = { issueNumber: 42, admittedKind: 'feature', issueManagedBranches: true, preBranchSdd: true, issueAuthor: 'author', trustedBotLogin: 'vypbot' }; +const verified = { path: 'specs/feature.md', capabilityId: 'feature', branchName: 'feature/42', branchHeadSha: sha, firstSha: sha, currentSha: sha }; +const identity = { owner: 'acme', repository: 'demo' }; +const issueUrl = 'https://github.com/acme/demo/issues/42'; +const render = (readiness: ReturnType, url = issueUrl) => formatSddStatus(readiness, resolveStaticPublicationCatalog('en-US').catalog, url, 42, identity).join('\n'); + +describe('SDD status projection and safe navigation', () => { + it.each([ + ['admitted feature', {}, ['in-progress'], true], + ['contract change', { admittedKind: 'bugfix' }, ['in-progress', 'contract-change'], true], + ['output label alone', { admittedKind: 'bugfix' }, ['in-progress', 'SDD'], false], + ['unstarted feature', {}, ['feature'], false], + ['help', { admittedKind: 'help' }, ['in-progress', 'contract-change'], false], + ['disabled gate', { preBranchSdd: false }, ['in-progress'], false], + ['unknown configuration', { preBranchSdd: undefined }, ['in-progress'], null], + ['invalid configuration', { issueManagedBranches: false }, ['in-progress'], null], + ] as const)('uses canonical requirement for %s', (_name, override, labels, required) => { + expect(initialSddStatus({ ...context, ...override }, labels)).toEqual({ required, state: required === false ? 'not-required' : 'unavailable' }); + }); + + it.each([ + ['not-required', 'not-required'], ['absent', 'unavailable'], ['unavailable', 'unavailable'], + ['invalid', 'unverified'], ['stale', 'unverified'], ['awaiting-answers', 'awaiting-answers'], + ['publication-pending', 'publication-pending'], ['published', 'ready'], + ] as const)('projects %s with no historical SHA mistaken for readiness', (state, expected) => { + const status = projectSddStatus({ state, path: verified.path, verified, pendingQuestionIds: ['Q1'], cardId: 7 }, { state: 'unavailable', required: true }); + expect(status.state).toBe(expected); + expect(status.commitSha).toBe(state === 'published' ? sha : undefined); + expect(status.path).toBe(['not-required', 'unavailable', 'unverified'].includes(expected) ? undefined : verified.path); + expect(Object.isFrozen(status)).toBe(true); + }); + + it.each(['no proof', 'wrong path', 'abbreviated SHA'])('rejects a published projection with %s', scenario => { + const proof = scenario === 'no proof' ? undefined : { ...verified, path: scenario === 'wrong path' ? 'specs/foreign.md' : verified.path, currentSha: scenario === 'abbreviated SHA' ? 'abc123' : sha }; + const status = projectSddStatus({ state: 'published', path: verified.path, verified: proof }); + expect(status.state).toBe('unverified'); expect(status.commitSha).toBeUndefined(); + expect(render(status)).not.toContain('/blob/'); + }); + + it.each([ + 'http://github.com/acme/demo/issues/42', 'https://github.com/foreign/demo/issues/42', + 'https://github.com/acme/foreign/issues/42', 'https://github.com/acme/demo/issues/43', + 'https://user:secret@github.com/acme/demo/issues/42', 'https://github.com/acme/demo/issues/42?token=secret', 'broken', + ])('omits all evidence links for an invalid issue identity %s', url => { + const status = projectSddStatus({ state: 'published', path: verified.path, verified, cardId: 7 }); + const body = render(status, url); + expect(body).not.toContain(']('); expect(body).not.toContain('secret'); expect(body).toContain(sha); + }); + + it('bounds pending identifiers without disclosing question text and links verified evidence to its own issue', () => { + const pending = projectSddStatus({ state: 'awaiting-answers', path: verified.path, pendingQuestionIds: ['Q1', 'Q1', 'Q9', 'TOKEN=secret'], cardId: 7 }); + expect(Object.isFrozen(pending.pendingQuestionIds)).toBe(true); + const body = render(pending); + expect(body).toContain('**Pending questions:** Q1'); expect(body).not.toMatch(/Q9|TOKEN|secret/u); + expect(body).toContain(issueUrl + '#issuecomment-7'); + const ready = render(projectSddStatus({ state: 'published', path: verified.path, verified })); + expect(ready).toContain('https://github.com/acme/demo/blob/' + sha + '/' + verified.path); + }); + + it.each(['en-US', 'es-ES'])('renders all six closed states using the bundled %s catalog', locale => { + const catalog = resolveStaticPublicationCatalog(locale).catalog; + for (const state of ['not-required', 'unavailable', 'unverified', 'awaiting-answers', 'publication-pending', 'ready'] as const) { + const body = formatSddStatus({ state, required: state !== 'not-required', path: verified.path, commitSha: sha, pendingQuestionIds: ['Q1'] }, catalog).join('\n'); + expect(body).not.toMatch(/interaction\.status|undefined|TOKEN/u); + expect(body).toContain(locale === 'es-ES' ? 'Preparación del SDD' : 'SDD readiness'); + } + }); +}); diff --git a/src/application/policies/publication_message_catalog.ts b/src/application/policies/publication_message_catalog.ts index 8371f26b9..5ad2226f7 100644 --- a/src/application/policies/publication_message_catalog.ts +++ b/src/application/policies/publication_message_catalog.ts @@ -103,6 +103,19 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.status.sddRequired', 'interaction.status.sddNotRequired', 'interaction.status.sddUnavailable', + 'interaction.status.sddRequirementUnknown', + 'interaction.status.sddUnverified', + 'interaction.status.sddAwaitingAnswers', + 'interaction.status.sddPublicationPending', + 'interaction.status.sddReady', + 'interaction.status.sddOwner', + 'interaction.status.sddQuestions', + 'interaction.status.sddCommit', + 'interaction.status.sddNextInspect', + 'interaction.status.sddNextAnswers', + 'interaction.status.sddNextPublication', + 'interaction.status.sddNextReady', + 'interaction.status.sddCard', 'cli.answer', 'cli.outcome', 'cli.status', @@ -262,6 +275,19 @@ const ENGLISH_MESSAGES: Readonly> = 'interaction.status.sddRequired': 'Required', 'interaction.status.sddNotRequired': 'Not required', 'interaction.status.sddUnavailable': 'Specification evidence unavailable; readiness is unverified.', + 'interaction.status.sddRequirementUnknown': 'Requirement unavailable', + 'interaction.status.sddUnverified': 'Publication evidence is invalid, absent or stale; readiness is unverified.', + 'interaction.status.sddAwaitingAnswers': 'Answers required', + 'interaction.status.sddPublicationPending': 'Publication pending', + 'interaction.status.sddReady': 'Published and verified', + 'interaction.status.sddOwner': 'Specification', + 'interaction.status.sddQuestions': 'Pending questions', + 'interaction.status.sddCommit': 'Verified publication', + 'interaction.status.sddNextInspect': 'Inspect the SDD card and retry after correcting its evidence.', + 'interaction.status.sddNextAnswers': 'Answer these questions in the issue using their question identifiers.', + 'interaction.status.sddNextPublication': 'Resume SDD preparation to publish the approved specification.', + 'interaction.status.sddNextReady': 'Continue implementation on the linked branch.', + 'interaction.status.sddCard': 'Open SDD card', 'cli.answer': 'Answer', 'cli.outcome': 'Outcome', 'cli.status': 'Status', @@ -380,6 +406,19 @@ const SPANISH_MESSAGES: Readonly> = 'interaction.status.sddRequired': 'Obligatoria', 'interaction.status.sddNotRequired': 'No obligatoria', 'interaction.status.sddUnavailable': 'Evidencia de la especificación no disponible; preparación sin verificar.', + 'interaction.status.sddRequirementUnknown': 'Requisito no disponible', + 'interaction.status.sddUnverified': 'La publicación es inválida, está ausente o desactualizada; preparación sin verificar.', + 'interaction.status.sddAwaitingAnswers': 'Respuestas necesarias', + 'interaction.status.sddPublicationPending': 'Publicación pendiente', + 'interaction.status.sddReady': 'Publicado y verificado', + 'interaction.status.sddOwner': 'Especificación', + 'interaction.status.sddQuestions': 'Preguntas pendientes', + 'interaction.status.sddCommit': 'Publicación verificada', + 'interaction.status.sddNextInspect': 'Revisa la tarjeta del SDD y reintenta tras corregir su evidencia.', + 'interaction.status.sddNextAnswers': 'Responde estas preguntas en la issue usando sus identificadores.', + 'interaction.status.sddNextPublication': 'Retoma la preparación del SDD para publicar la especificación aprobada.', + 'interaction.status.sddNextReady': 'Continúa la implementación en la rama vinculada.', + 'interaction.status.sddCard': 'Abrir tarjeta del SDD', 'cli.answer': 'Respuesta', 'cli.outcome': 'Resultado', 'cli.status': 'Estado', diff --git a/src/application/policies/sdd_issue_identity_policy.ts b/src/application/policies/sdd_issue_identity_policy.ts new file mode 100644 index 000000000..2cad45edb --- /dev/null +++ b/src/application/policies/sdd_issue_identity_policy.ts @@ -0,0 +1,19 @@ +import { createHash } from 'node:crypto'; +import { normalizeSddIssueTitle } from '../../domain/pre_branch_sdd'; +import { getVisibleIssueDescription } from './recommendation_policy'; + +export interface SddIssueIdentity { + readonly issueNumber: number; + readonly issueTitle: string; + readonly issueBody: string; + readonly admittedKind: string; + readonly profileDigest?: string; +} + +/** Producer and read-only queries use the same identity and immutable first base. */ +export function buildSddIssueDigest(context: SddIssueIdentity, baseSha: string): string { + return createHash('sha256').update(JSON.stringify([ + context.issueNumber, normalizeSddIssueTitle(context.issueTitle), getVisibleIssueDescription(context.issueBody), + context.admittedKind, context.profileDigest ?? '', baseSha, + ])).digest('hex'); +} diff --git a/src/application/policies/sdd_query_card_policy.ts b/src/application/policies/sdd_query_card_policy.ts new file mode 100644 index 000000000..a7bfdce53 --- /dev/null +++ b/src/application/policies/sdd_query_card_policy.ts @@ -0,0 +1,30 @@ +import { readSddGateRecord, SDD_GATE_MARKER, type SddGateRecord } from '../../domain/pre_branch_sdd'; + +export interface SddQueryComment { + readonly id: number; + readonly body: string | null; + readonly user?: { readonly login?: string }; +} + +export type SddCardSelection = + | { readonly state: 'absent' | 'invalid' | 'unavailable' } + | { readonly state: 'valid'; readonly id: number; readonly body: string; readonly record: SddGateRecord }; + +/** The latest owned card is authoritative even when invalid; an older card cannot restore readiness. */ +export function selectOwnedSddCard(comments: readonly SddQueryComment[], issueNumber: number, trustedBotLogin: string): SddCardSelection { + const login = trustedBotLogin.trim().toLowerCase(); + if (!/^[a-z0-9][a-z0-9-]{0,38}(?:\[bot\])?$/u.test(login)) return { state: 'unavailable' }; + const candidates = comments.filter(comment => comment.user?.login?.toLowerCase() === login && typeof comment.body === 'string' + && (comment.body.includes(SDD_GATE_MARKER) || /^## (?:SDD work status|Estado del SDD)\s*$/mu.test(comment.body))); + if (!candidates.length) return { state: 'absent' }; + if (candidates.some(comment => !Number.isSafeInteger(comment.id) || comment.id < 1)) return { state: 'invalid' }; + const latestId = Math.max(...candidates.map(comment => comment.id)); + const latest = candidates.filter(comment => comment.id === latestId); + const bodies = new Set(latest.map(comment => comment.body)); + if (bodies.size !== 1) return { state: 'invalid' }; + const body = latest[0].body!; + if ([...body.matchAll(/', 8)] }); + expect(await f.query.read(f.context)).toEqual({ state: 'invalid' }); + }); + it('ignores a copied publication by a foreign author', async () => { + const f = fixture(); f.comments.mockResolvedValue({ ...(await f.comments()), value: [f.comment(), f.comment('', 8, 'human')] }); + expect(await f.query.read(f.context)).toMatchObject({ state: 'published' }); + }); + it('keeps a reused Q1 pending when its retained answer belongs to the previous round', async () => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', round: 2, + plan: { ...f.record.plan, questions: [{ id: 'Q1', text: 'What should the new behavior be?', owner: 'issue-author' }] }, + answers: [{ questionId: 'Q1', author: 'author', commentId: 6, text: 'Old choice', round: 1, questionText: 'What was the previous behavior?' }] }); + expect(await f.query.read(f.context)).toMatchObject({ state: 'awaiting-answers', pendingQuestionIds: ['Q1'], verified: { firstSha: first } }); + }); + it.each(['issue-author', 'maintainer'])('accepts only a current answer authorized for %s', async owner => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, questions: [{ id: 'Q1', text: 'What should this behavior be?', owner }] } }); + const current = await f.comments(); current.value.push(f.comment('SDD Q1: Keep the verified behavior.', 9, owner === 'maintainer' ? 'maintainer' : 'author')); + f.comments.mockResolvedValue(current); + expect(await f.query.read(f.context)).toMatchObject({ state: 'publication-pending', pendingQuestionIds: [] }); + }); + it.each(['human', 'vypbot'])('does not consume a pending answer by %s', author => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, questions: [{ id: 'Q1', text: 'What should this behavior be?', owner: 'issue-author' }] } }); + return f.comments().then(async current => { + current.value.push(f.comment('SDD Q1: An unauthorized answer.', 9, author)); f.comments.mockResolvedValue(current); + expect(await f.query.read(f.context)).toMatchObject({ state: 'awaiting-answers', pendingQuestionIds: ['Q1'] }); + }); + }); + it('reports first publication pending from validated new-owner metadata without a branch', async () => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', commitSha: undefined, branchName: undefined, + plan: { ...f.record.plan, action: 'companion', path: 'specs/new-feature.md' } }); + expect(await f.query.read(f.context)).toMatchObject({ state: 'publication-pending', path: 'specs/new-feature.md', pendingQuestionIds: [] }); + expect(f.ports.workspace.verifyPublication).not.toHaveBeenCalled(); + }); + it('distinguishes absent evidence from a malformed publication', async () => { + const f = fixture(); f.comments.mockResolvedValue({ ...(await f.comments()), value: [] }); + expect(await f.query.read(f.context)).toEqual({ state: 'absent' }); + }); + it('does not turn partial provider inventory into an absent or ready result', async () => { + const f = fixture(); const incomplete = await f.comments(); incomplete.coverage.limitReached = true; f.comments.mockResolvedValue(incomplete); + expect(await f.query.read(f.context)).toEqual({ state: 'unavailable' }); + }); + it.each(['issue edit', 'card edit', 'branch push', 'label edit'])('loses readiness when the final fence observes %s', async mutation => { + const f = fixture(); + if (mutation === 'issue edit') f.ports.descriptions.getDescription.mockResolvedValueOnce('Human requirements').mockResolvedValue('New requirements'); + if (mutation === 'card edit') { + const initial = await f.comments(); + f.comments.mockResolvedValueOnce(initial).mockResolvedValue({ ...initial, value: [f.comment('## SDD work status\nmissing evidence')] }); + } + if (mutation === 'branch push') f.ports.branches.getLinkedBranch.mockResolvedValueOnce({ name: 'feature/42', headSha: head }).mockResolvedValue({ name: 'feature/42', headSha: 'f'.repeat(40) }); + if (mutation === 'label edit') f.ports.labels.getLabels.mockResolvedValueOnce(['feature', 'in-progress']).mockResolvedValue(['feature']); + expect(await f.query.read(f.context)).toEqual({ state: 'stale' }); + }); + it('does not infer disabled gate configuration from missing legacy fields', async () => { + const f = fixture(); expect(await f.query.read({ ...f.context, preBranchSdd: undefined })).toEqual({ state: 'unavailable' }); + expect(f.comments).not.toHaveBeenCalled(); + }); + it('keeps help branchless without reading publication or comment authority', async () => { + const f = fixture(); expect(await f.query.read({ ...f.context, admittedKind: 'help' })).toEqual({ state: 'not-required' }); + expect(f.comments).not.toHaveBeenCalled(); + }); + it('uses canonical admission and start policy rather than the SDD output label', async () => { + const f = fixture(); f.ports.labels.getLabels.mockResolvedValue(['feature', 'SDD']); + expect(await f.query.read(f.context)).toEqual({ state: 'not-required' }); + expect(f.ports.workspace.verifyPublication).not.toHaveBeenCalled(); + }); + it('returns unavailable after an authority read fails without exposing the provider error', async () => { + const f = fixture(); f.comments.mockRejectedValue(new Error('private provider response')); + expect(await f.query.read(f.context)).toEqual({ state: 'unavailable' }); + }); + it('physically excludes source, label, publication and agent mutation methods from bound capabilities', () => { + const f = fixture(); Object.assign(f.ports.labels, { setLabels: jest.fn() }); Object.assign(f.ports.workspace, { publish: jest.fn(), validateDraft: jest.fn() }); + const bound = bindSddEvidenceReadPorts(f.ports); + expect(Object.keys(bound.labels)).toEqual(['getLabels']); + expect(Object.keys(bound.workspace)).toEqual(['loadSnapshot', 'loadSnapshotAtCommit', 'verifyPublication', 'isAncestor']); + expect(Object.isFrozen(bound)).toBe(true); expect(Object.isFrozen(bound.workspace)).toBe(true); + }); +}); diff --git a/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts b/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts index 5d31ec705..f636d74dc 100644 --- a/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts +++ b/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts @@ -1,4 +1,4 @@ -import { createHash } from 'node:crypto'; +import { buildSddIssueDigest } from '../../policies/sdd_issue_identity_policy'; import type { AgentConfiguration } from '../../ports/agent_configuration_ports'; import type { FindingsQueryPort } from '../../ports/agent_findings_ports'; import type { BoundIssueCommentUpsertPort, IssueCommentPublicationTarget } from '../../ports/issue_lifecycle_ports'; @@ -320,10 +320,7 @@ function latestOwnedCard(comments: readonly IssueCommentPublicationTarget[], iss } function issueDigest(context: PreBranchSddContext, baseSha: string): string { - return createHash('sha256').update(JSON.stringify([ - context.issueNumber, normalizeSddIssueTitle(context.issueTitle), getVisibleIssueDescription(context.issueBody), context.admittedKind, - context.profileDigest ?? '', baseSha, - ])).digest('hex'); + return buildSddIssueDigest(context, baseSha); } function asRecord(value: unknown): Record { diff --git a/src/application/usecases/sdd/sdd_evidence_query_use_case.ts b/src/application/usecases/sdd/sdd_evidence_query_use_case.ts new file mode 100644 index 000000000..41b02bbd8 --- /dev/null +++ b/src/application/usecases/sdd/sdd_evidence_query_use_case.ts @@ -0,0 +1,164 @@ +import { decideIssueStart } from '../../../domain/issue_start_policy'; +import { parseSddAnswer, parseSddPlan, type SddGateRecord } from '../../../domain/pre_branch_sdd'; +import { bindSddEvidenceReadPorts, type SddEvidenceQueryContext, type SddEvidenceReadPorts, type VerifiedSddEvidence, type SddEvidenceSnapshot } from '../../ports/sdd_evidence_read_ports'; +import type { SddCatalogSnapshot } from '../../ports/pre_branch_sdd_ports'; +import { buildSddIssueDigest } from '../../policies/sdd_issue_identity_policy'; +import { selectOwnedSddCard, type SddQueryComment } from '../../policies/sdd_query_card_policy'; + +export type { VerifiedSddEvidence, SddEvidenceSnapshot } from '../../ports/sdd_evidence_read_ports'; + +interface ReadFacts { + readonly title: string; + readonly body: string; + readonly labels: readonly string[]; + readonly comments: readonly SddQueryComment[]; +} + +/** Reads gate authority and immutable publication evidence without any mutation or model capability. */ +export class SddEvidenceQueryUseCase { + private readonly ports: SddEvidenceReadPorts; + constructor(ports: SddEvidenceReadPorts) { this.ports = bindSddEvidenceReadPorts(ports); } + + async read(context: SddEvidenceQueryContext): Promise { + try { + if (!Number.isSafeInteger(context.issueNumber) || context.issueNumber < 1 + || typeof context.issueManagedBranches !== 'boolean' || typeof context.preBranchSdd !== 'boolean' + || !context.admittedKind) return { state: 'unavailable' }; + decideIssueStart({ kind: context.admittedKind, labels: [], issueManagedBranches: context.issueManagedBranches, preBranchSdd: context.preBranchSdd }); + if (context.admittedKind === 'help' || !context.preBranchSdd) return { state: 'not-required' }; + const facts = await this.readFacts(context.issueNumber); + if (!this.required(context, facts.labels)) return { state: 'not-required' }; + const selection = selectOwnedSddCard(facts.comments, context.issueNumber, context.trustedBotLogin); + if (selection.state !== 'valid') return { state: selection.state }; + const { record, id: cardId } = selection; + const digest = this.digest(context, facts, record.baseSha); + const stale = digest !== record.issueDigest; + const result = await this.inspectRecord(context, facts, record, cardId, stale); + if (!['invalid', 'unavailable'].includes(result.state) + && !await this.fence(context, facts, selection.body, cardId, record, result.verified)) return { state: 'stale' }; + return Object.freeze(result); + } catch { return { state: 'unavailable' }; } + } + + private async inspectRecord(context: SddEvidenceQueryContext, facts: ReadFacts, record: SddGateRecord, cardId: number, stale: boolean): Promise { + const path = record.plan.path; + let verified: VerifiedSddEvidence | undefined; + if (record.commitSha) { + if (!record.branchName) return { state: 'invalid' }; + const linked = await this.ports.branches.getLinkedBranch(context.issueNumber, record.branchName); + if (linked?.name !== record.branchName || !fullSha(linked.headSha)) return { state: 'invalid' }; + if (!await this.ports.workspace.verifyPublication(record.branchName, record.baseSha, record.commitSha, path) + || !await this.ports.workspace.isAncestor(record.commitSha, linked.headSha)) return { state: 'invalid' }; + if (!ownedAt(await this.ports.workspace.loadSnapshotAtCommit(record.commitSha), record.commitSha, record)) return { state: 'invalid' }; + verified = Object.freeze({ path, capabilityId: record.plan.capabilityId, branchName: record.branchName, + branchHeadSha: linked.headSha, firstSha: record.commitSha }); + if (record.phase === 'published') { + const currentSha = record.revisionSha ?? record.commitSha; + if (record.revisionSha && (!record.revisionBaseSha + || !await this.ports.workspace.isAncestor(record.commitSha, record.revisionBaseSha) + || !await this.ports.workspace.verifyPublication(record.branchName, record.revisionBaseSha, record.revisionSha, path) + || !await this.ports.workspace.isAncestor(record.revisionSha, linked.headSha) + || !ownedAt(await this.ports.workspace.loadSnapshotAtCommit(record.revisionSha), record.revisionSha, record))) return { state: 'invalid' }; + verified = Object.freeze({ ...verified, currentSha }); + } + } else { + if (record.phase === 'published') return { state: 'invalid' }; + const source = record.branchName ?? context.baseBranch; + if (!source) return { state: 'unavailable' }; + if (record.branchName) { + const linked = await this.ports.branches.getLinkedBranch(context.issueNumber, record.branchName); + if (linked?.name !== record.branchName || linked.headSha !== record.baseSha) return { state: 'invalid' }; + } + const snapshot = await this.ports.workspace.loadSnapshot(source); + if (snapshot.baseSha !== record.baseSha) return { state: 'stale' }; + try { parseSddPlan(record.plan, new Map(snapshot.capabilities.map(capability => [capability.id, capability.specs]))); } + catch { return { state: 'invalid' }; } + } + if (stale) return { state: 'stale', ...(verified ? { verified } : {}) }; + if (record.phase === 'published') return { state: 'published', path, cardId, verified }; + const pendingQuestionIds = await this.pendingQuestions(context, facts.comments, record, cardId); + return { state: pendingQuestionIds.length ? 'awaiting-answers' : 'publication-pending', path, cardId, + pendingQuestionIds: Object.freeze(pendingQuestionIds), ...(verified ? { verified } : {}) }; + } + + private async pendingQuestions(context: SddEvidenceQueryContext, comments: readonly SddQueryComment[], record: SddGateRecord, cardId: number): Promise { + const pending: string[] = []; + const cutoff = Math.max(cardId, ...(record.answers ?? []).map(answer => answer.commentId)); + for (const question of record.plan.questions) { + const retained = (record.answers ?? []).filter(answer => { + const provenance = answer as typeof answer & { readonly round?: number; readonly questionText?: string }; + return Number.isSafeInteger(answer.commentId) && provenance.round === record.round + && answer.questionId === question.id && provenance.questionText === question.text; + }); + let answered = false; + for (const answer of retained) if (await this.authorized(context, question.owner, answer.author)) { answered = true; break; } + if (!answered) { + for (const comment of [...comments].sort((a, b) => b.id - a.id)) { + if (!Number.isSafeInteger(comment.id) || comment.id <= cutoff || !comment.user?.login + || comment.user.login.toLowerCase() === context.trustedBotLogin.toLowerCase() + || !comment.body || !parseSddAnswer(comment.body, question.id)) continue; + if (await this.authorized(context, question.owner, comment.user.login)) { answered = true; break; } + } + } + if (!answered) pending.push(question.id); + } + return pending; + } + + private async authorized(context: SddEvidenceQueryContext, owner: 'issue-author' | 'maintainer', author: string): Promise { + return owner === 'issue-author' ? Boolean(context.issueAuthor && author.toLowerCase() === context.issueAuthor.toLowerCase()) + : this.ports.actors.isActorAllowedToModifyFiles(author); + } + + private async fence(context: SddEvidenceQueryContext, original: ReadFacts, cardBody: string, cardId: number, record: SddGateRecord, verified?: VerifiedSddEvidence): Promise { + const fresh = await this.readFacts(context.issueNumber); + const card = selectOwnedSddCard(fresh.comments, context.issueNumber, context.trustedBotLogin); + if (card.state !== 'valid' || card.id !== cardId || card.body !== cardBody + || this.digest(context, fresh, record.baseSha) !== this.digest(context, original, record.baseSha) + || canonicalLabels(fresh.labels) !== canonicalLabels(original.labels) + || !this.required(context, fresh.labels)) return false; + // Answer arrivals or edits must not let a stale question projection become current. + if (record.phase === 'awaiting-answer' + && JSON.stringify(fresh.comments) !== JSON.stringify(original.comments)) return false; + if (record.branchName) { + const linked = await this.ports.branches.getLinkedBranch(context.issueNumber, record.branchName); + if (linked?.name !== record.branchName || linked.headSha !== (verified?.branchHeadSha ?? record.baseSha)) return false; + if (verified && (!await this.ports.workspace.verifyPublication(record.branchName, record.baseSha, record.commitSha!, record.plan.path) + || (record.phase === 'published' && record.revisionSha + && !await this.ports.workspace.verifyPublication(record.branchName, record.revisionBaseSha!, record.revisionSha, record.plan.path)))) return false; + } else if (!context.baseBranch || (await this.ports.workspace.loadSnapshot(context.baseBranch)).baseSha !== record.baseSha) return false; + return true; + } + + private required(context: SddEvidenceQueryContext, labels: readonly string[]): boolean { + return decideIssueStart({ kind: context.admittedKind, labels, + issueManagedBranches: context.issueManagedBranches!, preBranchSdd: context.preBranchSdd! }).sddRequired; + } + + private digest(context: SddEvidenceQueryContext, facts: ReadFacts, baseSha: string): string { + return buildSddIssueDigest({ issueNumber: context.issueNumber, issueTitle: facts.title, issueBody: facts.body, + admittedKind: context.admittedKind!, profileDigest: context.profileDigest }, baseSha); + } + + private async readFacts(issueNumber: number): Promise { + const [title, body, labels, comments] = await Promise.all([ + this.ports.titles.getTitle(issueNumber), this.ports.descriptions.getDescription(issueNumber), + this.ports.labels.getLabels(issueNumber), this.ports.comments.listIssueComments(issueNumber), + ]); + if (title === undefined || body === undefined || comments.coverage.source !== 'issue-comments' + || comments.coverage.status !== 'complete' || comments.coverage.limitReached || comments.coverage.providerLimitReached + || comments.coverage.omittedItems !== 0 || comments.coverage.truncatedItems !== 0) throw new Error('Incomplete SDD authority.'); + return { title, body, labels, comments: comments.value }; + } +} + +function ownedAt(snapshot: SddCatalogSnapshot, sha: string, record: SddGateRecord): boolean { + if (!Array.isArray(snapshot.capabilities) || snapshot.capabilities.some(capability => !capability || typeof capability.id !== 'string' + || !Array.isArray(capability.specs) || capability.specs.some((path: unknown) => typeof path !== 'string')) + || new Set(snapshot.capabilities.map(capability => capability.id)).size !== snapshot.capabilities.length) return false; + const owners = snapshot.capabilities.filter(capability => capability.specs.includes(record.plan.path)); + return snapshot.baseSha === sha && owners.length === 1 && owners[0].id === record.plan.capabilityId; +} + +function fullSha(value: unknown): value is string { return typeof value === 'string' && /^[a-f0-9]{40}$/u.test(value); } +function canonicalLabels(labels: readonly string[]): string { return JSON.stringify([...new Set(labels.map(label => label.trim().toLowerCase()))].sort()); } diff --git a/src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts b/src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts index eec21c66e..09bb18fd6 100644 --- a/src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts +++ b/src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts @@ -4,9 +4,9 @@ const sha = 'a'.repeat(40); describe('LinkedBranchReadinessRepository', () => { it('selects the exact issue-linked ref and its commit SHA', async () => { - const graphql = jest.fn().mockResolvedValue({ repository: { issue: { linkedBranches: { nodes: [ + const graphql = jest.fn().mockResolvedValue({ repository: { ref: { target: { oid: sha } }, issue: { linkedBranches: { nodes: [ { ref: { name: 'refs/heads/feature/420-other', target: { oid: 'b'.repeat(40) } } }, - { ref: { name: 'refs/heads/feature/42-change', target: { oid: sha } } }, + { ref: { name: 'refs/heads/feature/42-change', repository: { nameWithOwner: 'acme/repo' }, target: { oid: sha } } }, ] } } } }); const repository = new LinkedBranchReadinessRepository({ getClient: () => ({ graphql }) } as never); await expect(repository.getLinkedBranch('acme', 'repo', 42, 'feature/42-change', 'token')) @@ -16,6 +16,22 @@ describe('LinkedBranchReadinessRepository', () => { }); }); + it.each([ + ['stale native SHA', 'b'.repeat(40), 'acme/repo'], + ['deleted native branch', undefined, 'acme/repo'], + ['foreign native repository', sha, 'other/repo'], + ['missing native repository', sha, undefined], + ])('rejects %s even with an otherwise valid PR fallback', async (_reason, remoteSha, nameWithOwner) => { + const graphql = jest.fn().mockResolvedValue({ repository: { + ref: { target: { oid: remoteSha } }, issue: { + linkedBranches: { nodes: [{ ref: { name: 'feature/42-change', repository: { nameWithOwner }, target: { oid: sha } } }] }, + closedByPullRequestsReferences: { nodes: [{ state: 'OPEN', headRefName: 'feature/42-change', headRefOid: remoteSha, headRepository: { nameWithOwner: 'acme/repo' } }] }, + }, + } }); + const repository = new LinkedBranchReadinessRepository({ getClient: () => ({ graphql }) } as never); + await expect(repository.getLinkedBranch('acme', 'repo', 42, 'feature/42-change', 'token')).resolves.toBeUndefined(); + }); + it.each(['', '../feature/42-change', '/feature/42-change'])('rejects unsafe expected ref %s', async branch => { const graphql = jest.fn().mockResolvedValue({ repository: { issue: { linkedBranches: { nodes: [] } } } }); const repository = new LinkedBranchReadinessRepository({ getClient: () => ({ graphql }) } as never); diff --git a/src/data/repository/branch/linked_branch_readiness_repository.ts b/src/data/repository/branch/linked_branch_readiness_repository.ts index 0fd495c76..74098652f 100644 --- a/src/data/repository/branch/linked_branch_readiness_repository.ts +++ b/src/data/repository/branch/linked_branch_readiness_repository.ts @@ -8,7 +8,7 @@ interface Response { readonly issue?: { readonly linkedBranches?: { readonly nodes?: ReadonlyArray<{ - readonly ref?: { readonly name?: string; readonly target?: { readonly oid?: string } }; + readonly ref?: { readonly name?: string; readonly repository?: { readonly nameWithOwner?: string }; readonly target?: { readonly oid?: string } }; } | null>; }; readonly closedByPullRequestsReferences?: { @@ -42,7 +42,7 @@ export class LinkedBranchReadinessRepository implements LinkedBranchReadinessPor ref(qualifiedName: $ref) { target { ... on Commit { oid } } } issue(number: $issueNumber) { linkedBranches(first: 100) { - nodes { ref { name target { ... on Commit { oid } } } } + nodes { ref { name repository { nameWithOwner } target { ... on Commit { oid } } } } } closedByPullRequestsReferences(first: 100) { nodes { state headRefName headRefOid headRepository { nameWithOwner } } @@ -56,11 +56,13 @@ export class LinkedBranchReadinessRepository implements LinkedBranchReadinessPor return name === expected || name === `refs/heads/${expected}` || name === `/${expected}`; }); const directlyLinkedSha = fullSha(match?.ref?.target?.oid); - if (directlyLinkedSha) return Object.freeze({ name: expected, headSha: directlyLinkedSha }); + const remoteSha = fullSha(response.repository?.ref?.target?.oid); + if (match) return directlyLinkedSha && directlyLinkedSha === remoteSha + && match.ref?.repository?.nameWithOwner?.toLowerCase() === `${owner}/${repository}`.toLowerCase() + ? Object.freeze({ name: expected, headSha: directlyLinkedSha }) : undefined; // GitHub removes linkedBranches when its branch becomes a linked PR. // Native issue association plus the exact same-repository live ref retains // that proof; a PR body or a stale/deleted head alone is insufficient. - const remoteSha = fullSha(response.repository?.ref?.target?.oid); const linkedPullRequest = response.repository?.issue?.closedByPullRequestsReferences?.nodes?.find(pr => pr?.state === 'OPEN' && pr.headRefName === expected && pr.headRepository?.nameWithOwner?.toLowerCase() === `${owner}/${repository}`.toLowerCase() diff --git a/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts b/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts index ba6443430..086c70631 100644 --- a/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts +++ b/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts @@ -83,6 +83,24 @@ describe('PreBranchSddWorkspaceAdapter with a local bare remote', () => { expect(git(repo, 'status', '--porcelain')).toBe(''); }); + it('reads the owning catalog at an immutable commit without checking out or changing current files', async () => { + const original = git(repo, 'rev-parse', 'HEAD'); + fs.writeFileSync(path.join(repo, 'specs/catalog.json'), '{"version":2,"capabilities":[]}\n'); + git(repo, 'add', 'specs/catalog.json'); git(repo, 'commit', '-m', 'later catalog'); + const current = git(repo, 'rev-parse', 'HEAD'); + const snapshot = await new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit(original); + expect(snapshot.baseSha).toBe(original); + expect(snapshot.capabilities.map(capability => capability.id)).toEqual(['payments']); + expect(git(repo, 'rev-parse', 'HEAD')).toBe(current); + expect(fs.readFileSync(path.join(repo, 'specs/catalog.json'), 'utf8')).toContain('"version":2'); + expect(git(repo, 'status', '--porcelain')).toBe(''); + }); + + it('rejects a non-SHA publication identifier before reading Git objects', async () => { + await expect(new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit('develop:specs/catalog.json')).rejects.toThrow('invalid commit SHA'); + expect(git(repo, 'status', '--porcelain')).toBe(''); + }); + it('reads only safe SDD paths from the selected base commit', async () => { const workspace = new PreBranchSddWorkspaceAdapter(repo); const snapshot = await workspace.loadSnapshot('develop'); diff --git a/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts b/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts index 8f8808ac3..667f751ac 100644 --- a/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts @@ -19,7 +19,7 @@ import { const mockIssue = {}; const mockPullRequest = {}; -const mockContext = { kind: 'context' }; +const mockContext = { kind: 'context', listIssueComments: jest.fn() }; const mockResolution = { kind: "resolution" }; const mockFindings = {}; const mockLanguage = {}; @@ -118,7 +118,11 @@ describe("main run route composition root", () => { expect(IssueCommentUseCase).toHaveBeenCalledWith( ...Array.from({ length: 14 }, () => expect.anything()), mockContext, + expect.objectContaining({ read: expect.any(Function) }), ); + const query = jest.mocked(IssueCommentUseCase).mock.calls[0][15]; + expect(Object.keys(query!)).toEqual(['read']); + expect(Object.isFrozen(query)).toBe(true); }); it("keeps Bugbot resolution out of the review-comment autofix composition", () => { diff --git a/src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts b/src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts new file mode 100644 index 000000000..6d54dcce0 --- /dev/null +++ b/src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts @@ -0,0 +1,62 @@ +import { renderSddGateRecord, type SddGateRecord } from '../../../domain/pre_branch_sdd'; +import { buildSddIssueDigest } from '../../../application/policies/sdd_issue_identity_policy'; +import { createSddEvidenceQueryPort } from '../sdd_evidence_composition_root'; + +const mockTitle = jest.fn(async () => 'Feature'); +const mockDescription = jest.fn(async () => 'Human requirements'); +const mockLabels = jest.fn(async () => ['feature', 'in-progress']); +const mockPermission = jest.fn(async () => true); +const mockWrite = jest.fn(() => { throw new Error('query must not mutate'); }); +const mockHead = 'e'.repeat(40), mockBase = 'a'.repeat(40), mockFirst = 'b'.repeat(40); +const mockGraphql = jest.fn(async () => ({ repository: { ref: { target: { oid: mockHead } }, issue: { linkedBranches: { nodes: [{ ref: { + name: 'feature/42', repository: { nameWithOwner: 'acme/demo' }, target: { oid: mockHead }, +} }] } } } })); +const mockGetClient = jest.fn(() => ({ graphql: mockGraphql })); +const mockSnapshot = jest.fn(async (sha: string) => ({ baseSha: sha, template: '', standard: '', capabilities: [{ + id: 'feature', title: 'Feature', status: 'implemented', scope: 'Feature', owner: 'Maintainer', lastVerified: '2026-10-11', + specs: ['specs/feature.md'], workflows: [], entrypoints: [], code: [], tests: [], documentation: [], +}] })); +const mockVerify = jest.fn(async () => true); +const mockAncestor = jest.fn(async () => true); + +jest.mock('../issue_metadata_composition_root', () => ({ createIssueMetadataCompositionRoot: () => ({ getTitle: mockTitle, setTitle: mockWrite }) })); +jest.mock('../issue_content_composition_root', () => ({ createIssueContentCompositionRoot: () => ({ getDescription: mockDescription, updateDescription: mockWrite }) })); +jest.mock('../issue_labels_composition_root', () => ({ createIssueLabelRepository: () => ({ getLabels: mockLabels, setLabels: mockWrite }) })); +jest.mock('../actor_authorization_composition_root', () => ({ createActorAuthorizationRepository: () => ({ isActorAllowedToModifyFiles: mockPermission }) })); +jest.mock('../github_project_client_factory', () => ({ createGraphqlTransportClient: () => ({ getClient: mockGetClient }) })); +jest.mock('../../pre_branch_sdd_workspace_adapter', () => ({ PreBranchSddWorkspaceAdapter: jest.fn(() => ({ + loadSnapshot: mockSnapshot, loadSnapshotAtCommit: mockSnapshot, verifyPublication: mockVerify, isAncestor: mockAncestor, + publish: mockWrite, ensureBranch: mockWrite, +})) })); + +describe('composed read-only SDD query', () => { + it('reads owned publication and currently authorized answers with the bound repository identity while discarding mutation capabilities', async () => { + const binding = { owner: 'acme', repository: 'demo', token: 'fixture-token' }; + const context = { issueNumber: 42, admittedKind: 'feature' as const, profileDigest: 'profile', issueAuthor: 'author', + trustedBotLogin: 'vypbot', issueManagedBranches: true, preBranchSdd: true, baseBranch: 'develop' }; + const questionText = 'Which current behavior should be adopted?'; + const record: SddGateRecord = { version: 1, issueNumber: 42, phase: 'awaiting-answer', round: 2, + issueDigest: buildSddIssueDigest({ issueNumber: 42, issueTitle: 'Feature', issueBody: 'Human requirements', admittedKind: 'feature', profileDigest: 'profile' }, mockBase), + baseSha: mockBase, commitSha: mockFirst, branchName: 'feature/42', + plan: { action: 'update', path: 'specs/feature.md', capabilityId: 'feature', reason: 'This capability owns the feature contract.', + questions: [{ id: 'Q1', text: questionText, owner: 'maintainer' }] }, + answers: [{ questionId: 'Q1', author: 'maintainer', commentId: 6, text: 'Current authorized answer', round: 2, questionText }], + }; + const listIssueComments = jest.fn(async () => ({ value: [{ id: 7, body: renderSddGateRecord(record), user: { login: 'vypbot' } }], + coverage: { source: 'issue-comments' as const, status: 'complete' as const, pagesFetched: 1, itemsFetched: 1, itemsRetained: 1, + omittedItems: 0, truncatedItems: 0, limitReached: false } })); + const query = createSddEvidenceQueryPort(binding, { listIssueComments }); + expect(Object.isFrozen(query)).toBe(true); + expect(Object.keys(query)).toEqual(['read']); + expect(await query.read(context)).toMatchObject({ state: 'publication-pending', pendingQuestionIds: [], verified: { firstSha: mockFirst, branchHeadSha: mockHead } }); + expect(mockTitle).toHaveBeenCalledWith('acme', 'demo', 42, 'fixture-token'); + expect(mockDescription).toHaveBeenCalledWith('acme', 'demo', 42, 'fixture-token'); + expect(mockLabels).toHaveBeenCalledWith('acme', 'demo', 42, 'fixture-token'); + expect(mockPermission).toHaveBeenCalledWith('acme', 'demo', 'maintainer', 'fixture-token'); + expect(mockGetClient).toHaveBeenCalledWith('fixture-token'); + expect(mockGraphql).toHaveBeenCalledWith(expect.any(String), { owner: 'acme', repository: 'demo', issueNumber: 42, ref: 'refs/heads/feature/42' }); + expect(mockSnapshot).toHaveBeenCalledWith(mockFirst); + expect(mockVerify).toHaveBeenCalledWith('feature/42', mockBase, mockFirst, 'specs/feature.md'); + expect(mockWrite).not.toHaveBeenCalled(); + }); +}); diff --git a/src/infrastructure/composition/main_run_route_composition_root.ts b/src/infrastructure/composition/main_run_route_composition_root.ts index 808abbcc4..f6e6965d3 100644 --- a/src/infrastructure/composition/main_run_route_composition_root.ts +++ b/src/infrastructure/composition/main_run_route_composition_root.ts @@ -1,4 +1,5 @@ import type { RememberBranchQueryPorts } from '../../application/usecases/comment_automation_contracts'; +import { createSddEvidenceQueryPort } from './sdd_evidence_composition_root'; import { LinkedBranchReadinessRepository } from '../../data/repository/branch/linked_branch_readiness_repository'; import { PullRequestChangesRepository } from '../../data/repository/pull_request/pull_request_changes_repository'; import { CommitUseCase } from "../../application/usecases/commit_use_case"; @@ -239,6 +240,7 @@ export function createIssueCommentUseCaseCompositionRoot(binding: BugbotScmBindi createIssueUseCaseCompositionRoot(binding), rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context, + createSddEvidenceQueryPort(binding, bugbot.scm.context), ); } diff --git a/src/infrastructure/composition/sdd_evidence_composition_root.ts b/src/infrastructure/composition/sdd_evidence_composition_root.ts new file mode 100644 index 000000000..2726a0ea7 --- /dev/null +++ b/src/infrastructure/composition/sdd_evidence_composition_root.ts @@ -0,0 +1,33 @@ +import { SddEvidenceQueryUseCase } from '../../application/usecases/sdd/sdd_evidence_query_use_case'; +import { bindSddEvidenceReadPorts, type SddEvidenceReadPorts, type SddEvidenceQueryPort } from '../../application/ports/sdd_evidence_read_ports'; +import type { BoundBugbotContextReadPorts } from '../../application/ports/bugbot_context_ports'; +import { LinkedBranchReadinessRepository } from '../../data/repository/branch/linked_branch_readiness_repository'; +import { PreBranchSddWorkspaceAdapter } from '../pre_branch_sdd_workspace_adapter'; +import type { BugbotScmBinding } from './bugbot_scm_port_factory'; +import { createGraphqlTransportClient } from './github_project_client_factory'; +import { createIssueMetadataCompositionRoot } from './issue_metadata_composition_root'; +import { createIssueContentCompositionRoot } from './issue_content_composition_root'; +import { createIssueLabelRepository } from './issue_labels_composition_root'; +import { createActorAuthorizationRepository } from './actor_authorization_composition_root'; +import { bindActorAuthorization, bindIssueLabels } from './lifecycle_capability_port_binding'; +import { bindIssueDescriptionQuery } from './shared_capability_port_binding'; + +/** No provisioning, mutation, agent or approval capability crosses this binding. */ +export function createSddEvidenceReadPorts(binding: BugbotScmBinding, comments: Pick): SddEvidenceReadPorts { + const titles = createIssueMetadataCompositionRoot(); + const branches = new LinkedBranchReadinessRepository(createGraphqlTransportClient()); + return bindSddEvidenceReadPorts({ + comments, + descriptions: bindIssueDescriptionQuery(createIssueContentCompositionRoot(), binding), + titles: { getTitle: issueNumber => titles.getTitle(binding.owner, binding.repository, issueNumber, binding.token) }, + labels: bindIssueLabels(createIssueLabelRepository(), binding), + actors: bindActorAuthorization(createActorAuthorizationRepository(), binding), + branches: { getLinkedBranch: (issueNumber, name) => branches.getLinkedBranch(binding.owner, binding.repository, issueNumber, name, binding.token) }, + workspace: new PreBranchSddWorkspaceAdapter(process.cwd(), binding.token), + }); +} + +export function createSddEvidenceQueryPort(binding: BugbotScmBinding, comments: Pick): SddEvidenceQueryPort { + const query = new SddEvidenceQueryUseCase(createSddEvidenceReadPorts(binding, comments)); + return Object.freeze({ read: query.read.bind(query) }); +} diff --git a/src/infrastructure/pre_branch_sdd_workspace_adapter.ts b/src/infrastructure/pre_branch_sdd_workspace_adapter.ts index f8013101f..28646c542 100644 --- a/src/infrastructure/pre_branch_sdd_workspace_adapter.ts +++ b/src/infrastructure/pre_branch_sdd_workspace_adapter.ts @@ -38,6 +38,11 @@ export class PreBranchSddWorkspaceAdapter implements PreBranchSddWorkspacePort { return this.readSnapshotAtSha(baseSha); } + async loadSnapshotAtCommit(commitSha: string): Promise { + assertSha(commitSha); + return this.readSnapshotAtSha(commitSha); + } + private async readSnapshotAtSha(baseSha: string): Promise { const raw = await this.git(['show', `${baseSha}:specs/catalog.json`]); const catalog = JSON.parse(raw) as Catalog; From fb8f3d9b93e91c22ff4e6c3be2e3fb39daf590b4 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 17:03:00 +0200 Subject: [PATCH 06/12] bugfix(#454): Bind SDD navigation to trusted server and retain unknown readiness --- build/cli/index.js | 12 ++++--- build/github_action/index.js | 12 ++++--- docs/issues/comment-commands.mdx | 4 +++ specs/pre-branch-sdd-status-readiness.md | 31 ++++++++++++++++--- .../__tests__/sdd_status_policy.test.ts | 19 +++++++++++- .../__tests__/status_command_policy.test.ts | 13 ++++++++ src/application/policies/sdd_status_policy.ts | 7 +++-- .../policies/status_command_policy.ts | 5 +-- .../ports/sdd_evidence_read_ports.ts | 2 ++ .../usecases/read_copilot_status_workflow.ts | 2 +- .../main_run_route_composition_root.test.ts | 2 +- .../sdd_evidence_composition_root.test.ts | 3 +- .../sdd_evidence_composition_root.ts | 2 +- 13 files changed, 92 insertions(+), 22 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 8d05e5945..cc309b5e7 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -43680,6 +43680,10 @@ function safeIssueUrl(value, issueNumber, identity) { return undefined; try { const url = new URL(value); + const server = new URL(identity.serverOrigin ?? 'https://github.com'); + if (server.protocol !== 'https:' || server.username || server.password || server.search || server.hash + || server.pathname !== '/' || url.origin !== server.origin) + return undefined; if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || !/^\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/issues\/[1-9][0-9]*$/u.test(url.pathname) || url.pathname.toLowerCase() !== `/${identity.owner}/${identity.repository}/issues/${issueNumber}`.toLowerCase()) @@ -48751,7 +48755,7 @@ function buildCopilotStatusSnapshot(execution) { ...(findingStateProjection.status === 'invalid' ? { findingStateEvidence: 'invalid' } : {}), pullRequestDescriptionMode: execution.ai.getPullRequestDescriptionMode(), ...(execution.issue?.url ? { issueUrl: execution.issue.url } : {}), - ...(execution.issueWorkflowKind ? { sddReadiness: (0, sdd_status_policy_1.initialSddStatus)({ + ...(!isPullRequestTarget && !execution.isPush && (execution.issue?.number > 0 || execution.isIssue) ? { sddReadiness: (0, sdd_status_policy_1.initialSddStatus)({ admittedKind: issue_workflow_profile_1.ISSUE_WORKFLOW_KINDS.find(kind => kind === execution.issueWorkflowKind), issueManagedBranches: execution.issue.issueManagedBranches, preBranchSdd: execution.preBranchSdd, @@ -48791,7 +48795,7 @@ function formatCopilotStatus(snapshot, locale = 'en-US', catalog = (0, publicati lines.push(label('interaction.status.findings', catalog.render('interaction.status.findingCounts', snapshot.findingStates))); } if (snapshot.sddReadiness) - lines.push(...(0, sdd_status_policy_1.formatSddStatus)(snapshot.sddReadiness, catalog, snapshot.issueUrl, snapshot.issueNumber, { owner: snapshot.owner, repository: snapshot.repository })); + lines.push(...(0, sdd_status_policy_1.formatSddStatus)(snapshot.sddReadiness, catalog, snapshot.issueUrl, snapshot.issueNumber, { owner: snapshot.owner, repository: snapshot.repository, serverOrigin: snapshot.serverOrigin })); return lines.join('\n'); } @@ -54817,7 +54821,7 @@ const bugbot_context_request_1 = __nccwpck_require__(98299); const application_error_1 = __nccwpck_require__(75999); /** Reads the current canonical card, without an agent, diff, rules or workspace mutation. */ async function readCopilotStatus(param, taskId, ports, sddQuery) { - let status = param.status; + let status = sddQuery ? Object.freeze({ ...param.status, serverOrigin: sddQuery.serverOrigin }) : param.status; if (status.target === 'issue' && param.sdd && status.issueNumber === param.sdd.issueNumber && sddQuery && status.sddReadiness) { const evidence = await sddQuery.read(param.sdd); status = Object.freeze({ ...status, sddReadiness: (0, sdd_status_policy_1.projectSddStatus)(evidence, status.sddReadiness) }); @@ -86336,7 +86340,7 @@ function createSddEvidenceReadPorts(binding, comments) { } function createSddEvidenceQueryPort(binding, comments) { const query = new sdd_evidence_query_use_case_1.SddEvidenceQueryUseCase(createSddEvidenceReadPorts(binding, comments)); - return Object.freeze({ read: query.read.bind(query) }); + return Object.freeze({ read: query.read.bind(query), serverOrigin: process.env.GITHUB_SERVER_URL ?? 'https://github.com' }); } diff --git a/build/github_action/index.js b/build/github_action/index.js index b950ccc50..2fec69c9d 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -46609,6 +46609,10 @@ function safeIssueUrl(value, issueNumber, identity) { return undefined; try { const url = new URL(value); + const server = new URL(identity.serverOrigin ?? 'https://github.com'); + if (server.protocol !== 'https:' || server.username || server.password || server.search || server.hash + || server.pathname !== '/' || url.origin !== server.origin) + return undefined; if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || !/^\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/issues\/[1-9][0-9]*$/u.test(url.pathname) || url.pathname.toLowerCase() !== `/${identity.owner}/${identity.repository}/issues/${issueNumber}`.toLowerCase()) @@ -48499,7 +48503,7 @@ function buildCopilotStatusSnapshot(execution) { ...(findingStateProjection.status === 'invalid' ? { findingStateEvidence: 'invalid' } : {}), pullRequestDescriptionMode: execution.ai.getPullRequestDescriptionMode(), ...(execution.issue?.url ? { issueUrl: execution.issue.url } : {}), - ...(execution.issueWorkflowKind ? { sddReadiness: (0, sdd_status_policy_1.initialSddStatus)({ + ...(!isPullRequestTarget && !execution.isPush && (execution.issue?.number > 0 || execution.isIssue) ? { sddReadiness: (0, sdd_status_policy_1.initialSddStatus)({ admittedKind: issue_workflow_profile_1.ISSUE_WORKFLOW_KINDS.find(kind => kind === execution.issueWorkflowKind), issueManagedBranches: execution.issue.issueManagedBranches, preBranchSdd: execution.preBranchSdd, @@ -48539,7 +48543,7 @@ function formatCopilotStatus(snapshot, locale = 'en-US', catalog = (0, publicati lines.push(label('interaction.status.findings', catalog.render('interaction.status.findingCounts', snapshot.findingStates))); } if (snapshot.sddReadiness) - lines.push(...(0, sdd_status_policy_1.formatSddStatus)(snapshot.sddReadiness, catalog, snapshot.issueUrl, snapshot.issueNumber, { owner: snapshot.owner, repository: snapshot.repository })); + lines.push(...(0, sdd_status_policy_1.formatSddStatus)(snapshot.sddReadiness, catalog, snapshot.issueUrl, snapshot.issueNumber, { owner: snapshot.owner, repository: snapshot.repository, serverOrigin: snapshot.serverOrigin })); return lines.join('\n'); } @@ -54835,7 +54839,7 @@ const bugbot_context_request_1 = __nccwpck_require__(98299); const application_error_1 = __nccwpck_require__(75999); /** Reads the current canonical card, without an agent, diff, rules or workspace mutation. */ async function readCopilotStatus(param, taskId, ports, sddQuery) { - let status = param.status; + let status = sddQuery ? Object.freeze({ ...param.status, serverOrigin: sddQuery.serverOrigin }) : param.status; if (status.target === 'issue' && param.sdd && status.issueNumber === param.sdd.issueNumber && sddQuery && status.sddReadiness) { const evidence = await sddQuery.read(param.sdd); status = Object.freeze({ ...status, sddReadiness: (0, sdd_status_policy_1.projectSddStatus)(evidence, status.sddReadiness) }); @@ -80739,7 +80743,7 @@ function createSddEvidenceReadPorts(binding, comments) { } function createSddEvidenceQueryPort(binding, comments) { const query = new sdd_evidence_query_use_case_1.SddEvidenceQueryUseCase(createSddEvidenceReadPorts(binding, comments)); - return Object.freeze({ read: query.read.bind(query) }); + return Object.freeze({ read: query.read.bind(query), serverOrigin: process.env.GITHUB_SERVER_URL ?? 'https://github.com' }); } diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index 840571468..c1abbe1be 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -280,3 +280,7 @@ The shared reply publisher adds the same current hidden identity used by other correlated replies; the welcome text embeds no separate marker. See [Bugbot detection](/bugbot/detection), [Autofix](/bugbot/autofix), and [Do user request](/bugbot/do-user-request) for the execution details. + +SDD status keeps an unknown requirement visible when the issue has no admitted +workflow kind. Evidence links use the configured GitHub server origin and the +same issue identity; an untrusted or mismatched URL is shown without navigation. diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index eb8ee9783..686373038 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -308,7 +308,7 @@ No schema or configuration migration is introduced. Existing gate records remain ## 14. Testing strategy and numeric budget -Minimum: **141 distinct cases** for this extension. Existing gate and PR status cases are retained but not counted again here. +Minimum: **149 distinct cases** for this extension. Existing gate and PR status cases are retained but not counted again here. | Area | Minimum distinct cases | Behaviors/risks covered | |---|---:|---| @@ -316,11 +316,11 @@ Minimum: **141 distinct cases** for this extension. Existing gate and PR status | Issue identity | 8 | Canonical visible body/title normalization, profile, kind, issue number and original base digest | | Owned card selection | 16 | Exact bot ownership, highest comment identity, corruption without older fallback, duplicate markers, safe IDs and retained answer IDs | | Authority/application/fences | 51 | Immutable first/revision proof, catalog ownership, current authorized answers, partial inventory, issue/card/label/head races, no write capabilities and three status integration outcomes | -| Closed projection/UX | 29 | Canonical requirement/configuration, eight evidence outcomes, withheld historical SHA, malformed proof, safe same-issue links, bounded IDs and both bundled locales | +| Closed projection/UX | 37 | Canonical requirement/configuration, eight evidence outcomes, withheld historical SHA, malformed proof, safe same-issue links, bounded IDs and both bundled locales | | Native linked ref adapter | 4 | Stale/deleted refs, foreign/missing repository identity; none may fall back to another PR | | Immutable workspace reads | 2 | Read the original catalog without checkout/current-file mutation; reject non-SHA identifiers | | Read-only composition | 1 | Repository-bound live publication query and current maintainer authorization without mutation capability | -| **Total** | **141** | No case counted twice | +| **Total** | **149** | No case counted twice | Maintain repository-wide thresholds. New pure readiness policy should meet at least 95% branch coverage; changed status modules should meet existing module thresholds, with a target of 90% branches and 95% lines/statements. Use deterministic records, clocks, provider fakes, and publication proofs; no sleeps or live services. Parse catalog structures and gate records rather than grep prose. UI fixtures require semantic assertions beyond snapshots. Manual acceptance confirms the section is readable on mobile and desktop and preserves the existing status ordering. @@ -368,7 +368,7 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex 2. Define typed readiness result and semantic read/verification ports; add pure policy tests for ownership, identity, pending IDs, and state mapping. 3. Integrate read-only orchestration into status while retaining the original snapshot on retrieval errors. 4. Add catalog messages and compact rendering for all states; ensure no timeline-card path is invoked. -5. Add adapter, integration, localization, security, and compatibility tests within the 141-case implementation budget. +5. Add adapter, integration, localization, security, and compatibility tests within the 149-case implementation budget. 6. Update command/operator documentation and contract examples. 7. Run specification, architecture, catalog, and repository validation required by the existing project workflow. @@ -378,7 +378,7 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex - [x] `/copilot status` remains read-only and preserves all existing fields and ordering. - [x] English and Spanish catalogs cover the readiness section. - [x] Typed application ports and Clean Architecture boundaries are enforced by tests/checks. -- [x] All 141 distinct extension cases, full coverage budgets and generated bundles pass before publication. +- [x] All 149 distinct extension cases, full coverage budgets and generated bundles pass before publication. - [x] User documentation explains the evidence limit and fail-closed behavior. - [x] Existing command grammar, lifecycle, labels, finding counts, and gate record ownership remain compatible. - [x] `specs/catalog.json` evidence and generated `specs/CATALOG.md` remain current; `pnpm run validate:specifications` passes. @@ -412,3 +412,24 @@ this extension; the gate's existing numeric budget does not count them again. ### Offline implementation validation — 2026-10-11 The 141 distinct extension cases are implemented. Full validation on the Action-managed head `43d710770972df3443b519ca0bd61c6ad6256c93` plus this contribution passed 567 Jest suites with 7,485 tests passed and 35 configured skips. Repository coverage budgets and the setup acceptance ledger passed; the ledger retains its 42 explicitly open cases. New identity and card policies and the read-only composition have 100% coverage; projection branch coverage is 96% and query branch coverage is 97.36%. Typecheck, lint, documentation, specification validation and generated bundles passed. Live acceptance remains pending promotion and is not inferred from these offline checks. + +### PR #465 review corrections (2026-10-11) + +The review identified two status defects. Evidence navigation now compares an +issue URL against the trusted workflow server origin supplied by the read-only +composition, defaulting to github.com. Matching owner/repository/issue paths on +another origin cannot produce links. Independently configured GitHub Enterprise +origins remain supported; unsafe server configuration suppresses navigation +without changing the evidence state or exposing its value. + +An issue with unknown admitted workflow kind now retains the SDD section with +unknown requirement and unavailable evidence. PR and push targets do not infer +issue eligibility from a coincidental kind. No model or new mutation capability +is introduced. Eight distinct cases extend the presentation allocation: three +foreign-origin URLs, configured Enterprise navigation, credential-bearing server +configuration, missing issue admission, and PR/push compatibility. + +The initial autofix run 38146515770 admitted the Windows agent but produced no +safe changed paths. These corrections are a normal contributor response on the +exact Action branch and are not evidence that the authenticated fixer succeeded. +Live recheck and native status acceptance remain pending. diff --git a/src/application/policies/__tests__/sdd_status_policy.test.ts b/src/application/policies/__tests__/sdd_status_policy.test.ts index 57e8efb47..c79ba588a 100644 --- a/src/application/policies/__tests__/sdd_status_policy.test.ts +++ b/src/application/policies/__tests__/sdd_status_policy.test.ts @@ -45,7 +45,8 @@ describe('SDD status projection and safe navigation', () => { it.each([ 'http://github.com/acme/demo/issues/42', 'https://github.com/foreign/demo/issues/42', 'https://github.com/acme/foreign/issues/42', 'https://github.com/acme/demo/issues/43', - 'https://user:secret@github.com/acme/demo/issues/42', 'https://github.com/acme/demo/issues/42?token=secret', 'broken', + 'https://attacker.example/acme/demo/issues/42', 'https://github.com.attacker.example/acme/demo/issues/42', + 'https://github.com:444/acme/demo/issues/42', 'https://user:secret@github.com/acme/demo/issues/42', 'https://github.com/acme/demo/issues/42?token=secret', 'broken', ])('omits all evidence links for an invalid issue identity %s', url => { const status = projectSddStatus({ state: 'published', path: verified.path, verified, cardId: 7 }); const body = render(status, url); @@ -71,3 +72,19 @@ describe('SDD status projection and safe navigation', () => { } }); }); + + describe('trusted GitHub server navigation', () => { + it('retains GitHub Enterprise links only for the independently configured origin', () => { + const status = projectSddStatus({ state: 'published', path: verified.path, verified, cardId: 7 }); + const body = formatSddStatus(status, resolveStaticPublicationCatalog('en-US').catalog, 'https://github.example/acme/demo/issues/42', 42, { ...identity, serverOrigin: 'https://github.example' }).join('\n'); + expect(body).toContain(`https://github.example/acme/demo/blob/${sha}/specs/feature.md`); + expect(body).toContain('https://github.example/acme/demo/issues/42#issuecomment-7'); + }); + it('omits links when the trusted server configuration has credentials', () => { + const status = projectSddStatus({ state: 'published', path: verified.path, verified, cardId: 7 }); + const body = formatSddStatus(status, resolveStaticPublicationCatalog('en-US').catalog, issueUrl, 42, { ...identity, serverOrigin: 'https://user:secret@github.com' }).join('\n'); + expect(body).not.toContain(']('); + expect(body).not.toContain('secret'); + expect(body).toContain(sha); + }); +}); diff --git a/src/application/policies/__tests__/status_command_policy.test.ts b/src/application/policies/__tests__/status_command_policy.test.ts index ba163ed75..569ed6a8e 100644 --- a/src/application/policies/__tests__/status_command_policy.test.ts +++ b/src/application/policies/__tests__/status_command_policy.test.ts @@ -497,3 +497,16 @@ describe('status command policy', () => { expect(body).not.toContain('ready'); }); }); + +describe('unknown issue admission status', () => { + it('shows unknown requirement and unavailable SDD evidence for an issue with no admitted kind', () => { + const snapshot = buildCopilotStatusSnapshot(execution({ isIssue: true, isPullRequest: false, issueNumber: 17, issue: { number: 17 }, pullRequest: { number: -1 }, issueWorkflowKind: undefined }) as never); + expect(snapshot.target).toBe('issue'); + expect(snapshot.sddReadiness).toEqual({ required: null, state: 'unavailable' }); + expect(formatCopilotStatus(snapshot)).toContain('SDD readiness'); + }); + it.each(['pull-request', 'push'])('does not infer issue SDD eligibility for a %s target', target => { + const snapshot = buildCopilotStatusSnapshot(execution({ isPush: target === 'push', isPullRequest: target === 'pull-request', pullRequest: { number: target === 'push' ? -1 : 21 }, issueWorkflowKind: 'feature' }) as never); + expect(snapshot.sddReadiness).toBeUndefined(); + }); +}); diff --git a/src/application/policies/sdd_status_policy.ts b/src/application/policies/sdd_status_policy.ts index 3a4adf1f4..81af265cb 100644 --- a/src/application/policies/sdd_status_policy.ts +++ b/src/application/policies/sdd_status_policy.ts @@ -38,7 +38,7 @@ export function projectSddStatus(evidence: SddEvidenceSnapshot, initial?: SddSta } /** Receives only the closed projection, never question prose, record reasons or provider errors. */ -export function formatSddStatus(readiness: SddStatusReadiness, catalog: PublicationMessageCatalog, issueUrl?: string, issueNumber?: number, identity?: { readonly owner: string; readonly repository: string }): string[] { +export function formatSddStatus(readiness: SddStatusReadiness, catalog: PublicationMessageCatalog, issueUrl?: string, issueNumber?: number, identity?: { readonly owner: string; readonly repository: string; readonly serverOrigin?: string }): string[] { const path = readiness.path && isSafeSddPath(readiness.path) ? readiness.path : undefined; const commitSha = readiness.commitSha && /^[a-f0-9]{40}$/u.test(readiness.commitSha) ? readiness.commitSha : undefined; const pending = readiness.pendingQuestionIds?.filter(id => /^Q[1-8]$/u.test(id)); @@ -70,10 +70,13 @@ export function formatSddStatus(readiness: SddStatusReadiness, catalog: Publicat return lines; } -function safeIssueUrl(value: string | undefined, issueNumber: number | undefined, identity: { readonly owner: string; readonly repository: string } | undefined): { repository: string; issue: string } | undefined { +function safeIssueUrl(value: string | undefined, issueNumber: number | undefined, identity: { readonly owner: string; readonly repository: string; readonly serverOrigin?: string } | undefined): { repository: string; issue: string } | undefined { if (!identity || !value || value.length > 2048 || !Number.isSafeInteger(issueNumber) || issueNumber! < 1) return undefined; try { const url = new URL(value); + const server = new URL(identity.serverOrigin ?? 'https://github.com'); + if (server.protocol !== 'https:' || server.username || server.password || server.search || server.hash + || server.pathname !== '/' || url.origin !== server.origin) return undefined; if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || !/^\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/issues\/[1-9][0-9]*$/u.test(url.pathname) || url.pathname.toLowerCase() !== `/${identity.owner}/${identity.repository}/issues/${issueNumber}`.toLowerCase()) return undefined; diff --git a/src/application/policies/status_command_policy.ts b/src/application/policies/status_command_policy.ts index cef1c6e22..c929d4f0b 100644 --- a/src/application/policies/status_command_policy.ts +++ b/src/application/policies/status_command_policy.ts @@ -64,6 +64,7 @@ export interface CopilotStatusSnapshot { readonly pullRequestDescriptionMode: string; readonly sddReadiness?: SddStatusReadiness; readonly issueUrl?: string; + readonly serverOrigin?: string; } /** Builds a read-only status snapshot from the facts already loaded by setup. */ @@ -123,7 +124,7 @@ export function buildCopilotStatusSnapshot(execution: CopilotStatusExecutionCont ...(findingStateProjection.status === 'invalid' ? { findingStateEvidence: 'invalid' as const } : {}), pullRequestDescriptionMode: execution.ai.getPullRequestDescriptionMode(), ...(execution.issue?.url ? { issueUrl: execution.issue.url } : {}), - ...(execution.issueWorkflowKind ? { sddReadiness: initialSddStatus({ + ...(!isPullRequestTarget && !execution.isPush && (execution.issue?.number > 0 || execution.isIssue) ? { sddReadiness: initialSddStatus({ admittedKind: ISSUE_WORKFLOW_KINDS.find(kind => kind === execution.issueWorkflowKind), issueManagedBranches: execution.issue.issueManagedBranches, preBranchSdd: execution.preBranchSdd, @@ -167,6 +168,6 @@ export function formatCopilotStatus( } else if (snapshot.findingStates) { lines.push(label('interaction.status.findings', catalog.render('interaction.status.findingCounts', snapshot.findingStates))); } - if (snapshot.sddReadiness) lines.push(...formatSddStatus(snapshot.sddReadiness, catalog, snapshot.issueUrl, snapshot.issueNumber, { owner: snapshot.owner, repository: snapshot.repository })); + if (snapshot.sddReadiness) lines.push(...formatSddStatus(snapshot.sddReadiness, catalog, snapshot.issueUrl, snapshot.issueNumber, { owner: snapshot.owner, repository: snapshot.repository, serverOrigin: snapshot.serverOrigin })); return lines.join('\n'); } diff --git a/src/application/ports/sdd_evidence_read_ports.ts b/src/application/ports/sdd_evidence_read_ports.ts index bbeaa0e56..fdf69db80 100644 --- a/src/application/ports/sdd_evidence_read_ports.ts +++ b/src/application/ports/sdd_evidence_read_ports.ts @@ -68,5 +68,7 @@ export interface SddEvidenceSnapshot { /** Application query capability supplied by composition; it cannot expose raw adapters. */ export interface SddEvidenceQueryPort { + /** Trusted workflow server origin; never inferred from issue or comment content. */ + readonly serverOrigin?: string; read(context: SddEvidenceQueryContext): Promise; } diff --git a/src/application/usecases/read_copilot_status_workflow.ts b/src/application/usecases/read_copilot_status_workflow.ts index d58e71d75..9a137d729 100644 --- a/src/application/usecases/read_copilot_status_workflow.ts +++ b/src/application/usecases/read_copilot_status_workflow.ts @@ -15,7 +15,7 @@ export type CopilotStatusReadPorts = Pick { expect.objectContaining({ read: expect.any(Function) }), ); const query = jest.mocked(IssueCommentUseCase).mock.calls[0][15]; - expect(Object.keys(query!)).toEqual(['read']); + expect(Object.keys(query!)).toEqual(['read', 'serverOrigin']); expect(Object.isFrozen(query)).toBe(true); }); diff --git a/src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts b/src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts index 6d54dcce0..67ffd8e83 100644 --- a/src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts @@ -47,7 +47,8 @@ describe('composed read-only SDD query', () => { omittedItems: 0, truncatedItems: 0, limitReached: false } })); const query = createSddEvidenceQueryPort(binding, { listIssueComments }); expect(Object.isFrozen(query)).toBe(true); - expect(Object.keys(query)).toEqual(['read']); + expect(Object.keys(query)).toEqual(['read', 'serverOrigin']); + expect(query.serverOrigin).toBe(process.env.GITHUB_SERVER_URL ?? 'https://github.com'); expect(await query.read(context)).toMatchObject({ state: 'publication-pending', pendingQuestionIds: [], verified: { firstSha: mockFirst, branchHeadSha: mockHead } }); expect(mockTitle).toHaveBeenCalledWith('acme', 'demo', 42, 'fixture-token'); expect(mockDescription).toHaveBeenCalledWith('acme', 'demo', 42, 'fixture-token'); diff --git a/src/infrastructure/composition/sdd_evidence_composition_root.ts b/src/infrastructure/composition/sdd_evidence_composition_root.ts index 2726a0ea7..bd4fd484f 100644 --- a/src/infrastructure/composition/sdd_evidence_composition_root.ts +++ b/src/infrastructure/composition/sdd_evidence_composition_root.ts @@ -29,5 +29,5 @@ export function createSddEvidenceReadPorts(binding: BugbotScmBinding, comments: export function createSddEvidenceQueryPort(binding: BugbotScmBinding, comments: Pick): SddEvidenceQueryPort { const query = new SddEvidenceQueryUseCase(createSddEvidenceReadPorts(binding, comments)); - return Object.freeze({ read: query.read.bind(query) }); + return Object.freeze({ read: query.read.bind(query), serverOrigin: process.env.GITHUB_SERVER_URL ?? 'https://github.com' }); } From 6604d074d78dc49d813a86d419ec5a3f5e52eb7d Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 21:50:16 +0200 Subject: [PATCH 07/12] docs(status): distinguish SDD readiness from completed implementation --- docs/issues/comment-commands.mdx | 4 +++- specs/pre-branch-sdd-status-readiness.md | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index d2d3787f0..038e103d1 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -127,7 +127,9 @@ the owning specification, then links the verified current publication once ready. Old answer rounds and stale issue or branch evidence cannot establish readiness. A `published` marker or existing branch alone never means the SDD is ready. Missing or unverified evidence blocks a readiness claim; status remains -read-only and preserves its existing fields. +read-only and preserves its existing fields. SDD readiness confirms the published +specification and its branch evidence. It does not certify that implementation +is finished, that CI or review passed, or that the PR can be merged. Read-only commands answer the current comment, using the issue or PR as background. Add the file, symbol, failure or requested work after the command to diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index 686373038..3cc43783b 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -433,3 +433,8 @@ The initial autofix run 38146515770 admitted the Windows agent but produced no safe changed paths. These corrections are a normal contributor response on the exact Action branch and are not evidence that the authenticated fixer succeeded. Live recheck and native status acceptance remain pending. + + +### Native dogfooding evidence after parent synchronization + +Action run `38166458576` on `windows-intel-runner-1` completed 570 suites and 7,576 tests with 20 platform skips, then published synchronized head `3edeb4da098712385c9176c918d3dd0814883296` and its correlated receipt for comment `6112587961`. Its native PowerShell hash-module and clean-checkout fixtures passed in 1.086, 1.982 and 1.828 seconds respectively, with unchanged isolation and budgets. The synchronized bundles validate locally. This establishes native verification and an actual pushed branch, not Windows agent writing or live status acceptance after promotion. User documentation explicitly separates SDD readiness from implementation completion, CI/review and merge authorization. From bad9f8a2cbc21deabc505cd2356937530cb9da2c Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 22:17:03 +0200 Subject: [PATCH 08/12] feature: retain verified SDD publication history in status --- build/cli/index.js | 30 ++++++-- build/github_action/index.js | 30 ++++++-- docs/issues/comment-commands.mdx | 7 ++ specs/pre-branch-sdd-gate.md | 2 +- specs/pre-branch-sdd-status-readiness.md | 18 +++-- .../__tests__/sdd_status_policy.test.ts | 76 ++++++++++++++++++- .../policies/publication_message_catalog.ts | 6 ++ src/application/policies/sdd_status_policy.ts | 27 +++++-- .../sdd_evidence_query_use_case.test.ts | 11 +++ 9 files changed, 179 insertions(+), 28 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 6c7feb53c..02ba388dc 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -42499,10 +42499,12 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.status.sddUnverified', 'interaction.status.sddAwaitingAnswers', 'interaction.status.sddPublicationPending', + 'interaction.status.sddRevisionPending', 'interaction.status.sddReady', 'interaction.status.sddOwner', 'interaction.status.sddQuestions', 'interaction.status.sddCommit', + 'interaction.status.sddFirstCommit', 'interaction.status.sddNextInspect', 'interaction.status.sddNextAnswers', 'interaction.status.sddNextPublication', @@ -42628,10 +42630,12 @@ const ENGLISH_MESSAGES = Object.freeze({ 'interaction.status.sddUnverified': 'Publication evidence is invalid, absent or stale; readiness is unverified.', 'interaction.status.sddAwaitingAnswers': 'Answers required', 'interaction.status.sddPublicationPending': 'Publication pending', + 'interaction.status.sddRevisionPending': 'Revision pending; current readiness is unverified.', 'interaction.status.sddReady': 'Published and verified', 'interaction.status.sddOwner': 'Specification', 'interaction.status.sddQuestions': 'Pending questions', 'interaction.status.sddCommit': 'Verified publication', + 'interaction.status.sddFirstCommit': 'First verified publication (history)', 'interaction.status.sddNextInspect': 'Inspect the SDD card and retry after correcting its evidence.', 'interaction.status.sddNextAnswers': 'Answer these questions in the issue using their question identifiers.', 'interaction.status.sddNextPublication': 'Resume SDD preparation to publish the approved specification.', @@ -42758,10 +42762,12 @@ const SPANISH_MESSAGES = Object.freeze({ 'interaction.status.sddUnverified': 'La publicación es inválida, está ausente o desactualizada; preparación sin verificar.', 'interaction.status.sddAwaitingAnswers': 'Respuestas necesarias', 'interaction.status.sddPublicationPending': 'Publicación pendiente', + 'interaction.status.sddRevisionPending': 'Revisión pendiente; preparación actual sin verificar.', 'interaction.status.sddReady': 'Publicado y verificado', 'interaction.status.sddOwner': 'Especificación', 'interaction.status.sddQuestions': 'Preguntas pendientes', 'interaction.status.sddCommit': 'Publicación verificada', + 'interaction.status.sddFirstCommit': 'Primera publicación verificada (histórica)', 'interaction.status.sddNextInspect': 'Revisa la tarjeta del SDD y reintenta tras corregir su evidencia.', 'interaction.status.sddNextAnswers': 'Responde estas preguntas en la issue usando sus identificadores.', 'interaction.status.sddNextPublication': 'Retoma la preparación del SDD para publicar la especificación aprobada.', @@ -43631,14 +43637,19 @@ function initialSddStatus(context, labels) { } } function projectSddStatus(evidence, initial) { + const firstCommitSha = evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.firstSha ?? '') + ? evidence.verified.firstSha : undefined; + const revisionPending = (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') && Boolean(firstCommitSha); const state = evidence.state === 'not-required' ? 'not-required' : evidence.state === 'absent' || evidence.state === 'unavailable' ? 'unavailable' : evidence.state === 'stale' || evidence.state === 'invalid' ? 'unverified' - : evidence.state === 'published' ? (evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.currentSha ?? '') ? 'ready' : 'unverified') : evidence.state; + : evidence.state === 'published' ? (evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.currentSha ?? '') ? 'ready' : 'unverified') + : revisionPending ? 'revision-pending' : evidence.state; return Object.freeze({ state, required: state === 'not-required' ? false : state === 'unavailable' ? initial?.required ?? null : true, ...(evidence.path && !['not-required', 'unavailable', 'unverified'].includes(state) ? { path: evidence.path } : {}), - ...(state === 'awaiting-answers' && evidence.pendingQuestionIds ? { pendingQuestionIds: Object.freeze([...evidence.pendingQuestionIds]) } : {}), + ...((state === 'awaiting-answers' || state === 'revision-pending') && evidence.pendingQuestionIds ? { pendingQuestionIds: Object.freeze([...evidence.pendingQuestionIds]) } : {}), ...(state === 'ready' && evidence.verified?.currentSha ? { commitSha: evidence.verified.currentSha } : {}), + ...((state === 'revision-pending' || state === 'ready') && firstCommitSha ? { firstCommitSha } : {}), ...(evidence.cardId ? { cardId: evidence.cardId } : {}), }); } @@ -43646,16 +43657,19 @@ function projectSddStatus(evidence, initial) { function formatSddStatus(readiness, catalog, issueUrl, issueNumber, identity) { const path = readiness.path && (0, pre_branch_sdd_1.isSafeSddPath)(readiness.path) ? readiness.path : undefined; const commitSha = readiness.commitSha && /^[a-f0-9]{40}$/u.test(readiness.commitSha) ? readiness.commitSha : undefined; + const firstCommitSha = readiness.firstCommitSha && /^[a-f0-9]{40}$/u.test(readiness.firstCommitSha) ? readiness.firstCommitSha : undefined; const pending = readiness.pendingQuestionIds?.filter(id => /^Q[1-8]$/u.test(id)); - const state = readiness.state === 'ready' && (!path || !commitSha) ? 'unverified' : readiness.state; + const state = (readiness.state === 'ready' && (!path || !commitSha)) + || (readiness.state === 'revision-pending' && (!path || !firstCommitSha)) ? 'unverified' : readiness.state; const message = { 'not-required': 'interaction.status.sddNotRequired', unavailable: 'interaction.status.sddUnavailable', unverified: 'interaction.status.sddUnverified', 'awaiting-answers': 'interaction.status.sddAwaitingAnswers', - 'publication-pending': 'interaction.status.sddPublicationPending', ready: 'interaction.status.sddReady', + 'publication-pending': 'interaction.status.sddPublicationPending', 'revision-pending': 'interaction.status.sddRevisionPending', ready: 'interaction.status.sddReady', }; const next = { 'not-required': undefined, unavailable: 'interaction.status.sddNextInspect', unverified: 'interaction.status.sddNextInspect', - 'awaiting-answers': 'interaction.status.sddNextAnswers', 'publication-pending': 'interaction.status.sddNextPublication', ready: 'interaction.status.sddNextReady', + 'awaiting-answers': 'interaction.status.sddNextAnswers', 'publication-pending': 'interaction.status.sddNextPublication', + 'revision-pending': pending?.length ? 'interaction.status.sddNextAnswers' : 'interaction.status.sddNextPublication', ready: 'interaction.status.sddNextReady', }; const label = (id, value) => `- **${catalog.render(id)}:** ${value}`; const lines = ['', `## ${catalog.render('interaction.status.sddHeading')}`, @@ -43666,12 +43680,16 @@ function formatSddStatus(readiness, catalog, issueUrl, issueNumber, identity) { const base = safeIssueUrl(issueUrl, issueNumber, identity); if (path && !['unavailable', 'unverified'].includes(state)) lines.push(label('interaction.status.sddOwner', `\`${path}\``)); - if (state === 'awaiting-answers' && pending?.length) + if ((state === 'awaiting-answers' || state === 'revision-pending') && pending?.length) lines.push(label('interaction.status.sddQuestions', [...new Set(pending)].slice(0, 8).join(', '))); if (state === 'ready' && commitSha && path) { const evidence = base ? `[\`${commitSha}\`](${base.repository}/blob/${commitSha}/${path})` : `\`${commitSha}\``; lines.push(label('interaction.status.sddCommit', evidence)); } + if ((state === 'revision-pending' || (state === 'ready' && firstCommitSha !== commitSha)) && firstCommitSha && path) { + const evidence = base ? `[\`${firstCommitSha}\`](${base.repository}/blob/${firstCommitSha}/${path})` : `\`${firstCommitSha}\``; + lines.push(label('interaction.status.sddFirstCommit', evidence)); + } if (next[state]) lines.push('', catalog.render(next[state])); if (base && Number.isSafeInteger(readiness.cardId) && readiness.cardId > 0) diff --git a/build/github_action/index.js b/build/github_action/index.js index f96806ef9..af2db3fc5 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -45311,10 +45311,12 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.status.sddUnverified', 'interaction.status.sddAwaitingAnswers', 'interaction.status.sddPublicationPending', + 'interaction.status.sddRevisionPending', 'interaction.status.sddReady', 'interaction.status.sddOwner', 'interaction.status.sddQuestions', 'interaction.status.sddCommit', + 'interaction.status.sddFirstCommit', 'interaction.status.sddNextInspect', 'interaction.status.sddNextAnswers', 'interaction.status.sddNextPublication', @@ -45440,10 +45442,12 @@ const ENGLISH_MESSAGES = Object.freeze({ 'interaction.status.sddUnverified': 'Publication evidence is invalid, absent or stale; readiness is unverified.', 'interaction.status.sddAwaitingAnswers': 'Answers required', 'interaction.status.sddPublicationPending': 'Publication pending', + 'interaction.status.sddRevisionPending': 'Revision pending; current readiness is unverified.', 'interaction.status.sddReady': 'Published and verified', 'interaction.status.sddOwner': 'Specification', 'interaction.status.sddQuestions': 'Pending questions', 'interaction.status.sddCommit': 'Verified publication', + 'interaction.status.sddFirstCommit': 'First verified publication (history)', 'interaction.status.sddNextInspect': 'Inspect the SDD card and retry after correcting its evidence.', 'interaction.status.sddNextAnswers': 'Answer these questions in the issue using their question identifiers.', 'interaction.status.sddNextPublication': 'Resume SDD preparation to publish the approved specification.', @@ -45570,10 +45574,12 @@ const SPANISH_MESSAGES = Object.freeze({ 'interaction.status.sddUnverified': 'La publicación es inválida, está ausente o desactualizada; preparación sin verificar.', 'interaction.status.sddAwaitingAnswers': 'Respuestas necesarias', 'interaction.status.sddPublicationPending': 'Publicación pendiente', + 'interaction.status.sddRevisionPending': 'Revisión pendiente; preparación actual sin verificar.', 'interaction.status.sddReady': 'Publicado y verificado', 'interaction.status.sddOwner': 'Especificación', 'interaction.status.sddQuestions': 'Preguntas pendientes', 'interaction.status.sddCommit': 'Publicación verificada', + 'interaction.status.sddFirstCommit': 'Primera publicación verificada (histórica)', 'interaction.status.sddNextInspect': 'Revisa la tarjeta del SDD y reintenta tras corregir su evidencia.', 'interaction.status.sddNextAnswers': 'Responde estas preguntas en la issue usando sus identificadores.', 'interaction.status.sddNextPublication': 'Retoma la preparación del SDD para publicar la especificación aprobada.', @@ -46560,14 +46566,19 @@ function initialSddStatus(context, labels) { } } function projectSddStatus(evidence, initial) { + const firstCommitSha = evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.firstSha ?? '') + ? evidence.verified.firstSha : undefined; + const revisionPending = (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') && Boolean(firstCommitSha); const state = evidence.state === 'not-required' ? 'not-required' : evidence.state === 'absent' || evidence.state === 'unavailable' ? 'unavailable' : evidence.state === 'stale' || evidence.state === 'invalid' ? 'unverified' - : evidence.state === 'published' ? (evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.currentSha ?? '') ? 'ready' : 'unverified') : evidence.state; + : evidence.state === 'published' ? (evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.currentSha ?? '') ? 'ready' : 'unverified') + : revisionPending ? 'revision-pending' : evidence.state; return Object.freeze({ state, required: state === 'not-required' ? false : state === 'unavailable' ? initial?.required ?? null : true, ...(evidence.path && !['not-required', 'unavailable', 'unverified'].includes(state) ? { path: evidence.path } : {}), - ...(state === 'awaiting-answers' && evidence.pendingQuestionIds ? { pendingQuestionIds: Object.freeze([...evidence.pendingQuestionIds]) } : {}), + ...((state === 'awaiting-answers' || state === 'revision-pending') && evidence.pendingQuestionIds ? { pendingQuestionIds: Object.freeze([...evidence.pendingQuestionIds]) } : {}), ...(state === 'ready' && evidence.verified?.currentSha ? { commitSha: evidence.verified.currentSha } : {}), + ...((state === 'revision-pending' || state === 'ready') && firstCommitSha ? { firstCommitSha } : {}), ...(evidence.cardId ? { cardId: evidence.cardId } : {}), }); } @@ -46575,16 +46586,19 @@ function projectSddStatus(evidence, initial) { function formatSddStatus(readiness, catalog, issueUrl, issueNumber, identity) { const path = readiness.path && (0, pre_branch_sdd_1.isSafeSddPath)(readiness.path) ? readiness.path : undefined; const commitSha = readiness.commitSha && /^[a-f0-9]{40}$/u.test(readiness.commitSha) ? readiness.commitSha : undefined; + const firstCommitSha = readiness.firstCommitSha && /^[a-f0-9]{40}$/u.test(readiness.firstCommitSha) ? readiness.firstCommitSha : undefined; const pending = readiness.pendingQuestionIds?.filter(id => /^Q[1-8]$/u.test(id)); - const state = readiness.state === 'ready' && (!path || !commitSha) ? 'unverified' : readiness.state; + const state = (readiness.state === 'ready' && (!path || !commitSha)) + || (readiness.state === 'revision-pending' && (!path || !firstCommitSha)) ? 'unverified' : readiness.state; const message = { 'not-required': 'interaction.status.sddNotRequired', unavailable: 'interaction.status.sddUnavailable', unverified: 'interaction.status.sddUnverified', 'awaiting-answers': 'interaction.status.sddAwaitingAnswers', - 'publication-pending': 'interaction.status.sddPublicationPending', ready: 'interaction.status.sddReady', + 'publication-pending': 'interaction.status.sddPublicationPending', 'revision-pending': 'interaction.status.sddRevisionPending', ready: 'interaction.status.sddReady', }; const next = { 'not-required': undefined, unavailable: 'interaction.status.sddNextInspect', unverified: 'interaction.status.sddNextInspect', - 'awaiting-answers': 'interaction.status.sddNextAnswers', 'publication-pending': 'interaction.status.sddNextPublication', ready: 'interaction.status.sddNextReady', + 'awaiting-answers': 'interaction.status.sddNextAnswers', 'publication-pending': 'interaction.status.sddNextPublication', + 'revision-pending': pending?.length ? 'interaction.status.sddNextAnswers' : 'interaction.status.sddNextPublication', ready: 'interaction.status.sddNextReady', }; const label = (id, value) => `- **${catalog.render(id)}:** ${value}`; const lines = ['', `## ${catalog.render('interaction.status.sddHeading')}`, @@ -46595,12 +46609,16 @@ function formatSddStatus(readiness, catalog, issueUrl, issueNumber, identity) { const base = safeIssueUrl(issueUrl, issueNumber, identity); if (path && !['unavailable', 'unverified'].includes(state)) lines.push(label('interaction.status.sddOwner', `\`${path}\``)); - if (state === 'awaiting-answers' && pending?.length) + if ((state === 'awaiting-answers' || state === 'revision-pending') && pending?.length) lines.push(label('interaction.status.sddQuestions', [...new Set(pending)].slice(0, 8).join(', '))); if (state === 'ready' && commitSha && path) { const evidence = base ? `[\`${commitSha}\`](${base.repository}/blob/${commitSha}/${path})` : `\`${commitSha}\``; lines.push(label('interaction.status.sddCommit', evidence)); } + if ((state === 'revision-pending' || (state === 'ready' && firstCommitSha !== commitSha)) && firstCommitSha && path) { + const evidence = base ? `[\`${firstCommitSha}\`](${base.repository}/blob/${firstCommitSha}/${path})` : `\`${firstCommitSha}\``; + lines.push(label('interaction.status.sddFirstCommit', evidence)); + } if (next[state]) lines.push('', catalog.render(next[state])); if (base && Number.isSafeInteger(readiness.cardId) && readiness.cardId > 0) diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index 038e103d1..62ac504db 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -131,6 +131,13 @@ read-only and preserves its existing fields. SDD readiness confirms the publishe specification and its branch evidence. It does not certify that implementation is finished, that CI or review passed, or that the PR can be merged. +After the first verified publication, a new clarification round reports +**Revision pending**. Its first-publication link is explicitly historical; it +does not certify the pending revision. The section keeps unanswered question +identifiers visible, or asks to resume publication once answers are complete. +Once the revision is verified, status shows the current publication and the +immutable first publication separately when their commits differ. + Read-only commands answer the current comment, using the issue or PR as background. Add the file, symbol, failure or requested work after the command to make the subject explicit. For example, `/copilot explain src/domain/copilot_command.ts` diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index 63419c6e4..5ddb69e75 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -718,7 +718,7 @@ this feature introduces no PRD/ADR documentation or catalog schema. ## Proposed status readiness extension -The proposed read-only status extension from [issue #454](https://github.com/vypdev/copilot/issues/454) is specified in [`pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md). Its 30-case rendering/localization regression budget is separate from the gate's acceptance budget above. The current partial projection fails closed for required evidence, but does not yet project gate records or publication proofs. +The read-only status extension from [issue #454](https://github.com/vypdev/copilot/issues/454) is specified in [`pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md). Its original 30 rendering/localization regression cases are included in the companion's distinct extension budget, separate from the gate's acceptance budget above. The managed implementation reads the latest owned gate record, verifies immutable catalog ownership and remote publication, and fences current issue/card/branch evidence. Live command acceptance and promotion remain pending; local implementation evidence alone does not certify them. ### 1. Executive summary diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index 3cc43783b..826b9bc87 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -308,19 +308,19 @@ No schema or configuration migration is introduced. Existing gate records remain ## 14. Testing strategy and numeric budget -Minimum: **149 distinct cases** for this extension. Existing gate and PR status cases are retained but not counted again here. +Minimum: **170 distinct cases** for this extension. Existing gate and PR status cases are retained but not counted again here. | Area | Minimum distinct cases | Behaviors/risks covered | |---|---:|---| | Existing basic status presentation | 30 | Original issue readiness, unavailable/not-required and bilingual render regression cases | | Issue identity | 8 | Canonical visible body/title normalization, profile, kind, issue number and original base digest | | Owned card selection | 16 | Exact bot ownership, highest comment identity, corruption without older fallback, duplicate markers, safe IDs and retained answer IDs | -| Authority/application/fences | 51 | Immutable first/revision proof, catalog ownership, current authorized answers, partial inventory, issue/card/label/head races, no write capabilities and three status integration outcomes | -| Closed projection/UX | 37 | Canonical requirement/configuration, eight evidence outcomes, withheld historical SHA, malformed proof, safe same-issue links, bounded IDs and both bundled locales | +| Authority/application/fences | 53 | Immutable first/revision proof, catalog ownership, current authorized answers, partial inventory, issue/card/label/head races, no write capabilities, status integration and both pending-revision outcomes | +| Closed projection/UX | 56 | Canonical requirement/configuration, eight evidence outcomes, explicitly historical first publication during a pending revision and alongside a different verified revision, malformed proof, safe same-issue links, bounded IDs and both bundled locales | | Native linked ref adapter | 4 | Stale/deleted refs, foreign/missing repository identity; none may fall back to another PR | | Immutable workspace reads | 2 | Read the original catalog without checkout/current-file mutation; reject non-SHA identifiers | | Read-only composition | 1 | Repository-bound live publication query and current maintainer authorization without mutation capability | -| **Total** | **149** | No case counted twice | +| **Total** | **170** | No case counted twice | Maintain repository-wide thresholds. New pure readiness policy should meet at least 95% branch coverage; changed status modules should meet existing module thresholds, with a target of 90% branches and 95% lines/statements. Use deterministic records, clocks, provider fakes, and publication proofs; no sleeps or live services. Parse catalog structures and gate records rather than grep prose. UI fixtures require semantic assertions beyond snapshots. Manual acceptance confirms the section is readable on mobile and desktop and preserves the existing status ordering. @@ -368,7 +368,7 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex 2. Define typed readiness result and semantic read/verification ports; add pure policy tests for ownership, identity, pending IDs, and state mapping. 3. Integrate read-only orchestration into status while retaining the original snapshot on retrieval errors. 4. Add catalog messages and compact rendering for all states; ensure no timeline-card path is invoked. -5. Add adapter, integration, localization, security, and compatibility tests within the 149-case implementation budget. +5. Add adapter, integration, localization, security, and compatibility tests within the 170-case implementation budget. 6. Update command/operator documentation and contract examples. 7. Run specification, architecture, catalog, and repository validation required by the existing project workflow. @@ -378,7 +378,7 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex - [x] `/copilot status` remains read-only and preserves all existing fields and ordering. - [x] English and Spanish catalogs cover the readiness section. - [x] Typed application ports and Clean Architecture boundaries are enforced by tests/checks. -- [x] All 149 distinct extension cases, full coverage budgets and generated bundles pass before publication. +- [x] All 170 distinct extension cases, full coverage budgets and generated bundles pass before publication. - [x] User documentation explains the evidence limit and fail-closed behavior. - [x] Existing command grammar, lifecycle, labels, finding counts, and gate record ownership remain compatible. - [x] `specs/catalog.json` evidence and generated `specs/CATALOG.md` remain current; `pnpm run validate:specifications` passes. @@ -438,3 +438,9 @@ Live recheck and native status acceptance remain pending. ### Native dogfooding evidence after parent synchronization Action run `38166458576` on `windows-intel-runner-1` completed 570 suites and 7,576 tests with 20 platform skips, then published synchronized head `3edeb4da098712385c9176c918d3dd0814883296` and its correlated receipt for comment `6112587961`. Its native PowerShell hash-module and clean-checkout fixtures passed in 1.086, 1.982 and 1.828 seconds respectively, with unchanged isolation and budgets. The synchronized bundles validate locally. This establishes native verification and an actual pushed branch, not Windows agent writing or live status acceptance after promotion. User documentation explicitly separates SDD readiness from implementation completion, CI/review and merge authorization. + +### Publication history and pending-revision correction from dogfooding + +The original progress comparison exposed the missing revision-pending view. Subsequent real Bugbot findings `4242981181` and `4242981184` identified loss of the first publication alongside a verified revision and an obsolete implementation claim in the owning gate SDD. The projection now retains a independently verified immutable first SHA: pending revisions label it as historical and withhold current readiness; verified revisions show current and first publications separately only when they differ. Current pending question identifiers remain visible. Absent, stale, invalid or denied evidence never promotes historical publication to current readiness. The owning gate SDD and user documentation describe the actual implementation while keeping post-promotion acceptance explicitly open. + +Twenty-one distinct new contracts cover both pending phases, incomplete publication metadata, unknown requirement, bilingual history, invalid navigation, malformed projected evidence, deduplication of identical publication links and real status integration. The extension budget increases from 149 to 170 without recounting shared gate tests. Full local verification passed 570 suites and 7,583 tests with 35 existing platform skips (7,618 total). The changed projection has 100% lines/statements/branches/functions; all existing budgets, typecheck, lint, specifications/catalog, documentation, graph update and compilation passed. Fresh independent review and live command/replay checks still require the published implementation and promotion. diff --git a/src/application/policies/__tests__/sdd_status_policy.test.ts b/src/application/policies/__tests__/sdd_status_policy.test.ts index c79ba588a..7b9727437 100644 --- a/src/application/policies/__tests__/sdd_status_policy.test.ts +++ b/src/application/policies/__tests__/sdd_status_policy.test.ts @@ -25,8 +25,8 @@ describe('SDD status projection and safe navigation', () => { it.each([ ['not-required', 'not-required'], ['absent', 'unavailable'], ['unavailable', 'unavailable'], - ['invalid', 'unverified'], ['stale', 'unverified'], ['awaiting-answers', 'awaiting-answers'], - ['publication-pending', 'publication-pending'], ['published', 'ready'], + ['invalid', 'unverified'], ['stale', 'unverified'], ['awaiting-answers', 'revision-pending'], + ['publication-pending', 'revision-pending'], ['published', 'ready'], ] as const)('projects %s with no historical SHA mistaken for readiness', (state, expected) => { const status = projectSddStatus({ state, path: verified.path, verified, pendingQuestionIds: ['Q1'], cardId: 7 }, { state: 'unavailable', required: true }); expect(status.state).toBe(expected); @@ -35,6 +35,78 @@ describe('SDD status projection and safe navigation', () => { expect(Object.isFrozen(status)).toBe(true); }); + it.each(['awaiting-answers', 'publication-pending'] as const)('retains only immutable first-publication history while %s', state => { + const status = projectSddStatus({ state, path: verified.path, verified: { ...verified, currentSha: undefined }, pendingQuestionIds: ['Q1'] }); + expect(status).toMatchObject({ state: 'revision-pending', firstCommitSha: sha, pendingQuestionIds: ['Q1'] }); + expect(status.commitSha).toBeUndefined(); + }); + it.each(['no proof', 'wrong path', 'abbreviated SHA'])('does not invent revision history with %s', scenario => { + const proof = scenario === 'no proof' ? undefined : { ...verified, + path: scenario === 'wrong path' ? 'specs/foreign.md' : verified.path, + firstSha: scenario === 'abbreviated SHA' ? 'abc123' : sha }; + const status = projectSddStatus({ state: 'publication-pending', path: verified.path, verified: proof }); + expect(status.state).toBe('publication-pending'); expect(status.firstCommitSha).toBeUndefined(); + expect(render(status)).not.toContain('/blob/'); + }); + it.each(['en-US', 'es-ES'])('labels pending revision evidence as history in %s without certifying readiness', locale => { + const status = projectSddStatus({ state: 'awaiting-answers', path: verified.path, + verified: { ...verified, currentSha: undefined }, pendingQuestionIds: ['Q1'], cardId: 7 }); + const body = formatSddStatus(status, resolveStaticPublicationCatalog(locale).catalog, issueUrl, 42, identity).join('\n'); + expect(body).toContain(locale === 'es-ES' ? 'Revisión pendiente' : 'Revision pending'); + expect(body).toContain(locale === 'es-ES' ? '(histórica)' : '(history)'); + expect(body).toContain(`/blob/${sha}/${verified.path}`); expect(body).toContain('Q1'); + expect(body).not.toContain(locale === 'es-ES' ? '**Publicación verificada:**' : '**Verified publication:**'); + expect(body).not.toContain(locale === 'es-ES' ? 'Continúa la implementación' : 'Continue implementation'); + }); + it.each(['missing path', 'missing SHA', 'short SHA'])('withholds malformed revision projection: %s', scenario => { + const body = render({ state: 'revision-pending', required: true, + path: scenario === 'missing path' ? undefined : verified.path, + firstCommitSha: scenario === 'missing SHA' ? undefined : scenario === 'short SHA' ? 'abc123' : sha }); + expect(body).not.toContain('/blob/'); expect(body).not.toContain('(history)'); + expect(body).toContain('readiness is unverified'); + }); + it('offers publication recovery after revision answers are complete', () => { + const body = render(projectSddStatus({ state: 'publication-pending', path: verified.path, verified })); + expect(body).toContain('Resume SDD preparation'); expect(body).not.toContain('Answer these questions'); + }); + it('retains the historical SHA without a link when the issue URL is foreign', () => { + const body = render(projectSddStatus({ state: 'awaiting-answers', path: verified.path, verified }), 'https://attacker.example/acme/demo/issues/42'); + expect(body).toContain(sha); expect(body).toContain('(history)'); expect(body).not.toContain(']('); + }); + it('rejects matching-path revision metadata with a missing first SHA', () => { + const incomplete = { ...verified, firstSha: undefined } as unknown as typeof verified; + const status = projectSddStatus({ state: 'publication-pending', path: verified.path, verified: incomplete }); + expect(status.state).toBe('publication-pending'); expect(status.firstCommitSha).toBeUndefined(); + expect(render(status)).not.toContain('/blob/'); + }); + it('does not certify a published record that only retains the first SHA', () => { + const status = projectSddStatus({ state: 'published', path: verified.path, verified: { ...verified, currentSha: undefined } }); + expect(status.state).toBe('unverified'); expect(status.commitSha).toBeUndefined(); + expect(render(status)).not.toContain('/blob/'); + }); + it('keeps the requirement unknown when unavailable evidence has no admitted initial state', () => { + const status = projectSddStatus({ state: 'unavailable' }); + expect(status).toEqual({ state: 'unavailable', required: null }); + expect(render(status)).toContain('Requirement unavailable'); + }); + it.each(['en-US', 'es-ES'])('retains the immutable first publication alongside a verified revision in %s', locale => { + const current = 'b'.repeat(40); + const status = projectSddStatus({ state: 'published', path: verified.path, verified: { ...verified, currentSha: current } }); + expect(status).toMatchObject({ state: 'ready', commitSha: current, firstCommitSha: sha }); + const body = formatSddStatus(status, resolveStaticPublicationCatalog(locale).catalog, issueUrl, 42, identity).join('\n'); + expect(body).toContain(`/blob/${current}/${verified.path}`); expect(body).toContain(`/blob/${sha}/${verified.path}`); + expect(body).toContain(locale === 'es-ES' ? '(histórica)' : '(history)'); + }); + it('does not duplicate the first publication when it is also the current verified commit', () => { + const status = projectSddStatus({ state: 'published', path: verified.path, verified }); + expect(status.firstCommitSha).toBe(sha); + const body = render(status); expect(body.match(/\/blob\//gu)).toHaveLength(1); expect(body).not.toContain('(history)'); + }); + it('keeps a verified current revision visible without echoing a malformed historical SHA', () => { + const body = render({ state: 'ready', required: true, path: verified.path, commitSha: sha, firstCommitSha: 'PRIVATE-CREDENTIAL' }); + expect(body).toContain(`/blob/${sha}/${verified.path}`); expect(body).not.toContain('PRIVATE-'); expect(body).not.toContain('(history)'); + }); + it.each(['no proof', 'wrong path', 'abbreviated SHA'])('rejects a published projection with %s', scenario => { const proof = scenario === 'no proof' ? undefined : { ...verified, path: scenario === 'wrong path' ? 'specs/foreign.md' : verified.path, currentSha: scenario === 'abbreviated SHA' ? 'abc123' : sha }; const status = projectSddStatus({ state: 'published', path: verified.path, verified: proof }); diff --git a/src/application/policies/publication_message_catalog.ts b/src/application/policies/publication_message_catalog.ts index 5ad2226f7..b18160ed4 100644 --- a/src/application/policies/publication_message_catalog.ts +++ b/src/application/policies/publication_message_catalog.ts @@ -107,10 +107,12 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.status.sddUnverified', 'interaction.status.sddAwaitingAnswers', 'interaction.status.sddPublicationPending', + 'interaction.status.sddRevisionPending', 'interaction.status.sddReady', 'interaction.status.sddOwner', 'interaction.status.sddQuestions', 'interaction.status.sddCommit', + 'interaction.status.sddFirstCommit', 'interaction.status.sddNextInspect', 'interaction.status.sddNextAnswers', 'interaction.status.sddNextPublication', @@ -279,10 +281,12 @@ const ENGLISH_MESSAGES: Readonly> = 'interaction.status.sddUnverified': 'Publication evidence is invalid, absent or stale; readiness is unverified.', 'interaction.status.sddAwaitingAnswers': 'Answers required', 'interaction.status.sddPublicationPending': 'Publication pending', + 'interaction.status.sddRevisionPending': 'Revision pending; current readiness is unverified.', 'interaction.status.sddReady': 'Published and verified', 'interaction.status.sddOwner': 'Specification', 'interaction.status.sddQuestions': 'Pending questions', 'interaction.status.sddCommit': 'Verified publication', + 'interaction.status.sddFirstCommit': 'First verified publication (history)', 'interaction.status.sddNextInspect': 'Inspect the SDD card and retry after correcting its evidence.', 'interaction.status.sddNextAnswers': 'Answer these questions in the issue using their question identifiers.', 'interaction.status.sddNextPublication': 'Resume SDD preparation to publish the approved specification.', @@ -410,10 +414,12 @@ const SPANISH_MESSAGES: Readonly> = 'interaction.status.sddUnverified': 'La publicación es inválida, está ausente o desactualizada; preparación sin verificar.', 'interaction.status.sddAwaitingAnswers': 'Respuestas necesarias', 'interaction.status.sddPublicationPending': 'Publicación pendiente', + 'interaction.status.sddRevisionPending': 'Revisión pendiente; preparación actual sin verificar.', 'interaction.status.sddReady': 'Publicado y verificado', 'interaction.status.sddOwner': 'Especificación', 'interaction.status.sddQuestions': 'Preguntas pendientes', 'interaction.status.sddCommit': 'Publicación verificada', + 'interaction.status.sddFirstCommit': 'Primera publicación verificada (histórica)', 'interaction.status.sddNextInspect': 'Revisa la tarjeta del SDD y reintenta tras corregir su evidencia.', 'interaction.status.sddNextAnswers': 'Responde estas preguntas en la issue usando sus identificadores.', 'interaction.status.sddNextPublication': 'Retoma la preparación del SDD para publicar la especificación aprobada.', diff --git a/src/application/policies/sdd_status_policy.ts b/src/application/policies/sdd_status_policy.ts index 81af265cb..a686725e8 100644 --- a/src/application/policies/sdd_status_policy.ts +++ b/src/application/policies/sdd_status_policy.ts @@ -5,11 +5,12 @@ import type { SddEvidenceSnapshot } from '../ports/sdd_evidence_read_ports'; import type { PublicationMessageCatalog, PublicationMessageId } from './publication_message_catalog'; export interface SddStatusReadiness { - readonly state: 'not-required' | 'unavailable' | 'unverified' | 'awaiting-answers' | 'publication-pending' | 'ready'; + readonly state: 'not-required' | 'unavailable' | 'unverified' | 'awaiting-answers' | 'publication-pending' | 'revision-pending' | 'ready'; readonly required: boolean | null; readonly path?: string; readonly pendingQuestionIds?: readonly string[]; readonly commitSha?: string; + readonly firstCommitSha?: string; readonly cardId?: number; } @@ -25,14 +26,19 @@ export function initialSddStatus(context: Pick /^Q[1-8]$/u.test(id)); - const state = readiness.state === 'ready' && (!path || !commitSha) ? 'unverified' : readiness.state; + const state = (readiness.state === 'ready' && (!path || !commitSha)) + || (readiness.state === 'revision-pending' && (!path || !firstCommitSha)) ? 'unverified' : readiness.state; const message: Record = { 'not-required': 'interaction.status.sddNotRequired', unavailable: 'interaction.status.sddUnavailable', unverified: 'interaction.status.sddUnverified', 'awaiting-answers': 'interaction.status.sddAwaitingAnswers', - 'publication-pending': 'interaction.status.sddPublicationPending', ready: 'interaction.status.sddReady', + 'publication-pending': 'interaction.status.sddPublicationPending', 'revision-pending': 'interaction.status.sddRevisionPending', ready: 'interaction.status.sddReady', }; const next: Record = { 'not-required': undefined, unavailable: 'interaction.status.sddNextInspect', unverified: 'interaction.status.sddNextInspect', - 'awaiting-answers': 'interaction.status.sddNextAnswers', 'publication-pending': 'interaction.status.sddNextPublication', ready: 'interaction.status.sddNextReady', + 'awaiting-answers': 'interaction.status.sddNextAnswers', 'publication-pending': 'interaction.status.sddNextPublication', + 'revision-pending': pending?.length ? 'interaction.status.sddNextAnswers' : 'interaction.status.sddNextPublication', ready: 'interaction.status.sddNextReady', }; const label = (id: PublicationMessageId, value: string) => `- **${catalog.render(id)}:** ${value}`; const lines = ['', `## ${catalog.render('interaction.status.sddHeading')}`, @@ -60,11 +69,15 @@ export function formatSddStatus(readiness: SddStatusReadiness, catalog: Publicat ]; const base = safeIssueUrl(issueUrl, issueNumber, identity); if (path && !['unavailable', 'unverified'].includes(state)) lines.push(label('interaction.status.sddOwner', `\`${path}\``)); - if (state === 'awaiting-answers' && pending?.length) lines.push(label('interaction.status.sddQuestions', [...new Set(pending)].slice(0, 8).join(', '))); + if ((state === 'awaiting-answers' || state === 'revision-pending') && pending?.length) lines.push(label('interaction.status.sddQuestions', [...new Set(pending)].slice(0, 8).join(', '))); if (state === 'ready' && commitSha && path) { const evidence = base ? `[\`${commitSha}\`](${base.repository}/blob/${commitSha}/${path})` : `\`${commitSha}\``; lines.push(label('interaction.status.sddCommit', evidence)); } + if ((state === 'revision-pending' || (state === 'ready' && firstCommitSha !== commitSha)) && firstCommitSha && path) { + const evidence = base ? `[\`${firstCommitSha}\`](${base.repository}/blob/${firstCommitSha}/${path})` : `\`${firstCommitSha}\``; + lines.push(label('interaction.status.sddFirstCommit', evidence)); + } if (next[state]) lines.push('', catalog.render(next[state]!)); if (base && Number.isSafeInteger(readiness.cardId) && readiness.cardId! > 0) lines.push(`[${catalog.render('interaction.status.sddCard')}](${base.issue}#issuecomment-${readiness.cardId})`); return lines; diff --git a/src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts b/src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts index d74363ae4..0e509f71a 100644 --- a/src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts +++ b/src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts @@ -58,6 +58,17 @@ describe('read-only SDD authority and publication query', () => { const f = fixture(); expect(await f.query.read({ ...f.context, issueNumber })).toEqual({ state: 'unavailable' }); expect(f.comments).not.toHaveBeenCalled(); }); + it.each([true, false])('integrates a verified first publication and pending revision with questions=%s', async hasQuestions => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', round: 2, + plan: { ...f.record.plan, questions: hasQuestions ? [{ id: 'Q1', text: 'What should the revision change?', owner: 'issue-author' }] : [] } }); + const statusContext = { sdd: f.context, status: { owner: 'acme', repository: 'demo', event: 'issue_comment', action: 'created', target: 'issue', issueNumber: 42, + issueLabels: [], pullRequestLabels: [], pullRequestDescriptionMode: 'append', sddReadiness: { state: 'unavailable', required: true } } } as unknown as CommentAutomationContext; + const result = await readCopilotStatus(statusContext, 'IssueCommentUseCase', undefined, { read: f.query.read.bind(f.query) }); + expect(result.payload).toMatchObject({ status: { sddReadiness: { state: 'revision-pending', firstCommitSha: first, + pendingQuestionIds: hasQuestions ? ['Q1'] : [], path: 'specs/feature.md' } } }); + expect((result.payload as { status: { sddReadiness: { commitSha?: string } } }).status.sddReadiness.commitSha).toBeUndefined(); + expect(f.ports.workspace.verifyPublication).toHaveBeenCalledTimes(2); + }); it.each(['issueManagedBranches', 'admittedKind'] as const)('does not guess missing configuration %s', async field => { const f = fixture(); expect(await f.query.read({ ...f.context, [field]: undefined })).toEqual({ state: 'unavailable' }); expect(f.comments).not.toHaveBeenCalled(); From 14a539a09b9a8e03ce5f8b08116417db2e95e7b9 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 22:44:48 +0200 Subject: [PATCH 09/12] feature: bound SDD authorization reads and preserve verified publication history --- build/cli/index.js | 37 +++++++++----- build/github_action/index.js | 37 +++++++++----- docs/issues/comment-commands.mdx | 2 +- specs/pre-branch-sdd-status-readiness.md | 23 ++++++--- .../__tests__/sdd_status_policy.test.ts | 17 ++++++- src/application/policies/sdd_status_policy.ts | 9 ++-- .../sdd_evidence_query_use_case.test.ts | 51 +++++++++++++++++++ .../sdd/sdd_evidence_query_use_case.ts | 29 +++++++---- 8 files changed, 156 insertions(+), 49 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 02ba388dc..370498c7e 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -43637,16 +43637,17 @@ function initialSddStatus(context, labels) { } } function projectSddStatus(evidence, initial) { - const firstCommitSha = evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.firstSha ?? '') + const path = evidence.path ?? (evidence.state === 'stale' ? evidence.verified?.path : undefined); + const firstCommitSha = evidence.verified?.path === path && /^[a-f0-9]{40}$/u.test(evidence.verified?.firstSha ?? '') ? evidence.verified.firstSha : undefined; - const revisionPending = (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') && Boolean(firstCommitSha); + const revisionPending = (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending' || evidence.state === 'stale') && Boolean(firstCommitSha); const state = evidence.state === 'not-required' ? 'not-required' : evidence.state === 'absent' || evidence.state === 'unavailable' ? 'unavailable' - : evidence.state === 'stale' || evidence.state === 'invalid' ? 'unverified' + : evidence.state === 'stale' ? (revisionPending ? 'revision-pending' : 'unverified') : evidence.state === 'invalid' ? 'unverified' : evidence.state === 'published' ? (evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.currentSha ?? '') ? 'ready' : 'unverified') : revisionPending ? 'revision-pending' : evidence.state; return Object.freeze({ state, required: state === 'not-required' ? false : state === 'unavailable' ? initial?.required ?? null : true, - ...(evidence.path && !['not-required', 'unavailable', 'unverified'].includes(state) ? { path: evidence.path } : {}), + ...(path && !['not-required', 'unavailable', 'unverified'].includes(state) ? { path } : {}), ...((state === 'awaiting-answers' || state === 'revision-pending') && evidence.pendingQuestionIds ? { pendingQuestionIds: Object.freeze([...evidence.pendingQuestionIds]) } : {}), ...(state === 'ready' && evidence.verified?.currentSha ? { commitSha: evidence.verified.currentSha } : {}), ...((state === 'revision-pending' || state === 'ready') && firstCommitSha ? { firstCommitSha } : {}), @@ -55321,6 +55322,7 @@ const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_evidence_read_ports_1 = __nccwpck_require__(94752); const sdd_issue_identity_policy_1 = __nccwpck_require__(99167); const sdd_query_card_policy_1 = __nccwpck_require__(56285); +const MAX_AUTHORIZATION_LOOKUPS = 16; /** Reads gate authority and immutable publication evidence without any mutation or model capability. */ class SddEvidenceQueryUseCase { constructor(ports) { this.ports = (0, sdd_evidence_read_ports_1.bindSddEvidenceReadPorts)(ports); } @@ -55342,7 +55344,7 @@ class SddEvidenceQueryUseCase { const { record, id: cardId } = selection; const digest = this.digest(context, facts, record.baseSha); const stale = digest !== record.issueDigest; - const result = await this.inspectRecord(context, facts, record, cardId, stale); + const result = await this.inspectRecord(context, facts, record, cardId, stale, new Map()); if (!['invalid', 'unavailable'].includes(result.state) && !await this.fence(context, facts, selection.body, cardId, record, result.verified)) return { state: 'stale' }; @@ -55352,7 +55354,7 @@ class SddEvidenceQueryUseCase { return { state: 'unavailable' }; } } - async inspectRecord(context, facts, record, cardId, stale) { + async inspectRecord(context, facts, record, cardId, stale, authorizations) { const path = record.plan.path; let verified; if (record.commitSha) { @@ -55404,11 +55406,11 @@ class SddEvidenceQueryUseCase { return { state: 'stale', ...(verified ? { verified } : {}) }; if (record.phase === 'published') return { state: 'published', path, cardId, verified }; - const pendingQuestionIds = await this.pendingQuestions(context, facts.comments, record, cardId); + const pendingQuestionIds = await this.pendingQuestions(context, facts.comments, record, cardId, authorizations); return { state: pendingQuestionIds.length ? 'awaiting-answers' : 'publication-pending', path, cardId, pendingQuestionIds: Object.freeze(pendingQuestionIds), ...(verified ? { verified } : {}) }; } - async pendingQuestions(context, comments, record, cardId) { + async pendingQuestions(context, comments, record, cardId, authorizations) { const pending = []; const cutoff = Math.max(cardId, ...(record.answers ?? []).map(answer => answer.commentId)); for (const question of record.plan.questions) { @@ -55419,7 +55421,7 @@ class SddEvidenceQueryUseCase { }); let answered = false; for (const answer of retained) - if (await this.authorized(context, question.owner, answer.author)) { + if (await this.authorized(context, question.owner, answer.author, authorizations)) { answered = true; break; } @@ -55429,7 +55431,7 @@ class SddEvidenceQueryUseCase { || comment.user.login.toLowerCase() === context.trustedBotLogin.toLowerCase() || !comment.body || !(0, pre_branch_sdd_1.parseSddAnswer)(comment.body, question.id)) continue; - if (await this.authorized(context, question.owner, comment.user.login)) { + if (await this.authorized(context, question.owner, comment.user.login, authorizations)) { answered = true; break; } @@ -55440,9 +55442,18 @@ class SddEvidenceQueryUseCase { } return pending; } - async authorized(context, owner, author) { - return owner === 'issue-author' ? Boolean(context.issueAuthor && author.toLowerCase() === context.issueAuthor.toLowerCase()) - : this.ports.actors.isActorAllowedToModifyFiles(author); + async authorized(context, owner, author, authorizations) { + if (owner === 'issue-author') + return Boolean(context.issueAuthor && author.toLowerCase() === context.issueAuthor.toLowerCase()); + const key = author.toLowerCase(); + let authorization = authorizations.get(key); + if (!authorization) { + if (authorizations.size >= MAX_AUTHORIZATION_LOOKUPS) + throw new Error('SDD authorization inventory exceeds its bound.'); + authorization = this.ports.actors.isActorAllowedToModifyFiles(author); + authorizations.set(key, authorization); + } + return authorization; } async fence(context, original, cardBody, cardId, record, verified) { const fresh = await this.readFacts(context.issueNumber); diff --git a/build/github_action/index.js b/build/github_action/index.js index af2db3fc5..40bd2fd99 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -46566,16 +46566,17 @@ function initialSddStatus(context, labels) { } } function projectSddStatus(evidence, initial) { - const firstCommitSha = evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.firstSha ?? '') + const path = evidence.path ?? (evidence.state === 'stale' ? evidence.verified?.path : undefined); + const firstCommitSha = evidence.verified?.path === path && /^[a-f0-9]{40}$/u.test(evidence.verified?.firstSha ?? '') ? evidence.verified.firstSha : undefined; - const revisionPending = (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') && Boolean(firstCommitSha); + const revisionPending = (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending' || evidence.state === 'stale') && Boolean(firstCommitSha); const state = evidence.state === 'not-required' ? 'not-required' : evidence.state === 'absent' || evidence.state === 'unavailable' ? 'unavailable' - : evidence.state === 'stale' || evidence.state === 'invalid' ? 'unverified' + : evidence.state === 'stale' ? (revisionPending ? 'revision-pending' : 'unverified') : evidence.state === 'invalid' ? 'unverified' : evidence.state === 'published' ? (evidence.verified?.path === evidence.path && /^[a-f0-9]{40}$/u.test(evidence.verified?.currentSha ?? '') ? 'ready' : 'unverified') : revisionPending ? 'revision-pending' : evidence.state; return Object.freeze({ state, required: state === 'not-required' ? false : state === 'unavailable' ? initial?.required ?? null : true, - ...(evidence.path && !['not-required', 'unavailable', 'unverified'].includes(state) ? { path: evidence.path } : {}), + ...(path && !['not-required', 'unavailable', 'unverified'].includes(state) ? { path } : {}), ...((state === 'awaiting-answers' || state === 'revision-pending') && evidence.pendingQuestionIds ? { pendingQuestionIds: Object.freeze([...evidence.pendingQuestionIds]) } : {}), ...(state === 'ready' && evidence.verified?.currentSha ? { commitSha: evidence.verified.currentSha } : {}), ...((state === 'revision-pending' || state === 'ready') && firstCommitSha ? { firstCommitSha } : {}), @@ -55339,6 +55340,7 @@ const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_evidence_read_ports_1 = __nccwpck_require__(94752); const sdd_issue_identity_policy_1 = __nccwpck_require__(99167); const sdd_query_card_policy_1 = __nccwpck_require__(56285); +const MAX_AUTHORIZATION_LOOKUPS = 16; /** Reads gate authority and immutable publication evidence without any mutation or model capability. */ class SddEvidenceQueryUseCase { constructor(ports) { this.ports = (0, sdd_evidence_read_ports_1.bindSddEvidenceReadPorts)(ports); } @@ -55360,7 +55362,7 @@ class SddEvidenceQueryUseCase { const { record, id: cardId } = selection; const digest = this.digest(context, facts, record.baseSha); const stale = digest !== record.issueDigest; - const result = await this.inspectRecord(context, facts, record, cardId, stale); + const result = await this.inspectRecord(context, facts, record, cardId, stale, new Map()); if (!['invalid', 'unavailable'].includes(result.state) && !await this.fence(context, facts, selection.body, cardId, record, result.verified)) return { state: 'stale' }; @@ -55370,7 +55372,7 @@ class SddEvidenceQueryUseCase { return { state: 'unavailable' }; } } - async inspectRecord(context, facts, record, cardId, stale) { + async inspectRecord(context, facts, record, cardId, stale, authorizations) { const path = record.plan.path; let verified; if (record.commitSha) { @@ -55422,11 +55424,11 @@ class SddEvidenceQueryUseCase { return { state: 'stale', ...(verified ? { verified } : {}) }; if (record.phase === 'published') return { state: 'published', path, cardId, verified }; - const pendingQuestionIds = await this.pendingQuestions(context, facts.comments, record, cardId); + const pendingQuestionIds = await this.pendingQuestions(context, facts.comments, record, cardId, authorizations); return { state: pendingQuestionIds.length ? 'awaiting-answers' : 'publication-pending', path, cardId, pendingQuestionIds: Object.freeze(pendingQuestionIds), ...(verified ? { verified } : {}) }; } - async pendingQuestions(context, comments, record, cardId) { + async pendingQuestions(context, comments, record, cardId, authorizations) { const pending = []; const cutoff = Math.max(cardId, ...(record.answers ?? []).map(answer => answer.commentId)); for (const question of record.plan.questions) { @@ -55437,7 +55439,7 @@ class SddEvidenceQueryUseCase { }); let answered = false; for (const answer of retained) - if (await this.authorized(context, question.owner, answer.author)) { + if (await this.authorized(context, question.owner, answer.author, authorizations)) { answered = true; break; } @@ -55447,7 +55449,7 @@ class SddEvidenceQueryUseCase { || comment.user.login.toLowerCase() === context.trustedBotLogin.toLowerCase() || !comment.body || !(0, pre_branch_sdd_1.parseSddAnswer)(comment.body, question.id)) continue; - if (await this.authorized(context, question.owner, comment.user.login)) { + if (await this.authorized(context, question.owner, comment.user.login, authorizations)) { answered = true; break; } @@ -55458,9 +55460,18 @@ class SddEvidenceQueryUseCase { } return pending; } - async authorized(context, owner, author) { - return owner === 'issue-author' ? Boolean(context.issueAuthor && author.toLowerCase() === context.issueAuthor.toLowerCase()) - : this.ports.actors.isActorAllowedToModifyFiles(author); + async authorized(context, owner, author, authorizations) { + if (owner === 'issue-author') + return Boolean(context.issueAuthor && author.toLowerCase() === context.issueAuthor.toLowerCase()); + const key = author.toLowerCase(); + let authorization = authorizations.get(key); + if (!authorization) { + if (authorizations.size >= MAX_AUTHORIZATION_LOOKUPS) + throw new Error('SDD authorization inventory exceeds its bound.'); + authorization = this.ports.actors.isActorAllowedToModifyFiles(author); + authorizations.set(key, authorization); + } + return authorization; } async fence(context, original, cardBody, cardId, record, verified) { const fresh = await this.readFacts(context.issueNumber); diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index 62ac504db..33d471963 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -136,7 +136,7 @@ After the first verified publication, a new clarification round reports does not certify the pending revision. The section keeps unanswered question identifiers visible, or asks to resume publication once answers are complete. Once the revision is verified, status shows the current publication and the -immutable first publication separately when their commits differ. +immutable first publication separately when their commits differ. A change to the issue body keeps a separately verified first publication explicitly historical until the revision is published. Failed final identity checks remove that link. Status reads each maintainer's current permission once per request, with at most 16 distinct accounts; failed or exhausted permission reads make SDD evidence unavailable. Later requests recheck permissions. Read-only commands answer the current comment, using the issue or PR as background. Add the file, symbol, failure or requested work after the command to diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index 826b9bc87..d83c604cb 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -184,7 +184,7 @@ Text equivalent: the existing status use case calls typed read and verification - Application contracts: immutable issue identity and existing snapshot input; typed `SddReadiness` result containing requirement, owner path, pending IDs, verified SHA, and evidence state. - Semantic ports: `readLatestValidSddGateEvidence(issueIdentity)` and existing publication proof query, without provider-specific DTOs. - Durable state/schema ownership: the existing gate owns `copilot:sdd-gate:v1`; the status command is a read-only consumer and creates no new durable state. -- Concurrency/idempotency: status is a snapshot read; each request validates current evidence. It neither combines rounds nor writes markers. +- Concurrency/idempotency: status is a snapshot read; each request validates current evidence. It neither combines rounds nor writes markers. Maintainer authorization is memoized by case-insensitive account only within one read, shared between retained and live answers. At most 16 distinct maintainer accounts are checked; an exhausted bound or failed permission read makes evidence unavailable. Issue-author identity checks consume no permission lookup. No grant survives into another request. - Trusted and untrusted inputs: issue text, human comments, arbitrary markers, and provider payloads are untrusted. Only validated Action-owned durable evidence and existing authorization/publication proofs count. - Provider error mapping: retrieval failure yields unavailable/unknown; invalid schema or identity yields invalid/unverified. Errors do not discard the existing status snapshot or expose raw exceptions. @@ -308,19 +308,19 @@ No schema or configuration migration is introduced. Existing gate records remain ## 14. Testing strategy and numeric budget -Minimum: **170 distinct cases** for this extension. Existing gate and PR status cases are retained but not counted again here. +Minimum: **181 distinct cases** for this extension. Existing gate and PR status cases are retained but not counted again here. | Area | Minimum distinct cases | Behaviors/risks covered | |---|---:|---| | Existing basic status presentation | 30 | Original issue readiness, unavailable/not-required and bilingual render regression cases | | Issue identity | 8 | Canonical visible body/title normalization, profile, kind, issue number and original base digest | | Owned card selection | 16 | Exact bot ownership, highest comment identity, corruption without older fallback, duplicate markers, safe IDs and retained answer IDs | -| Authority/application/fences | 53 | Immutable first/revision proof, catalog ownership, current authorized answers, partial inventory, issue/card/label/head races, no write capabilities, status integration and both pending-revision outcomes | -| Closed projection/UX | 56 | Canonical requirement/configuration, eight evidence outcomes, explicitly historical first publication during a pending revision and alongside a different verified revision, malformed proof, safe same-issue links, bounded IDs and both bundled locales | +| Authority/application/fences | 60 | Immutable first/revision proof, catalog ownership, current authorized answers, partial inventory, issue/card/label/head races, no write capabilities, status integration and both pending-revision outcomes | +| Closed projection/UX | 60 | Canonical requirement/configuration, eight evidence outcomes, explicitly historical first publication during a pending revision and alongside a different verified revision, malformed proof, safe same-issue links, bounded IDs and both bundled locales | | Native linked ref adapter | 4 | Stale/deleted refs, foreign/missing repository identity; none may fall back to another PR | | Immutable workspace reads | 2 | Read the original catalog without checkout/current-file mutation; reject non-SHA identifiers | | Read-only composition | 1 | Repository-bound live publication query and current maintainer authorization without mutation capability | -| **Total** | **170** | No case counted twice | +| **Total** | **181** | No case counted twice | Maintain repository-wide thresholds. New pure readiness policy should meet at least 95% branch coverage; changed status modules should meet existing module thresholds, with a target of 90% branches and 95% lines/statements. Use deterministic records, clocks, provider fakes, and publication proofs; no sleeps or live services. Parse catalog structures and gate records rather than grep prose. UI fixtures require semantic assertions beyond snapshots. Manual acceptance confirms the section is readable on mobile and desktop and preserves the existing status ordering. @@ -368,7 +368,7 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex 2. Define typed readiness result and semantic read/verification ports; add pure policy tests for ownership, identity, pending IDs, and state mapping. 3. Integrate read-only orchestration into status while retaining the original snapshot on retrieval errors. 4. Add catalog messages and compact rendering for all states; ensure no timeline-card path is invoked. -5. Add adapter, integration, localization, security, and compatibility tests within the 170-case implementation budget. +5. Add adapter, integration, localization, security, and compatibility tests within the 181-case implementation budget. 6. Update command/operator documentation and contract examples. 7. Run specification, architecture, catalog, and repository validation required by the existing project workflow. @@ -378,7 +378,7 @@ Keep examples synchronized with catalog fixtures. No new guide is mandated if ex - [x] `/copilot status` remains read-only and preserves all existing fields and ordering. - [x] English and Spanish catalogs cover the readiness section. - [x] Typed application ports and Clean Architecture boundaries are enforced by tests/checks. -- [x] All 170 distinct extension cases, full coverage budgets and generated bundles pass before publication. +- [x] All 181 distinct extension cases, full coverage budgets and generated bundles pass before publication. - [x] User documentation explains the evidence limit and fail-closed behavior. - [x] Existing command grammar, lifecycle, labels, finding counts, and gate record ownership remain compatible. - [x] `specs/catalog.json` evidence and generated `specs/CATALOG.md` remain current; `pnpm run validate:specifications` passes. @@ -441,6 +441,13 @@ Action run `38166458576` on `windows-intel-runner-1` completed 570 suites and 7, ### Publication history and pending-revision correction from dogfooding -The original progress comparison exposed the missing revision-pending view. Subsequent real Bugbot findings `4242981181` and `4242981184` identified loss of the first publication alongside a verified revision and an obsolete implementation claim in the owning gate SDD. The projection now retains a independently verified immutable first SHA: pending revisions label it as historical and withhold current readiness; verified revisions show current and first publications separately only when they differ. Current pending question identifiers remain visible. Absent, stale, invalid or denied evidence never promotes historical publication to current readiness. The owning gate SDD and user documentation describe the actual implementation while keeping post-promotion acceptance explicitly open. +The original progress comparison exposed the missing revision-pending view. Subsequent real Bugbot findings `4242981181` and `4242981184` identified loss of the first publication alongside a verified revision and an obsolete implementation claim in the owning gate SDD. The projection now retains an independently verified immutable first SHA: pending revisions label it as historical and withhold current readiness; verified revisions show current and first publications separately only when they differ. Current pending question identifiers remain visible. Absent, stale, invalid or denied evidence never promotes historical publication to current readiness. Independently verified first-publication evidence may remain explicitly historical after an issue-body revision; a failed final identity fence removes that proof entirely. The owning gate SDD and user documentation describe the actual implementation while keeping post-promotion acceptance explicitly open. Twenty-one distinct new contracts cover both pending phases, incomplete publication metadata, unknown requirement, bilingual history, invalid navigation, malformed projected evidence, deduplication of identical publication links and real status integration. The extension budget increases from 149 to 170 without recounting shared gate tests. Full local verification passed 570 suites and 7,583 tests with 35 existing platform skips (7,618 total). The changed projection has 100% lines/statements/branches/functions; all existing budgets, typecheck, lint, specifications/catalog, documentation, graph update and compilation passed. Fresh independent review and live command/replay checks still require the published implementation and promotion. + + +### Bounded permission reads and stale-publication history + +Fresh native review on head `bad9f8a2cbc21deabc505cd2356937530cb9da2c` identified `4243078289` and `4243078290`. The reader now checks each maintainer account once per request, with a 16-account maximum and no cross-request permission cache. It preserves an independently verified first publication as historical revision-pending evidence when the issue body has changed. Failed final fences, missing first-publication proof and mismatched specification paths cannot restore history or certify current readiness. + +Eleven new distinct contracts cover permission reuse across case variants, questions and retained/live answers; fresh reauthorization; the exact account bound and exhaustion; denied-read retry; issue-author identity without permission requests; bilingual stale history; loss of final proof; and foreign specification paths. The extension budget increases from 170 to 181 without recounting shared gate tests. Focused verification passed 117 cases. Full local verification passed 570 suites and 7,594 tests with 35 configured skips (7,629 total). The projection has 100% coverage; the evidence reader has 99.01% lines, 96.96% statements, 97.43% branches and 100% functions, satisfying unchanged coverage budgets. Typecheck and lint passed. Fresh review and live command acceptance remain required after publication and promotion. diff --git a/src/application/policies/__tests__/sdd_status_policy.test.ts b/src/application/policies/__tests__/sdd_status_policy.test.ts index 7b9727437..387d8dbc9 100644 --- a/src/application/policies/__tests__/sdd_status_policy.test.ts +++ b/src/application/policies/__tests__/sdd_status_policy.test.ts @@ -25,7 +25,7 @@ describe('SDD status projection and safe navigation', () => { it.each([ ['not-required', 'not-required'], ['absent', 'unavailable'], ['unavailable', 'unavailable'], - ['invalid', 'unverified'], ['stale', 'unverified'], ['awaiting-answers', 'revision-pending'], + ['invalid', 'unverified'], ['stale', 'revision-pending'], ['awaiting-answers', 'revision-pending'], ['publication-pending', 'revision-pending'], ['published', 'ready'], ] as const)('projects %s with no historical SHA mistaken for readiness', (state, expected) => { const status = projectSddStatus({ state, path: verified.path, verified, pendingQuestionIds: ['Q1'], cardId: 7 }, { state: 'unavailable', required: true }); @@ -106,6 +106,21 @@ describe('SDD status projection and safe navigation', () => { const body = render({ state: 'ready', required: true, path: verified.path, commitSha: sha, firstCommitSha: 'PRIVATE-CREDENTIAL' }); expect(body).toContain(`/blob/${sha}/${verified.path}`); expect(body).not.toContain('PRIVATE-'); expect(body).not.toContain('(history)'); }); + it.each(['en-US', 'es-ES'])('retains only verified publication history after a stable issue edit in %s', locale => { + const status = projectSddStatus({ state: 'stale', verified: { ...verified, currentSha: 'b'.repeat(40) } }); + expect(status).toMatchObject({ state: 'revision-pending', firstCommitSha: sha, path: verified.path }); expect(status.commitSha).toBeUndefined(); + const body = formatSddStatus(status, resolveStaticPublicationCatalog(locale).catalog, issueUrl, 42, identity).join('\n'); + expect(body).toContain(`/blob/${sha}/${verified.path}`); expect(body).not.toContain(`/blob/${'b'.repeat(40)}/`); + expect(body).toContain(locale === 'es-ES' ? 'Revisión pendiente' : 'Revision pending'); + }); + it('does not revive history after a stale result loses publication proof at its final fence', () => { + const status = projectSddStatus({ state: 'stale' }); + expect(status).toEqual({ state: 'unverified', required: true }); expect(render(status)).not.toContain('/blob/'); + }); + it('does not substitute a mismatched path to restore stale publication history', () => { + const status = projectSddStatus({ state: 'stale', path: 'specs/foreign.md', verified }); + expect(status.state).toBe('unverified'); expect(status.firstCommitSha).toBeUndefined(); expect(render(status)).not.toContain('/blob/'); + }); it.each(['no proof', 'wrong path', 'abbreviated SHA'])('rejects a published projection with %s', scenario => { const proof = scenario === 'no proof' ? undefined : { ...verified, path: scenario === 'wrong path' ? 'specs/foreign.md' : verified.path, currentSha: scenario === 'abbreviated SHA' ? 'abc123' : sha }; diff --git a/src/application/policies/sdd_status_policy.ts b/src/application/policies/sdd_status_policy.ts index a686725e8..c9612da24 100644 --- a/src/application/policies/sdd_status_policy.ts +++ b/src/application/policies/sdd_status_policy.ts @@ -26,16 +26,17 @@ export function initialSddStatus(context: Pick { const f = fixture(); f.ports.descriptions.getDescription.mockResolvedValue('Changed requirements'); expect(await f.query.read(f.context)).toMatchObject({ state: 'stale', verified: { firstSha: first } }); }); + it('authorizes a repeated candidate account once across questions and login case variants', async () => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, questions: [ + { id: 'Q1', text: 'First decision?', owner: 'maintainer' }, { id: 'Q2', text: 'Second decision?', owner: 'maintainer' }, + ] } }); + const current = await f.comments(); current.value.push(f.comment('SDD Q1: Proposed choice', 9, 'untrusted'), f.comment('SDD Q2: Proposed choice', 10, 'UNTRUSTED')); + f.comments.mockResolvedValue(current); + expect(await f.query.read(f.context)).toMatchObject({ state: 'awaiting-answers', pendingQuestionIds: ['Q1', 'Q2'] }); + expect(f.ports.actors.isActorAllowedToModifyFiles).toHaveBeenCalledTimes(1); + }); + it('never carries cached authorization into a subsequent status query', async () => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, + questions: [{ id: 'Q1', text: 'Current decision?', owner: 'maintainer' }] } }); + const current = await f.comments(); current.value.push(f.comment('SDD Q1: Current choice', 9, 'candidate')); f.comments.mockResolvedValue(current); + f.ports.actors.isActorAllowedToModifyFiles.mockResolvedValueOnce(false).mockResolvedValueOnce(true); + expect(await f.query.read(f.context)).toMatchObject({ state: 'awaiting-answers' }); + expect(await f.query.read(f.context)).toMatchObject({ state: 'publication-pending', pendingQuestionIds: [] }); + expect(f.ports.actors.isActorAllowedToModifyFiles).toHaveBeenCalledTimes(2); + }); + it.each([16, 17])('bounds distinct maintainer candidates to 16 lookups with %s candidates', async candidates => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, + questions: [{ id: 'Q1', text: 'Current decision?', owner: 'maintainer' }] } }); + const current = await f.comments(); for (let i = 0; i < candidates; i++) current.value.push(f.comment('SDD Q1: Proposed choice', 9 + i, `candidate-${i}`)); + f.comments.mockResolvedValue(current); + expect(await f.query.read(f.context)).toMatchObject({ state: candidates === 16 ? 'awaiting-answers' : 'unavailable' }); + expect(f.ports.actors.isActorAllowedToModifyFiles).toHaveBeenCalledTimes(16); + }); + it('shares one authorization for retained and live answers without changing answer provenance', async () => { + const f = fixture(); const text = 'Current decision?'; Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, + questions: [{ id: 'Q1', text, owner: 'maintainer' }] }, answers: [{ questionId: 'Q1', author: 'untrusted', commentId: 6, + text: 'Retained choice', round: 1, questionText: text }] }); + const current = await f.comments(); current.value.push(f.comment('SDD Q1: Current choice', 9, 'UNTRUSTED')); f.comments.mockResolvedValue(current); + expect(await f.query.read(f.context)).toMatchObject({ state: 'awaiting-answers', pendingQuestionIds: ['Q1'] }); + expect(f.ports.actors.isActorAllowedToModifyFiles).toHaveBeenCalledTimes(1); + }); + it('does not spend the maintainer API budget on issue-author questions', async () => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, + questions: [{ id: 'Q1', text: 'Author decision?', owner: 'issue-author' }] } }); + const current = await f.comments(); for (let i = 0; i < 17; i++) current.value.push(f.comment('SDD Q1: Proposed choice', 9 + i, `candidate-${i}`)); + f.comments.mockResolvedValue(current); + expect(await f.query.read(f.context)).toMatchObject({ state: 'awaiting-answers', pendingQuestionIds: ['Q1'] }); + expect(f.ports.actors.isActorAllowedToModifyFiles).not.toHaveBeenCalled(); + }); + it('contains a denied authorization and retries it freshly on the next query', async () => { + const f = fixture(); Object.assign(f.record, { phase: 'awaiting-answer', plan: { ...f.record.plan, + questions: [{ id: 'Q1', text: 'Current decision?', owner: 'maintainer' }] } }); + const current = await f.comments(); current.value.push(f.comment('SDD Q1: Current choice', 9, 'candidate')); f.comments.mockResolvedValue(current); + f.ports.actors.isActorAllowedToModifyFiles.mockRejectedValueOnce(new Error('PRIVATE-CREDENTIAL')).mockResolvedValueOnce(true); + expect(await f.query.read(f.context)).toEqual({ state: 'unavailable' }); + expect(await f.query.read(f.context)).toMatchObject({ state: 'publication-pending' }); + expect(f.ports.actors.isActorAllowedToModifyFiles).toHaveBeenCalledTimes(2); + }); it('never restores readiness from an older valid card after the latest owned card is corrupted', async () => { const f = fixture(); f.comments.mockResolvedValue({ ...(await f.comments()), value: [f.comment(), f.comment('', 8)] }); expect(await f.query.read(f.context)).toEqual({ state: 'invalid' }); diff --git a/src/application/usecases/sdd/sdd_evidence_query_use_case.ts b/src/application/usecases/sdd/sdd_evidence_query_use_case.ts index 41b02bbd8..609636ecd 100644 --- a/src/application/usecases/sdd/sdd_evidence_query_use_case.ts +++ b/src/application/usecases/sdd/sdd_evidence_query_use_case.ts @@ -13,6 +13,7 @@ interface ReadFacts { readonly labels: readonly string[]; readonly comments: readonly SddQueryComment[]; } +const MAX_AUTHORIZATION_LOOKUPS = 16; /** Reads gate authority and immutable publication evidence without any mutation or model capability. */ export class SddEvidenceQueryUseCase { @@ -33,14 +34,15 @@ export class SddEvidenceQueryUseCase { const { record, id: cardId } = selection; const digest = this.digest(context, facts, record.baseSha); const stale = digest !== record.issueDigest; - const result = await this.inspectRecord(context, facts, record, cardId, stale); + const result = await this.inspectRecord(context, facts, record, cardId, stale, new Map()); if (!['invalid', 'unavailable'].includes(result.state) && !await this.fence(context, facts, selection.body, cardId, record, result.verified)) return { state: 'stale' }; return Object.freeze(result); } catch { return { state: 'unavailable' }; } } - private async inspectRecord(context: SddEvidenceQueryContext, facts: ReadFacts, record: SddGateRecord, cardId: number, stale: boolean): Promise { + private async inspectRecord(context: SddEvidenceQueryContext, facts: ReadFacts, record: SddGateRecord, cardId: number, stale: boolean, + authorizations: Map>): Promise { const path = record.plan.path; let verified: VerifiedSddEvidence | undefined; if (record.commitSha) { @@ -76,12 +78,13 @@ export class SddEvidenceQueryUseCase { } if (stale) return { state: 'stale', ...(verified ? { verified } : {}) }; if (record.phase === 'published') return { state: 'published', path, cardId, verified }; - const pendingQuestionIds = await this.pendingQuestions(context, facts.comments, record, cardId); + const pendingQuestionIds = await this.pendingQuestions(context, facts.comments, record, cardId, authorizations); return { state: pendingQuestionIds.length ? 'awaiting-answers' : 'publication-pending', path, cardId, pendingQuestionIds: Object.freeze(pendingQuestionIds), ...(verified ? { verified } : {}) }; } - private async pendingQuestions(context: SddEvidenceQueryContext, comments: readonly SddQueryComment[], record: SddGateRecord, cardId: number): Promise { + private async pendingQuestions(context: SddEvidenceQueryContext, comments: readonly SddQueryComment[], record: SddGateRecord, cardId: number, + authorizations: Map>): Promise { const pending: string[] = []; const cutoff = Math.max(cardId, ...(record.answers ?? []).map(answer => answer.commentId)); for (const question of record.plan.questions) { @@ -91,13 +94,13 @@ export class SddEvidenceQueryUseCase { && answer.questionId === question.id && provenance.questionText === question.text; }); let answered = false; - for (const answer of retained) if (await this.authorized(context, question.owner, answer.author)) { answered = true; break; } + for (const answer of retained) if (await this.authorized(context, question.owner, answer.author, authorizations)) { answered = true; break; } if (!answered) { for (const comment of [...comments].sort((a, b) => b.id - a.id)) { if (!Number.isSafeInteger(comment.id) || comment.id <= cutoff || !comment.user?.login || comment.user.login.toLowerCase() === context.trustedBotLogin.toLowerCase() || !comment.body || !parseSddAnswer(comment.body, question.id)) continue; - if (await this.authorized(context, question.owner, comment.user.login)) { answered = true; break; } + if (await this.authorized(context, question.owner, comment.user.login, authorizations)) { answered = true; break; } } } if (!answered) pending.push(question.id); @@ -105,9 +108,17 @@ export class SddEvidenceQueryUseCase { return pending; } - private async authorized(context: SddEvidenceQueryContext, owner: 'issue-author' | 'maintainer', author: string): Promise { - return owner === 'issue-author' ? Boolean(context.issueAuthor && author.toLowerCase() === context.issueAuthor.toLowerCase()) - : this.ports.actors.isActorAllowedToModifyFiles(author); + private async authorized(context: SddEvidenceQueryContext, owner: 'issue-author' | 'maintainer', author: string, + authorizations: Map>): Promise { + if (owner === 'issue-author') return Boolean(context.issueAuthor && author.toLowerCase() === context.issueAuthor.toLowerCase()); + const key = author.toLowerCase(); + let authorization = authorizations.get(key); + if (!authorization) { + if (authorizations.size >= MAX_AUTHORIZATION_LOOKUPS) throw new Error('SDD authorization inventory exceeds its bound.'); + authorization = this.ports.actors.isActorAllowedToModifyFiles(author); + authorizations.set(key, authorization); + } + return authorization; } private async fence(context: SddEvidenceQueryContext, original: ReadFacts, cardBody: string, cardId: number, record: SddGateRecord, verified?: VerifiedSddEvidence): Promise { From 36cb934bb05e8de03115b10df4f824e6368fa1fe Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 23:18:44 +0200 Subject: [PATCH 10/12] docs: record complete native SDD re-review with preserved bounds --- specs/pre-branch-sdd-status-readiness.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index d83c604cb..574b0512e 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -451,3 +451,10 @@ Twenty-one distinct new contracts cover both pending phases, incomplete publicat Fresh native review on head `bad9f8a2cbc21deabc505cd2356937530cb9da2c` identified `4243078289` and `4243078290`. The reader now checks each maintainer account once per request, with a 16-account maximum and no cross-request permission cache. It preserves an independently verified first publication as historical revision-pending evidence when the issue body has changed. Failed final fences, missing first-publication proof and mismatched specification paths cannot restore history or certify current readiness. Eleven new distinct contracts cover permission reuse across case variants, questions and retained/live answers; fresh reauthorization; the exact account bound and exhaustion; denied-read retry; issue-author identity without permission requests; bilingual stale history; loss of final proof; and foreign specification paths. The extension budget increases from 170 to 181 without recounting shared gate tests. Focused verification passed 117 cases. Full local verification passed 570 suites and 7,594 tests with 35 configured skips (7,629 total). The projection has 100% coverage; the evidence reader has 99.01% lines, 96.96% statements, 97.43% branches and 100% functions, satisfying unchanged coverage budgets. Typecheck and lint passed. Fresh review and live command acceptance remain required after publication and promotion. + + +### Complete native re-review with unchanged bounds + +Native review `38173590725` attempt 1 returned partial human-conversation coverage because the author's clarification comment `6102272561` was 4,074 characters, just over the existing 4,000-character per-item limit. The neutral Check and aggregate card explicitly withheld a clean-PR claim and retained the two open findings. The author condensed only redundant phrasing in that same clarification to 3,981 normalized characters, preserving every Q1–Q8 requirement, numeric budget and template heading. The issue state/title/labels and the exact feature ref SHA remained unchanged. + +Attempt 2 re-read the same implementation head `14a539a09b9a8e03ce5f8b08116417db2e95e7b9` with complete human coverage. It resolved both findings and published complete approval evidence with zero open, reopened, dismissed, verification-required or unknown findings. All six findings from this feature's review are now resolved by the Action. The prior CI rejection was a final blank line in a shared SDK ZIP fixture from default source `972e11d52dee5d6bb00df4b0b3d4ab262751b101`; default head `3d906a34218ecc7988354bcf3843db1f19a78b23` corrects that whitespace. Fresh CI of this contribution and live status acceptance remain separate gates. No conversation limit or review guard was relaxed. From 8b1a42c0e826c7b270f8e9d0cd8dd9034cef8b0a Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 23:34:09 +0200 Subject: [PATCH 11/12] docs: reconcile SDD status case-count evidence --- specs/pre-branch-sdd-status-readiness.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index 574b0512e..b86beb8df 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -411,7 +411,7 @@ this extension; the gate's existing numeric budget does not count them again. ### Offline implementation validation — 2026-10-11 -The 141 distinct extension cases are implemented. Full validation on the Action-managed head `43d710770972df3443b519ca0bd61c6ad6256c93` plus this contribution passed 567 Jest suites with 7,485 tests passed and 35 configured skips. Repository coverage budgets and the setup acceptance ledger passed; the ledger retains its 42 explicitly open cases. New identity and card policies and the read-only composition have 100% coverage; projection branch coverage is 96% and query branch coverage is 97.36%. Typecheck, lint, documentation, specification validation and generated bundles passed. Live acceptance remains pending promotion and is not inferred from these offline checks. +The initial implementation comprised 141 distinct extension cases. The first review added eight cases (149), the publication-history correction added 21 (170), and the bounded-authorization/stale-history correction added 11 (181). These increments are described below and are not recounted as shared gate tests. The current 181-case implementation was verified by the 570-suite, 7,594-pass run documented under “Bounded permission reads and stale-publication history”. Initial full validation on the Action-managed head `43d710770972df3443b519ca0bd61c6ad6256c93` plus this contribution passed 567 Jest suites with 7,485 tests passed and 35 configured skips. Repository coverage budgets and the setup acceptance ledger passed; the ledger retains its 42 explicitly open cases. New identity and card policies and the read-only composition have 100% coverage; projection branch coverage is 96% and query branch coverage is 97.36%. Typecheck, lint, documentation, specification validation and generated bundles passed. Live acceptance remains pending promotion and is not inferred from these offline checks. ### PR #465 review corrections (2026-10-11) @@ -458,3 +458,7 @@ Eleven new distinct contracts cover permission reuse across case variants, quest Native review `38173590725` attempt 1 returned partial human-conversation coverage because the author's clarification comment `6102272561` was 4,074 characters, just over the existing 4,000-character per-item limit. The neutral Check and aggregate card explicitly withheld a clean-PR claim and retained the two open findings. The author condensed only redundant phrasing in that same clarification to 3,981 normalized characters, preserving every Q1–Q8 requirement, numeric budget and template heading. The issue state/title/labels and the exact feature ref SHA remained unchanged. Attempt 2 re-read the same implementation head `14a539a09b9a8e03ce5f8b08116417db2e95e7b9` with complete human coverage. It resolved both findings and published complete approval evidence with zero open, reopened, dismissed, verification-required or unknown findings. All six findings from this feature's review are now resolved by the Action. The prior CI rejection was a final blank line in a shared SDK ZIP fixture from default source `972e11d52dee5d6bb00df4b0b3d4ab262751b101`; default head `3d906a34218ecc7988354bcf3843db1f19a78b23` corrects that whitespace. Fresh CI of this contribution and live status acceptance remain separate gates. No conversation limit or review guard was relaxed. + +### Reconciled case-count evidence + +Native finding `4243268247` identified that the initial 141-case verification paragraph could be read as the current implemented total. The evidence now labels that historical baseline explicitly and accounts for all later increments: 141 + 8 + 21 + 11 = 181 distinct extension cases. This documentation correction introduces no new test or runtime behavior and preserves the separate pending live-acceptance gate. From f4e41a6540e7908ed4251ae7388f4dcb46e53cce Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 11 Oct 2026 23:51:30 +0200 Subject: [PATCH 12/12] feature: reject ambiguous SDD ownership after complete card-body loss --- build/cli/index.js | 32 +++++++++++++- build/github_action/index.js | 32 +++++++++++++- docs/issues/comment-commands.mdx | 6 +++ specs/pre-branch-sdd-gate.md | 26 ++++++----- specs/pre-branch-sdd-status-readiness.md | 43 ++++++++++++++++--- .../__tests__/sdd_query_card_policy.test.ts | 32 ++++++++++++++ .../policies/sdd_query_card_policy.ts | 31 +++++++++++-- .../sdd_evidence_query_use_case.test.ts | 15 ++++++- 8 files changed, 189 insertions(+), 28 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 370498c7e..60e85a487 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -43584,13 +43584,23 @@ function buildSddIssueDigest(context, baseSha) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.selectOwnedSddCard = selectOwnedSddCard; const pre_branch_sdd_1 = __nccwpck_require__(34730); +const publication_identity_policy_1 = __nccwpck_require__(45403); /** The latest owned card is authoritative even when invalid; an older card cannot restore readiness. */ function selectOwnedSddCard(comments, issueNumber, trustedBotLogin) { const login = trustedBotLogin.trim().toLowerCase(); if (!/^[a-z0-9][a-z0-9-]{0,38}(?:\[bot\])?$/u.test(login)) return { state: 'unavailable' }; - const candidates = comments.filter(comment => comment.user?.login?.toLowerCase() === login && typeof comment.body === 'string' - && (comment.body.includes(pre_branch_sdd_1.SDD_GATE_MARKER) || /^## (?:SDD work status|Estado del SDD)\s*$/mu.test(comment.body))); + const owned = comments.filter(comment => comment.user?.login?.toLowerCase() === login); + const versions = new Map(); + for (const comment of owned) { + const bodies = versions.get(comment.id) ?? new Set(); + bodies.add(comment.body); + versions.set(comment.id, bodies); + } + // Unknown owned comments are potential damaged SDD cards. Unsafe IDs and + // ambiguous duplicate bodies cannot be filtered as another product surface. + const candidates = owned.filter(comment => !Number.isSafeInteger(comment.id) || comment.id < 1 + || versions.get(comment.id).size !== 1 || !isOtherOwnedPublication(comment.body, issueNumber)); if (!candidates.length) return { state: 'absent' }; if (candidates.some(comment => !Number.isSafeInteger(comment.id) || comment.id < 1)) @@ -43601,6 +43611,8 @@ function selectOwnedSddCard(comments, issueNumber, trustedBotLogin) { if (bodies.size !== 1) return { state: 'invalid' }; const body = latest[0].body; + if (typeof body !== 'string') + return { state: 'invalid' }; if ([...body.matchAll(/`)])).toEqual({ state: 'invalid' }); + }); + it('allows a newer identified SDD card to supersede an older unknown owned comment', () => { + expect(select([card(6, 'Older damaged card'), card(7)])).toMatchObject({ state: 'valid', id: 7 }); + }); +}); diff --git a/src/application/policies/sdd_query_card_policy.ts b/src/application/policies/sdd_query_card_policy.ts index a7bfdce53..f36d577b8 100644 --- a/src/application/policies/sdd_query_card_policy.ts +++ b/src/application/policies/sdd_query_card_policy.ts @@ -1,4 +1,5 @@ import { readSddGateRecord, SDD_GATE_MARKER, type SddGateRecord } from '../../domain/pre_branch_sdd'; +import { parsePublicationMarker, parsePublicationReplyMarker, parsePublicationTransitionMarker } from './publication_identity_policy'; export interface SddQueryComment { readonly id: number; @@ -14,17 +15,41 @@ export type SddCardSelection = export function selectOwnedSddCard(comments: readonly SddQueryComment[], issueNumber: number, trustedBotLogin: string): SddCardSelection { const login = trustedBotLogin.trim().toLowerCase(); if (!/^[a-z0-9][a-z0-9-]{0,38}(?:\[bot\])?$/u.test(login)) return { state: 'unavailable' }; - const candidates = comments.filter(comment => comment.user?.login?.toLowerCase() === login && typeof comment.body === 'string' - && (comment.body.includes(SDD_GATE_MARKER) || /^## (?:SDD work status|Estado del SDD)\s*$/mu.test(comment.body))); + const owned = comments.filter(comment => comment.user?.login?.toLowerCase() === login); + const versions = new Map>(); + for (const comment of owned) { + const bodies = versions.get(comment.id) ?? new Set(); + bodies.add(comment.body); + versions.set(comment.id, bodies); + } + // Unknown owned comments are potential damaged SDD cards. Unsafe IDs and + // ambiguous duplicate bodies cannot be filtered as another product surface. + const candidates = owned.filter(comment => !Number.isSafeInteger(comment.id) || comment.id < 1 + || versions.get(comment.id)!.size !== 1 || !isOtherOwnedPublication(comment.body, issueNumber)); if (!candidates.length) return { state: 'absent' }; if (candidates.some(comment => !Number.isSafeInteger(comment.id) || comment.id < 1)) return { state: 'invalid' }; const latestId = Math.max(...candidates.map(comment => comment.id)); const latest = candidates.filter(comment => comment.id === latestId); const bodies = new Set(latest.map(comment => comment.body)); if (bodies.size !== 1) return { state: 'invalid' }; - const body = latest[0].body!; + const body = latest[0].body; + if (typeof body !== 'string') return { state: 'invalid' }; if ([...body.matchAll(/