Date: September 13, 2026

Author: SCYTHE Team

Category: Evidence-Centered Computing, RF, GraphOps, Promotion Discipline

—

An SDR is plugged into this workstation. A `Realtek RTL2838` sits on

`Bus 001 Device 026`, tuned to nothing, doing nothing. Over the last several

weeks SCYTHE built the entire path that would let a live walk produce evidence

good enough to promote into GraphOps — the durable ledger, the ownership lock,

reconciliation, ceilings, the execution boundary, the derived-evidence reader

and its producer.

And then, at the moment the run was scoped, the answer came back: **do not open

the radio.**

Not because the radio is dangerous. Because a capture would have changed

nothing, and running one anyway would have been authorization theater with a

USB cable attached.

That decision, and the six slices of code that followed from it, are the most

interesting thing SCYTHE has built this quarter. The engineering story is not

“we captured RF.” It is “we established exactly what our evidence is allowed to

claim, and then wrote code that cannot claim more.”

## The Finding That Made the Run Small

The question was simple: what does a walk verdict actually depend on?

SCYTHE’s walk checker compares two position fixes against a kinematic budget.

Did the operator move further than a person walking could have moved in the

elapsed time? Two hashes travel with each fix:

“`text

signal_chain_hash          sensor identity, sample type, rate, antenna,

                           feedline, extension, gain  — all DECLARED

receiver_state_chain_hash  a manifest of position, speed and orientation

                           authorities

“`

Neither touches a sample. `check_walk_step` never sees one either. A walk

verdict is a statement about displacement, and displacement is computed from

latitude, longitude and time.

So we asked what a capture would buy. It would activate the volatile IQ buffer,

`rtl_tcp`’s socket binding, the ring’s invalidation rules, and the recovery

subsystem’s posture — the entire Q1 surface — and produce an artefact

identical in every field the verdict depends on. A live observation does not

become more meaningful because a receiver was consuming samples nobody looks at.

The act was classified instead:

“`text

LIVE_POSITION_ATTESTATION      a real bounded observation over real

                               attested positions

INSTRUMENT_CONFIGURED_IDLE     the instrument is declared, and idle

RF_MEASUREMENT_NOT_PERFORMED   no sample was taken, and none was needed

“`

The RTL2838’s presence may be declared. The run must not open, tune, reset,

claim, or otherwise communicate with it.

## The Schema Could Not Express That, So It Changed

Here is where a contract stops being prose. The artefact provenance in

`scythe_derived_evidence` is a closed field set — a name that is not in the

set is refused rather than ignored — and it carried no measurement status at

all. An artefact could describe a sample rate and a gain, and nothing in the

record distinguished configured from exercised.

Adding a required field to a closed set changes what the old schema name means,

so the name changed with it:

“`text

scythe.derived-evidence-artefact.v1  ->  .v2

“`

A v1 artefact is now refused as foreign — carrying the new fields or not,

because the version is not a hint to be overridden by whatever happens to be

present. There is no migration and no default, and none is needed: no v1

artefact was ever produced. Writing a compatibility path for a population of

zero would mean inventing exactly the defaults the amendment forbids, for a

reader that would never meet one.

The two new fields are closed to a single value each:

“`python

MEASUREMENT_STATUSES = (RF_MEASUREMENT_NOT_PERFORMED,)

INSTRUMENT_STATES    = (INSTRUMENT_CONFIGURED_IDLE,)

“`

One member is the honest size. This tree can produce exactly one kind of

artefact, and a second value would name a capability that does not exist, read

by a reader that has never seen one produce anything. When a measurement path

is contracted, its amendment adds its value.

## Configuration Is Not Measurement

The subtler rule is the one a well-meaning classifier would get wrong.

An artefact may legitimately carry the configuration it did not exercise —

`sample_rate_hz = 2_400_000`, `gain_db = 40.2`, `device_id = “rtl2838-0bda:2838″`

— and it is still a run where nothing was measured. Every populated setting

travels beside its own label:

“`text

sample_rate_hz             2400000

sample_rate_hz_exercise    CONFIGURED_NOT_EXERCISED

gain_db                    40.2

gain_db_exercise           CONFIGURED_NOT_EXERCISED

“`

A setting without its label, a label without its setting, and any other label

are each refused — and neither the reader nor the producer supplies the missing

claim, because labelling on the caller’s behalf would make the label unable to

be wrong.

The reader’s status function reads one field and looks at nothing else:

“`python

def declared_measurement_status(provenance):

    “””What the artefact says, never what the reader would guess.”””

    return provenance[“measurement_status”]

“`

Not `device_id`: an RTL2838 in the manifest is an instrument that was named,

and naming one is not using one. Not the settings: a populated rate and gain

are a configuration, and the whole rule is that a declared configuration must

not imply it was exercised.

The important design choice is what happens to that configured-idle artefact:

it is accepted, with its status preserved, not refused. Refusing it would

teach a producer to omit the configuration entirely, after which the reader

knows less about what was attached than it does now. That is the refusal that

makes the record worse.

## The Observer Cannot Say It Either

The artefact is only half the record. The observation record — the thing that

says what this run was — also has to carry the declaration, and here the

property is an absence rather than a feature.

The observer has:

– no constructor field for a measurement status

– no `run` parameter for one

– and, enforced by an AST test over the entire module, **no occurrence of

  `RF_MEASUREMENT_NOT_PERFORMED` or `INSTRUMENT_CONFIGURED_IDLE` as a name or

  inside any string**

Containment, not equality — a token spliced into a longer literal would reach a

record just as surely as one standing alone.

The only route is an `Artefact`, taken nominally:

“`python

InstrumentDeclaration.from_artefact(artefact)   # type(artefact) is Artefact

“`

The observer never met an instrument and neither did the producer. The only

party with a claim to make is the artefact, and the artefact makes it in

writing. So a run carries a declaration or says it has none:

| run | authority | status |

| — | — | — |

| derived artefact | `CARRIED_FROM_ARTEFACT` | the artefact’s own values |

| constructed | `NO_INSTRUMENT_DECLARATION` | `NO_INSTRUMENT_DECLARATION` |

A constructed run has no instrument — not an idle one, not a configured

one, none. A constructed run handed a carried declaration, and a live run

handed one from nowhere, both publish no record at all: the mismatch is

refused before the first verdict rather than recorded beside the numbers it

would undermine.

## One Read, Or The Record Describes A Different File

Verdicts and the declaration must come from the same bytes. Reading the

artefact path once for the verdicts and again for the declaration would attest

an instrument belonging to whatever the second open found.

So the verdict source became a type rather than two arguments:

“`python

VerdictSource(declaration=…, verdicts=…)   # one artefact, one read

“`

A declaration passed *beside* an iterator is a declaration about whatever the

caller says. One test counts the opens; one negative control performs the

second one and fails that test.

The same rule surfaced again a directory down. The record reports whether the

promotion lineage changed during the observation, by digesting every published

generation before and after. An empty digest map turned out to have **three**

causes:

“`text

{}   the namespace does not exist

{}   it exists and holds no generation

{}   the listing failed

“`

A record publishing only the map flattens all three into one — and the

flattened version reads as the most reassuring of them: *a valid empty

generation was observed and did not change*. Nobody made that claim.

The fix is a single snapshot from a single directory listing:

“`python

LineageSnapshot(presence, digest)

#   NO_LINEAGE_NAMESPACE | LINEAGE_HOLDS_NO_GENERATION | LINEAGE_PRESENT

“`

Two listings can describe two filesystem instants, so presence answered by one

and digests taken from the other is a record about no single moment. And a

listing that fails for any reason other than a missing namespace raises

`LINEAGE_INSPECTION_REFUSED` and publishes nothing — *we could not look* is not

an answer to *what was there*.

An earlier draft of that record also carried a `lineage_present` boolean beside

