HOMELAB-1728: LD4.7 — signals-from-production intake (collector arm, SLO as data, burn in Go) #599
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!599
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-1728-signals"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the builder half of HOMELAB-1728 (LD4.7 · signals-from-production intake), built to
.rpiv/artifacts/epic-ld4/design-LD4.7-signals.mdand the orchestrator adjudication on the ticket (SLO substitute accepted:probe_successavailability, target 0.99, burn computed in Go, zero monitoring writes; collector = in-dispatcher armsignalintake/; R-23 active/300s/go:in-process/consumer:architect;DigestHashadditive ledger field; no cross-project SEND read in v1).Companion PR: Homelab/prod#— (fixtures ns + config + the one DANGEROUS CNP rule).
What this is
The collector arm (R-23) that turns application telemetry into direction: once per window it measures the availability SLI per surface, computes the error-budget burn in Go, correlates signals per surface, and emits one cited direction item per group above P4 into 4.1's inbox. Signals propose; the architect drafts; the operator ratifies.
The three Verify lines, mechanized
sendrast-testproduces a digested signal with evidence link + priority proposalTestBurnScaleZeroFullOutage(120 failing scrapes → error ratio 1.0 → burn 100 at the 0.99 target) →TestAC1ScaleZeroBurnProducesADigestedCitedSignal(onedirection.Item,source: application-signal,P1, non-emptyPriorityCitation, body carriessignal_digest_hash=…+ the Grafana link, sink idempotent on a second emit) →TestArmPassEmitsOneCitedItem(the same end-to-end through the arm)scale deploy/sendrast-test-canary --replicas=0, one pass, read the item back,--replicas=1(prod PR ships the fixture), plusTestBurnReplayRealProductionBurn: the real 2026-10-02T01:29Z burn re-derived from retained samples (30 failing scrapes in the 120-scrape hour → ratio 0.25 → burn 25 → above the 14.4 rung)TestAC2SignalWithoutMeasuredEvidenceIsRefused: empty evidence →ErrUncitedSignal; a link with no observed number →ErrUncitedSignal(a link is an invitation to look, not a measurement); 4.1'sFixtureSink.Emitindependently refuses a hand-built priority with no citation; zero items landTestArmRecordsTheLogCoverageGap: a live pass whose Loki source has no application coverage writes a namedno-coveragerefusal row — the absence of evidence is loud in production tooCitesDigest(draftBody, ledger)— true when the draft quotes a hash aKindDigestrow holds, false for an unknown/empty hash, error on an unreadable ledger (cites_test.go); the hash is on the item body, in theld:signal-digestmarker payload and on the ledger rowsignal_digest_hash=<hash>after the intake sweep runs (awaits the routine executor R-11 is paused on — stated, not claimed)Supporting:
go test -race ./...+go vet ./...green; one-writer-per-class (WriterForis structural: this is the onlyapplication-signalwriter); no priority without a citation is a signature property (Proposetakes the citation as a parameter and refuses an unmeasured one); quiet windows write nothing; a re-planned window issues no query at all.Deviations from the brief (each deliberate, all reported to the ticket)
burn_rate 200is 100 at the adjudicated target. The brief's §7.2 figure assumed a 0.995 target; the orchestrator ruled 0.99, so a full outage burns1.0/0.01 = 100. Both rows are asserted inTestBurnScaleZeroFullOutageso the arithmetic is visible rather than remembered.ld:signal-digest, not 4.1'sld:direction-digest. A signal item's external id contains the window, and 4.1's guard 3 (FindDigestIssue) matches its marker plus the window string — a signal item carrying 4.1's marker would make the daily sweep believe the digest execution issue already existed and it would write nothing. The distinct marker is the difference between a working loop and a silently starving one (asserted inplane_test.go).tcp_connectblackbox module. Everyhttp_edge_*module asserts TLS and would fail against an in-cluster plain-HTTP Service, and adding an in-cluster HTTP module would be a write intomonitoring— the change the design forbids.probe_successand the burn arithmetic are identical, so the code path is the same one production uses. (Prod PR detail.)ownership/verdict.goextraction would refactor 4.5b's landedsuppression.Plan. The arm takes anOwnershipReaderinterface and degrades tounknown(logged once), which proposes a draft; the architect's own reads still stand between that and work. Wiring it later is not a rewrite. No 4.5b code was touched.LogCoverageships ON, and the anomaly leg claims nothing. No application logs are shipped to Loki anywhere inprod/; the adapter + the pure z-score planner ship, and the live surface is the named refusal. The enabling change (a log shipper) is a separate E-adj ticket.Not in this PR (stated, not hidden)
SENDread in v1 (adjudicated);direction.Ledgerinterface;intake-daily-sweepispausedondep:role-intake, dep:LD4.1-intake-arm, so signal items land in Plane but are not yet batch-digested —emits: [R-11]declares the edge and the registry's cycle check runs on it.Reviewer notes
signalintake/plan.gois sha256-pinned by R-23'sscriptRefandTestInProcessPinsMatchTheCodere-derives it — any change to the planner must re-pin.LD4.7's pure half: the signal schema, the gauge burn arithmetic over probe_success, the priority ladder, the planner's ErrUncitedSignal seam, the digest hash and CitesDigest. - burn.go owns avg_over_time SEMANTICS as code (a 0/1 gauge is a MEAN, not a rate()), half-open windows, ErrNoSamples for an empty window (a coverage gap must never read as a quiet window), and budget_consumed_ratio. - priority.go: Propose takes the citation as a PARAMETER and refuses one that is not measured, so there is no code path that yields a priority without evidence. Retiring surfaces are forced to P4 with the retire ticket as the citation; an edge-observed fault caps at P3. - plan.go reuses 4.1's WindowKey/Plan shape and refuses the whole window on an unmeasured group (batch-wide, the 4.1 ruling). - digest.go renders the item body with signal_digest_hash=..., cites.go decides whether a draft quotes a hash the ledger actually holds (Verify line 3). - direction.Entry gains ONE additive omitempty DigestHash field (the transition 4.1's KindDigest comment reserves for 4.7; the 4.8 Run.Target pattern). - Policy as data: lab-director/signals/{slo-policy,priority-ladder}.yaml (target 0.99, retire surfaces flagged with SEND-546, fixture SLO flagged).CI red → green, plus one path correction to the tree map in the description.
Run #1806 (
dispatcher-test) failed and the failure was real, not flake: the job mounts onlylab-director/dispatcherinto the golang container (-v $PWD/lab-director/dispatcher:/src), so../../signals/slo-policy.yamlresolved outside the mount and the policy tests could not read the shipped documents. I reproduced it locally by copying just the dispatcher tree into an empty directory — the same shape as the mount — which is the check localgo testcannot give you.Fix (commit
5d56312): the policy documents now live atlab-director/dispatcher/signals/{slo-policy,priority-ladder}.yaml(+ their README) — i.e.lab-director/signals/in the description becomeslab-director/dispatcher/signals/. Nothing about the design changes (policy as data, read by R-23, validated by the loader, pinned by tests); "adjacent to the dispatcher" now means inside the module, which is the only place the CI that must prove it parses can see it. A test that cannot run in CI is a test that does not exist.signalintake's own default policy paths are also aligned with the deployment's flat ConfigMap mount (/etc/lab-director/*.yaml), so the package defaults and theconfigpackage defaults are the same two paths.Run #1809 at head
5d56312b2d: dispatcher-test success, all four jobs green.Landing review — VERDICT: PASS (2 blockers found, both fixed; head reviewed
57e3e418f7)Fresh-context review of the diff + ticket HOMELAB-1728 +
design-LD4.7-signals.md. CI green on the reviewed head: run #1818, 4/4 jobs (alert-receiver-test,dispatcher-test,helm-validate,receiver-test).go vet+go test -race -count=1 ./...green locally (31 packages).Verified as claimed (primary sources)
Planner.PlanWindowrefuses per group before any write, and the refusal is batch-wide: empty evidence, a link with no observed number, and zero samples all returnErrUncitedSignal, so a refused window emits nothing.Proposetakes the citation as a parameter (priority.go:229) andItemForrefuses an empty rendered citation; 4.1'sItem.Validate/sink refuses the illegal pair a second time. Asserted byTestAC2SignalWithoutMeasuredEvidenceIsRefused(incl.direction.ErrInvalidItemfrom the sink,sink.Len()==0). ZERO Plane writes on refusal: confirmed.TestBurnScaleZeroFullOutage: 120/120 failing → ratio 1.0 → burn 100 at the adjudicated 0.99, with the brief's 0.995 row (200) asserted beside it.TestBurnReplayRealProductionBurn: the real 2026-10-02T01:29Z series (30/120 → 0.25 → 25) crosses the 14.4 rung; I re-derived the rung ordering by hand — the replay's ~2.03d series keepsslowestat ~0.51 (<1.0), so the fast rung is genuinely what fires, and the scale-0 fixture's 3d window makesslowestfire first (P1 either way, per the ladder). Half-open windows andErrNoSampleson an empty window are asserted.CitesDigestWhichidentity; digest hash order-independent and measurement-sensitive.sha256sum signalintake/plan.go=49da6976…— matches the registryscriptRefexactly, andplan.gois untouched by the fix commit. Entry is active / 300s /go:in-process/consumer: architect/emits: [R-11], and the reserved-ids header is reconciled in the same commit.Entry.DigestHashis additive +omitempty(the LD4.8Run.Targetpattern): no other entry kind sets it.ld:signal-digestdeviation is correct. 4.1'sFindDigestIssue(intake/plane.go:374) matches its marker plus the window string, and a signal item's external id is<window>#<group_id>— sharing the marker would make the daily sweep believe the digest issue existed and write nothing. The deviation is right; see B2 for why it was not yet effective.buildRunners⇒go func,fatal:false), the pass is pure-function dominated with bounded HTTP, andsignals:/arms.signalsare additive with default OFF. No test contacts a real service.BLOCKERS found in review — fixed at
57e3e418f7(fix worker, fresh context; delta re-verified by me)Both in the emission path, both reproduced with scratch tests (since deleted):
SweepWindowindexedplan.Groups[i]againstplan.Items[i], butplan.Groupsholds every planned group (recorded included) whileplan.Itemsholds only those above P4 — so a recorded group sorting first (the shipped policy forces its tworetiringsurfaces to P4) made the emitted Plane item's title name another surface. Fixed by a group-id→Grouplookup offitem.ExternalID, failing loudly on a miss, deliberately without touching the sha256-pinnedplan.go.PlaneSink.EmitTitledsentitem.Body(human HTML), soRenderItemDescription— the function that prepends 4.1'sld:directionmarker and appendsld:signal-digest— had no production caller. Consequence:intake.ParseDirectionItemcould not read what the sink wrote, soExists/FindSignalDigestnever matched (read-before-write dedup and guard 3 inert → a retry or restart created a duplicate Plane item), and R-11 (the declaredemitstarget) could not read the items at all. Fixed via anEmitDescribedseam;item.Bodystays the body inside the payload. The AC tests passed only becausedirection.FixtureSinkdedups in memory — the new tests drive the realPlaneSinkoverhttptestand assert the description parses withintake.ParseDirectionItem, carriesMarkerSignalDigest, and sends 0 further POSTs on re-emit.Concerns (do not block; recommend follow-up)
app-endpoint, soedgePathCapis inert for the real surfaces. The ticket's three Verify lines are met, so this is a scope reduction that needs a stated deviation + a follow-up ticket, not a silent one.ProposereadsLongBurn);ShortBurn/ShortRatio/Band.Thresholdare measured but dead, soburn.go's "both windows must agree" prose is not what the code does.now.Add(-Bands[0].Long)), even when theslowestrung fired — a P1 from the 3d rung can carry a citation whose numbers do not contain the burn that fired.fixture: trueis validated but never rendered, so a fixture burn would be presented as an objective (the yaml comment claims otherwise).PlaneSink.Bindhas no caller (sink HTTP usescontext.Background(), so no pass cancellation);warnOnce/Warnedonly set/read a bool and are never logged (the lost read-before-write guard is silent);ObservationSpanis the fetched-series span capped at the ladder lookback rather than the SLI's history;CitesDigest→LedgerDigestHashesinheritsList()'s 500-row oldest-first cap (a false negative once the process ledger exceeds 500 rows — the deterministic leg of Verify line 3, currently only called from tests).httptest.NewServerbinds localhost); the README states it accurately.Merging on green CI with no open blocker findings.
57e3e418f7b8a7234d24