Skip to content

ADR 0011: fleet operations and measurement as sibling repositories - #1

Open
thierrysays wants to merge 7 commits into
mainfrom
claude/fleet-ops-measurement-harness-atqrlp
Open

ADR 0011: fleet operations and measurement as sibling repositories#1
thierrysays wants to merge 7 commits into
mainfrom
claude/fleet-ops-measurement-harness-atqrlp

Conversation

@thierrysays

@thierrysays thierrysays commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

Spin off fleet operations (OTA, rollback, SBOM, reproducible builds, orchestration) and measurement harness (power, latency, thermal throttling) into separate hardware-agnostic repositories. This clarifies the governance claim of this repository by removing cross-cutting concerns that do not belong in a refusal-with-evidence system.

Changes

  • ADR 0011 (docs/adr/0011-cross-cutting-work-lives-in-sibling-repositories.md): Documents the decision to separate fleet operations and measurement into fleet-ops-lab and measurement-harness repositories, with rationale and consequences.

  • Build logs (docs/BUILD_LOG.en.md and docs/BUILD_LOG.fr.md): Record the spinoff on 2026-08-21, describing:

    • measurement-harness: instrument-agnostic power/latency/thermal measurement with strict provenance tracking (provenance.kind derived from instrument, never asserted; synthetic reports explicitly marked; integration not averaging; anti-noise rules for throttle detection)
    • fleet-ops-lab: A/B slots with automatic rollback, digest-bound manifests, SBOM diffing, reproducible-build checking, waved rollouts with halt rule (silence = rollback; fleet halts itself on failure budget exceeded)
    • Both repositories pushed to GitHub; temporary spinoff/ staging directory removed after transplant
  • CLAUDE.md: Updated project conventions to document:

    • Fleet operations and measurement live in sibling repositories (hardware-agnostic, no mutual dependencies)
    • Interface is file-based: measurement-harness/energy-model/v1 export replaces DeviceProfile.energy_model, with source string copied verbatim into energy_model_source
    • Neither repository may be added as a dependency
    • Updated porting milestone The standard of work, the four gaps it named, and the defect the fuzzer found #2 to reference measurement-harness as the producer and specify that its ina219.py porting note is the specification

Rationale

Three reasons for separation:

  1. Governance claim clarity: This repository's claim is that every control either fires or does not, provably. A power meter and rollout controller prove nothing of the kind; folding them in dilutes an auditable claim.

  2. Hardware agnosticism mismatch: hal/devices.py names five specific boards (admission gate requirement). A measurement harness or update model that hard-codes five boards is a lab benchmark script, not an instrument. Agnostic tools should not be constrained by this repository's hardware specificity.

  3. Dependency budget: This package carries one runtime dependency as a governance property. A measurement harness speaking I²C, SCPI, and USB-HID cannot live under that rule.

Cost

Invariant 10 stands unchanged: every energy_model remains labelled estimate until measured. The replacement now has a named producer and format, with energy_model_source carrying the harness's source string verbatim (including digest and any throughput regression note). Manual file copying replaces mechanical coupling — a real risk of schema drift that is accepted because auditability of the governance claim is the whole product.

Repositories

Neither depends on this package, and this package depends on neither. The only interface is a file.

https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR

thierrysays and others added 7 commits August 21, 2026 14:56
Fleet operations and measurement kept being described as part of this
programme while plainly not being part of this repository. Both use the whole
rig rather than any one board, and both are hardware-agnostic in a way this
package deliberately is not — hal/devices.py names five specific boards
because the admission gate has to know what each can enforce.

ADR 0011 records the decision and its cost: three repositories to keep
coherent instead of one, and a real risk of schema drift across a boundary
that nothing mechanically couples.

measurement-harness — instrument-agnostic power, latency and thermal
measurement. The core knows open/read/close returning volts and amps and
nothing else. Built around one rule: a figure that was not measured never
leaves labelled as one. provenance.kind is derived from the instrument, not
asserted by the caller, and the energy-model export refuses a synthetic report
unless asked in as many words, at which point the source string says so
permanently. Energy is integrated over actual sample timestamps rather than
averaged; every figure carries an error bar from the instrument's own
declarations; the throttle verdict says throughput regressed and never claims
a cause without a temperature series. 61 tests, mypy --strict clean, 96%
coverage, no runtime dependencies. ina219.py is written from the datasheet,
has driven nothing, and refuses to return readings.

fleet-ops-lab — A/B slots with automatic rollback, digest-bound manifests,
SBOM diffing, reproducible-build checking, waved rollouts with a halt rule.
Two rules: silence is a rollback (activation is provisional, PENDING reverts
on the node's own timer), and the fleet halts itself (no override on the
budget). The transfer that succeeds and delivers the wrong bytes is a separate
failure code from the transfer that drops, because a did-the-download-work
check misses it. No cryptography ships; an absent verifier with a required
quorum raises rather than passing. 67 tests, mypy --strict clean, 96%
coverage, no runtime dependencies.

