From 00bafef77e505eb4da3ce28c370640d076867ef2 Mon Sep 17 00:00:00 2001 From: RollNuts Date: Sun, 19 Jul 2026 23:12:34 +0900 Subject: [PATCH] Build versioned public contract --- .github/ISSUE_TEMPLATE/config.yml | 6 +- .github/ISSUE_TEMPLATE/spec-clarification.md | 37 +- .github/ISSUE_TEMPLATE/spec-correction.md | 51 +- .github/workflows/contract.yml | 23 + ACK_LABEL.md | 88 +- CHANGELOG.md | 25 + CHECK_STATES.md | 23 + COMPATIBILITY.md | 11 + CONTRIBUTING.md | 68 +- DATA_HANDLING.md | 69 +- EVENTS.md | 187 +--- LICENSE | 7 - OUTPUT.md | 293 +----- README.md | 161 +--- RELEASING.md | 10 + TRUTH_MATRIX.md | 22 + WEBHOOK_EVENTS.md | 14 + contract/check-states.json | 212 +++++ contract/webhook-events.json | 119 +++ examples/ack-label.json | 8 + examples/check-run-acknowledged.json | 11 + examples/check-run-clear-to-land.json | 10 + examples/check-run-clear.json | 7 +- examples/check-run-heading-to.json | 9 + examples/check-run-paused.json | 4 +- .../invalid/check-ack-action-required.json | 8 + examples/invalid/check-ack-false.json | 8 + examples/invalid/check-clear-neutral.json | 7 + examples/invalid/check-heading-neutral.json | 6 + examples/invalid/check-markers-success.json | 7 + examples/invalid/check-paused-ack-true.json | 8 + examples/invalid/check-paused-neutral.json | 8 + examples/invalid/raw-pr-number-mismatch.json | 21 + examples/manifest.json | 102 +++ examples/pr-comment-surface.json | 5 +- examples/raw-pull-request-opened.json | 22 + examples/webhook-envelope-check-run.json | 7 + ...ok-envelope-installation-repositories.json | 11 + examples/webhook-envelope-pull-request.json | 10 + package-lock.json | 34 + package.json | 30 +- schemas/ack-label.schema.json | 32 +- schemas/check-run-signal.schema.json | 841 ++++++++++++++---- schemas/pr-comment-surface.schema.json | 63 +- schemas/webhook-routing-envelope.schema.json | 302 +++++++ schemas/webhook-routing-event.schema.json | 36 +- scripts/check-contract.mjs | 21 + scripts/check-examples.py | 23 + scripts/check-json.mjs | 16 +- scripts/check-links.py | 18 + scripts/check-schema.py | 12 + scripts/check-types.mjs | 7 + scripts/generate-contract.mjs | 97 ++ scripts/generate-events.mjs | 60 ++ tests/types/contract.test.ts | 22 + tsconfig.json | 11 + types/check-states.generated.d.ts | 124 +++ types/index.d.ts | 127 +-- types/webhook-events.generated.d.ts | 74 ++ 59 files changed, 2465 insertions(+), 1190 deletions(-) create mode 100644 .github/workflows/contract.yml create mode 100644 CHANGELOG.md create mode 100644 CHECK_STATES.md create mode 100644 COMPATIBILITY.md create mode 100644 RELEASING.md create mode 100644 TRUTH_MATRIX.md create mode 100644 WEBHOOK_EVENTS.md create mode 100644 contract/check-states.json create mode 100644 contract/webhook-events.json create mode 100644 examples/ack-label.json create mode 100644 examples/check-run-acknowledged.json create mode 100644 examples/check-run-clear-to-land.json create mode 100644 examples/check-run-heading-to.json create mode 100644 examples/invalid/check-ack-action-required.json create mode 100644 examples/invalid/check-ack-false.json create mode 100644 examples/invalid/check-clear-neutral.json create mode 100644 examples/invalid/check-heading-neutral.json create mode 100644 examples/invalid/check-markers-success.json create mode 100644 examples/invalid/check-paused-ack-true.json create mode 100644 examples/invalid/check-paused-neutral.json create mode 100644 examples/invalid/raw-pr-number-mismatch.json create mode 100644 examples/manifest.json create mode 100644 examples/raw-pull-request-opened.json create mode 100644 examples/webhook-envelope-check-run.json create mode 100644 examples/webhook-envelope-installation-repositories.json create mode 100644 examples/webhook-envelope-pull-request.json create mode 100644 package-lock.json create mode 100644 schemas/webhook-routing-envelope.schema.json create mode 100644 scripts/check-contract.mjs create mode 100644 scripts/check-examples.py create mode 100644 scripts/check-links.py create mode 100644 scripts/check-schema.py create mode 100644 scripts/check-types.mjs create mode 100644 scripts/generate-contract.mjs create mode 100644 scripts/generate-events.mjs create mode 100644 tests/types/contract.test.ts create mode 100644 tsconfig.json create mode 100644 types/check-states.generated.d.ts create mode 100644 types/webhook-events.generated.d.ts diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0498a06..5326d8e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Veripsa support - url: https://veripsa.com - about: For account, billing, or installation questions, please reach out via veripsa.com — this repo is for the public webhook spec only. + url: https://veripsa.com/support + about: Account, installation, and billing questions belong in the canonical support channel. - name: Recent Veripsa changes url: https://veripsa.com/whats-new - about: Check the canonical product change log before reporting a behavior change. + about: Check user-visible product changes before reporting public-contract drift. diff --git a/.github/ISSUE_TEMPLATE/spec-clarification.md b/.github/ISSUE_TEMPLATE/spec-clarification.md index 49ff5f2..f4c3ef5 100644 --- a/.github/ISSUE_TEMPLATE/spec-clarification.md +++ b/.github/ISSUE_TEMPLATE/spec-clarification.md @@ -1,39 +1,14 @@ --- name: Spec clarification -about: The spec is ambiguous and I'm not sure how to integrate -title: "[clarify] " -labels: spec-clarification +about: Clarify an ambiguous observable integration contract +labels: '' assignees: '' --- -## What are you trying to integrate? +## Integration surface -In one sentence — a dashboard, an agent that reads Veripsa's verdict, a -log shipper, a security review of the App's permissions, etc. +## Ambiguous contract text -## Which part of the spec is ambiguous? +## Concrete observable example -Quote the spec line and explain what two (or more) readings of it you -came up with. - -## What would unblock you? - -- [ ] A concrete example added to the spec. -- [ ] A clarification of which reading is correct. -- [ ] A statement that this is intentionally undefined and the App may - change behaviour here. - -## Anything else? - -(Optional.) Link to the section, or paste the lines of pseudocode you -were trying to write. - ---- - -**Please do not include:** - -- Source code or file contents from your repo. -- Customer names or third-party identifiers. - -This repo is documentation only; we do not need either to answer a -clarification question. +Do not include source code, diffs, customer names, tokens, or private identifiers. diff --git a/.github/ISSUE_TEMPLATE/spec-correction.md b/.github/ISSUE_TEMPLATE/spec-correction.md index 70ee2d7..1638daa 100644 --- a/.github/ISSUE_TEMPLATE/spec-correction.md +++ b/.github/ISSUE_TEMPLATE/spec-correction.md @@ -1,54 +1,15 @@ --- name: Spec correction -about: The spec on this repo and the App's actual behaviour disagree +about: Report a difference between the public contract and visible App behavior title: "[spec] " -labels: spec-correction +labels: '' assignees: '' --- -## Where in the spec? +## Contract file and section -Which file and section? (e.g. `OUTPUT.md` → "The verdict ladder", or -`EVENTS.md` → "Subscribed events".) +## Visible check title, conclusion, marker, or label behavior -## What does the spec say? +## Date observed -Quote (or screenshot) the spec line you believe is wrong. - -## What did the App actually do? - -Describe what you observed on a real PR or a real webhook payload: - -- which event you saw, -- which check title / `conclusion` was posted, -- which marker text appeared in the comment, -- or which label-event behaviour you observed. - -> Example: "The spec says `Veripsa — Heads up` posts `neutral`, but on -> PR-17 I saw the check posted as `action_required`." - -**Do not paste private source code, diffs, or file contents** — this -spec repo is content-free and we do not need them. Pasting the check -title, the comment marker, the conclusion, and the verdict word is -enough. - -## When did you observe it? - -A date or a date range. The Veripsa team correlates against the deploy -log. - -## Anything else? - -(Optional.) Screenshots of the GitHub check or the comment (with -private content redacted) help. - ---- - -**Please do not include:** - -- Source code or file contents from your repo. -- Diffs or commit patches. -- Customer names or third-party identifiers. - -We only need to know what Veripsa's surfaces showed you and what the -spec on this repo said. +Do not include source code, diffs, customer names, tokens, or private identifiers. diff --git a/.github/workflows/contract.yml b/.github/workflows/contract.yml new file mode 100644 index 0000000..c9b2757 --- /dev/null +++ b/.github/workflows/contract.yml @@ -0,0 +1,23 @@ +name: public-contract +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install --disable-pip-version-check "jsonschema>=4.23,<5" + - run: npm ci + - run: npm run check + - run: npm pack --dry-run diff --git a/ACK_LABEL.md b/ACK_LABEL.md index f0c61c0..bf58a64 100644 --- a/ACK_LABEL.md +++ b/ACK_LABEL.md @@ -1,88 +1,28 @@ # The `veripsa-ack` label -`veripsa-ack` is the single PR label Veripsa reacts to. It is an **optional, -explicit, recorded acknowledgement** of a structural coupling that Veripsa -has surfaced. Installing or using Veripsa does not require a routine ACK step, -per-agent setup, CLI polling command, or copied merge rule. +`veripsa-ack` means: **“I have seen this specific material coupling and I am proceeding anyway.”** It is not correctness approval, code review approval, or proof that the coupling is resolved. -## What "ack" means +## Definition versus application -**Ack means: "I have seen this coupling and I am proceeding anyway."** +Two operations are deliberately separate: -It does **not** mean: +1. **Repository label definition:** Veripsa may lazily create the repository's `veripsa-ack` label definition when a material state first needs it. +2. **Application to a PR:** Veripsa never applies `veripsa-ack` to a pull request on its own. A human or agent with suitable base-repository permission must explicitly apply it. -- "this code is correct" — Veripsa does not assert correctness; -- "this PR is approved" — ack is **not** a review approval; GitHub's - normal review and branch-protection rules are independent of - `veripsa-ack`; -- "the collision is resolved" — the collision may still be real; ack - only records that someone took responsibility for proceeding. +This distinction is part of the v1 public contract. -## When the label matters +## Effect -The label is meaningful only on PRs where Veripsa has surfaced a -**material coupling** in its high-salience check state (see -[`OUTPUT.md`](./OUTPUT.md)). The state appears automatically; recording an ACK -is optional unless the repository owner independently makes the Veripsa check -required and chooses ACK as its exception path. +For a material `Wait in line` or named-partner `Heads up` state, the unacknowledged normalized check is `Paused (acknowledge to proceed)` with `action_required`. A matching explicit ACK reshapes it to `Acknowledged`, `neutral`, and `acknowledged: true`. -- On a **Clear** PR, the label has no effect. -- On a **Heads up** PR with no named in-flight partner (a solo notice), - or a minor build/list-file overlap, the label has no effect — those - never pause. -- On a material PR — a **Wait in line** verdict, or a **Heads up** - verdict with a named in-flight partner PR — the label toggles the - check's `conclusion` from `action_required` to `neutral` and - re-renders the comment to record the ack. +The check gates a merge only when the repository owner independently marks the Veripsa check as required. -## Adding / removing the label +## Staleness -- **Who can add it.** Anyone with write access to the PR's base - repository (humans, bots, autonomous agents using a token with - `pull_requests: write`). Fork contributors cannot add it; their PRs - are treated as non-material for ack purposes. -- **How an agent adds it.** Using the standard GitHub CLI: - `gh pr edit --add-label veripsa-ack`. -- **How a human adds it.** From the PR sidebar's Labels picker. -- **Removing it.** Removing the label re-raises the pause on the next - event that re-analyses the PR. +An ACK is bound to the specific material coupling. When partners or colliding paths materially change, the prior ACK becomes stale, the high-salience state returns, and the stale PR label is removed or slated for removal. -Veripsa is subscribed to `pull_request` `labeled` / `unlabeled` events -but only acts on them when the changed label is exactly `veripsa-ack`. -Other label changes are ignored at the routing layer. +The comment's 12-lowercase-hex ACK snapshot token is opaque. Its format is public; its derivation is not. -## Stale acks +## Fork PRs -An ack is bound to the **specific coupling** it covered (which partners, -which files). If the coupling **materially changes** between events — a -different partner PR, a different set of colliding files — the prior -ack is considered **stale**: - -- the high-salience state re-raises on the next analysis, -- the `veripsa-ack` label is slated for removal so the PR visibly reads - un-acked again. - -This makes ack a **fresh, recorded decision per coupling**, not a -one-time opt-out for the lifetime of the PR. - -The binding is recorded in an invisible HTML marker -(``) inside Veripsa's PR comment; see -[`OUTPUT.md`](./OUTPUT.md) for the marker shape. Integrators generally -do not need to parse it. - -## What Veripsa never does with labels - -- Veripsa does not add or remove **any label other than `veripsa-ack`**. -- Veripsa does not add `veripsa-ack` itself; the label is - human-or-agent-applied. -- Veripsa does not interpret label color, description, or position in - the picker. The label name is the entire contract. - -## Naming and color - -- Veripsa relies on the **exact label name** `veripsa-ack`. Capitalisation - matters. The label is created lazily on first need; it does not have - to exist on the repository ahead of time. -- Color, description, and label-picker position are not part of the - contract — you may change them freely. Veripsa will not overwrite - them. +Fork contributors cannot apply the base repository's label. Fork PRs therefore keep the advisory non-paused form and receive redacted cross-PR context. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..396633e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## 1.0.0 — 2026-07-19 + +Initial versioned public contract. + +### Corrected + +- Separates exact `check_run.name` (`Veripsa`) from human-facing `check_run.output.title`. +- Locks `Heading to` to `success` across manifest, Schema, types, examples, and docs. +- Rejects impossible token/conclusion/underlying-verdict/ACK combinations. +- Defines the ACK snapshot as a 12-lowercase-hex opaque wire token. +- Defines clean `Clear` as check-only and clear coordination comments as `Clear to land`. +- Separates repository label-definition creation from explicit PR label application. + +### Added + +- Canonical check-state and webhook-event manifests. +- Full eight-event normalized routing envelope. +- Positive/negative example validation, TypeScript compile tests, local-link tests, generated-file drift checks, and CI. + +### Deprecated + +- `schemas/webhook-routing-event.schema.json` for new integrations. It remains a v1 compatibility schema for selected raw GitHub bodies. +- `VeripsaVerdictToken` in favor of `VeripsaCheckToken`. diff --git a/CHECK_STATES.md b/CHECK_STATES.md new file mode 100644 index 0000000..d61dc26 --- /dev/null +++ b/CHECK_STATES.md @@ -0,0 +1,23 @@ +# Check-state contract + +Generated from `contract/check-states.json` for contract **v1.0.0**. Human sentence copy may evolve; integrations depend on `check_run.name === "Veripsa"` plus the normalized token. + +| Token | Observable title example | Conclusion | Underlying verdict | ACK field | PR comment | Stability | +| --- | --- | --- | --- | --- | --- | --- | +| `Clear` | `Veripsa — Clear` | `success` | `clear` | `forbidden` | `never` | `stable` | +| `Clear to land` | `Veripsa — Clear to land` | `success` | `clear` | `forbidden` | `required` | `stable` | +| `Heads up` | `Veripsa — Heads up` | `neutral` | `heads_up` | `forbidden` | `when_coordination_context_exists` | `stable` | +| `Wait in line` | `Veripsa — Wait in line` | `neutral` | `wait_in_line` | `forbidden` | `when_coordination_context_exists` | `stable` | +| `Unknown` | `Veripsa — Unknown` | `neutral` | `unknown` | `forbidden` | `required` | `stable` | +| `Paused (acknowledge to proceed)` | `Veripsa — Paused (acknowledge to proceed)` | `action_required` | `heads_up` / `wait_in_line` | `false` | `required` | `stable` | +| `Acknowledged` | `Veripsa — Acknowledged` | `neutral` | `heads_up` / `wait_in_line` | `true` | `required` | `stable` | +| `Unresolved merge conflict markers` | `Veripsa — Unresolved merge conflict markers` | `action_required` | `merge_conflict_markers` | `forbidden` | `required` | `stable` | +| `Acknowledgement verification pending` | `Veripsa — acknowledgement verification pending` | `neutral` | — | `forbidden` | `never` | `stable` | +| `Heading to` | `Veripsa — heading to main` | `success` | — | `forbidden` | `never` | `stable` | +| `Watching` | `Veripsa is now watching` | `neutral` | — | `forbidden` | `never` | `stable` | +| `Early-access limit reached` | `Veripsa paused — early-access limit reached` | `neutral` | — | `forbidden` | `never` | `stable` | +| `Changed files not read` | `Veripsa — changed files not read, not analyzed` | `neutral` | — | `forbidden` | `never` | `stable` | +| `Branch state not verified` | `Veripsa — branch state not verified, retrying` | `neutral` | — | `forbidden` | `never` | `stable` | +| `Merge queue: clear` | `Veripsa — merge queue: clear` | `success` | — | `forbidden` | `never` | `stable` | +| `Merge queue: review overlap` | `Veripsa — merge queue: review overlap` | `neutral` | — | `forbidden` | `never` | `stable` | +| `Merge queue: not analyzed` | `Veripsa — merge queue: not analyzed` | `neutral` | — | `forbidden` | `never` | `stable` | diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 0000000..bd3c49d --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,11 @@ +# Compatibility policy + +The public contract is versioned independently from product deployment. + +- **Patch:** prose clarification, stricter tests that do not reject previously valid contract data, or corrections to generated artifacts without changing stable wire shapes. +- **Minor:** additive token/event/optional field/schema path. Consumers must treat unknown future titles/tokens as unsupported rather than guessing. +- **Major:** removal/rename of a stable token, field, marker, schema path, or a changed required conclusion/meaning. + +Deprecated v1 paths remain available for the lifetime of v1. New integrations should prefer `webhook-routing-envelope.schema.json`; `webhook-routing-event.schema.json` remains a selected raw-body compatibility surface. + +Human-facing sentence copy and paragraph order are not stable. Stable assets are the exact check name, normalized tokens and their conclusion constraints, marker shapes, label name/meaning, versioned schema IDs, and exported TypeScript names not marked deprecated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2387435..5265427 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,66 +1,16 @@ -# Contributing to veripsa-webhook-spec +# Contributing -Thank you for helping keep Veripsa's public integration contract accurate. +Contributions may clarify observable output, fix Schema/type/example drift, improve agent/dashboard consumption, or correct the public data boundary. Do not submit Core source, private scoring or heuristics, SQL/table internals, customer code, diffs, tokens, or proprietary identifiers. -This repository exists so external tools can safely depend on the Veripsa -GitHub App surface without needing access to Veripsa Core internals. Good -contributions usually make the contract easier to consume from an agent, -dashboard, log shipper, security review, or test fixture. - -## Good contributions - -We welcome pull requests and issues for: - -- corrections when the App's visible behaviour and this spec disagree, -- clearer wording around check-run titles, conclusions, PR comments, and - `veripsa-ack`, -- JSON Schema or TypeScript type fixes, -- small examples that show how an agent or dashboard should branch on the - public surface, -- security / compliance documentation for the public GitHub App surface, -- typo and link fixes. - -## Out of scope - -Please do not send: - -- Veripsa Core engine internals, -- requests to publish scoring formulas or collision heuristics, -- private repository source code, diffs, or proprietary file contents, -- customer names, tokens, installation secrets, or confidential identifiers, -- code that requires Veripsa to read or publish source file bodies. - -The public contract is intentionally content-free. A useful issue report can -usually be written with only the check title, GitHub `conclusion`, PR number -shape, label name, marker text, and the date you observed the behaviour. - -## Compatibility rules - -When proposing a change, call out whether it affects any stable surface: - -- check-run title prefix tokens, -- GitHub check `conclusion` values, -- PR comment markers, -- `veripsa-ack` semantics, -- schema file paths, -- TypeScript exported names. - -If the change only improves prose, say that explicitly. - -## Local validation - -This repository has no build step. To validate JSON files: +Run: ```sh -npm run check:json +npm ci +npm run generate +npm run check +npm pack --dry-run ``` -The script checks that every JSON file under `schemas/` and `examples/` -parses correctly. Schema semantic validation is intentionally kept -dependency-free for now so the repo stays easy to inspect. - -## Security +Generated files must be committed with their canonical manifest changes. Classify contract impact as patch, minor/additive, or major/breaking in the PR description. Prose-only changes must say so explicitly. -Do not report vulnerabilities by opening a public issue if the report -contains sensitive details. Use the disclosure path linked from -. +Security reports do not belong in public issues. Follow . diff --git a/DATA_HANDLING.md b/DATA_HANDLING.md index a90b7d2..bab6141 100644 --- a/DATA_HANDLING.md +++ b/DATA_HANDLING.md @@ -1,70 +1,27 @@ # Data handling -This document describes the public data boundary of Veripsa Core. It says -what can reach the service, what can be retained, and how deletion works. It -does **not** describe Veripsa's private analysis, ranking, or scoring methods. - -The [Privacy Policy](https://veripsa.com/privacy) is the controlling policy if -this summary and the live policy ever differ. +The Privacy Policy at is controlling if this summary ever diverges. ## What reaches the service -GitHub sends signed webhook requests for the events and repositories covered -by an installation. Veripsa reduces each request to the fields needed for -routing and processing before it enters the durable delivery queue. The -original request body exists transiently during request handling; it is not -kept as the durable queue payload or written to application logs. +GitHub sends signed webhook requests for repositories selected in an installation. Veripsa reduces each request before durable queuing. The original body exists transiently during handling and is not the durable queue payload or application-log content. -To keep an advisory current, Veripsa Core may also read repository files and -diff data transiently through the GitHub API. Source file bodies and diff -bodies are discarded rather than stored or displayed. GitHub secret-store -values are not exposed to the App and are never available to read. Committed -configuration text, including a committed `.env`-style file, may be read -transiently, but its body and values are not retained. +Veripsa may read repository files and diff data transiently through GitHub APIs. Source and diff bodies are discarded rather than retained or displayed. GitHub secret-store values are not exposed to the App. A committed configuration file may be read transiently, but its body and values are not retained. ## What may be retained -The retained data is content-free service metadata, including: - -- GitHub installation, account, repository, pull-request, commit, and delivery - identifiers; -- public GitHub login handles needed to identify an author, sender, or actor; -- repository and pull-request metadata such as owner/repository name, PR - number, branch or ref, commit and content fingerprints, labels, changed path - names, line ranges, and timestamps; -- content-free structural metadata such as language, file/config/schema and - symbol names and kinds, and named relationship types between those records; -- content-free working state such as advisory state, in-flight coordination, - coverage freshness/counts, and check, comment, and optional ACK state; -- delivery and lifecycle metadata such as event/action, processing status, - retry state, outcome, and operational error details. +Content-free service metadata may include installation/account/repository/PR/commit/delivery identifiers; public GitHub handles; repository/ref/path/line-range/configuration-key/schema/language/symbol names; fingerprints; named structural relationships; coverage freshness; advisory/check/comment/ACK state; delivery/retry/outcome/error metadata; and timestamps. -GitHub login handles and repository, path, configuration-key, or symbol names -can reveal personal or customer information even though they are not source -bodies. Veripsa treats them as scoped customer data. Commit-message bodies, PR -or issue bodies, source bodies, diff bodies, and configuration or secret value -bodies are not retained. +Names can themselves contain personal or customer information, so Veripsa treats them as scoped customer data. Source bodies, diff bodies, commit-message bodies, PR/issue bodies, and configuration/secret value bodies are not retained. -## Retention and deletion +## Retention classes -| Data class | Retention and deletion | +| Class | Retention | | --- | --- | -| Active content-free working set | Kept while needed to serve an installed repository. Inactive rebuildable working-set and cache rows may be pruned after a period of inactivity, normally 30 days. Purged on uninstall and account erasure. | -| Operational push and landing telemetry | Retained for 30 days, then pruned. | -| Webhook delivery audit | A completed delivery's reduced payload is cleared as soon as processing completes. Terminal completed or failed delivery rows are pruned after 30 days. | -| Advisory history | Kept as an append-only content-free record until account erasure; it is not the live working set. | -| Account-lifecycle audit | Kept as an append-only operational record until account erasure. | - -Uninstalling the GitHub App stops new processing and purges the rebuildable -working set. To delete append-only advisory and lifecycle records as well, -request account erasure at [support@veripsa.com](mailto:support@veripsa.com). - -You can limit or change the repositories in scope from the GitHub App's -installation settings at any time. - -## Canonical policy and security links +| Active content-free working set | Kept while needed for an installed repository; inactive rebuildable rows may normally be pruned after 30 days; purged on uninstall and account erasure. | +| Operational push and landing telemetry | 30 days, then pruned. | +| Webhook delivery audit | Reduced payload cleared after processing; terminal rows pruned after 30 days. | +| Advisory history | Append-only content-free record retained until account erasure. | +| Account-lifecycle audit | Append-only operational record retained until account erasure. | -- [Privacy Policy](https://veripsa.com/privacy) -- [Trust Center and detailed retention table](https://veripsa.com/trust) -- [Security overview](https://veripsa.com/security) -- [Vulnerability disclosure](https://veripsa.com/security/disclosure) +Uninstall stops new processing and purges rebuildable working state. Account erasure also removes append-only advisory and lifecycle records; request it through or `support@veripsa.com`. diff --git a/EVENTS.md b/EVENTS.md index c05634c..000ed97 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -1,175 +1,44 @@ -# GitHub events Veripsa subscribes to +# GitHub permissions and events -The Veripsa GitHub App registers for the events listed below. The -authoritative source is the App manifest; this page is a human-readable -summary of what each event is used for from the **integrator's** point -of view. It does **not** describe the engine's internal processing. - -If this page ever diverges from the App's actual registration, the App -manifest is the truth — please [open an issue](./.github/ISSUE_TEMPLATE/) -and the spec will be corrected. +The GitHub App manifest is the runtime authority; this repository is the versioned public contract for integrators. Report visible drift through the issue templates without including private code. ## Permissions -The App requests the following GitHub permissions (read-only unless -noted): - -| Permission | Level | Why | -|------------------|-------|-----------------------------------------------------| -| `contents` | read | Read repository structure and file text transiently to derive the documented content-free metadata. | -| `merge_queues` | read | Keep merge-group state consistent with merge queues. | -| `pull_requests` | write | Post advisory comments and manage the ack label. | -| `checks` | write | Post advisory check runs on PR head commits. | -| `metadata` | read | Standard GitHub App requirement. | - -Veripsa never requests `contents: write`. It does not push commits, open -PRs, or change branch protection. - -GitHub exposes PR conversations and labels through issue-backed APIs. Veripsa -uses them only for the PR's own managed comment and `veripsa-ack` label; it -does not request the `issues` permission or subscribe to Issue events. - -## Subscribed events - -### `pull_request` - -The primary event. Veripsa reads PR-level structure (head SHA, base ref, -changed files) to maintain its pre-merge view of in-flight work. - -Triggers analysis on: `opened`, `synchronize`, `reopened`, -`ready_for_review`, `converted_to_draft`, `edited` (when the base ref retargets), -`labeled` / `unlabeled` (only when the changed label is `veripsa-ack` — -see [`ACK_LABEL.md`](./ACK_LABEL.md)), and on transitions that retire a -PR from the in-flight set (`closed`, `merged`). - -### `push` - -Used to detect direct-to-branch changes (including branch-only work that -has not yet opened a PR) so the in-flight view stays current. - -### `repository` - -Used to track repository lifecycle (renamed, transferred, archived, -deleted) so Veripsa's view of which repositories it watches stays -consistent. - -### `check_suite` and `check_run` - -Used so Veripsa can observe whether other checks on a PR have completed. -The App posts its **own** check runs on PR head commits (see -[`OUTPUT.md`](./OUTPUT.md)); its own requested suite can also trigger recovery -of a missing check for a fresh PR head. - -### `merge_group` - -Subscribed so Veripsa can stay consistent with repositories that use -GitHub's merge queue feature. Veripsa is **not** a merge queue itself; -this subscription exists only to keep the in-flight view honest when one -is in use. - -## Automatic App lifecycle deliveries - -### `installation` and `installation_repositories` - -GitHub sends these App lifecycle deliveries automatically; they are not extra -Issue or organization-event subscriptions. - -Lifecycle events for the App itself. Veripsa uses these to: - -- onboard newly added repositories, -- release in-flight reservations when a repository is removed or the App - is uninstalled / suspended, -- clean up data when the App is uninstalled (`installation.deleted`). - -## What Veripsa does **not** subscribe to - -Among the events Veripsa explicitly does not request: `issues`, -`issue_comment`, `sub_issues`, `release`, `workflow_run`, `deployment`, -`deployment_status`, `repository_dispatch`, and organization-level events. - -## Payload-shape examples - -Veripsa branches on a small, stable subset of each payload. The -following are the fields integrators most often need to mirror when -writing tools that consume the same webhook stream. - -### `pull_request` - -```json -{ - "action": "opened", - "number": 42, - "pull_request": { - "number": 42, - "head": { "sha": "<40-hex>", "ref": "" }, - "base": { "ref": "main" }, - "draft": false, - "labels": [ { "name": "veripsa-ack" } ] - }, - "repository": { "full_name": "/" }, - "installation": { "id": 12345678 }, - "sender": { "login": "" } -} -``` - -Veripsa keys on `action`, `pull_request.head.sha`, `pull_request.number`, -`pull_request.draft`, `pull_request.labels[].name` (only the -`veripsa-ack` entry), and `repository.full_name`. The `installation.id` -identifies the GitHub App installation that received the event. +| Permission | Level | Public purpose | +| --- | --- | --- | +| `contents` | read | Read repository structure and file/diff text transiently to derive content-free metadata. | +| `merge_queues` | read | Keep merge-group state consistent where GitHub merge queues are used. | +| `pull_requests` | write | Maintain the App's managed PR comment, lazily create the label definition when needed, and react to explicit `veripsa-ack` application/removal. | +| `checks` | write | Create or update Veripsa check runs. | +| `metadata` | read | Standard GitHub App repository metadata. | -### `push` +Veripsa does not request `contents: write`, does not push commits, does not open PRs, and does not change branch protection. -```json -{ - "ref": "refs/heads/", - "before": "<40-hex>", - "after": "<40-hex>", - "repository": { "full_name": "/" }, - "installation": { "id": 12345678 } -} -``` +## Event families -### `installation` / `installation_repositories` +The complete normalized event contract is generated in [`WEBHOOK_EVENTS.md`](./WEBHOOK_EVENTS.md) and validated by [`schemas/webhook-routing-envelope.schema.json`](./schemas/webhook-routing-envelope.schema.json): -```json -{ - "action": "created", - "installation": { - "id": 12345678, - "account": { "login": "", "type": "Organization" } - }, - "repositories_added": [ { "full_name": "/" } ], - "repositories_removed": [] -} -``` +- `pull_request` +- `push` +- `repository` +- `check_suite` +- `check_run` +- `merge_group` +- `installation` +- `installation_repositories` -These examples are abridged — GitHub adds many more fields to each payload. -They are not a sanitizer allow-list: Veripsa retains only the minimized -routing, lifecycle, structural, and output metadata documented in -[`DATA_HANDLING.md`](./DATA_HANDLING.md). +The normalized envelope carries `event` explicitly. It does not ask consumers to infer an event name from payload shape. -## Language and framework coverage +## Raw payload compatibility schema -Coverage varies by repository shape. This public matrix describes capability -levels, not private analysis or scoring mechanics. +[`schemas/webhook-routing-event.schema.json`](./schemas/webhook-routing-event.schema.json) remains at its existing path for v1 compatibility. It validates selected abridged raw bodies only (`pull_request`, `push`, and installation lifecycle). Raw GitHub bodies do not include the `X-GitHub-Event` header, so new integrations should use the normalized envelope instead. -| Coverage tier | Surfaces | -| --- | --- | -| Rich structural | Python, JavaScript/JSX, TypeScript/TSX, Go, Java, Ruby, PHP, C#, Rust, C/C++, Kotlin, Swift, Dart, Elixir, and HTML | -| Component structural | Astro, Vue, and Svelte | -| Stylesheet structural | CSS, SCSS/Sass, Less, and Stylus | -| File-level fallback | Other changed paths remain eligible for direct same-file collision detection. | +For raw pull-request examples, the top-level `number` is canonical. The semantic example test rejects a contradictory nested `pull_request.number`; standard JSON Schema cannot compare two arbitrary sibling values. -Framework and stylesheet tiers also contribute to coverage. This describes -coverage, not private relationship, ranking, or verdict mechanics. When coverage is insufficient, Veripsa returns -`Unknown` rather than a false `Clear`. +## Events not requested -If there is a language you need covered, [open a feature -request](./.github/ISSUE_TEMPLATE/). +Veripsa does not subscribe to Issue, issue-comment, sub-issue, release, workflow-run, deployment, deployment-status, repository-dispatch, or organization-level event families. -## Source +## `check_suite` and `check_run` boundary -This page is derived from the App's registered manifest on GitHub -and the event dispatch in the App's webhook handler. -If you see the App behaving in a way this page does not describe, please -[open an issue](./.github/ISSUE_TEMPLATE/). +These deliveries are used to observe completion/recovery state. Veripsa may retain reduced check/head identifiers and processing outcomes needed operationally. It does not retain other providers' check output bodies as customer content. diff --git a/LICENSE b/LICENSE index c4c834b..87ccbbf 100644 --- a/LICENSE +++ b/LICENSE @@ -19,10 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---- - -Note: this license applies to the contents of this webhook-spec repository -only: the README, per-surface documentation files, JSON Schemas, examples, -TypeScript declarations, and supporting metadata. The Veripsa Core engine -itself is not open-source and is not distributed under this license. diff --git a/OUTPUT.md b/OUTPUT.md index b27f6e5..8bb9854 100644 --- a/OUTPUT.md +++ b/OUTPUT.md @@ -1,284 +1,57 @@ -# What Veripsa posts back +# Observable check and comment output -For every analyzed PR, Veripsa upserts one **check run** on the PR's head -commit, carrying the verdict and a GitHub `conclusion`. When there is -coordination context worth explaining, it may also upsert one managed **PR -comment** with a stable HTML marker. A PR that stays clear from its first -analysis may remain check-only. +## Check fields -Both surfaces are updated in place when present; re-analysis does not append -duplicate checks or managed comments. +A normalized signal maps fields without renaming their source: -> Veripsa does not block merges by itself. Whether a Veripsa check is -> required to merge is entirely controlled by your repository's -> branch-protection rules. +- `name` ← `check_run.name`, exactly `Veripsa`; +- `title` ← `check_run.output.title`, human-facing and allowed to evolve outside stable token text; +- `conclusion` ← `check_run.conclusion`; +- `token` ← the normalization rules generated in [`CHECK_STATES.md`](./CHECK_STATES.md). -## The verdict ladder +Do not look for `Veripsa — ` in `check_run.name`; it belongs to normal human-facing titles. -Veripsa surfaces one of four verdicts on the check run. The exact title -copy after the em-dash may evolve; the **shape** of the ladder is stable -and is what integrators should branch on (via the check title prefix, -not full-string match). +## Check states -The table below is the ladder in its **advisory form**. A minor Heads up is a -variant of Heads up, not a fifth verdict. For a -**material** in-flight collision — a **Wait in line** verdict, or a -**Heads up** verdict with a named in-flight partner PR — the check is -normally posted in the pause-and-acknowledge shape instead -(`Veripsa — Paused (acknowledge to proceed)` / -`Veripsa — Acknowledged`; see -[一時停止 (pause-and-acknowledge)](#一時停止-pause-and-acknowledge) -below). The advisory form of those verdicts is still observable — on -fork PRs, and when the pause state could not be computed for an event — -so integrators should handle both shapes. +[`contract/check-states.json`](./contract/check-states.json) is canonical. It generates the discriminated JSON Schema, TypeScript union, and [`CHECK_STATES.md`](./CHECK_STATES.md). Important invariants include: -| Verdict | Check title prefix | When it appears | GitHub `conclusion` | -|------------------|------------------------------|--------------------------------------------------|---------------------| -| Clear | `Veripsa — Clear` *or* `Veripsa — Clear to land` | Nothing else in flight touches this PR's surface (the `Clear to land` variant signals the same verdict alongside an informational co-signal — e.g. nearby in-flight work that is not a collision). | `success` | -| Heads up | `Veripsa — Heads up` | Cross-PR overlap (advisory form; with a named in-flight partner PR it is material and posts paused — see below). | `neutral` | -| Wait in line | `Veripsa — Wait in line` | Direct collision ahead (advisory form; normally posts paused — see below). | `neutral` | -| Heads up (minor) | `Veripsa — Heads up` | Minor overlap on a build / list file (never pauses). | `neutral` | -| Unknown | `Veripsa — Unknown` | Not enough signal to call it clear. | `neutral` | +- `Clear`, `Clear to land`, `Heading to`, and `Merge queue: clear` use `success`. +- `Paused (acknowledge to proceed)` and `Unresolved merge conflict markers` use `action_required`. +- `Acknowledged` uses `neutral` and requires `acknowledged: true` in the normalized signal. +- `Heading to ` is an honest no-reservation/no-analyzable-surface result. It satisfies a required check but does not approve correctness. -The check title starts with the literal string `Veripsa — `; the token -that follows (`Clear`, `Clear to land`, `Heads up`, `Wait in line`, -`Unknown`, `Paused (acknowledge to proceed)`, `Acknowledged`, -`Unresolved merge conflict markers`) is the machine-readable signal. -The trailing copy after the second em-dash (when present) is -human-facing and may vary. +## Comment availability -Integrators should branch on the prefix tokens up to (but not including) -the second em-dash; treat both `Clear` and `Clear to land` as the -**Clear** verdict. +Veripsa keeps at most one managed coordination comment per PR and updates it in place. -### Normalized check signal +- A completely clean `Clear` result is check-only. +- A clear PR with coordination co-signals uses `Clear to land` and has a managed comment. +- `Heads up` and `Wait in line` normally carry coordination context; a check can still survive when a separate comment post is unavailable. +- `Unknown`, paused, acknowledged, and unresolved-marker states carry explanatory context. +- Operational install/retry/merge-group states do not use the verdict-bearing comment schema. -The package schema/type named `VeripsaCheckRunSignal` is a documented -**normalized view**, not GitHub's raw check-run payload. Map -`check_run.name` to `name`, `check_run.output.title` to `title`, and -`check_run.conclusion` to `conclusion`. Normalize the title to `token` as -follows: +The stable comment marker is: -- For verdict titles beginning `Veripsa — `, take the stable prefix described - above and normalize only its first letter to title case. -- Map `Veripsa is now watching` to `Watching` and - `Veripsa paused — early-access limit reached` to - `Early-access limit reached`. -- Map the documented changed-files, branch-verification, heading, - acknowledgement-verification, and merge-queue titles to their correspondingly - named enum value; dynamic branch text is not part of the `Heading to` token. -- Treat an unknown future title as an unsupported token; branch on the GitHub - `conclusion` and display the full title rather than guessing a verdict. - -## When the `conclusion` changes - -- **`success`** is posted for a **Clear** PR verdict and the - **Merge queue: clear** operational state. -- **`neutral`** is posted for the advisory verdicts, for the - **Acknowledged** pause state, and for the informational states below. -- **`action_required`** is posted in exactly two situations: a material - in-flight collision that has not yet been acknowledged (see - [一時停止 (pause-and-acknowledge)](#一時停止-pause-and-acknowledge)) - and unresolved merge conflict markers (below). - -A `neutral` check is, by GitHub's definition, a check that has completed -and does not gate a merge. An `action_required` check gates the merge -only if your branch-protection policy marks the Veripsa check as -required. The signal you act on lives in the **check title** and, when present, -the managed **PR comment**. - -## Informational (non-verdict) check states - -Veripsa also posts checks for operational states that are not themselves a PR -coordination verdict. Normalize their observable titles to the tokens below: - -- **"Veripsa is now watching"** — posted on first install / first - repository ingest before a PR verdict is available. Token: `Watching`. -- **"Veripsa paused — early-access limit reached"** — posted when the - installation is beyond its plan's coverage and Veripsa is not - analyzing further. This is a visible state, not a silent miss; it is - `neutral`, never `failure`. Token: `Early-access limit reached`. -- **"Veripsa — changed files not read, not analyzed"** — posted when - Veripsa could not read the PR's changed-files list for that event. - Veripsa does not silently fall back to "Clear" in this case; it - surfaces that it did not analyze. Token: `Changed files not read`. -- **"Veripsa — branch state not verified, retrying"** — posted when live - branch state could not be verified safely. Token: `Branch state not verified`. -- **"Veripsa — acknowledgement verification pending"** — preserves an - existing ACK without silently rebinding it while branch state is unavailable. - Token: `Acknowledgement verification pending`. -- **"Veripsa — heading to "** — no in-flight reservation is recorded - for that PR yet. A PR with no analyzable surface (for example a docs-only - change) keeps this state for its whole life — that is the designed - honest-empty outcome, not a stall, and its `success` conclusion satisfies a - required check. Token: `Heading to`; the branch is dynamic display text. -- **"Veripsa — merge queue: clear"**, **"review overlap"**, or **"not - analyzed"** — the observable merge-group states. Tokens: `Merge queue: - clear`, `Merge queue: review overlap`, and `Merge queue: not analyzed`. - -All of these operational states are `neutral` except `Merge queue: clear` -and `Heading to `, which are `success`. - -## Unresolved merge conflict markers - -If the PR's diff adds a paired set of literal git merge conflict marker shapes -(`<<<<<<<` together with `>>>>>>>`) at the start of added lines in one or more -changed files, Veripsa treats it as a likely unresolved conflict and posts a -high-salience result: - -| Check title | GitHub `conclusion` | -|-------------------------------------------------------|---------------------| -| `Veripsa — Unresolved merge conflict markers` | `action_required` | - -There is **no separate second check**: the one-check-per-head-commit -contract (see [Check-run idempotency](#check-run-idempotency)) still -holds. The same check run's conclusion, title, and summary are replaced -with the high-salience lead; any coupling verdict for the same PR remains -visible in the PR comment body, below the conflict-marker block. - -This and the pause-and-acknowledge state (next section) are the only two -situations in which Veripsa emits `action_required`. The detector inspects the -diff transiently, but only the path and marker line number cross the retained or -posted boundary; file and diff bodies are not retained or posted. A literal -pair can also appear intentionally (for example, in documentation), so the -result asks for inspection rather than asserting that the change is wrong. - -## 一時停止 (pause-and-acknowledge) - -Normal Veripsa use requires no acknowledgement step, agent command, copied -merge rule, or extra per-agent setup. When a verdict names a **material** -in-flight collision — **Wait in line**, or **Heads up** with a named in-flight partner PR — -Veripsa posts the check with `conclusion: action_required` until the PR -carries a matching `veripsa-ack` label or the coupling changes. This is a -high-salience GitHub-native signal, not a workflow Veripsa forces: when the -check is not required by repository policy, the PR can still merge without -adding the label. Teams that want an auditable exception can optionally use -the label; see [`ACK_LABEL.md`](./ACK_LABEL.md). - -The check title reshapes around the ack state: - -| State on the PR | Check title prefix | GitHub `conclusion` | -|---------------------------------------------------------------|-----------------------------------------------|---------------------| -| Paused — `veripsa-ack` not yet on the PR | `Veripsa — Paused (acknowledge to proceed)` | `action_required` | -| Acknowledged — `veripsa-ack` is on the PR for this coupling | `Veripsa — Acknowledged` | `neutral` | -| Re-paused — coupling materially changed; prior ack went stale | `Veripsa — Paused (acknowledge to proceed)` | `action_required` | - -On a re-pause, Veripsa also removes the stale `veripsa-ack` label so -the PR visibly reads un-acked again. - -What does **not** pause: - -- **Heads up** with no named in-flight partner (a solo notice), and the - minor build/list-file overlap — these stay plain `neutral`. -- **Unknown** and the informational states — `neutral`. -- **Fork PRs** — never paused (a fork contributor cannot add the base - repository's label); the advisory `neutral` form of the verdict is - posted as-is. - -If the pause state cannot be computed for an event (for example, a -transient GitHub API error), Veripsa fails open: the advisory `neutral` -form of the verdict is posted unchanged, never a spurious pause. - -The `action_required` conclusion signals that a specific collision deserves -attention and has no matching optional acknowledgement. It is **not** Veripsa -asserting that the change is wrong or imposing an ACK ceremony. Whether it -actually blocks the merge is decided by your branch-protection policy (the -Veripsa check must be marked required for `action_required` to gate the merge). - -## The PR comment - -When a verdict needs explanation, Veripsa upserts a managed PR comment with -human-readable context and structured pointers an agent can react to. Clear -or no-reservation results may be check-only. The normalized -`VeripsaPrCommentSurface` schema covers verdict-bearing coordination comments; -operational comments remain human-facing unless separately documented. - -### The comment marker - -Every Veripsa comment **starts** with an invisible HTML marker that -uniquely identifies it as Veripsa's comment for that PR: - -``` +```html ``` -For example, on PR #42 the marker is ``. - -The marker is: +Locate the managed comment by marker, never author display name, timestamp, or sentence copy. -- **Invisible** in GitHub's rendered markdown — it is an HTML comment. -- **Stable** across re-renders of the same PR — `` is the PR - number. -- **Sufficient to locate** Veripsa's comment in the PR's comment thread - programmatically: search the body for the marker substring. +## ACK snapshot marker -> Integrators MUST locate the comment by the marker, not by author -> display name, posting timestamp, or body text. +When pause-and-acknowledge is active, the managed comment carries: -### Comment idempotency - -When a PR needs a comment, Veripsa keeps **at most one** marked comment per PR. - -- The first analysis that needs a comment creates it with the marker prepended. -- Subsequent comment-bearing analyses **edit the existing comment in place** - — same marker, replaced body. -- The marker is preserved on edit; the rest of the body may change - freely between events. -- A PR that stays clear from the start may have no Veripsa PR comment. - -### Where structured info lives in the body - -The comment body is GitHub-flavored markdown intended for humans. A few -conventions agents can rely on: - -- After its `### Veripsa — heading to ...` heading, a coordination comment has - a bold human lead whose verdict prefix matches the check (for example, - `Clear`, `Clear to land`, `Wait in line`, `Heads up`, or `Unknown`). Full - sentence copy and icons may evolve; normalize the prefix to the legacy - `leadingVerdict` values in the schema rather than matching the entire line. - On a **paused** PR the bold lead still carries the - underlying verdict (e.g. **Wait in line**) with the pause instruction - quoted directly beneath it; on an **acknowledged** PR the header is - rewritten to the acknowledged state. -- Cross-PR partners are referenced using stable, content-free refs: - - `PR-` for PRs (e.g. `PR-17`), - - `BR-` for branch-only changes that have not yet opened - a PR. -- File paths are rendered as inline code spans (`` `path/to/file.ext` ``). -- The body **never** contains file contents, diffs, or excerpts of - customer source. Only paths, symbol names, line ranges, PR / branch - refs. - -### The ack-snapshot marker - -When 一時停止 (pause-and-acknowledge) has engaged on a PR — a material -collision was posted — the comment body also carries a second invisible -HTML marker that binds an acknowledgement to the specific coupling it -covers: - -``` - +```html + ``` -This marker is read back by Veripsa on subsequent events to decide -whether an existing `veripsa-ack` label is still valid for the -**current** coupling, or whether the coupling has materially changed -and the ack is stale. Integrators generally do not need to parse this -marker. +The wire format is stable in v1. The 12-hex value is opaque: compare or preserve it only as directed by this contract; do not infer partners, files, scores, or any internal mechanism from it. -## Check-run idempotency +## Content boundary -Veripsa upserts **one** check per (PR head commit, App). Re-analysis of -the same head commit updates the existing check in place rather than -creating a new one. +Comments may identify content-free pointers such as PR refs, branch refs, paths, symbol names, and line ranges. They never contain source bodies, diff bodies, commit-message bodies, PR/issue body text, or secret values. -## What Veripsa does NOT post +## Idempotency -- Veripsa does not open or close PRs. -- Veripsa does not push commits, suggest commits, or open PRs of its - own. -- Veripsa does not post issue comments outside the PR's own comment - thread. -- Veripsa does not post review comments on individual diff lines. -- Veripsa never posts a `failure`, `cancelled`, or `timed_out` - conclusion as part of its verdict surface. +Veripsa upserts one check per `(PR head commit, App)` and at most one marked managed comment per PR. Re-analysis updates existing surfaces rather than appending duplicate managed output. diff --git a/README.md b/README.md index 24769fc..090db27 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,68 @@ # veripsa-webhook-spec -The public integration contract for **Veripsa Core** — the advisory, -content-free GitHub App for pre-merge PR traffic control between open pull -requests. +The versioned public integration contract for **Veripsa Core** — the advisory, content-free GitHub App for pre-merge PR traffic control between open pull requests. It documents only what an integrator can observe: subscribed event families, requested permissions, normalized checks and comments, `veripsa-ack`, and the content-free data boundary. -This repository is the open-source surface that external tools can build against. It defines: +> This repository does not publish Veripsa Core internals, scoring, ranking, or collision heuristics. -- which **GitHub webhook events** the App subscribes to, -- the shape of **what it posts back** — check runs and optional managed PR - comments, -- the semantics of the **`veripsa-ack`** label, -- machine-readable **JSON Schemas**, TypeScript declarations, and examples - for agents, dashboards, log shippers, and security reviewers. +## Stable field boundary -Veripsa lives at [veripsa.com](https://veripsa.com). The App itself is -installed from a free on-ramp; this repo is published separately so -integrators can treat the public output surface as a stable contract. +- `check_run.name` is exactly `Veripsa`. +- `check_run.output.title` is the human-facing title. Normal verdict titles begin with `Veripsa — `, but integrations should normalize them to the stable token in [`CHECK_STATES.md`](./CHECK_STATES.md), not full-string-match evolving sentence copy. +- An unknown future title is unsupported: preserve and display the full title and GitHub `conclusion`; do not guess a known verdict. -> This is the **integration contract**. It is **not** documentation of how -> Veripsa decides anything internally. +## Start here -## Best links for readers - -| Need | Link | +| Need | Source | | --- | --- | -| Product overview | | | Collision walkthrough | | +| Observable truth matrix | [`TRUTH_MATRIX.md`](./TRUTH_MATRIX.md) | +| Check/token/conclusion contract | [`CHECK_STATES.md`](./CHECK_STATES.md) | +| Output and comment normalization | [`OUTPUT.md`](./OUTPUT.md) | +| Event families and fields | [`WEBHOOK_EVENTS.md`](./WEBHOOK_EVENTS.md) | +| Permissions and raw webhook notes | [`EVENTS.md`](./EVENTS.md) | +| ACK label semantics | [`ACK_LABEL.md`](./ACK_LABEL.md) | | Data handling and retention | [`DATA_HANDLING.md`](./DATA_HANDLING.md) | -| Agent usage guide | | -| Recent improvements | | - -## Who this is for - -- Developers building tooling on top of Veripsa output (dashboards, agent - rules, log shippers). -- AI-agent maintainers who want their agent to **read Veripsa's check / - comment / label** and react to it correctly. -- Security and compliance reviewers who want to know exactly which GitHub - permissions and events the App uses. - -## Contract boundary - -- It is **not** a description of Veripsa's internal engine, which - stays private. -- It is **not** an API to call into Veripsa. The App is event-driven over - GitHub webhooks; there is no public REST surface for integrators today. -- It is **not** an SLA. Veripsa is **advisory by default**; branch protection - or rulesets decide whether an `action_required` check conclusion holds a - merge. - -## Current integration scope - -- Veripsa is **content-free by design**: it does not store or display customer - source file bodies or diff contents. This repo documents the public output - contract, not a code-review surface. -- Veripsa coordinates work **within one installed repository** today. - Cross-repository coordination is not a claim made by this spec. -- Veripsa works alongside merge queues, AI reviewers, CI, tests, and human - review. It adds the earlier open-PR collision and landing-order signal before - `main` changes. -- The four base traffic signals are **Clear**, **Heads up**, **Wait in line**, - and **Unknown**. A material coupling may add **Paused (acknowledge to - proceed)** as a control overlay; Paused is not a fifth traffic verdict. - -## Repository map - -- [`DATA_HANDLING.md`](./DATA_HANDLING.md) — what GitHub data can reach the - service, what content-free metadata may be retained, and how retention, - uninstall, and account erasure differ. -- [`EVENTS.md`](./EVENTS.md) — the GitHub webhook events Veripsa - subscribes to, the GitHub permissions it requests, the events it - explicitly does **not** subscribe to, and a high-level note on - language coverage. -- [`OUTPUT.md`](./OUTPUT.md) — the four base traffic signals, material-coupling - Paused/Acknowledged controls, GitHub `conclusion` mapping, PR-comment marker - convention, and idempotent-upsert contract. -- [`ACK_LABEL.md`](./ACK_LABEL.md) — the `veripsa-ack` label: what adding - it means, what removing it means, who can add it, and the stale-ack rule. -- [`schemas/`](./schemas/) — JSON Schemas for the public contract surfaces. -- [`types/index.d.ts`](./types/index.d.ts) — TypeScript declarations for - integrators that want to branch on Veripsa's stable tokens. -- [`examples/`](./examples/) — copy-pasteable payload and comment examples. - -## Consuming the spec - -The Markdown files are the human-readable source of truth. The schema and -type files mirror the same public surface for tool authors. - -The check-run and coordination-comment assets are normalized views documented -in [`OUTPUT.md`](./OUTPUT.md). The webhook-routing schema instead validates the -abridged raw GitHub bodies shown in [`EVENTS.md`](./EVENTS.md); the TypeScript -routing interface is a flattened normalized view. - -This repository includes `package.json` metadata so the assets can be -consumed as a package or published to a registry without changing paths. -Until a registry release exists, use GitHub paths directly or vendor the -specific schema files you need. - -Stable entry points: +| Compatibility and migration | [`COMPATIBILITY.md`](./COMPATIBILITY.md) | + +## Machine-readable entry points ```text +contract/check-states.json +contract/webhook-events.json schemas/check-run-signal.schema.json schemas/pr-comment-surface.schema.json -schemas/webhook-routing-event.schema.json +schemas/webhook-routing-envelope.schema.json +schemas/webhook-routing-event.schema.json # legacy selected raw bodies schemas/ack-label.schema.json types/index.d.ts ``` -## Compatibility policy +The two files under `contract/` are the canonical machine sources. Generated Schema, declaration, and Markdown files are checked for drift in CI. -Integrators may rely on: +## Version and consumption -- the literal check-run name prefix `Veripsa — `, -- the verdict / state tokens listed in [`OUTPUT.md`](./OUTPUT.md), -- the PR comment marker shape ``, -- the ack-snapshot marker prefix ``. | +| ACK snapshot | ``; stable wire shape, opaque value. | +| ACK meaning | Seen this specific coupling and proceeding; not review/correctness/resolution. | +| Label ownership | Veripsa may create the repository label definition; only a human/agent explicitly applies it to a PR. | +| Event families | Eight discriminated families in `contract/webhook-events.json`. | +| Permissions | `contents:read`, `merge_queues:read`, `pull_requests:write`, `checks:write`, `metadata:read`. | +| Retained | Reduced content-free identifiers, names, paths/symbols/relationships, working/advisory/lifecycle and delivery metadata. | +| Transient-only | Raw webhook body, source/diff bodies, commit/PR/issue body text, configuration values, unrelated webhook fields. | +| Retention | Working/telemetry/delivery data is short-lived; advisory/lifecycle audit remains until account erasure. | +| Support | ; best-effort target, not a contractual SLA. | +| Security | Email `support@veripsa.com` with subject prefix `[SECURITY]`; canonical process at . | +| Fair-use limit | No automatic charge or paid-plan move; affected analysis surfaces an honest non-clear state rather than silently claiming safety. | +| Product scope | Advisory by default, content-free, within one installed repository; not merge queue, AI reviewer, or correctness checker. | diff --git a/WEBHOOK_EVENTS.md b/WEBHOOK_EVENTS.md new file mode 100644 index 0000000..428f097 --- /dev/null +++ b/WEBHOOK_EVENTS.md @@ -0,0 +1,14 @@ +# Webhook event contract + +Generated from `contract/webhook-events.json` for contract **v1.0.0**. This table describes the reduced normalized routing envelope, not the raw GitHub body. + +| Event | Required normalized fields | Optional fields | Used for | Retained boundary | Transient-only boundary | +| --- | --- | --- | --- | --- | --- | +| `pull_request` | `action`, `installationId`, `repositoryFullName`, `prNumber`, `headSha`, `baseRef` | `branchRef` | PR lifecycle, head/base identity, changed-file routing, and exact veripsa-ack label transitions. | Reduced identifiers, refs, labels, paths, processing state, and timestamps as documented in DATA_HANDLING.md. | Original webhook body, PR/source/diff bodies, and fields outside the reduced routing surface. | +| `push` | `installationId`, `repositoryFullName`, `branchRef`, `headSha` | `action` | Direct-to-branch and branch-only work tracking. | Reduced repository/ref/commit and processing metadata. | Original webhook body and commit/file bodies. | +| `repository` | `action`, `installationId`, `repositoryFullName` | — | Rename, transfer, archive, and deletion lifecycle consistency. | Reduced repository and lifecycle identifiers/status. | Original webhook body and unrelated repository fields. | +| `check_suite` | `action`, `installationId`, `repositoryFullName` | `headSha` | Observe other check completion and recover a missing fresh-head Veripsa check. | Reduced check/head identifiers and processing outcome when needed operationally. | Check output bodies and unrelated suite payload fields. | +| `check_run` | `action`, `installationId`, `repositoryFullName` | `headSha` | Observe other check completion and the App's own requested-check lifecycle. | Reduced check/head identifiers and processing outcome when needed operationally. | Other providers' check output text and unrelated payload fields. | +| `merge_group` | `action`, `installationId`, `repositoryFullName`, `headSha`, `baseRef` | — | Keep the in-flight view consistent with GitHub merge queues. | Reduced merge-group, repository, ref, head, and processing metadata. | Original webhook body and unrelated merge-group fields. | +| `installation` | `action`, `installationId`, `accountLogin`, `accountType` | — | Install, suspend, unsuspend, and uninstall lifecycle. | Installation/account identifiers and lifecycle audit metadata. | Original webhook body and unrelated account fields. | +| `installation_repositories` | `action`, `installationId`, `accountLogin`, `accountType`, `repositoriesAdded`, `repositoriesRemoved` | — | Add or remove repositories from an installation. | Installation/account/repository identifiers and lifecycle processing state. | Original webhook body and unrelated repository fields. | diff --git a/contract/check-states.json b/contract/check-states.json new file mode 100644 index 0000000..d386c94 --- /dev/null +++ b/contract/check-states.json @@ -0,0 +1,212 @@ +{ + "contractVersion": "1.0.0", + "checkName": "Veripsa", + "titleField": "check_run.output.title", + "states": [ + { + "token": "Clear", + "kind": "verdict", + "titlePattern": "^Veripsa — Clear(?: — .+)?$", + "titleExample": "Veripsa — Clear", + "conclusion": "success", + "underlyingVerdicts": [ + "clear" + ], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Clear to land", + "kind": "verdict", + "titlePattern": "^Veripsa — Clear to land(?: — .+)?$", + "titleExample": "Veripsa — Clear to land", + "conclusion": "success", + "underlyingVerdicts": [ + "clear" + ], + "acknowledged": "forbidden", + "commentAvailability": "required", + "stability": "stable" + }, + { + "token": "Heads up", + "kind": "verdict", + "titlePattern": "^Veripsa — Heads up(?: — .+)?$", + "titleExample": "Veripsa — Heads up", + "conclusion": "neutral", + "underlyingVerdicts": [ + "heads_up" + ], + "acknowledged": "forbidden", + "commentAvailability": "when_coordination_context_exists", + "stability": "stable" + }, + { + "token": "Wait in line", + "kind": "verdict", + "titlePattern": "^Veripsa — Wait in line(?: — .+)?$", + "titleExample": "Veripsa — Wait in line", + "conclusion": "neutral", + "underlyingVerdicts": [ + "wait_in_line" + ], + "acknowledged": "forbidden", + "commentAvailability": "when_coordination_context_exists", + "stability": "stable" + }, + { + "token": "Unknown", + "kind": "verdict", + "titlePattern": "^Veripsa — Unknown(?: — .+)?$", + "titleExample": "Veripsa — Unknown", + "conclusion": "neutral", + "underlyingVerdicts": [ + "unknown" + ], + "acknowledged": "forbidden", + "commentAvailability": "required", + "stability": "stable" + }, + { + "token": "Paused (acknowledge to proceed)", + "kind": "pause", + "titlePattern": "^Veripsa — Paused \\(acknowledge to proceed\\)(?: — .+)?$", + "titleExample": "Veripsa — Paused (acknowledge to proceed)", + "conclusion": "action_required", + "underlyingVerdicts": [ + "heads_up", + "wait_in_line" + ], + "acknowledged": "false", + "commentAvailability": "required", + "stability": "stable" + }, + { + "token": "Acknowledged", + "kind": "pause", + "titlePattern": "^Veripsa — Acknowledged(?: — .+)?$", + "titleExample": "Veripsa — Acknowledged", + "conclusion": "neutral", + "underlyingVerdicts": [ + "heads_up", + "wait_in_line" + ], + "acknowledged": "true", + "commentAvailability": "required", + "stability": "stable" + }, + { + "token": "Unresolved merge conflict markers", + "kind": "high_salience", + "titlePattern": "^Veripsa — Unresolved merge conflict markers$", + "titleExample": "Veripsa — Unresolved merge conflict markers", + "conclusion": "action_required", + "underlyingVerdicts": [ + "merge_conflict_markers" + ], + "acknowledged": "forbidden", + "commentAvailability": "required", + "stability": "stable" + }, + { + "token": "Acknowledgement verification pending", + "kind": "operational", + "titlePattern": "^Veripsa — acknowledgement verification pending$", + "titleExample": "Veripsa — acknowledgement verification pending", + "conclusion": "neutral", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Heading to", + "kind": "operational", + "titlePattern": "^Veripsa — heading to .+$", + "titleExample": "Veripsa — heading to main", + "conclusion": "success", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Watching", + "kind": "operational", + "titlePattern": "^Veripsa is now watching$", + "titleExample": "Veripsa is now watching", + "conclusion": "neutral", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Early-access limit reached", + "kind": "operational", + "titlePattern": "^Veripsa paused — early-access limit reached$", + "titleExample": "Veripsa paused — early-access limit reached", + "conclusion": "neutral", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Changed files not read", + "kind": "operational", + "titlePattern": "^Veripsa — changed files not read, not analyzed$", + "titleExample": "Veripsa — changed files not read, not analyzed", + "conclusion": "neutral", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Branch state not verified", + "kind": "operational", + "titlePattern": "^Veripsa — branch state not verified, retrying$", + "titleExample": "Veripsa — branch state not verified, retrying", + "conclusion": "neutral", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Merge queue: clear", + "kind": "merge_queue", + "titlePattern": "^Veripsa — merge queue: clear$", + "titleExample": "Veripsa — merge queue: clear", + "conclusion": "success", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Merge queue: review overlap", + "kind": "merge_queue", + "titlePattern": "^Veripsa — merge queue: review overlap$", + "titleExample": "Veripsa — merge queue: review overlap", + "conclusion": "neutral", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + }, + { + "token": "Merge queue: not analyzed", + "kind": "merge_queue", + "titlePattern": "^Veripsa — merge queue: not analyzed$", + "titleExample": "Veripsa — merge queue: not analyzed", + "conclusion": "neutral", + "underlyingVerdicts": [], + "acknowledged": "forbidden", + "commentAvailability": "never", + "stability": "stable" + } + ] +} diff --git a/contract/webhook-events.json b/contract/webhook-events.json new file mode 100644 index 0000000..5bbed0f --- /dev/null +++ b/contract/webhook-events.json @@ -0,0 +1,119 @@ +{ + "contractVersion": "1.0.0", + "events": [ + { + "event": "pull_request", + "required": [ + "action", + "installationId", + "repositoryFullName", + "prNumber", + "headSha", + "baseRef" + ], + "optional": [ + "branchRef" + ], + "uses": "PR lifecycle, head/base identity, changed-file routing, and exact veripsa-ack label transitions.", + "retained": "Reduced identifiers, refs, labels, paths, processing state, and timestamps as documented in DATA_HANDLING.md.", + "transient": "Original webhook body, PR/source/diff bodies, and fields outside the reduced routing surface." + }, + { + "event": "push", + "required": [ + "installationId", + "repositoryFullName", + "branchRef", + "headSha" + ], + "optional": [ + "action" + ], + "uses": "Direct-to-branch and branch-only work tracking.", + "retained": "Reduced repository/ref/commit and processing metadata.", + "transient": "Original webhook body and commit/file bodies." + }, + { + "event": "repository", + "required": [ + "action", + "installationId", + "repositoryFullName" + ], + "optional": [], + "uses": "Rename, transfer, archive, and deletion lifecycle consistency.", + "retained": "Reduced repository and lifecycle identifiers/status.", + "transient": "Original webhook body and unrelated repository fields." + }, + { + "event": "check_suite", + "required": [ + "action", + "installationId", + "repositoryFullName" + ], + "optional": [ + "headSha" + ], + "uses": "Observe other check completion and recover a missing fresh-head Veripsa check.", + "retained": "Reduced check/head identifiers and processing outcome when needed operationally.", + "transient": "Check output bodies and unrelated suite payload fields." + }, + { + "event": "check_run", + "required": [ + "action", + "installationId", + "repositoryFullName" + ], + "optional": [ + "headSha" + ], + "uses": "Observe other check completion and the App's own requested-check lifecycle.", + "retained": "Reduced check/head identifiers and processing outcome when needed operationally.", + "transient": "Other providers' check output text and unrelated payload fields." + }, + { + "event": "merge_group", + "required": [ + "action", + "installationId", + "repositoryFullName", + "headSha", + "baseRef" + ], + "optional": [], + "uses": "Keep the in-flight view consistent with GitHub merge queues.", + "retained": "Reduced merge-group, repository, ref, head, and processing metadata.", + "transient": "Original webhook body and unrelated merge-group fields." + }, + { + "event": "installation", + "required": [ + "action", + "installationId", + "accountLogin", + "accountType" + ], + "optional": [], + "uses": "Install, suspend, unsuspend, and uninstall lifecycle.", + "retained": "Installation/account identifiers and lifecycle audit metadata.", + "transient": "Original webhook body and unrelated account fields." + }, + { + "event": "installation_repositories", + "required": [ + "action", + "installationId", + "accountLogin", + "accountType", + "repositoriesAdded", + "repositoriesRemoved" + ], + "optional": [], + "uses": "Add or remove repositories from an installation.", + "retained": "Installation/account/repository identifiers and lifecycle processing state.", + "transient": "Original webhook body and unrelated repository fields." + } + ] +} diff --git a/examples/ack-label.json b/examples/ack-label.json new file mode 100644 index 0000000..18efbab --- /dev/null +++ b/examples/ack-label.json @@ -0,0 +1,8 @@ +{ + "name": "veripsa-ack", + "meaning": "seen_and_proceeding", + "repositoryDefinitionCreatedBy": "veripsa_when_needed", + "appliedToPullRequestBy": "human_or_agent", + "effectWhenPaused": "action_required_to_neutral", + "staleWhenMaterialCouplingChanges": true +} diff --git a/examples/check-run-acknowledged.json b/examples/check-run-acknowledged.json new file mode 100644 index 0000000..6fd53f2 --- /dev/null +++ b/examples/check-run-acknowledged.json @@ -0,0 +1,11 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Acknowledged", + "token": "Acknowledged", + "conclusion": "neutral", + "underlyingVerdict": "heads_up", + "acknowledged": true, + "prNumber": 43, + "repositoryFullName": "example/repo", + "headSha": "89abcdef0123456789abcdef0123456789abcdef" +} diff --git a/examples/check-run-clear-to-land.json b/examples/check-run-clear-to-land.json new file mode 100644 index 0000000..127f8ba --- /dev/null +++ b/examples/check-run-clear-to-land.json @@ -0,0 +1,10 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Clear to land", + "token": "Clear to land", + "conclusion": "success", + "underlyingVerdict": "clear", + "prNumber": 42, + "repositoryFullName": "example/repo", + "headSha": "0123456789abcdef0123456789abcdef01234567" +} diff --git a/examples/check-run-clear.json b/examples/check-run-clear.json index 94cf1e4..6841fc6 100644 --- a/examples/check-run-clear.json +++ b/examples/check-run-clear.json @@ -1,11 +1,10 @@ { "name": "Veripsa", - "title": "Veripsa — Clear to land", - "token": "Clear to land", + "title": "Veripsa — Clear", + "token": "Clear", "conclusion": "success", "underlyingVerdict": "clear", "prNumber": 42, "repositoryFullName": "example/repo", - "headSha": "0123456789abcdef0123456789abcdef01234567", - "acknowledged": false + "headSha": "0123456789abcdef0123456789abcdef01234567" } diff --git a/examples/check-run-heading-to.json b/examples/check-run-heading-to.json new file mode 100644 index 0000000..ad64947 --- /dev/null +++ b/examples/check-run-heading-to.json @@ -0,0 +1,9 @@ +{ + "name": "Veripsa", + "title": "Veripsa — heading to main", + "token": "Heading to", + "conclusion": "success", + "prNumber": 44, + "repositoryFullName": "example/repo", + "headSha": "1111111111111111111111111111111111111111" +} diff --git a/examples/check-run-paused.json b/examples/check-run-paused.json index a5438ae..b192725 100644 --- a/examples/check-run-paused.json +++ b/examples/check-run-paused.json @@ -4,8 +4,8 @@ "token": "Paused (acknowledge to proceed)", "conclusion": "action_required", "underlyingVerdict": "wait_in_line", + "acknowledged": false, "prNumber": 43, "repositoryFullName": "example/repo", - "headSha": "89abcdef0123456789abcdef0123456789abcdef", - "acknowledged": false + "headSha": "89abcdef0123456789abcdef0123456789abcdef" } diff --git a/examples/invalid/check-ack-action-required.json b/examples/invalid/check-ack-action-required.json new file mode 100644 index 0000000..f855aa8 --- /dev/null +++ b/examples/invalid/check-ack-action-required.json @@ -0,0 +1,8 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Acknowledged", + "token": "Acknowledged", + "conclusion": "action_required", + "underlyingVerdict": "heads_up", + "acknowledged": true +} diff --git a/examples/invalid/check-ack-false.json b/examples/invalid/check-ack-false.json new file mode 100644 index 0000000..9a6894c --- /dev/null +++ b/examples/invalid/check-ack-false.json @@ -0,0 +1,8 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Acknowledged", + "token": "Acknowledged", + "conclusion": "neutral", + "underlyingVerdict": "heads_up", + "acknowledged": false +} diff --git a/examples/invalid/check-clear-neutral.json b/examples/invalid/check-clear-neutral.json new file mode 100644 index 0000000..29bd0e0 --- /dev/null +++ b/examples/invalid/check-clear-neutral.json @@ -0,0 +1,7 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Clear", + "token": "Clear", + "conclusion": "neutral", + "underlyingVerdict": "clear" +} diff --git a/examples/invalid/check-heading-neutral.json b/examples/invalid/check-heading-neutral.json new file mode 100644 index 0000000..dad1ea5 --- /dev/null +++ b/examples/invalid/check-heading-neutral.json @@ -0,0 +1,6 @@ +{ + "name": "Veripsa", + "title": "Veripsa — heading to main", + "token": "Heading to", + "conclusion": "neutral" +} diff --git a/examples/invalid/check-markers-success.json b/examples/invalid/check-markers-success.json new file mode 100644 index 0000000..bed2f66 --- /dev/null +++ b/examples/invalid/check-markers-success.json @@ -0,0 +1,7 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Unresolved merge conflict markers", + "token": "Unresolved merge conflict markers", + "conclusion": "success", + "underlyingVerdict": "merge_conflict_markers" +} diff --git a/examples/invalid/check-paused-ack-true.json b/examples/invalid/check-paused-ack-true.json new file mode 100644 index 0000000..34d2184 --- /dev/null +++ b/examples/invalid/check-paused-ack-true.json @@ -0,0 +1,8 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Paused (acknowledge to proceed)", + "token": "Paused (acknowledge to proceed)", + "conclusion": "action_required", + "underlyingVerdict": "wait_in_line", + "acknowledged": true +} diff --git a/examples/invalid/check-paused-neutral.json b/examples/invalid/check-paused-neutral.json new file mode 100644 index 0000000..dc1c884 --- /dev/null +++ b/examples/invalid/check-paused-neutral.json @@ -0,0 +1,8 @@ +{ + "name": "Veripsa", + "title": "Veripsa — Paused (acknowledge to proceed)", + "token": "Paused (acknowledge to proceed)", + "conclusion": "neutral", + "underlyingVerdict": "wait_in_line", + "acknowledged": false +} diff --git a/examples/invalid/raw-pr-number-mismatch.json b/examples/invalid/raw-pr-number-mismatch.json new file mode 100644 index 0000000..571e8fe --- /dev/null +++ b/examples/invalid/raw-pr-number-mismatch.json @@ -0,0 +1,21 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 43, + "head": { + "sha": "0123456789abcdef0123456789abcdef01234567", + "ref": "x" + }, + "base": { + "ref": "main" + }, + "draft": false + }, + "repository": { + "full_name": "example/repo" + }, + "installation": { + "id": 1 + } +} diff --git a/examples/manifest.json b/examples/manifest.json new file mode 100644 index 0000000..fd0f7eb --- /dev/null +++ b/examples/manifest.json @@ -0,0 +1,102 @@ +[ + { + "file": "examples/check-run-clear.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": true + }, + { + "file": "examples/check-run-clear-to-land.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": true + }, + { + "file": "examples/check-run-heading-to.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": true + }, + { + "file": "examples/check-run-paused.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": true + }, + { + "file": "examples/check-run-acknowledged.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": true + }, + { + "file": "examples/pr-comment-surface.json", + "schema": "schemas/pr-comment-surface.schema.json", + "valid": true + }, + { + "file": "examples/ack-label.json", + "schema": "schemas/ack-label.schema.json", + "valid": true + }, + { + "file": "examples/webhook-envelope-pull-request.json", + "schema": "schemas/webhook-routing-envelope.schema.json", + "valid": true + }, + { + "file": "examples/webhook-envelope-check-run.json", + "schema": "schemas/webhook-routing-envelope.schema.json", + "valid": true + }, + { + "file": "examples/webhook-envelope-installation-repositories.json", + "schema": "schemas/webhook-routing-envelope.schema.json", + "valid": true + }, + { + "file": "examples/raw-pull-request-opened.json", + "schema": "schemas/webhook-routing-event.schema.json", + "valid": true + }, + { + "file": "examples/pull-request-opened-routing-event.json", + "schema": "schemas/webhook-routing-event.schema.json", + "valid": true + }, + { + "file": "examples/invalid/check-clear-neutral.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": false + }, + { + "file": "examples/invalid/check-heading-neutral.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": false + }, + { + "file": "examples/invalid/check-paused-neutral.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": false + }, + { + "file": "examples/invalid/check-ack-action-required.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": false + }, + { + "file": "examples/invalid/check-markers-success.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": false + }, + { + "file": "examples/invalid/check-paused-ack-true.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": false + }, + { + "file": "examples/invalid/check-ack-false.json", + "schema": "schemas/check-run-signal.schema.json", + "valid": false + }, + { + "file": "examples/invalid/raw-pr-number-mismatch.json", + "schema": "schemas/webhook-routing-event.schema.json", + "valid": false + } +] diff --git a/examples/pr-comment-surface.json b/examples/pr-comment-surface.json index 2fdb7d2..46b83b1 100644 --- a/examples/pr-comment-surface.json +++ b/examples/pr-comment-surface.json @@ -1,13 +1,12 @@ { "marker": "", "ackSnapshotMarker": "", - "leadingVerdict": "Wait in line.", + "leadingVerdict": "Acknowledged.", "partnerRefs": [ "PR-42" ], "branchRefs": [], "paths": [ - "orders/pricing.py", - "tests/test_pricing.py" + "orders/pricing.py" ] } diff --git a/examples/raw-pull-request-opened.json b/examples/raw-pull-request-opened.json new file mode 100644 index 0000000..e776fe6 --- /dev/null +++ b/examples/raw-pull-request-opened.json @@ -0,0 +1,22 @@ +{ + "action": "opened", + "number": 42, + "pull_request": { + "number": 42, + "head": { + "sha": "0123456789abcdef0123456789abcdef01234567", + "ref": "agent/pricing-a" + }, + "base": { + "ref": "main" + }, + "draft": false, + "labels": [] + }, + "repository": { + "full_name": "example/repo" + }, + "installation": { + "id": 12345678 + } +} diff --git a/examples/webhook-envelope-check-run.json b/examples/webhook-envelope-check-run.json new file mode 100644 index 0000000..f921e4b --- /dev/null +++ b/examples/webhook-envelope-check-run.json @@ -0,0 +1,7 @@ +{ + "event": "check_run", + "action": "completed", + "installationId": 12345678, + "repositoryFullName": "example/repo", + "headSha": "0123456789abcdef0123456789abcdef01234567" +} diff --git a/examples/webhook-envelope-installation-repositories.json b/examples/webhook-envelope-installation-repositories.json new file mode 100644 index 0000000..dff7c67 --- /dev/null +++ b/examples/webhook-envelope-installation-repositories.json @@ -0,0 +1,11 @@ +{ + "event": "installation_repositories", + "action": "added", + "installationId": 12345678, + "accountLogin": "example-org", + "accountType": "Organization", + "repositoriesAdded": [ + "example/repo" + ], + "repositoriesRemoved": [] +} diff --git a/examples/webhook-envelope-pull-request.json b/examples/webhook-envelope-pull-request.json new file mode 100644 index 0000000..586c1c8 --- /dev/null +++ b/examples/webhook-envelope-pull-request.json @@ -0,0 +1,10 @@ +{ + "event": "pull_request", + "action": "opened", + "installationId": 12345678, + "repositoryFullName": "example/repo", + "prNumber": 42, + "headSha": "0123456789abcdef0123456789abcdef01234567", + "baseRef": "main", + "branchRef": "agent/pricing-a" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7804af5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,34 @@ +{ + "name": "@veripsa/webhook-spec", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@veripsa/webhook-spec", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "5.8.3" + }, + "engines": { + "node": ">=20", + "npm": ">=10" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json index e823257..9bb7477 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@veripsa/webhook-spec", - "version": "0.0.0", - "description": "Public integration contract, JSON Schemas, examples, and TypeScript declarations for the Veripsa GitHub App surface.", + "version": "1.0.0", + "description": "Versioned public integration contract, JSON Schemas, examples, and TypeScript declarations for the Veripsa GitHub App surface.", "license": "MIT", "type": "module", "sideEffects": false, @@ -25,23 +25,47 @@ ], "files": [ "README.md", + "CHANGELOG.md", + "COMPATIBILITY.md", + "TRUTH_MATRIX.md", + "CHECK_STATES.md", + "WEBHOOK_EVENTS.md", "DATA_HANDLING.md", "EVENTS.md", "OUTPUT.md", "ACK_LABEL.md", + "contract/", "schemas/", "examples/", "types/" ], "exports": { + "./contract/check-states": "./contract/check-states.json", + "./contract/webhook-events": "./contract/webhook-events.json", "./schemas/check-run-signal": "./schemas/check-run-signal.schema.json", "./schemas/pr-comment-surface": "./schemas/pr-comment-surface.schema.json", + "./schemas/webhook-routing-envelope": "./schemas/webhook-routing-envelope.schema.json", "./schemas/webhook-routing-event": "./schemas/webhook-routing-event.schema.json", "./schemas/ack-label": "./schemas/ack-label.schema.json", "./types": "./types/index.d.ts" }, "types": "./types/index.d.ts", "scripts": { - "check:json": "node scripts/check-json.mjs" + "generate": "node scripts/generate-contract.mjs && node scripts/generate-events.mjs", + "check:json": "node scripts/check-json.mjs", + "check:schema": "python3 scripts/check-schema.py", + "check:examples": "python3 scripts/check-examples.py", + "check:types": "node scripts/check-types.mjs", + "check:contract": "node scripts/generate-contract.mjs --check && node scripts/generate-events.mjs --check && node scripts/check-contract.mjs", + "check:links": "python3 scripts/check-links.py", + "check": "npm run check:json && npm run check:schema && npm run check:examples && npm run check:types && npm run check:contract && npm run check:links", + "pack:dry-run": "npm pack --dry-run" + }, + "engines": { + "node": ">=20", + "npm": ">=10" + }, + "devDependencies": { + "typescript": "5.8.3" } } diff --git a/schemas/ack-label.schema.json b/schemas/ack-label.schema.json index cfe0fc2..a12c32a 100644 --- a/schemas/ack-label.schema.json +++ b/schemas/ack-label.schema.json @@ -1,32 +1,16 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/schemas/ack-label.schema.json", + "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/raw/v1.0.0/schemas/ack-label.schema.json", "title": "Veripsa ack label contract", - "description": "Stable public semantics of the single label Veripsa reacts to.", "type": "object", "additionalProperties": false, - "required": [ - "name", - "meaning", - "appliedBy", - "effectWhenPaused", - "staleWhenMaterialCouplingChanges" - ], + "required": ["name", "meaning", "repositoryDefinitionCreatedBy", "appliedToPullRequestBy", "effectWhenPaused", "staleWhenMaterialCouplingChanges"], "properties": { - "name": { - "const": "veripsa-ack" - }, - "meaning": { - "const": "seen_and_proceeding" - }, - "appliedBy": { - "const": "human_or_agent" - }, - "effectWhenPaused": { - "const": "action_required_to_neutral" - }, - "staleWhenMaterialCouplingChanges": { - "const": true - } + "name": {"const": "veripsa-ack"}, + "meaning": {"const": "seen_and_proceeding"}, + "repositoryDefinitionCreatedBy": {"const": "veripsa_when_needed"}, + "appliedToPullRequestBy": {"const": "human_or_agent"}, + "effectWhenPaused": {"const": "action_required_to_neutral"}, + "staleWhenMaterialCouplingChanges": {"const": true} } } diff --git a/schemas/check-run-signal.schema.json b/schemas/check-run-signal.schema.json index a9b5a9c..c0d939a 100644 --- a/schemas/check-run-signal.schema.json +++ b/schemas/check-run-signal.schema.json @@ -1,173 +1,680 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/schemas/check-run-signal.schema.json", + "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/raw/v1.0.0/schemas/check-run-signal.schema.json", "title": "Veripsa normalized check-run signal", - "description": "A normalized view derived from a GitHub check run. name comes from check_run.name, title from check_run.output.title, conclusion from check_run.conclusion, and token is parsed using OUTPUT.md. This is not the raw GitHub payload.", - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "title", - "token", - "conclusion" - ], - "properties": { - "name": { - "const": "Veripsa" - }, - "title": { - "type": "string", - "description": "Full human-facing check title. Branch on token, not the entire title string." - }, - "token": { - "type": "string", - "enum": [ - "Clear", - "Clear to land", - "Heads up", - "Wait in line", - "Unknown", - "Paused (acknowledge to proceed)", - "Acknowledged", - "Unresolved merge conflict markers", - "Acknowledgement verification pending", - "Heading to", - "Watching", - "Early-access limit reached", - "Changed files not read", - "Branch state not verified", - "Merge queue: clear", - "Merge queue: review overlap", - "Merge queue: not analyzed" - ] - }, - "conclusion": { - "type": "string", - "enum": [ - "success", - "neutral", - "action_required" - ] - }, - "underlyingVerdict": { - "type": "string", - "enum": [ - "clear", - "heads_up", - "wait_in_line", - "unknown", - "merge_conflict_markers" - ] - }, - "prNumber": { - "type": "integer", - "minimum": 1 - }, - "repositoryFullName": { - "type": "string", - "pattern": "^[^/\\s]+/[^/\\s]+$" - }, - "headSha": { - "type": "string", - "pattern": "^[0-9a-f]{40}$" - }, - "acknowledged": { - "type": "boolean" - } - }, - "allOf": [ - { - "if": { - "properties": { - "token": { - "enum": [ - "Clear", - "Clear to land" - ] - } - }, - "required": [ - "token" - ] - }, - "then": { - "properties": { - "conclusion": { - "const": "success" - } - } - } - }, - { - "if": { - "properties": { - "token": { - "enum": [ - "Paused (acknowledge to proceed)", - "Unresolved merge conflict markers" - ] - } - }, - "required": [ - "token" - ] - }, - "then": { - "properties": { - "conclusion": { - "const": "action_required" - } - } - } - }, - { - "if": { - "properties": { - "token": { - "enum": [ - "Heads up", - "Wait in line", - "Unknown", - "Acknowledged", - "Acknowledgement verification pending", - "Heading to", - "Watching", - "Early-access limit reached", - "Changed files not read", - "Branch state not verified", - "Merge queue: review overlap", - "Merge queue: not analyzed" - ] - } - }, - "required": [ - "token" - ] - }, - "then": { - "properties": { - "conclusion": { - "const": "neutral" - } - } - } - }, - { - "if": { - "properties": { - "token": { - "const": "Merge queue: clear" - } - }, - "required": [ - "token" - ] - }, - "then": { - "properties": { - "conclusion": { - "const": "success" - } + "description": "Normalized public signal derived from check_run.name, check_run.output.title, and check_run.conclusion. The oneOf branches reject impossible token/conclusion/underlying-verdict/ack combinations.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Clear(?: — .+)?$" + }, + "token": { + "const": "Clear" + }, + "conclusion": { + "const": "success" + }, + "underlyingVerdict": { + "const": "clear" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Clear to land(?: — .+)?$" + }, + "token": { + "const": "Clear to land" + }, + "conclusion": { + "const": "success" + }, + "underlyingVerdict": { + "const": "clear" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Heads up(?: — .+)?$" + }, + "token": { + "const": "Heads up" + }, + "conclusion": { + "const": "neutral" + }, + "underlyingVerdict": { + "const": "heads_up" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Wait in line(?: — .+)?$" + }, + "token": { + "const": "Wait in line" + }, + "conclusion": { + "const": "neutral" + }, + "underlyingVerdict": { + "const": "wait_in_line" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Unknown(?: — .+)?$" + }, + "token": { + "const": "Unknown" + }, + "conclusion": { + "const": "neutral" + }, + "underlyingVerdict": { + "const": "unknown" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Paused \\(acknowledge to proceed\\)(?: — .+)?$" + }, + "token": { + "const": "Paused (acknowledge to proceed)" + }, + "conclusion": { + "const": "action_required" + }, + "underlyingVerdict": { + "enum": [ + "heads_up", + "wait_in_line" + ] + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "acknowledged": { + "const": false + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict", + "acknowledged" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Acknowledged(?: — .+)?$" + }, + "token": { + "const": "Acknowledged" + }, + "conclusion": { + "const": "neutral" + }, + "underlyingVerdict": { + "enum": [ + "heads_up", + "wait_in_line" + ] + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "acknowledged": { + "const": true + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion", + "underlyingVerdict" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — Unresolved merge conflict markers$" + }, + "token": { + "const": "Unresolved merge conflict markers" + }, + "conclusion": { + "const": "action_required" + }, + "underlyingVerdict": { + "const": "merge_conflict_markers" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — acknowledgement verification pending$" + }, + "token": { + "const": "Acknowledgement verification pending" + }, + "conclusion": { + "const": "neutral" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — heading to .+$" + }, + "token": { + "const": "Heading to" + }, + "conclusion": { + "const": "success" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa is now watching$" + }, + "token": { + "const": "Watching" + }, + "conclusion": { + "const": "neutral" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa paused — early-access limit reached$" + }, + "token": { + "const": "Early-access limit reached" + }, + "conclusion": { + "const": "neutral" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — changed files not read, not analyzed$" + }, + "token": { + "const": "Changed files not read" + }, + "conclusion": { + "const": "neutral" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — branch state not verified, retrying$" + }, + "token": { + "const": "Branch state not verified" + }, + "conclusion": { + "const": "neutral" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — merge queue: clear$" + }, + "token": { + "const": "Merge queue: clear" + }, + "conclusion": { + "const": "success" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — merge queue: review overlap$" + }, + "token": { + "const": "Merge queue: review overlap" + }, + "conclusion": { + "const": "neutral" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "title", + "token", + "conclusion" + ], + "properties": { + "name": { + "const": "Veripsa" + }, + "title": { + "type": "string", + "pattern": "^Veripsa — merge queue: not analyzed$" + }, + "token": { + "const": "Merge queue: not analyzed" + }, + "conclusion": { + "const": "neutral" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" } } } diff --git a/schemas/pr-comment-surface.schema.json b/schemas/pr-comment-surface.schema.json index a02334c..ae5636e 100644 --- a/schemas/pr-comment-surface.schema.json +++ b/schemas/pr-comment-surface.schema.json @@ -1,58 +1,27 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/schemas/pr-comment-surface.schema.json", + "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/raw/v1.0.0/schemas/pr-comment-surface.schema.json", "title": "Veripsa normalized coordination-comment surface", - "description": "Normalized verdict prefix, stable markers, and content-free refs for a verdict-bearing coordination comment. This is not the raw Markdown body and does not model every operational comment.", + "description": "Normalized stable markers and content-free refs. A clean Clear result is check-only; clear comments normalize to Clear to land.", "type": "object", "additionalProperties": false, - "required": [ - "marker", - "leadingVerdict" - ], + "required": ["marker", "leadingVerdict"], "properties": { - "marker": { - "type": "string", - "pattern": "^$" - }, + "marker": {"type": "string", "pattern": "^$"}, "ackSnapshotMarker": { "type": "string", - "pattern": "^$" - }, - "leadingVerdict": { - "type": "string", - "description": "Legacy normalized value derived from the evolving bold human lead; do not match the full Markdown sentence.", - "enum": [ - "Clear to land.", - "Wait in line.", - "Heads up.", - "Heads up — minor overlap.", - "Unknown.", - "Acknowledged." - ] - }, - "partnerRefs": { - "type": "array", - "items": { - "type": "string", - "pattern": "^PR-[1-9][0-9]*$" - }, - "uniqueItems": true - }, - "branchRefs": { - "type": "array", - "items": { - "type": "string", - "pattern": "^BR-[A-Za-z0-9._-]+$" - }, - "uniqueItems": true + "pattern": "^$", + "description": "The 12-lowercase-hex wire token is opaque; do not infer meaning from its value." }, - "paths": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "uniqueItems": true + "leadingVerdict": {"enum": ["Clear to land.", "Wait in line.", "Heads up.", "Heads up — minor overlap.", "Unknown.", "Acknowledged."]}, + "partnerRefs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^PR-[1-9][0-9]*$"}}, + "branchRefs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^BR-[A-Za-z0-9._/-]+$"}}, + "paths": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}} + }, + "allOf": [ + { + "if": {"properties": {"leadingVerdict": {"const": "Acknowledged."}}, "required": ["leadingVerdict"]}, + "then": {"required": ["ackSnapshotMarker"]} } - } + ] } diff --git a/schemas/webhook-routing-envelope.schema.json b/schemas/webhook-routing-envelope.schema.json new file mode 100644 index 0000000..fc7b996 --- /dev/null +++ b/schemas/webhook-routing-envelope.schema.json @@ -0,0 +1,302 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/raw/v1.0.0/schemas/webhook-routing-envelope.schema.json", + "title": "Veripsa normalized webhook-routing envelope", + "description": "A discriminated normalized envelope covering every public event family listed in EVENTS.md. It is not a raw GitHub webhook body.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "action", + "installationId", + "repositoryFullName", + "prNumber", + "headSha", + "baseRef" + ], + "properties": { + "event": { + "const": "pull_request" + }, + "action": { + "type": "string", + "minLength": 1 + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "prNumber": { + "type": "integer", + "minimum": 1 + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "baseRef": { + "type": "string", + "minLength": 1 + }, + "branchRef": { + "type": "string", + "minLength": 1 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "installationId", + "repositoryFullName", + "branchRef", + "headSha" + ], + "properties": { + "event": { + "const": "push" + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "branchRef": { + "type": "string", + "minLength": 1 + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "action": { + "type": "string", + "minLength": 1 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "action", + "installationId", + "repositoryFullName" + ], + "properties": { + "event": { + "const": "repository" + }, + "action": { + "type": "string", + "minLength": 1 + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "action", + "installationId", + "repositoryFullName" + ], + "properties": { + "event": { + "const": "check_suite" + }, + "action": { + "type": "string", + "minLength": 1 + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "action", + "installationId", + "repositoryFullName" + ], + "properties": { + "event": { + "const": "check_run" + }, + "action": { + "type": "string", + "minLength": 1 + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "action", + "installationId", + "repositoryFullName", + "headSha", + "baseRef" + ], + "properties": { + "event": { + "const": "merge_group" + }, + "action": { + "type": "string", + "minLength": 1 + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "repositoryFullName": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "headSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "baseRef": { + "type": "string", + "minLength": 1 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "action", + "installationId", + "accountLogin", + "accountType" + ], + "properties": { + "event": { + "const": "installation" + }, + "action": { + "type": "string", + "minLength": 1 + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "accountLogin": { + "type": "string", + "minLength": 1 + }, + "accountType": { + "enum": [ + "User", + "Organization" + ] + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "action", + "installationId", + "accountLogin", + "accountType", + "repositoriesAdded", + "repositoriesRemoved" + ], + "properties": { + "event": { + "const": "installation_repositories" + }, + "action": { + "type": "string", + "minLength": 1 + }, + "installationId": { + "type": "integer", + "minimum": 1 + }, + "accountLogin": { + "type": "string", + "minLength": 1 + }, + "accountType": { + "enum": [ + "User", + "Organization" + ] + }, + "repositoriesAdded": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + } + }, + "repositoriesRemoved": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + } + } + } + } + ] +} diff --git a/schemas/webhook-routing-event.schema.json b/schemas/webhook-routing-event.schema.json index d0ea432..26031d4 100644 --- a/schemas/webhook-routing-event.schema.json +++ b/schemas/webhook-routing-event.schema.json @@ -1,11 +1,12 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/schemas/webhook-routing-event.schema.json", - "title": "Veripsa webhook routing event", - "description": "Abridged GitHub webhook fields that matter at the public Veripsa routing-contract level.", + "$id": "https://github.com/GetVeripsa/veripsa-webhook-spec/raw/v1.0.0/schemas/webhook-routing-event.schema.json", + "title": "Legacy selected raw GitHub webhook body shapes", + "description": "Compatibility schema for selected raw bodies: pull_request, push, and installation lifecycle. It does not carry the X-GitHub-Event header and is not the complete event contract. New integrations should use webhook-routing-envelope.schema.json.", + "$comment": "Deprecated in v1.0.0 for new integrations; retained through the v1 compatibility window.", "oneOf": [ { - "title": "pull_request", + "title": "pull_request raw body", "type": "object", "additionalProperties": true, "required": [ @@ -17,17 +18,18 @@ ], "properties": { "action": { - "type": "string" + "type": "string", + "minLength": 1 }, "number": { "type": "integer", - "minimum": 1 + "minimum": 1, + "description": "Canonical PR number for this compatibility schema." }, "pull_request": { "type": "object", "additionalProperties": true, "required": [ - "number", "head", "base", "draft" @@ -35,7 +37,8 @@ "properties": { "number": { "type": "integer", - "minimum": 1 + "minimum": 1, + "description": "When present, semantic example validation requires equality with top-level number." }, "head": { "type": "object", @@ -117,7 +120,7 @@ } }, { - "title": "push", + "title": "push raw body", "type": "object", "additionalProperties": true, "required": [ @@ -169,7 +172,7 @@ } }, { - "title": "installation_or_installation_repositories", + "title": "installation lifecycle raw body", "type": "object", "additionalProperties": true, "required": [ @@ -178,7 +181,8 @@ ], "properties": { "action": { - "type": "string" + "type": "string", + "minLength": 1 }, "installation": { "type": "object", @@ -201,10 +205,10 @@ ], "properties": { "login": { - "type": "string" + "type": "string", + "minLength": 1 }, "type": { - "type": "string", "enum": [ "User", "Organization" @@ -219,6 +223,9 @@ "items": { "type": "object", "additionalProperties": true, + "required": [ + "full_name" + ], "properties": { "full_name": { "type": "string", @@ -232,6 +239,9 @@ "items": { "type": "object", "additionalProperties": true, + "required": [ + "full_name" + ], "properties": { "full_name": { "type": "string", diff --git a/scripts/check-contract.mjs b/scripts/check-contract.mjs new file mode 100644 index 0000000..6e2e1cb --- /dev/null +++ b/scripts/check-contract.mjs @@ -0,0 +1,21 @@ +import { readFile } from "node:fs/promises"; +const pkg = JSON.parse(await readFile("package.json", "utf8")); +const states = JSON.parse(await readFile("contract/check-states.json", "utf8")); +const events = JSON.parse(await readFile("contract/webhook-events.json", "utf8")); +if (pkg.version === "0.0.0" || pkg.version !== states.contractVersion || pkg.version !== events.contractVersion) { + throw new Error("package and contract versions must match and must not be 0.0.0"); +} +const readme = await readFile("README.md", "utf8"); +if (!readme.includes('`check_run.name` is exactly `Veripsa`')) throw new Error("README must lock check_run.name"); +if (!readme.includes('`check_run.output.title`')) throw new Error("README must separate output.title from name"); +if (readme.includes("check-run name prefix `Veripsa — `")) throw new Error("stale name/title wording returned"); +const heading = states.states.find(s => s.token === "Heading to"); +if (!heading || heading.conclusion !== "success") throw new Error("Heading to must remain success"); + +const types = await readFile("types/index.d.ts", "utf8"); +if (!types.includes("VeripsaOpaqueLowercaseHex12")) throw new Error("ACK marker token must be branded as validated 12-lowercase-hex"); +if (types.includes("veripsa-ack-snap:${string}")) throw new Error("ACK marker type must not accept arbitrary strings"); + +const expectedEvents = ["pull_request","push","repository","check_suite","check_run","merge_group","installation","installation_repositories"]; +if (JSON.stringify(events.events.map(e=>e.event)) !== JSON.stringify(expectedEvents)) throw new Error("event manifest drift"); +console.log("contract invariants verified"); diff --git a/scripts/check-examples.py b/scripts/check-examples.py new file mode 100644 index 0000000..fdc5f99 --- /dev/null +++ b/scripts/check-examples.py @@ -0,0 +1,23 @@ +from pathlib import Path +import json +from jsonschema import Draft202012Validator +root = Path(__file__).resolve().parents[1] +manifest = json.loads((root / "examples/manifest.json").read_text(encoding="utf-8")) +passed = 0 +for item in manifest: + data = json.loads((root / item["file"]).read_text(encoding="utf-8")) + schema = json.loads((root / item["schema"]).read_text(encoding="utf-8")) + errors = list(Draft202012Validator(schema).iter_errors(data)) + # JSON Schema cannot express equality between duplicate raw GitHub fields. + # Treat the top-level PR number as canonical and reject contradictory fixtures. + if item["schema"].endswith("webhook-routing-event.schema.json") and isinstance(data, dict): + nested = ((data.get("pull_request") or {}).get("number")) + if data.get("number") is not None and nested is not None and data["number"] != nested: + errors.append(ValueError("top-level and nested PR numbers differ")) + expected_valid = bool(item["valid"]) + if expected_valid and errors: + raise SystemExit(f"{item['file']} unexpectedly invalid: {errors[0]}") + if not expected_valid and not errors: + raise SystemExit(f"{item['file']} unexpectedly valid") + passed += 1 +print(f"validated {passed} positive/negative examples") diff --git a/scripts/check-json.mjs b/scripts/check-json.mjs index 3528b5b..a2871c8 100644 --- a/scripts/check-json.mjs +++ b/scripts/check-json.mjs @@ -1,25 +1,17 @@ import { readdir, readFile } from "node:fs/promises"; import { join } from "node:path"; - async function* walk(dir) { for (const entry of await readdir(dir, { withFileTypes: true })) { const path = join(dir, entry.name); - if (entry.isDirectory()) { - yield* walk(path); - } else if (entry.isFile() && entry.name.endsWith(".json")) { - yield path; - } + if (entry.isDirectory()) yield* walk(path); + else if (entry.isFile() && entry.name.endsWith(".json")) yield path; } } - -const roots = ["schemas", "examples"]; let checked = 0; - -for (const root of roots) { +for (const root of ["contract", "schemas", "examples"]) { for await (const path of walk(root)) { JSON.parse(await readFile(path, "utf8")); checked += 1; } } - -console.log(`checked ${checked} JSON file${checked === 1 ? "" : "s"}`); +console.log(`checked ${checked} JSON files`); diff --git a/scripts/check-links.py b/scripts/check-links.py new file mode 100644 index 0000000..36681f1 --- /dev/null +++ b/scripts/check-links.py @@ -0,0 +1,18 @@ +from pathlib import Path +import re +from urllib.parse import unquote +root = Path(__file__).resolve().parents[1] +link_re = re.compile(r"(?") + if target.startswith(("http://", "https://", "mailto:")) or target.startswith("#"): + checked += 1 + continue + path_part = unquote(target.split("#", 1)[0]) + if path_part and not (md.parent / path_part).resolve().exists(): + raise SystemExit(f"{md.relative_to(root)}: missing relative link {target}") + checked += 1 +print(f"checked {checked} Markdown links (relative targets + external URL syntax)") diff --git a/scripts/check-schema.py b/scripts/check-schema.py new file mode 100644 index 0000000..ceefd09 --- /dev/null +++ b/scripts/check-schema.py @@ -0,0 +1,12 @@ +from pathlib import Path +import json +from jsonschema import Draft202012Validator +root = Path(__file__).resolve().parents[1] +count = 0 +for path in sorted((root / "schemas").glob("*.json")): + schema = json.loads(path.read_text(encoding="utf-8")) + Draft202012Validator.check_schema(schema) + if f"/v{json.loads((root/'contract/check-states.json').read_text())['contractVersion']}/" not in schema.get("$id", ""): + raise SystemExit(f"{path}: $id is not version-pinned") + count += 1 +print(f"validated {count} draft-2020-12 schemas") diff --git a/scripts/check-types.mjs b/scripts/check-types.mjs new file mode 100644 index 0000000..2f662dd --- /dev/null +++ b/scripts/check-types.mjs @@ -0,0 +1,7 @@ +import { spawnSync } from "node:child_process"; +const run = spawnSync("tsc", ["-p", "tsconfig.json", "--noEmit"], { stdio: "inherit", shell: process.platform === "win32" }); +if (run.error?.code === "ENOENT") { + console.error("TypeScript compiler not found. Install TypeScript 5.x before running check:types."); + process.exit(1); +} +process.exit(run.status ?? 1); diff --git a/scripts/generate-contract.mjs b/scripts/generate-contract.mjs new file mode 100644 index 0000000..c93848b --- /dev/null +++ b/scripts/generate-contract.mjs @@ -0,0 +1,97 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const checkOnly = process.argv.includes("--check"); +const root = resolve(import.meta.dirname, ".."); +const manifest = JSON.parse(await readFile(resolve(root, "contract/check-states.json"), "utf8")); +const common = { + name: { const: manifest.checkName }, + title: { type: "string" }, + token: { type: "string" }, + conclusion: { enum: ["success", "neutral", "action_required"] }, + underlyingVerdict: { type: "string" }, + prNumber: { type: "integer", minimum: 1 }, + repositoryFullName: { type: "string", pattern: "^[^/\\s]+/[^/\\s]+$" }, + headSha: { type: "string", pattern: "^[0-9a-f]{40}$" }, + acknowledged: { type: "boolean" } +}; + +function schemaBranch(state) { + const properties = { + ...common, + name: { const: manifest.checkName }, + title: { type: "string", pattern: state.titlePattern }, + token: { const: state.token }, + conclusion: { const: state.conclusion } + }; + const required = ["name", "title", "token", "conclusion"]; + if (state.underlyingVerdicts.length === 1) { + properties.underlyingVerdict = { const: state.underlyingVerdicts[0] }; + required.push("underlyingVerdict"); + } else if (state.underlyingVerdicts.length > 1) { + properties.underlyingVerdict = { enum: state.underlyingVerdicts }; + required.push("underlyingVerdict"); + } else { + delete properties.underlyingVerdict; + } + if (state.acknowledged === "true") { + properties.acknowledged = { const: true }; + required.push("acknowledged"); + } else if (state.acknowledged === "false") { + properties.acknowledged = { const: false }; + } else { + delete properties.acknowledged; + } + return { type: "object", additionalProperties: false, required, properties }; +} + +const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: `https://github.com/GetVeripsa/veripsa-webhook-spec/raw/v${manifest.contractVersion}/schemas/check-run-signal.schema.json`, + title: "Veripsa normalized check-run signal", + description: "Normalized public signal derived from check_run.name, check_run.output.title, and check_run.conclusion. The oneOf branches reject impossible token/conclusion/underlying-verdict/ack combinations.", + oneOf: manifest.states.map(schemaBranch) +}; + +const quoteUnion = (values) => values.map(v => ` | ${JSON.stringify(v)}`).join("\n"); +const tokens = manifest.states.map(s => s.token); +const underlying = [...new Set(manifest.states.flatMap(s => s.underlyingVerdicts))]; +const stateUnion = manifest.states.map((s) => { + const lines = [ + ` token: ${JSON.stringify(s.token)};`, + ` conclusion: ${JSON.stringify(s.conclusion)};` + ]; + if (s.underlyingVerdicts.length === 1) lines.push(` underlyingVerdict: ${JSON.stringify(s.underlyingVerdicts[0])};`); + if (s.underlyingVerdicts.length > 1) lines.push(` underlyingVerdict: ${s.underlyingVerdicts.map(JSON.stringify).join(" | ")};`); + if (s.acknowledged === "true") lines.push(" acknowledged: true;"); + if (s.acknowledged === "false") lines.push(" acknowledged?: false;"); + if (s.acknowledged === "forbidden") lines.push(" acknowledged?: never;"); + return ` | {\n${lines.join("\n")}\n }`; +}).join("\n"); +const types = `/** Generated from contract/check-states.json. Do not edit by hand. */\n` + +`export type VeripsaCheckToken =\n${quoteUnion(tokens)};\n\n` + +`/** @deprecated Use VeripsaCheckToken; operational states are not verdicts. */\n` + +`export type VeripsaVerdictToken = VeripsaCheckToken;\n\n` + +`export type VeripsaUnderlyingVerdict =\n${quoteUnion(underlying)};\n\n` + +`export type VeripsaCheckState =\n${stateUnion};\n`; + +const header = `# Check-state contract\n\nGenerated from \`contract/check-states.json\` for contract **v${manifest.contractVersion}**. Human sentence copy may evolve; integrations depend on \`check_run.name === \"${manifest.checkName}\"\` plus the normalized token.\n\n`; +const table = [ + "| Token | Observable title example | Conclusion | Underlying verdict | ACK field | PR comment | Stability |", + "| --- | --- | --- | --- | --- | --- | --- |", + ...manifest.states.map(s => `| \`${s.token}\` | \`${s.titleExample}\` | \`${s.conclusion}\` | ${s.underlyingVerdicts.length ? s.underlyingVerdicts.map(v=>`\`${v}\``).join(" / ") : "—"} | \`${s.acknowledged}\` | \`${s.commentAvailability}\` | \`${s.stability}\` |`) +].join("\n") + "\n"; + +async function emit(path, content) { + const full = resolve(root, path); + if (checkOnly) { + const current = await readFile(full, "utf8").catch(() => ""); + if (current !== content) throw new Error(`${path} is stale; run npm run generate`); + } else { + await writeFile(full, content, "utf8"); + } +} +await emit("schemas/check-run-signal.schema.json", JSON.stringify(schema, null, 2) + "\n"); +await emit("types/check-states.generated.d.ts", types); +await emit("CHECK_STATES.md", header + table); +console.log(checkOnly ? "check-state generated files are current" : "generated check-state contract"); diff --git a/scripts/generate-events.mjs b/scripts/generate-events.mjs new file mode 100644 index 0000000..2ab7f96 --- /dev/null +++ b/scripts/generate-events.mjs @@ -0,0 +1,60 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const checkOnly = process.argv.includes("--check"); +const root = resolve(import.meta.dirname, ".."); +const manifest = JSON.parse(await readFile(resolve(root, "contract/webhook-events.json"), "utf8")); +const fieldSchemas = { + action: { type: "string", minLength: 1 }, + installationId: { type: "integer", minimum: 1 }, + repositoryFullName: { type: "string", pattern: "^[^/\\s]+/[^/\\s]+$" }, + prNumber: { type: "integer", minimum: 1 }, + headSha: { type: "string", pattern: "^[0-9a-f]{40}$" }, + baseRef: { type: "string", minLength: 1 }, + branchRef: { type: "string", minLength: 1 }, + accountLogin: { type: "string", minLength: 1 }, + accountType: { enum: ["User", "Organization"] }, + repositoriesAdded: { type: "array", uniqueItems: true, items: { type: "string", pattern: "^[^/\\s]+/[^/\\s]+$" } }, + repositoriesRemoved: { type: "array", uniqueItems: true, items: { type: "string", pattern: "^[^/\\s]+/[^/\\s]+$" } } +}; +function branch(evt) { + const fields = [...evt.required, ...evt.optional]; + const properties = { event: { const: evt.event } }; + for (const field of fields) properties[field] = fieldSchemas[field]; + return { type: "object", additionalProperties: false, required: ["event", ...evt.required], properties }; +} +const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: `https://github.com/GetVeripsa/veripsa-webhook-spec/raw/v${manifest.contractVersion}/schemas/webhook-routing-envelope.schema.json`, + title: "Veripsa normalized webhook-routing envelope", + description: "A discriminated normalized envelope covering every public event family listed in EVENTS.md. It is not a raw GitHub webhook body.", + oneOf: manifest.events.map(branch) +}; +const tsType = (field) => ({ + action:"string", installationId:"number", repositoryFullName:"string", prNumber:"number", headSha:"string", baseRef:"string", branchRef:"string", accountLogin:"string", accountType:'"User" | "Organization"', repositoriesAdded:"string[]", repositoriesRemoved:"string[]" +})[field]; +const union = manifest.events.map(evt => { + const lines = [` event: ${JSON.stringify(evt.event)};`]; + for (const f of evt.required) lines.push(` ${f}: ${tsType(f)};`); + for (const f of evt.optional) lines.push(` ${f}?: ${tsType(f)};`); + return ` | {\n${lines.join("\n")}\n }`; +}).join("\n"); +const types = `/** Generated from contract/webhook-events.json. Do not edit by hand. */\n` + +`export type VeripsaWebhookEventName =\n${manifest.events.map(e=>` | ${JSON.stringify(e.event)}`).join("\n")};\n\n` + +`export type VeripsaWebhookRoutingEnvelope =\n${union};\n`; +const docs = `# Webhook event contract\n\nGenerated from \`contract/webhook-events.json\` for contract **v${manifest.contractVersion}**. This table describes the reduced normalized routing envelope, not the raw GitHub body.\n\n` + [ + "| Event | Required normalized fields | Optional fields | Used for | Retained boundary | Transient-only boundary |", + "| --- | --- | --- | --- | --- | --- |", + ...manifest.events.map(e => `| \`${e.event}\` | ${e.required.map(f=>`\`${f}\``).join(", ")} | ${e.optional.length ? e.optional.map(f=>`\`${f}\``).join(", ") : "—"} | ${e.uses} | ${e.retained} | ${e.transient} |`) +].join("\n") + "\n"; +async function emit(path, content) { + const full = resolve(root, path); + if (checkOnly) { + const current = await readFile(full, "utf8").catch(() => ""); + if (current !== content) throw new Error(`${path} is stale; run npm run generate`); + } else await writeFile(full, content, "utf8"); +} +await emit("schemas/webhook-routing-envelope.schema.json", JSON.stringify(schema, null, 2) + "\n"); +await emit("types/webhook-events.generated.d.ts", types); +await emit("WEBHOOK_EVENTS.md", docs); +console.log(checkOnly ? "webhook generated files are current" : "generated webhook contract"); diff --git a/tests/types/contract.test.ts b/tests/types/contract.test.ts new file mode 100644 index 0000000..048d111 --- /dev/null +++ b/tests/types/contract.test.ts @@ -0,0 +1,22 @@ +import type { VeripsaAckSnapshotMarker, VeripsaCheckRunSignal, VeripsaOpaqueLowercaseHex12, VeripsaPrCommentSurface, VeripsaWebhookRoutingEnvelope } from "../../types/index"; +const accept = (_value: VeripsaCheckRunSignal) => undefined; +accept({ name: "Veripsa", title: "Veripsa — heading to main", token: "Heading to", conclusion: "success" }); +accept({ name: "Veripsa", title: "Veripsa — Paused (acknowledge to proceed)", token: "Paused (acknowledge to proceed)", conclusion: "action_required", underlyingVerdict: "wait_in_line", acknowledged: false }); +accept({ name: "Veripsa", title: "Veripsa — Acknowledged", token: "Acknowledged", conclusion: "neutral", underlyingVerdict: "heads_up", acknowledged: true }); +// @ts-expect-error Clear cannot be neutral. +accept({ name: "Veripsa", title: "Veripsa — Clear", token: "Clear", conclusion: "neutral", underlyingVerdict: "clear" }); +// @ts-expect-error Heading to cannot be neutral. +accept({ name: "Veripsa", title: "Veripsa — heading to main", token: "Heading to", conclusion: "neutral" }); +// @ts-expect-error Paused cannot be acknowledged true. +accept({ name: "Veripsa", title: "Veripsa — Paused (acknowledge to proceed)", token: "Paused (acknowledge to proceed)", conclusion: "action_required", underlyingVerdict: "wait_in_line", acknowledged: true }); +// @ts-expect-error Acknowledged requires acknowledged true. +accept({ name: "Veripsa", title: "Veripsa — Acknowledged", token: "Acknowledged", conclusion: "neutral", underlyingVerdict: "heads_up", acknowledged: false }); +const comment: VeripsaPrCommentSurface = { marker: "", leadingVerdict: "Clear to land." }; +const event: VeripsaWebhookRoutingEnvelope = { event: "merge_group", action: "checks_requested", installationId: 1, repositoryFullName: "example/repo", headSha: "0123456789abcdef0123456789abcdef01234567", baseRef: "main" }; +void comment; void event; + +declare const validatedAckToken: VeripsaOpaqueLowercaseHex12; +const validAckMarker: VeripsaAckSnapshotMarker = ``; +// @ts-expect-error Arbitrary strings are not validated 12-lowercase-hex tokens. +const invalidAckMarker: VeripsaAckSnapshotMarker = ""; +void validAckMarker; void invalidAckMarker; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d12558f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "skipLibCheck": false + }, + "files": ["tests/types/contract.test.ts"] +} diff --git a/types/check-states.generated.d.ts b/types/check-states.generated.d.ts new file mode 100644 index 0000000..5cab8e2 --- /dev/null +++ b/types/check-states.generated.d.ts @@ -0,0 +1,124 @@ +/** Generated from contract/check-states.json. Do not edit by hand. */ +export type VeripsaCheckToken = + | "Clear" + | "Clear to land" + | "Heads up" + | "Wait in line" + | "Unknown" + | "Paused (acknowledge to proceed)" + | "Acknowledged" + | "Unresolved merge conflict markers" + | "Acknowledgement verification pending" + | "Heading to" + | "Watching" + | "Early-access limit reached" + | "Changed files not read" + | "Branch state not verified" + | "Merge queue: clear" + | "Merge queue: review overlap" + | "Merge queue: not analyzed"; + +/** @deprecated Use VeripsaCheckToken; operational states are not verdicts. */ +export type VeripsaVerdictToken = VeripsaCheckToken; + +export type VeripsaUnderlyingVerdict = + | "clear" + | "heads_up" + | "wait_in_line" + | "unknown" + | "merge_conflict_markers"; + +export type VeripsaCheckState = + | { + token: "Clear"; + conclusion: "success"; + underlyingVerdict: "clear"; + acknowledged?: never; + } + | { + token: "Clear to land"; + conclusion: "success"; + underlyingVerdict: "clear"; + acknowledged?: never; + } + | { + token: "Heads up"; + conclusion: "neutral"; + underlyingVerdict: "heads_up"; + acknowledged?: never; + } + | { + token: "Wait in line"; + conclusion: "neutral"; + underlyingVerdict: "wait_in_line"; + acknowledged?: never; + } + | { + token: "Unknown"; + conclusion: "neutral"; + underlyingVerdict: "unknown"; + acknowledged?: never; + } + | { + token: "Paused (acknowledge to proceed)"; + conclusion: "action_required"; + underlyingVerdict: "heads_up" | "wait_in_line"; + acknowledged?: false; + } + | { + token: "Acknowledged"; + conclusion: "neutral"; + underlyingVerdict: "heads_up" | "wait_in_line"; + acknowledged: true; + } + | { + token: "Unresolved merge conflict markers"; + conclusion: "action_required"; + underlyingVerdict: "merge_conflict_markers"; + acknowledged?: never; + } + | { + token: "Acknowledgement verification pending"; + conclusion: "neutral"; + acknowledged?: never; + } + | { + token: "Heading to"; + conclusion: "success"; + acknowledged?: never; + } + | { + token: "Watching"; + conclusion: "neutral"; + acknowledged?: never; + } + | { + token: "Early-access limit reached"; + conclusion: "neutral"; + acknowledged?: never; + } + | { + token: "Changed files not read"; + conclusion: "neutral"; + acknowledged?: never; + } + | { + token: "Branch state not verified"; + conclusion: "neutral"; + acknowledged?: never; + } + | { + token: "Merge queue: clear"; + conclusion: "success"; + acknowledged?: never; + } + | { + token: "Merge queue: review overlap"; + conclusion: "neutral"; + acknowledged?: never; + } + | { + token: "Merge queue: not analyzed"; + conclusion: "neutral"; + acknowledged?: never; + }; diff --git a/types/index.d.ts b/types/index.d.ts index 52b7515..6b579e1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,87 +1,40 @@ -/** - * Public TypeScript declarations for the Veripsa GitHub App integration - * surface. These types describe normalized public signals, not raw GitHub - * webhook or check-run payloads. See OUTPUT.md for the normalization rules. - * They do not expose Veripsa Core engine internals. - */ +export * from "./check-states.generated"; +export * from "./webhook-events.generated"; +import type { VeripsaCheckState } from "./check-states.generated"; +import type { VeripsaWebhookRoutingEnvelope } from "./webhook-events.generated"; export type VeripsaCheckName = "Veripsa"; +export type VeripsaConclusion = "success" | "neutral" | "action_required"; -export type VeripsaConclusion = - | "success" - | "neutral" - | "action_required"; - -export type VeripsaVerdictToken = - | "Clear" - | "Clear to land" - | "Heads up" - | "Wait in line" - | "Unknown" - | "Paused (acknowledge to proceed)" - | "Acknowledged" - | "Unresolved merge conflict markers" - | "Acknowledgement verification pending" - | "Heading to" - | "Watching" - | "Early-access limit reached" - | "Changed files not read" - | "Branch state not verified" - | "Merge queue: clear" - | "Merge queue: review overlap" - | "Merge queue: not analyzed"; - -export type VeripsaUnderlyingVerdict = - | "clear" - | "heads_up" - | "wait_in_line" - | "unknown" - | "merge_conflict_markers"; - -export interface VeripsaCheckRunSignal { - /** - * Normalized view: `name` = check_run.name, `title` = check_run.output.title, - * `conclusion` = check_run.conclusion, and `token` follows OUTPUT.md. - */ - /** Literal check-run name posted by the GitHub App. */ +export interface VeripsaCheckRunBase { + /** Literal check_run.name. It is not the human-facing title. */ name: VeripsaCheckName; - - /** Human title. Branch on `token`, not the full title string. */ + /** Full check_run.output.title. Branch on token, not the full sentence. */ title: string; - - /** Stable normalized token derived from the observable title. */ - token: VeripsaVerdictToken; - - /** GitHub check-run conclusion. */ - conclusion: VeripsaConclusion; - - /** Underlying verdict represented by the public token. */ - underlyingVerdict?: VeripsaUnderlyingVerdict; - - /** Pull request number when the check belongs to a PR. */ prNumber?: number; - - /** Repository full name in `owner/name` form. */ repositoryFullName?: string; - - /** PR head commit SHA. */ headSha?: string; - - /** Whether `veripsa-ack` currently resolves this material coupling. */ - acknowledged?: boolean; } +export type VeripsaCheckRunSignal = VeripsaCheckRunBase & VeripsaCheckState; -export interface VeripsaPrCommentSurface { - /** Stable marker at the top of the Veripsa PR comment. */ - marker: ``; +/** + * Opaque token validated at the normalization boundary as exactly 12 lowercase + * hexadecimal characters. The brand prevents arbitrary strings from being + * treated as validated tokens in TypeScript; consumers must validate first. + */ +declare const veripsaOpaqueLowercaseHex12Brand: unique symbol; +export type VeripsaOpaqueLowercaseHex12 = string & { + readonly [veripsaOpaqueLowercaseHex12Brand]: "VeripsaOpaqueLowercaseHex12"; +}; - /** Optional marker that binds ack to a specific material coupling. */ - ackSnapshotMarker?: ``; +/** Runtime wire format: . */ +export type VeripsaAckSnapshotMarker = + ``; - /** - * Normalized verdict prefix for a coordination comment. These legacy values - * are not the full human-facing bold sentence; see OUTPUT.md. - */ +export interface VeripsaPrCommentSurface { + marker: ``; + ackSnapshotMarker?: VeripsaAckSnapshotMarker; + /** Clean Clear is check-only; a clear coordination comment normalizes to Clear to land. */ leadingVerdict: | "Clear to land." | "Wait in line." @@ -89,41 +42,19 @@ export interface VeripsaPrCommentSurface { | "Heads up — minor overlap." | "Unknown." | "Acknowledged."; - - /** Content-free PR refs such as `PR-17`. */ partnerRefs?: string[]; - - /** Content-free branch refs such as `BR-feature-agent-a`. */ branchRefs?: string[]; - - /** Content-free paths rendered in the comment body. */ paths?: string[]; } export interface VeripsaAckLabelContract { name: "veripsa-ack"; meaning: "seen_and_proceeding"; - appliedBy: "human_or_agent"; + repositoryDefinitionCreatedBy: "veripsa_when_needed"; + appliedToPullRequestBy: "human_or_agent"; effectWhenPaused: "action_required_to_neutral"; staleWhenMaterialCouplingChanges: true; } -export interface VeripsaWebhookRoutingEvent { - event: - | "pull_request" - | "push" - | "repository" - | "check_suite" - | "check_run" - | "merge_group" - | "installation" - | "installation_repositories"; - action?: string; - /** Present for repository-scoped events; absent for account-level lifecycle events. */ - repositoryFullName?: string; - installationId?: number; - prNumber?: number; - headSha?: string; - baseRef?: string; - branchRef?: string; -} +/** @deprecated Use VeripsaWebhookRoutingEnvelope; this alias remains for v1 compatibility. */ +export type VeripsaWebhookRoutingEvent = VeripsaWebhookRoutingEnvelope; diff --git a/types/webhook-events.generated.d.ts b/types/webhook-events.generated.d.ts new file mode 100644 index 0000000..fbdf8aa --- /dev/null +++ b/types/webhook-events.generated.d.ts @@ -0,0 +1,74 @@ +/** Generated from contract/webhook-events.json. Do not edit by hand. */ +export type VeripsaWebhookEventName = + | "pull_request" + | "push" + | "repository" + | "check_suite" + | "check_run" + | "merge_group" + | "installation" + | "installation_repositories"; + +export type VeripsaWebhookRoutingEnvelope = + | { + event: "pull_request"; + action: string; + installationId: number; + repositoryFullName: string; + prNumber: number; + headSha: string; + baseRef: string; + branchRef?: string; + } + | { + event: "push"; + installationId: number; + repositoryFullName: string; + branchRef: string; + headSha: string; + action?: string; + } + | { + event: "repository"; + action: string; + installationId: number; + repositoryFullName: string; + } + | { + event: "check_suite"; + action: string; + installationId: number; + repositoryFullName: string; + headSha?: string; + } + | { + event: "check_run"; + action: string; + installationId: number; + repositoryFullName: string; + headSha?: string; + } + | { + event: "merge_group"; + action: string; + installationId: number; + repositoryFullName: string; + headSha: string; + baseRef: string; + } + | { + event: "installation"; + action: string; + installationId: number; + accountLogin: string; + accountType: "User" | "Organization"; + } + | { + event: "installation_repositories"; + action: string; + installationId: number; + accountLogin: string; + accountType: "User" | "Organization"; + repositoriesAdded: string[]; + repositoriesRemoved: string[]; + };