From e860a91d80100fa7595afb171555241eb5b06ce0 Mon Sep 17 00:00:00 2001 From: "Rene Garza Jr." Date: Sat, 3 Oct 2026 02:12:07 -0500 Subject: [PATCH 1/4] fix: byte-bound secondmate home summary base surfaces with an omission marker A registry with thousands of projects pushed the whole home summary past the 262144-byte reader limit, so the parent marked the secondmate unavailable. The producer now cuts oversized base surfaces (projects first) to fit and records one summary_bytes marker per cut surface with kept and omitted counts. Bearings discloses the marker in omitted[]. A summary that already fits is emitted unchanged. --- bin/fm-bearings-snapshot.sh | 3 + bin/fm-fleet-snapshot.sh | 33 +++++++++- docs/scripts.md | 2 +- tests/fm-bearings-snapshot.test.sh | 98 ++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/bin/fm-bearings-snapshot.sh b/bin/fm-bearings-snapshot.sh index 405ae39737c..59b10eac55e 100755 --- a/bin/fm-bearings-snapshot.sh +++ b/bin/fm-bearings-snapshot.sh @@ -935,6 +935,9 @@ MODEL=$(printf '%s' "$SNAP" | jq -L "$SCRIPT_DIR" \ (($snap.secondmate_current.records // [])[] as $m | ([($m.omitted // [])[] | select(.surface == "lifecycle_inventory") | .count] | add // 0) as $n | if $n > 0 then {surface:("secondmate " + $m.id + " parked lifecycle facts omitted by summary bound: \($n)"), reveal:"refresh after parked work decreases; omitted tasks may not resurface at expiry until then"} else empty end), + (($snap.secondmate_current.records // [])[] as $m + | ($m.omitted // [])[] | select(.surface == "summary_bytes") + | {surface:("secondmate " + $m.id + " \(.name) omitted by summary byte limit: \(.omitted) (kept \(.kept))"), reveal:"shrink that surface in the secondmate home; omitted rows are unread, and an omitted project reads as active"}), (if $all_secondmates == 0 and ($secondmates_all | length) > $secondmates_n then {surface:("secondmates showing \($secondmates_n) of \($secondmates_all | length)"), reveal:"--all-secondmates"} else empty end), (if (($snap.secondmate_current.truncated // 0) > 0) then {surface:("registered secondmates omitted by snapshot bound: \($snap.secondmate_current.truncated)"), reveal:"raise FM_SNAPSHOT_SECONDMATES"} else empty end), (if $snap.secondmate_current.registry.input_truncated == true then {surface:"secondmate registry input truncated by bounded read", reveal:"raise FM_SNAPSHOT_REGISTRY_LINES or FM_SNAPSHOT_REGISTRY_BYTES"} else empty end), diff --git a/bin/fm-fleet-snapshot.sh b/bin/fm-fleet-snapshot.sh index d10cbc83872..10479ff1b86 100755 --- a/bin/fm-fleet-snapshot.sh +++ b/bin/fm-fleet-snapshot.sh @@ -271,7 +271,12 @@ hold_bucket, hold_age_days, and plural blocker fields for downstream projections. Each summary budgets lifecycle_inventory toward the fixed 262144-byte reader limit while retaining current parked task facts in deterministic order. omitted[] discloses additional parked facts; those tasks may not resurface at -expiry until the budget clears. Other base summary surfaces remain unbounded. +expiry until the budget clears. When the base surfaces alone exceed that limit, +projects, landed, endpoints, queued, holds, active_children, and decisions_open +are cut to a prefix in that order until the summary fits, and omitted[] carries +one {surface:"summary_bytes",name,kept,omitted} marker per cut surface. Parked +and archived project rows are kept ahead of active ones because an omitted +project reads as active. A summary that fits carries no marker and is unchanged. A captain hold is actionable only when every blocker is Done, any hold-until date has arrived, and an undated hold remains below the aging threshold. Cross-home collection uses FM_SNAPSHOT_SECONDMATES (default 20, 0 lifts the @@ -1321,7 +1326,31 @@ secondmate_home_summary_json() { # 0 then 1 else 0 end)) as $row_bytes + | if (.bytes + $row_bytes) <= $summary_max_bytes then .kept += 1 | .bytes += $row_bytes + else .full = true end + end) | .kept) as $kept + | .[$name] = $rows[:$kept] + | .omitted += [byte_marker($name; $kept; ($total - $kept))] + end; + reduce ("projects", "landed", "endpoints", "queued", "holds", "active_children", "decisions_open") as $name + (.; byte_bound($name))' } # Current registered-secondmate aggregation. diff --git a/docs/scripts.md b/docs/scripts.md index 358fb9a9be5..7d5c4d11896 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -192,7 +192,7 @@ Project lifecycle posture is a projection rule on that same contract, not a seco `projects[]` is the registry surface and `bin/fm-project-posture.sh` is the write owner. Bearings applies parked and archived placement to Charted Next, omission, and `omitted[]` after one identity-normalization pass. Each secondmate summary budgets current parked task facts in deterministic order against the fixed 262144-byte reader limit and discloses every excluded lifecycle row in `omitted[]`; an omitted parked task may not auto-resurface at expiry until earlier parked work clears the byte budget. -The remaining base summary surfaces retain their pre-existing unbounded behavior, so an unusually large registry or base surface can still exceed that reader limit and make the secondmate summary unavailable. +A base surface that would push the summary past that limit, starting with `projects[]`, is cut to fit and reported as one `secondmate omitted by summary byte limit: (kept )` row, so the secondmate stays readable; `bin/fm-fleet-snapshot.sh --help` owns the cut order and marker shape. Live PR suppression covers main-home lifecycle facts, recorded PR URLs, and repository-scoped branch matches only, so a parked or archived secondmate PR may briefly appear in Captain's Call. Consumer example: an external tool reads `.schema` first and refuses a major version it does not understand, then treats `omitted` as a disclosure to surface, never to hide. diff --git a/tests/fm-bearings-snapshot.test.sh b/tests/fm-bearings-snapshot.test.sh index 7168e5d5ecc..567551f31aa 100755 --- a/tests/fm-bearings-snapshot.test.sh +++ b/tests/fm-bearings-snapshot.test.sh @@ -4646,6 +4646,103 @@ EOF pass "lifecycle inventory is bounded with explicit omission disclosure" } +refresh_byte_bound_mate() { # + local mate=$1 fakebin=$2 count=$3 title=$4 i=1 + { + while [ "$i" -le "$count" ]; do + printf -- '- active-%04d [no-mistakes] - Active app (added 2026-07-01)\n' "$i" + i=$((i + 1)) + done + printf -- '- parked-app [direct-PR parked:2026-09-01] - Parked app (added 2026-07-01)\n' + printf -- '- archived-app [local-only archived] - Archived app (added 2026-07-01)\n' + } > "$mate/data/projects.md" + cat > "$mate/data/backlog.md" </dev/null \ + || fail "byte-bound fixture summary refresh failed" +} + +test_summary_base_surfaces_are_byte_bounded_with_one_marker() { + local home mate fakebin ledger summary json bytes row_bytes gap count pad title kept + home=$(make_home summary-byte-bound) + mate="$TMP_ROOT/summary-byte-bound-mate" + : > "$home/data/secondmates.md" + make_valid_secondmate_home bytes-mate "$mate" + append_secondmate_registry "$home" bytes-mate "$mate" + fakebin=$(make_fakebin "$home") + ledger="$mate/state/home-summary.json" + + # A small registry is published whole, in registry order, with no marker. + refresh_byte_bound_mate "$mate" "$fakebin" 3 T + jq -e ' + ([.projects[].name] == ["active-0001","active-0002","active-0003","parked-app","archived-app"]) + and (.omitted | any(.surface == "summary_bytes") | not) + ' "$ledger" >/dev/null || fail "small summary changed or carried a byte marker: $(<"$ledger")" + + # Tune the summary to exactly the reader limit: it still needs no marker. + count=2000 + refresh_byte_bound_mate "$mate" "$fakebin" "$count" T + bytes=$(wc -c < "$ledger" | tr -d ' ') + row_bytes=$(jq '(.projects[0] | tojson | utf8bytelength) + 1' "$ledger") + gap=$((262144 - bytes)) + [ "$gap" -gt 0 ] || fail "boundary fixture started over the limit: $bytes" + count=$((count + gap / row_bytes)) + pad=$((gap % row_bytes)) + title=T + [ "$pad" -eq 0 ] || title=$(printf "T%0${pad}d" 0) + refresh_byte_bound_mate "$mate" "$fakebin" "$count" "$title" + bytes=$(wc -c < "$ledger" | tr -d ' ') + [ "$bytes" -eq 262144 ] || fail "boundary fixture is not exactly at the limit: $bytes" + jq -e --argjson total "$((count + 2))" ' + (.projects | length) == $total and (.omitted | any(.surface == "summary_bytes") | not) + ' "$ledger" >/dev/null || fail "summary exactly at the limit was cut: $(jq -c .omitted "$ledger")" + + # One byte more crosses the limit: projects are cut and one marker says so. + refresh_byte_bound_mate "$mate" "$fakebin" "$count" "${title}0" + bytes=$(wc -c < "$ledger" | tr -d ' ') + [ "$bytes" -le 262144 ] || fail "summary one byte over the limit was not bounded: $bytes" + jq -e --argjson total "$((count + 2))" ' + (.projects | length) as $kept + | $kept < $total + and ([.omitted[] | select(.surface == "summary_bytes")] + == [{surface:"summary_bytes",name:"projects",kept:$kept,omitted:($total - $kept)}]) + ' "$ledger" >/dev/null || fail "boundary overflow marker is missing or miscounted: $(jq -c .omitted "$ledger")" + + # A registry far over the limit stays readable, keeps posture rows, and the + # parent discloses the omission instead of marking the secondmate unavailable. + refresh_byte_bound_mate "$mate" "$fakebin" 4000 T + summary=$(<"$ledger") + bytes=$(wc -c < "$ledger" | tr -d ' ') + [ "$bytes" -le 262144 ] || fail "large registry summary exceeded the reader limit: $bytes" + kept=$(printf '%s' "$summary" | jq '.projects | length') + printf '%s' "$summary" | jq -e --argjson kept "$kept" ' + $kept > 2 and $kept < 4002 + and ([.omitted[] | select(.surface == "summary_bytes")] + == [{surface:"summary_bytes",name:"projects",kept:$kept,omitted:(4002 - $kept)}]) + and (.projects | any(.name == "parked-app" and .posture == "parked")) + and (.projects | any(.name == "archived-app" and .posture == "archived")) + and (.queued | any(.id == "q-active")) + ' >/dev/null || fail "large registry was not cut with a correct marker: $(printf '%s' "$summary" | jq -c .omitted)" + json=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-08-01T18:00:00Z \ + FM_SNAPSHOT_NOW_EPOCH=1785607200 FM_BEARINGS_NOW=2026-08-01T18:00:00Z \ + NET_LOG="$home/net.log" "$BEARINGS" --json --all-queued) + printf '%s' "$json" | jq -e --argjson kept "$kept" ' + (.secondmates | any(.id == "bytes-mate" and .state != "unknown")) + and (.gates | any(.id == "q-active" and .owner == "bytes-mate")) + and (.omitted | any(.surface == ("secondmate bytes-mate projects omitted by summary byte limit: " + + ((4002 - $kept) | tostring) + " (kept " + ($kept | tostring) + ")"))) + ' >/dev/null || fail "byte-bounded summary was unavailable or undisclosed: $json" + pass "oversized base surfaces are byte-bounded with one disclosed omission marker" +} + test_expired_secondmate_park_survives_summary_bounds_and_cache() { local home mate fakebin sshbin summary json i home=$(make_home expired-secondmate-park-cache) @@ -4861,5 +4958,6 @@ test_archive_filter_updates_summary_counts test_archived_main_orphan_does_not_emit_inventory_gate test_long_secondmate_project_identity_is_preserved test_lifecycle_inventory_is_bounded_and_disclosed +test_summary_base_surfaces_are_byte_bounded_with_one_marker test_expired_secondmate_park_survives_summary_bounds_and_cache test_expired_project_park_resurfaces_with_one_wake From 1f42b8628c530ac07f5c31884a822df589d8993d Mon Sep 17 00:00:00 2001 From: "Rene Garza Jr." Date: Sat, 3 Oct 2026 02:26:51 -0500 Subject: [PATCH 2/4] no-mistakes(review): Bound summary disclosures, preserve classifications, and correct CI count --- .github/workflows/ci.yml | 4 +- bin/fm-bearings-snapshot.sh | 2 +- bin/fm-fleet-snapshot.sh | 48 ++++++++++-------- bin/fm-project-lifecycle.jq | 9 +++- docs/scripts.md | 1 + tests/fm-bearings-snapshot.test.sh | 81 +++++++++++++++++++++++++++++- 6 files changed, 117 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 225ae823149..78b96deacc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -454,8 +454,8 @@ jobs: bearings_output=$(/bin/bash tests/fm-bearings-snapshot.test.sh) printf '%s\n' "$bearings_output" bearings_count=$(printf '%s\n' "$bearings_output" | grep -c '^ok - ') - [ "$bearings_count" -eq 82 ] || { - echo "::error::expected 82 Bearings tests, got $bearings_count" + [ "$bearings_count" -eq 83 ] || { + echo "::error::expected 83 Bearings tests, got $bearings_count" exit 1 } diff --git a/bin/fm-bearings-snapshot.sh b/bin/fm-bearings-snapshot.sh index 59b10eac55e..a1428afff90 100755 --- a/bin/fm-bearings-snapshot.sh +++ b/bin/fm-bearings-snapshot.sh @@ -937,7 +937,7 @@ MODEL=$(printf '%s' "$SNAP" | jq -L "$SCRIPT_DIR" \ | if $n > 0 then {surface:("secondmate " + $m.id + " parked lifecycle facts omitted by summary bound: \($n)"), reveal:"refresh after parked work decreases; omitted tasks may not resurface at expiry until then"} else empty end), (($snap.secondmate_current.records // [])[] as $m | ($m.omitted // [])[] | select(.surface == "summary_bytes") - | {surface:("secondmate " + $m.id + " \(.name) omitted by summary byte limit: \(.omitted) (kept \(.kept))"), reveal:"shrink that surface in the secondmate home; omitted rows are unread, and an omitted project reads as active"}), + | {surface:("secondmate " + $m.id + " \(.name) omitted by summary byte limit: \(.omitted) (kept \(.kept))"), reveal:"shrink that surface in the secondmate home; omitted content is unread, and an omitted project reads as active"}), (if $all_secondmates == 0 and ($secondmates_all | length) > $secondmates_n then {surface:("secondmates showing \($secondmates_n) of \($secondmates_all | length)"), reveal:"--all-secondmates"} else empty end), (if (($snap.secondmate_current.truncated // 0) > 0) then {surface:("registered secondmates omitted by snapshot bound: \($snap.secondmate_current.truncated)"), reveal:"raise FM_SNAPSHOT_SECONDMATES"} else empty end), (if $snap.secondmate_current.registry.input_truncated == true then {surface:"secondmate registry input truncated by bounded read", reveal:"raise FM_SNAPSHOT_REGISTRY_LINES or FM_SNAPSHOT_REGISTRY_BYTES"} else empty end), diff --git a/bin/fm-fleet-snapshot.sh b/bin/fm-fleet-snapshot.sh index 10479ff1b86..508ac13fd29 100755 --- a/bin/fm-fleet-snapshot.sh +++ b/bin/fm-fleet-snapshot.sh @@ -272,9 +272,12 @@ projections. Each summary budgets lifecycle_inventory toward the fixed 262144-byte reader limit while retaining current parked task facts in deterministic order. omitted[] discloses additional parked facts; those tasks may not resurface at expiry until the budget clears. When the base surfaces alone exceed that limit, -projects, landed, endpoints, queued, holds, active_children, and decisions_open -are cut to a prefix in that order until the summary fits, and omitted[] carries -one {surface:"summary_bytes",name,kept,omitted} marker per cut surface. Parked +projects, landed, endpoints, queued, holds, active_children, decisions_open, +omitted[].archived_projects, omitted[].parked_projects, reason, invalidity.ids, +and contributions.captain are cut to a prefix in that order until the summary +fits. omitted[] carries one {surface:"summary_bytes",name,kept,omitted} marker +per cut surface, with a dotted path as name and row or string-character counts. +Readers preserve the producer state and validity when byte omissions exist. Parked and archived project rows are kept ahead of active ones because an omitted project reads as active. A summary that fits carries no marker and is unchanged. A captain hold is actionable only when every blocker is Done, any @@ -1327,30 +1330,31 @@ secondmate_home_summary_json() { # 0 then 1 else 0 end)) as $row_bytes - | if (.bytes + $row_bytes) <= $summary_max_bytes then .kept += 1 | .bytes += $row_bytes - else .full = true end - end) | .kept) as $kept - | .[$name] = $rows[:$kept] - | .omitted += [byte_marker($name; $kept; ($total - $kept))] + | . as $summary + | def candidate($kept): + $summary | setpath($path; $rows[:$kept]) + | .omitted += [byte_marker($name; $kept; ($total - $kept))]; + ({low:0,high:$total} + | until(.high - .low <= 1; + ((.low + .high) / 2 | floor) as $mid + | if (candidate($mid) | summary_bytes) <= $summary_max_bytes + then .low = $mid else .high = $mid end) + | .low) as $kept + | candidate($kept) end; - reduce ("projects", "landed", "endpoints", "queued", "holds", "active_children", "decisions_open") as $name - (.; byte_bound($name))' + ([(["projects", "landed", "endpoints", "queued", "holds", "active_children", "decisions_open"][] | [.])] + + [(.omitted | to_entries[] | select(.value.surface == "project_lifecycle") + | ["omitted", .key, "archived_projects"], ["omitted", .key, "parked_projects"])] + + [["reason"], ["invalidity", "ids"], ["contributions", "captain"]]) as $paths + | reduce $paths[] as $path (.; byte_bound($path))' } # Current registered-secondmate aggregation. diff --git a/bin/fm-project-lifecycle.jq b/bin/fm-project-lifecycle.jq index 4248e67afeb..72c7afb0e8f 100644 --- a/bin/fm-project-lifecycle.jq +++ b/bin/fm-project-lifecycle.jq @@ -46,7 +46,8 @@ def fm_invalidity_reason($kind; $ids): def fm_secondmate_summary_at($today): if (has("projects") and has("lifecycle_inventory")) | not then . else - (.projects // []) as $projects + {state,valid,reason,invalidity} as $producer_classification + | (.projects // []) as $projects | .bounds as $bounds | {active_children:(.active_children | length),holds:(.holds | length), decisions_open:(.decisions_open | length),queued:(.queued | length), @@ -210,7 +211,10 @@ def fm_secondmate_summary_at($today): and $retained_invalid_ids == $current_unknown then .reason else fm_invalidity_reason("child_current_unavailable"; $current_unknown) end)} else null end) as $current_invalidity - | if $current_invalidity == null then + | if any(.omitted[]?; .surface == "summary_bytes" and .omitted > 0) then + . + $producer_classification + else + if $current_invalidity == null then .valid = true | .invalidity = {kind:null,ids:[]} | .reason = null @@ -228,4 +232,5 @@ def fm_secondmate_summary_at($today): elif (.holds | length) > 0 then "externally_held" else "no_active_work" end) end + end end; diff --git a/docs/scripts.md b/docs/scripts.md index 7d5c4d11896..41f7256be15 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -193,6 +193,7 @@ Project lifecycle posture is a projection rule on that same contract, not a seco Bearings applies parked and archived placement to Charted Next, omission, and `omitted[]` after one identity-normalization pass. Each secondmate summary budgets current parked task facts in deterministic order against the fixed 262144-byte reader limit and discloses every excluded lifecycle row in `omitted[]`; an omitted parked task may not auto-resurface at expiry until earlier parked work clears the byte budget. A base surface that would push the summary past that limit, starting with `projects[]`, is cut to fit and reported as one `secondmate omitted by summary byte limit: (kept )` row, so the secondmate stays readable; `bin/fm-fleet-snapshot.sh --help` owns the cut order and marker shape. +When byte omissions exist, read-time lifecycle normalization preserves the producer's state and validity classification rather than inferring absence from incomplete evidence. Live PR suppression covers main-home lifecycle facts, recorded PR URLs, and repository-scoped branch matches only, so a parked or archived secondmate PR may briefly appear in Captain's Call. Consumer example: an external tool reads `.schema` first and refuses a major version it does not understand, then treats `omitted` as a disclosure to surface, never to hide. diff --git a/tests/fm-bearings-snapshot.test.sh b/tests/fm-bearings-snapshot.test.sh index 567551f31aa..4a671eab4e0 100755 --- a/tests/fm-bearings-snapshot.test.sh +++ b/tests/fm-bearings-snapshot.test.sh @@ -4671,7 +4671,7 @@ EOF } test_summary_base_surfaces_are_byte_bounded_with_one_marker() { - local home mate fakebin ledger summary json bytes row_bytes gap count pad title kept + local home mate fakebin ledger summary json bytes row_bytes gap count pad title kept mode name i long_name home=$(make_home summary-byte-bound) mate="$TMP_ROOT/summary-byte-bound-mate" : > "$home/data/secondmates.md" @@ -4740,6 +4740,85 @@ test_summary_base_surfaces_are_byte_bounded_with_one_marker() { and (.omitted | any(.surface == ("secondmate bytes-mate projects omitted by summary byte limit: " + ((4002 - $kept) | tostring) + " (kept " + ($kept | tostring) + ")"))) ' >/dev/null || fail "byte-bounded summary was unavailable or undisclosed: $json" + long_name=$(printf '%01000d' 0) + for mode in archived parked invalid; do + : > "$mate/data/projects.md" + printf '## In flight\n' > "$mate/data/backlog.md" + if [ "$mode" != invalid ]; then + printf '\n## Queued\n' >> "$mate/data/backlog.md" + [ "$mode" != archived ] || printf '\n## Done\n' >> "$mate/data/backlog.md" + fi + i=1 + while [ "$i" -le 300 ]; do + name=$(printf '%s-%04d-%s' "$mode" "$i" "$long_name") + case "$mode" in + archived) + printf -- '- %s [local-only archived] - App (added 2026-07-01)\n' "$name" >> "$mate/data/projects.md" + printf -- '- [x] done-%04d - Done (repo: %s) (kind: ship) (done 2026-07-01)\n' "$i" "$name" >> "$mate/data/backlog.md" + ;; + parked) + printf -- '- %s [direct-PR parked:2026-09-01] - App (added 2026-07-01)\n' "$name" >> "$mate/data/projects.md" + printf -- '- [ ] queued-%04d - Next (repo: %s) (kind: ship)\n' "$i" "$name" >> "$mate/data/backlog.md" + ;; + invalid) + printf -- '- [ ] %s - Orphan (repo: sample) (kind: ship)\n' "$name" >> "$mate/data/backlog.md" + ;; + esac + i=$((i + 1)) + done + [ "$mode" != invalid ] || printf '\n## Queued\n' >> "$mate/data/backlog.md" + [ "$mode" = archived ] || printf '\n## Done\n' >> "$mate/data/backlog.md" + PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$mate" \ + FM_SNAPSHOT_NOW=2026-08-01T18:00:00Z FM_SNAPSHOT_NOW_EPOCH=1785607200 \ + "$ROOT/bin/fm-home-summary-refresh.sh" >/dev/null || fail "$mode disclosure refresh failed" + bytes=$(wc -c < "$ledger" | tr -d ' ') + [ "$bytes" -le 262144 ] || fail "$mode disclosure exceeded the reader byte limit: $bytes" + jq -e --arg mode "$mode" ' + all(.omitted[] | select(.surface == "summary_bytes"); + (keys == ["kept","name","omitted","surface"]) and .omitted > 0) + and (if $mode == "invalid" then + (.invalidity.ids | length) as $kept + | .valid == false and .invalidity.kind == "orphan_in_flight" + and (.omitted | any(.surface == "summary_bytes" and .name == "reason")) + and (.omitted | any(.surface == "summary_bytes" and .name == "invalidity.ids" + and .kept == $kept and .omitted == (300 - $kept))) + else + (if $mode == "archived" then "archived_projects" else "parked_projects" end) as $field + | (.omitted[] | select(.surface == "project_lifecycle") | .[$field] | length) as $kept + | .valid == true + and (.omitted | any(.surface == "summary_bytes" and .name == ("omitted.0." + $field) + and .kept == $kept and .omitted == ((if $mode == "archived" then 300 else 280 end) - $kept))) + end) + ' "$ledger" >/dev/null || fail "$mode byte marker or classification was incorrect" + json=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-08-01T18:00:00Z \ + FM_SNAPSHOT_NOW_EPOCH=1785607200 "$ROOT/bin/fm-fleet-snapshot.sh" --json) + printf '%s' "$json" | jq -e --slurpfile produced "$ledger" ' + .secondmate_current.records[] | select(.id == "bytes-mate") + | .current.state == $produced[0].state + and .provenance.summary_valid == $produced[0].valid + and .invalidity == $produced[0].invalidity + and .provenance.selected == "structured-home" + ' >/dev/null || fail "$mode byte-bounded summary lost its producer classification" + done + + jq -e -L "$ROOT/bin" ' + include "fm-project-lifecycle"; + . as $base + | all(["projects","landed","endpoints","queued","holds","active_children","decisions_open", + "omitted.0.archived_projects","omitted.0.parked_projects","reason","invalidity.ids", + "contributions.captain"][]; . as $surface + | all(["captain_decision","active_child_work","externally_held","no_active_work","unknown"][]; . as $state + | all([null,"orphan_in_flight","unowned_current","terminal_in_flight","child_current_unavailable","missing_backlog"][]; . as $kind + | $base + {state:$state,valid:($kind == null),reason:(if $kind == null then null else "partial diagnostic" end), + invalidity:{kind:$kind,ids:[]},projects:[],lifecycle_inventory:[], + active_children:[],holds:[],decisions_open:[{id:"child",key:"blocked",verb:"blocked",repo:null}], + queued:[],landed:[],endpoints:[], + counts:{active_children:0,holds:0,decisions_open:2,queued:0,landed:0,endpoints:0}, + omitted:[{surface:"summary_bytes",name:$surface,kept:0,omitted:1}]} + | {state,valid,reason,invalidity} as $classification + | fm_secondmate_summary_at("2026-08-01") + | {state,valid,reason,invalidity} == $classification))) + ' "$ledger" >/dev/null || fail "byte omissions changed a producer classification at read time" pass "oversized base surfaces are byte-bounded with one disclosed omission marker" } From beaabd4f1011385a50b134ae87c3f60a65c9800c Mon Sep 17 00:00:00 2001 From: "Rene Garza Jr." Date: Sat, 3 Oct 2026 02:32:56 -0500 Subject: [PATCH 3/4] no-mistakes(review): Exclude byte omissions from row-bound counters --- bin/fm-fleet-snapshot.sh | 5 +++- bin/fm-project-lifecycle.jq | 7 ++++-- tests/fm-bearings-snapshot.test.sh | 37 +++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/bin/fm-fleet-snapshot.sh b/bin/fm-fleet-snapshot.sh index 508ac13fd29..e549b1acf23 100755 --- a/bin/fm-fleet-snapshot.sh +++ b/bin/fm-fleet-snapshot.sh @@ -2235,7 +2235,10 @@ secondmate_landed_from_current_json() { # (.landed | length))) + | select(.provenance.selected == "structured-home") + | ([.omitted[]? | select(.surface == "summary_bytes" and .name == "landed") | .omitted] + | add // 0) as $byte_omitted + | select((.counts.landed - (.landed | length)) > $byte_omitted) | .home], unreadable:[ $current.records[] | select(.current.state == "unknown" and .provenance.selected != "structured-home") diff --git a/bin/fm-project-lifecycle.jq b/bin/fm-project-lifecycle.jq index 72c7afb0e8f..743e2429b8f 100644 --- a/bin/fm-project-lifecycle.jq +++ b/bin/fm-project-lifecycle.jq @@ -28,8 +28,11 @@ def fm_merge_by_id($base; $extra): if any(.[]; .id == $row.id) then . else . + [$row] end); def fm_set_surface_omission($surface; $count): - .omitted = ([.omitted[]? | select(.surface != $surface)] - + [if $count > 0 then {surface:$surface,count:$count} else empty end]); + ([.omitted[]? | select(.surface == "summary_bytes" and .name == $surface) | .omitted] + | add // 0) as $byte_omitted + | ($count - $byte_omitted) as $row_omitted + | .omitted = ([.omitted[]? | select(.surface != $surface)] + + [if $row_omitted > 0 then {surface:$surface,count:$row_omitted} else empty end]); def fm_invalidity_reason($kind; $ids): if $kind == "child_current_unavailable" then diff --git a/tests/fm-bearings-snapshot.test.sh b/tests/fm-bearings-snapshot.test.sh index 4a671eab4e0..a3451167ec7 100755 --- a/tests/fm-bearings-snapshot.test.sh +++ b/tests/fm-bearings-snapshot.test.sh @@ -4671,7 +4671,7 @@ EOF } test_summary_base_surfaces_are_byte_bounded_with_one_marker() { - local home mate fakebin ledger summary json bytes row_bytes gap count pad title kept mode name i long_name + local home mate fakebin ledger summary json bytes row_bytes gap count pad title kept mode name i long_name row_omitted home=$(make_home summary-byte-bound) mate="$TMP_ROOT/summary-byte-bound-mate" : > "$home/data/secondmates.md" @@ -4819,6 +4819,41 @@ test_summary_base_surfaces_are_byte_bounded_with_one_marker() { | fm_secondmate_summary_at("2026-08-01") | {state,valid,reason,invalidity} == $classification))) ' "$ledger" >/dev/null || fail "byte omissions changed a producer classification at read time" + for row_omitted in 0 3; do + jq --argjson row_omitted "$row_omitted" ' + ["active_children","decisions_open","holds","queued"] as $surfaces + | . + {projects:[],lifecycle_inventory:[],active_children:[],decisions_open:[],holds:[],queued:[], + landed:[],endpoints:[],state:"captain_decision",valid:true,reason:null,invalidity:{kind:null,ids:[]}} + | .bounds = ($surfaces | map({key:.,value:1001}) | from_entries) + | .counts = ((.bounds | map_values(. + $row_omitted)) + {landed:(1001 + $row_omitted),endpoints:0}) + | .omitted = ((($surfaces + ["landed"]) | map({surface:"summary_bytes",name:.,kept:0,omitted:1001})) + + (if $row_omitted > 0 then ($surfaces | map({surface:.,count:$row_omitted})) else [] end)) + ' "$ledger" > "$mate/state/byte-omission-fixture.json" || fail "omission fixture creation failed" + mv "$mate/state/byte-omission-fixture.json" "$ledger" + jq -e -L "$ROOT/bin" --argjson row_omitted "$row_omitted" ' + include "fm-project-lifecycle"; + [.omitted[] | select(.surface == "summary_bytes")] as $markers + | fm_secondmate_summary_at("2026-08-01") + | fm_secondmate_summary_at("2026-08-01") + | ([.omitted[] | select(.surface == "summary_bytes")] == $markers) + and ([.omitted[] | select(.surface != "summary_bytes")] + == (if $row_omitted > 0 then + ["queued","active_children","holds","decisions_open"] | map({surface:.,count:$row_omitted}) + else [] end)) + ' "$ledger" >/dev/null || fail "normalization counted byte omissions as row omissions" + json=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-08-01T18:00:00Z \ + FM_SNAPSHOT_NOW_EPOCH=1785607200 FM_BEARINGS_NOW=2026-08-01T18:00:00Z \ + NET_LOG="$home/net.log" "$BEARINGS" --json) + printf '%s' "$json" | jq -e --argjson row_omitted "$row_omitted" ' + [.omitted[] | select(.surface | startswith("secondmate bytes-mate "))] as $rows + | ([$rows[] | select(.surface | contains("summary byte limit: 1001 (kept 0)"))] | length) == 5 + and ([$rows[] | select(.surface | contains("omitted by snapshot bound:"))] as $bounded + | ($bounded | length) == (if $row_omitted > 0 then 4 else 0 end) + and all($bounded[]; .surface | endswith("snapshot bound: " + ($row_omitted | tostring)))) + and ((.omitted | any(.surface == "secondmate home Done capped at the snapshot layer for 1 home(s)")) + == ($row_omitted > 0)) + ' >/dev/null || fail "Bearings duplicated byte omissions or lost genuine row-bound omissions" + done pass "oversized base surfaces are byte-bounded with one disclosed omission marker" } From 73ba8abd61b66acd1f381f0d0bedf7c35af401cb Mon Sep 17 00:00:00 2001 From: "Rene Garza Jr." Date: Sat, 3 Oct 2026 02:42:01 -0500 Subject: [PATCH 4/4] no-mistakes(document): Correct documentation for bounded secondmate summaries --- bin/fm-fleet-snapshot.sh | 5 +++-- docs/captain-hold-lifecycle.md | 6 +++--- docs/scripts.md | 5 ++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/fm-fleet-snapshot.sh b/bin/fm-fleet-snapshot.sh index e549b1acf23..13642af9acc 100755 --- a/bin/fm-fleet-snapshot.sh +++ b/bin/fm-fleet-snapshot.sh @@ -97,8 +97,9 @@ # freshness is "cached" only for the cache source, and observed_at/age_seconds # come from the selected summary's generation. Every successfully sampled home also carries # reconcile_inventory independently of projection trust. -# Actionable captain holds appear in decisions_open; every captain hold remains -# in the bounded queued inventory with its structured classification metadata. +# Exported actionable captain holds appear in decisions_open; exported captain +# holds retain their structured classification metadata in queued, subject to +# the row and byte bounds described by --help. # Before that queued bound is applied, non-captain-actionable rows are selected # ahead of captain-actionable rows so separately projected live decisions cannot # crowd Charted-Next-eligible work out of the summary. Each group is ordered by diff --git a/docs/captain-hold-lifecycle.md b/docs/captain-hold-lifecycle.md index 8ded139b27b..c5073d20788 100644 --- a/docs/captain-hold-lifecycle.md +++ b/docs/captain-hold-lifecycle.md @@ -364,7 +364,7 @@ That aging is a projection safety net only. The durable deferral remains re-holding with `--until`. The fleet snapshot's secondmate-home summary classifies an actionable captain hold as `captain_decision`. -It preserves every captain hold in the bounded queued inventory of the owning home. +It retains exported captain holds in the queued inventory of the owning home, subject to the summary bounds below. ### Bearings placement @@ -385,8 +385,8 @@ Three accepted limits remain deliberate: - A remote or secondmate hold retains the producer home's age and aging decision from the summary's capture time and threshold rather than being recomputed by the parent. - A rare concurrent answer-close and re-hold race can leave the newly re-held task without its age basis. -- Cross-home summaries remain bounded by `FM_SNAPSHOT_SECONDMATE_DECISIONS` and `FM_SNAPSHOT_SECONDMATE_QUEUED`. - A remote deferred hold beyond those bounds is not exported, so it can be neither gated nor revealed. +- Cross-home summaries apply row and byte bounds owned by `bin/fm-fleet-snapshot.sh --help`. + A remote deferred hold omitted by those bounds is not exported, so it can be neither gated nor revealed. Re-holding through the wrapper with `--until` remains the durable fix rather than relying on the projection safety net. diff --git a/docs/scripts.md b/docs/scripts.md index 41f7256be15..1cf613388ae 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -191,9 +191,8 @@ Renaming or removing a field or surface, or removing or renaming an enum value, Project lifecycle posture is a projection rule on that same contract, not a second vocabulary. `projects[]` is the registry surface and `bin/fm-project-posture.sh` is the write owner. Bearings applies parked and archived placement to Charted Next, omission, and `omitted[]` after one identity-normalization pass. -Each secondmate summary budgets current parked task facts in deterministic order against the fixed 262144-byte reader limit and discloses every excluded lifecycle row in `omitted[]`; an omitted parked task may not auto-resurface at expiry until earlier parked work clears the byte budget. -A base surface that would push the summary past that limit, starting with `projects[]`, is cut to fit and reported as one `secondmate omitted by summary byte limit: (kept )` row, so the secondmate stays readable; `bin/fm-fleet-snapshot.sh --help` owns the cut order and marker shape. -When byte omissions exist, read-time lifecycle normalization preserves the producer's state and validity classification rather than inferring absence from incomplete evidence. +`bin/fm-fleet-snapshot.sh --help` owns the secondmate summary's byte budget, cut order, marker shape, classification preservation, and parked-task expiry limitation. +Bearings reports each byte cut as one `secondmate omitted by summary byte limit: (kept )` row, separately from snapshot row-bound omissions. Live PR suppression covers main-home lifecycle facts, recorded PR URLs, and repository-scoped branch matches only, so a parked or archived secondmate PR may briefly appear in Captain's Call. Consumer example: an external tool reads `.schema` first and refuses a major version it does not understand, then treats `omitted` as a disclosure to surface, never to hide.