Deployment-attributed fallback evidence¶
FallbackJournal is opt-in host-owned evidence for live compatibility routes.
It does not switch dispatch or establish that engine is unreachable.
from symfonic.platform.fallback_journal import FallbackJournal
from symfonic.platform.observability import observability
fallback_services = observability(
fallback_journal=FallbackJournal(
"/var/lib/myapp/fallbacks.jsonl", # provision this directory first
release_version="11.0",
deployment_id="staging-build-123",
),
)
# Inject fallback_services.fallback_telemetry_sink through the existing public
# AgentBuilder.fallback_telemetry(...) composition seam.
Use explicit non-sensitive build/deployment labels, never user input or secrets.
release_version is caller-declared, not inferred from a release flag or
installed package. Every JSONL journal row also carries package_version
when the installed symfonic-core wheel provably owns the running code; see
Installed package version.
The callback emits kernel_fallback_attribution.v1 JSONL records with timestamp,
version, deployment, capability, classified reason, entrypoint and legacy outcome.
It does not copy prompts, raw refusal prose, tenant IDs or run identity. Existing
kernel_fallback.v1 SQL metrics remain unchanged when configured alongside it:
that SQL/Mongo metrics row has no package_version column and is not read by
the deployment report, so version binding and the zero verdict cover the JSONL
journals only.
The journal requires POSIX local-file locking. Each completed write is fsynced; multiple local writers coordinate through advisory locks. Remote filesystems, cross-machine sharing and crash recovery of an interrupted partial line are not attested. A partial final line is incomplete evidence, not a valid JSON record. Each event adds synchronous IO; operators provision capacity, retention and rotation. The callback opens/closes the file on every event and creates new files with mode 0600, but does not change permissions on an existing file.
Direct callback failures propagate, but the cutover board suppresses them so
the legacy turn continues. The host-owned sink atomically snapshots
(failed_attempts, in_flight_attempts, capture_epoch) before suppression.
The epoch is a new random identifier for each sink instance, so a restart or
sink replacement changes it. fallback_services.fallback_capture_snapshot is
None when no counted sink was installed. An in-flight journal write can still
fail, and a fallback can be between board entry and sink entry, so zero counters
alone are insufficient.
A healthy dispatch does not prove a successful
journal write. Operators must establish journal health independently. Absence
of records cannot prove zero fallback without population, observation-window,
admission/refusal and completeness evidence. CORE-24 retains those obligations.
Opt-in invocation dispatch capture¶
Install DispatchOutcomeJournal through AgentBuilder.dispatch_outcomes(sink).
The existing SymfonicAgent.dispatch_outcome_capture_snapshot remains a read-only
three-tuple (failed_attempts, in_flight_attempts, capture_epoch).
For deployment evidence, use dispatch_outcome_capture_counts: an atomic
four-tuple with successful sink calls as delivered_count in position four. It returns None when no sink is installed;
never replace this with invented zero counters. Normal sink exceptions increase
the failure count without changing the turn result. A blocked callback remains
in-flight until it exits. The epoch belongs to the observer instance, so rebuilding
the agent changes it. Declarative SubAgentSpec children built by the parent
share its observer, counters and epoch because they inherit the same sink.
Their failed or blocked callbacks therefore appear in the parent's snapshot.
Caller-built SubAgent(agent=...) children retain their own observer and sink;
inventory and snapshot those separately when including them in deployment
evidence. Sharing a sink across independently built agents does not merge their
observers automatically.
At each paused/drained boundary, read
dispatch_before = agent.dispatch_outcome_capture_counts and later
dispatch_after = agent.dispatch_outcome_capture_counts. Require both readings,
zero in-flight counts, an unchanged epoch and nondecreasing failure and delivered
counts.
Include each observer's actual journal files (and rotations) in
expected_dispatch_journals, mapped by replica and journal ID. In both boundary
records, include matching dispatch_snapshots entries containing failure_count,
in_flight, epoch and delivered_count from those measured tuples.
The delivered delta for each observer must equal the in-window dispatch rows in
its declared journal files. An empty global dispatch journal, a failure delta,
or any count mismatch invalidates healthy capture.
This accessor supplies dispatch capture health only. Staging deployment, complete replica/file inventory, host ingress/drain controls and invocation population reconciliation remain separate evidence obligations; these snapshots alone do not establish fleet zero, fallback unreachability or engine retirement.
Pre-admission Kernel-route attempts (CORE-300)¶
Install a separate host-owned callback with
AgentBuilder.kernel_route_attempts(RouteAttemptJournal(path, release_version=..., deployment_id=...))
from symfonic.platform.route_attempt_journal. This observes the actual
SymfonicAgent invocation route selection, before admission, release-contract
validation or continuation token recognition. It does not count status reads of
route_for, the typed stream's continuation-availability check, unconsumed
generators, direct legacy routes, or guards that refuse before route selection.
Resume checkpoint setup currently precedes selection; failures there do not
become Kernel-route attempts. Preserve that distinction in ingress accounting.
kernel_route_attempt.v1 stores only entrypoint, capability,
selected_route="kernel", observed_at, release_version and deployment_id
along with its schema kind. It carries no payload, raw reason, run/session,
tenant or token values. Later fallback or preparation failure leaves the attempt
intact. Dispatch outcomes still describe the separate subsequent boundary; no
native dispatch or native entry count is substituted for an attempt.
agent.kernel_route_attempt_capture_counts is an atomic
(failure_count, in_flight, epoch, delivered_count) tuple, or None when unwired.
Ordinary sink failures increment failures without changing admission or turn
results; cancellation/BaseException is counted and propagates. Declarative
children share their parent's observer and epoch. Independently supplied children
must be inventoried separately, even when sharing a callback.
At both paused/drained boundaries, record these measured tuples under
capture_boundaries[replica][boundary]["route_attempt_snapshots"][journal_id]
using the same four named fields as dispatch snapshots. Declare every active
observer's files and rotations in expected_route_attempt_journals, shaped as
{replica: {journal_id: {"files": [path, ...]}}}. Independent observers cannot
share an attested attempt file. The delivered delta must equal selected
in-window attempt rows across those files. Missing inventory, snapshots, files
or rotations, failures, in-flight callbacks, epoch replacement, counter resets
and row mismatches prevent complete zero evidence. An absent sink is unknown,
never an invented zero. The report exposes kernel_route_attempt_count,
kernel_route_attempt_groups and route_attempt_capture_status separately.
To require the actual denominator, supply an explicit nonempty
min_kernel_route_attempts_by_capability mapping in ZeroWindowPolicy or its
CLI JSON policy, for example {"invocation.run": 100, "invocation.stream": 100}.
These illustrative counts do not approve a policy. No threshold is inferred
from observations; a supplied approval reference remains an unverified caller
claim. Required capability groups below their positive minima return
below_threshold; healthy measured attempt capture is mandatory. A continuation
requirement additionally needs positive attempts through both resume and
resume_interrupt; exercising one cannot certify the other. Native entry and
dispatch capture requirements remain in force, and positive fallback/direct
legacy observations retain precedence over incomplete evidence.
Policies omitting this mapping preserve their existing native-entry window
semantics for old inputs and do not satisfy the CORE-24 per-capability
attempt criterion. Selecting new attempt rows without their inventory makes
the report incomplete for every policy. A file may intentionally contain
several observation schemas: each observer is reconciled only against its own
schema's rows; the attempt observer still requires its separate snapshot.
Attempt policies report population_basis="pre_admission_kernel_route_attempts"
and explicit capability thresholds/entrypoint counts. These producer observations
cover the instrumented compatibility facade only; they do not automatically
include direct public Agent roots or establish the whole ingress population.
Complete fleet/host inventory, owner/SRE policy and an operated healthy window
remain external evidence obligations. No bounded window verdict authorizes
fallback unreachability or retirement.
Opt-in native entry capture¶
Install the journal through the existing host-owned event sinks, preserving other observers. The provider and scope are the deployment's own composition inputs; provision the journal directory before serving traffic.
from dataclasses import replace
from symfonic import Agent
from symfonic.platform import observability, telemetry
from symfonic.platform.native_entry_journal import NativeEntryJournal
services = observability()
native = NativeEntryJournal(
"/var/lib/myapp/native.jsonl",
release_version="11.0",
deployment_id="staging-build-123",
entrypoint="http_chat",
)
services = replace(services, event_sinks=(*services.event_sinks, native))
agent = Agent(provider, capabilities=[telemetry(services, scope)])
The binding hook records entry into the Kernel before its prompt/model stages,
not a pre-Kernel admission attempt or successful completion. Provider
construction can fail before that hook and produce no record. No run, tenant,
session, or payload values are persisted; identity_known states only whether
the supplied run identity was nonempty. Collector construction or an earlier
sink's binding failure can prevent this journal's bind_run from being called;
those missing entries do not increment its failure counter. Install this sink
after any composition that replaces event_sinks (including generated OTel
setup), and
verify health independently; an uninstrumented root is not a zero-fallback root.
native.capture_snapshot atomically returns
(failed_attempts, in_flight_attempts, capture_epoch). Failed writes are
counted only after this journal's bind_run starts, even if the Kernel observer
suppresses the exception and the public Agent.run result succeeds. Zero
counted failures therefore does not establish complete native entry capture.
A new journal instance gets a new epoch; do not replace or restart it during
an attested window. A successful write after a failure does not repair the
lost entry.
Keep NativeEntryJournal last in event_sinks: a disk failure in its binding
hook skips run binding for later sinks in the existing fanout. Earlier sinks
are already bound when that hook runs. This ordering concerns run binding,
not event delivery, and does not change telemetry failure handling.
Read-only deployment report¶
Approved quick-nap composition refusal capture¶
The first approved native composition producer is opt-in and fixed to the
shipped ConsolidationSchedule constructor, preserving nightly/deep defaults:
from symfonic.platform.migration_refusal_journal import MigrationRefusalJournal
from symfonic.platform.quick_nap_composition import quick_nap_schedule
refusals = MigrationRefusalJournal(
"/var/lib/myapp/composition-refusals.jsonl",
release_version="11.0",
deployment_id="staging-build-123",
)
schedule = quick_nap_schedule(5, refusal_observer=refusals)
# quick_nap_schedule(0, refusal_observer=refusals) records the actual rejection
# and re-raises the same MemoryContractError. Disable naps by omitting their
# coordinator/stage, not by constructing a nonpositive schedule.
Only an exact built-in int <= 0, rejected by the shipped quick cadence
validator, emits native_composition_refusal.v1. Boolean/custom/string inputs
and unrelated errors are not classified. Traceback provenance narrows the
producer; it makes no process-tamperproof claim. The frozen event records only
quick_nap_nonpositive, ConsolidationSchedule.quick_every_turns, composition
and approval reference CORE-1:d79e0ce1-8a48-4edc-bce4-966d9c40159b. The
automation-authored approval record explicitly confirms the owner's decision
on 2026-09-23; its author is not being identified as the owner.
No observer means no writes. Normal observer exceptions preserve the original refusal, while cancellation/BaseException propagates. The same local-lock, fsync, bounded-label, health, capacity and rotation limits apply to this journal. Include its files in the explicit report inventory. Refusals count composition attempts, not invocation population or Kernel entry; they cannot establish fallback rates, zero fallback, unreachability or retirement. CORE-59 remains incomplete: legacy invocation refusal approval is still UNKNOWN.
report_deployment_evidence reads only the explicit local JSONL paths supplied
by an operator. It performs no discovery, network access, or writes. Select an
aware UTC half-open window [start, end) and exact release/deployment labels:
from datetime import UTC, datetime
from symfonic.platform.deployment_evidence import report_deployment_evidence
from symfonic.platform.zero_window import evaluate_zero_window
# Single-replica example. At BOTH boundaries, deployment-specific host controls
# must pause ingress, drain every admitted turn (including pre-sink fallbacks),
# drain telemetry callbacks, then snapshot while ingress remains paused.
# The library does not perform or verify these controls. Set the attestations
# below to True only after those checks succeed; never replace None with zero.
capture_before = fallback_services.fallback_capture_snapshot
native_before = native.capture_snapshot
# dispatch_agent is the instrumented SymfonicAgent root from dispatch capture
# above; inventory caller-built children and independent observers separately.
dispatch_before = dispatch_agent.dispatch_outcome_capture_counts
capture_before_at = datetime.now(UTC) # Local observation immediately after the read.
if (capture_before is None or dispatch_before is None or capture_before[1] != 0
or native_before[1] != 0 or dispatch_before[1] != 0):
raise RuntimeError("capture is unwired or not drained")
start = datetime.now(UTC)
# Resume ingress, serve the selected window, then pause/drain again.
end = datetime.now(UTC)
capture_after = fallback_services.fallback_capture_snapshot
native_after = native.capture_snapshot
dispatch_after = dispatch_agent.dispatch_outcome_capture_counts
capture_after_at = datetime.now(UTC) # Local observation immediately after the read.
if (capture_after is None or dispatch_after is None
or capture_after[2] != capture_before[2] or capture_after[1] != 0
or native_after[2] != native_before[2] or native_after[1] != 0
or dispatch_after[2] != dispatch_before[2] or dispatch_after[1] != 0):
raise RuntimeError("capture sink restarted, replaced, or is not drained")
if (capture_after[0] < capture_before[0] or native_after[0] < native_before[0]
or dispatch_after[0] < dispatch_before[0]
or dispatch_after[3] < dispatch_before[3]):
raise RuntimeError("capture counter reset")
paths = ["/var/lib/myapp/fallbacks.jsonl", "/var/lib/myapp/native.jsonl",
"/var/lib/myapp/dispatch.jsonl"]
report = report_deployment_evidence(
paths,
release_version="11.0",
deployment_id="staging-build-123",
start=start,
end=end,
manifest={
"release_version": "11.0",
"deployment_id": "staging-build-123",
"start": start,
"end": end,
"expected_files": paths,
"expected_replicas": ["replica-a"],
"expected_native_journals": {"replica-a": {"native-primary": {
"files": ["/var/lib/myapp/native.jsonl"], "entrypoint": "http_chat",
}}},
"exercised_population": 100,
# Required for a zero verdict: the fallback attribution journal is the
# only one that records a legacy guard refusal as a fallback.
"expected_fallback_journals": {"replica-a": {"fallback-primary": {
"files": ["/var/lib/myapp/fallbacks.jsonl"],
}}},
"expected_dispatch_journals": {"replica-a": {"dispatch-primary": {
"files": ["/var/lib/myapp/dispatch.jsonl"],
}}},
"minimum_exercised_population": 100,
# The symfonic-core version deployed for this window, from your release
# record; a zero verdict needs every in-window row stamped with it.
"package_version": deployed_package_version,
"coverage_attested": True,
"health_attested": True,
"capture_attested": True,
"capture_boundaries": {"replica-a": {
"start": {
"ingress_paused_at_snapshot": True,
"admitted_turns_drained": True,
"telemetry_callbacks_drained": True,
"observed_at": capture_before_at.isoformat(),
"snapshot": {"failure_count": capture_before[0],
"in_flight": capture_before[1], "epoch": capture_before[2]},
"native_snapshots": {"native-primary": {
"failure_count": native_before[0],
"in_flight": native_before[1], "epoch": native_before[2],
}},
"dispatch_snapshots": {"dispatch-primary": {
"failure_count": dispatch_before[0],
"in_flight": dispatch_before[1], "epoch": dispatch_before[2],
"delivered_count": dispatch_before[3],
}},
},
"end": {
"ingress_paused_at_snapshot": True,
"admitted_turns_drained": True,
"telemetry_callbacks_drained": True,
"observed_at": capture_after_at.isoformat(),
"snapshot": {"failure_count": capture_after[0],
"in_flight": capture_after[1], "epoch": capture_after[2]},
"native_snapshots": {"native-primary": {
"failure_count": native_after[0],
"in_flight": native_after[1], "epoch": native_after[2],
}},
"dispatch_snapshots": {"dispatch-primary": {
"failure_count": dispatch_after[0],
"in_flight": dispatch_after[1], "epoch": dispatch_after[2],
"delivered_count": dispatch_after[3],
}},
},
}},
},
)
# Supply approved_policy as a ZeroWindowPolicy backed by an owner/SRE decision;
# this example does not select or approve deployment thresholds.
verdict = evaluate_zero_window(report, approved_policy)
# Read verdict["verdict"] for the bounded window decision.
The manifest is an operator attestation. Supply the complete replica and file
inventory, including rotations; certify coverage, capture and journal health
for that exact window. exercised_population and its minimum are explicit
operator values; the report has no default population threshold or window and
does not derive population from journal entries. The report derives
capture_failure_count and native_capture_failure_count from each
replica's start/end atomic snapshots. Declare every active
NativeEntryJournal instance in expected_native_journals, keyed by replica
and stable non-sensitive journal ID, with its file path(s) and entrypoint.
Each native file must belong to one journal ID, appear in expected_files,
and any native rows in it must use its declared entrypoint. Each boundary's
native_snapshots must contain exactly those IDs. The report sums their
failure deltas and refuses
missing or extra snapshots, changed epochs, resets, and in-flight writes.
Legacy native_snapshot manifests without this inventory remain readable but
are incomplete and cannot receive a zero-window verdict. The inventory is
an operator attestation: the report cannot discover an omitted journal or
bind a row to a specific instance when journals share a file, so shared
native files are refused. Certify every active instance and keep its epoch
stable for the whole window; a replacement invalidates that window. Both
boundaries require snapshots. Every listed replica must attest paused ingress,
drained admitted turns and drained
telemetry callbacks at both boundaries. Missing or false attestations, missing
replicas, nonzero in-flight counts, a changed fallback or native capture epoch,
a reset counter or a positive failure delta in either capture make
completeness incomplete and withhold a zero-window verdict. Each boundary record must include an aware UTC
observed_at: start must be at or before the requested start, end must be at
or after the requested end, and start must precede end. Missing, invalid or
out-of-order times make evidence incomplete. The snapshots bracket the selected
window, so
boundary-adjacent failures may conservatively increase the delta. The example
records observation times immediately after snapshot reads, not window bounds;
these remain operator claims. The report cannot verify that the deployment
actually paused/drained, that the snapshots
were taken at the claimed time, or that fleet membership is complete. A
matching manifest produces
at most completeness="operator_attested", never executable coverage proof.
Dispatch capture health is an additional zero-window requirement. Without
expected_dispatch_journals, a readable report can still show
completeness="operator_attested" and issues=[] while
dispatch_capture_status="incomplete". It cannot reach
zero_observed_within_policy; absent a higher-precedence observation or empty
native population, its verdict is incomplete_evidence. Positive fallback or
legacy-body observations and not_measurable take precedence.
Supply the dispatch inventory and matching snapshots at both boundaries for
every replica and observer. Healthy capture requires zero in-flight writes,
unchanged epochs, nondecreasing counters and zero failure deltas. Use the
evaluator's verdict field for the decision, rather than completeness alone.
The result separates native_entry_count (records of
kernel_native_entry.v1, outcome="native_entry") from fallback_event_count
(records of kernel_fallback_attribution.v1, outcome="legacy"). Fallback
groups preserve entrypoint, capability and reason. Native entries group by the
operator entrypoint label and expose the count with unknown identity. Neither
count is a unique turn count, and the report computes no fallback rate. Native
entry capture proves entry only, not successful completion or parity.
Fallback records retain their existing created_at UTC timestamp spelling;
the reader also accepts observed_at. Native records use observed_at and a
boolean identity_known, without copying identities. Malformed JSON, missing
required fields, unknown schemas/outcomes, invalid timestamps, unreadable files
and partial final lines are flagged, with valid positive observations retained.
Valid records for other exact deployments/releases or outside the window are
excluded. A record timestamp must be aware UTC; other offsets and naive times
are rejected rather than silently reinterpreted.
Duplicate paths, symlink/parent-path aliases, and hardlinks to the same opened
capture file are rejected to prevent counting a capture twice.
Missing manifest population, replica/file inventory, capture/health/coverage
attestation, unknown native identity, or a fallback with an unknown
entrypoint, capability or reason code makes completeness incomplete. The report
retains these fallback observations and exposes
unknown_fallback_attribution_count so missing attribution cannot look like a
complete capture.
An empty selected population is status="not_measurable" even when all
attestations are present. Positive counts return positive_observations, with
any completeness limitations alongside them. Zero observed fallback events
never mean zero proven fallback. approved_migration_refusal_count and its
closed metadata groups report this composition category separately; its status
is observed only for positive counts and otherwise unobserved. Refusal-only
files retain positive refusal counts, but do not supply invocation population:
the overall status remains not_measurable, completeness remains incomplete,
and empty_observed_population remains present even with a complete manifest.
The report cannot return a retirement pass, establish fallback unreachability,
or discharge the fourteen-condition retirement gate.
Installed package version¶
Every JSONL journal described here (fallback attribution, dispatch outcome,
Kernel route attempt, native entry and migration refusal) stamps
package_version only when the symfonic-core dist-info that
importlib.metadata resolves provably owns the code the process imported
(symfonic.platform.artifact_identity). All of these must hold:
- the install is not editable (
direct_url.json); - its RECORD lists every loaded
symfonicmodule, includingsymfonic/__init__.pyand the stamping modules, at the path it was imported from, with a matching sha256. A loaded module the RECORD does not list (one added to the install later, or another distribution's) withholds the version; - its RECORD lists
METADATAwith a matching sha256, and the stamped version is read from those same bytes, so an edited version label is not stamped; - none of those files, nor the RECORD, changed after the process started
(read from
/proc); - the process itself imported
symfonic: a worker forked from a parent that had already imported it, and had not stamped yet, runs code read before the worker's own start, so it is unstamped.
Otherwise the key is omitted, never invented and never read from
pyproject.toml. So a source checkout, an editable install, a checkout
shadowing another install's dist-info, a hand-edited file or version label, a
host whose start time cannot be read (non-Linux) and a pre-fork server that
imports Core in the master before its first journal write all write unstamped
rows, which withhold zero. The result is resolved at the first journal write
and frozen for the process. Each later write re-checks every module imported,
re-imported or reloaded since, against the same RECORD bytes; a module that
fails, or a RECORD that changed or is gone (a version-changing reinstall
removes the old dist-info), withdraws the version for the rest of the process.
Code loaded before the first write was attested by it, and a worker forked
after that write keeps the parent's answer under the same re-checks. Only
loaded modules are attested; a replaced file the process never imports is
not. A file written in roughly the last 20 ms before the process started also
counts as changed (fail closed). This proves the RECORD and the .py files on
disk agree. It is not build authenticity: a RECORD rewritten to match edited
files before the process starts passes, and the bytecode actually executed is
not hashed.
The report adds package_version_counts (in-window rows per stamped version),
unstamped_package_version_count (rows written before stamping, or by a
process whose install could not be proven) and declared_package_version. Two stamped
versions in one window add mixed_package_versions. A manifest may declare
package_version; every in-window row, unstamped ones included, must then
match it or the report adds package_version_mismatch. Each of these issues
leaves the report incomplete, so the window verdict cannot read
zero_observed_within_policy. A manifest that omits package_version reports
unstamped rows without treating them as an issue, so the report stays useful for
inspection. Rows written before stamping remain valid v1 rows.
A zero verdict additionally requires the version to be declared and bound: see
package_version_binding under the window verdict below.
Zero-fallback window verdict¶
evaluate_zero_window(report, ZeroWindowPolicy(min_window=..., min_native_entries=..., approval_reference=...))
classifies one report as fallback_observed, legacy_body_observed, not_measurable,
incomplete_evidence, below_threshold or zero_observed_within_policy. The
policy has no defaults: no owner/SRE has approved a window or volume. Its
required approval reference is supplied by the caller and echoed for audit;
this function cannot verify the referenced decision. The proposed 28 days and
10,000 per-capability route attempts in
docs/concepts/kernel-fallback-telemetry-v11.md are not interchangeable with
this function's deployment-level native Kernel-entry count. Native entries
carry no capability, so a per-capability denominator does not exist here.
The span is the requested, operator-attested capture window, not evidence that
events were evenly spread across it. zero_observed_within_policy means only
that no fallback record was seen for that deployment/release window and
no dispatch record observed a fallback or direct legacy body, with attested
capture, healthy dispatch capture and healthy fallback attribution journal
capture for every listed replica, and that every in-window row was stamped
with the one package_version the manifest declares.
A dispatch fallback or direct legacy outcome returns legacy_body_observed when
no fallback journal record is present. This does not satisfy the per-capability
retirement criterion.
Every verdict carries package_version_binding. Only package_version_bound
permits zero: the manifest declared a bounded package_version, every
in-window row is stamped, and every stamp equals the declaration. Otherwise
the value names what is missing and the verdict is incomplete_evidence:
package_version_binding |
Meaning | What to do |
|---|---|---|
package_version_undeclared |
The manifest omits package_version, or declares an invalid value such as unknown |
Declare the deployed version from your release record |
package_version_unstamped_rows |
Some in-window rows carry no version (pre-stamping install, or unreadable metadata) | Capture a window written entirely by a stamping install |
package_version_mixed |
Rows from two installed versions share the window | Narrow the window to one rollout, or report each version separately |
package_version_mismatch |
Rows are from one version, but not the declared one | Correct the declaration or the deployment you selected |
package_version_unreported / package_version_unobserved |
The report carries no usable version counts | Pass an unmodified report_deployment_evidence result |
Zero also requires the fallback attribution journal. The dispatch journal does
not see every legacy body reach: with ask_user_enabled and no scope or
session, the turn falls back and the guard that refuses it is the first
statement of the legacy body. Dispatch records that turn as approved_rejection
with selected_route="none"; only the fallback journal records it as a
legacy fallback. So the manifest must declare expected_fallback_journals
(per replica, {journal_id: {"files": [...]}}), and every declared file must be
selected and readable. Create the file before the window opens, because a
journal with no fallback has never been written. The report's
fallback_capture_status is:
undeclared: no inventory. This is not a report issue; old manifests stay readable.incomplete: a malformed inventory, a declared file not selected or unreadable, a fallback row in an undeclared file (fallback_journal_binding_mismatch), a declared file that is also a native, dispatch or route-attempt journal or holds any other row kind (fallback_journal_kind_mismatch), one file declared twice, for two replicas or through a symlink (fallback_journal_file_overlap), or a failed or in-flight fallback capture. Give every replica'sFallbackJournalits own file; fallback rows in a mismatched file are still counted.healthy: zero is permitted.
incomplete_evidence also lists incomplete_reasons: any of
report_incomplete (see the report's issues), dispatch_capture_unhealthy,
route_attempt_capture_unhealthy, fallback_capture_undeclared,
fallback_capture_incomplete, and the binding value above. Neither
requirement hides a finding: an observed fallback or legacy body is reported
whatever the version or capture evidence says.