RFC 0001: StaticFacts v0 public contract¶
- Status: implemented (initial full-analysis producer)
- Contract:
schemas/static-facts-v0.json - First producer:
qual static-facts [PATH...] - Follow-ups: StaticFacts projection, semantic dependency graph, ChangeImpact
Summary¶
StaticFacts v0 is a stable, versioned projection of qual's static
semantic model for Poietra and fast-manim. It is not a serialization of
LifecycleFacts, the abstract heap, cache entries, or internal handles. The
producer converts those implementation details into snapshot-scoped public
identifiers, self-contained source anchors, reason-carrying unknown values,
and deterministically ordered records.
The v0 surface is deliberately limited to:
- discovered Scenes;
- reachable objects and their provenance;
- play/wait groups and their animations;
- updater registrations;
- membership and render order at play boundaries;
- renderer risks/blockers; and
- coverage frontiers where the analysis lost precision.
This RFC defines the contract boundary and the minimum schema. The initial
producer is implemented by src/static_facts.rs and exposed by the
static-facts CLI command.
Goals and non-goals¶
The contract must let a consumer answer “which static candidates might this runtime or source operation refer to?” without depending on qual's Rust types. In particular, two calls to the same helper from different call sites remain distinct candidates, and an allocation in a loop is never represented as a singleton.
The following remain outside qual's responsibility:
- runtime object or play-execution identifiers;
- final StaticFacts/RenderTrace matching;
- gesture timing, follower semantics, or a final TracePlan;
- render-cache, checkpoint, or fork decisions;
- visual validation; and
safe_to_skip_render,safe_to_fork, or any equivalent optimization permission.
StaticFacts reports evidence that may block an optimization. Only a renderer's runtime proof may grant one.
Envelope and semantic inputs¶
A document has schema_version: 0 and contains:
- the producing tool version and semantic build digest;
- the source snapshot identity and resolved semantic configuration;
- the selected, versioned Manim knowledge profile and its source digest;
- render profiles considered by the analysis;
- source-file records;
- Scenes, objects, plays, animations, and updaters;
- renderer risks; and
- coverage frontiers.
Rule selection, suppression, confidence thresholds, baselines, output format,
and fail level are not semantic inputs. The producer requests every
fact capability needed by the projection regardless of --select or
--ignore. Consequently, those options do not enter semantic_config_hash or
the snapshot ID.
The project root is intentionally absent. Paths are project-relative POSIX paths, so moving the same source snapshot to another machine does not change the output.
Source anchors¶
Every source-backed public fact uses a SourceAnchor containing:
- project-relative POSIX
path; - SHA-256 of the exact on-disk bytes, before decoding;
- the normalized source
encodingand whether a UTF-8 BOM was present; - an end-exclusive byte range in the parser-visible Unicode text encoded as UTF-8; and
- an end-exclusive line/column span.
Lines and columns are one-based. A column counts Unicode scalar values from
the start of its physical line; it is neither a UTF-8 byte column nor a
terminal display-cell/grapheme column. The UTF-8 byte range addresses the
decoded parser text after BOM handling. files[].decoded_utf8_hash hashes
exactly that text. These rules make Shift-JIS/CP932 and Japanese anchors
round-trip without confusing raw-file offsets with parser offsets.
An anchor's raw_content_hash must equal the corresponding file record. A
consumer must reject or re-request facts when the hash does not match its
source bytes.
Snapshot-scoped identifiers¶
Internal FileId, ObjectId, PlayGroupId, allocation indices, and cache
keys never cross the contract boundary. Public IDs are opaque strings with a
kind prefix and SHA-256 payload:
snapshot:sf0:<64 lowercase hex digits>
scene:sf0:<64 lowercase hex digits>
object:sf0:<64 lowercase hex digits>
play:sf0:<64 lowercase hex digits>
animation:sf0:<64 lowercase hex digits>
updater:sf0:<64 lowercase hex digits>
risk:sf0:<64 lowercase hex digits>
frontier:sf0:<64 lowercase hex digits>
The hash input is a compact UTF-8 JSON array, serialized by
serde_json::to_vec. Arrays, rather than JSON objects, fix field order. Hash
inputs contain only strings, integers, booleans, and nested arrays. A source
anchor in an ID preimage is represented as:
[path, raw_content_hash, encoding, bom,
utf8_start, utf8_end, start_line, start_column, end_line, end_column]
The domain-separated preimages are:
snapshot = ["static-facts-v0", "snapshot", tool_version,
semantic_build_hash, target_python, semantic_config_hash,
[knowledge_name, knowledge_schema_version, knowledge_source_digest],
sorted [[path, raw_hash, encoding, bom, decoded_utf8_hash], ...]]
scene = ["static-facts-v0", "scene", snapshot_id,
qualified_name, definition_anchor]
object = ["static-facts-v0", "object", snapshot_id, scene_id,
allocation_anchor, bounded_call_context, cardinality]
play = ["static-facts-v0", "play", snapshot_id, scene_id,
call_anchor, bounded_helper_call_path, cardinality]
animation = ["static-facts-v0", "animation", snapshot_id, play_id,
source_argument_ordinal, source_anchor]
updater = ["static-facts-v0", "updater", snapshot_id, scene_id,
registration_anchor, bounded_call_context, registration_ordinal]
Risk and frontier IDs use the same domain prefix followed by the snapshot ID, entity kind, risk/frontier kind, primary anchor, and sorted related entity IDs.
Object call contexts keep the most recent two call sites, oldest first, which
matches the v0 k = 2 allocation identity. Play helper paths contain at most
32 call sites, outermost first. Reaching the helper depth cap produces a
coverage frontier; the producer must not fabricate the missing suffix.
IDs are stable only within one source snapshot and one v0 semantic producer.
They are not edit-stable IDs. Identity across edits belongs to the later
rematching contract and is reported as Match | Ambiguous | Missing.
Cardinality and execution certainty¶
cardinality is one of singleton, many, or maybe-many. It describes how
many runtime instances an abstract candidate may represent and participates
in object and play IDs. maybe-many is a known conservative abstraction, not
a reason-less null.
Play repetitions separately retains exact, interval, or symbolic numeric
information. execution_certainty says whether the source operation executes
on all analyzed paths. A branch-dependent operation therefore has unknown
execution certainty with a branch-join reason even if its candidate identity
and repetition interval are otherwise known. A loop-dependent play instead
uses loop-widening; a play nested in both may retain both reasons.
animation_arguments_complete is known yes only when every written play
argument was compiled individually. A self.play(*animations) splat makes it
Unknown with star-arguments provenance, creates a call-resolution frontier,
and prevents the projected animation_ids from being read as exhaustive.
Unknown values and provenance¶
No missing semantic fact is represented by null. A field-specific known
shape is paired with this unknown shape:
{
"status": "unknown",
"reasons": [
{
"kind": "dynamic-call-target",
"anchor": {
"path": "scene.py",
"raw_content_hash": "sha256:...",
"encoding": "utf-8",
"byte_order_mark": false,
"utf8_byte_range": { "start": 120, "end": 132 },
"unicode_span": {
"start": { "line": 8, "column": 9 },
"end": { "line": 8, "column": 21 }
}
}
}
]
}
An unknown candidate set may retain conservative candidates; unknown write
channels may retain known_channels; an unknown order may retain the
unordered candidate_object_ids. Those partial facts never turn an unknown
status into a proof.
The producer may keep the existing internal Num, Truth, and Presence
lattices. It records provenance in a sidecar keyed by internal fact identity
and field, then joins/deduplicates reasons during projection. The v0 sidecar
records causes that are explicit in CFG, call, and lifecycle facts (including
branch-dependent plays, non-literal duration overrides, and starred play
arguments). A lattice value alone is never treated as causal evidence. Reasons
are joined and sorted by kind, anchor, and related IDs. If provenance is
unexpectedly absent, the projection uses unsupported-semantics at the owning
source anchor; it never emits a reason-less unknown.
The v0 reason taxonomy is closed by the JSON Schema. A new reason kind is a contract change and requires a schema-version decision.
The projection also distinguishes non-literal-expression (a numeric value
is not statically reducible), untracked-value (the semantic heap has no
projectable value for a reachable candidate), and unknown-callback (an
updater registration is known but its callback target is not). These are
precision boundaries, not permission to guess a value.
Entity records¶
Scenes and objects¶
A Scene record identifies the qualified class and definition anchor, then lists its public object, play, and updater IDs. Constructor availability is a reason-carrying truth fact.
An object is reachable when it appears in a Scene/play membership snapshot, is an animation/updater candidate, remains in the final heap, or is connected to one of those objects by a parent/child, copy, generated-target, or replacement relation. The projection does not dump every interpreter temporary. Each object exposes allocation anchor, bounded call context, cardinality, kind candidates, binding/name candidates, relations, and final membership.
Plays and animations¶
A play record covers one abstract Scene.play, wait, or pause execution
candidate. It exposes its source call, helper path, cardinality, certainty,
repetition count, duration, ordered animation IDs, and membership/render-order
facts at three boundaries:
before: immediately before Manim compiles/begins the group;during: after begin/introducer/auto-add effects and before finish cleanup;after: after remover/replacement cleanup.
For a known membership boundary, entries is the exact set of objects that
may be present at that boundary; an omitted Scene object is known absent. For
an unknown boundary, entries are only the proven subset and omission carries
no meaning. A known render-order list is the exact effective display order;
an unknown order exposes only an unordered conservative candidate set.
An animation record distinguishes its resolved kind candidates, so
Transform and ReplacementTransform cannot collapse merely because they
share targets. It reports live target candidates, write channels, and
introducer/remover/replacement effects. Unknown targets and write channels are
reason-carrying unknown facts and also generate renderer risks.
Updaters¶
An updater record identifies the registration site and host/callback candidates. Hosts may be the Scene itself or reachable objects. It reports time-based behavior, possible write channels, and the play candidates during which it may be active. A possible active updater is a risk; StaticFacts does not conclude that a renderer optimization is unsafe or safe from that fact alone.
Renderer risks and coverage frontiers¶
renderer_risks is a one-way list of possible blockers:
- dynamic call;
- unknown animation target;
- active updater;
always_redraw;- dynamic wait or stop condition;
- camera mutation;
- external state or I/O;
- randomness;
- unknown write channel; and
- unknown render order.
Each record has certain or possible certainty, a primary anchor, related
candidate IDs, and applicable render profiles. The schema intentionally has no
optimization-permission field.
coverage_frontiers reports where exact projection stopped: decode/parse
failure, unresolved or dynamic resolution, recursion/depth fallback, branch or
loop widening, candidate caps, and unsupported semantics. Unknown fields refer
to the same structured reason taxonomy. A top-level completeness value is
complete only when no frontier exists; otherwise it is partial.
Deterministic serialization¶
For identical inputs, output must be byte-identical across runs and worker counts. When the semantic dependency graph adds an incremental path, it must also be byte-identical across cache hits, incremental analysis, and full analysis. The producer therefore:
- emits no timestamps, host paths, process IDs, or nondeterministic map order;
- sorts files by path and every entity array by public ID;
- sorts ID/reference sets lexicographically and preserves only semantically ordered arrays such as animations and call paths;
- sorts/deduplicates unknown reasons and risk profile names;
- serializes with the repository's single fixed pretty-print policy and one trailing newline; and
- constructs the projection from the same immutable source-byte snapshot used for parsing and hashing.
The initial CLI always performs full analysis, so cache state cannot affect facts. Incremental and full producers must eventually feed the same projection and pass byte-stability tests before an incremental route is enabled.
Security and execution boundary¶
Generating StaticFacts reads project source and versioned knowledge profiles. It must never import or execute Manim, plugins, analyzed user code, module-level expressions, callbacks, or patch results. Any unavailable dynamic behavior becomes a risk/frontier with reasons.
Follow-up contracts¶
The semantic dependency graph is a fact layer, not the cache partition. Cache components may consume its forward edges, while ChangeImpact consumes its reverse edges and projects reason paths through StaticFacts IDs and anchors. ChangeImpact compares two source snapshots so deleted and renamed definitions remain discoverable from the base graph.
Source patching and rematching are defined by RFC 0004. They consume these anchors and raw hash preconditions through separate request/output schemas. Arbitrary Python rewrites, complete CST preservation, and dynamic-call rewrites are not implied by either contract.
Acceptance and rollout¶
This contract-definition change is accepted when:
- the schema itself parses as Draft 2020-12;
- a representative fixture validates;
- unknown values without reasons and unknown extra fields are rejected; and
- the fixture demonstrates Japanese/Shift-JIS anchor metadata, distinct helper call contexts, non-singleton loop allocation, Transform versus ReplacementTransform, updater/risk facts, and a dynamic-call frontier.
The initial producer additionally proves:
- worker counts produce byte-identical JSON;
- rule selection does not affect StaticFacts;
- source anchors round-trip against raw Shift-JIS and Japanese source; and
- neither Manim nor analyzed code is imported or executed.
Incremental/full equality remains the acceptance gate for the later semantic
dependency graph integration; the initial static-facts command has no
incremental execution path.