Skip to content

feat(scripts): extend §1.5 verb-canary coverage (Phase E)#215

Merged
hyperpolymath merged 2 commits into
mainfrom
phase-e-smoke-verb-canary-expansion
Jun 13, 2026
Merged

feat(scripts): extend §1.5 verb-canary coverage (Phase E)#215
hyperpolymath merged 2 commits into
mainfrom
phase-e-smoke-verb-canary-expansion

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Tightens scripts/hcg-policy-smoke.sh against three verb-governance regression classes the original three canaries (DELETE/PUT/PATCH on /cartridges and /health) don't catch. Single-lane HCG tier-2 channel (standards#91); Phase E (standards#100) is the active phase.

What this PR adds

Three new deny-mode verb-canary probes:

  1. OPTIONS /cartridgesglobal_verbs: [GET, POST] bans OPTIONS, but a CORS preflight auto-responder added later would silently bypass policy. The canary fails closed against that regression class.

  2. DELETE /cartridge/probe/invoke — exercises the regex route ^/cartridge/[A-Za-z0-9_.-]+/invoke$ under a banned verb. The existing exact-path canaries don't catch a regex-matcher regression where the path is accepted under any verb instead of only the verb the rule lists.

  3. GET /cartridges/ssg-mcp/webhook — the path is in the policy as a documented public exception, but only for POST. The canary verifies the {path, verb} pairing is enforced: GET on the same path must default-deny because no rule covers it.

Runbook §1.5 description updated to enumerate the expanded canary set; runbook version bump 0.4 → 0.5.