Nothing changes in src/. Invariant 10 stands: every energy_model here is still
labelled estimate. What changes is that the replacement now has a named
producer and a named format, and energy_model_source will carry the harness's
source string verbatim, digest and all.

Both trees are staged under spinoff/ because the GitHub App backing this
session cannot create repositories (403 Resource not accessible by
integration). Nothing there is imported, packaged, or reached by make qa,
which scopes ruff, mypy, bandit and pytest to src and tests.
spinoff/README.md carries the transplant commands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR
Both repositories now exist and carry the work:

  https://github.com/thierrysays/measurement-harness
  https://github.com/thierrysays/fleet-ops-lab

The trees were staged under spinoff/ only because the session could not create
repositories. Leaving them here would have quietly undone ADR 0011 — a
governance repository that also contains a benchmark suite and a deployment
tool is exactly the arrangement that ADR argues against — so the directory goes
in the same change that gives it somewhere to go.

The build log entries that described the staging are corrected to record where
the work actually landed, in both languages, and ADR 0011 now links the two
repositories by URL rather than by name alone.

Nothing in src/ was touched at any point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR
Seven points, now the standing default across this repository and both
siblings: technical documentation, functional documentation, a neophyte path, a
bare-metal run, a five-tier test harness, a QA gate, and a threat model whose
residual risks are each pinned by a test.

The same list is in measurement-harness/CLAUDE.md and fleet-ops-lab/CLAUDE.md,
where all seven are met.

Here, two are not, and the entry says so rather than leaving it to be
discovered. There is no bare-metal guide — nothing here has run on a board, and
writing it is part of milestone 1. The suite is one flat tier plus
test_adversarial.py, which is the pen-test tier under an older name; there is no
smoke tier and the security concerns are spread through the adversarial file
rather than separated.

Stating a gap is cheaper than finding it. Neither is fixed in this change,
because doing so would mean restructuring 113 tests in a change whose subject is
a paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR
Every repository carries glossolalie-advisory as a topic — the tag that makes
the family findable from one search — alongside its own descriptive topics
rather than instead of them. Checked against the account before writing it down:
it sits with fifteen to twenty descriptive topics on every substantive
repository, so it is the common tag and not a replacement for the rest.

A new repository is not finished until it has it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR
Running `make qa` leaves .coverage, and on a full run .mypy_cache and
.ruff_cache as well. None were ignored, so the gate a contributor is told to run
dirties the repository it is checking, and the first thing anyone does with an
untracked build artefact is commit it by accident.

Found by running the gate here to verify the test count for a handoff document,
which is the same way a contributor would find it.

The two sibling repositories already carry these entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR
Counted rather than remembered. The figure was accurate at v0.1.0 and the suite
has grown since; both places that quote it as current guidance were still
saying 113.

Changed:
  CLAUDE.md       make test  # 113 tests, ~0.5s  ->  132 tests, ~0.5s
  CONTRIBUTING.md make test  # 113 tests         ->  132 tests

The timing holds: 0.38 to 0.42 seconds across three runs.

Left alone, deliberately: docs/BUILD_LOG.en.md and .fr.md record 113 as the
state at the end of day one, docs/adr/0010 cites it as the count at v0.1.0, and
docs/AUDIT-2026-08-21.md records 113 passing on the day of the audit. All three
were true when written. Editing a dated record to match today makes it a worse
record, and the build log is the one place in this repository where the reasons
behind a choice are still legible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR
The count next to `make test` said 113 while the suite was 132, and nothing in
the gate noticed, because nothing in the gate reads the documentation. This adds
the step that does.

`scripts/check_documented_counts.py` takes the count from pytest's own
collection rather than by parsing its output, so a future pytest release that
rewords its summary line does not break the check. It compares that against
every `# N tests` in CLAUDE.md and CONTRIBUTING.md, prints file:line for each
mismatch, and exits non-zero.

Two failure paths, both exercised before committing:

  drift    CLAUDE.md:11: documented 131 tests, the suite collects 132
  deletion no documented test count found in CLAUDE.md, CONTRIBUTING.md

The second matters more than it looks. A check that guards a number passes
vacuously once somebody deletes the number, so an empty result is a failure
rather than a pass.

Scope is deliberately narrow: the files that state a count as current guidance.
The dated records are left alone. docs/BUILD_LOG.* records the state at the end
of day one, docs/adr/0010 cites the count at v0.1.0, and docs/AUDIT-2026-08-21
records what passed on the day of the audit. A gate that rewrites history to
match today produces a worse record than no gate.

Wired into `make qa` and into the CI qa job, which runs its steps explicitly
rather than through make. Also brought scripts/ inside the tooling it belongs
to: ruff now lints it and mypy --strict now checks it, since a gate whose own
code is unchecked is making an exception for itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBPJHknRGLJzEbEvLQLbaR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant