Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions bin/fm-bearings-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 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),
Expand Down
47 changes: 42 additions & 5 deletions bin/fm-fleet-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -271,7 +272,15 @@ 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, 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
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
Expand Down Expand Up @@ -1321,7 +1330,32 @@ secondmate_home_summary_json() { # <backlog-json-file> <tasks-json-file> <proje
| if (.bytes + $row_bytes) <= $lifecycle_budget then
.rows += [$row] | .bytes += $row_bytes
else . end)) as $selected
| summary($selected.rows; ($lifecycle_total - ($selected.rows | length)))'
| summary($selected.rows; ($lifecycle_total - ($selected.rows | length)))
| def summary_bytes: tojson | utf8bytelength + 1;
def byte_marker($name; $kept; $omitted):
{surface:"summary_bytes",name:$name,kept:$kept,omitted:$omitted};
def byte_bound($path):
if summary_bytes <= $summary_max_bytes or (getpath($path) | length) == 0 then .
else ($path | map(tostring) | join(".")) as $name
| (getpath($path) | if $path == ["projects"] then sort_by(.posture == "active") else . end) as $rows
| ($rows | length) as $total
| . 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;
([(["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.
Expand Down Expand Up @@ -2202,7 +2236,10 @@ secondmate_landed_from_current_json() { # <secondmate-current-json-file> <outpu
| $mate.landed[]
| . + {home:$mate.home,home_id:$mate.id}],
truncated:[ $current.records[]
| select(.provenance.selected == "structured-home" and (.counts.landed > (.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")
Expand Down
16 changes: 12 additions & 4 deletions bin/fm-project-lifecycle.jq
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,7 +49,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),
Expand Down Expand Up @@ -210,7 +214,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
Expand All @@ -228,4 +235,5 @@ def fm_secondmate_summary_at($today):
elif (.holds | length) > 0 then "externally_held"
else "no_active_work" end)
end
end
end;
6 changes: 3 additions & 3 deletions docs/captain-hold-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +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.
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.
`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 <id> <surface> omitted by summary byte limit: <omitted> (kept <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.
Expand Down
Loading
Loading