From 82bc9be5eae5170a669313a467cb71919eaff5a1 Mon Sep 17 00:00:00 2001 From: vypbot Date: Sun, 11 Oct 2026 12:45:01 +0200 Subject: [PATCH 1/9] docs(sdd): specify issue contract in specs/pre-branch-sdd-diff.md --- specs/CATALOG.md | 4 +- specs/catalog.json | 3 +- specs/pre-branch-sdd-diff.md | 369 +++++++++++++++++++++++++++++++++++ 3 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 specs/pre-branch-sdd-diff.md diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 9abaf78f4..3e2b36880 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-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 | 51 paths · 2026-10-10 | | `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 | @@ -148,7 +148,7 @@ debt or convert unknown historic intent into a design decision. - Owner: Copilot maintainers - Last verified: 2026-10-10 -- 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-diff.md`](./pre-branch-sdd-diff.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 ff7e31a14..09df74bd3 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1177,7 +1177,8 @@ "lastVerified": "2026-10-10", "specs": [ "specs/issue-start-and-branch-readiness.md", - "specs/pre-branch-sdd-gate.md" + "specs/pre-branch-sdd-gate.md", + "specs/pre-branch-sdd-diff.md" ], "workflows": [ ".github/workflows/copilot_issue.yml", diff --git a/specs/pre-branch-sdd-diff.md b/specs/pre-branch-sdd-diff.md new file mode 100644 index 000000000..fd18f6389 --- /dev/null +++ b/specs/pre-branch-sdd-diff.md @@ -0,0 +1,369 @@ +# Proposed change appendix: `/copilot sdd-diff` + +- Status: Draft +- Date: 2026-10-11 +- Catalog capability ID: `issue-start-and-sdd-readiness` +- Last verified: not applicable (prospective companion) +- Owners: Existing `issue-start-and-sdd-readiness` capability owners; individual names are not established by the issue. +- Scope: Add a deterministic, read-only command that compares the immutable first publication and current verified publication of the owning SDD for an eligible issue. +- Related issues/PRs: [Issue #464](https://github.com/OWNER/REPO/issues/464); live issue #454 is identified as human UX evidence, but its repository URL is not established here. +- Required review gates: product UX, architecture, testing, documentation, security/operations +- Open decisions blocking readiness: None from the supplied answers. Repository-specific command syntax, existing reply marker, current limits for SDD files, exact catalog evidence format, and coverage thresholds must be confirmed against the owning implementation before readiness; do not invent or weaken them. + +> This is a prospective companion appendix. Preserve the owning SDD and its existing contract verbatim; append this proposed change. Repository-specific paths and contracts cited below must be reconciled with the existing capability before implementation. + +## 1. Executive summary + +Add `/copilot sdd-diff` with no arguments. Any GitHub participant permitted to comment may invoke it as a deterministic query, subject to the existing issue-start admission/profile policy. For an eligible issue, the command verifies the trusted gate record, exact admitted issue, owning catalogued SDD path, exact linked branch, immutable publications and current issue digest. It then compares the first and current published documents and presents bounded structural evidence with safe immutable links. It does not infer semantic requirement changes. + +A ready comparison is permitted only when the digest matches, the current publication is verified and the final freshness fence succeeds. Stale, clarification-pending, initial-only, invalid, absent and unavailable evidence remain distinct. The command has no model or write capability. + +```text +comment -> parse exact no-argument command -> resolve eligible target and trusted evidence + -> verify digest, branch, publications and ancestry -> bounded immutable diff + -> freshness fence -> update one correlated localized reply +``` + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +Developers can inspect an SDD gate's first publication and later revisions, but cannot ask the conversation for a bounded structural comparison. Showing an unverified or pending revision as current requirements could mislead branch work. + +### 2.2 Current behavior + +1. The existing gate records first publication and subsequent revision information; the issue states that first-SDD commit preservation already exists. +2. No current `/copilot sdd-diff` behavior is asserted by the issue. +3. The proposed command uses existing admission, gate record, catalog ownership, linked-branch readiness and publication verification contracts; it introduces no new record store. + +### 2.3 Evidence + +- Code/workflow/documentation references: supplied answers identify `readSddGateRecord` schema v1, `PreBranchSddWorkspacePort.readSdd`, `verifyPublication`, `isAncestor`, `BoundLinkedBranchReadinessPort.getLinkedBranch`, command parsing/help and comment orchestration. Exact source paths are unprovided and remain to be confirmed. +- Incidents or concrete examples: issue #464 reports that developers cannot inspect bounded changes; issue #454 is named as a real first/revised SDD human UX fixture. +- External primary sources: not applicable; the contract is repository-owned. +- Unknowns: current implementation names, catalog evidence schema, exact existing SDD file limits, command catalogue syntax and repository coverage thresholds. + +### 2.4 Retrospective classification (as-built baselines only) + +Not applicable: this is a prospective specification, not a reconstruction of implemented behavior. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| Issue participant | Inspect published SDD structure | Issue comment `/copilot sdd-diff` | One correlated issue reply and immutable links | +| PR participant | Query only an explicitly linked eligible issue | PR comment | One correlated PR reply, or not-applicable/ambiguous/unavailable result | +| Maintainer | Preserve trust and operability | Catalog, docs, checks | Capability evidence and validation output | + +**First publication** is the immutable `commitSha`/`baseSha` pair. **Current publication** is `revisionSha`/`revisionBaseSha` when present. **Structural diff** means added/removed lines and changed section titles; it makes no claim about semantic requirements. **Stale** means the current normalized issue digest differs from the trusted record. **Pending-revision** means the digest matches an awaiting-answer record while the verified first publication remains available; the requested revision is not yet published. + +## 4. Goals + +### 4.1 Goals + +1. Provide a deterministic, read-only comparison for eligible targets. +2. Identify exact immutable first/current SHAs and verified owning SDD path. +3. Keep state distinctions honest and output bounded under hostile or excessive input. +4. Reuse existing gate ownership and correlated-comment behavior without changing the gate contract. + +### 4.2 Non-goals + +1. Semantic analysis, model calls, requirement inference, or content excerpts. +2. Editing source or SDDs, creating branches, changing approvals, labels, checks, or lifecycle state. +3. Arbitrary paths/refs, branch scans, PR-number equivalence, or user-provided URLs. + +### 4.3 Fixed product/safety invariants + +1. No current-comparison claim or counts without exact identity, valid trusted record, matching digest, verified publications/ancestry, exact linked branch and successful final freshness fence. +2. No writer capability; no model; no source content in replies. +3. Files and computation are bounded: per document at most 70,000 UTF-16 characters, 280,000 UTF-8 bytes and 2,000 lines; at most 2,000,000 line-comparison steps. Reply at most 6,000 characters. +4. At most eight changed section titles, each at most 120 characters, with omitted-title count disclosed. These fixed limits cannot be configured. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | User/operator effect | +|---|---|---|---| +| Issue query | No diff command asserted | Parse exact no-argument command | Predictable entry | +| Target proof | Existing gate policy | Exact issue admission; PR native closing-issue relationship only | No guessed target | +| Evidence | Gate retains publication records | Verify record, digest, linked branch, immutable publication and ancestry | Trustworthy state | +| Comparison | No conversation comparison asserted | Bounded line diff and section titles | Structural changes visible | +| Reply | Existing orchestration | Localized, replay-safe single correlated reply | No duplicate noise | + +```mermaid +flowchart LR + A[Comment] --> B[Resolve exact eligible issue] + B --> C[Verify record, digest, branch, publications] + C --> D[Bounded structural diff] + D --> E[Freshness fence] + E --> F[One localized correlated reply] +``` + +Text equivalent: comment -> exact target and evidence verification -> bounded comparison -> freshness check -> correlated reply. + +## 6. Functional behavior and state model + +### 6.1 Happy path + +1. Parse `/copilot sdd-diff` with no arguments using the existing deterministic parser. Invalid arguments follow existing help behavior and do not invoke a model. +2. On an issue, apply existing admission/profile policy: feature issues and admitted branch-bearing contract-change issues for which the SDD gate applies are eligible. Unsupported, exempt and help targets return not-applicable without writes other than the correlated query reply. +3. On a PR, query the exact event PR's native GitHub `closingIssuesReferences` relationship through a narrow read-only projection. Validate same-repository canonical PR identity and pagination completeness. Verify each candidate issue's admission/profile before considering it. Zero eligible linked issues is not-applicable; multiple eligible links or disagreeing authoritative identity is ambiguous/unavailable. Incomplete/unreadable authority is unavailable. Never infer from equal numbers, body text, branch scans, or a newest candidate. +4. Read the latest configured-bot-owned valid schema-v1 gate record for the exact issue. Confirm current catalog ownership and its allowlisted SDD path. Preserve `commitSha`/`baseSha` as first publication; use `revisionSha`/`revisionBaseSha` as current publication when present. Do not weaken schema or create another store. +5. Verify exact branch linkage and live branch name/head using existing readiness proof. Verify publication and ancestry at immutable SHAs. A missing, deleted, mismatched or unverifiable branch/publication cannot support a current comparison. +6. Compute the same digest as the gate from current normalized title, visible human body, admitted kind, profile and original recorded base. Digest mismatch takes precedence and reports stale. A digest-matching awaiting-answer record with verified first publication reports pending-revision. Neither permits current counts. A current comparison requires matching digest and verified current publication. +7. Read only the catalogued SDD at the two verified immutable SHAs. Apply all fixed input and work limits. If exceeded, report bounded/unavailable with safe verified links and a specific limit explanation; never truncate and claim exact counts. +8. Recheck issue title/body, record, branch identity and relevant publication facts before replying. Any race invalidates current claims. +9. Use effective issue locale, or PR locale on the PR surface. Render complete English/Spanish catalogs; unsupported locale or invalid catalog uses atomic English fallback. Update/reuse the existing exact repository/target/source-comment correlated reply. Replays update one reply; a new source comment gets its own. + +### 6.2 Alternative paths + +- Eligible issue before first publication: `no-published-SDD`. +- Valid initial publication without revision: `initial-only`; no two-revision comparison. +- Pending clarification with matching digest and verified first publication: `pending-revision`; no current counts. +- Digest mismatch: `stale`, with SDD question-card link; not pending-revision. +- Forged, foreign, malformed or unknown-version record: invalid/unavailable, never a comparison. +- Missing branch/publication and provider read errors: distinct missing/unavailable states where evidence permits; do not fabricate facts. +- PR with no eligible native closing issue: not-applicable; ambiguity and authority failure are explicit. +- Changed documents that exceed limits: bounded/unavailable; safe verified revision links may be shown. + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +|---|---|---|---|---| +| Not-applicable | Target unsupported/exempt or no eligible PR link | This command does not apply | None | User selects applicable issue | +| No-published-SDD | Eligible issue has no first publication | No SDD has been published | Compared after gate publication and retry | Issue owner | +| Initial-only | First publication verified, no revision | Only first version exists | Compared after verified revision | Issue owner | +| Current comparison | Digest, branch, publications and fence all verify | Structural comparison is current | Re-run on new source comment | Gate/application | +| Pending-revision | Matching digest; trusted record awaits answers; first publication verified | Revision is being clarified; no current diff | Compared after publication; stale after issue edit | Issue owner | +| Stale | Current digest differs | Issue changed since gate record | Re-run after gate refresh | Issue owner | +| Invalid/unavailable | Trust, linkage or provider evidence fails | Current comparison cannot be verified | Retry when evidence available | Maintainer/operator | +| Bounded | Document or work limit exceeded | Exact comparison unavailable within safety bound | None until input changes | Issue/spec owner | + +Duplicate requests follow correlated replay rules. Out-of-order events must pass the final fence or lose current status. Cancellation/provider failure creates no lifecycle mutation; partial provider success is reported only as retained verified facts. + +## 7. User-facing configuration + +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +|---|---|---|---|---| +| Command | Exact comment text | `/copilot sdd-diff` | No arguments | Per source comment; no durable setting | +| Diff limits | Fixed policy constants | Limits in §4.3 | No user overrides | Repository contract | +| Locale | Existing effective locale | Issue locale; PR locale on PR | English, Spanish; atomic English fallback | Existing locale policy | + +Validation rejects arguments and invalid command forms through existing parser/help. No arbitrary path, ref, URL, limit, or credential is accepted. Existing repository admission/profile precedence applies; the command does not snapshot a new configuration. Limits, read-only behavior, exact identity, and verification requirements are intentionally not configurable. A recommended invocation is `/copilot sdd-diff`; meaningful alternative is the same command on a PR whose native relationship identifies exactly one eligible issue. + +## 8. Clean Architecture + +### 8.1 Responsibilities and dependency direction + +| Layer/boundary | Owns | Must not own/import | +|---|---|---| +| Domain/pure policy | Eligibility result, state precedence, digest decision, line diff bounds, title sanitization | GitHub DTOs, SDKs, I/O | +| Application | Read-only comparison use case, ordered verification and freshness fence | Concrete provider SDK/process concerns or writer ports | +| Adapters/data | Map semantic reads and provider failures; exact PR closing-issue projection | Product state policy | +| Infrastructure/composition | Wire narrowed read ports and existing comment reconciler | Business decisions or extra credentials | +| Entrypoints | Adapt issue/PR event and comment to command input | Duplicate orchestration or inferred identity | +| Presentation | Localized bounded view models and Markdown renderer | Publication mutation or state transitions | + +```mermaid +flowchart LR + E[Comment entrypoint] --> A[Read-only application use case] + A --> D[Pure trust, state and diff policies] + A --> P[Semantic read ports] + I[GitHub and workspace adapters] --> P + A --> V[Localized presentation] + V --> R[Existing correlated reply reconciler] +``` + +### 8.2 Contracts, state, and trust boundaries + +- Pure decisions: target applicability, digest precedence, state mapping, bounded diff/counts, changed-heading extraction, output escaping. +- Application contracts: immutable event identity and operation result; no provider DTOs. Separate comparison reads from reply publication orchestration; the use case receives no write-capable workspace/comment port. +- Semantic ports: reuse/narrow `readSdd`, `verifyPublication`, `isAncestor`, exact `getLinkedBranch`, bound issue comment/title/description/catalog readers. Add a typed read-only PR closing-issue projection through existing GraphQL transport if no exact semantic port exists. Include repository identity and pagination completeness; stop after finding a second eligible issue. +- Durable state/schema ownership: existing bot-owned `readSddGateRecord` schema v1 only. No schema/store changes. +- Concurrency/idempotency: repository, exact target and source-comment correlation through existing reconciler; final issue/record/branch fence immediately before rendering/publishing current claims. +- Trusted inputs: authenticated event identity, canonical repository identity, configured bot ownership, catalog ownership, validated SHA/path and provider-verified linkage/publication. Untrusted: issue/PR/comment text, record content until validated, headings, provider errors and URLs in bodies. +- Provider errors map to bounded unavailable states; raw errors are never echoed. + +### 8.3 Executable architecture constraints + +Architecture tests MUST prove the use case cannot depend on writer ports, model clients, source-edit/branch/approval/lifecycle services, or provider DTOs. Contract tests MUST assert schema-v1 compatibility and exact PR identity/pagination behavior. Dependency/cycle checks and adapter contract fixtures MUST be added to existing repository checks; exact command names are an implementation discovery item. + +## 9. UI/UX and content contract + +### 9.1 Information hierarchy + +Show current status first, verified facts next, then consequence/action, impact and safe evidence links; technical IDs are secondary. Counts and changed titles appear only for a verified current comparison. Historical links in stale/pending states must be explicitly labelled historical. + +### 9.2 Representative primary view + +```markdown + + +# 🔎 SDD structural comparison + +> **Current status:** Compared verified published revisions +> +> **Action required:** No action required + +## Changes + +- First publication: `FIRST_SHA` +- Current publication: `CURRENT_SHA` +- Added lines: 12 · Removed lines: 4 +- Changed sections: “Scope”, “Failure and recovery” + +This is a structural diff. It does not infer semantic requirement changes. + +## Links + +[First SDD](SAFE_IMMUTABLE_DOCUMENT_URL) · [Current SDD](SAFE_IMMUTABLE_DOCUMENT_URL) · [Compare commits](SAFE_COMPARE_URL) +``` + +Concrete variants: + +- **Pending/no action:** “No SDD revision has been published yet” (`no-published-SDD`) or “Only the first SDD publication is available” (`initial-only`); no current comparison claim. +- **Action required:** stale: “The issue description changed after this SDD record. Refresh the SDD gate before comparing.” Link to verified question card; historical publication links only if all required publication/path/ancestry checks pass. +- **Blocked/failed:** “A current comparison is unavailable because the linked branch could not be verified. Retry after the issue’s linked branch is available.” Retain only verified facts; no counts. +- **Partially complete:** “The published documents were verified, but the comparison exceeded the fixed work limit. Exact counts are unavailable.” Provide only safe verified links. +- **Completed unchanged:** “The verified documents have no structural line changes.” Show both SHAs and safe links, zero counts, and the structural-only caveat. + +### 9.3 Issue, PR, and comment behavior + +- Deterministic title: localized equivalent of “SDD structural comparison”; purpose is query result, not a gate/status card. +- Merge/close/retry causes no lifecycle effect. Retrying the same source comment updates its correlated reply; a new source comment creates its own reply. +- One reply per source comment; no extra status cards, notifications, labels or checks. Preserve waiting labels. +- Links are built from policy-owned canonical repository identity plus validated SHA/path. No body-derived URL. Link document, commit and compare evidence descriptively. + +### 9.4 Accessibility, localization, and responsive behavior + +English and Spanish catalogs are complete and selected by existing effective-locale rules; unsupported or invalid catalogs fall back atomically to English. Use textual status, logical headings, short lists and descriptive links; tables are not required in the reply. Must remain readable on narrow screens and light/dark GitHub themes. Escape and bound titles to 120 characters; neutralize mentions, Markdown, control characters and HTML/command markers. Do not echo document contents, raw provider errors or untrusted URLs. Mermaid has the textual flow equivalent in §5. + +## 10. Failure, recovery, and cleanup + +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +|---|---|---|---|---|---| +| No publication | No comparison yet | Exact eligible target | On a new command only | Publish SDD through existing gate | None | +| Initial only | No revision pair | Verified first SHA if safe | No | Publish revision | None | +| Stale digest | Current counts withheld | Digest mismatch; safe historical links only after verification | No | Refresh gate/question card | Preserve labels | +| Pending answers | Requested revision is not current | Verified first publication | No | Answer existing clarification | Preserve labels | +| Record/owner/path invalid | Trust cannot be established | Sanitized state only | Bounded provider retry only | Maintainer review | No record repair by command | +| Branch/publication missing | Current comparison withheld | Independently verified facts | Bounded read retry per existing policy | Restore/fix linkage or retry later | None | +| Provider unavailable/incomplete PR pagination | Target/evidence unavailable | Safe event identity only | Existing bounded retry policy | Retry later | None | +| Input/work bound exceeded | Exact diff unavailable | Verified SHAs and safe links | No | Reduce/change SDD within existing constraints | None | +| Final fence changes | Race invalidates current claim | No current counts | Re-run on new event | Retry command | No stale reply claim | + +Errors use `impact -> cause -> action -> retained state`; a failed read must not imply publication or lifecycle failure. + +## 11. Security, permissions, and privacy + +1. Reuse existing least-privilege read permissions and comment-reply authority; comparison workflow itself has no writer port. Confirm exact workflow permission declarations during implementation. +2. Treat comments, issue/PR content, records and headings as untrusted. Bind repository and target to authenticated event; enforce exact admission, bot ownership, catalog path, branch and publication proofs. +3. Read only allowlisted spec content at immutable verified SHAs. Never accept arbitrary refs/paths/URLs or expand local setup credentials. Do not log or reply with file contents, tokens, PII beyond existing issue identity, or raw provider errors. +4. Resist replay/forgery through validated record ownership, exact source-comment correlation, bounded processing, output escaping, and final freshness fencing. + +## 12. Observability and operational UX + +- User-facing states are the state table in §6.3; distinguish not-applicable, stale, pending, invalid, missing and unavailable. +- Job Summary is not needed for a comment query; do not add one absent an existing operational convention. +- Logs may record correlation ID, state category, validated SHAs and bound outcome, but not document content or raw errors. Metrics should use bounded state labels; exact existing telemetry is unknown. +- Provider unavailability is not a failed SDD publication. Respect existing rate-limit/retry policy; no unbounded retries. +- Noise budget is one correlated reply per source comment, no notifications/status cards. + +## 13. Compatibility, migration, rollout, and rollback + +- Existing gate records and in-flight operations remain unchanged. Schema v1 is read as-is; malformed/foreign/unknown version fails closed. +- No config or record migration, deprecation, or feature flag is proposed. Existing command help/parser and orchestration are extended compatibly. +- Rollout adds command/docs/catalog evidence after acceptance checks. Rollback removes command dispatch while retaining existing gate records and behavior; no irreversible SDD or lifecycle effects are introduced. + +## 14. Testing strategy and numeric budget + +Minimum: 30 distinct risk-derived cases, meeting the supplied minimum of 24. Each case counts once; parameter rows count only when behavior is distinct. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Domain/configuration/pure planning | 6 | eligibility, digest precedence, state mapping, bounds, diff counts, unsafe titles | +| State/application/idempotency/races | 6 | no publication, initial-only, pending, stale, fence races, replay correlation | +| Adapters/provider contracts | 5 | schema-v1 ownership, exact branch, immutable publication/ancestry, PR pagination/link identity, provider errors | +| Workflows/setup/schema | 3 | read-only permissions, no writer/model wiring, existing record compatibility | +| UI/UX/localization/sanitization | 6 | English/Spanish, atomic fallback, links, escaping, omission disclosure, reply budget | +| Integration/security/migration | 4 | no-write end-to-end, forged/foreign record, excessive input/work, compatibility with gate | +| **Total** | **30** | No double counting | + +Keep all existing SDD gate coverage and budgets unchanged; shared helper changes require documented justification and regression coverage. Preserve repository-wide thresholds and meet or exceed them; exact thresholds are repository-specific and must be copied from current policy before Ready. New pure policies target at least 90% branch coverage. Changed modules must meet repository thresholds with no unexplained coverage decrease. Use deterministic clocks, IDs and fakes; no real waits/live services. Add table-driven state/bound tests, replay/race tests, adapter contracts and structural workflow permission checks. Golden replies require semantic assertions for states, links, counts, localization and forbidden content. Human UX evidence must review #454 first/revised SDD output and narrow-screen readability; do not rely on snapshots alone. + +Required distinct risk coverage includes eligibility/identity (4), record/ownership/publication trust (5), digest/revision races (4), diff/count/bounded work (5), and localization/sanitization/replay/no-write (6), totaling at least 24 cases within the 30-case distribution. + +## 15. Documentation and discoverability + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| User | `docs/issues/comment-commands.mdx` | Exact command, eligible targets, examples, states and links | Command catalog and link checks | +| Issue owner | `docs/issues/pre-branch-sdds.mdx` | First/current revision distinction, stale/pending recovery, structural-only caveat | Examples checked against fixtures | +| Operator | Same SDD guide | Invalid/unavailable evidence, retry and permission boundaries | State decision table review | +| Contributor | Capability SDD and catalog evidence | Ports, trust boundaries, 30-case budget, workflow checks | `pnpm run validate:specifications`; catalog generation/check as repository requires | + +Docs must explain PR exact-link rules, limits, no-write behavior and that line/title differences do not establish semantic requirement changes. Update the capability catalogue evidence and owning SDD or this approved bounded companion before branch creation. Keep documentation examples synchronized with fixtures. + +## 16. Acceptance + +1. Given an admitted eligible issue with a valid schema-v1 record, matching digest, exact linked branch, verified first/current publications and successful final fence, when a participant comments `/copilot sdd-diff`, then one localized reply shows both SHAs, safe immutable links, exact added/removed line counts and at most eight changed titles, and states that the result is structural only. +2. Given an eligible issue with no publication, when queried, then it reports `no-published-SDD` without calling it invalid or ready. +3. Given a verified first publication and no revision, when queried, then it reports `initial-only` and does not invent a comparison. +4. Given a matching digest and awaiting-answer record with verified first publication, when queried, then it reports pending-revision with no current counts. +5. Given any current digest mismatch, including an awaiting-answer record, when queried, then stale takes precedence over pending; no current counts appear and the question-card link is provided. +6. Given a foreign, forged, malformed or unknown-version record, when queried, then no current comparison is claimed and no untrusted values are rendered as authority. +7. Given wrong owner/path, absent publication, deleted or mismatched branch, failed ancestry/publication proof, when queried, then the result is invalid, missing or unavailable as supported by verified facts, never current-ready. +8. Given a PR with exact native same-repository closing-issue authority to zero eligible issues, when queried, then it reports not-applicable; with two eligible issues or disagreeing identity it reports ambiguous/unavailable. +9. Given incomplete PR pagination or unreadable authority, when queried, then it reports unavailable and does not fall back to body numbers or equal PR/issue numbers. +10. Given a document over any character, byte or line limit, or computation over 2,000,000 steps, when compared, then it reports bounded/unavailable without truncation or guessed counts. +11. Given more than eight changed section titles or unsafe headings, when rendered, then only up to eight escaped, bounded titles appear and the omitted count is explicit; mentions/control/Markdown injection is inert. +12. Given a document with no line changes, when queried, then it reports zero structural changes with verified links. +13. Given replay of the same source comment, when processed again, then the existing correlated reply is updated; a distinct source comment receives its own reply. +14. Given issue, record or branch mutation between collection and publication, when the final fence runs, then current counts and ready claims are withheld. +15. Given provider errors, when rendered, then raw error text and document contents are absent and impact/cause/action/retained-state order is used. +16. Given unsupported locale or invalid locale catalog, when rendered, then a complete English response is used atomically; supported issue/PR locales use English/Spanish catalogs respectively. +17. Given any invocation, when inspecting effects, then no model, source edit, branch, approval, label, lifecycle mutation, extra status card or notification occurred. +18. Given the implementation, when architecture/workflow checks run, then application dependencies contain no writer/model capability and exact PR identity and read-only permissions are structurally verified. +19. Given docs and catalog updates, when repository specification validation runs, then links/evidence are current and examples agree with executable fixtures. +20. Given issue #454's real first and revised SDD, when reviewers inspect the generated UX fixture, then the structural-only distinction and immutable navigation are understandable without inferring semantic requirements. + +## 17. Requirements traceability + +| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | +|---|---|---|---| +| Exact target and admission | Entry adapter, admission policy, PR-link port | Identity/admission and pagination contracts | Command guide | +| Trusted record and current readiness | Gate reader, digest/publication/branch policies | Forgery, owner, ancestry, race cases | SDD guide | +| Bounded structural result | Pure diff policy, read-only use case | Character/byte/line/step and count tests | Both user pages | +| Safe correlated reply | Renderer/reconciler | Locale, sanitization, replay tests | Command guide | +| Read-only architecture | Application ports/composition | Dependency and workflow permission checks | Contributor SDD | + +## 18. Implementation sequence + +1. Confirm existing parser, gate record schema, catalog ownership/evidence and repository coverage/workflow contracts; add exact PR closing-issue semantic projection if absent. +2. Define immutable read-only contracts and enforce absence of writer/model dependencies. +3. Implement pure eligibility, state precedence, bounded diff and sanitization policies with tests. +4. Implement ordered application reads, exact linkage/publication checks and final freshness fence. +5. Add adapter mappings and structural PR identity/pagination tests. +6. Add localized states and correlated reply integration with golden and semantic fixtures. +7. Update both issue docs, capability catalogue evidence and this companion/owning SDD before branch creation. +8. Complete 30-case minimum, all existing gate budgets, #454 UX review, architecture/workflow checks and specification validation. + +## 19. Definition of Done + +- [ ] Every normative requirement maps to acceptance and traceability. +- [ ] Existing SDD gate contract, record schema and budgets remain unchanged. +- [ ] Exact issue/PR identity, owner, path, branch, digest, publications and final fence are verified. +- [ ] Read-only architecture is enforced; no model, source, branch, approval or lifecycle capability is reachable. +- [ ] All fixed input/work/output limits and state distinctions pass. +- [ ] At least 30 distinct risk-derived cases pass, including all 24 named minimum risk categories; coverage meets repository thresholds and new pure policy branch target. +- [ ] English/Spanish, fallback, sanitization, replay, link safety and comment budget pass. +- [ ] Docs and catalog evidence are current; #454 human UX evidence is reviewed. +- [ ] Failure, race, unavailable, bounded and recovery behavior passes without false current claims. +- [ ] `pnpm run validate:specifications` passes and catalog output is current according to repository workflow. +- [ ] No readiness-blocking decision remains unresolved. + +## 20. References and decisions + +- Primary sources: Issue #464 and its explicit answer rounds supplied with this request; existing repository contracts named in those answers, pending source-path confirmation. +- Related specs/ADRs/issues/PRs: owning `issue-start-and-sdd-readiness` SDD; live issue #454 as UX evidence; exact URLs and catalog identifiers beyond the supplied capability ID remain to be confirmed from repository metadata. +- Decisions: companion SDD under the existing capability; exact native same-repository PR closing-issue relationship is authoritative; digest mismatch always reports stale; pending-revision requires matching digest and verified first publication; first publication fields remain immutable; no arbitrary paths, refs, body URLs, semantic inference, model, writes or new record store. +- Follow-up outside this spec: any broader semantic SDD analysis, changes to gate lifecycle/record schema, or general PR-to-issue discovery. \ No newline at end of file From cb6b3690dd3f3ac5679010f9c5acaee1effc0f8d Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Mon, 12 Oct 2026 16:09:53 +0200 Subject: [PATCH 2/9] feature/464: compare verified immutable SDD publications --- build/cli/index.js | 679 +++++++++++++++++- build/github_action/index.js | 674 ++++++++++++++++- docs/issues/comment-commands.mdx | 31 +- docs/issues/pre-branch-sdds.mdx | 17 + scripts/coverage-budgets.json | 25 + specs/CATALOG.md | 8 +- specs/catalog.json | 28 +- specs/pre-branch-sdd-diff.md | 126 +++- specs/pre-branch-sdd-status-readiness.md | 21 +- ...tic-github-publication-and-notification.md | 7 + .../sdd_diff_presentation_policy.test.ts | 123 ++++ .../policies/agent_activity_policy.ts | 2 +- .../policies/copilot_interaction_policy.ts | 1 + .../policies/publication_message_catalog.ts | 3 + .../policies/sdd_diff_message_catalog.ts | 80 +++ .../policies/sdd_diff_presentation_policy.ts | 83 +++ .../semantic_result_publication_policy.ts | 9 + src/application/ports/sdd_diff_read_ports.ts | 42 ++ .../ports/sdd_evidence_read_ports.ts | 2 + .../sdd_diff_comment_interaction.test.ts | 74 ++ .../comment_automation_command_workflow.ts | 11 + .../usecases/comment_automation_context.ts | 30 + .../usecases/comment_automation_contracts.ts | 2 + .../usecases/comment_automation_use_case.ts | 2 +- .../usecases/issue_comment_use_case.ts | 3 + .../pull_request_review_comment_use_case.ts | 3 + .../__tests__/sdd_diff_query_use_case.test.ts | 152 ++++ .../sdd_evidence_query_use_case.test.ts | 21 + .../usecases/sdd/sdd_diff_query_use_case.ts | 121 ++++ .../sdd/sdd_evidence_query_use_case.ts | 6 +- .../sdd_diff_reply_publication.test.ts | 121 ++++ .../steps/common/publish_resume_workflow.ts | 10 +- .../common/reply_publication_workflow.ts | 11 +- .../github_publication_mutation_baseline.json | 81 ++- src/data/model/__tests__/execution.test.ts | 26 + .../sdd_diff_link_repository.test.ts | 91 +++ .../sdd/sdd_diff_link_repository.ts | 69 ++ .../__tests__/pre_branch_sdd_diff.test.ts | 90 +++ src/domain/copilot_command.ts | 2 + src/domain/pre_branch_sdd_diff.ts | 120 ++++ .../main_run_route_composition_root.test.ts | 5 + .../sdd_diff_composition_root.test.ts | 64 ++ .../main_run_route_composition_root.ts | 3 + .../composition/sdd_diff_composition_root.ts | 25 + 44 files changed, 3014 insertions(+), 90 deletions(-) create mode 100644 src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts create mode 100644 src/application/policies/sdd_diff_message_catalog.ts create mode 100644 src/application/policies/sdd_diff_presentation_policy.ts create mode 100644 src/application/ports/sdd_diff_read_ports.ts create mode 100644 src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts create mode 100644 src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts create mode 100644 src/application/usecases/sdd/sdd_diff_query_use_case.ts create mode 100644 src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts create mode 100644 src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts create mode 100644 src/data/repository/sdd/sdd_diff_link_repository.ts create mode 100644 src/domain/__tests__/pre_branch_sdd_diff.test.ts create mode 100644 src/domain/pre_branch_sdd_diff.ts create mode 100644 src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts create mode 100644 src/infrastructure/composition/sdd_diff_composition_root.ts diff --git a/build/cli/index.js b/build/cli/index.js index 5158c07c5..a900948d5 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -36940,7 +36940,7 @@ exports.shouldTrackAgentActivity = shouldTrackAgentActivity; const agent_1 = __nccwpck_require__(89040); const copilot_command_1 = __nccwpck_require__(11771); const branch_sync_command_1 = __nccwpck_require__(51114); -const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'dismiss', 'remember']); +const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'sdd-diff', 'dismiss', 'remember']); /** Decides whether a route can invoke an agent for its current event. */ function shouldTrackAgentActivity(execution, route) { if (!hasTarget(execution)) @@ -40583,6 +40583,7 @@ const READ_ONLY_COMMANDS = Object.freeze([ ['/copilot recheck', 'interaction.help.command.recheck'], ['/copilot description', 'interaction.help.command.description'], ['/copilot status', 'interaction.help.command.status'], + ['/copilot sdd-diff', 'interaction.help.command.sddDiff'], ]); const CHANGE_COMMANDS = Object.freeze([ ['/copilot fix ', 'interaction.help.command.fixOne'], @@ -42463,6 +42464,7 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.help.command.recheck', 'interaction.help.command.description', 'interaction.help.command.status', + 'interaction.help.command.sddDiff', 'interaction.help.command.fixOne', 'interaction.help.command.fixAll', 'interaction.help.command.dismiss', @@ -42582,6 +42584,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 're-run the review and reconcile findings.', 'interaction.help.command.description': 'refresh the pull-request description.', 'interaction.help.command.status': 'show the current automation status.', + 'interaction.help.command.sddDiff': 'compare verified first and current SDD publications without a model.', 'interaction.help.command.fixOne': 'fix one reported finding.', 'interaction.help.command.fixAll': 'fix all unresolved findings.', 'interaction.help.command.dismiss': 'dismiss a finding.', @@ -42710,6 +42713,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 'repite la revisión y reconcilia los hallazgos.', 'interaction.help.command.description': 'actualiza la descripción del pull request.', 'interaction.help.command.status': 'muestra el estado actual de la automatización.', + 'interaction.help.command.sddDiff': 'compara las publicaciones inicial y actual verificadas del SDD sin modelo.', 'interaction.help.command.fixOne': 'corrige un hallazgo.', 'interaction.help.command.fixAll': 'corrige todos los hallazgos sin resolver.', 'interaction.help.command.dismiss': 'descarta un hallazgo.', @@ -43571,6 +43575,191 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 2230: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPANISH_SDD_DIFF_DEFINITION = exports.ENGLISH_SDD_DIFF_DEFINITION = exports.SDD_DIFF_MESSAGE_IDS = void 0; +exports.resolveSddDiffCatalog = resolveSddDiffCatalog; +exports.isSddDiffResolvedLocale = isSddDiffResolvedLocale; +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +exports.SDD_DIFF_MESSAGE_IDS = Object.freeze([ + 'sddDiff.heading', 'sddDiff.status', 'sddDiff.action', 'sddDiff.not-applicable', 'sddDiff.no-publication', + 'sddDiff.initial-only', 'sddDiff.stale', 'sddDiff.pending-revision', 'sddDiff.invalid', 'sddDiff.unavailable', + 'sddDiff.ambiguous', 'sddDiff.bounded', 'sddDiff.compared', 'sddDiff.none', 'sddDiff.publish', 'sddDiff.refresh', + 'sddDiff.answer', 'sddDiff.retry', 'sddDiff.choose', 'sddDiff.reduce', 'sddDiff.first', 'sddDiff.current', + 'sddDiff.historical', 'sddDiff.card', 'sddDiff.commit', 'sddDiff.compare', 'sddDiff.lines', 'sddDiff.sections', 'sddDiff.omitted', 'sddDiff.structural', + 'sddDiff.charactersLimit', 'sddDiff.bytesLimit', 'sddDiff.linesLimit', 'sddDiff.workLimit', +]); +const EN = Object.freeze({ + 'sddDiff.heading': 'SDD structural comparison', 'sddDiff.status': 'Current status', 'sddDiff.action': 'Next action', + 'sddDiff.not-applicable': 'An SDD comparison is not applicable to this target.', + 'sddDiff.no-publication': 'No verified SDD publication is available yet.', + 'sddDiff.initial-only': 'Only the first publication is verified; there is no revision pair to compare.', + 'sddDiff.stale': 'Current issue, SDD or branch evidence changed or disagrees. Historical evidence does not certify a current comparison.', + 'sddDiff.pending-revision': 'The requested SDD revision is pending. Current change counts are unavailable.', + 'sddDiff.invalid': 'The owning SDD evidence could not be verified.', + 'sddDiff.unavailable': 'The comparison could not be verified. No current change counts are available.', + 'sddDiff.ambiguous': 'More than one issue is linked to this PR.', + 'sddDiff.bounded': 'The verified documents exceed a fixed comparison limit; exact counts are unavailable.', + 'sddDiff.compared': 'Compared verified published revisions.', 'sddDiff.none': 'No action is required.', + 'sddDiff.publish': 'Publish the SDD through the existing issue workflow, then retry this command.', + 'sddDiff.refresh': 'Retry this command with current evidence; if the issue changed, resume its existing SDD clarification workflow.', + 'sddDiff.answer': 'Answer the existing SDD questions or resume publication, then retry this command.', + 'sddDiff.retry': 'Inspect the existing SDD evidence and read access, then retry this command.', + 'sddDiff.choose': 'Run this command in the intended issue conversation.', + 'sddDiff.reduce': 'Keep the SDD within the documented fixed limits, then retry this command.', + 'sddDiff.first': 'First publication', 'sddDiff.current': 'Current publication', 'sddDiff.historical': 'Historical first publication', + 'sddDiff.card': 'SDD clarification card', 'sddDiff.commit': 'Publication commit', 'sddDiff.compare': 'Compare publication commits', + 'sddDiff.lines': 'Added lines: {added}; removed lines: {removed}.', 'sddDiff.sections': 'Changed section titles', + 'sddDiff.omitted': 'Additional changed titles omitted: {count}.', + 'sddDiff.charactersLimit': 'Limit: 70,000 characters per document.', 'sddDiff.bytesLimit': 'Limit: 280,000 UTF-8 bytes per document.', + 'sddDiff.linesLimit': 'Limit: 2,000 content lines per document.', 'sddDiff.workLimit': 'Limit: 2,000,000 line-comparison steps.', + 'sddDiff.structural': 'This compares structure and content lines; it does not assess semantic requirements.', +}); +const ES = Object.freeze({ + 'sddDiff.heading': 'Comparación estructural del SDD', 'sddDiff.status': 'Estado actual', 'sddDiff.action': 'Siguiente acción', + 'sddDiff.not-applicable': 'La comparación de SDD no corresponde a este destino.', + 'sddDiff.no-publication': 'Todavía no hay una publicación de SDD verificada.', + 'sddDiff.initial-only': 'Sólo está verificada la primera publicación; no hay dos revisiones que comparar.', + 'sddDiff.stale': 'La evidencia de la issue, del SDD o de la rama cambió o no coincide. La evidencia histórica no certifica una comparación actual.', + 'sddDiff.pending-revision': 'La revisión solicitada del SDD está pendiente. No hay recuentos de cambios actuales.', + 'sddDiff.invalid': 'No se pudo verificar la evidencia del SDD propietario.', + 'sddDiff.unavailable': 'No se pudo verificar la comparación. No hay recuentos de cambios actuales.', + 'sddDiff.ambiguous': 'Esta PR enlaza más de una issue.', + 'sddDiff.bounded': 'Los documentos verificados superan un límite fijo de comparación; no hay recuentos exactos.', + 'sddDiff.compared': 'Se compararon las revisiones publicadas y verificadas.', 'sddDiff.none': 'No se requiere ninguna acción.', + 'sddDiff.publish': 'Publica el SDD mediante el flujo existente de la issue y vuelve a ejecutar el comando.', + 'sddDiff.refresh': 'Vuelve a ejecutar el comando con evidencia actual; si cambió la issue, retoma su flujo existente de aclaraciones del SDD.', + 'sddDiff.answer': 'Responde las preguntas existentes del SDD o retoma su publicación y vuelve a ejecutar el comando.', + 'sddDiff.retry': 'Revisa la evidencia existente del SDD y los permisos de lectura y vuelve a ejecutar el comando.', + 'sddDiff.choose': 'Ejecuta el comando en la conversación de la issue deseada.', + 'sddDiff.reduce': 'Mantén el SDD dentro de los límites fijos documentados y vuelve a ejecutar el comando.', + 'sddDiff.first': 'Primera publicación', 'sddDiff.current': 'Publicación actual', 'sddDiff.historical': 'Primera publicación histórica', + 'sddDiff.card': 'Tarjeta de aclaraciones del SDD', 'sddDiff.commit': 'Commit de publicación', 'sddDiff.compare': 'Comparar commits de publicación', + 'sddDiff.lines': 'Líneas añadidas: {added}; líneas eliminadas: {removed}.', 'sddDiff.sections': 'Títulos de secciones modificadas', + 'sddDiff.omitted': 'Títulos modificados adicionales omitidos: {count}.', + 'sddDiff.charactersLimit': 'Límite: 70.000 caracteres por documento.', 'sddDiff.bytesLimit': 'Límite: 280.000 bytes UTF-8 por documento.', + 'sddDiff.linesLimit': 'Límite: 2.000 líneas de contenido por documento.', 'sddDiff.workLimit': 'Límite: 2.000.000 pasos de comparación de líneas.', + 'sddDiff.structural': 'La comparación incluye estructura y líneas de contenido; no evalúa requisitos semánticos.', +}); +exports.ENGLISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'en-US', compatibleBaseLanguage: 'en', messages: EN, +}); +exports.SPANISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', messages: ES, +}); +function resolveSddDiffCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_SDD_DIFF_DEFINITION, [exports.ENGLISH_SDD_DIFF_DEFINITION, exports.SPANISH_SDD_DIFF_DEFINITION]); +} +/** Locale admission belongs to the complete bundled catalog boundary. */ +function isSddDiffResolvedLocale(value) { + return value === 'en-US' || value === 'es-ES'; +} + + +/***/ }), + +/***/ 63302: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.renderSddDiff = renderSddDiff; +const pre_branch_sdd_1 = __nccwpck_require__(34730); +const STATES = ['not-applicable', 'no-publication', 'initial-only', 'stale', 'pending-revision', 'invalid', 'unavailable', 'ambiguous', 'bounded', 'compared']; +const ACTIONS = Object.freeze({ + 'not-applicable': 'sddDiff.none', 'no-publication': 'sddDiff.publish', 'initial-only': 'sddDiff.none', + stale: 'sddDiff.refresh', 'pending-revision': 'sddDiff.answer', invalid: 'sddDiff.retry', unavailable: 'sddDiff.retry', + ambiguous: 'sddDiff.choose', bounded: 'sddDiff.reduce', compared: 'sddDiff.none', +}); +/** Immutable links are constructed only from a bound server/repository and verified publication facts. */ +function renderSddDiff(snapshot, scope, catalog) { + const render = catalog.message; + const heading = `## ${render('sddDiff.heading')}`; + const fallback = `${heading}\n\n${render('sddDiff.unavailable')}\n\n**${render('sddDiff.action')}:** ${render('sddDiff.retry')}`; + const root = repositoryRoot(scope); + if (!root || !STATES.includes(snapshot.state)) + return fallback; + const lines = [heading, '', `**${render('sddDiff.status')}:** ${render(`sddDiff.${snapshot.state}`)}`, '', + `**${render('sddDiff.action')}:** ${render(ACTIONS[snapshot.state])}`]; + if ('publication' in snapshot && snapshot.publication) { + const publication = snapshot.publication; + if (!(0, pre_branch_sdd_1.isSafeSddPath)(publication.path) || !sha(publication.firstSha)) + return fallback; + const historical = snapshot.state === 'stale' || snapshot.state === 'pending-revision'; + if (historical) + lines.push('', link('sddDiff.historical', publication.firstSha, publication.path)); + else { + lines.push('', link('sddDiff.first', publication.firstSha, publication.path)); + if (snapshot.state !== 'initial-only') { + if (!sha(publication.currentSha) || publication.currentSha === publication.firstSha) + return fallback; + lines.push(link('sddDiff.current', publication.currentSha, publication.path)); + } + } + } + else if (snapshot.state === 'initial-only' || snapshot.state === 'compared' || snapshot.state === 'bounded') + return fallback; + if (snapshot.card && (snapshot.state === 'stale' || snapshot.state === 'pending-revision')) { + if (!positive(snapshot.card.issueNumber) || !positive(snapshot.card.commentId)) + return fallback; + lines.push('', `[${render('sddDiff.card')}](${root}/issues/${snapshot.card.issueNumber}#issuecomment-${snapshot.card.commentId})`); + } + if (snapshot.state === 'compared' || snapshot.state === 'bounded') { + const publication = snapshot.publication; + lines.push('', `[${render('sddDiff.compare')}](${root}/compare/${publication.firstSha}...${publication.currentSha})`); + } + if (snapshot.state === 'bounded') { + if (!['characters', 'bytes', 'lines', 'work'].includes(snapshot.reason)) + return fallback; + lines.push('', render(`sddDiff.${snapshot.reason}Limit`)); + } + if (snapshot.state === 'compared') { + const changes = snapshot.changes; + if (changes.state !== 'compared' || !count(changes.addedLines, 2000) || !count(changes.removedLines, 2000) + || !count(changes.omittedSections, 4000) || !Array.isArray(changes.sections) || changes.sections.length > 8 + || changes.sections.some(title => typeof title !== 'string' || title.length > 120)) + return fallback; + lines.push('', render('sddDiff.lines', { added: changes.addedLines, removed: changes.removedLines }), '', render('sddDiff.structural')); + if (changes.sections.length) + lines.push('', `**${render('sddDiff.sections')}:**`, ...changes.sections.map(title => `- ${escapeTitle(title)}`)); + if (changes.omittedSections) + lines.push('', render('sddDiff.omitted', { count: changes.omittedSections })); + } + const body = lines.join('\n'); + return body.length <= 5600 ? body : fallback; + function link(label, commit, path) { + return `[${render(label)} · ${commit.slice(0, 9)}](${root}/blob/${commit}/${path}) · [${render('sddDiff.commit')}](${root}/commit/${commit})`; + } +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function count(value, max) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= max; } +function sha(value) { return typeof value === 'string' && /^[a-f0-9]{40}$/u.test(value); } +function repositoryRoot(scope) { + if (scope.serverOrigin.length > 200 || ![scope.owner, scope.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return undefined; + try { + const url = new URL(scope.serverOrigin); + return url.protocol === 'https:' && !url.username && !url.password && url.pathname === '/' && !url.search && !url.hash + ? `${url.origin}/${scope.owner}/${scope.repository}` : undefined; + } + catch { + return undefined; + } +} +function escapeTitle(value) { + return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').replace(/@/gu, '@\u200b') + .replace(/([\\`*_{}[\]()<>#|~!])/gu, '\\$1'); +} + + /***/ }), /***/ 99167: @@ -43778,6 +43967,7 @@ exports.hasOwnedPrimaryIssuePublication = hasOwnedPrimaryIssuePublication; exports.selectSemanticReplyIntents = selectSemanticReplyIntents; exports.renderSemanticReply = renderSemanticReply; exports.renderSemanticStatus = renderSemanticStatus; +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const github_publication_1 = __nccwpck_require__(35793); const github_user_policy_1 = __nccwpck_require__(84403); const result_1 = __nccwpck_require__(73817); @@ -43865,6 +44055,11 @@ function selectSemanticReplyIntents(context) { return [replyIntent(context, correlationId, 'direct-answer', directAnswer)]; const publication = (0, result_1.getResultPayload)(payload.publication); const kind = publication?.kind; + if (kind === 'sdd-diff' && result.id.endsWith('.SddDiff') + && typeof publication?.markdown === 'string' && publication.markdown.trim() && publication.markdown.length <= 5600 + && (0, sdd_diff_message_catalog_1.isSddDiffResolvedLocale)(publication.locale)) { + return [replyIntent({ ...context, locale: publication.locale }, correlationId, 'copilot-sdd-diff', Object.freeze({ kind: 'sdd-diff', markdown: publication.markdown }))]; + } if (kind === 'help' || kind === 'welcome') { const botLogin = typeof publication?.botLogin === 'string' && publication.botLogin.trim() ? publication.botLogin.trim() @@ -43914,6 +44109,8 @@ function renderSemanticReply(intent, catalog = (0, publication_message_catalog_1 return `${marker}\n\n${body}`; } function renderReplyBody(intent, catalog) { + if (intent.projection.kind === 'sdd-diff') + return (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.markdown); if (intent.projection.kind === 'implementation-result') { return safeCatalogSentence(catalog.render(intent.projection.outcome === 'changes-pushed' ? 'interaction.implementation.pushed' : 'interaction.implementation.noChanges', { branch: inlineRef(intent.projection.branch) })); @@ -52588,6 +52785,8 @@ async function runDoUserRequestAction(param, options, intentPayload) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; +const sdd_diff_presentation_policy_1 = __nccwpck_require__(63302); +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const result_1 = __nccwpck_require__(73817); const read_copilot_status_workflow_1 = __nccwpck_require__(57206); const copilot_interaction_policy_1 = __nccwpck_require__(90108); @@ -52603,6 +52802,15 @@ const LEARNED_BUGBOT_RULE_PATH = '.copilot/BUGBOT.learned.md'; async function runExplicitCommentCommand(param, options, command, actorAuthorizationPort) { if (command.name === 'help') return runHelpCommand(param, options); + if (command.name === 'sdd-diff') { + const snapshot = param.sddDiff && options.sddDiffQueryPort + ? await options.sddDiffQueryPort.read(param.sddDiff) : { state: 'unavailable' }; + const catalog = (0, sdd_diff_message_catalog_1.resolveSddDiffCatalog)(param.language.locale); + const markdown = (0, sdd_diff_presentation_policy_1.renderSddDiff)(snapshot, { owner: param.status.owner, repository: param.status.repository, + serverOrigin: options.sddDiffQueryPort?.serverOrigin ?? 'https://github.com' }, catalog); + return [new result_1.Result({ id: `${options.taskId}.SddDiff`, success: true, executed: true, stepFormat: 'markdown', + steps: [markdown], payload: Object.freeze({ publication: Object.freeze({ kind: 'sdd-diff', markdown, locale: catalog.locale }) }) })]; + } if (command.name === 'status') return [await (0, read_copilot_status_workflow_1.readCopilotStatus)(param, options.taskId, options.statusReadPorts, options.sddQueryPort)]; if (command.name === 'dismiss') @@ -52846,6 +53054,7 @@ function projectCommentAutomationContext(source, language, userComment) { }), think: (0, think_workflow_1.projectThinkContext)(source), status: (0, status_command_policy_1.buildCopilotStatusSnapshot)(source), + ...projectSddDiffRequest(source), ...(source.issue?.number > 0 && !(source.pullRequest?.number > 0) ? { sdd: Object.freeze({ issueNumber: source.issue.number, admittedKind: issue_workflow_profile_1.ISSUE_WORKFLOW_KINDS.find(kind => kind === source.issueWorkflowKind), @@ -52903,6 +53112,29 @@ function withCommentLanguageAdaptation(context, adaptation) { }), }); } +/** Copies trusted workflow/profile facts; PR issue identity/author come only from the native link query. */ +function projectSddDiffRequest(source) { + const status = (0, status_command_policy_1.buildCopilotStatusSnapshot)(source); + const repositoryId = source.inputs?.repository?.id; + const number = status.target === 'pull-request' ? status.pullRequestNumber : status.issueNumber; + if (!Number.isSafeInteger(repositoryId) || !repositoryId || repositoryId < 1 || !number || number < 1 + || !source.issueWorkflowProfile || typeof source.issue.issueManagedBranches !== 'boolean' + || typeof source.preBranchSdd !== 'boolean') + return {}; + const list = (...values) => Object.freeze(values.filter((value) => typeof value === 'string')); + const labels = source.labels; + return { sddDiff: Object.freeze({ + target: Object.freeze({ kind: status.target, number }), repositoryId, + ...(status.target === 'issue' && source.inputs?.issue?.number === number + ? { issueAuthor: source.inputs.issue.user?.login } : {}), + profile: Object.freeze({ schemaVersion: source.issueWorkflowProfile.schemaVersion, enabled: Object.freeze([...source.issueWorkflowProfile.enabled]) }), + labelConfiguration: Object.freeze({ feature: list(labels.feature, labels.enhancement), bugfix: list(labels.bugfix, labels.bug), + documentation: list(labels.documentation, labels.docs), chore: list(labels.chore, labels.maintenance), + help: list(labels.help, labels.question), hotfix: list(labels.hotfix), release: list(labels.release) }), + profileDigest: source.issueWorkflowProfileDigest, trustedBotLogin: source.tokenUser?.trim() ?? '', + issueManagedBranches: source.issue.issueManagedBranches, preBranchSdd: source.preBranchSdd, baseBranch: source.branches?.development, + }) }; +} /***/ }), @@ -53026,7 +53258,7 @@ async function runCommentAutomation(initialParam, options, actorAuthorizationPor return [(0, comment_automation_command_workflow_1.invalidCommentCommandResult)(options.taskId, command.reason)]; } const isPublicMetadataCommand = command.kind === 'command' - && (command.command.name === 'help' || command.command.name === 'status'); + && (command.command.name === 'help' || command.command.name === 'status' || command.command.name === 'sdd-diff'); if (!isPublicMetadataCommand && param.membersOnly && !await actorAuthorizationPort.isActorAllowedToUseMemberOnlyAutomation(param.actor)) { (0, logging_ports_1.logInfo)('Skipping agent automation because ai-members-only is enabled and the actor is not authorized.'); @@ -53663,7 +53895,7 @@ const comment_automation_context_1 = __nccwpck_require__(37055); const pull_request_workflow_context_1 = __nccwpck_require__(73447); const pre_branch_sdd_1 = __nccwpck_require__(34730); class IssueCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, sddQueryPort) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, sddQueryPort, sddDiffQueryPort) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -53680,6 +53912,7 @@ class IssueCommentUseCase { this.rememberBranchPorts = rememberBranchPorts; this.statusReadPorts = statusReadPorts; this.sddQueryPort = sddQueryPort; + this.sddDiffQueryPort = sddDiffQueryPort; this.taskId = "IssueCommentUseCase"; } async invoke(param) { @@ -53718,6 +53951,7 @@ class IssueCommentUseCase { rememberBranchPorts: this.rememberBranchPorts, statusReadPorts: this.statusReadPorts, sddQueryPort: this.sddQueryPort, + sddDiffQueryPort: this.sddDiffQueryPort, syncBranchUseCase: this.syncBranchUseCase ? { invoke: (options) => this.syncBranchUseCase.invoke({ context: context.branchSync, options }) } : undefined, @@ -54376,7 +54610,7 @@ const check_pull_request_comment_language_use_case_1 = __nccwpck_require__(21729 const comment_automation_context_1 = __nccwpck_require__(37055); const pull_request_workflow_context_1 = __nccwpck_require__(73447); class PullRequestReviewCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts, sddDiffQueryPort) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -54391,6 +54625,7 @@ class PullRequestReviewCommentUseCase { this.syncBranchUseCase = syncBranchUseCase; this.rememberBranchPorts = rememberBranchPorts; this.statusReadPorts = statusReadPorts; + this.sddDiffQueryPort = sddDiffQueryPort; this.taskId = "PullRequestReviewCommentUseCase"; } async invoke(param) { @@ -54416,6 +54651,7 @@ class PullRequestReviewCommentUseCase { rememberBugbotRuleUseCase: this.rememberBugbotRuleUseCase, rememberBranchPorts: this.rememberBranchPorts, statusReadPorts: this.statusReadPorts, + sddDiffQueryPort: this.sddDiffQueryPort, syncBranchUseCase: this.syncBranchUseCase ? { invoke: (options) => this.syncBranchUseCase.invoke({ context: context.branchSync, options }) } : undefined, @@ -55354,6 +55590,151 @@ function canonicalIssueUrl(context) { } +/***/ }), + +/***/ 48684: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffQueryUseCase = void 0; +const issue_workflow_profile_1 = __nccwpck_require__(26744); +const pre_branch_sdd_diff_1 = __nccwpck_require__(28558); +/** Structural comparison over shared verified evidence, with no writer/model capability. */ +class SddDiffQueryUseCase { + constructor(ports) { + this.ports = Object.freeze({ + evidence: Object.freeze({ read: ports.evidence.read.bind(ports.evidence) }), + links: Object.freeze({ readClosingIssues: ports.links.readClosingIssues.bind(ports.links) }), + labels: Object.freeze({ getLabels: ports.labels.getLabels.bind(ports.labels) }), + descriptions: Object.freeze({ getDescription: ports.descriptions.getDescription.bind(ports.descriptions) }), + documents: Object.freeze({ readSdd: ports.documents.readSdd.bind(ports.documents) }), + }); + } + async read(request) { + try { + if (!positive(request.target.number) || !positive(request.repositoryId) + || !['issue', 'pull-request'].includes(request.target.kind)) + return { state: 'unavailable' }; + let issueNumber = request.target.number; + let issueAuthor = request.issueAuthor; + let links; + if (request.target.kind === 'pull-request') { + const association = await this.ports.links.readClosingIssues(request.target.number); + if (association.state !== 'verified' || association.pullNumber !== request.target.number) + return { state: 'unavailable' }; + if (association.issueNumbers.length === 0) + return { state: 'not-applicable' }; + if (association.issueNumbers.length !== 1) + return { state: 'ambiguous' }; + links = association; + issueNumber = association.issueNumbers[0]; + issueAuthor = association.issueAuthors[issueNumber]; + } + if (!issueAuthor?.trim()) + return { state: 'unavailable' }; + const context = await this.context(request, issueNumber, issueAuthor); + if (!context) + return { state: 'not-applicable' }; + const original = await this.ports.evidence.read(context); + const projection = projectEvidence(original, issueNumber); + if (original.state !== 'published' || !original.verified?.currentSha) + return projection; + if (!/^[a-f0-9]{64}$/u.test(original.authorityDigest ?? '')) + return { state: 'unavailable' }; + if (links && (links.headSha !== original.verified.branchHeadSha || links.headBranch !== original.verified.branchName)) { + return historicalStale(original, issueNumber); + } + const publication = publicationOf(original); + let result = { state: 'initial-only', publication }; + if (original.verified.currentSha !== original.verified.firstSha) { + const [before, after] = await Promise.all([ + this.ports.documents.readSdd(original.verified.firstSha, original.verified.path), + this.ports.documents.readSdd(original.verified.currentSha, original.verified.path), + ]); + if (before === undefined || after === undefined) + return { state: 'unavailable' }; + const changes = (0, pre_branch_sdd_diff_1.compareSddStructure)(before, after); + result = changes.state === 'bounded' + ? { state: 'bounded', publication, reason: changes.reason } + : { state: 'compared', publication, changes }; + } + // Re-admit the exact issue, then re-read the shared fenced authority. + const freshContext = await this.context(request, issueNumber, issueAuthor); + if (!freshContext || freshContext.admittedKind !== context.admittedKind) + return { state: 'stale' }; + const fresh = await this.ports.evidence.read(freshContext); + if (!original.authorityDigest || fresh.authorityDigest !== original.authorityDigest + || proofOf(fresh) !== proofOf(original)) { + return fresh.state === 'published' ? historicalStale(fresh, issueNumber) : projectEvidence(fresh, issueNumber); + } + if (links) { + const freshLinks = await this.ports.links.readClosingIssues(request.target.number); + if (freshLinks.state !== 'verified') + return { state: 'unavailable' }; + if (associationProof(freshLinks) !== associationProof(links)) + return historicalStale(fresh, issueNumber); + } + return Object.freeze(result); + } + catch { + return { state: 'unavailable' }; + } + } + async context(request, issueNumber, issueAuthor) { + const [labels, body] = await Promise.all([ + this.ports.labels.getLabels(issueNumber), this.ports.descriptions.getDescription(issueNumber), + ]); + if (body === undefined) + throw new Error('Issue admission is unavailable.'); + const admission = (0, issue_workflow_profile_1.classifyIssueWorkflow)(labels, request.profile, request.labelConfiguration, body); + return admission.status === 'eligible' ? Object.freeze({ issueNumber, admittedKind: admission.kind, issueAuthor, + profileDigest: request.profileDigest, trustedBotLogin: request.trustedBotLogin, + issueManagedBranches: request.issueManagedBranches, preBranchSdd: request.preBranchSdd, baseBranch: request.baseBranch }) : undefined; + } +} +exports.SddDiffQueryUseCase = SddDiffQueryUseCase; +function projectEvidence(evidence, issueNumber) { + if (evidence.state === 'not-required') + return { state: 'not-applicable' }; + if (evidence.state === 'absent') + return { state: 'no-publication' }; + if (evidence.state === 'invalid' || evidence.state === 'unavailable') + return { state: evidence.state }; + const publication = publicationOf(evidence); + if (evidence.state === 'stale') + return historicalStale(evidence, issueNumber); + if (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') { + return publication ? { state: 'pending-revision', publication, ...cardOf(evidence, issueNumber) } : { state: 'no-publication' }; + } + if (!publication?.currentSha) + return { state: 'invalid' }; + return publication.currentSha === publication.firstSha ? { state: 'initial-only', publication } : { state: 'unavailable' }; +} +function publicationOf(evidence) { + return evidence.verified ? Object.freeze({ path: evidence.verified.path, firstSha: evidence.verified.firstSha, + ...(evidence.verified.currentSha ? { currentSha: evidence.verified.currentSha } : {}) }) : undefined; +} +function historicalStale(evidence, issueNumber) { + const publication = publicationOf(evidence); + return { state: 'stale', ...(publication ? { publication } : {}), ...cardOf(evidence, issueNumber) }; +} +function cardOf(evidence, issueNumber) { + return positive(evidence.cardId) ? { card: Object.freeze({ issueNumber, commentId: evidence.cardId }) } : {}; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function proofOf(evidence) { + const verified = evidence.verified; + return JSON.stringify([evidence.state, evidence.authorityDigest, evidence.cardId, evidence.path, evidence.pendingQuestionIds, + verified?.path, verified?.capabilityId, verified?.branchName, verified?.branchHeadSha, verified?.firstSha, verified?.currentSha]); +} +function associationProof(links) { + return JSON.stringify([links.pullNumber, links.headSha, links.headBranch, + links.issueNumbers.map(number => [number, links.issueAuthors[number].toLowerCase()])]); +} + + /***/ }), /***/ 39893: @@ -55363,6 +55744,7 @@ function canonicalIssueUrl(context) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SddEvidenceQueryUseCase = void 0; +const node_crypto_1 = __nccwpck_require__(6005); const issue_start_policy_1 = __nccwpck_require__(90332); const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_evidence_read_ports_1 = __nccwpck_require__(94752); @@ -55394,7 +55776,10 @@ class SddEvidenceQueryUseCase { if (!['invalid', 'unavailable'].includes(result.state) && !await this.fence(context, facts, selection.body, cardId, record, result.verified)) return { state: 'stale' }; - return Object.freeze(result); + return Object.freeze({ ...result, ...(!['invalid', 'unavailable'].includes(result.state) ? { + authorityDigest: (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([cardId, selection.body, digest, canonicalLabels(facts.labels), + record.phase === 'awaiting-answer' ? facts.comments : null])).digest('hex'), + } : {}) }); } catch { return { state: 'unavailable' }; @@ -68255,7 +68640,7 @@ function containsCredentialMaterial(value, insideStorage = false) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.DryRunSetupPlanConfirmation = exports.SetupPlanConfirmationAdapter = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_questionnaire_policy_1 = __nccwpck_require__(6009); class SetupPlanConfirmationAdapter { constructor(terminal, assumeYes) { @@ -68388,7 +68773,7 @@ async function collectSetupCredentials(input) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupCredentialPromptAdapter = exports.SetupTerminalCancelledError = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_token_permission_presenter_1 = __nccwpck_require__(63206); const setup_interaction_cancelled_error_1 = __nccwpck_require__(38313); const setup_workflow_pat_storage_copy_1 = __nccwpck_require__(97798); @@ -68666,7 +69051,7 @@ exports.SetupDoctorPresenter = void 0; exports.renderDoctorReport = renderDoctorReport; exports.doctorCheckLabel = doctorCheckLabel; const setup_doctor_message_catalog_1 = __nccwpck_require__(80226); -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); class SetupDoctorPresenter { constructor(catalog = (0, setup_doctor_message_catalog_1.resolveStaticSetupDoctorCatalog)()) { this.catalog = catalog; @@ -68749,7 +69134,7 @@ function doctorCheckLabel(id, catalog = (0, setup_doctor_message_catalog_1.resol Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ConsoleSetupJourneyPresenter = void 0; exports.renderSetupJourney = renderSetupJourney; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); class ConsoleSetupJourneyPresenter { present(view) { console.log(renderSetupJourney(view)); @@ -69009,7 +69394,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ConsoleSetupPlanPresenter = void 0; exports.renderSetupPlan = renderSetupPlan; const setup_configuration_policy_1 = __nccwpck_require__(56637); -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const pull_request_approval_policy_1 = __nccwpck_require__(98820); const setup_workflow_pat_storage_copy_1 = __nccwpck_require__(97798); class ConsoleSetupPlanPresenter { @@ -69087,7 +69472,7 @@ function storageLabel(policy) { /***/ }), -/***/ 83434: +/***/ 76673: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -69241,7 +69626,7 @@ function stripAnsi(value) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ConsoleSetupQuestionRenderer = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_questionnaire_policy_1 = __nccwpck_require__(6009); const setup_question_guidance_policy_1 = __nccwpck_require__(42775); class ConsoleSetupQuestionRenderer { @@ -69788,7 +70173,7 @@ exports.renderSetupTokenPermissionSummary = renderSetupTokenPermissionSummary; exports.renderSetupTokenPermissionRequirements = renderSetupTokenPermissionRequirements; exports.renderSetupTokenPermissionReport = renderSetupTokenPermissionReport; const node_process_1 = __nccwpck_require__(97742); -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_permission_summary_policy_1 = __nccwpck_require__(10267); class ConsoleSetupTokenPermissionPresenter { constructor(mode = 'full') { @@ -69937,7 +70322,7 @@ function workflowPatStorageCopy(storage) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupWorkflowUpdatePromptAdapter = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_credential_prompt_adapter_1 = __nccwpck_require__(93232); class SetupWorkflowUpdatePromptAdapter { constructor(terminal) { @@ -80231,6 +80616,85 @@ class RepositorySecretsCommandRepository { exports.RepositorySecretsCommandRepository = RepositorySecretsCommandRepository; +/***/ }), + +/***/ 85885: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffLinkRepository = void 0; +const QUERY = `query SddDiffClosingIssues($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + databaseId nameWithOwner + pullRequest(number: $number) { + number headRefOid headRefName headRepository { databaseId nameWithOwner } repository { databaseId nameWithOwner } + closingIssuesReferences(first: 100) { + totalCount pageInfo { hasNextPage } + nodes { number author { login } repository { databaseId nameWithOwner } } + } + } + } +}`; +/** One bounded native read; absent, incomplete or conflicting identities never become guessed links. */ +class SddDiffLinkRepository { + constructor(client, binding) { + this.client = client; + this.binding = Object.freeze({ ...binding }); + } + async readClosingIssues(pullNumber) { + const unavailable = Object.freeze({ state: 'unavailable' }); + const binding = this.binding; + if (!positive(pullNumber) || !positive(binding.repositoryId) + || ![binding.owner, binding.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return unavailable; + try { + const response = object(await this.client.getClient(binding.token).graphql(QUERY, { + owner: binding.owner, repo: binding.repository, number: pullNumber, + })); + const repository = object(response?.repository); + const pull = object(repository?.pullRequest); + const links = object(pull?.closingIssuesReferences); + const page = object(links?.pageInfo); + if (!sameRepository(repository, binding) || !sameRepository(object(pull?.repository), binding) + || !sameRepository(object(pull?.headRepository), binding) + || typeof pull?.headRefName !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,249}$/u.test(pull.headRefName) + || pull?.number !== pullNumber || typeof pull.headRefOid !== 'string' || !/^[a-f0-9]{40}$/u.test(pull.headRefOid) + || page?.hasNextPage !== false || !Array.isArray(links?.nodes) + || !Number.isSafeInteger(links.totalCount) || links.totalCount !== links.nodes.length || links.nodes.length > 100) + return unavailable; + const numbers = []; + const authors = {}; + for (const value of links.nodes) { + const issue = object(value); + const author = object(issue?.author); + if (typeof author?.login !== 'string' || !author.login.trim() || author.login.length > 100 + || !positive(issue?.number) || !sameRepository(object(issue?.repository), binding) + || numbers.includes(issue.number)) + return unavailable; + numbers.push(issue.number); + authors[issue.number] = author.login; + } + return Object.freeze({ state: 'verified', pullNumber, headSha: pull.headRefOid, headBranch: pull.headRefName, + issueNumbers: Object.freeze(numbers.sort((left, right) => left - right)), issueAuthors: Object.freeze(authors) }); + } + catch { + return unavailable; + } + } +} +exports.SddDiffLinkRepository = SddDiffLinkRepository; +function object(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function sameRepository(value, binding) { + return value?.databaseId === binding.repositoryId && typeof value.nameWithOwner === 'string' + && value.nameWithOwner.toLowerCase() === `${binding.owner}/${binding.repository}`.toLowerCase(); +} + + /***/ }), /***/ 40941: @@ -81270,6 +81734,7 @@ exports.COPILOT_COMMAND_NAMES = [ 'estimate', 'test-plan', 'status', + 'sdd-diff', 'description', 'explain', 'diagnose', @@ -81328,6 +81793,8 @@ function parseCopilotCommand(raw) { && name !== 'remember') { return { kind: 'invalid', reason: `Copilot commands accept at most ${MAX_ARGUMENTS} arguments.` }; } + if (name === 'sdd-diff' && argumentsList.length > 0) + return { kind: 'invalid', reason: '/copilot sdd-diff does not accept arguments.' }; if ((name === 'fix' || name === 'dismiss' || name === 'implement' || name === 'remember') && argumentsList.length === 0) { return { kind: 'invalid', reason: `/${name} requires at least one argument.` }; } @@ -83319,6 +83786,153 @@ function isRecord(value) { } +/***/ }), + +/***/ 28558: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SDD_DIFF_LIMITS = void 0; +exports.compareSddStructure = compareSddStructure; +exports.SDD_DIFF_LIMITS = Object.freeze({ + characters: 70000, bytes: 280000, lines: 2000, comparisons: 2000000, + sectionTitles: 8, titleCharacters: 120, +}); +class ComparisonBudgetExceeded extends Error { +} +/** Exact line counts with Hirschberg's linear-memory LCS; every equality consumes bounded work. */ +function compareSddStructure(before, after) { + for (const text of [before, after]) { + if (Buffer.byteLength(text, 'utf8') > exports.SDD_DIFF_LIMITS.bytes) + return Object.freeze({ state: 'bounded', reason: 'bytes' }); + if (text.length > exports.SDD_DIFF_LIMITS.characters) + return Object.freeze({ state: 'bounded', reason: 'characters' }); + } + const left = documentLines(before); + const right = documentLines(after); + if (left.length > exports.SDD_DIFF_LIMITS.lines || right.length > exports.SDD_DIFF_LIMITS.lines) { + return Object.freeze({ state: 'bounded', reason: 'lines' }); + } + const leftMatches = new Uint8Array(left.length); + const rightMatches = new Uint8Array(right.length); + let comparisons = 0; + let common = 0; + const equal = (a, b) => { + if (comparisons === exports.SDD_DIFF_LIMITS.comparisons) + throw new ComparisonBudgetExceeded(); + comparisons++; + return a === b; + }; + const match = (a, b) => { leftMatches[a] = 1; rightMatches[b] = 1; common++; }; + const row = (ls, le, rs, re, reverse) => { + let previous = new Uint16Array(re - rs + 1); + let current = new Uint16Array(re - rs + 1); + for (let i = 0; i < le - ls; i++) { + current[0] = 0; + for (let j = 1; j <= re - rs; j++) { + current[j] = equal(left[reverse ? le - i - 1 : ls + i], right[reverse ? re - j : rs + j - 1]) + ? previous[j - 1] + 1 : Math.max(previous[j], current[j - 1]); + } + [previous, current] = [current, previous]; + } + return previous; + }; + // Row arrays leave scope before recursion; no quadratic trace is retained. + const split = (ls, middle, le, rs, re) => { + const forward = row(ls, middle, rs, re, false); + const backward = row(middle, le, rs, re, true); + let best = -1; + let offset = 0; + for (let j = 0; j <= re - rs; j++) { + const score = forward[j] + backward[re - rs - j]; + if (score > best) { + best = score; + offset = j; + } + } + return rs + offset; + }; + const align = (ls, le, rs, re) => { + if (ls === le || rs === re) + return; + if (le - ls === 1) { + for (let j = rs; j < re; j++) + if (equal(left[ls], right[j])) { + match(ls, j); + break; + } + return; + } + const middle = ls + Math.floor((le - ls) / 2); + const rightMiddle = split(ls, middle, le, rs, re); + align(ls, middle, rs, rightMiddle); + align(middle, le, rightMiddle, re); + }; + try { + let start = 0; + let leftEnd = left.length; + let rightEnd = right.length; + while (start < leftEnd && start < rightEnd && equal(left[start], right[start])) { + match(start, start); + start++; + } + while (start < leftEnd && start < rightEnd && equal(left[leftEnd - 1], right[rightEnd - 1])) { + match(--leftEnd, --rightEnd); + } + align(start, leftEnd, start, rightEnd); + } + catch (error) { + if (!(error instanceof ComparisonBudgetExceeded)) + throw error; + return Object.freeze({ state: 'bounded', reason: 'work' }); + } + const titles = new Set(); + collectChangedSections(right, rightMatches, titles); + collectChangedSections(left, leftMatches, titles); + return Object.freeze({ state: 'compared', addedLines: right.length - common, removedLines: left.length - common, + sections: Object.freeze([...titles].slice(0, exports.SDD_DIFF_LIMITS.sectionTitles).map(boundedTitle)), + omittedSections: Math.max(0, titles.size - exports.SDD_DIFF_LIMITS.sectionTitles) }); +} +/** CRLF and terminal newline markers do not count as added/removed content lines. */ +function documentLines(text) { + if (!text) + return []; + const lines = text.split(/\r?\n/u); + if (text.endsWith('\n')) + lines.pop(); + return lines; +} +function collectChangedSections(lines, matches, titles) { + let fence; + let title; + for (let index = 0; index < lines.length; index++) { + const boundary = lines[index].match(/^ {0,3}(`{3,}|~{3,})(.*)$/u); + if (boundary) { + if (!fence && (boundary[1][0] !== '`' || !boundary[2].includes('`'))) + fence = boundary[1]; + else if (fence && boundary[1][0] === fence[0] && boundary[1].length >= fence.length + && /^[ \t]*$/u.test(boundary[2])) + fence = undefined; + } + else if (!fence) { + const heading = lines[index].match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/u); + if (heading) + title = heading[1]; + } + if (matches[index] === 0 && title) + titles.add(title); + } +} +function boundedTitle(title) { + let value = title.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').slice(0, exports.SDD_DIFF_LIMITS.titleCharacters); + if (/[\uD800-\uDBFF]$/u.test(value)) + value = value.slice(0, -1); + return value; +} + + /***/ }), /***/ 98820: @@ -85954,6 +86568,7 @@ exports.createIssueCommentUseCaseCompositionRoot = createIssueCommentUseCaseComp exports.createPullRequestReviewCommentUseCaseCompositionRoot = createPullRequestReviewCommentUseCaseCompositionRoot; exports.createCommitUseCaseCompositionRoot = createCommitUseCaseCompositionRoot; exports.createMainRunRouteCompositionRoot = createMainRunRouteCompositionRoot; +const sdd_diff_composition_root_1 = __nccwpck_require__(83434); const sdd_evidence_composition_root_1 = __nccwpck_require__(51671); const linked_branch_readiness_repository_1 = __nccwpck_require__(79421); const pull_request_changes_repository_1 = __nccwpck_require__(71564); @@ -86072,7 +86687,7 @@ function createIssueCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context, (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context)); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context, (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context)); } function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); @@ -86088,7 +86703,7 @@ function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context, (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context)); } function createCommitUseCaseCompositionRoot(projectBoardCommandPort, binding) { return new commit_use_case_1.CommitUseCase(new notify_new_commit_on_issue_use_case_1.NotifyNewCommitOnIssueUseCase((0, push_single_action_capability_port_binding_1.bindIssueReopen)((0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), binding)), new check_changes_issue_size_use_case_1.CheckChangesIssueSizeUseCase((0, lifecycle_capability_port_binding_2.bindProjectBoardCommands)(projectBoardCommandPort, binding), (0, lifecycle_capability_port_binding_2.bindIssueLabels)((0, issue_labels_composition_root_1.createIssueLabelRepository)(), binding), (0, push_single_action_capability_port_binding_1.bindPullRequestBranchQuery)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchChangeSize)(new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), binding)), createDetectPotentialProblemsUseCase(binding), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(binding), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)()); @@ -86520,6 +87135,38 @@ function bindSetupRemoteConfiguration(port, binding) { } +/***/ }), + +/***/ 83434: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSddDiffQueryPort = createSddDiffQueryPort; +const sdd_evidence_query_use_case_1 = __nccwpck_require__(39893); +const sdd_diff_query_use_case_1 = __nccwpck_require__(48684); +const sdd_diff_link_repository_1 = __nccwpck_require__(85885); +const pre_branch_sdd_workspace_adapter_1 = __nccwpck_require__(35849); +const sdd_evidence_composition_root_1 = __nccwpck_require__(51671); +const github_project_client_factory_1 = __nccwpck_require__(23691); +/** No agent, provisioning, approval or lifecycle writer is supplied. */ +function createSddDiffQueryPort(binding, comments) { + const bound = Object.freeze({ ...binding }); + const reads = (0, sdd_evidence_composition_root_1.createSddEvidenceReadPorts)(bound, comments); + const evidence = new sdd_evidence_query_use_case_1.SddEvidenceQueryUseCase(reads); + const documents = new pre_branch_sdd_workspace_adapter_1.PreBranchSddWorkspaceAdapter(process.cwd(), bound.token); + const client = (0, github_project_client_factory_1.createGraphqlTransportClient)(); + return Object.freeze({ serverOrigin: process.env.GITHUB_SERVER_URL ?? 'https://github.com', + read: (request) => new sdd_diff_query_use_case_1.SddDiffQueryUseCase({ evidence: Object.freeze({ read: evidence.read.bind(evidence) }), + links: new sdd_diff_link_repository_1.SddDiffLinkRepository(client, { ...bound, repositoryId: request.repositoryId }), + labels: reads.labels, descriptions: reads.descriptions, + documents: Object.freeze({ readSdd: documents.readSdd.bind(documents) }), + }).read(request), + }); +} + + /***/ }), /***/ 51671: diff --git a/build/github_action/index.js b/build/github_action/index.js index 81d5fe8ad..ba0212ca8 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -39404,7 +39404,7 @@ exports.shouldTrackAgentActivity = shouldTrackAgentActivity; const agent_1 = __nccwpck_require__(89040); const copilot_command_1 = __nccwpck_require__(11771); const branch_sync_command_1 = __nccwpck_require__(51114); -const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'dismiss', 'remember']); +const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'sdd-diff', 'dismiss', 'remember']); /** Decides whether a route can invoke an agent for its current event. */ function shouldTrackAgentActivity(execution, route) { if (!hasTarget(execution)) @@ -43261,6 +43261,7 @@ const READ_ONLY_COMMANDS = Object.freeze([ ['/copilot recheck', 'interaction.help.command.recheck'], ['/copilot description', 'interaction.help.command.description'], ['/copilot status', 'interaction.help.command.status'], + ['/copilot sdd-diff', 'interaction.help.command.sddDiff'], ]); const CHANGE_COMMANDS = Object.freeze([ ['/copilot fix ', 'interaction.help.command.fixOne'], @@ -45278,6 +45279,7 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.help.command.recheck', 'interaction.help.command.description', 'interaction.help.command.status', + 'interaction.help.command.sddDiff', 'interaction.help.command.fixOne', 'interaction.help.command.fixAll', 'interaction.help.command.dismiss', @@ -45397,6 +45399,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 're-run the review and reconcile findings.', 'interaction.help.command.description': 'refresh the pull-request description.', 'interaction.help.command.status': 'show the current automation status.', + 'interaction.help.command.sddDiff': 'compare verified first and current SDD publications without a model.', 'interaction.help.command.fixOne': 'fix one reported finding.', 'interaction.help.command.fixAll': 'fix all unresolved findings.', 'interaction.help.command.dismiss': 'dismiss a finding.', @@ -45525,6 +45528,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 'repite la revisión y reconcilia los hallazgos.', 'interaction.help.command.description': 'actualiza la descripción del pull request.', 'interaction.help.command.status': 'muestra el estado actual de la automatización.', + 'interaction.help.command.sddDiff': 'compara las publicaciones inicial y actual verificadas del SDD sin modelo.', 'interaction.help.command.fixOne': 'corrige un hallazgo.', 'interaction.help.command.fixAll': 'corrige todos los hallazgos sin resolver.', 'interaction.help.command.dismiss': 'descarta un hallazgo.', @@ -46505,6 +46509,191 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 2230: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPANISH_SDD_DIFF_DEFINITION = exports.ENGLISH_SDD_DIFF_DEFINITION = exports.SDD_DIFF_MESSAGE_IDS = void 0; +exports.resolveSddDiffCatalog = resolveSddDiffCatalog; +exports.isSddDiffResolvedLocale = isSddDiffResolvedLocale; +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +exports.SDD_DIFF_MESSAGE_IDS = Object.freeze([ + 'sddDiff.heading', 'sddDiff.status', 'sddDiff.action', 'sddDiff.not-applicable', 'sddDiff.no-publication', + 'sddDiff.initial-only', 'sddDiff.stale', 'sddDiff.pending-revision', 'sddDiff.invalid', 'sddDiff.unavailable', + 'sddDiff.ambiguous', 'sddDiff.bounded', 'sddDiff.compared', 'sddDiff.none', 'sddDiff.publish', 'sddDiff.refresh', + 'sddDiff.answer', 'sddDiff.retry', 'sddDiff.choose', 'sddDiff.reduce', 'sddDiff.first', 'sddDiff.current', + 'sddDiff.historical', 'sddDiff.card', 'sddDiff.commit', 'sddDiff.compare', 'sddDiff.lines', 'sddDiff.sections', 'sddDiff.omitted', 'sddDiff.structural', + 'sddDiff.charactersLimit', 'sddDiff.bytesLimit', 'sddDiff.linesLimit', 'sddDiff.workLimit', +]); +const EN = Object.freeze({ + 'sddDiff.heading': 'SDD structural comparison', 'sddDiff.status': 'Current status', 'sddDiff.action': 'Next action', + 'sddDiff.not-applicable': 'An SDD comparison is not applicable to this target.', + 'sddDiff.no-publication': 'No verified SDD publication is available yet.', + 'sddDiff.initial-only': 'Only the first publication is verified; there is no revision pair to compare.', + 'sddDiff.stale': 'Current issue, SDD or branch evidence changed or disagrees. Historical evidence does not certify a current comparison.', + 'sddDiff.pending-revision': 'The requested SDD revision is pending. Current change counts are unavailable.', + 'sddDiff.invalid': 'The owning SDD evidence could not be verified.', + 'sddDiff.unavailable': 'The comparison could not be verified. No current change counts are available.', + 'sddDiff.ambiguous': 'More than one issue is linked to this PR.', + 'sddDiff.bounded': 'The verified documents exceed a fixed comparison limit; exact counts are unavailable.', + 'sddDiff.compared': 'Compared verified published revisions.', 'sddDiff.none': 'No action is required.', + 'sddDiff.publish': 'Publish the SDD through the existing issue workflow, then retry this command.', + 'sddDiff.refresh': 'Retry this command with current evidence; if the issue changed, resume its existing SDD clarification workflow.', + 'sddDiff.answer': 'Answer the existing SDD questions or resume publication, then retry this command.', + 'sddDiff.retry': 'Inspect the existing SDD evidence and read access, then retry this command.', + 'sddDiff.choose': 'Run this command in the intended issue conversation.', + 'sddDiff.reduce': 'Keep the SDD within the documented fixed limits, then retry this command.', + 'sddDiff.first': 'First publication', 'sddDiff.current': 'Current publication', 'sddDiff.historical': 'Historical first publication', + 'sddDiff.card': 'SDD clarification card', 'sddDiff.commit': 'Publication commit', 'sddDiff.compare': 'Compare publication commits', + 'sddDiff.lines': 'Added lines: {added}; removed lines: {removed}.', 'sddDiff.sections': 'Changed section titles', + 'sddDiff.omitted': 'Additional changed titles omitted: {count}.', + 'sddDiff.charactersLimit': 'Limit: 70,000 characters per document.', 'sddDiff.bytesLimit': 'Limit: 280,000 UTF-8 bytes per document.', + 'sddDiff.linesLimit': 'Limit: 2,000 content lines per document.', 'sddDiff.workLimit': 'Limit: 2,000,000 line-comparison steps.', + 'sddDiff.structural': 'This compares structure and content lines; it does not assess semantic requirements.', +}); +const ES = Object.freeze({ + 'sddDiff.heading': 'Comparación estructural del SDD', 'sddDiff.status': 'Estado actual', 'sddDiff.action': 'Siguiente acción', + 'sddDiff.not-applicable': 'La comparación de SDD no corresponde a este destino.', + 'sddDiff.no-publication': 'Todavía no hay una publicación de SDD verificada.', + 'sddDiff.initial-only': 'Sólo está verificada la primera publicación; no hay dos revisiones que comparar.', + 'sddDiff.stale': 'La evidencia de la issue, del SDD o de la rama cambió o no coincide. La evidencia histórica no certifica una comparación actual.', + 'sddDiff.pending-revision': 'La revisión solicitada del SDD está pendiente. No hay recuentos de cambios actuales.', + 'sddDiff.invalid': 'No se pudo verificar la evidencia del SDD propietario.', + 'sddDiff.unavailable': 'No se pudo verificar la comparación. No hay recuentos de cambios actuales.', + 'sddDiff.ambiguous': 'Esta PR enlaza más de una issue.', + 'sddDiff.bounded': 'Los documentos verificados superan un límite fijo de comparación; no hay recuentos exactos.', + 'sddDiff.compared': 'Se compararon las revisiones publicadas y verificadas.', 'sddDiff.none': 'No se requiere ninguna acción.', + 'sddDiff.publish': 'Publica el SDD mediante el flujo existente de la issue y vuelve a ejecutar el comando.', + 'sddDiff.refresh': 'Vuelve a ejecutar el comando con evidencia actual; si cambió la issue, retoma su flujo existente de aclaraciones del SDD.', + 'sddDiff.answer': 'Responde las preguntas existentes del SDD o retoma su publicación y vuelve a ejecutar el comando.', + 'sddDiff.retry': 'Revisa la evidencia existente del SDD y los permisos de lectura y vuelve a ejecutar el comando.', + 'sddDiff.choose': 'Ejecuta el comando en la conversación de la issue deseada.', + 'sddDiff.reduce': 'Mantén el SDD dentro de los límites fijos documentados y vuelve a ejecutar el comando.', + 'sddDiff.first': 'Primera publicación', 'sddDiff.current': 'Publicación actual', 'sddDiff.historical': 'Primera publicación histórica', + 'sddDiff.card': 'Tarjeta de aclaraciones del SDD', 'sddDiff.commit': 'Commit de publicación', 'sddDiff.compare': 'Comparar commits de publicación', + 'sddDiff.lines': 'Líneas añadidas: {added}; líneas eliminadas: {removed}.', 'sddDiff.sections': 'Títulos de secciones modificadas', + 'sddDiff.omitted': 'Títulos modificados adicionales omitidos: {count}.', + 'sddDiff.charactersLimit': 'Límite: 70.000 caracteres por documento.', 'sddDiff.bytesLimit': 'Límite: 280.000 bytes UTF-8 por documento.', + 'sddDiff.linesLimit': 'Límite: 2.000 líneas de contenido por documento.', 'sddDiff.workLimit': 'Límite: 2.000.000 pasos de comparación de líneas.', + 'sddDiff.structural': 'La comparación incluye estructura y líneas de contenido; no evalúa requisitos semánticos.', +}); +exports.ENGLISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'en-US', compatibleBaseLanguage: 'en', messages: EN, +}); +exports.SPANISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', messages: ES, +}); +function resolveSddDiffCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_SDD_DIFF_DEFINITION, [exports.ENGLISH_SDD_DIFF_DEFINITION, exports.SPANISH_SDD_DIFF_DEFINITION]); +} +/** Locale admission belongs to the complete bundled catalog boundary. */ +function isSddDiffResolvedLocale(value) { + return value === 'en-US' || value === 'es-ES'; +} + + +/***/ }), + +/***/ 63302: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.renderSddDiff = renderSddDiff; +const pre_branch_sdd_1 = __nccwpck_require__(34730); +const STATES = ['not-applicable', 'no-publication', 'initial-only', 'stale', 'pending-revision', 'invalid', 'unavailable', 'ambiguous', 'bounded', 'compared']; +const ACTIONS = Object.freeze({ + 'not-applicable': 'sddDiff.none', 'no-publication': 'sddDiff.publish', 'initial-only': 'sddDiff.none', + stale: 'sddDiff.refresh', 'pending-revision': 'sddDiff.answer', invalid: 'sddDiff.retry', unavailable: 'sddDiff.retry', + ambiguous: 'sddDiff.choose', bounded: 'sddDiff.reduce', compared: 'sddDiff.none', +}); +/** Immutable links are constructed only from a bound server/repository and verified publication facts. */ +function renderSddDiff(snapshot, scope, catalog) { + const render = catalog.message; + const heading = `## ${render('sddDiff.heading')}`; + const fallback = `${heading}\n\n${render('sddDiff.unavailable')}\n\n**${render('sddDiff.action')}:** ${render('sddDiff.retry')}`; + const root = repositoryRoot(scope); + if (!root || !STATES.includes(snapshot.state)) + return fallback; + const lines = [heading, '', `**${render('sddDiff.status')}:** ${render(`sddDiff.${snapshot.state}`)}`, '', + `**${render('sddDiff.action')}:** ${render(ACTIONS[snapshot.state])}`]; + if ('publication' in snapshot && snapshot.publication) { + const publication = snapshot.publication; + if (!(0, pre_branch_sdd_1.isSafeSddPath)(publication.path) || !sha(publication.firstSha)) + return fallback; + const historical = snapshot.state === 'stale' || snapshot.state === 'pending-revision'; + if (historical) + lines.push('', link('sddDiff.historical', publication.firstSha, publication.path)); + else { + lines.push('', link('sddDiff.first', publication.firstSha, publication.path)); + if (snapshot.state !== 'initial-only') { + if (!sha(publication.currentSha) || publication.currentSha === publication.firstSha) + return fallback; + lines.push(link('sddDiff.current', publication.currentSha, publication.path)); + } + } + } + else if (snapshot.state === 'initial-only' || snapshot.state === 'compared' || snapshot.state === 'bounded') + return fallback; + if (snapshot.card && (snapshot.state === 'stale' || snapshot.state === 'pending-revision')) { + if (!positive(snapshot.card.issueNumber) || !positive(snapshot.card.commentId)) + return fallback; + lines.push('', `[${render('sddDiff.card')}](${root}/issues/${snapshot.card.issueNumber}#issuecomment-${snapshot.card.commentId})`); + } + if (snapshot.state === 'compared' || snapshot.state === 'bounded') { + const publication = snapshot.publication; + lines.push('', `[${render('sddDiff.compare')}](${root}/compare/${publication.firstSha}...${publication.currentSha})`); + } + if (snapshot.state === 'bounded') { + if (!['characters', 'bytes', 'lines', 'work'].includes(snapshot.reason)) + return fallback; + lines.push('', render(`sddDiff.${snapshot.reason}Limit`)); + } + if (snapshot.state === 'compared') { + const changes = snapshot.changes; + if (changes.state !== 'compared' || !count(changes.addedLines, 2000) || !count(changes.removedLines, 2000) + || !count(changes.omittedSections, 4000) || !Array.isArray(changes.sections) || changes.sections.length > 8 + || changes.sections.some(title => typeof title !== 'string' || title.length > 120)) + return fallback; + lines.push('', render('sddDiff.lines', { added: changes.addedLines, removed: changes.removedLines }), '', render('sddDiff.structural')); + if (changes.sections.length) + lines.push('', `**${render('sddDiff.sections')}:**`, ...changes.sections.map(title => `- ${escapeTitle(title)}`)); + if (changes.omittedSections) + lines.push('', render('sddDiff.omitted', { count: changes.omittedSections })); + } + const body = lines.join('\n'); + return body.length <= 5600 ? body : fallback; + function link(label, commit, path) { + return `[${render(label)} · ${commit.slice(0, 9)}](${root}/blob/${commit}/${path}) · [${render('sddDiff.commit')}](${root}/commit/${commit})`; + } +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function count(value, max) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= max; } +function sha(value) { return typeof value === 'string' && /^[a-f0-9]{40}$/u.test(value); } +function repositoryRoot(scope) { + if (scope.serverOrigin.length > 200 || ![scope.owner, scope.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return undefined; + try { + const url = new URL(scope.serverOrigin); + return url.protocol === 'https:' && !url.username && !url.password && url.pathname === '/' && !url.search && !url.hash + ? `${url.origin}/${scope.owner}/${scope.repository}` : undefined; + } + catch { + return undefined; + } +} +function escapeTitle(value) { + return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').replace(/@/gu, '@\u200b') + .replace(/([\\`*_{}[\]()<>#|~!])/gu, '\\$1'); +} + + /***/ }), /***/ 99167: @@ -46712,6 +46901,7 @@ exports.hasOwnedPrimaryIssuePublication = hasOwnedPrimaryIssuePublication; exports.selectSemanticReplyIntents = selectSemanticReplyIntents; exports.renderSemanticReply = renderSemanticReply; exports.renderSemanticStatus = renderSemanticStatus; +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const github_publication_1 = __nccwpck_require__(35793); const github_user_policy_1 = __nccwpck_require__(84403); const result_1 = __nccwpck_require__(73817); @@ -46799,6 +46989,11 @@ function selectSemanticReplyIntents(context) { return [replyIntent(context, correlationId, 'direct-answer', directAnswer)]; const publication = (0, result_1.getResultPayload)(payload.publication); const kind = publication?.kind; + if (kind === 'sdd-diff' && result.id.endsWith('.SddDiff') + && typeof publication?.markdown === 'string' && publication.markdown.trim() && publication.markdown.length <= 5600 + && (0, sdd_diff_message_catalog_1.isSddDiffResolvedLocale)(publication.locale)) { + return [replyIntent({ ...context, locale: publication.locale }, correlationId, 'copilot-sdd-diff', Object.freeze({ kind: 'sdd-diff', markdown: publication.markdown }))]; + } if (kind === 'help' || kind === 'welcome') { const botLogin = typeof publication?.botLogin === 'string' && publication.botLogin.trim() ? publication.botLogin.trim() @@ -46848,6 +47043,8 @@ function renderSemanticReply(intent, catalog = (0, publication_message_catalog_1 return `${marker}\n\n${body}`; } function renderReplyBody(intent, catalog) { + if (intent.projection.kind === 'sdd-diff') + return (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.markdown); if (intent.projection.kind === 'implementation-result') { return safeCatalogSentence(catalog.render(intent.projection.outcome === 'changes-pushed' ? 'interaction.implementation.pushed' : 'interaction.implementation.noChanges', { branch: inlineRef(intent.projection.branch) })); @@ -52448,6 +52645,8 @@ async function runDoUserRequestAction(param, options, intentPayload) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; +const sdd_diff_presentation_policy_1 = __nccwpck_require__(63302); +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const result_1 = __nccwpck_require__(73817); const read_copilot_status_workflow_1 = __nccwpck_require__(57206); const copilot_interaction_policy_1 = __nccwpck_require__(90108); @@ -52463,6 +52662,15 @@ const LEARNED_BUGBOT_RULE_PATH = '.copilot/BUGBOT.learned.md'; async function runExplicitCommentCommand(param, options, command, actorAuthorizationPort) { if (command.name === 'help') return runHelpCommand(param, options); + if (command.name === 'sdd-diff') { + const snapshot = param.sddDiff && options.sddDiffQueryPort + ? await options.sddDiffQueryPort.read(param.sddDiff) : { state: 'unavailable' }; + const catalog = (0, sdd_diff_message_catalog_1.resolveSddDiffCatalog)(param.language.locale); + const markdown = (0, sdd_diff_presentation_policy_1.renderSddDiff)(snapshot, { owner: param.status.owner, repository: param.status.repository, + serverOrigin: options.sddDiffQueryPort?.serverOrigin ?? 'https://github.com' }, catalog); + return [new result_1.Result({ id: `${options.taskId}.SddDiff`, success: true, executed: true, stepFormat: 'markdown', + steps: [markdown], payload: Object.freeze({ publication: Object.freeze({ kind: 'sdd-diff', markdown, locale: catalog.locale }) }) })]; + } if (command.name === 'status') return [await (0, read_copilot_status_workflow_1.readCopilotStatus)(param, options.taskId, options.statusReadPorts, options.sddQueryPort)]; if (command.name === 'dismiss') @@ -52706,6 +52914,7 @@ function projectCommentAutomationContext(source, language, userComment) { }), think: (0, think_workflow_1.projectThinkContext)(source), status: (0, status_command_policy_1.buildCopilotStatusSnapshot)(source), + ...projectSddDiffRequest(source), ...(source.issue?.number > 0 && !(source.pullRequest?.number > 0) ? { sdd: Object.freeze({ issueNumber: source.issue.number, admittedKind: issue_workflow_profile_1.ISSUE_WORKFLOW_KINDS.find(kind => kind === source.issueWorkflowKind), @@ -52763,6 +52972,29 @@ function withCommentLanguageAdaptation(context, adaptation) { }), }); } +/** Copies trusted workflow/profile facts; PR issue identity/author come only from the native link query. */ +function projectSddDiffRequest(source) { + const status = (0, status_command_policy_1.buildCopilotStatusSnapshot)(source); + const repositoryId = source.inputs?.repository?.id; + const number = status.target === 'pull-request' ? status.pullRequestNumber : status.issueNumber; + if (!Number.isSafeInteger(repositoryId) || !repositoryId || repositoryId < 1 || !number || number < 1 + || !source.issueWorkflowProfile || typeof source.issue.issueManagedBranches !== 'boolean' + || typeof source.preBranchSdd !== 'boolean') + return {}; + const list = (...values) => Object.freeze(values.filter((value) => typeof value === 'string')); + const labels = source.labels; + return { sddDiff: Object.freeze({ + target: Object.freeze({ kind: status.target, number }), repositoryId, + ...(status.target === 'issue' && source.inputs?.issue?.number === number + ? { issueAuthor: source.inputs.issue.user?.login } : {}), + profile: Object.freeze({ schemaVersion: source.issueWorkflowProfile.schemaVersion, enabled: Object.freeze([...source.issueWorkflowProfile.enabled]) }), + labelConfiguration: Object.freeze({ feature: list(labels.feature, labels.enhancement), bugfix: list(labels.bugfix, labels.bug), + documentation: list(labels.documentation, labels.docs), chore: list(labels.chore, labels.maintenance), + help: list(labels.help, labels.question), hotfix: list(labels.hotfix), release: list(labels.release) }), + profileDigest: source.issueWorkflowProfileDigest, trustedBotLogin: source.tokenUser?.trim() ?? '', + issueManagedBranches: source.issue.issueManagedBranches, preBranchSdd: source.preBranchSdd, baseBranch: source.branches?.development, + }) }; +} /***/ }), @@ -52886,7 +53118,7 @@ async function runCommentAutomation(initialParam, options, actorAuthorizationPor return [(0, comment_automation_command_workflow_1.invalidCommentCommandResult)(options.taskId, command.reason)]; } const isPublicMetadataCommand = command.kind === 'command' - && (command.command.name === 'help' || command.command.name === 'status'); + && (command.command.name === 'help' || command.command.name === 'status' || command.command.name === 'sdd-diff'); if (!isPublicMetadataCommand && param.membersOnly && !await actorAuthorizationPort.isActorAllowedToUseMemberOnlyAutomation(param.actor)) { (0, logging_ports_1.logInfo)('Skipping agent automation because ai-members-only is enabled and the actor is not authorized.'); @@ -53558,7 +53790,7 @@ const comment_automation_context_1 = __nccwpck_require__(37055); const pull_request_workflow_context_1 = __nccwpck_require__(73447); const pre_branch_sdd_1 = __nccwpck_require__(34730); class IssueCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, sddQueryPort) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, sddQueryPort, sddDiffQueryPort) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -53575,6 +53807,7 @@ class IssueCommentUseCase { this.rememberBranchPorts = rememberBranchPorts; this.statusReadPorts = statusReadPorts; this.sddQueryPort = sddQueryPort; + this.sddDiffQueryPort = sddDiffQueryPort; this.taskId = "IssueCommentUseCase"; } async invoke(param) { @@ -53613,6 +53846,7 @@ class IssueCommentUseCase { rememberBranchPorts: this.rememberBranchPorts, statusReadPorts: this.statusReadPorts, sddQueryPort: this.sddQueryPort, + sddDiffQueryPort: this.sddDiffQueryPort, syncBranchUseCase: this.syncBranchUseCase ? { invoke: (options) => this.syncBranchUseCase.invoke({ context: context.branchSync, options }) } : undefined, @@ -54399,7 +54633,7 @@ const check_pull_request_comment_language_use_case_1 = __nccwpck_require__(21729 const comment_automation_context_1 = __nccwpck_require__(37055); const pull_request_workflow_context_1 = __nccwpck_require__(73447); class PullRequestReviewCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts, sddDiffQueryPort) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -54414,6 +54648,7 @@ class PullRequestReviewCommentUseCase { this.syncBranchUseCase = syncBranchUseCase; this.rememberBranchPorts = rememberBranchPorts; this.statusReadPorts = statusReadPorts; + this.sddDiffQueryPort = sddDiffQueryPort; this.taskId = "PullRequestReviewCommentUseCase"; } async invoke(param) { @@ -54439,6 +54674,7 @@ class PullRequestReviewCommentUseCase { rememberBugbotRuleUseCase: this.rememberBugbotRuleUseCase, rememberBranchPorts: this.rememberBranchPorts, statusReadPorts: this.statusReadPorts, + sddDiffQueryPort: this.sddDiffQueryPort, syncBranchUseCase: this.syncBranchUseCase ? { invoke: (options) => this.syncBranchUseCase.invoke({ context: context.branchSync, options }) } : undefined, @@ -55377,6 +55613,151 @@ function canonicalIssueUrl(context) { } +/***/ }), + +/***/ 48684: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffQueryUseCase = void 0; +const issue_workflow_profile_1 = __nccwpck_require__(26744); +const pre_branch_sdd_diff_1 = __nccwpck_require__(28558); +/** Structural comparison over shared verified evidence, with no writer/model capability. */ +class SddDiffQueryUseCase { + constructor(ports) { + this.ports = Object.freeze({ + evidence: Object.freeze({ read: ports.evidence.read.bind(ports.evidence) }), + links: Object.freeze({ readClosingIssues: ports.links.readClosingIssues.bind(ports.links) }), + labels: Object.freeze({ getLabels: ports.labels.getLabels.bind(ports.labels) }), + descriptions: Object.freeze({ getDescription: ports.descriptions.getDescription.bind(ports.descriptions) }), + documents: Object.freeze({ readSdd: ports.documents.readSdd.bind(ports.documents) }), + }); + } + async read(request) { + try { + if (!positive(request.target.number) || !positive(request.repositoryId) + || !['issue', 'pull-request'].includes(request.target.kind)) + return { state: 'unavailable' }; + let issueNumber = request.target.number; + let issueAuthor = request.issueAuthor; + let links; + if (request.target.kind === 'pull-request') { + const association = await this.ports.links.readClosingIssues(request.target.number); + if (association.state !== 'verified' || association.pullNumber !== request.target.number) + return { state: 'unavailable' }; + if (association.issueNumbers.length === 0) + return { state: 'not-applicable' }; + if (association.issueNumbers.length !== 1) + return { state: 'ambiguous' }; + links = association; + issueNumber = association.issueNumbers[0]; + issueAuthor = association.issueAuthors[issueNumber]; + } + if (!issueAuthor?.trim()) + return { state: 'unavailable' }; + const context = await this.context(request, issueNumber, issueAuthor); + if (!context) + return { state: 'not-applicable' }; + const original = await this.ports.evidence.read(context); + const projection = projectEvidence(original, issueNumber); + if (original.state !== 'published' || !original.verified?.currentSha) + return projection; + if (!/^[a-f0-9]{64}$/u.test(original.authorityDigest ?? '')) + return { state: 'unavailable' }; + if (links && (links.headSha !== original.verified.branchHeadSha || links.headBranch !== original.verified.branchName)) { + return historicalStale(original, issueNumber); + } + const publication = publicationOf(original); + let result = { state: 'initial-only', publication }; + if (original.verified.currentSha !== original.verified.firstSha) { + const [before, after] = await Promise.all([ + this.ports.documents.readSdd(original.verified.firstSha, original.verified.path), + this.ports.documents.readSdd(original.verified.currentSha, original.verified.path), + ]); + if (before === undefined || after === undefined) + return { state: 'unavailable' }; + const changes = (0, pre_branch_sdd_diff_1.compareSddStructure)(before, after); + result = changes.state === 'bounded' + ? { state: 'bounded', publication, reason: changes.reason } + : { state: 'compared', publication, changes }; + } + // Re-admit the exact issue, then re-read the shared fenced authority. + const freshContext = await this.context(request, issueNumber, issueAuthor); + if (!freshContext || freshContext.admittedKind !== context.admittedKind) + return { state: 'stale' }; + const fresh = await this.ports.evidence.read(freshContext); + if (!original.authorityDigest || fresh.authorityDigest !== original.authorityDigest + || proofOf(fresh) !== proofOf(original)) { + return fresh.state === 'published' ? historicalStale(fresh, issueNumber) : projectEvidence(fresh, issueNumber); + } + if (links) { + const freshLinks = await this.ports.links.readClosingIssues(request.target.number); + if (freshLinks.state !== 'verified') + return { state: 'unavailable' }; + if (associationProof(freshLinks) !== associationProof(links)) + return historicalStale(fresh, issueNumber); + } + return Object.freeze(result); + } + catch { + return { state: 'unavailable' }; + } + } + async context(request, issueNumber, issueAuthor) { + const [labels, body] = await Promise.all([ + this.ports.labels.getLabels(issueNumber), this.ports.descriptions.getDescription(issueNumber), + ]); + if (body === undefined) + throw new Error('Issue admission is unavailable.'); + const admission = (0, issue_workflow_profile_1.classifyIssueWorkflow)(labels, request.profile, request.labelConfiguration, body); + return admission.status === 'eligible' ? Object.freeze({ issueNumber, admittedKind: admission.kind, issueAuthor, + profileDigest: request.profileDigest, trustedBotLogin: request.trustedBotLogin, + issueManagedBranches: request.issueManagedBranches, preBranchSdd: request.preBranchSdd, baseBranch: request.baseBranch }) : undefined; + } +} +exports.SddDiffQueryUseCase = SddDiffQueryUseCase; +function projectEvidence(evidence, issueNumber) { + if (evidence.state === 'not-required') + return { state: 'not-applicable' }; + if (evidence.state === 'absent') + return { state: 'no-publication' }; + if (evidence.state === 'invalid' || evidence.state === 'unavailable') + return { state: evidence.state }; + const publication = publicationOf(evidence); + if (evidence.state === 'stale') + return historicalStale(evidence, issueNumber); + if (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') { + return publication ? { state: 'pending-revision', publication, ...cardOf(evidence, issueNumber) } : { state: 'no-publication' }; + } + if (!publication?.currentSha) + return { state: 'invalid' }; + return publication.currentSha === publication.firstSha ? { state: 'initial-only', publication } : { state: 'unavailable' }; +} +function publicationOf(evidence) { + return evidence.verified ? Object.freeze({ path: evidence.verified.path, firstSha: evidence.verified.firstSha, + ...(evidence.verified.currentSha ? { currentSha: evidence.verified.currentSha } : {}) }) : undefined; +} +function historicalStale(evidence, issueNumber) { + const publication = publicationOf(evidence); + return { state: 'stale', ...(publication ? { publication } : {}), ...cardOf(evidence, issueNumber) }; +} +function cardOf(evidence, issueNumber) { + return positive(evidence.cardId) ? { card: Object.freeze({ issueNumber, commentId: evidence.cardId }) } : {}; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function proofOf(evidence) { + const verified = evidence.verified; + return JSON.stringify([evidence.state, evidence.authorityDigest, evidence.cardId, evidence.path, evidence.pendingQuestionIds, + verified?.path, verified?.capabilityId, verified?.branchName, verified?.branchHeadSha, verified?.firstSha, verified?.currentSha]); +} +function associationProof(links) { + return JSON.stringify([links.pullNumber, links.headSha, links.headBranch, + links.issueNumbers.map(number => [number, links.issueAuthors[number].toLowerCase()])]); +} + + /***/ }), /***/ 39893: @@ -55386,6 +55767,7 @@ function canonicalIssueUrl(context) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SddEvidenceQueryUseCase = void 0; +const node_crypto_1 = __nccwpck_require__(6005); const issue_start_policy_1 = __nccwpck_require__(90332); const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_evidence_read_ports_1 = __nccwpck_require__(94752); @@ -55417,7 +55799,10 @@ class SddEvidenceQueryUseCase { if (!['invalid', 'unavailable'].includes(result.state) && !await this.fence(context, facts, selection.body, cardId, record, result.verified)) return { state: 'stale' }; - return Object.freeze(result); + return Object.freeze({ ...result, ...(!['invalid', 'unavailable'].includes(result.state) ? { + authorityDigest: (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([cardId, selection.body, digest, canonicalLabels(facts.labels), + record.phase === 'awaiting-answer' ? facts.comments : null])).digest('hex'), + } : {}) }); } catch { return { state: 'unavailable' }; @@ -61324,7 +61709,9 @@ async function runPublishResume(param, taskId, comments, catalogResolver, source (0, logging_ports_1.logInfo)('Conversation publication omitted: the configured bot identity is unavailable.'); return undefined; } - const catalog = await (0, publication_message_catalog_1.resolvePublicationCatalog)(param.locale, param.languageConfiguration, catalogResolver); + const catalog = statuses.length === 0 && replies.every(intent => intent.projection.kind === 'sdd-diff') + ? (0, publication_message_catalog_1.resolveStaticPublicationCatalog)(replies[0].locale).catalog + : await (0, publication_message_catalog_1.resolvePublicationCatalog)(param.locale, param.languageConfiguration, catalogResolver); let staleSourceEvidence; const compactedCommentIds = []; for (const intent of replies) { @@ -61443,7 +61830,7 @@ const publication_identity_policy_1 = __nccwpck_require__(45403); const publication_message_catalog_1 = __nccwpck_require__(34223); const semantic_result_publication_policy_1 = __nccwpck_require__(81985); const duplicate_comment_cleanup_workflow_1 = __nccwpck_require__(79544); -/** Publishes an explicit reply at most once for a trusted request correlation. */ +/** Publishes trusted correlated replies; only deterministic SDD diff refreshes its existing reply. */ async function reconcileReply(context, comments) { if (!context.botLogin.trim()) return unchangedOutcome(); @@ -61461,6 +61848,13 @@ async function reconcileReply(context, comments) { }); } const [canonical, ...duplicates] = owned.sort((left, right) => left.id - right.id); + if (context.intent.projection.kind === 'sdd-diff' && context.intent.messageKey === 'copilot-sdd-diff') { + const rendered = (0, semantic_result_publication_policy_1.renderSemanticReply)(context.intent, context.catalog); + if (canonical.body !== rendered) { + await comments.updateComment(target.number, canonical.id, rendered); + effect = 'updated'; + } + } let duplicatesRemoved = 0; let duplicatesCompacted = 0; const compactedCommentIds = []; @@ -74366,6 +74760,85 @@ class RepositorySecretsCommandRepository { exports.RepositorySecretsCommandRepository = RepositorySecretsCommandRepository; +/***/ }), + +/***/ 85885: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffLinkRepository = void 0; +const QUERY = `query SddDiffClosingIssues($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + databaseId nameWithOwner + pullRequest(number: $number) { + number headRefOid headRefName headRepository { databaseId nameWithOwner } repository { databaseId nameWithOwner } + closingIssuesReferences(first: 100) { + totalCount pageInfo { hasNextPage } + nodes { number author { login } repository { databaseId nameWithOwner } } + } + } + } +}`; +/** One bounded native read; absent, incomplete or conflicting identities never become guessed links. */ +class SddDiffLinkRepository { + constructor(client, binding) { + this.client = client; + this.binding = Object.freeze({ ...binding }); + } + async readClosingIssues(pullNumber) { + const unavailable = Object.freeze({ state: 'unavailable' }); + const binding = this.binding; + if (!positive(pullNumber) || !positive(binding.repositoryId) + || ![binding.owner, binding.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return unavailable; + try { + const response = object(await this.client.getClient(binding.token).graphql(QUERY, { + owner: binding.owner, repo: binding.repository, number: pullNumber, + })); + const repository = object(response?.repository); + const pull = object(repository?.pullRequest); + const links = object(pull?.closingIssuesReferences); + const page = object(links?.pageInfo); + if (!sameRepository(repository, binding) || !sameRepository(object(pull?.repository), binding) + || !sameRepository(object(pull?.headRepository), binding) + || typeof pull?.headRefName !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,249}$/u.test(pull.headRefName) + || pull?.number !== pullNumber || typeof pull.headRefOid !== 'string' || !/^[a-f0-9]{40}$/u.test(pull.headRefOid) + || page?.hasNextPage !== false || !Array.isArray(links?.nodes) + || !Number.isSafeInteger(links.totalCount) || links.totalCount !== links.nodes.length || links.nodes.length > 100) + return unavailable; + const numbers = []; + const authors = {}; + for (const value of links.nodes) { + const issue = object(value); + const author = object(issue?.author); + if (typeof author?.login !== 'string' || !author.login.trim() || author.login.length > 100 + || !positive(issue?.number) || !sameRepository(object(issue?.repository), binding) + || numbers.includes(issue.number)) + return unavailable; + numbers.push(issue.number); + authors[issue.number] = author.login; + } + return Object.freeze({ state: 'verified', pullNumber, headSha: pull.headRefOid, headBranch: pull.headRefName, + issueNumbers: Object.freeze(numbers.sort((left, right) => left - right)), issueAuthors: Object.freeze(authors) }); + } + catch { + return unavailable; + } + } +} +exports.SddDiffLinkRepository = SddDiffLinkRepository; +function object(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function sameRepository(value, binding) { + return value?.databaseId === binding.repositoryId && typeof value.nameWithOwner === 'string' + && value.nameWithOwner.toLowerCase() === `${binding.owner}/${binding.repository}`.toLowerCase(); +} + + /***/ }), /***/ 40941: @@ -75337,6 +75810,7 @@ exports.COPILOT_COMMAND_NAMES = [ 'estimate', 'test-plan', 'status', + 'sdd-diff', 'description', 'explain', 'diagnose', @@ -75395,6 +75869,8 @@ function parseCopilotCommand(raw) { && name !== 'remember') { return { kind: 'invalid', reason: `Copilot commands accept at most ${MAX_ARGUMENTS} arguments.` }; } + if (name === 'sdd-diff' && argumentsList.length > 0) + return { kind: 'invalid', reason: '/copilot sdd-diff does not accept arguments.' }; if ((name === 'fix' || name === 'dismiss' || name === 'implement' || name === 'remember') && argumentsList.length === 0) { return { kind: 'invalid', reason: `/${name} requires at least one argument.` }; } @@ -77386,6 +77862,153 @@ function isRecord(value) { } +/***/ }), + +/***/ 28558: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SDD_DIFF_LIMITS = void 0; +exports.compareSddStructure = compareSddStructure; +exports.SDD_DIFF_LIMITS = Object.freeze({ + characters: 70000, bytes: 280000, lines: 2000, comparisons: 2000000, + sectionTitles: 8, titleCharacters: 120, +}); +class ComparisonBudgetExceeded extends Error { +} +/** Exact line counts with Hirschberg's linear-memory LCS; every equality consumes bounded work. */ +function compareSddStructure(before, after) { + for (const text of [before, after]) { + if (Buffer.byteLength(text, 'utf8') > exports.SDD_DIFF_LIMITS.bytes) + return Object.freeze({ state: 'bounded', reason: 'bytes' }); + if (text.length > exports.SDD_DIFF_LIMITS.characters) + return Object.freeze({ state: 'bounded', reason: 'characters' }); + } + const left = documentLines(before); + const right = documentLines(after); + if (left.length > exports.SDD_DIFF_LIMITS.lines || right.length > exports.SDD_DIFF_LIMITS.lines) { + return Object.freeze({ state: 'bounded', reason: 'lines' }); + } + const leftMatches = new Uint8Array(left.length); + const rightMatches = new Uint8Array(right.length); + let comparisons = 0; + let common = 0; + const equal = (a, b) => { + if (comparisons === exports.SDD_DIFF_LIMITS.comparisons) + throw new ComparisonBudgetExceeded(); + comparisons++; + return a === b; + }; + const match = (a, b) => { leftMatches[a] = 1; rightMatches[b] = 1; common++; }; + const row = (ls, le, rs, re, reverse) => { + let previous = new Uint16Array(re - rs + 1); + let current = new Uint16Array(re - rs + 1); + for (let i = 0; i < le - ls; i++) { + current[0] = 0; + for (let j = 1; j <= re - rs; j++) { + current[j] = equal(left[reverse ? le - i - 1 : ls + i], right[reverse ? re - j : rs + j - 1]) + ? previous[j - 1] + 1 : Math.max(previous[j], current[j - 1]); + } + [previous, current] = [current, previous]; + } + return previous; + }; + // Row arrays leave scope before recursion; no quadratic trace is retained. + const split = (ls, middle, le, rs, re) => { + const forward = row(ls, middle, rs, re, false); + const backward = row(middle, le, rs, re, true); + let best = -1; + let offset = 0; + for (let j = 0; j <= re - rs; j++) { + const score = forward[j] + backward[re - rs - j]; + if (score > best) { + best = score; + offset = j; + } + } + return rs + offset; + }; + const align = (ls, le, rs, re) => { + if (ls === le || rs === re) + return; + if (le - ls === 1) { + for (let j = rs; j < re; j++) + if (equal(left[ls], right[j])) { + match(ls, j); + break; + } + return; + } + const middle = ls + Math.floor((le - ls) / 2); + const rightMiddle = split(ls, middle, le, rs, re); + align(ls, middle, rs, rightMiddle); + align(middle, le, rightMiddle, re); + }; + try { + let start = 0; + let leftEnd = left.length; + let rightEnd = right.length; + while (start < leftEnd && start < rightEnd && equal(left[start], right[start])) { + match(start, start); + start++; + } + while (start < leftEnd && start < rightEnd && equal(left[leftEnd - 1], right[rightEnd - 1])) { + match(--leftEnd, --rightEnd); + } + align(start, leftEnd, start, rightEnd); + } + catch (error) { + if (!(error instanceof ComparisonBudgetExceeded)) + throw error; + return Object.freeze({ state: 'bounded', reason: 'work' }); + } + const titles = new Set(); + collectChangedSections(right, rightMatches, titles); + collectChangedSections(left, leftMatches, titles); + return Object.freeze({ state: 'compared', addedLines: right.length - common, removedLines: left.length - common, + sections: Object.freeze([...titles].slice(0, exports.SDD_DIFF_LIMITS.sectionTitles).map(boundedTitle)), + omittedSections: Math.max(0, titles.size - exports.SDD_DIFF_LIMITS.sectionTitles) }); +} +/** CRLF and terminal newline markers do not count as added/removed content lines. */ +function documentLines(text) { + if (!text) + return []; + const lines = text.split(/\r?\n/u); + if (text.endsWith('\n')) + lines.pop(); + return lines; +} +function collectChangedSections(lines, matches, titles) { + let fence; + let title; + for (let index = 0; index < lines.length; index++) { + const boundary = lines[index].match(/^ {0,3}(`{3,}|~{3,})(.*)$/u); + if (boundary) { + if (!fence && (boundary[1][0] !== '`' || !boundary[2].includes('`'))) + fence = boundary[1]; + else if (fence && boundary[1][0] === fence[0] && boundary[1].length >= fence.length + && /^[ \t]*$/u.test(boundary[2])) + fence = undefined; + } + else if (!fence) { + const heading = lines[index].match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/u); + if (heading) + title = heading[1]; + } + if (matches[index] === 0 && title) + titles.add(title); + } +} +function boundedTitle(title) { + let value = title.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').slice(0, exports.SDD_DIFF_LIMITS.titleCharacters); + if (/[\uD800-\uDBFF]$/u.test(value)) + value = value.slice(0, -1); + return value; +} + + /***/ }), /***/ 8024: @@ -80392,6 +81015,7 @@ exports.createIssueCommentUseCaseCompositionRoot = createIssueCommentUseCaseComp exports.createPullRequestReviewCommentUseCaseCompositionRoot = createPullRequestReviewCommentUseCaseCompositionRoot; exports.createCommitUseCaseCompositionRoot = createCommitUseCaseCompositionRoot; exports.createMainRunRouteCompositionRoot = createMainRunRouteCompositionRoot; +const sdd_diff_composition_root_1 = __nccwpck_require__(83434); const sdd_evidence_composition_root_1 = __nccwpck_require__(51671); const linked_branch_readiness_repository_1 = __nccwpck_require__(79421); const pull_request_changes_repository_1 = __nccwpck_require__(71564); @@ -80510,7 +81134,7 @@ function createIssueCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context, (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context)); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context, (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context)); } function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); @@ -80526,7 +81150,7 @@ function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), bugbot.scm.context, (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context)); } function createCommitUseCaseCompositionRoot(projectBoardCommandPort, binding) { return new commit_use_case_1.CommitUseCase(new notify_new_commit_on_issue_use_case_1.NotifyNewCommitOnIssueUseCase((0, push_single_action_capability_port_binding_1.bindIssueReopen)((0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), binding)), new check_changes_issue_size_use_case_1.CheckChangesIssueSizeUseCase((0, lifecycle_capability_port_binding_2.bindProjectBoardCommands)(projectBoardCommandPort, binding), (0, lifecycle_capability_port_binding_2.bindIssueLabels)((0, issue_labels_composition_root_1.createIssueLabelRepository)(), binding), (0, push_single_action_capability_port_binding_1.bindPullRequestBranchQuery)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchChangeSize)(new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), binding)), createDetectPotentialProblemsUseCase(binding), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(binding), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)()); @@ -80958,6 +81582,38 @@ function bindSetupRemoteConfiguration(port, binding) { } +/***/ }), + +/***/ 83434: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSddDiffQueryPort = createSddDiffQueryPort; +const sdd_evidence_query_use_case_1 = __nccwpck_require__(39893); +const sdd_diff_query_use_case_1 = __nccwpck_require__(48684); +const sdd_diff_link_repository_1 = __nccwpck_require__(85885); +const pre_branch_sdd_workspace_adapter_1 = __nccwpck_require__(35849); +const sdd_evidence_composition_root_1 = __nccwpck_require__(51671); +const github_project_client_factory_1 = __nccwpck_require__(23691); +/** No agent, provisioning, approval or lifecycle writer is supplied. */ +function createSddDiffQueryPort(binding, comments) { + const bound = Object.freeze({ ...binding }); + const reads = (0, sdd_evidence_composition_root_1.createSddEvidenceReadPorts)(bound, comments); + const evidence = new sdd_evidence_query_use_case_1.SddEvidenceQueryUseCase(reads); + const documents = new pre_branch_sdd_workspace_adapter_1.PreBranchSddWorkspaceAdapter(process.cwd(), bound.token); + const client = (0, github_project_client_factory_1.createGraphqlTransportClient)(); + return Object.freeze({ serverOrigin: process.env.GITHUB_SERVER_URL ?? 'https://github.com', + read: (request) => new sdd_diff_query_use_case_1.SddDiffQueryUseCase({ evidence: Object.freeze({ read: evidence.read.bind(evidence) }), + links: new sdd_diff_link_repository_1.SddDiffLinkRepository(client, { ...bound, repositoryId: request.repositoryId }), + labels: reads.labels, descriptions: reads.descriptions, + documents: Object.freeze({ readSdd: documents.readSdd.bind(documents) }), + }).read(request), + }); +} + + /***/ }), /***/ 51671: diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index 7c3583512..b625f18a4 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -44,6 +44,7 @@ Commands are recognized at the beginning of a comment, case-insensitively. The c | `/copilot review` | Run the read-only review flow. | No | | `/copilot findings` | Reconcile and publish current findings. | No | | `/copilot recheck` | Re-run the review against the current branch. | No | +| `/copilot sdd-diff` | Compare verified first/current SDD publications without an agent. | No | | `/copilot status` | Show the current Copilot status. | No | | `/copilot description` | Refresh the PR description when that action is configured. | No | | `/copilot fix [finding-id ...]` | Fix one or more existing Bugbot findings. | Yes, if authorized | @@ -60,7 +61,7 @@ whitespace-separated, and commands are limited to 2,000 characters and 20 arguments. Missing or malformed required arguments produce a validation reply; they never mean “all” or authorize a broad change. -`help`, `status`, `dismiss`, `remember` and rejected commands do not run a +`help`, `status`, `sdd-diff`, `dismiss`, `remember` and rejected commands do not run a model or change the temporary agent activity label. `sync-branch --dry-run` and `--no-agent` also avoid model activity. Ordinary sync may use the conflict fixer, and `findings` runs a fresh review. These distinctions preserve each @@ -309,3 +310,31 @@ See [Bugbot detection](/bugbot/detection), [Autofix](/bugbot/autofix), and [Do u 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. + + +## Compare SDD publications + +Run `/copilot sdd-diff` without arguments in an eligible issue. The reply links +verified immutable first/current SDD publications and their commits, and compares +added/removed lines and up to eight changed section titles. The commit comparison +link can include other files changed between those commits; the reported counts +cover only the owning SDD. This is a structural comparison, without an assessment +of semantic requirements or document excerpts. + +A first publication without a revision reports `initial-only`. Pending answers +and a changed issue withhold current counts and link the existing clarification +card when verified. Missing, invalid or unavailable evidence is explicit. The +command reads current issue, record and branch facts again before claiming a +current comparison. It does not run an agent, change activity labels or edit files. + +On a PR, exactly one native same-repository closing issue is required, and its +current linked branch must match the PR head. Multiple native links report +ambiguity; the command never guesses from issue numbers in text. Use the intended +issue conversation to resolve ambiguity. A replay of the same source comment +refreshes its existing owned reply; a new comment gets a separate reply. + +Comparison limits are fixed: 70,000 characters, 280,000 UTF-8 bytes and 2,000 +content lines per document, and 2,000,000 line-comparison steps. Over-limit +results retain safe verified links and withhold exact counts. English and Spanish +are bundled; other locales receive a complete English fallback without a model. +Paths, refs, URLs and configurable limits are not accepted as command arguments. diff --git a/docs/issues/pre-branch-sdds.mdx b/docs/issues/pre-branch-sdds.mdx index 283ce1f65..34094d4d5 100644 --- a/docs/issues/pre-branch-sdds.mdx +++ b/docs/issues/pre-branch-sdds.mdx @@ -95,3 +95,20 @@ unavailable; a corrupted card, changed issue or racing branch cannot produce a ready result. A newer invalid owned card never restores an older ready state. Answers from older rounds without matching question provenance remain pending. Help issues remain branchless and do not require an SDD. + + +## Compare the first and current publication + +Use `/copilot sdd-diff` in the issue to inspect a bounded structural comparison +of the first and current verified SDD. A reply shows immutable document and +commit links, exact added/removed line counts and up to eight escaped changed +section titles. It does not quote document contents or decide whether semantic +requirements changed. The commit comparison may show other files; the command's +counts concern only the owning SDD. + +Only a first publication means there is no revision pair. Pending clarifications +or stale issue evidence withhold current counts and return you to the existing +SDD card. Invalid or unavailable authority cannot certify a comparison. Repeating +the same source comment refreshes one reply; it never regenerates the SDD or +changes the branch. See [comment commands](./comment-commands.mdx) for exact PR +link rules, fixed limits and the English/Spanish fallback contract. diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index 218781a88..4c62dda2b 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -532,6 +532,31 @@ } ], "successMessage": "canonical PR status coverage: PASS (marker policy 100%; read workflow 95% lines/statements, 90% branches/functions)" + }, + { + "name": "SDD structural comparison", + "missingEntryLabel": "SDD structural comparison", + "rules": [ + { + "files": [ + "src/domain/pre_branch_sdd_diff.ts", + "src/application/policies/sdd_diff_message_catalog.ts", + "src/application/policies/sdd_diff_presentation_policy.ts" + ], + "mode": "each", + "thresholdProfile": "exhaustive" + }, + { + "files": [ + "src/application/usecases/sdd/sdd_diff_query_use_case.ts", + "src/data/repository/sdd/sdd_diff_link_repository.ts", + "src/infrastructure/composition/sdd_diff_composition_root.ts" + ], + "mode": "each", + "thresholdProfile": "default" + } + ], + "successMessage": "SDD structural comparison coverage: PASS (pure policies 100%; each application/adapter/composition 95% lines/statements, 90% branches/functions)" } ] } diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 4d8e6593e..11dbf3e9f 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) + 3 companion | 83 paths · 2026-10-12 | +| `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; inspect verified SDD status and bounded structural publication comparisons | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 103 paths · 2026-10-12 | | `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) | 39 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) | 79 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 | 104 paths · 2026-10-11 | @@ -149,10 +149,10 @@ debt or convert unknown historic intent into a design decision. - Owner: Copilot maintainers - Last verified: 2026-10-12 - 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-diff.md`](./pre-branch-sdd-diff.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) +- 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) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.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) · [`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) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/domain/agent_response_length_failure.ts`](../src/domain/agent_response_length_failure.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/application/ports/sdd_evidence_read_ports.ts`](../src/application/ports/sdd_evidence_read_ports.ts) · [`src/application/policies/sdd_issue_identity_policy.ts`](../src/application/policies/sdd_issue_identity_policy.ts) · [`src/application/policies/sdd_query_card_policy.ts`](../src/application/policies/sdd_query_card_policy.ts) · [`src/application/policies/sdd_status_policy.ts`](../src/application/policies/sdd_status_policy.ts) · [`src/application/usecases/sdd/sdd_evidence_query_use_case.ts`](../src/application/usecases/sdd/sdd_evidence_query_use_case.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/comment_automation_contracts.ts`](../src/application/usecases/comment_automation_contracts.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/infrastructure/composition/sdd_evidence_composition_root.ts`](../src/infrastructure/composition/sdd_evidence_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.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) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/data/repository/__tests__/agent_json_schema_validator.test.ts`](../src/data/repository/__tests__/agent_json_schema_validator.test.ts) · [`src/application/policies/__tests__/sdd_issue_identity_policy.test.ts`](../src/application/policies/__tests__/sdd_issue_identity_policy.test.ts) · [`src/application/policies/__tests__/sdd_query_card_policy.test.ts`](../src/application/policies/__tests__/sdd_query_card_policy.test.ts) · [`src/application/policies/__tests__/sdd_status_policy.test.ts`](../src/application/policies/__tests__/sdd_status_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts) · [`src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts`](../src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.test.ts) · [`src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts`](../src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_evidence_composition_root.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) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/domain/agent_response_length_failure.ts`](../src/domain/agent_response_length_failure.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/application/ports/sdd_evidence_read_ports.ts`](../src/application/ports/sdd_evidence_read_ports.ts) · [`src/application/policies/sdd_issue_identity_policy.ts`](../src/application/policies/sdd_issue_identity_policy.ts) · [`src/application/policies/sdd_query_card_policy.ts`](../src/application/policies/sdd_query_card_policy.ts) · [`src/application/policies/sdd_status_policy.ts`](../src/application/policies/sdd_status_policy.ts) · [`src/application/usecases/sdd/sdd_evidence_query_use_case.ts`](../src/application/usecases/sdd/sdd_evidence_query_use_case.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/comment_automation_contracts.ts`](../src/application/usecases/comment_automation_contracts.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/infrastructure/composition/sdd_evidence_composition_root.ts`](../src/infrastructure/composition/sdd_evidence_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/domain/pre_branch_sdd_diff.ts`](../src/domain/pre_branch_sdd_diff.ts) · [`src/application/policies/sdd_diff_message_catalog.ts`](../src/application/policies/sdd_diff_message_catalog.ts) · [`src/application/policies/sdd_diff_presentation_policy.ts`](../src/application/policies/sdd_diff_presentation_policy.ts) · [`src/application/ports/sdd_diff_read_ports.ts`](../src/application/ports/sdd_diff_read_ports.ts) · [`src/application/usecases/sdd/sdd_diff_query_use_case.ts`](../src/application/usecases/sdd/sdd_diff_query_use_case.ts) · [`src/data/repository/sdd/sdd_diff_link_repository.ts`](../src/data/repository/sdd/sdd_diff_link_repository.ts) · [`src/infrastructure/composition/sdd_diff_composition_root.ts`](../src/infrastructure/composition/sdd_diff_composition_root.ts) · [`src/application/usecases/pull_request_review_comment_use_case.ts`](../src/application/usecases/pull_request_review_comment_use_case.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) +- 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) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/data/repository/__tests__/agent_json_schema_validator.test.ts`](../src/data/repository/__tests__/agent_json_schema_validator.test.ts) · [`src/application/policies/__tests__/sdd_issue_identity_policy.test.ts`](../src/application/policies/__tests__/sdd_issue_identity_policy.test.ts) · [`src/application/policies/__tests__/sdd_query_card_policy.test.ts`](../src/application/policies/__tests__/sdd_query_card_policy.test.ts) · [`src/application/policies/__tests__/sdd_status_policy.test.ts`](../src/application/policies/__tests__/sdd_status_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts) · [`src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts`](../src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.test.ts) · [`src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts`](../src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts) · [`src/domain/__tests__/pre_branch_sdd_diff.test.ts`](../src/domain/__tests__/pre_branch_sdd_diff.test.ts) · [`src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts`](../src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts) · [`src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts`](../src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts) · [`src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts) · [`src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts`](../src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts) · [`src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts`](../src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.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 e34d338fe..bce3df69a 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1176,7 +1176,7 @@ "id": "issue-start-and-sdd-readiness", "title": "Uniform issue start and pre-branch SDD readiness", "status": "implemented", - "scope": "Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work", + "scope": "Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work; inspect verified SDD status and bounded structural publication comparisons", "owner": "Copilot maintainers", "lastVerified": "2026-10-12", "specs": [ @@ -1188,7 +1188,8 @@ "workflows": [ ".github/workflows/copilot_issue.yml", ".github/workflows/copilot_issue_comment.yml", - ".github/workflows/copilot_commit.yml" + ".github/workflows/copilot_commit.yml", + ".github/workflows/copilot_pull_request_comment.yml" ], "entrypoints": [ "action.yml", @@ -1235,7 +1236,19 @@ "src/application/usecases/issue_comment_use_case.ts", "src/application/policies/semantic_result_publication_policy.ts", "src/infrastructure/composition/sdd_evidence_composition_root.ts", - "src/infrastructure/composition/main_run_route_composition_root.ts" + "src/infrastructure/composition/main_run_route_composition_root.ts", + "src/domain/pre_branch_sdd_diff.ts", + "src/application/policies/sdd_diff_message_catalog.ts", + "src/application/policies/sdd_diff_presentation_policy.ts", + "src/application/ports/sdd_diff_read_ports.ts", + "src/application/usecases/sdd/sdd_diff_query_use_case.ts", + "src/data/repository/sdd/sdd_diff_link_repository.ts", + "src/infrastructure/composition/sdd_diff_composition_root.ts", + "src/application/usecases/pull_request_review_comment_use_case.ts", + "src/application/usecases/steps/common/reply_publication_workflow.ts", + "src/application/usecases/steps/common/publish_resume_workflow.ts", + "scripts/coverage-budgets.json", + "src/architecture/github_publication_mutation_baseline.json" ], "tests": [ "src/domain/__tests__/issue_workflow_runtime_policy.test.ts", @@ -1264,7 +1277,14 @@ "src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts", "src/application/usecases/__tests__/read_copilot_status_workflow.test.ts", "src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts", - "src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts" + "src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts", + "src/domain/__tests__/pre_branch_sdd_diff.test.ts", + "src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts", + "src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts", + "src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts", + "src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts", + "src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts", + "src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts" ], "documentation": [ "docs/issues/branch-management.mdx", diff --git a/specs/pre-branch-sdd-diff.md b/specs/pre-branch-sdd-diff.md index fd18f6389..7520746af 100644 --- a/specs/pre-branch-sdd-diff.md +++ b/specs/pre-branch-sdd-diff.md @@ -1,14 +1,14 @@ # Proposed change appendix: `/copilot sdd-diff` -- Status: Draft -- Date: 2026-10-11 +- Status: Ready +- Date: 2026-10-12 - Catalog capability ID: `issue-start-and-sdd-readiness` -- Last verified: not applicable (prospective companion) +- Last verified: 2026-10-12 (local integration; live acceptance pending) - Owners: Existing `issue-start-and-sdd-readiness` capability owners; individual names are not established by the issue. - Scope: Add a deterministic, read-only command that compares the immutable first publication and current verified publication of the owning SDD for an eligible issue. -- Related issues/PRs: [Issue #464](https://github.com/OWNER/REPO/issues/464); live issue #454 is identified as human UX evidence, but its repository URL is not established here. +- Related issues/PRs: [Issue #464](https://github.com/vypdev/copilot/issues/464); [issue #454](https://github.com/vypdev/copilot/issues/454) provides the first/revised SDD human UX fixture. - Required review gates: product UX, architecture, testing, documentation, security/operations -- Open decisions blocking readiness: None from the supplied answers. Repository-specific command syntax, existing reply marker, current limits for SDD files, exact catalog evidence format, and coverage thresholds must be confirmed against the owning implementation before readiness; do not invent or weaken them. +- Open decisions blocking readiness: None. PR #465 is merged and the Action synchronized this exact managed branch to the shared foundation. Local implementation is verified; fresh PR review and live issue/PR acceptance remain separate completion gates. > This is a prospective companion appendix. Preserve the owning SDD and its existing contract verbatim; append this proposed change. Repository-specific paths and contracts cited below must be reconciled with the existing capability before implementation. @@ -38,10 +38,10 @@ Developers can inspect an SDD gate's first publication and later revisions, but ### 2.3 Evidence -- Code/workflow/documentation references: supplied answers identify `readSddGateRecord` schema v1, `PreBranchSddWorkspacePort.readSdd`, `verifyPublication`, `isAncestor`, `BoundLinkedBranchReadinessPort.getLinkedBranch`, command parsing/help and comment orchestration. Exact source paths are unprovided and remain to be confirmed. +- Code/workflow/documentation references: supplied answers identify `readSddGateRecord` schema v1, `PreBranchSddWorkspacePort.readSdd`, `verifyPublication`, `isAncestor`, `BoundLinkedBranchReadinessPort.getLinkedBranch`, command parsing/help and comment orchestration. The confirmed semantic boundary is `SddEvidenceQueryPort` in `src/application/ports/sdd_evidence_read_ports.ts` from PR #465. The existing immutable content read is `PreBranchSddWorkspacePort.readSdd` in `src/application/ports/pre_branch_sdd_ports.ts`; the command may receive only a read-only projection of it. - Incidents or concrete examples: issue #464 reports that developers cannot inspect bounded changes; issue #454 is named as a real first/revised SDD human UX fixture. - External primary sources: not applicable; the contract is repository-owned. -- Unknowns: current implementation names, catalog evidence schema, exact existing SDD file limits, command catalogue syntax and repository coverage thresholds. +- Remaining evidence: combined application/composition/routes against the Action-integrated shared query, fresh independent PR review, and live command/replay/locale acceptance. Operator GraphQL reads prove the native response shape, not bot-token permissions or a running command. ### 2.4 Retrospective classification (as-built baselines only) @@ -55,7 +55,7 @@ Not applicable: this is a prospective specification, not a reconstruction of imp | PR participant | Query only an explicitly linked eligible issue | PR comment | One correlated PR reply, or not-applicable/ambiguous/unavailable result | | Maintainer | Preserve trust and operability | Catalog, docs, checks | Capability evidence and validation output | -**First publication** is the immutable `commitSha`/`baseSha` pair. **Current publication** is `revisionSha`/`revisionBaseSha` when present. **Structural diff** means added/removed lines and changed section titles; it makes no claim about semantic requirements. **Stale** means the current normalized issue digest differs from the trusted record. **Pending-revision** means the digest matches an awaiting-answer record while the verified first publication remains available; the requested revision is not yet published. +**First publication** is the immutable `commitSha`/`baseSha` pair. **Current publication** is `revisionSha`/`revisionBaseSha` when present. **Structural diff** means added/removed lines and changed section titles; it makes no claim about semantic requirements. **Stale** means the current normalized issue digest differs from the trusted record, or exact issue/card/branch/PR authority changed or disagreed during the final fence. **Pending-revision** means the digest matches an awaiting-answer record while the verified first publication remains available; the requested revision is not yet published. ## 4. Goals @@ -76,7 +76,7 @@ Not applicable: this is a prospective specification, not a reconstruction of imp 1. No current-comparison claim or counts without exact identity, valid trusted record, matching digest, verified publications/ancestry, exact linked branch and successful final freshness fence. 2. No writer capability; no model; no source content in replies. -3. Files and computation are bounded: per document at most 70,000 UTF-16 characters, 280,000 UTF-8 bytes and 2,000 lines; at most 2,000,000 line-comparison steps. Reply at most 6,000 characters. +3. Files and computation are bounded: per document at most 70,000 UTF-16 characters, 280,000 UTF-8 bytes and 2,000 lines; at most 2,000,000 line-comparison steps. The complete reply remains at most 6,000 characters; the renderer reserves wrapper space by limiting its body to 5,600 characters. When both document byte and character ceilings are exceeded, the byte ceiling is reported first; neither limit is raised. 4. At most eight changed section titles, each at most 120 characters, with omitted-title count disclosed. These fixed limits cannot be configured. ## 5. Current versus proposed product journey @@ -106,7 +106,7 @@ Text equivalent: comment -> exact target and evidence verification -> bounded co 1. Parse `/copilot sdd-diff` with no arguments using the existing deterministic parser. Invalid arguments follow existing help behavior and do not invoke a model. 2. On an issue, apply existing admission/profile policy: feature issues and admitted branch-bearing contract-change issues for which the SDD gate applies are eligible. Unsupported, exempt and help targets return not-applicable without writes other than the correlated query reply. -3. On a PR, query the exact event PR's native GitHub `closingIssuesReferences` relationship through a narrow read-only projection. Validate same-repository canonical PR identity and pagination completeness. Verify each candidate issue's admission/profile before considering it. Zero eligible linked issues is not-applicable; multiple eligible links or disagreeing authoritative identity is ambiguous/unavailable. Incomplete/unreadable authority is unavailable. Never infer from equal numbers, body text, branch scans, or a newest candidate. +3. On a PR, query the exact event PR's native GitHub `closingIssuesReferences` relationship through a narrow read-only projection. Validate same-repository canonical PR identity and pagination completeness. Require exactly one native closing issue before reading its admission/profile. Zero native links or an ineligible sole issue is not-applicable; multiple native links are conservatively ambiguous, even if a subset might be eligible. Disagreeing authoritative identity is unavailable. Incomplete/unreadable authority is unavailable. Never infer from equal numbers, body text, branch scans, or a newest candidate. 4. Read the latest configured-bot-owned valid schema-v1 gate record for the exact issue. Confirm current catalog ownership and its allowlisted SDD path. Preserve `commitSha`/`baseSha` as first publication; use `revisionSha`/`revisionBaseSha` as current publication when present. Do not weaken schema or create another store. 5. Verify exact branch linkage and live branch name/head using existing readiness proof. Verify publication and ancestry at immutable SHAs. A missing, deleted, mismatched or unverifiable branch/publication cannot support a current comparison. 6. Compute the same digest as the gate from current normalized title, visible human body, admitted kind, profile and original recorded base. Digest mismatch takes precedence and reports stale. A digest-matching awaiting-answer record with verified first publication reports pending-revision. Neither permits current counts. A current comparison requires matching digest and verified current publication. @@ -122,7 +122,7 @@ Text equivalent: comment -> exact target and evidence verification -> bounded co - Digest mismatch: `stale`, with SDD question-card link; not pending-revision. - Forged, foreign, malformed or unknown-version record: invalid/unavailable, never a comparison. - Missing branch/publication and provider read errors: distinct missing/unavailable states where evidence permits; do not fabricate facts. -- PR with no eligible native closing issue: not-applicable; ambiguity and authority failure are explicit. +- PR with zero native closing issues or an ineligible sole issue: not-applicable; ambiguity and authority failure are explicit. - Changed documents that exceed limits: bounded/unavailable; safe verified revision links may be shown. ### 6.3 State machine @@ -134,7 +134,7 @@ Text equivalent: comment -> exact target and evidence verification -> bounded co | Initial-only | First publication verified, no revision | Only first version exists | Compared after verified revision | Issue owner | | Current comparison | Digest, branch, publications and fence all verify | Structural comparison is current | Re-run on new source comment | Gate/application | | Pending-revision | Matching digest; trusted record awaits answers; first publication verified | Revision is being clarified; no current diff | Compared after publication; stale after issue edit | Issue owner | -| Stale | Current digest differs | Issue changed since gate record | Re-run after gate refresh | Issue owner | +| Stale | Current digest differs or authority/head fence changes | Current comparison cannot be certified | Retry the read; refresh clarifications if the issue changed | Issue owner | | Invalid/unavailable | Trust, linkage or provider evidence fails | Current comparison cannot be verified | Retry when evidence available | Maintainer/operator | | Bounded | Document or work limit exceeded | Exact comparison unavailable within safety bound | None until input changes | Issue/spec owner | @@ -276,19 +276,23 @@ Errors use `impact -> cause -> action -> retained state`; a failed read must not ## 14. Testing strategy and numeric budget -Minimum: 30 distinct risk-derived cases, meeting the supplied minimum of 24. Each case counts once; parameter rows count only when behavior is distinct. +Minimum: **217 distinct extension cases**, with a 24-case risk floor exceeded by the concrete inventory below. Existing shared gate/status/publication cases remain unchanged and are not counted again. -| Area | Minimum distinct cases | Behaviors/risks covered | +| Area | Distinct cases | Behaviors/risks covered | |---|---:|---| -| Domain/configuration/pure planning | 6 | eligibility, digest precedence, state mapping, bounds, diff counts, unsafe titles | -| State/application/idempotency/races | 6 | no publication, initial-only, pending, stale, fence races, replay correlation | -| Adapters/provider contracts | 5 | schema-v1 ownership, exact branch, immutable publication/ancestry, PR pagination/link identity, provider errors | -| Workflows/setup/schema | 3 | read-only permissions, no writer/model wiring, existing record compatibility | -| UI/UX/localization/sanitization | 6 | English/Spanish, atomic fallback, links, escaping, omission disclosure, reply budget | -| Integration/security/migration | 4 | no-write end-to-end, forged/foreign record, excessive input/work, compatibility with gate | -| **Total** | **30** | No double counting | +| Pure bounded diff | 18 | Exact line/section changes, fenced headings, hostile input, all fixed resource ceilings | +| Presentation/localization/navigation | 60 | Ten states in both catalogs, atomic fallback, immutable commit/blob/compare links, clarification cards, escaping and bounds | +| Application/authority/fences | 52 | Admission, missing/invalid/pending/stale authority, immutable reads, issue/card/head/PR-link races, exact author and no writes | +| Native PR-link adapter | 45 | Bound repository ID/name and head repository, exact PR/head/ref, author identity, complete pagination, malformed/foreign/duplicate rows and denial | +| Read-only composition | 5 | Frozen repository binding, trusted server origin, native event repository ID and receiver-bound document read without writers | +| Parser/routes/agent activity | 15 | Whitespace/case, no arguments, EN/ES/fallback, absent query, no agent activation or activity | +| Correlated publication/replay | 17 | Same-source refresh, unchanged replay, distinct source, ownership/correlation/key isolation, duplicate healing, static catalogs and final sanitization | +| Actual Execution projection | 5 | Native repository ID/issue author/profile digest, immutable input, missing/zero/unsafe identity and native PR target | +| **Total** | **217** | Every case counted once | -Keep all existing SDD gate coverage and budgets unchanged; shared helper changes require documented justification and regression coverage. Preserve repository-wide thresholds and meet or exceed them; exact thresholds are repository-specific and must be copied from current policy before Ready. New pure policies target at least 90% branch coverage. Changed modules must meet repository thresholds with no unexplained coverage decrease. Use deterministic clocks, IDs and fakes; no real waits/live services. Add table-driven state/bound tests, replay/race tests, adapter contracts and structural workflow permission checks. Golden replies require semantic assertions for states, links, counts, localization and forbidden content. Human UX evidence must review #454 first/revised SDD output and narrow-screen readability; do not rely on snapshots alone. +The two new shared `SddEvidenceQueryUseCase` opaque-authority regressions are allocated only to the owning status companion (198 + 2 = 200); they are excluded from this 217-case budget. The gate's separate 149-case budget and both communication ledgers remain unchanged. + +Repository-wide thresholds remain 90% lines/statements, 88% functions and 82% branches. `scripts/coverage-budgets.json` additionally enforces 100% in every new pure module, and at least 95% lines/statements and 90% branches/functions in each new application, native adapter and composition module. All six new runtime modules currently achieve 100% in the focused scope. Deterministic fakes and actual Execution constructors provide local proof; live GitHub runs independently establish bot permissions, event wiring and human UX. Required distinct risk coverage includes eligibility/identity (4), record/ownership/publication trust (5), digest/revision races (4), diff/count/bounded work (5), and localization/sanitization/replay/no-write (6), totaling at least 24 cases within the 30-case distribution. @@ -299,7 +303,7 @@ Required distinct risk coverage includes eligibility/identity (4), record/owners | User | `docs/issues/comment-commands.mdx` | Exact command, eligible targets, examples, states and links | Command catalog and link checks | | Issue owner | `docs/issues/pre-branch-sdds.mdx` | First/current revision distinction, stale/pending recovery, structural-only caveat | Examples checked against fixtures | | Operator | Same SDD guide | Invalid/unavailable evidence, retry and permission boundaries | State decision table review | -| Contributor | Capability SDD and catalog evidence | Ports, trust boundaries, 30-case budget, workflow checks | `pnpm run validate:specifications`; catalog generation/check as repository requires | +| Contributor | Capability SDD and catalog evidence | Ports, trust boundaries, 217-case budget, workflow checks | `pnpm run validate:specifications`; catalog generation/check as repository requires | Docs must explain PR exact-link rules, limits, no-write behavior and that line/title differences do not establish semantic requirement changes. Update the capability catalogue evidence and owning SDD or this approved bounded companion before branch creation. Keep documentation examples synchronized with fixtures. @@ -312,7 +316,7 @@ Docs must explain PR exact-link rules, limits, no-write behavior and that line/t 5. Given any current digest mismatch, including an awaiting-answer record, when queried, then stale takes precedence over pending; no current counts appear and the question-card link is provided. 6. Given a foreign, forged, malformed or unknown-version record, when queried, then no current comparison is claimed and no untrusted values are rendered as authority. 7. Given wrong owner/path, absent publication, deleted or mismatched branch, failed ancestry/publication proof, when queried, then the result is invalid, missing or unavailable as supported by verified facts, never current-ready. -8. Given a PR with exact native same-repository closing-issue authority to zero eligible issues, when queried, then it reports not-applicable; with two eligible issues or disagreeing identity it reports ambiguous/unavailable. +8. Given a PR with exact native same-repository closing-issue authority to zero eligible issues, when queried, then it reports not-applicable; with two native closing issues or disagreeing identity it reports ambiguous/unavailable. 9. Given incomplete PR pagination or unreadable authority, when queried, then it reports unavailable and does not fall back to body numbers or equal PR/issue numbers. 10. Given a document over any character, byte or line limit, or computation over 2,000,000 steps, when compared, then it reports bounded/unavailable without truncation or guessed counts. 11. Given more than eight changed section titles or unsafe headings, when rendered, then only up to eight escaped, bounded titles appear and the omitted count is explicit; mentions/control/Markdown injection is inert. @@ -345,7 +349,7 @@ Docs must explain PR exact-link rules, limits, no-write behavior and that line/t 5. Add adapter mappings and structural PR identity/pagination tests. 6. Add localized states and correlated reply integration with golden and semantic fixtures. 7. Update both issue docs, capability catalogue evidence and this companion/owning SDD before branch creation. -8. Complete 30-case minimum, all existing gate budgets, #454 UX review, architecture/workflow checks and specification validation. +8. Complete 217-case minimum, all existing gate budgets, #454 UX review, architecture/workflow checks and specification validation. ## 19. Definition of Done @@ -366,4 +370,74 @@ Docs must explain PR exact-link rules, limits, no-write behavior and that line/t - Primary sources: Issue #464 and its explicit answer rounds supplied with this request; existing repository contracts named in those answers, pending source-path confirmation. - Related specs/ADRs/issues/PRs: owning `issue-start-and-sdd-readiness` SDD; live issue #454 as UX evidence; exact URLs and catalog identifiers beyond the supplied capability ID remain to be confirmed from repository metadata. - Decisions: companion SDD under the existing capability; exact native same-repository PR closing-issue relationship is authoritative; digest mismatch always reports stale; pending-revision requires matching digest and verified first publication; first publication fields remain immutable; no arbitrary paths, refs, body URLs, semantic inference, model, writes or new record store. -- Follow-up outside this spec: any broader semantic SDD analysis, changes to gate lifecycle/record schema, or general PR-to-issue discovery. \ No newline at end of file +- Follow-up outside this spec: any broader semantic SDD analysis, changes to gate lifecycle/record schema, or general PR-to-issue discovery. + +### Confirmed implementation bindings and dependency gate + +The verified shared query is `SddEvidenceQueryPort` in +`src/application/ports/sdd_evidence_read_ports.ts`, implemented by +`sdd_evidence_query_use_case.ts` from PR #465. Its closed snapshot distinguishes +not-required, absent, unavailable, invalid, stale, awaiting-answers, +publication-pending and published. Verified evidence contains the owning path, +capability, exact branch name/head and immutable first/current SHAs. The diff +must consume this query rather than duplicate its gate/digest/ancestry logic. +`PreBranchSddWorkspacePort.readSdd(commitSha, path)` supplies the two immutable +contents through a physically read-only projection; no draft, publish or repair +method may reach the command. + +Composition must provide only the shared query, immutable content read and exact +native PR closing-issue query. Both native association and shared evidence are +reread after collecting the documents, with all relevant identities compared +before any current count is rendered. A pending revision may expose a verified +first publication as history, never current counts. An original-only publication +has one immutable version and no fabricated second revision. Unavailable or +bounded comparison produces no guessed counts or silently truncated documents. + +### Partial implementation evidence (2026-10-12) + +The initial local 33-case observation was 11 new comparator cases plus 22 +pre-existing shared card/digest cases. The latter are preserved separately and +excluded from this extension; no shared gate or digest implementation is copied +into the diff feature. + +The current independent contribution has **106 distinct new cases**: + +| Implemented area | Distinct new cases | Verified risks | +|---|---:|---| +| Pure structural comparator | 18 | Exact line counts, honest character/byte/line/work ceilings, matching Markdown fences, bounded title changes and propagation of unexpected computation faults | +| Localized presentation | 52 | Ten states in English/Spanish, atomic fallback, historical-only pending/stale proof, exact safe repository links, missing projections, hostile headings, omitted titles and the 5,600-character body ceiling | +| Native PR closing-issue adapter | 36 | Exact repository IDs/names, PR number/head, complete bounded inventory, duplicate/foreign/malformed rows, denials, immutable binding/output and a constant read-only query | +| **Implemented partial total** | **106** | No shared helper case counted again | + +All four new runtime modules reach 100% lines/statements/branches/functions in +the focused scope. Typecheck and lint passed. Full validation in the existing +older diff worktree passed 563 suites, 7,270 tests and 26 configured skips +(7,296 total), including every unchanged official coverage budget. The setup +acceptance ledger retains 308/350 passing cases and 42 explicitly open cases. +This is partial validation on that worktree's ancestor, not proof of a combined +implementation on current master or the shared foundation. + +A read-only operator GraphQL query on real PR #465 confirmed the exact native +contract: repository database ID/name, exact PR number/head, sole same-repository +closing issue #454, totalCount 1 and hasNextPage false. It establishes response +shape only. Bot permission, application orchestration, final evidence/link +fences, parser/help/composition, correlated publication and live issue/PR UX +remain unverified. The final numeric distribution must include their distinct +risk cases before publication. The specification remains Draft. + + +### Integrated local implementation (2026-10-12) + +The earlier 106-case report is a historical partial snapshot. The current implementation has 212 dedicated cases across seven suites plus five actual Execution producer contracts, totaling 217 without shared-case duplication. The six new runtime modules have 100% lines/statements/branches/functions in focused coverage. Native Action-only synchronization committed `ad27952768674c11bb225bcf9389917c3c4b7f9f` and later `0f016278e34a7cbba0bfa4ad290e759492064994`; no manual parent merge or foundation copy was used. + +The shared snapshot now carries an optional opaque SHA-256 authority digest over the exact fenced card and live issue authority. It exposes no card contents or raw provider data. The diff application requires that digest before current claims and compares it with canonical publication facts after reading immutable documents. Native PR relationships additionally bind current head repository, ref, SHA and original issue author, including a final association fence. + +Only the `copilot-sdd-diff` reply family refreshes its existing exact bot-owned target/source correlation when the newly verified reading changes. Unchanged replay makes no mutation; other reply families retain their existing create-once behavior. Complete static catalogs also bypass dynamic localization for this command. Publication still follows the ordinary non-atomic GitHub comment boundary: the final authority fence prevents changes observed during collection, but does not claim a server-side compare-and-swap transaction. + +Live command activation, #454 first/revised UX, PR native-link acceptance, replay and locale checks remain pending promotion. Local coverage is not proof of those outcomes. + + +Stale presentation does not invent a cause: a card or head race is not described as an issue edit. The next action retries the current read first and directs the owner to clarifications conditionally when the issue itself changed. Existing state/fence cases cover this wording without adding mirrored prose tests or inflating the 217-case budget. + + +Final local integration on Action merge `86db9b1962915111f02613b4f49ec50a6a3f1cf6` passes the official full validation: 579 suites, 7,952 passing tests and 35 unchanged platform skips. Overall coverage is 97.25% lines, 96.23% statements, 92.70% branches and 97.10% functions; every new comparison module remains at 100%. Type, lint, workflow, documentation/specification validation, generated catalog and graph update pass. Live whitespace/case/newline synchronization run `38253646042` also passed on Windows with all five configured checks before publishing that parent merge. This proves operational synchronization and combined local source; it does not yet activate the new command on master. diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index a16d5b94d..ad2c34347 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -1,9 +1,9 @@ # Pre-branch SDD Readiness in `/copilot status` -- Status: Proposed +- Status: Implemented - Date: 2026-10-10 - Catalog capability ID: `issue-start-and-sdd-readiness` -- Last verified: 2026-10-11 +- Last verified: 2026-10-12 - 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` @@ -308,19 +308,19 @@ No schema or configuration migration is introduced. Existing gate records remain ## 14. Testing strategy and numeric budget -Minimum: **198 distinct cases** for this extension. Existing gate and PR status cases are retained but not counted again here. +Minimum: **200 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 | 28 | Exact bot ownership, highest comment identity, corruption without older fallback, duplicate markers, safe IDs and retained answer IDs | -| Authority/application/fences | 65 | 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; three native Execution profile projection contracts | +| Authority/application/fences | 67 | 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; three native Execution profile projection contracts | | 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** | **198** | No case counted twice | +| **Total** | **200** | 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 198-case implementation budget. +5. Add adapter, integration, localization, security, and compatibility tests within the 200-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 198 distinct extension cases, full coverage budgets and generated bundles pass before publication. +- [x] All 200 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. @@ -522,3 +522,10 @@ suites and 7,723 tests with 35 configured skips; unchanged coverage budgets and the 308/350 setup ledger passed. Typecheck, lint, specifications, documentation, graph update and build passed. A fresh native status request remains required; these offline checks do not certify that live result. + + +### Opaque authority identity for structural comparison (2026-10-12) + +The shared read-only snapshot optionally exposes `authorityDigest`, a SHA-256 fingerprint of the exact fenced bot-owned card body, canonical current issue digest/labels and relevant awaiting-answer provenance. It exposes none of those contents. Existing status presentation ignores this optional field and the stored schema-v1 gate record is unchanged. The structural comparison consumer requires and rechecks this digest, so a card-body mutation that preserves the visible publication fields cannot reuse old current counts. + +Two distinct authority-reader regressions prove stable opaque output and changed identity after a same-projection card-body replacement. They raise only this companion's authority allocation from 65 to 67 and total from 198 to 200. The separate gate's 149 cases and the structural comparison's 217 cases exclude these two regressions. Prior dated validation counts remain historical evidence. diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index 3554b4f26..5d0b59e5c 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -1439,3 +1439,10 @@ The live source head and development base are checked again after analysis, befo The repair owns **30 distinct cases**: 26 provider-bound immutable-comparison cases and four application outcomes (wrong base, wrong head, unavailable comparison, and a changed base during analysis). The existing head-race and prompt cases are retained, with explicit supplied-evidence and no-shell assertions. The conservative semantic ledger adds ten declarations rather than counting parameterized rows again. User documentation and catalog evidence change with implementation. Live acceptance must prove a supplied-source assessment and retained state on incomplete evidence; successful offline tests alone do not close that acceptance. Live human doctor push `38162035628`, head `25950008`, exposed a second comparison-boundary problem: 39 files contained 248,563 raw source-patch bytes, including 89,186 bytes of the generated specification index; JSON escaping exceeded the unchanged 256,000-byte budget. The exact generated `specs/CATALOG.md` path now carries metadata only, alongside build outputs. The editable `specs/catalog.json` still requires its full patch. Two distinct provider contracts cover the generated index and its source. Bounds/incomplete data now report `provider.unavailable`; only invalid or contradictory object identity uses `workflow.stale`, so the Job Summary does not falsely claim the branch advanced. Fresh live acceptance remains pending. + + +### Read-only SDD comparison reply refresh (2026-10-12) + +`/copilot sdd-diff` adds the closed `copilot-sdd-diff` correlated reply family. Its already bounded EN/ES presentation is selected only from an executed successful `.SddDiff` result with a valid resolved locale. Composition uses the static publication catalog for this deterministic reply, without invoking translation or an agent. + +For this family only, a repeated source comment refreshes the exact bot-owned target/correlation/message-key canonical reply when the latest verified reading changes; an identical replay is unchanged. Other reply families retain create-once behavior. Existing owned duplicate cleanup remains applicable and foreign markers are inert. The final sanitizer neutralizes automation markers and mentions. These 17 dedicated cases are allocated to the structural comparison's 217-case inventory, not recounted in the existing 135-case minimum or communication ledgers. Authority/freshness belongs to the comparison application and its shared evidence query; comment publication does not claim an atomic GitHub transaction. diff --git a/src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts b/src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts new file mode 100644 index 000000000..475a41524 --- /dev/null +++ b/src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts @@ -0,0 +1,123 @@ +import { renderSddDiff, type SddDiffSnapshot } from '../sdd_diff_presentation_policy'; +import { resolveSddDiffCatalog, SDD_DIFF_MESSAGE_IDS, ENGLISH_SDD_DIFF_DEFINITION, SPANISH_SDD_DIFF_DEFINITION } from '../sdd_diff_message_catalog'; +const first = 'a'.repeat(40), current = 'b'.repeat(40); +const publication = { path: 'specs/owning-sdd.md', firstSha: first, currentSha: current }; +const scope = { serverOrigin: 'https://github.example.test', owner: 'org', repository: 'repo' }; +const compared: SddDiffSnapshot = { state: 'compared', publication, + changes: { state: 'compared', addedLines: 0, removedLines: 0, sections: [], omittedSections: 0 } }; +const snapshots: readonly SddDiffSnapshot[] = [ + ...(['not-applicable', 'no-publication', 'invalid', 'unavailable', 'ambiguous'] as const).map(state => ({ state })), + { state: 'initial-only', publication }, { state: 'stale', publication }, { state: 'pending-revision', publication }, + { state: 'bounded', publication, reason: 'lines' }, compared, +]; +const render = (value: SddDiffSnapshot, locale = 'en-US') => renderSddDiff(value, scope, resolveSddDiffCatalog(locale)); +describe('bounded bilingual structural SDD reply', () => { + it.each(snapshots.flatMap(snapshot => ['en-US', 'es-ES'].map(locale => ({ snapshot, locale }))))('renders $snapshot.state in $locale without inventing counts or current links', ({ snapshot, locale }) => { + const catalog = resolveSddDiffCatalog(locale), body = render(snapshot, locale); + expect(body).toContain(catalog.message(`sddDiff.${snapshot.state}`)); + expect(body).toContain(catalog.message('sddDiff.action')); + expect(body.length).toBeLessThanOrEqual(5600); + if (snapshot.state === 'compared') expect(body).toContain(catalog.message('sddDiff.lines', { added: 0, removed: 0 })); + else expect(body).not.toContain(catalog.message('sddDiff.lines', { added: 0, removed: 0 })); + if (snapshot.state === 'stale' || snapshot.state === 'pending-revision') { + expect(body).toContain(catalog.message('sddDiff.historical')); + expect(body).toContain(`/blob/${first}/specs/owning-sdd.md`); + expect(body).not.toContain(current); + } + if (snapshot.state === 'compared' || snapshot.state === 'bounded') expect(body).toContain(`/blob/${current}/specs/owning-sdd.md`); + if (snapshot.state === 'bounded') expect(body).toContain(catalog.message('sddDiff.linesLimit')); + if (snapshot.state === 'initial-only') expect(body).not.toContain(current); + }); + it('constructs links at the bound enterprise origin rather than github.com or supplied heading URLs', () => { + expect(render(compared)).toContain(`https://github.example.test/org/repo/blob/${current}/specs/owning-sdd.md`); + expect(render(compared)).not.toContain('github.com'); + }); + it('falls back atomically to English for an unsupported locale without a model', () => { + expect(render(compared, 'fr-FR')).toBe(render(compared, 'en-US')); + }); + it('provides complete matching English and Spanish message inventories', () => { + for (const catalog of [ENGLISH_SDD_DIFF_DEFINITION, SPANISH_SDD_DIFF_DEFINITION]) { + expect(Object.keys(catalog.messages).sort()).toEqual([...SDD_DIFF_MESSAGE_IDS].sort()); + expect(SDD_DIFF_MESSAGE_IDS.every(id => typeof catalog.messages[id] === 'string')).toBe(true); + } + }); + it('escapes section Markdown, mentions and controls without exposing document excerpts', () => { + const body = render({ ...compared, changes: { state: 'compared', addedLines: 1, removedLines: 1, + sections: ['[click](javascript:alert(1)) @all