Skip to content

docs(contracts): document schema-parity coverage boundary and deferred gaps - #17

Merged
undeemed merged 33 commits into
mainfrom
fm/fpsm-parity-gap-doc
Aug 3, 2026
Merged

docs(contracts): document schema-parity coverage boundary and deferred gaps#17
undeemed merged 33 commits into
mainfrom
fm/fpsm-parity-gap-doc

Conversation

@undeemed

@undeemed undeemed commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Intent

Two independent small documentation changes on one PR, deliberately not expanded: (1) document the contract-parity coverage boundary in AGENTS.md - the parity helpers compare property names, the required set, and additionalProperties only, never a property type or its constraints, and the repo compensates with hand-written per-field tests, a convention previously written down nowhere; (2) replace the core-dump .gitignore patterns with a supplied fleet v6 block, verbatim and frozen.

This branch is governed by a ruling ledger the captain owns:
/home/fleet/.treehouse/code-5eb5c1/4/code/data/fpsm-parity-gap-doc/ruling-ledger.md
Rows R1-R60a. Read it before proposing edits; most apparent defects are already ruled there, including several the captain ruled DECLINE. Scope is documentation and .gitignore only (R6b): no schema file, no executable code, no test assertion. Doc-comment /// lines are in scope. The .gitignore core-dump block and its comment are frozen byte-identical (R2, R18).

Four rulings are applied and pending as of head f6b33b7: R57/R57a (parameterize the bind definiendum; bind takes a schema as OBJECT and the constraint rides in an explicit "for" slot on the predicate, never as the object, and a participle on the subject does not supply that slot), R58, R59/R59a, R60.

What Changed

  • Documented the contract-parity coverage boundary in AGENTS.md and in the doc comments on assert_object_parity (crates/contracts/src/lib.rs) and assert_same_shape (crates/contracts/src/test_support.rs): the parity helpers compare property names, required, and additionalProperties only, so a mismatched type or a dropped bound stays green, and any field constrained beyond type or carrying a deserialize_with validator needs its own dedicated test that binds every checked-in schema carrying that constraint. Named the hand-rolled sites (response_variants_match_schema, capability_fields_match_capability_schema, manifest_fields_match_sidecar_schema) where the comparison takes no generated schema in. Also corrected the test_support module doc, which claimed all schema-sync modules share one comparison.
  • Added a "Deferred contract work" section to docs/ARCHITECTURE.md registering three gaps as classes with derivable membership: fpsm-lease-ceiling-parity (missing maximum on $defs.ChangeRequest.lease_seconds in the broker request schema plus the gateway's independently stated ceiling), fpsm-unbound-carrier-parity (constraints whose test binds fewer schemas than carry them), and fpsm-capid-guard (checked-in constraints with no Rust-side counterpart to compare against). Scoped the broker schema-sync claim in the same file to what the contract tests actually reach.
  • Replaced the core / core.* crash-dump ignores in .gitignore with a depth-matching block (**/core, **/core.[0-9]*, **/core.*.[0-9]*) plus a !**/core/ negation so directories named core are no longer silently untracked; dumps land in the crashing process's cwd, which for cargo test is the package root, not the repo root.

Risk Assessment

✅ Low: Documentation and .gitignore only - no executable code, schema, or test assertion changed - and every factual claim in the added prose was verified against the cited tests, Rust types, and checked-in schemas.

Testing

Baseline and final cargo test --workspace are green. The AGENTS.md coverage-boundary claim was exercised by mutating checked-in schemas rather than by reading the helpers: a 15-row matrix and an exhaustive 31-keyword sweep confirm property names, required, and additionalProperties are caught while a drifted type or a dropped bound is not, except at the specific per-field tests the doc names; 17 constraint carriers turn out to be bound by no test, and every worked example in the new ARCHITECTURE.md section is in that list. The fpsm-lease-ceiling-parity entry reproduces exactly, ending with a green workspace and a gateway still publishing the old ceiling. The .gitignore block was exercised with real files at real repo paths, before and after, showing four dump shapes ignored and three previously-swallowed source paths restored. No screenshot was possible - no browser runs in this sandbox (chromium missing libnspr4/libnss3, browser-harness symlinked to a macOS path) - so the markdown surface is captured as a rendered HTML file instead; separately, the "byte-identical to the supplied fleet v6 block" requirement is a provenance claim with no source to diff against, though the block's behavior is fully verified.

Evidence: Parity mutation matrix: what the contract suite does and does not catch

--- A. keywords the parity helpers DO compare (names / required / additionalProperties) --- A1 property name renamed broker-request ChangeRequest.capability_id -> cap_id RED as documented caught by: ipc::tests::change_request_fields_match_request_schema_defs A2 required entry dropped broker-request ChangeRequest drops lease_seconds RED as documented A3 additionalProperties flipped broker-request ChangeRequest false -> true RED as documented --- B. property type: NOT compared by any parity helper (documented gap) --- B1 type drift, unasserted field broker-request lease_seconds integer -> string GREEN as documented B2 type drift, unbound carrier experiment change_request.parameters object -> string GREEN as documented B3 type drift, bound carrier broker-request ChangeRequest.parameters object -> string RED as documented caught by: ipc::tests::change_request_parameters_are_an_object_in_both --- C. constraints beyond type: caught only where a dedicated test binds that schema --- C1 protocol_version minimum sidecar.schema.json drops minimum RED as documented C2 hypothesis maxLength experiment.schema.json 500 -> 5000 RED as documented C3 lease ceiling (bound) experiment change_request.lease_seconds drops maximum RED as documented C4 lease floor (unbound) experiment change_request.lease_seconds drops minimum GREEN as documented C5 lease floor (unbound) broker-request ChangeRequest.lease_seconds drops minimum GREEN as documented C6 min_samples minimum experiment decision_bounds lone carrier drops minimum GREEN as documented C7 targets minItems sidecar.schema.json ProviderManifest.targets drops minItems GREEN as documented C8 capability_id pattern broker-request drops pattern GREEN as documented C9 capability_id minLength experiment drops minLength GREEN as documented worktree after matrix: 0 modified schema files

=== Contract-parity coverage boundary: mutation matrix ===
target commit: 53ee7cf   (no-mistakes(review): name field referent, make constraint the sweep unit)

--- A. keywords the parity helpers DO compare (names / required / additionalProperties) ---
A1 property name renamed           broker-request ChangeRequest.capability_id -> cap_id RED   as documented
                                     caught by: ipc::tests::change_request_fields_match_request_schema_defs
A2 required entry dropped          broker-request ChangeRequest drops lease_seconds RED   as documented
                                     caught by: ipc::tests::change_request_fields_match_request_schema_defs
A3 additionalProperties flipped    broker-request ChangeRequest false -> true     RED   as documented
                                     caught by: ipc::tests::change_request_fields_match_request_schema_defs

--- B. property type: NOT compared by any parity helper (documented gap) ---
B1 type drift, unasserted field    broker-request lease_seconds integer -> string GREEN as documented
B2 type drift, unbound carrier     experiment change_request.parameters object -> string GREEN as documented
B3 type drift, bound carrier       broker-request ChangeRequest.parameters object -> string RED   as documented
                                     caught by: ipc::tests::change_request_parameters_are_an_object_in_both

--- C. constraints beyond type: caught only where a dedicated test binds that schema ---
C1 protocol_version minimum        sidecar.schema.json drops minimum              RED   as documented
                                     caught by: tests::protocol_version_zero_is_rejected_like_the_schema
C2 hypothesis maxLength            experiment.schema.json 500 -> 5000             RED   as documented
                                     caught by: tests::the_hypothesis_is_bounded_like_the_schema
C3 lease ceiling (bound)           experiment change_request.lease_seconds drops maximum RED   as documented
                                     caught by: tests::lease_seconds_is_bounded_like_the_schema
C4 lease floor (unbound)           experiment change_request.lease_seconds drops minimum GREEN as documented
C5 lease floor (unbound)           broker-request ChangeRequest.lease_seconds drops minimum GREEN as documented
C6 min_samples minimum             experiment decision_bounds lone carrier drops minimum GREEN as documented
C7 targets minItems                sidecar.schema.json ProviderManifest.targets drops minItems GREEN as documented
C8 capability_id pattern           broker-request drops pattern                   GREEN as documented
C9 capability_id minLength         experiment drops minLength                     GREEN as documented

worktree after matrix: 0 modified schema files
Evidence: Constraint carrier sweep: all 31 constraining keywords in schemas/, bound vs unbound

sidecar.schema.json protocol_version minimum BOUND tests::protocol_version_zero_is_rejected_like_the_schema sidecar.schema.json targets minItems unbound - experiment.schema.json change_request/lease_seconds maximum BOUND tests::lease_seconds_is_bounded_like_the_schema experiment.schema.json change_request/lease_seconds minimum unbound - experiment.schema.json decision_bounds/min_samples minimum unbound - unbound carriers: 17 of the swept constraints have no test asserting them - broker-request.schema.json ChangeRequest/capability_id.pattern - broker-request.schema.json ChangeRequest/lease_seconds.minimum - experiment.schema.json change_request/capability_id.minLength - experiment.schema.json change_request/lease_seconds.minimum - experiment.schema.json decision_bounds/min_samples.minimum - sidecar.schema.json targets.minItems (full 17-row list and all 31 sweep rows in the artifact file)

=== Constraint carrier sweep: which checked-in constraints does a test bind? ===
Each row deletes one keyword from one checked-in schema and runs the contract suite.

schema                            field                                       keyword           verdict   binding test
broker-request.schema.json        owner                                       minLength         unbound   -
broker-request.schema.json        ChangeRequest/capability_id                 pattern           unbound   -
broker-request.schema.json        ChangeRequest/lease_seconds                 minimum           unbound   -
capability.schema.json            id                                          pattern           unbound   -
capability.schema.json            description                                 minLength         unbound   -
experiment.schema.json            hypothesis                                  minLength         BOUND     tests::the_hypothesis_is_bounded_like_the_schema
experiment.schema.json            hypothesis                                  maxLength         BOUND     tests::the_hypothesis_is_bounded_like_the_schema
experiment.schema.json            warmup_samples                              minimum           unbound   -
experiment.schema.json            warmup_samples                              maximum           BOUND     tests::sample_counts_are_bounded_like_the_schema
experiment.schema.json            baseline_samples                            minimum           unbound   -
experiment.schema.json            baseline_samples                            maximum           BOUND     tests::sample_counts_are_bounded_like_the_schema
experiment.schema.json            candidate_samples                           minimum           unbound   -
experiment.schema.json            candidate_samples                           maximum           BOUND     tests::sample_counts_are_bounded_like_the_schema
experiment.schema.json            change_request/capability_id                minLength         unbound   -
experiment.schema.json            change_request/lease_seconds                minimum           unbound   -
experiment.schema.json            change_request/lease_seconds                maximum           BOUND     tests::lease_seconds_is_bounded_like_the_schema
experiment.schema.json            decision_bounds/min_samples                 minimum           unbound   -
experiment.schema.json            decision_bounds/min_samples                 maximum           BOUND     tests::decision_bounds_are_bounded_like_the_schema
experiment.schema.json            decision_bounds/min_fps_improvement         minimum           BOUND     tests::decision_bounds_are_bounded_like_the_schema
experiment.schema.json            decision_bounds/max_temperature_c           exclusiveMinimum  BOUND     tests::decision_bounds_are_bounded_like_the_schema
experiment.schema.json            decision_bounds/max_temperature_c           maximum           BOUND     tests::decision_bounds_are_bounded_like_the_schema
experiment.schema.json            decision_bounds/max_power_w                 exclusiveMinimum  BOUND     tests::decision_bounds_are_bounded_like_the_schema
experiment.schema.json            decision_bounds/max_power_w                 maximum           BOUND     tests::decision_bounds_are_bounded_like_the_schema
experiment.schema.json            decision_bounds/max_errors                  minimum           unbound   -
experiment.schema.json            decision_bounds/max_errors                  maximum           BOUND     tests::decision_bounds_are_bounded_like_the_schema
metric-sample.schema.json         errors                                      minimum           unbound   -
sidecar.schema.json               id                                          pattern           unbound   -
sidecar.schema.json               protocol_version                            minimum           BOUND     tests::protocol_version_zero_is_rejected_like_the_schema
sidecar.schema.json               targets                                     minItems          unbound   -
verdict.schema.json               metric_summary/samples                      minimum           unbound   -
verdict.schema.json               metric_summary/total_errors                 minimum           unbound   -

unbound carriers: 17 of the swept constraints have no test asserting them
  - broker-request.schema.json  owner.minLength
  - broker-request.schema.json  ChangeRequest/capability_id.pattern
  - broker-request.schema.json  ChangeRequest/lease_seconds.minimum
  - capability.schema.json  id.pattern
  - capability.schema.json  description.minLength
  - experiment.schema.json  warmup_samples.minimum
  - experiment.schema.json  baseline_samples.minimum
  - experiment.schema.json  candidate_samples.minimum
  - experiment.schema.json  change_request/capability_id.minLength
  - experiment.schema.json  change_request/lease_seconds.minimum
  - experiment.schema.json  decision_bounds/min_samples.minimum
  - experiment.schema.json  decision_bounds/max_errors.minimum
  - metric-sample.schema.json  errors.minimum
  - sidecar.schema.json  id.pattern
  - sidecar.schema.json  targets.minItems
  - verdict.schema.json  metric_summary/samples.minimum
  - verdict.schema.json  metric_summary/total_errors.minimum
Evidence: fpsm-lease-ceiling-parity: stale published ceiling survives a green suite

step 0 baseline MAX_LEASE_SECONDS = 300 experiment.schema maximum = 300 broker-request maximum = None gateway tools/list advertises= 300 suite = GREEN step 1 raise the constant alone (300 -> 600) suite = RED failing: tests::lease_seconds_is_bounded_like_the_schema step 2 bring the experiment schema along, as the failure asks suite = GREEN gateway tools/list advertises= 300 <-- stale, nothing failed reverted; worktree entries dirty: 0

=== fpsm-lease-ceiling-parity: stale published ceiling survives a green suite ===
commit: 53ee7cf

step 0  baseline
        MAX_LEASE_SECONDS            = 300
        experiment.schema maximum    = 300
        broker-request maximum       = None
        gateway tools/list advertises= 300
        suite                        = GREEN

step 1  raise the constant alone (300 -> 600)
        suite                        = RED   failing: tests::lease_seconds_is_bounded_like_the_schema

step 2  bring the experiment schema along, as the failure asks
        suite                        = GREEN
        gateway tools/list advertises= 300   <-- stale, nothing failed

reverted; worktree entries dirty: 0
Evidence: Core-dump ignore rules exercised with real files, new block vs base block

### checked-in .gitignore at HEAD (fleet v6 block) PATH EXPECTED ACTUAL RESULT core ignored ignored ok (bare dump at repo root) crates/control-plane/core ignored ignored ok (dump from 'cargo test' cwd (package root)) sidecars/mock-provider/core.4711 ignored ignored ok (core.<pid> shape) apps/broker/core.broker.4711 ignored ignored ok (core.%e.%p shape) crates/control-plane/src/core/mod.rs tracked tracked ok (source dir named core/ must stay visible) crates/control-plane/src/core.rs tracked tracked ok (source file core.rs must stay visible) apps/broker/core.tuning.md tracked tracked ok (doc named core.<word> must stay visible) ### previous block at base 3f9a580 ("core" + "core.*"), for contrast crates/control-plane/src/core/mod.rs tracked ignored !! MISMATCH crates/control-plane/src/core.rs tracked ignored !! MISMATCH apps/broker/core.tuning.md tracked ignored !! MISMATCH worktree left dirty by this check: 0 entries

=== Core-dump ignore rules, exercised with real files in the real worktree ===
commit: 53ee7cf

### checked-in .gitignore at HEAD (fleet v6 block)
PATH                                     EXPECTED   ACTUAL   RESULT
core                                     ignored    ignored  ok  (bare dump at repo root)
crates/control-plane/core                ignored    ignored  ok  (dump from 'cargo test' cwd (package root))
sidecars/mock-provider/core.4711         ignored    ignored  ok  (core.<pid> shape)
apps/broker/core.broker.4711             ignored    ignored  ok  (core.%e.%p shape)
crates/control-plane/src/core/mod.rs     tracked    tracked  ok  (source dir named core/ must stay visible)
crates/control-plane/src/core.rs         tracked    tracked  ok  (source file core.rs must stay visible)
apps/broker/core.tuning.md               tracked    tracked  ok  (doc named core.<word> must stay visible)

### previous block at base 3f9a580 ("core" + "core.*"), for contrast
PATH                                     EXPECTED   ACTUAL   RESULT
core                                     ignored    ignored  ok  (bare dump at repo root)
crates/control-plane/core                ignored    ignored  ok  (dump from 'cargo test' cwd (package root))
sidecars/mock-provider/core.4711         ignored    ignored  ok  (core.<pid> shape)
apps/broker/core.broker.4711             ignored    ignored  ok  (core.%e.%p shape)
crates/control-plane/src/core/mod.rs     tracked    ignored  !! MISMATCH  (source dir named core/ must stay visible)
crates/control-plane/src/core.rs         tracked    ignored  !! MISMATCH  (source file core.rs must stay visible)
apps/broker/core.tuning.md               tracked    ignored  !! MISMATCH  (doc named core.<word> must stay visible)

worktree left dirty by this check: 0 entries
Evidence: AGENTS.md Repository rules rendered as GitHub-flavored Markdown, new rule highlighted
<!doctype html>
<html><head><meta charset="utf-8"><title>AGENTS.md - Repository rules (rendered)</title>
<style>
 body { margin:0; background:#f6f8fa; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; color:#1f2328; }
 .frame { max-width:1012px; margin:24px auto; background:#fff; border:1px solid #d1d9e0; border-radius:6px; }
 .head { padding:10px 16px; border-bottom:1px solid #d1d9e0; background:#f6f8fa; font-size:14px; color:#59636e; border-radius:6px 6px 0 0; }
 .head b { color:#1f2328; }
 .body { padding:24px 32px 32px; }
 h2 { font-size:24px; padding-bottom:.3em; border-bottom:1px solid #d1d9e0; margin-top:0; }
 code { background:#eff1f3; padding:.2em .4em; border-radius:6px; font-size:85%;
         font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
 li { margin:.35em 0; }
 li:nth-child(2) { background:#dafbe1; outline:2px solid #4ac26b; border-radius:6px;
                    padding:10px 12px; margin:10px -12px; }
 li:nth-child(2)::after { content:"added by this change"; display:block; margin-top:8px;
                    font-size:12px; font-weight:600; color:#1a7f37; letter-spacing:.05em; text-transform:uppercase; }
</style></head><body>
<div class="frame">
 <div class="head"><b>AGENTS.md</b> rendered as GitHub-flavored Markdown - commit 53ee7cf</div>
 <div class="body"><h2>Repository rules</h2>
<ul>
<li>Keep shared wire types in <code>crates/contracts</code>, and keep <code>schemas/*.json</code> in sync with them; the contract tests enforce matching fields and enum strings.</li>
<li>Give every field whose checked-in schema constrains it beyond <code>type</code> (a <code>minLength</code>, a <code>minItems</code>, a <code>pattern</code>, a numeric bound) or that carries a <code>deserialize_with</code> validator its own dedicated test that binds each checked-in schema carrying that constraint, not merely one of them; for a <code>deserialize_with</code> validator, which no checked-in schema can state, that test binds each checked-in schema publishing that field and asserts that field&#39;s declared <code>type</code> there as the counterpart.
A test binds a schema for a constraint by opening the checked-in file and asserting that constraint there; a sibling that only asserts what the Rust type rejects binds nothing and is not the pattern to copy. <code>protocol_version_zero_is_rejected_like_the_schema</code> and <code>the_hypothesis_is_bounded_like_the_schema</code> bind that way and are also complete for the constraints they assert, each opening the schema that carries it: <code>schemas/sidecar.schema.json</code> carries the <code>protocol_version</code> minimum alone, and <code>schemas/experiment.schema.json</code> the hypothesis length bounds alone. <code>change_request_parameters_are_an_object_in_both</code> binds that way for a declared <code>type</code>, but does so for fewer checked-in schemas than carry the one it asserts, the class registered as <code>fpsm-unbound-carrier-parity</code>.
Schema parity reaches property names, <code>required</code>, and <code>additionalProperties</code> only, so a mismatched <code>type</code> or a dropped bound otherwise stays green wherever the comparison is made: <code>assert_object_parity</code> and <code>assert_same_shape</code> compare each of those keywords between the generated and checked-in schemas; <code>response_variants_match_schema</code> hand-rolls the property-name and <code>required</code> comparison between those same sides but pins <code>additionalProperties</code> to <code>false</code> on the checked-in side rather than comparing it, so a generated side that drifts on that keyword stays green there; and <code>capability_fields_match_capability_schema</code> and <code>manifest_fields_match_sidecar_schema</code> compare the field set serde emits for a sample value against the checked-in <code>properties</code> and <code>required</code> and pin <code>additionalProperties</code> to <code>false</code> on that same checked-in side, holding the wire output to the checked-in schema but taking no generated schema in, so a generated side that drifts on any of the three stays green at those sites. Some checked-in schemas carrying such a constraint are bound for it by no test today, and the deferred contract work list in <code>docs/ARCHITECTURE.md</code> registers that gap in both <code>fpsm-capid-guard</code>, where writing a guard that proves parity is blocked, and <code>fpsm-unbound-carrier-parity</code>, where writing such a guard is merely pending; the missing binding test is writable under either.</li>
<li>Keep provider lifecycle behavior in <code>crates/provider-sdk</code>.</li>
<li>Keep the capability registry, policy, broker lifecycle, and experiment journal in <code>crates/control-plane</code>.</li>
<li>Keep the local IPC transport, framing, and peer-authentication seams behind traits in <code>crates/ipc</code> (Unix domain socket now, Windows named pipe later); the privileged broker in <code>apps/broker</code> composes them over the control plane and enforces peer auth, catalog policy, and single-owner-per-knob. The non-<code>Send</code> control plane is confined to one worker thread reached through a <code>Send</code> handle.</li>
<li>Keep the independent crash and lease recovery path in <code>apps/watchdog</code>; it reads the journal owned by <code>crates/control-plane</code> and writes only its own restore-outcome records, never the schema.</li>
<li>Keep the measurement model, immutable evaluator, and replayable trial records in <code>apps/experiment-runner</code>; the evaluator stays a pure function of recorded samples and fixed bounds.</li>
<li>Put provider-specific code in one <code>sidecars/&lt;provider&gt;</code> package; sidecars may not import each other.</li>
<li>Put non-Rust compatibility processes under <code>bridges/</code> and isolate them behind the sidecar protocol.</li>
<li>Do not vendor third-party binaries without confirmed redistribution rights.</li>
<li>Add tests before enabling a real write path.</li>
</ul>
</div>
</div></body></html>
Evidence: Mutation matrix runner (reproducible)
#!/usr/bin/env bash
# Mutation matrix: does the contract suite catch each kind of schema drift?
# Each row mutates a CHECKED-IN schema, runs the contract tests, reverts.
set -u
cd "$WT"
export CC=$PWD/target/zig-cc-native AR=$PWD/target/zig-ar-native
M=/tmp/no-mistakes-evidence/01KZ15E3WZC4QEPJMJJQ6Z3DQY/mutate.py

run() {
  local id="$1" expect="$2" desc="$3"; shift 3
  python3 "$M" "$@" || { echo "$id  MUTATION-FAILED"; return; }
  out=$(cargo test -p fpsmaxxing-contracts --quiet 2>&1)
  if grep -q "^test result: FAILED" <<<"$out" || grep -q "^error" <<<"$out"; then
    got=RED; failed=$(grep -E "^    tests::|^    ipc::tests::" <<<"$out" | tr -d ' ' | paste -sd, -)
  else
    got=GREEN; failed=""
  fi
  [ "$got" = "$expect" ] && verdict="as documented" || verdict="!! UNEXPECTED (expected $expect)"
  printf '%-34s %-46s %-5s %s\n' "$id" "$desc" "$got" "$verdict"
  [ -n "$failed" ] && printf '%-34s   caught by: %s\n' "" "$failed"
  git checkout -- schemas/
}

echo "=== Contract-parity coverage boundary: mutation matrix ==="
echo "target commit: $(git rev-parse --short HEAD)   ($(git log -1 --format=%s))"
echo
echo "--- A. keywords the parity helpers DO compare (names / required / additionalProperties) ---"
run "A1 property name renamed"  RED   "broker-request ChangeRequest.capability_id -> cap_id" \
    schemas/broker-request.schema.json rename '$defs/ChangeRequest/properties/capability_id' cap_id
run "A2 required entry dropped" RED   "broker-request ChangeRequest drops lease_seconds" \
    schemas/broker-request.schema.json droprequired '$defs/ChangeRequest' lease_seconds
run "A3 additionalProperties flipped" RED "broker-request ChangeRequest false -> true" \
    schemas/broker-request.schema.json set '$defs/ChangeRequest/additionalProperties' true

echo
echo "--- B. property type: NOT compared by any parity helper (documented gap) ---"
run "B1 type drift, unasserted field" GREEN "broker-request lease_seconds integer -> string" \
    schemas/broker-request.schema.json set '$defs/ChangeRequest/properties/lease_seconds/type' '"string"'
run "B2 type drift, unbound carrier"  GREEN "experiment change_request.parameters object -> string" \
    schemas/experiment.schema.json set '$defs/change_request/properties/parameters/type' '"string"'
run "B3 type drift, bound carrier"    RED   "broker-request ChangeRequest.parameters object -> string" \
    schemas/broker-request.schema.json set '$defs/ChangeRequest/properties/parameters/type' '"string"'

echo
echo "--- C. constraints beyond type: caught only where a dedicated test binds that schema ---"
run "C1 protocol_version minimum"  RED   "sidecar.schema.json drops minimum" \
    schemas/sidecar.schema.json del 'properties/protocol_version/minimum'
run "C2 hypothesis maxLength"      RED   "experiment.schema.json 500 -> 5000" \
    schemas/experiment.schema.json set 'properties/hypothesis/maxLength' 5000
run "C3 lease ceiling (bound)"     RED   "experiment change_request.lease_seconds drops maximum" \
    schemas/experiment.schema.json del '$defs/change_request/properties/lease_seconds/maximum'
run "C4 lease floor (unbound)"     GREEN "experiment change_request.lease_seconds drops minimum" \
    schemas/experiment.schema.json del '$defs/change_request/properties/lease_seconds/minimum'
run "C5 lease floor (unbound)"     GREEN "broker-request ChangeRequest.lease_seconds drops minimum" \
    schemas/broker-request.schema.json del '$defs/ChangeRequest/properties/lease_seconds/minimum'
run "C6 min_samples minimum"       GREEN "experiment decision_bounds lone carrier drops minimum" \
    schemas/experiment.schema.json del '$defs/decision_bounds/properties/min_samples/minimum'
run "C7 targets minItems"          GREEN "sidecar.schema.json ProviderManifest.targets drops minItems" \
    schemas/sidecar.schema.json del 'properties/targets/minItems'
run "C8 capability_id pattern"     GREEN "broker-request drops pattern" \
    schemas/broker-request.schema.json del '$defs/ChangeRequest/properties/capability_id/pattern'
run "C9 capability_id minLength"   GREEN "experiment drops minLength" \
    schemas/experiment.schema.json del '$defs/change_request/properties/capability_id/minLength'
echo
echo "worktree after matrix: $(git status --porcelain schemas/ | wc -l) modified schema files"
Evidence: Constraint sweep script (reproducible)
"""Sweep every constraining keyword in every checked-in schema and record whether
the contract suite binds it: delete the keyword, run the tests, restore.

This derives, per constraint rather than per field, the class membership that
AGENTS.md and docs/ARCHITECTURE.md describe."""
import json, glob, subprocess, os

KW = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum",
      "minLength", "maxLength", "minItems", "maxItems", "pattern", "multipleOf"]

def occurrences(path):
    doc = json.load(open(path))
    found = []
    def walk(node, ptr):
        if isinstance(node, dict):
            for k, v in node.items():
                if k in KW:
                    found.append((ptr, k, v))
                else:
                    walk(v, ptr + [k])
    walk(doc, [])
    return found

def without(path, ptr, kw):
    doc = json.load(open(path))
    n = doc
    for p in ptr:
        n = n[p]
    del n[kw]
    json.dump(doc, open(path, "w"), indent=2)
    open(path, "a").write("\n")

env = dict(os.environ, CC=os.getcwd() + "/target/zig-cc-native",
           AR=os.getcwd() + "/target/zig-ar-native")

print("=== Constraint carrier sweep: which checked-in constraints does a test bind? ===")
print("Each row deletes one keyword from one checked-in schema and runs the contract suite.")
print()
print(f"{'schema':<34}{'field':<44}{'keyword':<18}{'verdict':<10}binding test")
unbound = []
for path in sorted(glob.glob("schemas/*.json")):
    original = open(path).read()
    for ptr, kw, _ in occurrences(path):
        without(path, ptr, kw)
        r = subprocess.run(["cargo", "test", "-p", "fpsmaxxing-contracts", "--quiet"],
                           capture_output=True, text=True, env=env)
        open(path, "w").write(original)
        failing = sorted({l.strip() for l in (r.stdout + r.stderr).splitlines()
                          if l.startswith("    ") and "::" in l and " " not in l.strip()})
        bound = r.returncode != 0
        field = "/".join(p for p in ptr if p not in ("properties", "$defs"))
        if not bound:
            unbound.append((path, field, kw))
        print(f"{path.replace('schemas/',''):<34}{field:<44}{kw:<18}"
              f"{'BOUND' if bound else 'unbound':<10}{','.join(failing) if bound else '-'}")

print()
print(f"unbound carriers: {len(unbound)} of the swept constraints have no test asserting them")
for path, field, kw in unbound:
    print(f"  - {path.replace('schemas/','')}  {field}.{kw}")

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (5) ✅
  • ⚠️ .gitignore:41 - Old core.* ignored every dotted dump name; the replacement requires a numeric field (**/core.[0-9]*, **/core.*.[0-9]*). A host whose kernel.core_pattern has no numeric component - core.%e (exe only) or core.%h (hostname), both legal patterns - now produces a dump that is no longer ignored. By the block's own stated rationale that is a credential-exposure path, not bloat. Adding **/core.* back after the negation would restore the old breadth without weakening the !**/core/ fix. Flagging rather than fixing because the block is supplied verbatim and frozen.
  • ⚠️ AGENTS.md:18 - The same normative rule now exists in three places: AGENTS.md:18-20, the assert_object_parity doc comment (crates/contracts/src/lib.rs:492-503), and the assert_same_shape doc comment (crates/contracts/src/test_support.rs:59-71). The two Rust copies are near word-for-word identical, and nothing fails if one drifts from the other or from AGENTS.md - a future edit to the rule will silently leave two stale copies asserting the old contract. Compounding it, the AGENTS.md bullet is ~450 words in three sentences averaging ~90 words each, against that file's own bar ("keep entries concise", "point to the authoritative file"); the three docs/ARCHITECTURE.md entries have the same density. Suggest: one authoritative statement plus the pointer to docs/ARCHITECTURE.md, and doc comments that reference it instead of restating it.
  • ℹ️ .gitignore:29 - The comment credits **/ with matching at any depth, but a gitignore pattern with no slash already matches its basename at every level - the previous bare core line ignored crates/&lt;pkg&gt;/core too. The genuinely load-bearing addition is !**/core/ on line 40, which un-ignores directories named core (src/core/, a crates/core package) that the old bare pattern silently swallowed. Noting only; the block is frozen byte-identical by ruling.
  • ℹ️ crates/contracts/src/lib.rs:504 - assert_object_parity and test_support::assert_same_shape implement the same three-keyword comparison, differing only in taking &amp;Value plus a label vs &amp;schemars::Schema. Duplicating the twelve-line doc comment onto both is what made the drift surface twice. Having assert_object_parity delegate to the test_support helper (or exposing a &amp;Value entry point there) would leave one implementation and one place for the caveat.

🔧 Fix: parameterize bind definition, restore per-constraint quantifier
1 warning still open:

  • ⚠️ AGENTS.md:20 - The closing clause reads "blocked as it is in the first entry and merely pending in the second", immediately after naming "the deferred contract work list in docs/ARCHITECTURE.md". "Entry" is the word that list's items go by, and that list orders them fpsm-lease-ceiling-parity, fpsm-unbound-carrier-parity, fpsm-capid-guard (docs/ARCHITECTURE.md:105-107). Under that reading "the first entry" is fpsm-lease-ceiling-parity, which is not blocked - its deferred work is a binding test, writable today - so the sentence is false. The intended antecedent is the inline pair "both fpsm-capid-guard and fpsm-unbound-carrier-parity" earlier in the same clause, where first = capid-guard = blocked. Prior wording ("blocked as it is for capability_id and merely pending for the other") was unambiguous but paired a field against a class; naming the two classes outright - "blocked in fpsm-capid-guard and merely pending in fpsm-unbound-carrier-parity" - pairs class against class as ruled and removes the ordinal entirely.

🔧 Fix: replace status ordinal with per-class attachment in pointer
3 infos still open:

  • ℹ️ AGENTS.md:20 - The sentence lists two additionalProperties blind spots as if equivalent, but only one is real repo-wide. For capability_fields_match_capability_schema / manifest_fields_match_sidecar_schema, generated_schemas_match_checked_in_schemas (crates/contracts/src/lib.rs:609) already runs assert_object_parity over CapabilityDescriptor vs CAPABILITY_SCHEMA and ProviderManifest vs SIDECAR_SCHEMA (schema_cases, lib.rs:560-576), and that helper does compare additionalProperties - so a generated side that drifts there fails the suite. By contrast response_variants_match_schema (crates/contracts/src/ipc.rs:326) is the only test that walks the BrokerResponse oneOf branches; the per-branch assert_same_shape tests only cover $defs entries, so a generated branch dropping deny_unknown_fields really does stay green everywhere. The clause "stays green at those sites" is literally true for both, but a reader closing gaps will write a redundant guard for capability/manifest and may not see that the response-variant hole is unbounded. Naming which of the two is covered elsewhere would fix it.
  • ℹ️ docs/ARCHITECTURE.md:108 - fpsm-capid-guard names ChangeRequest::capability_id and ProviderManifest::targets; fpsm-unbound-carrier-parity names the lease floor, ChangeRequest::parameters, and DecisionBounds::min_samples. Sweeping the checked-in schemas for constraining keywords turns up further carriers neither entry mentions and no test binds: broker-request.schema.json .properties.owner (minLength), capability.schema.json .properties.id (pattern) and .properties.description (minLength), sidecar.schema.json .properties.id (pattern), metric-sample.schema.json .properties.errors (minimum), verdict.schema.json $defs.metric_summary.samples and .total_errors (minimum), experiment.schema.json warmup_samples/baseline_samples/candidate_samples (minimum) and $defs.decision_bounds.max_errors (minimum). The entries are class definitions and say "a further member", so nothing is false; but "Closing the class needs each such Rust type to carry its constraint" is not actionable without knowing the class is roughly a dozen fields rather than the two named. A member count, or a one-line note that the list is derivable by sweeping the schemas for constraining keywords, would make either entry closable.
  • ℹ️ docs/ARCHITECTURE.md:106 - Closing fpsm-lease-ceiling-parity means adding the maximum to $defs.ChangeRequest.lease_seconds in schemas/broker-request.schema.json. The moment that lands, lease_seconds_is_bounded_like_the_schema (crates/contracts/src/lib.rs:457) - which opens only EXPERIMENT_SCHEMA - stops binding every checked-in carrier of the ceiling, so the field that entry currently calls "compliant for its ceiling" (line 107) drops into fpsm-unbound-carrier-parity unless the test is extended in the same change. Neither entry states that coupling; the lease-ceiling entry says only "The deferred work is the binding test", which reads as one test rather than one test plus the schema edit that makes a second carrier exist. One clause noting that the ceiling test must gain the broker request schema alongside the schema edit would keep the two entries from trading a gap back and forth.

🔧 Fix: document class derivability, pin what closes lease-ceiling parity
2 warnings still open:

  • ⚠️ docs/ARCHITECTURE.md:105 - The new derivability line says class membership "is derivable by sweeping the checked-in schemas for constraining keywords". That sweep cannot reach the deserialize_with half of the trigger, and fpsm-unbound-carrier-parity names an instance the sweep misses. AGENTS.md:18 states the trigger as two disjuncts - a checked-in constraint beyond type, OR a deserialize_with validator, "which no checked-in schema can state". The validator lives on the Rust type (crates/contracts/src/lib.rs:90, the workspace's only one, on ChangeRequest::parameters), and its checked-in counterpart is the bare declared type: parameters is {&#34;type&#34;: &#34;object&#34;} in schemas/broker-request.schema.json and schemas/experiment.schema.json, carrying no constraining keyword. So a keyword sweep never surfaces it, yet line 108 names ChangeRequest::parameters as the class's second instance. A reader closing the class off this line closes it without the one member the entry works through by name. Remedy is one added clause covering the second disjunct's own sweep - the Rust types carrying a deserialize_with validator - which adds no count and no enumeration, so R7 is untouched, and keeps the checked-in-schema domain R30/R43 fixed for the general half.
  • ⚠️ docs/ARCHITECTURE.md:107 - R64 asked for the ambiguity to be resolved, and the applied wording resolves it into an exhaustive closure claim that no longer covers the entry's own second registered carrier. The entry registers two defects: the missing maximum in schemas/broker-request.schema.json, and the gateway's tools/list input schema "which no test compares against that constant in agreement". The clause now reads "Closing this is the schema edit together with the binding test, still not a reference from the gateway to the constant" - a closed list of two items, where the prior "The deferred work is the binding test, not a reference..." was the non-exhaustive contrast R64 describes. Neither item reaches the gateway: AGENTS.md:19 defines binding as opening the checked-in file, and the gateway ceiling is inline Rust at apps/gateway/src/lib.rs:67 with no reader of MAX_LEASE_SECONDS anywhere in apps/gateway. The exclusion clause bars only the gateway reference remedy (R15's rejected fix), not a test comparing the copies, so the gateway leg is now closed by nothing named - and the entry's own stale-ceiling-with-green-suite scenario two sentences earlier survives both stated closure steps. Either the closure list needs the gateway comparison as a third item, or the clause needs to say explicitly which registered leg it is scoped to.

🔧 Fix: extend class derivability sweep, add gateway closure item
2 warnings still open:

  • ⚠️ AGENTS.md:18 - The deserialize_with disjunct reads "for a deserialize_with validator, which no checked-in schema can state, that test binds each checked-in schema publishing it and asserts that field's declared type there as the counterpart." The nearest antecedent for "it" is the validator, and the immediately preceding relative clause states that no checked-in schema can state it - so the set the rule quantifies over is empty and the disjunct requires nothing, which is the vacuity R24 exists to remove. The intended referent is the field (or its declared type): the pre-R56 wording was "that field's declared type in each checked-in schema publishing it", where "that field" preceded the pronoun; the R56 role fix moved the noun behind it and broke the chain. It also contradicts line 19 of the same file, where change_request_parameters_are_an_object_in_both is said to bind "for fewer checked-in schemas than carry the one it asserts" - a claim that presupposes a non-empty publishing set - and contradicts the register, which names ChangeRequest::parameters as a class instance with two publishing schemas. Same byte-identical clause in all three copies: AGENTS.md line 18, the assert_object_parity doc comment, and the assert_same_shape doc comment. Remedy is a referent, not a rule change: name the field in the slot the pronoun occupies. R24's substance and R43's checked-in-only domain both survive it; touching ruled text, so the captain should rule.
  • ⚠️ docs/ARCHITECTURE.md:105 - R65's second sweep landed, but the quantifier closing the same sentence undoes it: "membership is derivable by sweeping the checked-in schemas for constraining keywords and the Rust types for a deserialize_with validator ... then applying the entry's own criterion to each carrier either sweep turns up." This section fixes "carrier" to mean a checked-in schema that carries the constraint - "leaving the unbound carriers free to drift", "A lone carrier falls in the same way" introducing a constraint carried by one schema, and the class id fpsm-unbound-carrier-parity itself. The Rust-side sweep turns up Rust fields, which are not carriers in that sense, so under the section's own vocabulary the added sweep contributes nothing for the criterion to be applied to, and ChangeRequest::parameters - the very member R65 was written to reach - is still underivable from this line. This is the shape R66a just resolved for bind by splitting verbs: one word carrying two referents inside one section with no rule reconciling them. Secondary defect in the same phrase: both entries are read per constraint, pinned by R33 for fpsm-unbound-carrier-parity and R44 for fpsm-capid-guard, while "each carrier" quantifies per schema, so the criterion is applied to the wrong kind of thing even for the first sweep. Remedy stays R7-clean - it changes what the criterion is applied to, adding no count and no enumeration - and is R30-clean, since the Rust-side domain is not widened. Touching text ruled at R63 and R65, so the captain should rule.

🔧 Fix: name field referent, make constraint the sweep unit
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • cargo test --workspace (baseline and final, both green; run with CC/AR zig shims in target/ that this environment needs for libsqlite3-sys)
  • cargo test -p fpsmaxxing-contracts x15 under a schema-mutation matrix (property rename, dropped required entry, additionalProperties flip, type drift on bound and unbound carriers, dropped bounds on bound and unbound carriers)
  • Exhaustive constraint sweep: deleted each of 31 constraining keywords across all 7 files in schemas/ one at a time, re-running cargo test -p fpsmaxxing-contracts per deletion, to derive bound vs unbound carriers
  • fpsm-lease-ceiling-parity two-step repro: MAX_LEASE_SECONDS 300 -> 600 then schemas/experiment.schema.json maximum -> 600, cargo test --workspace after each, gateway inline ceiling read from apps/gateway/src/lib.rs:67
  • git check-ignore over 7 real fixture files at real repo paths against HEAD's .gitignore and against the base 3f9a580 block in a throwaway repo
  • Rendered AGENTS.md through npx marked --gfm and inspected the Repository rules list markup to confirm the new three-sentence rule renders as one bullet
  • git status --porcelain after every mutation run to confirm all schema and Rust mutations were reverted
🔧 **Document** - 2 issues found → auto-fixed ✅
  • ℹ️ AGENTS.md:18 - The operative rule reads "a dedicated test that binds each checked-in schema carrying that constraint" - the constraint rides on a participle attached to the object, so the predicate carries no "for" slot, which is the two-place form R57/R58 replaced one line later at :19 ("A test binds a schema for a constraint...") and at :20 ("bound for it by no test today"). R59a swept the register and line 19/20 and ruled the surviving participle forms "true-but-fragile ... Move both", but its domain never included line 18. The sentence is not false (its quantifier ranges over one triggering constraint, so participle and for-slot pick out the same thing), which is why this is left rather than fixed: it is ruled text, the remedy must move three copies together (AGENTS.md:18, crates/contracts/src/lib.rs assert_object_parity, crates/contracts/src/test_support.rs assert_same_shape) without disturbing R24's counterpart, R43's checked-in domain, R56's role split, or R67's "that field" referent, and whether R59a extends to the rule itself is the captain's call.
  • ⚠️ crates/contracts/src/test_support.rs:4 - The module doc says "Every schema-sync test module compares them the same way, so the comparison lives here once rather than being re-derived per module." Both clauses are contradicted by what this branch newly documents in AGENTS.md:20: crates/contracts/src/lib.rs imports properties/string_set/serialized_fields but defines its own assert_object_parity, a re-derivation of assert_same_shape, and capability_fields_match_capability_schema and manifest_fields_match_sidecar_schema compare a serialized sample's field set with no generated schema at all. Not fixed here because the only accurate rewording is a description of the helper duplication that R21 approved as-is, filed outside the repo, and ruled the branch silent about; deciding whether an honest softening of this sentence breaches that silence is a captain call.

🔧 Fix: give bind predicate a for-slot, drop register overclaim
✅ Re-checked - no issues remain.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

undeemed added 30 commits August 2, 2026 04:31
…ump ignores

Constrained fields owe their own dedicated test. That convention is already
well followed - `lease_seconds_is_bounded_like_the_schema` and its siblings
exist because the schema parity test cannot cover them - but it was written
down nowhere, so the next person to add a constrained field would reasonably
assume parity covers it.

`generated_schemas_match_checked_in_schemas` compares only property names,
`required`, and `additionalProperties` via `assert_object_parity`. Measured:
setting `$defs.change_request.properties.capability_id.type` to `"integer"` in
`schemas/experiment.schema.json`, against `capability_id: String` in Rust,
leaves all 29 contract tests green.

Also replace the unanchored `core` / `core.*` crash-dump ignores. A slash-less
pattern matches at any depth and matches directories, so bare `core` would
silently untrack a future `src/core/` subsystem and bare `core.*` would swallow
a source file such as `api/core.ts`. The replacement anchors to the repo root,
negates a root-level `core/` package directory, and covers both per-host dump
shapes.

Verified: `git check-ignore` reports `core` and `core.node.12345` ignored and
`api/core.ts` not ignored; `cargo test --workspace` and `cargo fmt --check`
pass. No source or schema file is modified.
@undeemed
undeemed merged commit b22bc02 into main Aug 3, 2026
4 checks passed
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