diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 13035f9..d0ef9af 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -27,7 +27,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "5.0.1", + "version": "5.0.2", "source": "./plugins/agentic-engineering" }, { diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 13035f9..d0ef9af 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -27,7 +27,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "5.0.1", + "version": "5.0.2", "source": "./plugins/agentic-engineering" }, { diff --git a/docs/surveyor-contract-coverage.md b/docs/surveyor-contract-coverage.md new file mode 100644 index 0000000..492b154 --- /dev/null +++ b/docs/surveyor-contract-coverage.md @@ -0,0 +1,110 @@ +# Surveyor contract coverage + +[`surveyor-review-contract.test.sh`](../plugins/agentic-engineering/scripts/surveyor-review-contract.test.sh) +checks the operative instructions in the bundled +[`portfolio-surveyor`](../plugins/agentic-engineering/agents/portfolio-surveyor.agent.md). +The `lint-scripts` CI job discovers this suite beside the plugin's other tests. It checks structural +drift, not whether a model follows the instructions. It introduces no runtime policy or classifier. + +Every clause has a stable test ID. Each must appear inside its own section; the suite removes it +independently and requires rejection even with a copy elsewhere in the document. The corresponding +whitespace-only reflow must pass. Missing, repeated, or reversed section boundaries fail; a reversed +boundary cannot extend a scope to an example at the end of the file. Empty definitions fail. +These controls demonstrate the guards can +detect missing operative instructions without requiring the entire role to retain identical bytes. +They do not parse arbitrary Markdown or prove that nearby contradictory prose is harmless. + +Run the suite from the repository root: + +```sh +bash plugins/agentic-engineering/scripts/surveyor-review-contract.test.sh +``` + +For a direct RED check on a temporary mutated definition, use the test-only interface: + +```sh +bash plugins/agentic-engineering/scripts/surveyor-review-contract.test.sh --check /path/to/mutant.md +``` + +## Coverage ownership + +The inventory for [#95](https://github.com/devantler-tech/agent-plugins/issues/95) is anchored to +the consumer's +[test revision f8fe925d](https://github.com/devantler-tech/monorepo/blob/f8fe925dd1c8c623606af7d8e5bdd554b6a46f37/.claude/scripts/portfolio-surveyor.test.sh), +the latest change to that file before the issue was filed. It contains **28 checks directly against +the surveyor plus three compatibility-overlay loading checks**. The 31 entries below distinguish +those checks; the issue's shorthand is not a claim that all 31 are generic. Other tests in that +consumer file cover its classifier, product cards, security surveyor, and deployment policy. + +“Consumer” means the concrete deployment assertion remains there. A generic replacement pins the +current plugin contract rather than importing a login, namespace, path, or retired spelling. No +consumer overlay or test is removed by this suite. + +| Inventory | Source line | Assertion | Current coverage and owner | +|---|---:|---|---| +| O01 | 68 | Exact local programmed-bot classifier path | Consumer path; E01 pins a consumer-declared exact classifier and exit 0. | +| O02 | 76 | Programmed-bot exemption digest state | E05; ordinary hygiene remains required. | +| O03 | 80 | Botantler is only a classifier candidate | Consumer identity; E01/E04 pin successful classification and fail-closed errors. | +| O04 | 82 | KSail App search identity | Consumer identity; A04 pins exact trusted-login matching. | +| O05 | 84 | Complete current-head commit provenance | E02/E03 pin the full commits endpoint result and final-commit/head match. | +| O06 | 96 | Cursor Automation trusted-author identity | Consumer grant and identity; A04 pins exact matching. | +| O07 | 118 | Connector head match before recency | R06/R07. | +| O08 | 120 | Same-head connector findings win | R08–R11 include the current explicit resolution/re-request exception. | +| O09 | 122 | Abbreviated connector SHA is a head prefix | R02/R03/R12 include minimum length and backtick tolerance. | +| O10 | 126 | Reject abbreviated/full-SHA equality | R02 positively requires prefix matching and forbids full-length equality. | +| O11 | 131 | Well-formed nonmatching marker is stale | R04. | +| O12 | 133 | Missing, malformed, or short marker is none | R05 replaces the weak single-word `absent,` check. | +| O13 | 141 | Failed check-run is not findings | K02/K04 separate the title-based states. | +| O14 | 143 | Failed check-run has an error state | K04 and D02 pin the error and signal grammar. | +| O15 | 149 | Usage-limit signal is representable | D02/T03 pin the generic lane grammar and meaning; no fixed reviewer roster or occurrence count. | +| O16 | 161 | Dependency automation short-circuits | A01/A02, conditional on the consuming contract's designation. | +| O17 | 168 | Budget sampled at start and end | B01/B02 pin the timing and both budgets; no particular forge CLI is required. | +| O18 | 170 | Budget digest grammar | D01 pins the current generic start/end spelling. | +| O19 | 172 | Budget exhausted at start is explicit | B03. | +| O20 | 174 | Every digest carries its budget | D04; B04 covers an unavailable probe. | +| O21 | 176 | Exact Renovate identity | Consumer identity; A01 pins the declared exact identities. | +| O22 | 178 | Exact Dependabot identity | Consumer identity; A01 pins the declared exact identities. | +| O23 | 180 | No heavy automation-owned PR deepening | A02. | +| O24 | 182 | Automation-owned PR does not count as fire | A03. | +| O25 | 772 | All declared writer namespaces are scanned | C01; the actual namespace list stays in the consumer. | +| O26 | 774 | Claim branch matches issue number across lanes | C01/C02/C05 include the issue suffix and takeover suffix. | +| O27 | 776 | Claim scan is not assignee-gated | C03. | +| O28 | 778 | Unassigned writer-lane claim is representable | D03/D07; C04/D08 additionally require the declared lease before using it as a skip. | +| O29 | 188 | Maintenance skill declares compatibility overlay | Consumer loading contract; retained until consumer parity is proven. | +| O30 | 190 | Maintenance skill instructs loading that overlay | Consumer loading contract; retained until consumer parity is proven. | +| O31 | 192 | Maintenance skill must not forbid overlay loading | Consumer loading contract; retained until consumer parity is proven. | + +Additional generic coverage addresses the invariants explicitly named in #95 and their current +semantics: H01 pins complete thread pagination; H02–H05 pin newest-review selection by submission +time, empty newest findings, and stale body findings; R01 pins connector authentication; K01/K05/K06 +pin the check-run versus login split; K03 pins neutral findings; P01/D05/D06 pin complete queries and +incomplete-candidate handling; T01/T02 pin evidence-bearing, per-lane `none` output. + +The separate existing suites retain their own responsibilities: + +- `portfolio-surveyor-agent.test.sh`: the prescribed dependency-summary projection and guard admission. +- `surveyor-open-pr-links.test.sh`: the prescribed linked-PR count projection and malformed responses. +- `surveyor-selection-contract.test.sh`: section-scoped ranking and actionability obligations. +- The forge-guard, adapter, and classifier suites: executable command-boundary behavior. + +Passing these checks does not authorize deleting the consumer overlay. The consumer must still +verify its current overlay/procedure parity and preserve deployment-specific coverage before +removing duplicated generic assertions. Its concrete identities, lease policy, local classifier, +provider wiring, and product-specific tests remain consumer-owned. + +## Model-behavior evaluation + +[`fixtures/surveyor-review.json`](../plugins/agentic-engineering/scripts/fixtures/surveyor-review.json) +contains independent synthetic cases and separately keyed expected outcomes. Give an evaluator +only `consumerContract` and `cases`, together with the current bundled agent definition. Compare +the returned decisions with `expected` afterward; do not supply that answer key in its prompt. + +The cases cover stale versus current-head artifacts, same-head findings and their resolution, +backtick-wrapped and too-short markers, spoofed authors, shared vendor logins, neutral check-run +errors versus findings, newest-review body counts, incomplete pagination, branch-only claims, and +classifier failure. They need no credentials, forge access, installed plugin, or mutable runtime. + +Record the evaluated definition revision, evaluator, per-case result, and any disagreement in the +review evidence. This is a bounded behavior sample, not proof of compliance across models or +deployments. Real application discovery remains the separate acceptance work in +[#74](https://github.com/devantler-tech/agent-plugins/issues/74). diff --git a/plugins/agentic-engineering/.claude-plugin/plugin.json b/plugins/agentic-engineering/.claude-plugin/plugin.json index 4cdfbfd..6eefe01 100644 --- a/plugins/agentic-engineering/.claude-plugin/plugin.json +++ b/plugins/agentic-engineering/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "5.0.1", + "version": "5.0.2", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/agentic-engineering/README.md b/plugins/agentic-engineering/README.md index cfc1cbf..df64d58 100644 --- a/plugins/agentic-engineering/README.md +++ b/plugins/agentic-engineering/README.md @@ -219,6 +219,9 @@ improvement loop. The role/configuration boundary remains the one established by [ADR 0002](../../docs/adr/0002-automated-ai-engineer-plugin-boundary.md): portable decision logic lives in this plugin; consumer-owned facts live in `AGENTS.md`. +The [surveyor coverage guide](../../docs/surveyor-contract-coverage.md) identifies the generic +structural guards, the checks that stay in the consumer, and separate model-behavior scenarios. + ## Delivery ownership Every write-capable role owns selected engineering work from claim through exact-head review and diff --git a/plugins/agentic-engineering/plugin.json b/plugins/agentic-engineering/plugin.json index 4cdfbfd..6eefe01 100644 --- a/plugins/agentic-engineering/plugin.json +++ b/plugins/agentic-engineering/plugin.json @@ -1,7 +1,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "5.0.1", + "version": "5.0.2", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/agentic-engineering/scripts/fixtures/surveyor-review.json b/plugins/agentic-engineering/scripts/fixtures/surveyor-review.json new file mode 100644 index 0000000..ad10128 --- /dev/null +++ b/plugins/agentic-engineering/scripts/fixtures/surveyor-review.json @@ -0,0 +1,81 @@ +{ + "purpose": "Model-behavior evaluation, separate from structural CI guards. Give an evaluator only consumerContract and cases with the current portfolio-surveyor definition. Compare its answers with expected afterward. These synthetic cases require no forge or repository access.", + "consumerContract": { + "scope": "Only the fictional example/product repository and the supplied evidence; do not query any service.", + "trustGate": "The connector reviewer is exactly connector-review[bot]. The check-run reviewer is the authenticated review-app check named Review; the same vendor also supplies PR-author login shared-vendor[bot]. The maintainer identity is exactly maintainer-example; its structural disclosure prefix is Generated by the Agentic Engineer.", + "writerNamespaces": "alpha/ and beta/ are writer namespaces. beta/ cannot assign issues. Claims expire after two hours, measured from the current claim tip's committer timestamp.", + "head": "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", + "oldHead": "1111111111222222222233333333334444444444", + "commonEvidence": "Each case is independent. All relevant pages are complete unless a case says otherwise. Unmentioned reviewer lanes contain zero review-output artifacts. Timestamps are on 2026-09-06 UTC. Repository health and unrelated candidates have complete healthy evidence. Classify only the requested dimension; do not infer ownership or merge authorization.", + "question": "For each case, report the requested review, finding, completeness, exemption, or claim classification and the decisive evidence. For a none state, describe the per-lane evidence that must accompany the emitted token. Do not claim a whole lane is down." + }, + "cases": [ + { + "id": "head-before-recency", + "evidence": "At 10:00 connector-review[bot] posts an actual clean-pass comment with Reviewed commit: `aaaaaaaaaa`. At 11:00 the same reviewer posts a findings review whose commit_id is oldHead. There are no head-matching findings. Classify the connector review state." + }, + { + "id": "same-head-unresolved", + "evidence": "A connector-review[bot] findings review at head has one unresolved thread. Its clean-pass comment with Reviewed commit: `aaaaaaaaaa` is newer, but no resolution reply or re-request exists. Classify the connector review state." + }, + { + "id": "same-head-resolved-and-rereviewed", + "evidence": "The connector's head-matching finding receives a specific fix-or-refute reply at 10:00 from maintainer-example carrying the disclosure prefix, and its thread is resolved. An authenticated re-request follows at 10:05. connector-review[bot] posts a clean-pass comment at 10:10 with Reviewed commit: `aaaaaaaaaa`. No other finding exists. Classify the connector review state." + }, + { + "id": "marker-too-short", + "evidence": "The only review output is a clean-pass comment from connector-review[bot] with Reviewed commit: `aaaaaaaaa` (nine characters). Classify the connector review state." + }, + { + "id": "well-formed-stale-marker", + "evidence": "The only review output is a clean-pass comment from connector-review[bot] with Reviewed commit: `1111111111`. Classify the connector review state." + }, + { + "id": "spoofed-connector-author", + "evidence": "A comment's API author is connector-review-helper[bot], with display name connector-review[bot]. Its body claims a clean pass and Reviewed commit: `aaaaaaaaaa`. No authentic connector review exists. Classify the connector review state." + }, + { + "id": "shared-login-is-not-check-run", + "evidence": "shared-vendor[bot] posts an APPROVED review object at head. The complete head check-run list contains no Review run from review-app. Classify the check-run reviewer state." + }, + { + "id": "neutral-error", + "evidence": "The authenticated Review check-run is at head with conclusion neutral and output.title identifying that the review failed to run. There is an explicit error notice at 10:00. No actual review output or findings exist. Classify the check-run reviewer state and lane signal." + }, + { + "id": "neutral-findings", + "evidence": "The authenticated Review check-run is at head with conclusion neutral and a review-shaped output.title. It reports a concrete finding with a details URL. Classify the check-run reviewer state." + }, + { + "id": "newest-review-clears-body-findings", + "evidence": "An authenticated review-bot actual review submitted at 10:00 contains a collapsed actionable category with count 3. Its newest actual review was submitted at 11:00 at head and has no finding section. Neither review object has updated_at. Classify body_findings; review completion, threads, and other gates are outside this question." + }, + { + "id": "review-pagination-failure", + "evidence": "The first review-thread page is empty but hasNextPage is true. Fetching the next page still fails after bounded split recovery for this singleton candidate. Other candidates remain fully observed and healthy. Report whether this candidate can be CLEAR or MERGE-READY and whether unrelated completed evidence is retained." + }, + { + "id": "unassigned-writer-claim", + "evidence": "Issue 42 has no assignee and no open PR. A verified beta/repair-42 branch exists; its current tip's committer timestamp is ten minutes old. The declared two-hour lease applies. Classify this claim and whether it can support a selection skip." + }, + { + "id": "classifier-fails-closed", + "evidence": "The consumer names an exact programmed-bot exemption classifier. The PR's branch and title match its cheap candidate pattern, but the classifier exits 2. Classify the exemption evidence." + } + ], + "expected": { + "head-before-recency": "codex at current head. The ten-character backtick-wrapped marker matches the head prefix; the newer findings object is at an older head and cannot override it.", + "same-head-unresolved": "codex-findings at current head and NEEDS-FIX. A newer same-head clean pass alone does not supersede unresolved findings.", + "same-head-resolved-and-rereviewed": "codex at current head. Every finding has the required later disclosed resolution and resolved thread, followed by the authenticated re-request and subsequent head-matching clean pass.", + "marker-too-short": "none with per-lane evidence: the connector has one actual comment artifact, but its nine-character marker is invalid. No green or stale result may be inferred.", + "well-formed-stale-marker": "codex-stale at 1111111111, not none. A real authenticated review exists at an older head.", + "spoofed-connector-author": "none. The API author is not the exact declared connector identity; the spoofed artifact contributes no trusted connector review output.", + "shared-login-is-not-check-run": "none with zero check-run review output. An approval from the shared login cannot establish a check-run reviewer green.", + "neutral-error": "bugbot-error at current head, green_review none with zero actual review-output counts, plus an error LANE-SIGNAL at 10:00. This is not a findings result or evidence for a portfolio-wide outage.", + "neutral-findings": "bugbot-findings at current head and NEEDS-FIX, with the details URL. Neutral alone is not green or a failed-run classification.", + "newest-review-clears-body-findings": "body_findings=0 at current head, selected by submitted_at. Do not sum the older three findings or fall back because the newest review lacks a finding section.", + "review-pagination-failure": "Candidate-scoped QUERY-UNKNOWN/incomplete; never CLEAR or MERGE-READY. Retain unrelated completed evidence, and report nothing_on_fire false because a mandatory query remains failed.", + "unassigned-writer-claim": "CLAIMED with assignee=none(beta), naming beta/repair-42 and no open PR. The verified ten-minute claim is within the declared lease and can support a selection skip.", + "classifier-fails-closed": "Survey error/QUERY-UNKNOWN, not exempt-programmed-bot. A candidate-shaped branch or title cannot replace a successful classifier verdict." + } +} diff --git a/plugins/agentic-engineering/scripts/surveyor-review-contract.test.sh b/plugins/agentic-engineering/scripts/surveyor-review-contract.test.sh new file mode 100755 index 0000000..02ded50 --- /dev/null +++ b/plugins/agentic-engineering/scripts/surveyor-review-contract.test.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# Structural drift guards for the generic surveyor contract (#95). +# These checks pin operative instructions, not a model's compliance with them. +# Each clause is removed independently; a copy outside its section cannot rescue it. +# See docs/surveyor-contract-coverage.md for the consumer split and behavior scenarios. +set -euo pipefail + +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +SURVEYOR="$HERE/../agents/portfolio-surveyor.agent.md" + +# Report a harness failure on stderr and stop; contract diagnostics use stdout. +fail() { + printf 'surveyor review contract: FAIL — %s\n' "$*" >&2 + exit 1 +} + +# Keep scopes narrower than the whole document: a phrase in an example, digest, +# or another review lane does not establish the operative rule for this lane. +scope() { + case "$1" in + budget) START='### 0. Budget sample'; END='### 1. Open PRs' ;; + claims) START='### 2. Claim branches'; END='### 3. Short-circuit' ;; + automation) START='### 3. Short-circuit'; END='### 3a. Maintainer-login' ;; + hygiene) START='### 3b. Hygiene pentad'; END='### 3c. (e) Green-review' ;; + connector) START='**Connector lane.**'; END='**Check-run lane.**' ;; + check_run) START='**Check-run lane.**'; END=$'**`self@`**' ;; + exemption) START='### 3d. Programmed-bot'; END='### 3e. Review coordination' ;; + pagination) START='### 6. Reconcile the repo set'; END='## Return —' ;; + reporting) START='## Return —'; END='## Survey digest —' ;; + digest_budget) START='## Survey digest —'; END='### Operate' ;; + digest_operate) START='### Operate'; END='### Advance' ;; + digest_advance) START='### Advance'; END='### Digest rules' ;; + digest_rules) START='### Digest rules'; END='' ;; + *) fail "unknown test scope: $1" ;; + esac +} + +# A small test-only extractor. Both boundaries must exist exactly once, in order +# (the final section ends at the next heading or EOF). Whitespace is immaterial. +section_text() { + local source=$1 + awk -v start="$START" -v end="$END" ' + index($0, start) == 1 { starts++; start_line = NR; inside = 1 } + end != "" && index($0, end) == 1 { ends++; end_line = NR; inside = 0 } + end == "" && inside && /^#/ && index($0, start) != 1 { inside = 0 } + inside { text = text " " $0 } + END { + if (starts != 1 || (end != "" && (ends != 1 || end_line <= start_line))) exit 1 + gsub(/[[:space:]]+/, " ", text) + sub(/^ /, "", text); sub(/ $/, "", text) + print text + } + ' "$source" +} + +# Emit the stable ID, owning scope, and required clause for each contract. +# The last field may itself contain pipes (the digest's literal grammar). +requirements() { + cat <<'CLAUSES' +B01|budget|Before any other read, and again immediately before you emit the digest +B02|budget|record `remaining`/`limit` for the **graphql** and **core** budgets at both samples +B03|budget|start sample, still emit the line and mark it `EXHAUSTED_AT_START` +B04|budget|itself fails, emit `budget: unavailable:` once and continue fail-closed +C01|claims|**any writer namespace the deployment's Writer namespaces section records** — not just the lane you happen to be running in +C02|claims|ends in a **takeover suffix** (`--2`, `-3`, …) +C03|claims|**Do not gate this scan on assignees:** +C04|claims|a missing or malformed policy or timestamp makes that candidate's claim join `QUERY-UNKNOWN` +C05|claims|Report a branch that ends in `-` +A01|automation|When the consumer's contract designates dependency-update bots as automation-owned, a PR whose author is one of those **exact** bot identities is automation-owned +A02|automation|Emit only `AUTOMATION-OWNED (NO-ACTION)` from the cheap search row; do **not** deepen it, inspect its pentad or reviews +A03|automation|or count it against `nothing_on_fire` +A04|automation|Match every trusted identity by **exact login, never a substring** +H01|hygiene|Count all unresolved threads across **all pages**, regardless of author; paginate until exhausted +H02|hygiene|**Count ONLY the newest actual review** from that reviewer +H03|hygiene|Select the newest by the reviews endpoint's **submission timestamp** — not an `updated_at` field +H04|hygiene|a newest review with none means findings are cleared (`body_findings=0`); never fall back to an older review that still had sections +H05|hygiene|report `body_findings=-stale@` so the orchestrator re-verifies at head rather than treating it as open +R01|connector|require its API author to exactly match the reviewer App/login that the **Trust gate** assigns to this lane +R02|connector|test whether the **head starts with** the extracted sha — never full-length string equality +R03|connector|Require at least a 10-character prefix +R04|connector|report `codex-stale@`, never `none` +R05|connector|`none` is reserved for a marker that is **absent, malformed, or shorter than 10 characters** +R06|connector|HEAD-MATCH DECIDES FIRST — never rank the two surfaces by recency +R07|connector|a clean pass naming the head wins **even when a findings object exists at an older sha** +R08|connector|Same-sha tie-break: FINDINGS WIN by default +R09|connector|every finding thread carries a later disclosed resolution reply from the exact maintainer login and is resolved +R10|connector|a later authenticated re-request follows the latest such reply +R11|connector|the clean marker names the head and post-dates that re-request +R12|connector|skip the backticks +K01|check_run|The green lives on a check-run — not a review and not a comment +K02|check_run|conclusion alone cannot separate them, so **read `output.title` too** +K03|check_run|`bugbot-findings@` + details URL ⇒ **NEEDS-FIX** +K04|check_run|`bugbot-error@` + a LANE-SIGNAL row; `green_review` is `none` +K05|check_run|Match this lane on the CHECK-RUN only, never on its bot login +K06|check_run|An approval, comment, or review object from that login is **never** a green +E01|exemption|Apply that exemption **only** when the consumer's contract names an **exact classifier** and that classifier exits 0 +E02|exemption|the complete commit list from the forge's commits endpoint (not a summary field that omits raw committer provenance) +E03|exemption|The last commit must equal the head, so an adaptation commit revokes the exemption +E04|exemption|exit 2 or any query/classifier failure is a survey error and **fails closed** +E05|exemption|report `green_review=exempt-programmed-bot` and never classify them NEEDS-FIX for lacking a review — their (a)–(d) hygiene still counts +P01|pagination|if a result set reaches it, paginate or raise it and say so, rather than surveying a partial list +T01|reporting|**A bare `none` is never emittable** +T02|reporting|**Per-lane, never combined:** an aggregate count lets one lane's stale artifact mask another lane's missed one +T03|reporting|`usage-limit` is the spend-exhausted reason — distinct from `rate-limit` because it states no window and only the maintainer can lift it +D01|digest_budget|budget: graphql=/ · core=/[ · EXHAUSTED_AT_START] +D02|digest_operate|lane_signal=:@ +D03|digest_advance|CLAIMED: assignee=|none(), claim-branch=, no open PR +D04|digest_rules|**Always emit the `budget:` line.** +D05|digest_rules|Any mandatory query — enumeration, pagination, or a review-surface query — that remains failed after the bounded split recovery contract makes its affected candidates incomplete +D06|digest_rules|An incomplete candidate can never be classified clean: no `CLEAR`, `MERGE-READY`, `REVIEW-READY`, or "no signal" +D07|digest_rules|**(2) Lanes that cannot assign:** the branch alone is enough +D08|digest_rules|Apply the consumer's declared duration and timestamp source from step 2 before using it as selection skip evidence +CLAUSES +} + +# Check a supplied definition without changing it. Print every missing rule on +# stdout and return 1 if any rule or section is missing; otherwise return 0. +check_contract() { + local source=$1 id group clause text missing=0 START END + while IFS='|' read -r id group clause; do + scope "$group" + if ! text=$(section_text "$source"); then + printf 'missing section: %s (%s)\n' "$group" "$id" + missing=1 + elif [[ "$text" != *"$clause"* ]]; then + printf 'missing contract: %s\n' "$id" + missing=1 + fi + done < <(requirements) + return "$missing" +} + +# Test-only entrypoint for an explicit RED check on a separately mutated file. +if [ "${1:-}" = '--check' ] && [ "$#" -eq 2 ]; then + check_contract "$2" + exit +fi +[ "$#" -eq 0 ] || fail 'usage: surveyor-review-contract.test.sh [--check FILE]' + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT +check_contract "$SURVEYOR" || fail 'the shipped definition lost an operative contract' + +# Boundary order matters independently of clause presence. Preserve every line +# while moving the connector's end before its start, then try rescuing a removed +# clause from a later example. Neither malformed definition may be accepted. +scope connector +awk -v start="$START" -v end="$END" ' + { lines[NR] = $0 } + index($0, start) == 1 { starting = NR } + index($0, end) == 1 { ending = NR } + END { + for (i = 1; i <= NR; i++) { + if (i == starting) print lines[ending] + if (i != ending) print lines[i] + } + } +' "$SURVEYOR" > "$WORK/reordered.md" +awk ' + { gsub(/Require at least a 10-character prefix/, ""); print } + END { print "\n## Non-operative example\nRequire at least a 10-character prefix" } +' "$WORK/reordered.md" > "$WORK/reordered-rescued.md" +awk -v end="$END" 'index($0, end) != 1' "$SURVEYOR" > "$WORK/missing-end.md" +awk -v end="$END" '{ print; if (index($0, end) == 1) print }' "$SURVEYOR" > "$WORK/repeated-end.md" +for boundary in reordered reordered-rescued missing-end repeated-end; do + if check_contract "$WORK/$boundary.md" > "$WORK/output"; then + fail "$boundary: malformed section boundaries were accepted" + fi + grep -Fq 'missing section: connector (' "$WORK/output" || fail "$boundary: unrelated rejection" +done + +# Replace just one already-validated section. The replacement retains its start +# marker and its neighboring section; the other sections remain byte-identical. +replace_section() { + local replacement=$1 + awk -v start="$START" -v end="$END" -v replacement="$replacement" ' + index($0, start) == 1 { inside = 1; print replacement; next } + inside && end != "" && index($0, end) == 1 { inside = 0 } + !inside { print } + ' "$SURVEYOR" +} + +total=0 +while IFS='|' read -r id group clause; do + scope "$group" + text=$(section_text "$SURVEYOR") + tail_text=${text#*"$clause"} + [[ "$tail_text" != *"$clause"* ]] || fail "$id: mutation is ambiguous (clause occurs twice)" + + # Reflowing the same content is a positive control for the mutation renderer. + replace_section "$text" > "$WORK/reflow.md" + check_contract "$WORK/reflow.md" > "$WORK/output" || fail "$id: whitespace-only reflow was rejected" + + mutated=${text/"$clause"/} + [ "$mutated" != "$text" ] || fail "$id: removal made no change" + replace_section "$mutated" > "$WORK/mutant.md" + printf '\n## Non-operative example\n%s\n' "$clause" >> "$WORK/mutant.md" + if check_contract "$WORK/mutant.md" > "$WORK/output"; then + fail "$id: out-of-section wording rescued the removed contract" + fi + grep -Fxq "missing contract: $id" "$WORK/output" || fail "$id: mutation failed for an unrelated reason" + total=$((total + 1)) +done < <(requirements) + +: > "$WORK/empty.md" +if check_contract "$WORK/empty.md" > "$WORK/output"; then + fail 'an empty definition was accepted' +fi + +printf 'surveyor review contract: PASS (%s clauses; each removal rejected, each reflow accepted; four malformed boundaries and empty definition rejected)\n' "$total"