the enum. It was removed. A serialized copy is a second answer that can

disagree with the one next to it, and a reader has no way to tell which one the

writer meant. The convenience survives as a derived property; the evidence does

not store it twice.

## Names Are Checked By Machine, And Rejected Rather Than Judged

SCYTHE keeps two disjoint verdict vocabularies: **merit** codes say something

about the subject, **executability** codes say whether a verdict could be

reached at all. They are named disjointly and counted separately, and a name

that reads like a member of the other set is a bug even when the code is

correct.

A test enforces this mechanically. It parses every non-test module in the tree

via AST — never raw text — and discovers the universe rather than trusting a

hand-maintained list:

“`text

discovered tokens          395

declared merit codes        25

declared executability      50

“`

The check is component-level containment in both directions, plus negation-pair

detection for `UN`/`NON`/`NOT` prefixes. Every hit is either a real collision or

a recorded judgement with a reason. There is no third state.

The interesting part is the names it has killed. In the last three slices alone:

| candidate | collided with | outcome |

| — | — | — |

| `DECLARED_NOT_EXERCISED` | `LEDGER_GENERATION_UNDECLARED` | renamed `CONFIGURED_NOT_EXERCISED` |

| `ARTEFACT_DECLARED` | `LEDGER_GENERATION_UNDECLARED` | renamed `CARRIED_FROM_ARTEFACT` |

| `ARTEFACT_ATTESTED` | `LOCK_EXCLUSION_UNATTESTED` | rejected |

| `LINEAGE_ABSENT` | `ABSENT` (a coordinate kind) | renamed `NO_LINEAGE_NAMESPACE` |

| `LINEAGE_INSPECTION_FAILED` | `FAILED` | renamed `LINEAGE_INSPECTION_REFUSED` |

Each rejection is itself a test. `DECLARED_NOT_EXERCISED` would have collided,

and a test asserts it — so the reason survives after the prose explaining it has

been forgotten.

## Every Property Has A Mutation That Breaks It

A passing test suite proves that code runs. It does not prove the tests are

watching anything. SCYTHE’s answer is a negative control per property: a

deliberate mutation that must make exactly that property’s tests fail and no

others.

The last four slices added 53, on a set that numbers 132 at the time of

writing. A representative sample:

“`text

status inferred from populated sample rate and gain    -> 4/4 broken

status inferred from device identity                   -> 4/4 broken

the observer writes the status into the record itself  -> 1/1 broken

a measurement value spliced into the observer’s source -> 1/1 broken

the artefact opened twice, declaration and verdicts apart -> 1/1 broken

presence inferred from an empty digest map             -> 2/2 broken

an unreadable namespace reported as an empty lineage   -> 2/2 broken

a second answer serialized beside the enum             -> 2/2 broken

“`

Each control also runs every other test in its slice, to catch a mutation

that breaks half the suite and therefore proves nothing about which property

the tests are watching.

Controls earn their keep by finding real weaknesses, and three did:

– the closed-set test read its constants at import and never saw the sets open;

– the AST test compared strings by equality, so a token spliced into a longer

  literal passed it;

– an undeclared-name control reached only the reader’s copy of a set the

  producer binds separately.

All three were test bugs, found by the mutation rather than by review.

## Merge Is Not Acceptance

One process lesson is worth recording because it cost three repairs.

The governing discipline here is proposal → acceptance → implementation, each

a separate commit, merged before any code depends on it. Three times, an

amendment was merged while the document still described itself as `PROPOSED`.

Each was repaired by a forward acceptance commit rather than a rewritten merge:

treating the conversation as acceptance while the document said otherwise would

leave a contract contradicting itself, which is worse than a visible two-step.

The tempting generalization — a merge instruction implies acceptance — was

drafted, reviewed, and thrown out. It deletes a decision the contract exists to

require. A proposal may be merged precisely to preserve it as a proposal

without authorizing what it describes. What landed instead is a gate:

> A proposed amendment requires an explicit acceptance decision and an

> acceptance commit before implementation or execution. Merge approval alone

> does not supply acceptance unless it expressly says that the amendment’s

> substance is accepted.

## Where It Stands

“`text

Promotion Execution Contract    3,596 lines, amendments A-M

Test suite                      1,493 tests, OK (skipped=1)

Negative controls               132, all discriminating

Slices landed                   3, 4, 6, 6b, 7, 8, 9, 10a-10g

“`

Counts as of merge `f6bdf69`. They move every slice; the contract line count

and the vocabulary figures move more slowly.

The directories for the bounded run are pinned, resolved against the host, and

created at mode `0700`:

“`text

lineage root   /home/spectrcyde/scythe-ledger/promotion             absent

derived        /home/spectrcyde/scythe-live-observation/derived     empty

records        /home/spectrcyde/scythe-live-observation/records     empty

“`

The namespace-refusal check was run against the real function, not by eye —

a detail worth stating, because the rule is wider than it looks. The lineage

root is a *filename prefix*, not a directory, so the forbidden namespace is its

parent. Placing the lineage root directly inside the observation tree refuses

both output directories, and that is the arrangement anyone tidying paths

would reach for first.

## The Last Blocker Is A Person

Everything mechanical is done. What remains is not mechanical.

The run needs twelve operator-declared fixes: real positions, observed

during a walk, read off a screen and typed in by a person, roughly fifteen

seconds apart, each stamped with the host’s `monotonic_ns` at the instant the

runner accepts it. That is the host’s *ingestion* time, not the receiver’s fix

time, and the artefact says so — the two differ by however long the

transcription took, and no elapsed time may be computed across a phone’s wall

clock and a host monotonic value.

The authority chain is recorded in three fields rather than one, because

collapsing them would lose the step where the authority actually degrades:

“`text

position source       PHONE_DISPLAYED_FIX

transfer              HAND_TRANSCRIPTION

accepted authority    OPERATOR_DECLARED

“`

The phone may hold a genuine GNSS fix. What reaches the artefact is what a

person read and retyped. **A fix read off a screen and typed in is a typed

fix**, and it is not elevated to device-attested GNSS merely because a phone

displayed it.

So the run waits. Not on a missing feature, and not on a device — on somebody

walking.

It is worth being exact about what the code enforces here, because the

temptation is to describe the intent and let a reader hear a guarantee.

**The cadence is attested, not enforced.** The runner requires an interactive

terminal, which rejects a pipe or a redirected file. It cannot prove a person

was not pasting a buffer they prepared earlier: a paste into a live terminal

arrives through the same descriptor a typed line does. So the fifteen-second

spacing is an operator’s attestation and a target, never a refusal — M.5

declares only the count and the duration as active bounds. What the artefact

carries is the *actual* monotonic interval between accepted fixes, which means a

reviewer can see a cadence that does not look like walking even though nothing

refused it at the time.

**A short run publishes nothing.** The runner will not interpolate a missing

fix, will not repeat one to make the count, and will not substitute constructed

evidence. What it does with an incomplete set is refuse: on the 240-second

deadline it raises `ENTRY_DEADLINE_REACHED` and no artefact is written at all.

An earlier draft of this post said such a run “ends on its bounds with what it

has” — that describes the amendment’s intent for a run and not the code’s

behaviour, which has no notion of a partial artefact. Eleven fixes produce

nothing.

The no-fallback prohibition is the one that would be tempting at the moment it

mattered, which is exactly why it is written down.

—

*The Promotion Execution Contract lives at `docs/PROMOTION_EXECUTION_CONTRACT.md`.

The vocabulary rules it conforms to are in `SCYTHE_VERDICT_VOCABULARIES.md`.

Nothing described here has promoted anything to GraphOps: promotion authority

remains a separate explicit act, and the ARMED path stays blocked until a

completed observation record is reviewed.*

Leave a Reply

Your email address will not be published. Required fields are marked *