What this PR deliberately does NOT do

  • Probe HEAD. Curl with -X HEAD (vs --head) waits for a body the server will not send, which interacts badly with the script's --max-time 10. HEAD enforcement remains covered by the gateway's own unit tests; the §1.5 operator pre-check focuses on probes that survive curl's method quirks. The reasoning is captured inline in the script comment so a future maintainer doesn't add it back as an oversight.

  • Extend the --with-backend allow-path matrix. The authenticated routes the script probes in allow mode are the ones actually wired in BojRest.Router; the additional policy entries (graphql, sse, order, umoja/*, etc.) are declared-not-yet-wired per contract §8 and would 404 from BoJ, which the allow_or_upstream pattern misdiagnoses as gateway-deny. They stay in the deny matrix until they are wired in BoJ. The ssg-mcp-webhook-post route is not added to the --with-backend allow probes for the same reason.

  • Auto-derive the probe matrix from the policy YAML. The matrix stays hand-maintained and the parity-with-policy property remains a manual maintenance discipline. PR feat(scripts): hcg-policy-smoke.sh — §1.5 operator pre-check (Phase E) #210's commitment ("the script doubles as a policy-completeness checklist") is preserved.

  • Close standards#100. Per runbook §6.5 the joint-close happens after the §6.4 Trustfile flip, which itself follows the §3.3 100% production-soak window. Using Refs per the Phase E PR convention (chore(deps): bump nixpkgs from 01fbdee to 6368eda #38, feat(config): promote gateway policy example → live (Phase E §1.5) #208, feat(scripts): hcg-policy-smoke.sh — §1.5 operator pre-check (Phase E) #210).

Verification

  • bash -n scripts/hcg-policy-smoke.sh — syntax check passes.
  • Synthetic always-403 mock on :18443PASS=31 FAIL=0 (was 28); the three new canaries report PASS; exit 0.
  • --help and bad-args exit codes unchanged (64).
  • SPDX header MPL-2.0 unchanged.
  • Runbook cross-references resolve.

Channel position

standards#91 (parent, open)
├── #96 Phase A — closed
├── #97 Phase B — closed
├── #98 Phase C — closed
├── #99 Phase D — closed (joint-closed via boj-server#168)
└── #100 Phase E — IN PROGRESS
     ├── E5 runbook draft — boj-server#128 (landed)
     ├── E1 loopback prereqs — boj-server#130/#131/#132/#165/#173 (landed)
     ├── E1 deploy spec — http-capability-gateway#38 (landed)
     ├── E1 live policy promotion — boj-server#208 (landed)
     ├── §1.5 operator pre-check smoke — boj-server#210 (landed)
     ├── §1.5 verb-canary expansion — THIS PR (in review)
     ├── E1 .ctp signing — owner follow-up
     ├── E2 staging cut-over — owner follow-up
     ├── E3 telemetry verification — owner follow-up
     ├── E4 production rollout — owner follow-up
     └── §6.4 Trustfile flip + §6.5 joint-close — owner-only

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with Claude Code


Generated by Claude Code

Tightens `scripts/hcg-policy-smoke.sh` against three classes of
verb-governance regression the original three canaries (DELETE/PUT/
PATCH on `/cartridges` and `/health`) don't catch. Single-lane HCG
tier-2 channel (`standards#91`); Phase E (`standards#100`) is the
active phase.

Added probes:

1. **`OPTIONS /cartridges`** — `global_verbs: [GET, POST]` bans
   OPTIONS, but a CORS preflight auto-responder added later would
   silently bypass policy. The canary fails closed against that
   regression class.

2. **`DELETE /cartridge/probe/invoke`** — exercises the regex route
   `^/cartridge/[A-Za-z0-9_.-]+/invoke$` under a banned verb. The
   existing exact-path canaries don't catch a regex-matcher regression
   where the path is accepted under any verb instead of only the verb
   the rule lists.

3. **`GET /cartridges/ssg-mcp/webhook`** — the path is in the policy as
   a documented public exception, but only for POST. The canary
   verifies the `{path, verb}` pairing is enforced: GET on the same
   path must default-deny because no rule covers it.

Deliberate omission: HEAD. Curl with `-X HEAD` (vs `--head`) waits for
a body the server will not send, which interacts badly with the
script's `--max-time 10`. HEAD enforcement remains covered by the
gateway's own unit tests; the §1.5 operator pre-check focuses on
probes that survive curl's method quirks. The reasoning is captured
inline in the script comment so a future maintainer doesn't add it
back as an oversight.

Runbook §1.5 description updated to reflect the expanded canary set;
version bump 0.4 → 0.5; status line records the extension.

Verification:

- `bash -n scripts/hcg-policy-smoke.sh` — syntax check passes.
- Synthetic always-403 mock on :18443 — PASS=31 FAIL=0 (was 28),
  the three new canaries report PASS. Exits 0.
- `--help` and bad-args exit codes unchanged (64).

Out of scope:

- The `--with-backend` allow-path matrix is not extended here. The
  authenticated routes the script probes in allow mode are the ones
  actually wired in `BojRest.Router`; the additional policy entries
  (`graphql`, `sse`, `order`, `umoja/*`, etc.) are declared-not-yet-
  wired per contract §8 and would 404 from BoJ, which the
  `allow_or_upstream` pattern misdiagnoses as gateway-deny. They stay
  in the deny matrix until they are wired in BoJ.
- The `ssg-mcp-webhook-post` route is not added to the `--with-backend`
  allow probes for the same reason: the handler is in `openapi.yaml`
  but not yet in `router.ex`.
- The script does not parse `config/gateway-policy-boj.yaml` to derive
  the probe matrix; the matrix stays hand-maintained and the
  parity-with-policy property remains a manual maintenance discipline.
  PR #210's commitment ("the script doubles as a policy-completeness
  checklist") is preserved.

Channel position:

```
standards#91 (parent, open)
├── #96 Phase A — closed
├── #97 Phase B — closed
├── #98 Phase C — closed
├── #99 Phase D — closed (joint-closed via boj-server#168)
└── #100 Phase E — IN PROGRESS
     ├── E5 runbook draft — boj-server#128 (landed)
     ├── E1 loopback prereqs — boj-server#130/#131/#132/#165/#173 (landed)
     ├── E1 deploy spec — http-capability-gateway#38 (landed)
     ├── E1 live policy promotion — boj-server#208 (landed)
     ├── §1.5 operator pre-check smoke — boj-server#210 (landed)
     ├── §1.5 verb-canary expansion — THIS PR
     ├── E1 .ctp signing — owner follow-up
     ├── E2 staging cut-over — owner follow-up
     ├── E3 telemetry verification — owner follow-up
     ├── E4 production rollout — owner follow-up
     └── §6.4 Trustfile flip + §6.5 joint-close — owner-only
```

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 272 issues detected

Severity Count
🔴 Critical 15
🟠 High 138
🟡 Medium 119

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Issue in abi-drift.yml",
    "type": "missing_timeout_minutes",
    "file": "abi-drift.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "container-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath marked this pull request as ready for review June 13, 2026 06:44
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 13, 2026 06:44
@hyperpolymath hyperpolymath disabled auto-merge June 13, 2026 06:44
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 13, 2026 06:44
@hyperpolymath hyperpolymath disabled auto-merge June 13, 2026 06:46
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 13, 2026 06:46
@hyperpolymath hyperpolymath disabled auto-merge June 13, 2026 06:51
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 13, 2026 06:52
@hyperpolymath hyperpolymath disabled auto-merge June 13, 2026 06:55
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 13, 2026 06:55
@hyperpolymath hyperpolymath disabled auto-merge June 13, 2026 07:32
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 13, 2026 07:32
@hyperpolymath hyperpolymath disabled auto-merge June 13, 2026 09:07
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 13, 2026 09:07
@hyperpolymath hyperpolymath disabled auto-merge June 13, 2026 11:58
hyperpolymath added a commit that referenced this pull request Jun 13, 2026
…s) (#216)

## What this fixes

PRs **#213** (dependabot `flake.lock`) and **#215** (scripts-only) are
stuck at `mergeable_state: blocked` despite every check green (and #213
approved). Root cause: seven gates are **workflow-level path-filtered**
(`on.*.paths`). When a PR touches none of a gate's paths the workflow
never runs, so its **required** status check stays *"Expected"* forever
→ permanent block.

The asymmetry this exploits: **a path-filtered workflow that never runs
blocks; a job skipped via `if:` reports SUCCESS to required checks.**
This converts the former into the latter.

## Change (uniform across all 7)

`abi-drift`, `backend-assurance`, `e2e`, `lsp-dap-bsp`, `proofs`,
`truthfulness`, `zig-test`:

1. **Drop the `on.*.paths` filter** → the workflow always runs, so the
required check is always created.
2. **Add a lightweight `changes` job** that recomputes the gate's
*original* path set via `git diff origin/<base>...HEAD` (the same
pattern `abi-drift`/`zig-test` already use internally).
3. **Gate every heavy job** with `needs: changes` + `if:
needs.changes.outputs.run == 'true'`. Nothing relevant changed → heavy
job **skipped → passes** → PR unblocked *and* no wasted CI.

**No branch-protection change needed** — job/check names are unchanged.
`workflow_dispatch` added to all 7 for manual full runs.

## Safety

- **Fail-safe toward running:** the detector defaults `run=true` and
only sets `false` when a *successful* diff shows no relevant path
changed (any fetch/diff error or unknown base ⇒ run). The dangerous
direction — skipping a gate that *should* run — can't happen on
detection failure.
- Each regex **mirrors the gate's original `paths:`**, so a relevant
change runs it exactly as before (verified: 77/77 unit checks —
`abi/`→proofs+abi-drift, `ffi/`→zig+truthfulness+abi-drift,
`SafetyLemmas.idr`→backend-assurance+proofs, etc.; #213/#215 → all
skip).

## Validation

```
actionlint -shellcheck=  (all 7)        → EXIT 0   (structure/expressions/needs-graph valid)
actionlint (all 7)                      → only PRE-EXISTING shellcheck infos in original run-scripts (untouched)
detection regexes (77 checks)           → 77/77 correct
gated heavy jobs                        → 15/15 (abi-drift 1, backend-assurance 1, e2e 5, lsp-dap-bsp 4, proofs 2, truthfulness 1, zig-test 1)
leftover path filters                   → 0
```

**Self-validating:** editing a workflow file no longer self-triggers its
heavy gate (kept out of each regex), so **this PR's own checks exercise
the skip path** — the gates should report *skipped/success* here,
demonstrating the unblock live.

## Draft — why

I can't runtime-test the `run=true` branch (it needs the GH runners),
and this changes how **required** gates fire, so it wants your eyes
before merge. Mark ready / squash-merge when you're satisfied. After it
lands, re-check #213/#215 — they should flip from `blocked` to
mergeable.

> Out of scope (flagged, not fixed): the pre-existing shellcheck infos
in `abi-drift`/`zig-test` original scripts, and the Hypatia baseline
backlog (stale `GEMINI.md`, unpinned `governance.yml` action,
`missing_timeout_minutes` on several workflows) — these are non-blocking
and belong in a separate hygiene pass.

https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa

---
_Generated by [Claude
Code](https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa)_

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 272 issues detected

Severity Count
🔴 Critical 15
🟠 High 138
🟡 Medium 119

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Issue in abi-drift.yml",
    "type": "missing_timeout_minutes",
    "file": "abi-drift.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "missing_timeout_minutes",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in container-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "container-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "missing_timeout_minutes",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath merged commit cb021c4 into main Jun 13, 2026
48 checks passed
@hyperpolymath hyperpolymath deleted the phase-e-smoke-verb-canary-expansion branch June 13, 2026 12:08
hyperpolymath added a commit that referenced this pull request Jun 13, 2026
…219)

Documentation-only follow-up to #216 — captures the required-gate
"skip-shim" as a durable convention so it can't be silently regressed,
and records the work in machine-readable state. **No workflow or code
changes.**

## What's added
- **`docs/AI-CONVENTIONS.adoc`** — new "CI / Required Status Checks"
section (the do/don't rule). Also fixes a **stale** Banned-Languages
row: `TypeScript → AffineScript` (not ReScript — retired as the
destination 2026-04-30, per CLAUDE.md).
- **`.claude/CLAUDE.md`** — matching "CI / Required Status Checks"
section for AI agents.
- **`docs/wikis/CI-and-Required-Checks.adoc`** — new developer wiki
page: the problem (path-filtered required check → permanent "Expected" →
blocked PR), the fix, a copy-paste workflow template, a do/don't table,
and the green-but-blocked diagnostic. Linked from `Home.adoc`.
- **`.machine_readable/6a2/STATE.a2ml`** — 2026-06-13 session entry
(#216 + merged #213/#215 + follow-ups #218/#46/#47/#48); `last-updated`
bumped.
- **`.machine_readable/6a2/PLAYBOOK.a2ml`** — `[ci-required-gates]`
runbook section.
- **`CONTRIBUTING.md`** — brief pointer note.

Both a2ml files verified to still parse as TOML. Draft for your review.

https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa

---
_Generated by [Claude
Code](https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa)_

Co-authored-by: Claude <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jun 20, 2026
…229)

## Summary

Wires `scripts/hcg-surface-drift-check.sh` (landed in boj-server#228,
merged 2026-06-19) into GitHub Actions, so the surface⊆policy invariant
the ADR calls its largest declared risk is re-proven on every PR rather
than relying on the manual re-verification stamp in
`config/gateway-policy-boj.yaml`'s header.

PR #228 explicitly flagged this CI wiring as the follow-up step — "a CI
wiring PR should follow [the always-trigger + changes-job] pattern. Out
of scope here." This is that follow-up; the script, the router, and the
policy are unchanged.

## What lands

A single new file: `.github/workflows/hcg-surface-drift.yml`. The
workflow follows the boj-server "always-trigger + changes-job" pattern
documented in `docs/wikis/CI-and-Required-Checks.adoc` and
`.claude/CLAUDE.md` §"CI / Required Status Checks":

- **No `on.*.paths`** — the check is always created. A path-filtered
required workflow that never fires is the failure mode that stranded
#213/#215 until #216 fixed it; this gate is built to never re-introduce
it, regardless of whether it later joins `required_status_checks`.
- **Lightweight `changes` job** recomputes relevance via `git diff
origin/<base>...HEAD` against the four paths this gate cares about —
router (`elixir/lib/boj_rest/router.ex`), live policy
(`config/gateway-policy-boj.yaml`), the drift script
(`scripts/hcg-surface-drift-check.sh`), and the workflow file itself.
Fail-safe to `run=true` on any diff failure.
- **Heavy `check` job** is `needs: changes` + `if:
needs.changes.outputs.run == 'true'`. A skipped `if:` reports SUCCESS to
any future required-context list, so unrelated PRs never pay for it and
can never be blocked by it.
- **Pinned action**, **timeout-minutes**, **concurrency group**,
**`permissions: contents: read`**, **SPDX header** — matches the
canonical pattern in `.github/workflows/abi-drift.yml`.

The `check` job invokes the script with `bash
scripts/hcg-surface-drift-check.sh -v` (matching the test plan in #228)
so it works regardless of the script's file mode — #228 committed the
script as 0644.

## What this PR does NOT do

- **Does NOT** modify the runbook §1.5 ("Gateway-side prerequisites").
Adoption of the CI gate into the §1.5 checklist is a one-line
owner-driven runbook update — the PR #228 deliberate boundary stays in
place.
- **Does NOT** add the new check to `.github/settings.yml`'s
`required_status_checks` list (currently `hypatia-scan` + `codeql`).
Promotion to required is a settings change for the owner to make once
the gate has run green on a few PRs.
- **Does NOT** modify the live policy, the example policy, the router,
the script, or any other Phase E artefact. The change is wholly within
`.github/workflows/`.
- **Does NOT** pre-empt the §6.4 Trustfile flip (`tier_2_gateway.status`
stays `PENDING`), the staging soak (§3.3), or cerro-torre `.ctp` signing
— all of which remain owner-driven per the channel doctrine reaffirmed
in #207 / #224.
- Per the single-lane HCG channel discipline (pattern set in
`http-capability-gateway` PRs #14, #22, #26, #30, #38 and `boj-server`
PRs #168, #173, #224, #226, #228): joint-close is owner-only. **This PR
refs but does not close `standards#100`.**

## Channel state note

This session could not read `hyperpolymath/standards#91` / `#100` (the
session's MCP repo scope is restricted to `http-capability-gateway` and
`boj-server`), so the brief's instructed status comment on
`standards#91` could not be posted. State was reconstructed from the
canonical sources in this repo (ADR-0004, the integration plan, the
audit, the rollout runbook, the live policy,
`docs/wikis/CI-and-Required-Checks.adoc`) plus the merged-PR history of
both in-scope repos. Analysis: Phase A/B/C/D closed; Phase E
(`standards#100`) is the only open phase; #228 (2026-06-19) is the most
recent advance and explicitly named this CI wiring as the next step.

## Test plan

- [ ] **Required**: the `changes` job runs and emits `run=true` (because
`.github/workflows/hcg-surface-drift.yml` matches the path regex), so
the `check` job is gated through, not skipped, on this PR.
- [ ] **Required**: the `check` job runs `bash
scripts/hcg-surface-drift-check.sh -v` and exits 0 with the OK message —
current `main` (64a70c5) has 7 wired routes, 28 policy rules, no drift;
locally re-verified on this branch.
- [ ] **Synthetic skip**: on a follow-up PR that touches none of the
four watched paths, `changes.outputs.run` is `false` and `check` reports
`skipped` (which counts as success for any required-context list).
- [ ] **Synthetic drift**: a temporary PR adding `get "/__drift_test__"`
to `elixir/lib/boj_rest/router.ex` without a matching policy rule fires
`run=true`, `check` exits 1 with the route listed under `DRIFT:`, and
the PR is blocked from merge if/when this gate is promoted to required.
- [ ] No `actionlint` / Hypatia / SPDX gate fires on the new workflow
file.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with [Claude Code](https://claude.com/claude-code)


---
_Generated by [Claude
Code](https://claude.ai/code/session_019cKmxx6AkNjzhXT6ZoxGfx)_

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jun 21, 2026
## Summary

Adds `scripts/hcg-spec-coverage-check.sh`: a static, source-only audit
that asserts every HTTP route declared in
`docs/specification/openapi.yaml` is covered by at least one rule in the
HCG live Verb Governance Spec (`config/gateway-policy-boj.yaml`).

Companion / complement to PR #228's `hcg-surface-drift-check.sh`. The
two scripts bracket the contract §8 declared-surface invariant from both
directions:

| Script | Invariant | Catches |
|---|---|---|
| `hcg-surface-drift-check.sh` (#228) | wired (router.ex) ⊆ policy |
policy lag behind wiring |
| `hcg-spec-coverage-check.sh` (this PR) | declared (openapi.yaml) ⊆
policy | policy lag behind the spec |

Contract §8 (`docs/integration/http-capability-gateway-boj-contract.md`)
is explicit: "the Verb Governance Spec governs the **declared** surface
(openapi.yaml), not only the currently-wired subset.
Declared-but-unimplemented routes are still classified in the policy so
that when the gnosis handler grows them they are governed from day one
rather than silently exposed." The live policy header carries the
cross-check statement (*"Surface source:
docs/specification/openapi.yaml, cross-checked against
elixir/lib/boj_rest/router.ex"*); PR #228 made the router half
machine-checkable, this PR makes the openapi half machine-checkable.
Together they make the entire §1.5 re-verification stamp executable.

Without this check the risk is concrete: someone adds a new path to
`openapi.yaml` without a corresponding policy rule. The surface-drift
check does not catch it (the route is not yet wired in `router.ex`). The
day the route is wired, the surface-drift gate fires — but by then the
operator has to either (a) ship the wiring with a default-deny in
production for a route that should be live or (b) hold the wiring PR
until the policy catches up. Catching the gap at spec-edit time avoids
both, with no procedural cost above running the existing CI gate.

### What the script does

1. Extracts `(verb, path-template)` tuples from the `paths:` section of
`docs/specification/openapi.yaml` — path entries at exactly 2-space
indent, HTTP operations (get/post/put/delete/patch/head/options) at
exactly 4-space indent under each path. Other keys at 4-space indent
(parameters/summary/description/tags/...) are metadata, not operations,
and are skipped.
2. Extracts `(verb, path-pattern)` tuples from
`config/gateway-policy-boj.yaml` using the identical extraction block
that `hcg-surface-drift-check.sh` uses, so the two scripts cannot drift
in how they read the policy.
3. For each declared route, concretises `{name}`-style placeholders with
a known probe segment (`probe`, shared with the smoke + surface-drift
scripts so a future regex tightening fails all three in lock-step) and
asserts at least one policy rule covers it: literal equality for
non-regex paths; ERE `grep -E` match against the concrete URL for `^…`
regex paths. The declared verb must be in the policy rule's verb list.
4. Exit `0` on no gap, `1` on gap detected, `64` on bad usage.

### What this PR does NOT do

- Does **not** modify the rollout runbook §1.5 or the contract §8.
Adoption as the §1.5 declared-surface check is a separate, owner-driven
PR; this PR lands the artefact only so the runbook update is a one-line
wiring change. Matches the §228-then-runbook split.
- Does **not** wire the script into CI. Boj-server's CI discipline
(`docs/wikis/CI-and-Required-Checks.adoc` / `.claude/CLAUDE.md`)
requires path-filtered required checks to use the "always-trigger +
changes job" pattern; a CI wiring PR should follow that pattern,
matching the #228#229 split. Out of scope here.
- Does **not** modify the openapi.yaml or the policy. On this branch the
script reports OK against today's surface — every one of the 26 `(verb,
path)` pairs declared in openapi.yaml has a matching rule among the 28
`(verb, path)` rules in the live policy. The 2-rule surplus is the
policy's coverage of routes the openapi.yaml does not declare (notably
`/.well-known/boj-node-pubkey`, which the router wires but the spec does
not yet enumerate); the script intentionally does not penalise that
direction — see the script's `Limitations` header.
- Does **not** pre-empt the §6.4 Trustfile flip (`tier_2_gateway.status`
stays `PENDING`).
- Per single-lane HCG channel discipline (pattern set in
`http-capability-gateway` PRs #10, #11, #12, #14, #22, #26, #30, #38 and
`boj-server` PRs #78, #90, #106, #168, #173, #207, #208, #210, #215,
#222, #224, #226, #228, #229): joint-close is owner-only. **This PR refs
but does not close `standards#100`.**

### Channel state note

This session could not read `hyperpolymath/standards#91` / `#100` (the
session's repository scope is restricted to `http-capability-gateway`
and `boj-server`), so the brief's instructed status comment on
`standards#91` could not be posted. State was reconstructed from the
canonical sources in this repo (ADR-0004, the integration plan, the
audit, the rollout runbook, the live policy, the openapi spec, and the
merged-PR commit history) plus the current `main` of both in-scope
repos. The analysis: Phase A/B/C/D are closed (artefacts merged, runbook
§1.2 and the Phase-D status note in the runbook header confirm); Phase E
(`standards#100`) is the only open phase; all remaining §1 checklist
items are owner-driven (`!OWNER:` placeholders, D-4 rebaseline
`workflow_dispatch`, cerro-torre `.ctp` signing, the §6.4 Trustfile
flip). This PR advances Phase E §1.5 ("Gateway-side prerequisites") by
converting one half of the declared-surface invariant into an executable
artefact, mirroring exactly the script-first split of #228.

## Test plan

- [ ] Run the script on this branch's working tree: `bash
scripts/hcg-spec-coverage-check.sh` — expect exit `0`, "OK: every
openapi-declared route is covered by at least one policy rule." with
`Declared (openapi) routes: 26` and `Policy (verb,path) rules: 28`.
- [ ] Run `bash scripts/hcg-spec-coverage-check.sh -v` — expect the same
exit `0` plus a `Matched:` block listing each of the 26 declared routes
against its policy rule (literal `/health` → literal rule;
`/cartridge/{name}/invoke` → `^/cartridge/[A-Za-z0-9_.-]+/invoke$`
regex; `/grpc/{service}/{method}` → two-segment regex; `/umoja/peers`
matches both `GET` and `POST` rules; etc.).
- [ ] Synthetic gap test: build a temporary openapi.yaml containing a
single declared path with no policy rule and run `OPENAPI_FILE=... bash
scripts/hcg-spec-coverage-check.sh` — expect exit `1` with the route
listed under `GAP:`. (Verified locally on this branch.)
- [ ] Confirm `shellcheck scripts/hcg-spec-coverage-check.sh` produces
only the same `SC1001` info note that
`scripts/hcg-surface-drift-check.sh` produces today (the `\^` escape
inside a `case` pattern is intentional and matches the sibling script's
posture exactly).
- [ ] Confirm SPDX header + Owner copyright match the canonical estate
format (matches `scripts/hcg-surface-drift-check.sh`'s header shape).
- [ ] Verify `scripts/check-shebang-first.sh` is still green with the
new file present.
- [ ] Verify no Hypatia / governance / spdx gates fire on the new script
file.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
_Generated by [Claude
Code](https://claude.ai/code/session_013VLPKSTEMFnPYQdx6rD91b)_

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant