Repository navigation
v0.5.0 — M4 complete
M4 of ADR-0001, pose correctness and
homography steadiness: the code and its synthetic acceptance, with the device campaign on its own
milestone. .wnft format: still 0.3, in both codecs, so this tag freezes no new corpus.
Added
-
NftTrackercan read how far a fit is from one a plane can produce, and is off by default
(#161, M4's step 2,
#104). Two quantities are read on a fit's
homography before the pose step,d_scaleandd_orth; both are 0 for a plane seen by a
pinhole camera. The newconditioningoption has three states.- Off, the default. Nothing is computed and the pose path is unchanged. Every result now
carries two fields,conditioningandtrackLossConditioning, bothnullin this state. - Reporting,
{ mode: "report" }. Every result with a pose carriesconditioning, the two
numbers kept apart, never combined into one. Nothing is refused. - Refusing,
{ mode: "refuse", maxDScale, maxDOrth }. A fit past either threshold, or one
whose reading cannot be taken, gets no pose. A detection ends"LOST"with the new reason
"conditioning". A tracking step drops its lock with the newtrackLoss: "conditioning",
its reading intrackLossConditioning, and the frame is detected again.
No threshold is registered, and none is built in. The derivation on #90's sweep
(docs/benchmarks/2026-10-21-104-stage1-threshold-derivation.md) found no single value that
separates right fits from gross wrong ones pooled across the sweep's two views. So both thresholds
are the caller's: numbers ≥ 0,Infinityfor no bound. The constructor throws aRangeErroron a
refusing state without them, on a threshold given in another state, and on an unknown mode. What
is claimed: on that synthetic sweep, read per view at each view's own floors, no right fit of
either target is refused and each of the eight gross wrong fits of M2's 64 patches is
(test/pose/conditioning_sweep.test.ts). No device claim and no campaign claim. The reading and
the refusing rule are also exported, asconditioningandconditioningRefuses, with
DEFAULT_CONDITIONING. - Off, the default. Nothing is computed and the pose path is unchanged. Every result now
Changed
NftTrackertakes its pose from IPPE, above the contract (M4's step 3,
#105). The two poses a planar homography
allows are computed inpackages/nft-tracker/src/pose/; one is chosen per frame by depth,
by reprojection error over the frame's correspondences, and by a declared margin under which
the frame is ambiguous and takes a tracker-local copy of the closed form; no earlier frame
enters. The backend'sposeFromHomographyis no longer called by the tracker; the contract is
unchanged. Results carryposeSelection, and the tracker countsfallbackFramesper run. A
Detectionand a tracking step's result now carry theircorrespondences. What is claimed
for it, and no more: an accuracy result on rigid synthetic ground truth, from criteria
registered before the implementation (docs/benchmarks/2026-10-15-m4-step3-ippe-acceptance.md):
deterministic (bit-identical on repeat, unchanged under 60 random correspondence orders, 0 reads
ofMath.random), less accurate than the closed form below 7° of tilt (per-step medians of the
error ratio 2.2 to 3.0), a tie at 10° (median 0.968, quartiles straddling 1), and clearly more
accurate from 15° to 50° (2.4× to 3.5× on the per-step medians), the crossover lying between 7°
and 10°. No steadiness (jitterPx)
claim: that metric readsH, which the change does not touch, and so cannot respond to it. No
campaign claim.examples/bench-nft.htmlattests the code a session run executes
(#111, designed in
#110).-
One bundle for both arms. Both detection arms now load one bundle of the three
packages,examples/dist/webarkit-packages.mjs: the page through its import map, the worker
by URL. The separately bundled worker,examples/dist/detection-worker.mjs, is gone. -
npm run buildisscripts/build-provenance.mjs. It builds the packages, then the bundle,
thenexamples/dist/provenance.json: the commit, whether the tree was clean, and the hash of
every input. Without git it still builds, and the manifest says the commit is unknown. -
What Start checks. A session run (
?loops=or?run=) is checked at Start against that
manifest, and refused, with its reason and remedy, on:- a mismatch with the build;
- a dirty tree, or a manifest built without git;
- a module the check cannot see, or one run from the HTTP cache;
- a response without
Cache-Control: no-store.
The check attests the bytes the server serves at Start, not the bytes the page loaded. Other
runs refuse nothing and record the findings. -
What exports carry. Exports carry
provenancebesideprotocol, and
run.lostVisibility. They no longer carrydetection.workerBundleSha256. -
Parameters and server.
?loops=and?run=take whole numbers only, never floored. Serve
the repository withnpx http-server -c-1. The demos still load each package's owndist/. -
Replay.
scripts/replay-clips.mjstakes--packages <dist|bundle>.
-
Fixed
- The bench's guarded paths, before M4's measurements open
(#96 and
#95's[guarded]items, the guarded-items
half of M4's step 0).- A loop wrap is the clip restarting. Any step back in media time used to count as a loop
wrap, so a stutter or a seek could fabricate a loop in every metric read per loop, and end a
?loops=run a loop early. A step back is now a wrap only near the clip's start, with at most
0.5 s skipped across the wrap; any other step back refuses the run, naming the frame. No
committed export's figures change.- Where the 0.5 s bound comes from, and its status, are
#114's. - The fallback is chosen. Without a clip duration, the rule asks for a step back of more
than 0.5 s; that reuses the value, and was not derived. - Its revisiting is #114's: when, and
by what rule. - A refused run is a rejected sample. M4's runbook re-runs it and does not record it.
- Where the 0.5 s bound comes from, and its status, are
replay-clips --transferrecomputes each export's protocol with the tracker's own
defaults, and refuses a record the rule does not bear out, rather than trust
protocol.sessionRun. M3's committed exports all pass, and their transfer is unchanged.- A worker failure after a run ended fails that run, and the run a Download exports, so
that Download is refused; it used to be dropped, and the run exported as clean. A later
failure no longer replaces the first on the error line. - Smaller corrections.
- The worker passes on the decoder's warnings.
- Start names why a
.wnftdid not load, on both detection paths, where it had pointed at
an error line it had just cleared. - A replay arm with external detection exports
detection.path: "worker"and a new
detection.latencyOf. replay-clipsrefuses--tracking-onlywhere it does nothing. It also refuses
--packages bundleon a manifest built without git, where it had thrown aTypeError.firstStepLatencykeeps a sample detected up to 1 ms past the clip's end.- Refusals number frames alike, and a count recorded as text is quoted.
packages/nft-tracker's README andsrc/tracker.tssay what M3's tuning pass did to
each default, where they had called the defaults provisional until a pass that has since
run. The README also documents the four detection options and their domains.
- A loop wrap is the clip restarting. Any step back in media time used to count as a loop
What M4 did not deliver
- No threshold for the conditioning signal, and so no refusal the campaign can read.
#104's body requires the threshold recorded,
with its derivation, before the campaign reads it. The derivation
(docs/benchmarks/2026-10-21-104-stage1-threshold-derivation.md) found no single value: pooled
across #90's sweep's two views, nothing separates
right fits from gross wrong ones, in either form, at any of three focal lengths. Per view both
forms separate, by 4.44 on the first view and 1.28 on the second, and on the second that
separation is gone by fov 90°. The cause is measured and reproduced as a fatal check: the sweep's
second view is not a plane homography for the camera, so its correct fits read 0.0084 and 0.031
and sit above the first view's gross wrong ones. What is registered instead is the two per-view
intervals and that reason. The signal therefore ships off by default, and its refusing state takes
both thresholds from the caller or throws. - The three candidates were not weighed against each other. #104's body asks that its signal be
weighed against the two directions of #92 — a
limit scaled by the inlier count, and a bound on the scale change one step may imply — on #90's
sweep, by evidence. One of the three is weighed. #92 is on M5 and carries the comparison; the
instrument it needs is the derivation's script, which reproduces the frozen sweep under a fatal
step 0 and reads a criterion over every fit it produces. - Nothing here was read on a device. Every claim in this entry is synthetic: #90's sweep and the
acceptance files. The campaign that reads the tracker on the reference device is the "M4 campaign"
milestone, whose eight registered preconditions for round 1 are all open, so this milestone ships
with its measurement arm unstarted — which is what splitting it was for. - The contract question of #43 is unanswered,
and cannot be answered while there is one implementation: whethercv-backend-specmust say which
rootposeFromHomographyreturns, and whether a second implementation must match the first. #43's
other question, whether pose disambiguation stays above the contract, is answered in code and by
measurement. It was examined for gate 4 and left open with no milestone, named on
#9 so that neither lands without the other being
looked at.