Repository navigation
Boundary register: verify cross-boundary identifiers against records - #24
Conversation
…ecords Mismatched identifiers across boundaries (a role never granted the table it queries, an alert filtering on a resource type nothing emits, a log formatter writing "level" where the platform reads "severity", an uptime check on a service name that does not exist, a project-wide grant breaking an isolation claim) are defects no component-local test catches. Pact now refuses to assume such values. - src/pact/boundary_register.py: boundary-register/1 schema (shared with the factory and Constrain), mechanical verifier (token match, sha256 digest, placeholder refusal, root-confined paths, equal vs absent relations, near-miss detection), renderers, and the shared prompt rule. - contracts.py: validate_boundary_register() + find_boundary_usages(), wired into validate_all_contracts() as hard errors; a used boundary (external: names, network_call, database_/http_/external_ side effects) must be registered. Projects without cross-boundary usage are unaffected. - ComponentContract.boundary_identifiers carries the same entry shape. - cli/mcp/decomposer pass the project root to the gate; pact validate exits 1 on failure; new `pact boundary verify [project]`. - Interview, shaper and contract author prompts: cite the record or raise an open question; the contract author sees the verified register. - Handoff briefs render registered literals in tier 1 (never compressed); render_dependency_map includes them. - tests/test_boundary_register.py (63 tests); README, CLAUDE.md, REVIEW.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`pact boundary cite --kind file|observation --path <rel> --value <literal> [--line N] [--command "<cmd>"] [project]` reads the file under the project root with the verifier's path safety, hashes the whole file, finds (or confirms) the line containing the value as a token, requires --command and stamps observed_at (UTC RFC3339) for observations, and prints the record as JSON. It refuses with exit 1 when the literal is not where the record would claim, so every produced record round-trips through the verifier. The shared prompt rule now says records come from `pact boundary cite`, never typed. README/CLAUDE.md updated; 7 new tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Confidence Score: 3/5
Summary
Adds a mechanical boundary-register gate (file+contract entries, sha256-pinned records) as hard errors in validate_all_contracts, CLI and MCP. Code is sound and well-tested; coverage is per-component not per-usage, and a prompt instruction conflicts with the duplicate-id rule.
Important Files Changed
| File | Overview |
|---|---|
| src/pact/boundary_register.py | New mechanical verifier and models; placeholder, line-count and absent-mode edge cases |
| src/pact/contracts.py | Hard-error gate wired into validate_all_contracts; coverage per-component, regex wider than docs |
| src/pact/schemas.py | Adds boundary_identifiers list to ComponentContract with default |
| src/pact/cli.py | New pact boundary verify; pact validate now exits 1 on failure |
| src/pact/decomposer.py | Interview prompt rule; register rendered to contract author; gate receives project_dir |
| src/pact/mcp_server.py | pact_validate passes project_dir to gate |
| src/pact/implementer.py | Loads register entries into every handoff path |
| src/pact/interface_stub.py | Boundary literals rendered in tier 1 and dependency map |
| src/pact/agents/code_author.py | Threads boundary_entries into handoff brief |
| src/pact/agents/contract_author.py | Boundary rule in system prompt; copy-verbatim instruction conflicts with duplicate-id gate |
| src/pact/agents/shaper.py | Appends boundary rule to shaper system prompt |
| tests/test_boundary_register.py | 63 tests; none for per-usage coverage or denies-value semantics |
| README.md | Boundary register section; under-states side-effect prefixes gated |
| CLAUDE.md | Command and architecture notes; same prefix under-statement |
| REVIEW.md | New automated-review guidance for the repo |
Findings
- Gate now fails every existing project declaring database_/external_ side effects, after full decomposition LLM spend (scheduler state.fail); add CHANGELOG/migration note and a record sha256 helper.
Reviewed 1ebec55...17f4825 · ↻ Re-run review · View in Adapt
| covered = any(cid in ps and external in ps for ps in party_sets) | ||
| need = f"an entry with sides for both '{cid}' and '{external}'" | ||
| else: | ||
| covered = any(cid in ps and len(ps) > 1 for ps in party_sets) |
There was a problem hiding this comment.
Non-external usages are covered by any entry where cid has a counterparty; one log-field entry satisfies database_read, network_call and storage_write; match coverage per usage.
| with rationale if domains is non-empty""" | ||
| with rationale if domains is non-empty | ||
| - boundary_identifiers: for every identifier this component shares with another | ||
| party (other components or external:<system>), add a boundary-register/1 entry |
There was a problem hiding this comment.
Author is told to copy register entries verbatim, but reusing an entry id in boundary_identifiers trips the duplicate-id hard error; instruct 'reference, do not re-add' or tolerate identical copies.
| # cite a record that still verifies. Mechanical; no LLM; errors, never warnings. | ||
|
|
||
| # DataAccessSideEffect.type prefixes that cross a process/system boundary. | ||
| _BOUNDARY_SIDE_EFFECT_RE = re.compile( |
There was a problem hiding this comment.
Regex also gates db_, api_, auth_, storage_, secret_, queue_, iam_ prefixes while README and CLAUDE.md promise only database_/http_/external_; align docs or narrow the regex.
|
|
||
| # A value nobody can cite. "unknown"/"assumed"/"placeholder" are refused as the | ||
| # whole value; TODO/TBD/FIXME as a word anywhere; <...> and ??? anywhere. | ||
| _PLACEHOLDER_WHOLE_RE = re.compile( |
There was a problem hiding this comment.
Placeholder rules refuse legitimate literals: whole-value NA/n/a (region enums) and \bTODO|TBD\b inside todo-service or tbd-queue, plus List; anchor the patterns or allow-list.
| if kind == "file": | ||
| if line_no is None: | ||
| return errors | ||
| lines = text.split("\n") |
There was a problem hiding this comment.
text.split('\n') counts a trailing newline as a line: a 2-line file reports '3 lines' and line 3 passes absent-mode bounds; use splitlines().
| errors.extend(_verify_record( | ||
| rec, where=where, value=side["value"], files=files, mode="present", | ||
| )) | ||
| elif role == "denies" and isinstance(subject, str) and subject: |
There was a problem hiding this comment.
Denies side's own value is never checked and absence is tested over the whole record, so a policy dump where the subject holds any other role fails; scope to that value.
| if kind == "file": | ||
| line = rec.get("line") | ||
| if isinstance(line, bool) or not isinstance(line, int) or line < 1: | ||
| errors.append(f"{where}: file record needs 'line' as a 1-based integer (got {line!r})") |
There was a problem hiding this comment.
Denies-side file records must supply a line that is never read in absent mode; drop the requirement there or document what line to cite.
| sides: list[BoundarySide] = [] | ||
|
|
||
|
|
||
| class BoundaryRegister(BaseModel): |
There was a problem hiding this comment.
BoundaryRegister model is never used; load_register parses the mapping by hand. Use it for parsing or delete it.
|
|
||
| # Verify all entries together so ids are unique across file and contracts. | ||
| all_entries = list(file_entries) | ||
| for _, entries in contract_entries: |
There was a problem hiding this comment.
Contract origin cid is discarded, so errors for embedded entries never name the contract and duplicates always say 'also defined in register'; pass origin per contract.
…ry/Constrain - Token match: '.' and '/' that occur in the value act as joiners, so a joiner followed by an identifier character continues the token (foo.bar is not in foo.bar.baz; /health is not in /health/ready), while an undotted name still matches after an attribute dot (entry.severity). - Placeholder: whole value todo|tbd|tbc|assumed|placeholder|changeme|xxx+|?+ (case-insensitive, trimmed) or any <...> angle placeholder; unknown, none, null and n/a are real enum members, not placeholders. - Prompt rule: a denies side's value names the policy/resource whose record must exclude the subject. - Shared test vectors added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
The same class of defect keeps reaching production: identifiers that two parties must agree on, which nobody checked. Examples:
uptime_urlwhile every uptime check emittedcloud_run_revisionlevelwhile the platform reads onlyseverityBefore this change, Pact had no contract field for these identifiers, and
validate_cross_component_interfacesonly compared names the LLM wrote and only warned. The new rule is that an identifier is never assumed. It must cite a record that can be checked.What
src/pact/boundary_register.py: theboundary-register/1format, shared with the factory and Constrain (same field names). It has lenient Pydantic models and a mechanical verifier with no LLM:equalrelations, with a "near-miss" note when values differ only by case or separatorsabsentrelations for isolation: the subject must not appear as a token in any denies-side recordmea-authdoes not matchmea-auth-runtime)observed_at<...>,???, unknown, assumed)..and symlinks that escape are refusedcontracts.py:validate_boundary_register()andfind_boundary_usages(), wired intovalidate_all_contracts()as hard errors. That meanspact validate, the decomposer gate and MCPpact_validateall enforce it.external:<system>in dependencies, requires or side-effect targets; anetwork_callside effect; or adatabase_*,http_*orexternal_*data-access side effect.ComponentContract.boundary_identifiers: per-contract entries with the same shape. Entry ids must be unique across the register file and all contracts.pact boundary verify [project], which prints the entry id, side and reason for each failure and exits 1 on any error.pact validatenow also exits 1 when the gate fails.BOUNDARY_RULE_PROMPT: cite the record or raise an open question, never guess. The interview step asks a question for each identifier that has no record. The contract author receives the verified register to copy from.render_dependency_map.Tests
tests/test_boundary_register.py: 63 tests. Mutation checks confirm each group goes red when its feature is removed (gate wiring, token boundary, stale digest, absence check, rendering).2465 passedwithSIGNET_SESSIONunset.tests/test_agent_runtime.py::...test_fallback_session_id_is_stable_and_path_scoped, fails wheneverSIGNET_SESSIONis set in the environment. It fails the same way on main (1 failed, 2401 passed), so it is not caused by this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01Sy7XemWaqWysBtf6KHX7T7
Need help on this PR? Tag
@codesmith-botwith what you need.