Skip to content

Connector discipline complete, the watermelon detector, and the wrong-test detector — every affirmative earned, zero known gaps - #152

Draft
DanFashauer wants to merge 104 commits into
SignalGrid_Alphafrom
claude/signalgrid-launch-plan-emxm01
Draft

Connector discipline complete, the watermelon detector, and the wrong-test detector — every affirmative earned, zero known gaps#152
DanFashauer wants to merge 104 commits into
SignalGrid_Alphafrom
claude/signalgrid-launch-plan-emxm01

Conversation

@DanFashauer

@DanFashauer DanFashauer commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

One hundred four commits, one theme found repeatedly as the PR grew: the unearned affirmative — a green state reported without the thing it claims having been established. compliant: true echoed from a request; status: "sent" with nothing transmitted; "acknowledged within target" with no target; a ticket closed "resolved" with the concern still present; a device "correctly bound" to a policy running in report-only mode; a device "aligned" to a benchmark written for a different operating system; a "confirmed" assessment that stayed confirmed forever; a worker operating a controlled workflow while the labor plane says they are not on the clock; a Wi-Fi room fix silently treated as bed-level certainty; a session started by one radio blip or revoked by one missing observation; a device released to its first user with required day-zero payloads missing; a temporary enrollment pass quietly answering for standing credentials; a crash-looping host app graded identical to a healthy one; a cloned prox read and a cryptographic credential arriving as the same "granted"; a badge-in from hours ago graded like a current one; a step_up issued to a device with no way to answer it; a shared-account session no person could be attributed to; a "protected" device egressing raw with its edge client disabled; a mover's stale pre-transfer entitlements reading as generic drift. Each one is now a state the fabric can see, with the grant withdrawn until it is earned.

Eighteen concerns, in the order they landed:

  1. Connector discipline (commits 1–7): nac/uem actuator removal, the BYOD ownership axis, entitlement-binding, provisioning-order, plus the fix commit for nine defects an adversarial review reproduced in this PR's own first four commits — including a fixture lookup that granted for a fixture that did not exist.
  2. network-nac actually evaluates the segment instead of naming a verdict after it.
  3. response-accountability — the watermelon detector: a concern closed "resolved" while underlyingConcernStillPresent === true outranks every green process metric on the record; plus resolution timing (SLA/time-to-restore/backlog aging), the null-target fix (acknowledged_ungraded), the user-confirmation rule (a user saying "it's fixed" is not a signal re-check), and a fold-seed fix found by hunting the defect class inside the detector itself.
  4. Tenant-scoped connector config stores (nac/uem): the in-memory map was a singleton and the Redis key was global; both now take a required leading tenantId.
  5. policy-binding enforcement axis: a device correctly bound to a Conditional Access policy in report-only mode, a notify-only compliance policy, or ASR audit mode is governed on paper and gated by nothing — report_only → monitor (never a grant), disabled → restrict, absent → step_up. Enumerations 648 normalized / 1,440 raw, exactly 3 / 1 grants.
  6. benchmark-selection — the wrong-test detector, plus its /v1 arm and the recency axis. BaselineState records the ANSWER; this dimension grades the QUESTION: is the cited benchmark a real, current row of the published CIS catalog (454-entry committed snapshot, title-keyed — a family-keyed index hides 3 of the 7 superseded rows); did the content come from CIS's own publication, a named third-party implementation, or a bare "CIS" label; does the document target the platform the tool actually read; do the rule counts reconcile to their own total; is it the benchmark this workflow's persona/use case requires; and is the run still inside the operator's stated age bound (assessor-reported assessment_time + requirement maxAssessmentAgeDays + caller-supplied reference instant — no clock in any decision path). The /v1 arm adds DecisionEvidence.benchmarkSelection and an active v1 rule matching only the affirmative bad state (misfit → step_up) — unverified is deliberately excluded so a fleet not yet emitting the signal is not stepped up on day one; the v2 STRICT draft widens to it for tenants that opt in.
  7. Emitter discipline — the five outbound families of that commit (itsm, siem, syslog, telemetry, webhooks), the repository's longest-standing KNOWN_GAPS, each now behind the same unanimous fail-closed gate as every read connector: dev/alpha never emit; live requires the flag + a per-family credential + an INJECTED transport this repo does not ship. The fixture emitter records with a literal delivered: false — after syslog's sent lie, that claim is unrepresentable. The gap list has been empty since this landed (the gate read 39 of 39 then; it reads 43 of 43 now that shift-context, bootstrap-credential, challenge-capability, and sse-egress exist). A sixth outbound family joined later (concern 13); all gates are registered with the mutation guard, 0 exemptions.
  8. shift-context — the labor plane, the fabric's 40th family, with its /v1 arm and demo scenario. Custody knows which badge holds the device, access-governance knows the account is alive, the physical access control system knows its own door hours — nothing could say "this worker is not scheduled and not on the clock." Schedule standing is DERIVED from the WFM's reported shift window at a caller-supplied instant; the punch enum is the one trusted axis; the site question is graded only when the caller poses it. Scheduled-but-clocked-out and off-duty operation step up — deliberately never restrict, because an emergency call-in is legitimate. The /v1 arm mirrors benchmark-selection's (active v1 rule matches only misfit; v2 STRICT widens; fixture wfm-shift connector), and the offclock-medroom demo scenario shows the whole story: identity, posture, and custody all green, and the entry steps up on SHIFT_CONTEXT_MISFIT alone.
  9. Base reconciliation (five merge commits). First: SignalGrid_Alpha gained an independent Mac-lane implementation of nac discipline + webhooks gating — this branch's nac wins (deeper); base's ./nac subpath is kept; both webhook gates are kept as complementary layers, and base's 48/48 webhooks proof is adopted and wired into CI + preflight. Second: base gained a shared in-adapter emit gate (resolveEmission()) plus an mdm-profile proof — the gate is adopted, and syslog is composed rather than picked: a suppressing env returns the shared honest suppressed status like every other emitter, and past the gate — where the caller explicitly configured live delivery and a quiet status is at its most dangerous — the adapter still THROWS rather than reporting any status for an event that never left the process; both proofs re-pinned to that composed truth, and base's two new proofs registered in CI + preflight. Third: base gained the live lanesproof:live-fleet / proof:live-edr / proof:graph-wire against a REAL Fleet 4.89.2 and Wazuh (which found every host/policy route in the fleetdm adapter wrong; fixed and pinned), the pagination-truncation guard (adopted into preflight beside our drift gate), and published docs/STATUS.md; the telemetry surface merges both lanes (our emitter gate as the canonical entry + their FleetDM read types/config store), and the live proofs deliberately stay out of CI — they need a running Fleet/Wazuh and are documented live lanes. Fourth: conflict-free, but instructive — CI's ci↔preflight drift gate runs against the PR merge ref, so base's proof:absent-collection (registered there in preflight only) turned this PR red from the outside; the proof is now registered in CI on this branch, so the drift gate sees the merged truth. Fifth: base gained the read-error-swallowing guard (the third face of the absent-collection law: a lookup that FAILED must not report "nothing found") plus decision-port and preflight-CI parity ratchets — the guard's exemption list was written against base's tree, so on the merge ref it flagged this branch's nac rework; the exemptions are reconciled (the stale nac/store.ts entry removed — the method moved behind the gate — and resolveFromNAC given the twin onFault exemption of resolveFromUEM).
  10. Facility Trust Graph, phases 1–4 complete + zone-presence transitions + setup completion (@workspace/facility-trust-graph + @workspace/flows, from the owner's spatial-trust design — intake rows 16–18). The canonical, versioned space hierarchy whose spaceId is permanent identity and whose Cisco/access-control/EHR/RTLS identifiers are ATTACHMENTS, never keys — the proof migrates every Cisco id and shows every policy target intact. The location-certainty dimension: accuracy_class as an ordered ladder graded against a caller-supplied required floor per workflow, with the multi-bed rule mechanical — a Wi-Fi room_candidate against a bed_confirmed requirement steps up ("scan the wristband"); "open every patient in the room" is unrepresentable; wrong-map → restrict, unmapped → alert, stale/degraded/unavailable step up. Fused as the 31st composable kind. Phase 2: doors as PORTALS + correlateCrossing() — a badge event is evidence, never proof (corroborated / contradicted → alert on passback-tailgate-clone / unassessed — clock skew lands honestly). Phase 3: clinical bed context — ADT/FHIR assignment resolution (administrative truth that never claims presence; hierarchy-contradicting records are incoherent, never "probably the bed"), source-capability ceilings (Wi-Fi vouches for room_candidate at best; a claim above the ceiling gets NO partial credit), and the wristband-scan step-up path (a valid fresh ceremony attestation — method and instant only, no patient identifier crosses — proceeds without ever upgrading the accuracy class and satisfies only step-up-class concerns). Row 17 added the zone-presence state machine: presence EARNED by dwell (one blip never starts a session), exit CONFIRMED only by an affirmative observation outside a containment boundary past grace (hysteresis via the graph; sensor silence expires presence but never manufactures "they left"). Phase 4: the Site Context Gateway core — the minimization projector (the sensitive join stays local; upstream carries only outcome, reason codes, coarse zone via graph coarsening to the operator's kind ceiling, pseudonym, tier, health, latency, and the local audit-chain HEAD as tamper anchor; it REFUSES unrecognized fields rather than stripping them) and the restricted-mode grader (a required source unavailable, unrecognized, or absent — absence is not health — withdraws location-derived privileges; an unreadable health report is itself restricted). Row 18 (Jamf Setup Manager) added setup completion: a device released to its first user with required day-zero steps outstanding is released_incomplete (step_up), zero setup evidence at release is setup_bypassed (alert), and a simulated plan never grades a hollow green. The two candidates this row positioned — CAEP/Shared Signals and TAP-style bootstrap credentials — are now BUILT (concerns 11 and 13).
  11. bootstrap-credential — the 41st family, closing row 17's queued TAP candidate. A Temporary Access Pass–style enrollment credential is graded on four axes: scope (a bootstrap pass used in an operational workflow → restrict, not step_up — a step-up would let the suspect credential answer for itself), one-time standing, lifetime (DERIVED from issued/expires against a caller reference; no expiry is a visible unbounded, never silently fine), and issuance verification (broad-mint and location-only issuance are alerts — issuance defects are operator-scale, not per-session). An unposed workflow under a bootstrap credential fails closed, and the backstop carries its own BOOTSTRAP_UNGRADED reason so a weakened branch surfaces as ungraded instead of impersonating a verdict. The one deliberate asymmetry: a perfect bootstrap pass still reads monitor — only a standing credential can produce credential-context confirmation. Enumerations 1,944 normalized / 864 raw; 324 / 81 grants (all standing-credential states). Fused as the 32nd composable kind; incidents route to the Identity & Access queue, never the Service Desk.
  12. app-update stability axis (intake row 19, from Omnissa Intelligence's app-health telemetry): being current and managed is not the same as working. The connector now reads source-reported crash_count over a source-stated stability_window_hours (validated evidence, never reinterpreted); the caller poses maxCrashesInWindow and only then is stability graded — over the bound → step_up APP_UNSTABLE (the remedy is a challenge and a device swap, never a block); a count without its window is uninterpretable → unknown raises; an unposed question is unassessed and never forecloses. The grant now requires current + managed + clean + stable-when-posed.
  13. caep-events — the sixth outbound family, closing row 17's positioned CAEP candidate. One verdict change maps to one UNSIGNED SET claims set (RFC 8417 shape) over the five OpenID CAEP event URIs as an allowlist (session-revoked, token-claims-change, credential-change, assurance-level-change, device-compliance-change); reason codes travel as reason_admin; iat/event_timestamp/jti are all caller-supplied (no clock, no randomness — the jti is a decision id the fabric already minted). The subject is an opaque pseudonym with the same '@' tripwire as the gateway projector, so the easiest cross-system identity leak is unrepresentable. This repo holds no signing keys, so the proof pins that no string leaf of the claims set ever has JWT shape — nothing produced here can be mistaken for a signed token; signing is the injected transport's job in a private deployment. Same unanimous four-clause gate as the other five families; the fixture emitter records delivered: false.
  14. pacs-access, three new axes (intake rows 21 + 26). The credential-technology floor: a cloned 125 kHz prox read and a PKOC/Aliro cryptographic credential are different facts, and the caller may pose a per-workflow technology floor — below it → step_up CREDENTIAL_BELOW_FLOOR (a stronger challenge, deliberately never restrict/deny: "modernization should be evolutionary" is the essay's own law, and the legacy estate is graded, never condemned); unposed forecloses nothing, and posing the static floor is itself a recorded operator choice. The recency axis (from the owner's PACS API catalog's prescribed minimum signal set): a badge-in older than a caller-posed age bound is stale_evidence → step_up — a confirmed entry is not evidence of a CURRENT one forever; a future-dated event is a contradiction, never fresh. And reader/controller health, distinct from bridge reachability: the bridge can answer perfectly about a door whose controller is offline, so explicit offline steps up and degraded is a visible monitor — affirmative-only, so pre-axis bridges keep their behavior. The grant-safety enumeration widened to 97,200 combinations, graded in both poses (unposed: the new axes foreclose nothing; posed: the allow path shrinks to exactly the cryptographic third), 0 mismatches both ways.
  15. challenge-capability — the 42nd family (intake row 23, from the DigitalPersona Web Client API): the fabric's remedy doctrine chooses "step_up, never block" everywhere, and every such verdict silently assumed the challenge could be ANSWERED — a step-up posed to a device with no enrolled method, no authenticator, or a dead local agent is a deny wearing a step_up label. A bridge reports per-method standing (enrolled / authenticator present / client healthy over a six-method allowlist); the caller poses which methods its workflow's step-up would accept. READY is earned; UNANSWERABLE is itself an affirmative claim (every accepted method positively broken — silence never declares a dead end) and alerts at operator scale: fix enrollment or swap the device BEFORE the doomed challenge; blind spots monitor; unposed forecloses nothing. Deliberately never a step_up of its own — a dimension about challenge efficacy must not prescribe the challenge it just reported impossible. Fused as the 33rd composable kind.
  16. sso-session shared-account attribution (intake row 24, from DigitalPersona v4.4.0's multiple-device-bound-passkeys pattern): on a SHARED account (a nurse-station principal) the subject IS the account, so the subject comparison can never identify a person — a legitimate shared session either escalated as a false leftover on every tap or lost attribution entirely. Attribution now moves to the CREDENTIAL level: a holder-matching credential corroborates a first-class bound session; someone else's credential is mismatched (live → escalate — the leftover rule survives the pattern); and a live shared session with NO credential attribution is its own posture, unattributed_shared → step_up ("the account authenticated" is not "this person is identified"; re-auth as yourself, never a lockout). The credential comparison only ever downgrades, on every scope; enumeration widened to 4,608 combinations, 0 mismatches.
  17. sse-egress — the 43rd family (intake row 25): network-nac stops at LAN admission and edr-threat grades the endpoint agent; nothing asked whether the device's traffic actually traverses the deployment's mandated SWG/CASB/ZTNA edge. A "tunneled" claim is corroborated, never believed (the edge must AFFIRMATIVELY observe the device's traffic; a contradiction never grants); disabled and never-installed alert at operator scale; a bypass is visible and steps up; the mandate is caller-posed and unposed forecloses nothing. Fused as the 34th composable kind.
  18. access-governance lifecycle axis (intake row 27): a six-agent adversarial workflow audited the owner's canonical endpoint signal set fact-by-fact against the code — 43 signals covered with file+type evidence, two gaps confirmed. The J and M of JML are now built: lifecycleStage (affirmative-only) turns a recent transfer with over-privileged or recert-due entitlements into mover_stale_entitlement → ALERT with its own queue-readable reason (the pre-transfer-grants-never-revoked defect, no longer generic drift), and a new hire already holding standing privilege into joiner_over_provisioned → ALERT; a clean transition is a visible monitor — never a grant, never a nag — and a leaver still outranks everything. Enumeration widened to 18,000 combinations, 0 mismatches. The second confirmed gap — normalization-version stamping on evidence — is QUEUED as its own PR in docs/BUILD_BACKLOG.md with the audit's full evidence, because it touches the core decision/API contract rather than one family.

Also: the CI↔preflight proof list is derived instead of trusted; the phase-gate claim scan is negation-aware; docs/INTAKE_LEDGER.md durably tracks every product input the owner submits (twenty-seven rows this cycle, each with its disposition); two owner-compiled vendor catalogs are filed verbatim as durable adapter roadmaps (docs/inspiration/PACS_VENDOR_API_CATALOG.md — 61 physical-access platforms — and docs/inspiration/ENDPOINT_MANAGEMENT_API_CATALOG.md — 135 endpoint-management platforms — with no dependency taken on any listed vendor); /metrics gains an optional METRICS_TOKEN bearer gate and its no-tenant-label property is pinned by test; scripts/mac/run-everything.sh gives the Mac a one-command full local simulation (proofs → API → a real MCP stdio handshake → iOS simulator with mimicked badge hardware); scripts/mac/mcp-up.sh keeps the Claude Desktop MCP connection current with check-mcp-surface.mjs as the CI drift gate; docs/LANE_COORDINATION.md is the standing protocol for the parallel Claude lanes.

What changed

  • lib/integrations: six new read-only families (response-accountability, benchmark-selection, shift-context, bootstrap-credential, challenge-capability, sse-egress), the policy-binding enforcement axis, the uem ownership axis, the app-update stability axis, the pacs-access technology/recency/controller-health axes, the sso-session shared-account attribution axis, the access-governance lifecycle axis, tenant-scoped stores, nac fabrication fixes, syslog honesty, six emitter gates (resolve.ts per family + subpath exports, caep-events with its pure claims formatter), the shared in-adapter emit gate (adapters/emit-gate.ts), and the merged telemetry surface (emitter gate + FleetDM read types/config store, with the Mac lane's live-verified route fixes).
  • lib/facility-trust-graph (new package): the validated space model, the location-certainty dimension, door-crossing correlation, clinical bed context (assignment resolution, capability ceilings, the explicit-selection ceremony), zone-presence transitions (dwell/grace/hysteresis), and the Site Context Gateway core (minimization projector, restricted-mode grader, audit anchor) — pure and offline.
  • lib/flows: gradeSetupCompletion — the day-zero release grader (row 18).
  • lib/signalgrid-core: benchmarkSelection and shiftContext evidence fields (absent/unrecognized → unverified, never confirmed), active v1 misfit rules for both, v2 STRICT widenings, the fixture wfm-shift connector kind, and the offclock-medroom scenario. Signal Radar's compile-time exhaustiveness guard caught each category ripple (13 → 15) as designed. bootstrap_credential, challenge_capability, and sse_egress join posture composition as the 32nd–34th kinds and the incident playbook's routing.
  • artifacts/mcp-server: 7 tools including facility_graph and evaluate_location_certainty.
  • scripts: check-ci-preflight-sync.mjs, check-mcp-surface.mjs, the adopted check-pagination-truncation.mjs, unsafe-claim-classifier.ts, catalog generator, proofs for every change, the Mac turnkey runner + MCP launcher.
  • Committed CIS catalog snapshot: titles/versions/filing-buckets only — CIS rule content is licensed and is not reproduced; the loader mechanically refuses control-statement grammar, re-derives every declared count, refuses an empty or de-duplicated snapshot.

Validation

proof now
proof:signalgrid-core all invariants, incl. both /v1 arms' day-one-quiet + v1/v2 divergence pins
test:api 191/191 (incl. /metrics no-tenant-label + 15-category catalog pins)
proof:benchmark-selection 82/82 (38,400 + 1,920 enumerations; 10 / 1 grants)
proof:shift-context 50/50 (96 + 360; 2 / 1 grants)
proof:facility-trust-graph 120/120 (10,368-state sweep, 24 grants; crossing-window, ceiling-teeth, coherence-edge, never-upgraded, lie-not-steppable, dwell/grace/hysteresis, minimization-never-leaks, and restricted-never-loosens pins)
proof:bootstrap-credential 35/35 (1,944 + 864 enumerations; 324 / 81 grants, all standing)
proof:app-update 57/57 (incl. the row-19 stability pins)
proof:caep-events 17/17 (5 event kinds; the no-JWT-shape pin over every string leaf)
proof:pacs-access 92/92 (97,200-combination enumeration in BOTH poses; technology floor + recency + controller-health axes)
proof:challenge-capability 38/38 (81-cell standing sweep — answerable in exactly the all-affirmed cell)
proof:sso-session 88/88 (4,608 enumeration; the shared-account attribution axis)
proof:sse-egress 32/32 (45-cell dual-pose sweep — protected in exactly one cell, unposed always quiet)
proof:access-governance 65/65 (18,000 enumeration; the lifecycle axis)
proof:emitter-discipline / proof:webhooks 51/51 (six families), 48/48
proof:emit-gate / proof:mdm-profile 69/69, 18/18 (base's proofs, adopted + registered)
proof:policy-binding / proof:response-accountability 46/46, 82/82
proof:uem / proof:nac / proof:network-nac 50/50, 32/32, 37/37
proof:entitlement-binding / proof:provisioning-order 57/57, 42/42 (incl. the row-18 setup-completion pins)
mutation guard benchmark-selection 40/0, shift-context 22/0, facility-trust-graph 76 killed + 11 labeled-inert / 0 survivors, policy-binding 29/0, emitter gates 20/0, bootstrap-credential 0 survivors (6 labeled-inert), app-update axis 0 (1 labeled-inert), pacs-access 30/0, challenge-capability 23/0, sso-session 19/0, sse-egress 14/0, access-governance 18/0

Negative controls throughout are measured by exit status, never by counting FAIL lines. The most instructive finds: the benchmark-selection first draft collected rules_total and never read it; the core evidence default was unfalsifiable until the proof derived evidence through buildEvidence instead of literals; the shift-context and facility-trust-graph mutation sweeps each found unknown-axis branches silently masked by their own backstops — six real no-test gaps in the spatial dimension alone, all now ordering-pinned; the bootstrap-credential sweep found the unposed-workflow branch masked by a same-reason backstop, which is why the backstop now carries its own reason; the challenge-capability and sse-egress sweeps each found defensive-shell gaps (a null report reading as a quietly-empty clean read, a throwing getter defaulting silently, a hostile Proxy trusted) — all pinned. The Mac lane's live-Fleet proof is the same method against reality: every fixture-assumed route was wrong, and only a real server could say so.

Public-safety note

  • This PR uses public-safe content only and does not add secrets, tenant IDs, customer data, PHI/PII, live API calls, production-readiness claims, compliance/certification claims, partnership claims, replacement claims, or autonomous production-remediation claims.
  • Every family — readers and emitters alike — is fixture-backed behind tier + SIGNALGRID_LIVE_INTEGRATIONS + credential + an injected transport this repo does not ship. The live-lane proofs run only against self-hosted open-source servers an operator stands up themselves. The two filed vendor catalogs are reference inventories with no dependency taken on any listed vendor. The caep-events family emits unsigned claims sets only — no signing keys exist in this repository, and the proof pins that nothing it produces has signed-token shape.
  • No CIS certification, conformance, or partnership is claimed; SignalGrid performs no benchmark assessment; third-party CIS-aligned implementations are never represented as official CIS content.
  • shift-context reads a schedule and never manages one: GET-only, no punch writes, nothing payroll-adjacent. Patient-record behavior stays in HOST apps per the embedded-UX law — the phase-3 ceremony attestation carries a method and an instant, never a patient identifier; the phase-4 projector and the CAEP formatter both refuse an email-shaped identifier outright.

Remaining risks

  • The benchmark-selection requirement row, the shift-context site policy, the facility graph, and every caller-posed floor/bound are caller-authored: the fabric validates and grades them but cannot verify they are wise. The mitigation is that each is data an auditor can read.
  • Platforms with no published CIS benchmark sit at REQUIREMENT_ABSENT → step_up; workers outside the WFM sit at NOT_COVERED → step_up — truthful honest holes, named so muting them is a decision, not a drift.
  • The flows provisioning module predates the mutation guard's TARGETS and remains un-swept — the row-18 grader is pinned by its eight checks, not by mutation (noted in the ledger).
  • Normalization-version stamping on evidence is a CONFIRMED gap queued as its own PR in docs/BUILD_BACKLOG.md (intake row 27): only policyVersion is stamped today.

Screenshots / local QA if applicable

  • No UI surface. Local QA is the proof/gate output above, cloud lane only (validate-sim-macos.sh not run here — it rewrites package.json). On a Mac, ./scripts/mac/run-everything.sh runs the whole thing, iOS simulator included; the live Fleet/Wazuh lanes are documented in docs/ZERO_COST_LIVE_TEST_MATRIX.md.

Owner decision needed

  • None to merge. Kept as ONE PR deliberately: the concerns share the sync-manifest fingerprint and proof registries, so splitting would serialize dependent merges for review parallelism a solo owner does not use.
  • Still open, all owner infrastructure: merge timing; the tier-branch strategy (three options in docs/BRANCHING_AND_ENVIRONMENTS.md); enabling live emission in a private deployment by injecting a real transport.

claude added 30 commits July 28, 2026 20:16
The fabric graded every phishing-resistant credential as one thing. sso-session's
`SessionAssurance` collapses them into `phishing_resistant`, annotated "(e.g.
passkey / FIDO2 / platform)" — the "a passkey is a passkey" misconception encoded
as a type. On a shared, badge-checked-out device it could not tell a synced iCloud
passkey on someone's personal phone from a FIDO2 key in a nurse's badge holder,
which is the entire question that fabric exists to answer.

Two things Microsoft's own Entra documentation makes precise, and the tiering
folklore gets slightly wrong:

  - The boundary is ATTESTATION, not synced-vs-device-bound. "Unattested passkeys,
    including synced passkeys AND unattested device-bound passkeys, don't provide
    device provenance." So an unattested device-bound passkey grades with synced
    ones, not with security keys. Grading by credential type alone would have
    reproduced the misconception one tier over — granting to a device-bound
    credential whose provenance was never verified.
  - Synced custody is unknowable BY CONSTRUCTION: "administrators can't see or
    control exactly which devices hold a copy of a synced passkey, nor can they
    query where a synced passkey has been synchronized." Not a weak reading — no
    reading. Under the grant discipline that FORECLOSES rather than lowers, and the
    enumeration proves it: zero synced states grant across every combination of
    every other axis.

User verification discouraged restricts rather than raises, because the credential
is KNOWN to be exercisable on possession alone — single-factor behind a
phishing-resistant label. A profile claiming attestation enforced over a synced
credential alerts: the platform is not applying the claimed control.

Two defects the tooling caught in this change, both worth recording:

  - The raw grant-safety predicate was written against normalized field names while
    the harness passes the WIRE combo, so it read false everywhere. That direction
    of error is invisible — the sweep would have passed while proving nothing — so
    the proof now also asserts the raw sweep is non-vacuous.
  - The backstop pushed a real branch's reason code, which made deleting that branch
    undetectable (the mutation guard flagged it). It now pushes its own
    GRANT_BACKSTOP code, which is better at runtime too: a backstop firing means a
    branch that should have spoken did not, and an auditor needs to see that.

51 proof checks, both enumerations (2430 normalized / 3240 hostile raw), mutation
sweep 32 mutations / 27 killed / 0 survivors with 5 backstop terms documented as
inert. Registered with both guards from day one, wired into preflight and CI.
sso-session is deliberately NOT widened — its coarser question is a real one — but
the flattening is now documented at the type rather than left implicit.
…th evidence

**P1 — an identity could grant while holding a weak credential. Confirmed, fixed.**
The per-credential verdict recorded only THAT a backup existed, discarding its type,
attestation and user-verification posture. An attested security key alongside a
synced backup therefore returned `none` — keyed by identityRef, so it reads as an
identity answer — while the synced credential remained a usable authentication path.
An attacker uses the weakest path on offer, so that was a fail-open, and it
contradicted this dimension's own headline claim that no synced state grants.

evaluateIdentityPasskeys grades every registered credential and returns worst-wins
plus weakestCredentialRef, so there is something concrete to go fix. An empty
credential set is NOT a grant — absence of evidence is not confirmation. Every
verdict now carries credentialRef, so a per-credential answer cannot be mistaken for
an identity-wide one by a reader who only sees the payload.

**P2 — user verification was graded at the wrong ceremony. Confirmed, fixed.**
WebAuthn's registration-time userVerification is a PREFERENCE; the authentication
ceremony independently decides whether to require UV and whether to reject an
assertion lacking the flag — this repo's own relying party requires it at
authentication. Grading the registration preference would have restricted a
credential that is always authenticated with UV required. The wire field is now
`user_verification_policy`, documented as the ENFORCED authentication policy, and an
integrator holding only the registration preference must report `unknown` (step_up)
rather than assert a fact never established.

**P1 — "remove the default fetch transport". Declined, with evidence.**
The finding reads the gated transport as a live vendor call. It is the repo-wide
convention: 43 connectors ship the identical shape, and every one defaults to
fixture mode, requiring SIGNALGRID_TIER=beta|prod AND SIGNALGRID_LIVE_INTEGRATIONS=true
AND an explicit token before any request is possible. AGENTS.md's own implementation
guardrail allows exactly this — "read-only and fixture-backed unless a task
explicitly says otherwise and provides a safe private-test context" — and the gate IS
that context. Singling out the newest connector for a pattern shared by 43 would make
the tree inconsistent without closing anything. Raised for the owner rather than
changed unilaterally.

59 proof checks (was 51), mutation sweep 34/29 killed/0 survivors, preflight green.
…tity sets

All three findings were right, and two of them were holes in the fix I shipped an
hour ago rather than in the original design.

**P1 — a grant with no credential reference.** I added `credentialRef` precisely so
a verdict could be bound to the credential it graded, then let a report with the
field absent reach `none` anyway — and the proof's own GRANT fixture omitted it,
pinning the wrong behaviour. A verdict that cannot name its subject cannot support
the aggregator's claim that every usable credential was covered, which is the entire
reason the field exists. An empty or whitespace-only ref now raises
CREDENTIAL_REF_MISSING and is part of the positive-confirmation predicate.

**P1 — mixed identities in the aggregate.** evaluateIdentityPasskeys took
`reports[0].identityRef` and graded whatever it was handed. An upstream batching or
grouping error could therefore return identityConfirmed for the first identity over
a set containing someone ELSE's credentials — an unsafe allow produced by a caller's
bug rather than by any credential being weak. It now requires every report to carry
the same non-empty identityRef and fails closed with IDENTITY_SET_INCONSISTENT.

**P2 — the docs promised a ladder the evaluator does not enforce.** The table said
"any axis unknown → step_up", but an unreadable BACKUP is monitor, and the proof
pins that. The row now names the trust axes explicitly and gives the recovery axis
its own row, because an unreadable backup is a recovery gap rather than a trust one.
An integrator reading the table gets what the code does.

63 proof checks (was 59), mutation sweep 37/31 killed/0 survivors, preflight green.
Confirmed by reproduction before changing anything:

  single report claiming backup:registered → none | identityConfirmed: true
  duplicate refs                           → none | identityConfirmed: true

Worst-wins is only sound over EVERY usable credential. The aggregator checked that
each supplied credential granted, and silently assumed the caller had supplied them
all — which nothing established, and which the connector structurally cannot do
because it reads one credential per call. So a lone report that ITSELF asserted a
second authentication path exists still confirmed the identity, and the same
credential passed in twice looked like breadth while covering one credential.

evaluateIdentityPasskeys now fails closed on three signals of incompleteness:
duplicate credential refs; a report claiming `backup: "registered"` while the set
holds fewer than two distinct credentials (the set contradicts its own contents);
and a mismatch against an authoritative `expectedCredentialCount` when the IdP can
supply one. Completeness is a check, not a veto — a set that matches still confirms,
and that is pinned so the guard cannot become vacuously strict.

The transport is now genuinely per-credential rather than identity-keyed.
`fetchNormalizedSet(identityRef, refs)` reads each named credential, which makes
completeness the caller's visible responsibility instead of a silent assumption; the
mock transport gained a credentialReports map so a fixture can model an identity
holding several credentials of differing worth. An end-to-end check exercises that
path: an attested key plus a synced credential, fetched per-credential, does not
confirm and names the synced one as weakest.

This is the third round on the same underlying mistake, and worth naming: the
enumerations quantify over 2430 states of ONE report and say nothing about whether
the question is well-posed — what the verdict is scoped to, which ceremony a field
describes, whether the input set is whole. Every finding across these rounds lived
in that gap.

69 proof checks (was 63), mutation sweep 38/32 killed/0 survivors, preflight green.
All three let a grant through on something that was never established.

1. An empty or whitespace-only identityRef granted. A verdict that cannot name
   WHOSE credential it graded cannot support a grant on a shared device, where
   which human is holding it is the entire question. Now raises
   IDENTITY_REF_MISSING, same shape as the credentialRef guard beside it.

2. fetchNormalizedSet accepted a report whose credential_ref named a DIFFERENT
   credential than the one requested. A source answering a request for the weak
   credential with a healthy one would have been graded as if the weak credential
   had been read — and two such substitutions would satisfy an authoritative
   count while the real credentials were never seen. A mismatched ref is now
   malformed.

3. expectedCredentialCount was opt-in, so completeness was never positively
   established: any caller could confirm an identity by supplying a set and
   saying nothing about whether it was the whole set. It is now REQUIRED for
   identityConfirmed. Without it the aggregate still reports worst-wins over what
   it was given, but returns COMPLETENESS_UNPROVEN and refuses to confirm —
   "is this every credential?" is an unread axis like any other, and an unread
   axis raises.

proof:passkey-assurance 74/74 (was 69). The old "two sound credentials DO grant"
case now pins the no-count refusal; the same pair WITH a count still grants, so
this is a completeness rule rather than a blanket refusal. Mutation sweep: 42
mutations, 0 survivors — the new identityRef term in the positivelyConfirmed
backstop is registered inert with its reason, like the five terms beside it.
Preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
Grant-safety asks whether the input→verdict mapping is correct. The mutation
guard asks whether each condition in that mapping is load-bearing. Neither asks
whether the space being enumerated is the space that decides the question — and
that gap is invisible precisely because the enumeration is green.

passkey-assurance is the clean case, because the failure and the proof of its
absence sat side by side: the sweep covers a large single-credential state space
with zero mismatches, while the question the dimension answers is about an
IDENTITY, whose answer needs an axis no credential carries — whether the set is
the whole set. Three of the findings that produced its current shape lived
outside the enumerated space, and all three came from a reviewer reading the
contract rather than from the sweep.

Stated as a limit on what a green run buys: no unknown, missing or malformed
value reaches a grant, OVER THE AXES ENUMERATED. The trailing clause is the
whole of what review still has to do, and it does not shrink as the state count
grows.

Figures deliberately described rather than quoted — this section names the
guards, not proof:passkey-assurance, so a number here would sit outside the
figure guard's section scope and become exactly the fossil that guard exists to
prevent. The live counts stay in PASSKEY_ASSURANCE.md where they are checked.

guard:figures and check-doc-orphans both green.

Co-Authored-By: Claude <noreply@anthropic.com>
… diff

The first full sweep in a while came back with four survivors — guards their own
proofs cannot falsify. A survivor is either dead/inert code or real behaviour
with no test, and it looks IDENTICAL either way: the proof passed, and that is
all the guard observes. Deciding by reading the code is the same reasoning that
put the untested condition there to begin with, so each was classified by
behavioural diff instead — apply the mutation, dump the FULL output over the
connector's whole input space, compare.

  oauth-consent  `consent.grants === "none" &&`   0 of 6,480 verdicts changed
  dual-control   `!plain ||` (both normalizers)   0 of 239 hostile shapes
  dual-control   `readThrew ||`                   0 of 239 hostile shapes

All four are genuinely inert: when the input is not a plain object, or a getter
throws, every field reads undefined and the per-field checks already mark the
report malformed on that alone; and oauth-consent's risky-field block already
forecloses its disjunct for every grants value other than "none". All four are
KEPT as defence in depth — they state the rule directly instead of relying on a
downstream check to imply it — labelled inert in the source, and allowlisted
with reasons a reader can re-run rather than take on trust.

Also registers proof:pim-activation, the last QUEUED allow-path proof, and
removes its exclusion. It was excluded on the argument that it composes other
dimensions' verdicts rather than parsing a bridge report, but that describes WHAT
gets mutated, not whether the guards need to be falsifiable — and composition is
exactly where a grant leaks. Its sweep: 20 mutations, 20 killed, 0 survivors, so
the exclusion was overcautious rather than protective.

Registry now reports 20 allow-path proofs, 19 under the mutation guard, 0 QUEUED
— the first time that list has been empty. The one non-target is grant-safety
itself, which IS the harness. The QUEUED machinery stays in the output for the
next gap; a coverage report that only exists while coverage is partial is the
kind of thing that quietly does not come back.

Refreshes two SELF_REVIEW figures that had fossilized because they sit outside
the figure guard's section scope (it checks sections naming a PROOF; these name
the GUARDS): 105/95/7 -> 500 mutations, 460 killed, 40 documented-inert, 0
survivors; and "12 allow-path proofs, 3 under the guard, eight QUEUED" -> 20/19/0.
Both taken from real runs, not arithmetic.

guard:figures, check-doc-orphans and check-proof-counts all green.

Co-Authored-By: Claude <noreply@anthropic.com>
The lane was reset onto SignalGrid_Alpha after PR #139 squash-merged, so the
remote tip (e91f795, 101b6a8) is not an ancestor of the new base. Both of those
commits are contained in the squash-merge 2613e53 — nothing unmerged exists on
that side.

Recorded as an ours-strategy merge rather than a force-push: the resulting tree
is identical to the reset branch, the superseded commits stay reachable in the
history instead of being rewritten away, and the push is an ordinary
fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
…d a guard that disagreed with itself

An audit of every measurement-shaped claim in the docs found 11 stale ones. The
mechanism is worse than any individual number, and the worst finding is not a
number at all.

FALSE SAFETY GUARANTEE. preflight.mjs described itself as "a COMPLETE mirror of
the CI validation + docs-sanity jobs, so a green preflight genuinely means CI
will be green." Both halves were false: six jobs run on pull_request, and
preflight mirrors three. durable-persistence (Postgres), deploy-stack (Docker
compose smoke) and secret-scan (gitleaks) need external services and cannot run
in the harness. Both the source comment and SELF_REVIEW now say what is and is
not covered, and that a push can still go red after a clean preflight.

WRONG PRE-PUSH GATE. CLAUDE.md pinned "must be 66/66" against a suite of 87 (4
named simulator gates + 78 looped proof gates + 5 non-proof gates), and said
"~55 proof:* gates" against 82. The count is corrected; the PIN is removed
rather than refreshed. The harness already prints "== SUMMARY: N passed, M
failed ==", so the instruction is now to read M against 0 — a pinned total is
what turned a regression into a pass here, and a fresher pin only resets the
clock.

A GUARD THAT DISAGREED WITH ITSELF. check-guard-registries detected `figures=`
emissions with a regex anchored to `console.log(` on one line, so iac-proof.ts's
wrapped call read as "publishes no figures". It printed 16 emitters against 17
registrations and exited 0, because every check was one-directional
(detected ⊆ registered) and nothing asserted the reverse. Detector fixed and the
missing reverse assertion added. Negative-controlled: reverting the detector
makes the new assertion catch proof:iac and fail; restored, both totals agree at
17.

FOSSIL LEFT BEHIND BY ITS OWN FIX. a2dcc7a corrected "105 mutations, 95 killed,
7 documented-inert" in SELF_REVIEW.md and left an identical copy in
ZERO_COST_LIVE_TEST_MATRIX.md, which also said "39 deterministic proof scripts"
against 82. Both corrected.

VALIDATION_EVIDENCE is marked as the dated record it is rather than restated as
current. validate-sim-macos.sh runs `pnpm add -w`, which rewrites package.json
and the lockfile, so it was deliberately not run here; claiming "87/87 green"
would be asserting a result nobody observed. The section states the suite has
grown to 87 and gives the command to derive it.

WHY NOTHING CAUGHT ANY OF THIS. Two independent filters, and the second is the
one that matters. SCOPE: the figure guard reads only docs/*.md, and only inside a
##/### section containing a registered proof name — a section naming a GUARD, a
script or a suite matches nothing, and CLAUDE.md is never read. SHAPE: FIGURE_RE
matches only comma-formatted values >= 1,000, so every bare number is invisible
even inside a perfectly scoped section. Fixing scope alone would have caught
almost none of the 11.

The shape filter cannot simply be relaxed to "any number" — that sweeps in
versions, dates and illustrative examples — so it needs a design pass rather than
a one-line change. Until then the guard PRINTS both blind spots every run: 33 of
52 comma-figures sit outside any proof-named section, and ~121 bare
measurement-adjacent numbers exist that it cannot see. Same reason the QUEUED
list stayed in the registry check's output: partial coverage announced every run
is a very different thing from partial coverage that looks complete.

The honest summary, now stated in SELF_REVIEW: these guards catch drift in the
numbers they can see, and prose claims are not among them.

Full preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
PR #140 squash-merged as a2dcc7a, so the remote lane tip is not an ancestor of
the new base. Those commits are contained in a2dcc7a — nothing unmerged is on
that side. Recorded as an ours-strategy merge rather than a force-push: the tree
is identical to the reset branch and the push is an ordinary fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
The tail of the documentation audit. Every number below was re-derived by
running the command, not copied from the audit report — the audit's own findings
were treated as candidates, and one was overruled.

  proof:signalgrid-core assertions   166 -> 188   (8 live-tense places)
  SignalCategory union               12  -> 13    (3 places; the SAME file's
                                                   heading already said 13 and
                                                   listed them, so it contradicted
                                                   itself three times)
  Playwright E2E                     15  -> 35 tests, ~75 -> ~126 assertions,
                                     3   -> 5 surfaces, 4 -> 6 app builds
  E2E invocation                     `pnpm run e2e` -> `pnpm run test:e2e`

Derivations: `pnpm run proof:signalgrid-core | grep -i assert` prints
188/188. The SignalCategory union parses to 13 members. `playwright --list`
reports 35 tests in 8 files, and `grep -c 'command:' playwright.config.ts`
gives 6 webServer builds. The ~126 figure uses the SAME method the original ~75
used — counting `expect(` sites — confirmed by reproducing 23+33+19=75 exactly
against the three original spec files before applying it to all eight.

client-surfaces.spec.ts now drives signalgrid-desktop and signalgrid-mobile-pwa,
so both are removed from the e2e README's "deliberately NOT covered" list;
mockup-sandbox stays.

HISTORICAL FIGURES LEFT ALONE, and one made explicit. The maiden-run narrative
("shipped with one deliberately-red test … the suite is now 15/15") is a record
of a past episode, not a current count. But "is now 15/15" sitting in a cell that
opens with "35 tests" reads as a live contradiction whichever way it was meant,
so both instances are reworded to carry the marker plainly: "as it stood then (it
has since grown to the 35 tests counted above)". The rule this repo uses is that
a past figure is recognised by the words around it — that only works if the words
are actually there.

Wall-clock timings (~38s / ~40 seconds) are left unchanged: the suite was not
run end-to-end here, and inventing a duration would be exactly the fabricated
measurement this sweep exists to remove.

check-proof-counts, docs-sanity, check-doc-orphans and guard:figures all green.

Co-Authored-By: Claude <noreply@anthropic.com>
PR #142 squash-merged as ecabc37, so the remote lane tip is not an ancestor of
the new base. Those commits are contained in ecabc37. Recorded as an
ours-strategy merge rather than a force-push: the tree is identical to the reset
branch and the push is an ordinary fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
…ld have let CI mint owner-Mac evidence

Two halves that have to ship together, because the first makes the second safe.

THE HOLE. verify-all.mjs gated live-evidence emission on
`process.platform === "darwin"`, reasoning in its own comment that this was "a
condition a cloud sandbox cannot satisfy by accident. There is no override flag
on purpose — an override would be the lie with extra steps."

A GitHub Actions macOS runner is a cloud sandbox that satisfies it exactly, and
this repo already runs one (.github/workflows/ios-ci.yml, runs-on: macos-latest).
So adding a macOS workflow would have let CI mint artifacts/live-evidence/
mac-run.json — a file whose entire purpose is asserting the owner's real machine
ran both halves. Nothing about that would have been true and nothing would have
said so. The guard's intent was right; its test did not implement its intent.

A hosted runner is a throwaway VM: not MDM-enrolled, not the managed device whose
posture macos-posture exists to read. Under this repo's platform-honesty rule it
can no more stand in for real hardware than a simulator can.

Emission now refuses under CI (CI / GITHUB_ACTIONS), checked BEFORE the platform
test so the message names the real reason and so the branch is reachable for
testing on any host. Refusal rather than an `environment: "ci"` field: that field
would sit in a file still named mac-run.json, and a skimming reader takes the
filename over the field. No override, for the same reason the platform check has
none.

Both refusals are negative-controlled and were exercised:
  CI=true                      -> "REFUSED — running on a CI runner (macOS or not)"
  no CI, linux                 -> "REFUSED — this process is not running on macOS"

THE WORKFLOW. mac-lane.yml runs ./validate-sim-macos.sh on macos-latest, on
dispatch and weekly. Until now the repo's LARGEST gate set (4 simulator + 78
looped proof + 5 non-proof = 87) ran only on the owner's laptop, which made
"the suite is green" a claim resting on whoever last remembered to run it.

It deliberately does NOT pass --emit-evidence; the script refuses anyway, so the
boundary is enforced rather than remembered. The job also asserts the harness
restored package.json and pnpm-lock.yaml after its `pnpm add -w` — if that
restore ever breaks, the lockfile drifts and CI's --frozen-lockfile starts
failing on unrelated PRs, a confusing symptom a long way from its cause.

docs/MAC_LANE.md states the three tiers and what each CANNOT prove: Linux CI
(nothing device-specific), Mac rehearsal (nothing about real hardware), real Mac
(nothing about on-device MDM enforcement unless actually supervised). Linked from
INDEX so it is not an orphan.

Full preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
…thenticated planes

An audit of the running api-server found three surfaces that are correct for a
public review deployment and unacceptable in front of a customer. Each was
already known in prose; none was gated.

  GET /v1/keys            registered ABOVE the auth guard — Express matches in
                          registration order, so requireTenantContext never ran
                          for it — while DEMO_KEYS carries the RAW bearer. It
                          published an owner token for all seven seeded tenants
                          to anonymous callers. The route's own neighbouring
                          comment already said "any visitor can satisfy the role
                          check … must not be inherited by a real deployment".
  POST /api/sim/room-entry mounted with no auth; derives a tenant from a
                          CLIENT-SUPPLIED scenarioId, mints that tenant's own
                          operator/owner token, and then WRITES — decision,
                          evidence snapshot, two audit-ledger appends, a metrics
                          increment. An anonymous caller could move another
                          tenant's ledger.
  /cp/v1                  the whole control plane mounted with no auth, so its
                          client-supplied ?tenant= query parameter was the only
                          scoping present.

THE /v1 ISOLATION MODEL IS SOUND AND THIS DOES NOT TOUCH IT. All 32 /v1 routes
derive their tenant from the verified bearer; parseEvaluate whitelists request
fields and drops a body tenantId; scoped() returns the same 404 for a
cross-tenant read as for a nonexistent id, so there is no existence oracle.
These three are demo scaffolding reachable IN FRONT OF a correct model.

THE UNCOMFORTABLE PART. api.test.mjs asserted "keys discovery is public (200)"
and that a GOVERNMENT-tenant owner key is discoverable — while spawning that
server with NODE_ENV=production. The suite never missed the leak; it certified
it. NODE_ENV was never the signal: it says how Node should behave, not whether a
customer is on the other end.

ADDITIVE BY CONSTRUCTION. SIGNALGRID_PRODUCT_PROFILE=shared-device-gateway turns
the demo surfaces off; unset or unrecognized resolves to review-demo, so an
existing deployment changes nothing by upgrading. The demo surfaces are how this
repo is publicly reviewable, and a switch that silently broke them would trade
one credibility problem for another. The fail direction is deliberately the
OPPOSITE of resolveTier's: an unreadable tier must not enable live vendor calls,
an unreadable profile must not switch off the surfaces a reviewer depends on.
Both resolve to "changes nothing"; that shared rule points opposite ways because
the risky option differs.

Observed on a really-booted server in the gateway profile:
  /v1/keys        401, not 404 — with the route unregistered the path falls under
                  the /v1 auth guard like every other /v1 path, so it now DEMANDS
                  a credential instead of handing one out
  /sim/room-entry 404 — not mounted rather than 403'd, because a route that
                  exists and refuses still answers "does this deployment have a
                  simulator?"
  /cp/v1/tenants  404 — same reasoning
  demo bearer     401 — with no OIDC configured there is no credential a gateway
                  deployment can legitimately accept, so it refuses rather than
                  degrading to the fixture keys, which are exactly the tokens
                  /v1/keys used to publish
  /healthz        200 — every gateway assertion above is an ABSENCE, and a
                  failed-to-boot server would satisfy all of them

test:api 187/187, now covering BOTH profiles: the demo assertions are unchanged
and still run, and a fourth short-lived server proves the refusals.

A standalone proof:product-profile was written and then DELETED. It exercised the
predicate rather than the server — the weaker of two gates asserting one claim,
and it would have been the one in the fast preflight loop while the strong one
sat in the slower API suite. It also imported artifacts/api-server into scripts/,
crossing a package boundary the existing api-server proofs deliberately do not
(they reference it as a path — read the file, or spawn the built server).
Recorded in the doc so it is not re-added.

Deliberately NOT done: /metrics stays a process-global, tenant-unlabeled
aggregate. Adding a tenant label to "fix" the inference would make it worse. And
this does not narrow the supported surface to the gateway — it gates demo
affordances only; claiming otherwise would be the overstatement the rest of these
guards exist to prevent.

Full preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
PR #143 squash-merged as a761384, so the remote lane tip is not an ancestor of
the new base. Those commits are contained in a761384 — nothing unmerged is on
that side. Recorded as an ours-strategy merge rather than a force-push: the tree
is identical to the reset branch and the push is an ordinary fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
Found because a negative control did NOT fire. Two independent holes in the
historical-marker exemption, both fixed here, each verified in BOTH directions
against a figure first proven to be checked.

1. THE WINDOW CROSSED NEWLINES. HISTORICAL_BEFORE/AFTER used `[^.]{0,60}` — the
   window stops at a period but NOT at a line break, so a marker on the PREVIOUS
   line silently exempted a number on this one. Breaking a real checked figure
   PASSED while an innocent "before" sat at the end of the line above, and FAILED
   once the window was bounded to `[^.\n]{0,60}`.

2. THREE MARKERS WERE ORDINARY PROSE. `from`, `rather than` and `would` appear
   constantly in normal sentences and carry no past-tense intent — "stated plainly
   RATHER THAN buried" exempted the figure that followed. Removed, controlled the
   same way: the break is caught without them and passes with them restored.

`before` was tested for removal and KEPT. INTEGRATION_CATALOG's "measured it,
**before this fix**, at 21,168 reports" is exactly the deliberate historical usage
the marker list exists to honour. Dropping it would turn a correct sentence into a
failure, which is how a gate earns a reputation for crying wolf and gets switched
off.

Neither narrowing breaks a single currently-passing figure — measured on the real
docs, not assumed.

HOW THE CONTROL WAS BUILT, because the first two attempts were invalid and the
method is the transferable part. A control must FIRST prove the figure it targets
is actually checked: break it with no marker and confirm the guard fails. Both
earlier attempts skipped that step and picked numbers the guard never sees — one a
bare 3-digit value (FIGURE_RE only matches comma-formatted values >= 1,000), the
other inside a section naming proof:agent-identity, which is not in the registered
PROOFS list. Each produced a green run that proved nothing. The target used here,
1,944 in a section naming proof:agent-behavior, was confirmed checked before being
used to test anything.

WHAT THIS DOES NOT DO. It does not widen coverage. The scope filter (only
docs/*.md, only sections naming a registered emitter) and the shape filter
(comma-formatted >= 1,000 only) are unchanged, and both are still announced every
run. This makes the figures the guard ALREADY claims to check actually checked —
its green was weaker than it looked, in an unknown number of places, and that
number is now smaller by two whole classes of exemption.

Full preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
PR #144 squash-merged as 536a1fa, so the remote lane tip is not an ancestor of
the new base. Those commits are contained in 536a1fa — nothing unmerged is on
that side. Recorded as an ours-strategy merge rather than a force-push: the tree
is identical to the reset branch and the push is an ordinary fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
WHAT WAS THERE. `signalgrid-core-proof` checked `findDeviceByRef` in both
directions and one cross-tenant remediation approval. Those caught what they were
written for, and they are not coverage: a NEW reader added without a `scoped()`
call would pass every one of them.

WHAT THIS IS. Every tenant-scoped reader against every ORDERED PAIR of the seven
seeded tenants — 78 pairs across 7 readers, plus 5 list readers. A missing scope
is now a failure rather than an omission nobody notices.

THREE PROPERTIES, and the second is what makes the first mean anything:

  1. REFUSAL      — reading tenant A's row as tenant B yields undefined.
  2. NON-VACUITY  — the SAME read AS TENANT A yields the row. Without this, a
                    matrix of `=== undefined` assertions passes trivially against
                    ids that do not exist, and proves precisely nothing.
  3. NO ORACLE    — the cross-tenant result is byte-identical to reading a
                    fabricated id, so a caller cannot distinguish "exists
                    elsewhere" from "never existed". A distinguishable refusal
                    confirms existence, which leaks the thing scoping hides.

Property 2 is in the proof because I hit that exact trap TWICE tonight while
building negative controls for the figure guard — once on a bare 3-digit number
FIGURE_RE never checks, once inside a section naming an unregistered proof. Both
produced confident green runs over nothing. The guard against my own repeated
mistake is written into the file rather than left to memory. A sweep-level
non-vacuity check covers the same failure shape for the enumeration as a whole.

NEGATIVE CONTROL. Deleting the `row.tenantId !== tenantId` comparison in
`scoped()` fails the gate immediately and across every reader; restored,
206/206. A gate that has never failed is indistinguishable from one that cannot.

SAMPLING NOTE. There is no `listIdentities`/`listDevices`, so identity and device
refs are sourced from a seeded decision. A tenant with no decisions therefore
contributes no pair — legitimate, handled by the null guard, and counted so the
skips cannot silently hollow out the sweep.

THIS FOUND NOTHING, and the proof's own header says so. The api-server surface
map established the model is already sound: all 32 `/v1` routes derive their
tenant from the verified bearer, `parseEvaluate` drops a body tenantId, and
`scoped()` gives no existence oracle. This is REGRESSION PROTECTION, not a
discovery — a real improvement to what is guaranteed, and it would be
overstatement to call it more.

Full preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
PR #145 squash-merged as dabbbe1, so the remote lane tip is not an ancestor of
the new base. Those commits are contained in dabbbe1. Recorded as an
ours-strategy merge rather than a force-push: the tree is identical to the reset
branch and the push is an ordinary fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
…ath guard

phase:gate failed RED on scripts/src/tenant-isolation-proof.ts. Its unsafe-file-
path rule matches a path SEGMENT beginning `tenant`/`customer`/`phi`/`pii` —
which is exactly how a file that HOLDS tenant data announces itself.
`docs/tenant-data.json` trips it identically.

THE RULE WAS RIGHT AND THE FILENAME WAS WRONG. The tempting fix was to narrow the
pattern so my file passes; that is weakening a fail-closed data-leak guard to
accommodate a test's name, and it would have left the guard slightly worse at the
job it exists for. Renamed to isolation-scope-proof.ts instead
(proof:isolation-scope), with the reasoning recorded in the file header so the
name does not get "improved" back by someone who only sees an odd choice.

The proof is unchanged: 206/206 over 78 ordered tenant pairs, still
negative-controlled.

WORTH NOTING SEPARATELY. phase:gate is a CI job preflight does not mirror, so a
green local run said nothing about it. PR #142 corrected preflight's claim of
mirroring "all three CI jobs" and enumerated six — this is at least a seventh.
That correction was better than what it replaced and still not complete. Logged
rather than quietly left, since a freshly-corrected sentence going stale in its
own way is the exact failure #142 was about.

Also pre-existing and unfixed here: the unsafe-CLAIM scan matches ~60 doc lines,
nearly all of them NEGATIONS ("no autonomous production remediation", "Do not say
SignalGrid replaces Jamf"). It is substring-based and cannot tell a claim from
its denial, so it flags the most carefully-worded docs. YELLOW is manual-review
rather than failure so nothing is broken, but a signal that is almost pure noise
is one a reviewer learns to ignore — which is how a gate stops working.

Full preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
…t run proved

The owner dispatched the Mac lane for the first time. It failed, and the failure
was this workflow's rather than the code's.

  == SUMMARY: 86 passed, 1 failed ==
     failed: proof:enrollment-race

That proof races 12 concurrent WebAuthn enrollments against a REAL Redis to show
the SET NX PX lock holds, and it REFUSES to run without REDIS_URL rather than
skipping quietly — correct for a race proof, where a silent skip is
indistinguishable from a pass. The job never provided Redis, so the gate could
not run.

FIXED BY INSTALLING THE DEPENDENCY, NOT BY DROPPING THE GATE. `brew install redis`
plus a health check before the suite starts, so a Redis that fails to come up is
attributable instead of surfacing later as a mystery proof failure. macOS runners
cannot use `services:` — that is Linux-container only — so Homebrew it is. A lane
that quietly omits a gate while calling itself "the full suite" is exactly the
failure the rest of this repo's guards exist to prevent.

WHAT THE RUN PROVED, recorded in docs/MAC_LANE.md because a workflow nobody has
run is a claim rather than a lane:

  - The lockfile-restore assertion added in #143 PASSED — "package.json and
    pnpm-lock.yaml are unchanged". It was written blind and it works.
  - 86 + 1 = 87 gates, independently confirming the count #142 corrected in
    CLAUDE.md. The `must be 66/66` pin it replaced would have read as a pass
    while 21 gates went unaccounted for. That correction is now backed by a real
    run rather than by a derivation.

ALSO CORRECTED, because this change invalidated it: MAC_LANE.md said preflight
and this suite "both mirror the service-free gates". Provisioning Redis makes that
false for this suite. The same paragraph now also names phase-pr-evidence — the
job that failed PR #146 — as one neither lane covers, so the "six CI jobs" figure
from #142 is marked as still incomplete rather than left looking settled. Deriving
the job list mechanically instead of maintaining it by hand is tracked work.

Full preflight green.

Co-Authored-By: Claude <noreply@anthropic.com>
PR #146 squash-merged as e56ffff, so the remote lane tip is not an ancestor of
the new base. Those commits are contained in e56ffff. Recorded as an
ours-strategy merge rather than a force-push: the tree is identical to the reset
branch and the push is an ordinary fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
Two dispatched runs failed for reasons the logs did not state. Both causes were
in the workflows, not in the code they were testing.

MAC LANE — the run testing the fix was cancelled by the run testing the bug.
`concurrency.group` was keyed on `github.ref`, so every dispatch and every
re-run of an older attempt shared one group. A fresh dispatch of the Redis fix
started at 13:06; a re-run of the earlier failing attempt started at 13:08 and
cancelled it. The survivor was the run on stale code — the exact inversion of
what `cancel-in-progress` is for. Keyed on `github.sha` now: two dispatches of
the same commit still dedupe, which is the real waste, while a run of one commit
can no longer kill a run of another.

PROMOTE TIER — an opaque red X for a repository fact, not a run failure.
The four tier branches are all pinned to the `Merge PR #65` commit while every
PR since has merged into `SignalGrid_Alpha`, so `dev` is not fed and every
promotion this workflow can offer has an empty diff. GitHub answers that with a
422 the script did not catch. Two changes:

  - The 422 is caught and reported as the no-op it is, matched on both the
    structured `errors[]` and the message text because Octokit has moved the
    body between them across versions. Any other 422 still throws — an
    unrecognised validation failure is a real failure — but logs the response
    body first so the next one is diagnosable from the run alone.

  - A new "Tier state" step reports where each tier actually points, before the
    promotion and regardless of its outcome, and says plainly when every tier is
    behind the default branch. It MEASURES the gap rather than quoting it, so it
    cannot go stale the way the docs did.

Both script bodies were exercised against stubs rather than eyeballed: the
stale-everywhere, missing-branch and all-current cases for the state report; and
empty-diff (notice, no throw), unrecognised-422 (still throws) and success for
the create step.

DOCS — two claims that were true when written and quietly stopped being true.
`BRANCHING_AND_ENVIRONMENTS.md` called `dev` the default branch; `OWNER_ACTIONS.md`
said the promotion branches "track the current build". Neither holds. Both now
state the designed model and the current state separately, and neither hardcodes
the size of the gap.

Reconnecting the pipeline is left as an owner decision with three defensible
answers (feed `dev`, re-point the default to `dev`, or retire the tier branches),
because all three are product calls and two of them move protected branches.

Validation: preflight PASSED; proof-count check 31/31; phase:summary-check pass;
git diff --check clean. phase:gate is YELLOW for touching workflows, as intended;
its `unsafeClaims=found` is the pre-existing repo-wide negation set, and the diff
adds none of the protected wording.

Public-safety note: workflow and documentation changes only. No connector, no
runtime path, no vendor call, no evidence emission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The lane branch carried the pre-squash commits for #147; the default branch
carries GitHub's squash of the same change. `git diff` between the remote lane
tip and the default branch is EMPTY and their tree hashes are identical, so
this `-s ours` merge discards no content — it only records that the lane's
older history is already represented, letting the new work push as a
fast-forward instead of a force-push.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…t 422)

I diagnosed the original red X as an empty-diff 422 and shipped a handler for it.
Dispatching the fixed workflow proved that wrong. Both runs — the owner's
alpha → beta and my dev → alpha — failed identically:

    POST /repos/.../pulls - 403
    GitHub Actions is not permitted to create or approve pull requests.

The 422 never occurred. GitHub rejects the call on policy before it ever looks at
the diff, so the empty diff, while real, was never reached. The 422 handler stays
— it is correct and will matter the moment PRs can be created — but it was not
the bug.

WHAT THE 403 ACTUALLY IS. Repository policy, not a defect: Settings → Actions →
General → Workflow permissions → "Allow GitHub Actions to create and approve pull
requests" is off, which is GitHub's default. The job's own `pull-requests: write`
grant cannot override it, so this call could never have succeeded.

Failing red for a permanent, deliberate setting only teaches people to ignore red.
So the workflow now does the rest of its job: it catches this specific 403 and
hands back the compare link that opens the identical PR in one click, then exits
green. That path needs no elevated permission and no stored credential — a better
default than asking an owner to switch off a security setting to make a
convenience workflow work. Enabling it remains possible, and is described as
optional, because it buys convenience and nothing else.

An unrecognised 403 still throws, so this is not a blanket swallow of the status.

The docs are corrected to match. They named the empty diff as the reason the
workflow failed; that was my error, and it is now stated as two separate facts —
the tiers are not fed (an owner decision, unchanged), and Actions cannot open PRs
(fine, and now handled).

WHAT THE FIRST DISPATCH DID PROVE. The new Tier state step ran and reported:

    dev    7ee88ef  108 commit(s) behind `SignalGrid_Alpha`
    alpha  7ee88ef  108 commit(s) behind `SignalGrid_Alpha`
    beta   7ee88ef  108 commit(s) behind `SignalGrid_Alpha`
    prod   7ee88ef  108 commit(s) behind `SignalGrid_Alpha`

108 matches `git rev-list --count` locally, so the live measurement is right. The
one-line error log also worked: the failure now reads as a single legible
statement instead of a hundred lines of Octokit object dump, which is how the real
cause became visible at all.

Validation: the create step is exercised against stubs for all five outcomes —
empty diff (notice, no throw), unrecognised 422 (throws), Actions-may-not-open-PRs
(notice + compare link, no throw), unrecognised 403 (throws), success (opens PR).
YAML parses; no comma-formatted figures or protected wording added to docs;
phase:summary-check pass; git diff --check clean.

Public-safety note: workflow and documentation changes only. No connector, no
runtime path, no vendor call, no evidence emission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…iled)

PR #147 shipped the Redis fix with "STILL UNVERIFIED" in its own commit message,
because a macOS runner cannot be driven from the cloud lane. It is verified now,
and the two dispatches in between are worth recording — the first of them failed
in a way that is easy to skim straight past.

THE CANCELLED RUN. The second dispatch never executed a single gate. It was
cancelled by a re-run of the earlier failing attempt, which shared its
`github.ref`-keyed concurrency group. The run that survived was the one testing the
OLD commit, so the lane reported on stale code while looking like it had reported
on the fix. A cancelled run is not a failed run and does not read as one.

THE VERIFIED RUN, dispatched after the concurrency key moved to `github.sha`:

    == SUMMARY: 88 passed, 0 failed ==
    ✅ Simulation validation GREEN.

Redis came up via Homebrew, `proof:enrollment-race` ran rather than refusing, and
the lockfile-restore assertion held again ("package.json and pnpm-lock.yaml are
unchanged").

THE COUNT MOVED: 87 gates on the first run, 88 on this one. That is the suite
growing between the two, and it is exactly why neither this file nor CLAUDE.md pins
a total — the doc now says so explicitly, in the same place it reports the number,
so nobody reads 88 as a target to check against later.

Public-safety note: documentation only. No connector, no runtime path, no vendor
call, no evidence emission — and note this run deliberately minted none: a hosted
macOS runner is a throwaway VM, and `verify-all.mjs` refuses to emit under CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…wallowing faults

An adversarial review of this branch found two defects in the "Report tier state" step
I added three commits ago. Both made the run summary assert, as measured fact, something
it had not measured. Both survived two independent skeptics attacking them on mechanism
and on reachability.

1. THE NO-OP CLAIM DID NOT FOLLOW FROM THE MEASUREMENT.

`missing` is `ahead_by` on `<tier>...<default>` — the count of commits the tier is
MISSING. It says nothing about commits the tier HAS that the default lacks. The banner
fired on "all four tiers are behind" and from that concluded "there is nothing for any
promotion to carry and each one is a no-op". Those are different comparisons: a tier can
be behind the default and simultaneously ahead of the tier above it.

Reachable by following this repo's own documented flow. `BRANCHING_AND_ENVIRONMENTS.md`
says "merge feature work into `dev`". Land one commit there and the merge base does not
move, so all four tiers still read 108 behind, the banner still fires — and `dev → alpha`
now carries real work the summary just called a no-op. Worse, the very same run would
print "Open the `dev` → `alpha` pull request" with a live compare link, contradicting
itself in one summary.

Fixed by measuring the thing being claimed: one compare of `<to>...<from>`, the pair
actually dispatched, whose `ahead_by` IS the number of commits the promotion would move.
The staleness observation survives as a separate paragraph that now says explicitly that
being behind the default "does not by itself make a promotion empty".

2. A BARE `catch` TURNED EVERY FAULT INTO A FALSE ANCESTRY CLAIM.

`catch { return null }` — no error binding, so no discrimination and no logging was even
structurally possible — and `null` rendered as "unrelated history", a definite statement
about git ancestry. A 502 or a socket timeout on one tier's compare would print that
falsehood, then drop the row from the staleness filter so the explanatory banner vanished
exactly when its data was least trustworthy, with the job still green. A false statement
and a fault reading as success, from one line.

Now discriminated the way `shaOf` twenty lines above already did it, and the way this
file's own create step states the rule for itself: a 404 means no common ancestor (a real
answer, since the branch is known to exist) and renders as "no common ancestor"; anything
else is logged on one readable line and rethrown.

VALIDATION. The step is exercised against stubs for six states, including two that are
regression tests for exactly these defects:
  - reality today (tiers identical to each other, all 108 behind default) -> reports BOTH
    "carries nothing" and the entry-point note, without conflating them
  - DIVERGED (tiers 108 behind, but dev->alpha carries 1) -> "carries 1 commit(s)"; the
    false no-op is gone
  - transient 502 on a tier compare -> logs and THROWS, prints no ancestry claim
  - genuine 404 -> "no common ancestor"
  - a tier branch missing -> degrades, no crash
  - all tiers current -> "carries nothing", no entry-point note
The five create-step outcomes from the previous commit still pass unchanged.

YAML parses; git diff --check clean; phase:summary-check pass.

Public-safety note: workflow change only. No connector, no runtime path, no vendor call,
no evidence emission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…es ruled out

`fromDevicePosture` tested five specific bad values and let everything else fall
through to `{ action: "none", posture: "compliant", reason: "COMPLIANT_MANAGED" }`.
That is golden rule 2 inverted — an unknown signal must raise assurance, never lower
it — and the reason code made it worse by asserting the device WAS managed and
compliant when its management state was unreadable.

WHAT WAS UNHANDLED. Every Graph field carries an `"unknown"` member.
`deviceComplianceState` also carries `"in_grace_period"`; `deviceManagementState`
also carries `"retire_pending"`; and `deviceRegistrationState` was never read at all,
so `not_registered` contributed nothing either. Six unhandled values across four
fields, plus one field entirely ignored.

MEASURED, NOT ESTIMATED. Enumerating the full 900-state input space before the fix:
216 states composed to `none`, of which 213 were NOT positively confirmed on every
input. The grant path was 72x wider than the three genuinely-clean states. After the
fix it is exactly 3.

THE FIX. Affirmative concerns keep their existing severities. Every unconfirmed input
now contributes `step_up` — foreclosing the grant without denying, because an
unreadable signal is not evidence of wrongdoing and denying on one would strand a
clinician mid-shift over an API timeout. Each unknown gets its own reason code rather
than a single catch-all, so the evidence names WHICH signal could not be confirmed.

This brings the adapter into line with its own siblings, which had already drawn the
distinction: `fromMacosPosture` says "an unreadable Mac is never fused as compliant",
and `macos-posture/types.ts` encodes it in the type — "null = enrollment state could
not be determined (unknown, not 'unmanaged')".

THE GATE. Eight named cases (one per newly-handled value) plus an EXHAUSTIVE sweep of
all 900 states asserting zero unjustified grants. The sweep is the real gate: a
spot-check cannot see a field nobody thought to test, which is exactly how
`deviceRegistrationState` went unread for as long as it did.

Non-vacuity is asserted too — "zero unjustified grants" passes trivially if nothing
ever grants, so the proof also pins the grant path as REACHABLE and exactly 3 states
wide.

NEGATIVE CONTROL RUN, not assumed. Deleting the `MANAGEMENT_STATE_UNKNOWN` candidate
— reintroducing precisely the original bug — takes the proof to 68/71 and the
exhaustive check names the three leaked states by hand
(`enabled/{none,low,medium}/compliant/unknown/registered`). Restored, back to 71/71.
The gate genuinely checks the thing it claims to check.

Found by an adversarial survey that was looking for something else entirely.

Validation: proof:posture-composition 71/71 (was 62/62); full preflight PASSED.

Public-safety note: pure decision-logic and proof change. No connector I/O, no vendor
call, no new dependency, no evidence emission. The change only ever makes the fabric
MORE conservative — no state that previously stepped up, alerted, restricted or
escalated is relaxed by it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The lane branch still carries the pre-squash commits for #148; the default branch
carries GitHub's squash of the same change. Verified before merging: the two trees
are byte-identical (2b734d3) and `git diff` between them is EMPTY, so this `-s ours`
merge discards no content. It records that the lane's older history is already
represented, letting the new work push as a fast-forward rather than a force-push.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
`lib/ddm-connector/src/apple-schema.ts` says of its pinned version: "Must match the
macos-posture alignment's pinned version." Nothing checked it.

Two subsystems pin the same upstream — `DDM_APPLE_SCHEMA_VERSION` and
`APPLE_DEVICE_MANAGEMENT_SCHEMA_VERSION`, both "26.4". Each proof regex-tested only
its OWN constant for shape (`/^\d+\.\d+$/`), and no proof imported both, so the two
could drift to different Apple releases with every gate green: the repo asserting one
canonical vocabulary while grading against two. Reconciling one alignment on a new OS
release and forgetting the other is the obvious way in, and it would have been silent.

The check lives in `macos-apple-schema-proof.ts` rather than either connector because
it is a claim ABOUT the pair, and that proof already owns the alignment contract.

NEGATIVE CONTROL RUN, not assumed. Drifting the ddm pin to "26.5" takes the proof to
52/53 with a message that names both values —

    FAIL — both Apple alignments pin the SAME upstream release
           (macos-posture=26.4, ddm-connector=26.5)

— so a future failure is self-explaining rather than a bare assertion number.
Restored, back to 53/53.

The check count moved 52 → 53, which the repo's own guards caught in sequence and
which is the system working: `check-proof-counts.mjs` failed preflight on the stale
"52 checks" in INTEGRATION_CATALOG.md, and once that was corrected
`check-live-sync.mjs` failed on the un-republished manifest. Both updated. The
manifest diff is exactly the count plus its version/fingerprint bump — no evidence
field is touched, and `liveEvidence` remains `none`.

Validation: proof:macos-apple-schema 53/53; full preflight PASSED.

Public-safety note: proof, doc, and generated-manifest change only. No connector I/O,
no vendor call, no new dependency, no evidence emission — and note the check is
offline by construction: it compares two in-repo constants and fetches nothing. The
pinned catalog remains the contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
claude added 30 commits July 31, 2026 17:22
…idence to correlate

Phase 2 of intake row 16, plus a hygiene find.

PORTAL ADJACENCY. Door nodes gain `connects` — the space(s) a door opens to
besides its parent — validated at build: connects on a non-door, into nowhere,
into itself, or into another door all refuse. doorSides() derives the full set
a door touches in either direction.

THE CROSSING CORRELATOR (correlate.ts). A badge event is evidence, not proof:
correlateCrossing() grades one door crossing against one subsequent device
observation over the graph's adjacency.
- corroborated: observed inside a side or its DESCENDANT (Bed B corroborates a
  crossing into Room 312), within the caller's stated window. Carried as
  evidence — deliberately never a grant and never an accuracy upgrade: the
  certainty ladder belongs to the sources.
- contradicted: observed where this door does not lead → alert. The badge
  crossed one threshold and the device is somewhere else: passback, tailgate,
  or a cloned badge.
- unassessed: before the crossing or outside the window — no claim posed;
  clock skew lands here honestly instead of passing silently.
- unknown (unresolvable door, unreadable window or instant) raises; an
  observed space the graph does not carry alerts (measurement broken).
No clock in the decision path: both instants come from the events, the window
from the caller, and the boundary is inclusive and exact (90.000s in,
90.001s out — pinned).

Also fixed: the vendor-index key embedded literal NUL bytes (valid JS, but it
made graph.ts read as binary to grep/diff tooling — unreviewable). Now
JSON.stringify([ns, key, id]): collision-proof and readable.

Proof 44 -> 55 checks (adjacency refusals, both directions of corroboration,
the contradiction alert, both unassessed rungs, the inclusive boundary, every
unknown raising). Mutation guard: 37 mutations, 0 survivors, 7 documented-
inert (unchanged). Typecheck, MCP surface gate, invariants, registries,
docs, live-sync all green; fixture graph gains the west corridor so the MCP
facility_graph tool shows a real portal.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The owner supplied the Mac lane's session id and asked for the connection
between lanes to stay current. Direct channels do not exist: session
transcripts are not readable across sessions, and cross-session triggers are
disabled for this organization — verified, not assumed. The channel both
lanes genuinely share is the repository, so the protocol now lives there:

- docs/LANE_COORDINATION.md: the lane table (cloud: PR #152 branch, fabric
  scope; Mac: SignalGrid_Alpha, iOS + local harness), the shared-surface
  serialization list (discipline gate, mutation guard, sync manifest, proof
  registration, subpath exports), announce-in-the-commit, the owner-approved
  merge direction (the cloud lane absorbs base movement; the webhooks
  dual-gate is the reconciliation precedent), and the learned hazards
  (validate-sim-macos.sh rewrites package.json; the mutation guard never runs
  concurrently; the manifest is generated, never hand-edited).
- CLAUDE.md points to it, so EVERY session in this repo — either lane, any
  future one — loads the protocol at start. That is what makes it a standing
  agent rather than a note: it rides the instruction file both lanes already
  obey.

The doc-orphan gate caught the new file unlinked (exit 1) and it is now
indexed — the gate doing its job on its author.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…f, composed with the emitter-discipline pins

Second base reconciliation, same standing owner approval as d125a0a. Five
Mac-lane commits land: the shared in-adapter emit gate (siem/telemetry/itsm
routed through resolveEmission), the mdm-profile proof, backlog/doc refreshes.

Which side won, per conflict:
- lib/integrations/package.json: UNION — kept every branch subpath export and
  adopted base's "./emit-gate".
- syslog/transport.ts: COMPOSED, not either parent. Gate first — a suppressing
  env returns the shared honest `suppressed` status like every other emitter
  (base's design). Past the gate, where the caller explicitly configured live
  delivery, the adapter still THROWS rather than reporting any status for an
  event that never left the process (branch's design) — a quiet status is at
  its most dangerous exactly where delivery is expected. The gate stands
  beside the refusal; it does not soften it.
- check-connector-discipline.mjs: OURS — the 40/40 gate with empty KNOWN_GAPS
  supersedes the base's partial 36/36.

Both proofs updated to pin the composed truth instead of their parents:
proof:emit-gate 69/69 (syslog check now pins the live-mode throw),
proof:emitter-discipline 43/43 (throw proven with live delivery FULLY
configured — beta tier + flag "true" — plus a suppressed-status check).
Registered base's two new proofs in CI + preflight, which neither lane had
done (the ci↔preflight drift gate only compares those two lists against each
other, so a proof in neither slips through).

Gates: typecheck, review:invariants, connector-discipline, mcp-surface,
ci↔preflight sync, proof figures, live-sync, proof:webhooks 48/48,
proof:mdm-profile 18/18, proof:signalgrid-simulator — all green.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The merge added a 43rd check (suppressed-status under a suppressing env);
the catalog's quoted count is what check-proof-counts pins.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The manifest derives proof counts from the docs; the previous commit moved
the catalog to 43 and the committed manifest still said 42. The first red
run stopped at check-proof-counts, so live-sync only surfaced this now.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…s truth, the ceiling is physics, the scan is the step-up

Three pieces in lib/facility-trust-graph/src/clinical.ts:

- resolveClinicalAssignment: ADT/FHIR unit/room/bed identifiers resolve
  through vendor attachments (never keys) to ONE coherent target space. An
  assignment is administrative truth — it names the workflow's target and
  can never claim anyone is at it. Every pairwise coherence edge is checked
  (bed↔room, bed↔unit, room↔unit) plus attachment-kind agreement; a record
  that contradicts the graph's own hierarchy is incoherent, never "probably
  the bed". Unmapped identifiers alert; a patient-shaped extra key is
  malformed at the boundary.

- applyCapabilityCeiling: the maximum class each recognized technology can
  physically vouch for (wifi -> room_candidate at best, the owner's verbatim
  rule; IR/ultrasound/UWB RTLS -> bed_confirmed; the generic "rtls" label ->
  nothing). A claim above the ceiling is NOT demoted to the ceiling — no
  partial credit for a caught lie; the certainty becomes unknown and the
  verdict an alert.

- evaluateBedWorkflow + gradeExplicitSelection: the wristband-scan step-up
  path. Insufficient precision, wrong-bed mismatch, absent assignment, and
  location-gone-dark all step up to an explicit-selection ceremony
  (wristband_scan / manual_selection) attested by the HOST app — method and
  instant only, no patient identifier crosses (embedded-UX law). A valid,
  fresh attestation (supplied bound + supplied reference instant, no clock)
  proceeds WITHOUT ever upgrading the accuracy class, and satisfies only
  step-up-class concerns — never a wrong map, a broken clinical mapping, or
  a source claim above its ceiling.

Proof grows 55 -> 91 checks (capability teeth, every coherence edge, the
phase-3 headline, the never-upgraded pin, the lie-is-not-steppable pin, the
hand-crafted-resolution grant pin). Mutation guard: clinical.ts registered;
64 mutations, 53 killed, 11 known-inert with reasons, 0 survivors — three
survivors became new checks, one redundant conjunct was deleted, four are
labeled inert in the source with ALLOWED entries.

Phase 4 (Site Context Gateway) remains documented roadmap.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…ence does neither (intake row 17)

The spatial-trust research report (ledger row 17) is external validation of
the row-16 architecture — its central rule and nearly all of its mechanics
map element-by-element onto phases 1-3, named in the ledger. Its ONE
mechanical gap: phases 1-3 grade single observations; nothing graded a
SEQUENCE for session continuity — the geofence entry/exit state machine
with dwell, grace, and hysteresis ("exit should not revoke on one missing
radio observation").

lib/facility-trust-graph/src/transition.ts — gradeZonePresence:
- Presence is EARNED: continuous in-zone evidence spanning a caller-supplied
  entry dwell. One blip spans zero seconds and never meets a positive dwell;
  a visit that ends before dwell was never an entry.
- Exit is CONFIRMED only by an affirmative observation OUTSIDE a
  caller-supplied containment boundary past the exit grace — hysteresis via
  the graph (out of the room is not out of the unit), not distance. Sensor
  silence EXPIRES presence (step up, attenuate) but never manufactures the
  affirmative "they left": a dead access point is not a door event.
- One missing observation never revokes: within grace the state is
  probably_outside with action monitor — retained, watched.
- No clock: every instant supplied, every bound caller policy. Disordered,
  future-dated, or unreadable sequences raise; empty evidence cannot answer
  a posed question.

Proof grows 91 -> 104 checks. Mutation guard: 76 mutations, 65 killed, 11
known-inert, 0 survivors — the one survivor became the pin that omitting
the staleness bound never buys eternal presence (grace expires it alone).

Positioned, not built: CAEP/Shared Signals as the outbound session-signal
emitter (behind emitter discipline when built); TAP-style bootstrap
credential grading queued as a candidate dimension.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The owner re-supplied the spatial-trust research report as a markdown
upload — the same document already assessed as intake ledger row 17
(zone-presence state machine built at 9f03c7f; everything else covered or
positioned). No new build; the file-form second read confirmed the
disposition. Committed verbatim under docs/inspiration/ with a provenance
preamble so the ledger row points at a document, not a chat scrollback;
linked from INSPIRATION.md; row 17 updated to reference it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…tays local, the cloud gets the minimum or nothing

The Site Context Gateway's pure decision/data core
(lib/facility-trust-graph/src/gateway.ts), completing the owner's four-phase
implementation order. Transport, sync cadence and config-down integrity
remain control-plane/edge-sync — nothing here opens a socket.

- projectUpstreamRecord, the minimization projector: the hybrid contract
  from the row-17 report made mechanical. Upstream carries ONLY outcome,
  reason codes, coarse zone, pseudonym, device tier, source health, latency
  and the audit anchor. It REFUSES what it does not recognize instead of
  stripping it — a silently dropped patient_id teaches callers to keep
  sending one. Spatial content coarsens through the graph to the operator's
  kind ceiling; a bed-level decision projects as its unit, and the proof
  pins that the serialized record contains no trace of the room or bed id.
  When no coarse ancestor exists the record carries nothing spatial — never
  the raw id — and an unmapped precise id refuses outright. An email-shaped
  pseudonym trips the raw-identifier tripwire.

- deriveGatewayMode, the restricted-mode grader: the operator poses which
  local sources high-trust workflows require. A required source unavailable,
  unrecognized, or absent from the report (absence is not health) yields a
  defined restricted mode with location-derived privileges WITHDRAWN — a
  restricted place never silently loosens because location went dark. An
  unreadable report or policy is itself restricted. Non-required sources
  never restrict: the posed set governs.

- The audit anchor: @workspace/audit is already an atomic hash-chained
  ledger and is NOT rebuilt; the upstream record carries the local chain
  HEAD so the control plane can detect tampering or truncation without
  receiving any sensitive record.

Proof grows 104 -> 120 checks. Mutation guard: gateway.ts registered; 87
mutations, 76 killed, 11 known-inert, 0 survivors on the first sweep.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…intake row 18)

Two inputs (ledger row 18). The "Cybersecurity Across All Domains" poster
is a document-template taxonomy — program paperwork, human-owned; every
decision-relevant slice already names an existing surface (mapped in the
ledger). The Jamf Setup Manager guide surfaced one genuine gap: the guide
URL is bot-protected (recorded honestly; assessed via the official
Jamf-Concepts repo), and the tool's whole purpose — hold the Mac in Setup
Assistant until required day-zero items install — has a documented failure
mode, the release race. The fabric could PLAN zero-touch setup but nothing
graded the RELEASE: a device enrolled, compliant-looking, and in a user's
hands with required payloads missing was unrepresentable.

gradeSetupCompletion in @workspace/flows provisioning:
- The ENFORCED plan's steps are the required set; a simulated plan required
  nothing on-device, so grading one is unknown (PLAN_NOT_ENFORCED) — never
  a hollow green.
- Absence of a step report is never success; an unrecognized result value
  is outstanding, not succeeded.
- A supplied first-user-session instant (no clock) means the device was
  released: all-succeeded -> released_complete; outstanding steps ->
  released_incomplete (step_up — the enrollmentComplete race made visible);
  ZERO reports at release -> setup_bypassed (alert — in use with no
  evidence guided setup ever ran); no session yet -> in_setup (monitor —
  failing DURING guided setup is the retry loop, not a release).

proof:provisioning-order grows 34 -> 42 checks; catalog count and manifest
updated in the same commit. Honest note in the ledger: the flows
provisioning module predates the mutation guard's TARGETS and remains
un-swept — the grader is pinned by its eight checks, not by mutation.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…pagination guard

Third base reconciliation, same standing owner approval. Seven Mac-lane
commits land: proof:live-fleet / proof:live-edr / proof:graph-wire (proofs
against a REAL Fleet 4.89.2 and Wazuh — which found every host/policy route
in the fleetdm adapter wrong and fixed them), the pagination-truncation
guard, and docs/STATUS.md publishing.

Which side won, per conflict:
- scripts/preflight.mjs: UNION — kept our CI<->preflight drift gate line and
  adopted their pagination-truncation guard line.
- lib/integrations/src/integrations/telemetry/index.ts (add/add): UNION —
  our ./resolve gate exports (the canonical emitter entry) plus their
  ./types and config-store exports; no symbol collisions.

The three live-* proofs stay OUT of CI/preflight deliberately — they need a
running Fleet/Wazuh and are documented as live lanes in
docs/ZERO_COST_LIVE_TEST_MATRIX.md; the drift gate's registered/exempt
model is unaffected.

Verified on the merged tree: typecheck 0 errors; proof:emit-gate 69/69 and
proof:emitter-discipline 43/43 still pin the rewritten fleetdm adapter;
connector discipline, invariants, pagination guard, counts, figures,
orphans, mcp-surface, guard registries, live-sync (manifest regenerated),
simulator — all green.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…41st family (row 17 queue closed)

The auth plane's provenance dimension, from the row-17 research report's
TAP workflow made mechanical (Entra Temporary Access Pass and peers are
the reference shape). A session that merely LOOKED authenticated can now
be seen for what opened it:

- THE SCOPE RULE: a bootstrap pass on an operational workflow restricts —
  "the pass can access only authenticator enrollment or recovery", and a
  step-up would let the suspect credential answer for itself. An unposed
  workflow under a bootstrap pass fails closed to step_up: silence never
  widens enrollment-only.
- Lifetime is DERIVED (issued/expires instants + caller reference — no
  clock): expired-but-in-use restricts; NO expiry is `unbounded` —
  "shortest practical" was not practiced, visibly.
- Issuance defects alert: a pass minted broad, or verified by LOCATION
  ALONE (location corroborates an issuance, never carries it).
- A reusable "one-time" pass steps up — weaker than the mechanism promises.
- The clean state is a STANDING strong credential and only that: a
  perfectly-used bootstrap pass still reads monitor, because a temporary
  credential is an elevated state, not a clean one. Enumeration proves it:
  1,944 normalized + 864 raw-wire states, and no bootstrap combination
  reaches the clean state.

Standard discipline from birth: 4-clause gate (tier + flag +
BOOTSTRAP_CREDENTIAL_ACCESS_TOKEN + injected transport), GET-only,
defensive normalizer (own-property reads, prototype-chain key scan,
throwing-getter and Object.prototype hostiles pinned), fixture transport.
Fused as the 32nd composable kind, routed in the incident playbook
(Identity & Access queue, never Service Desk, never SecOps).
proof:bootstrap-credential 35/35, registered in CI + preflight + figures
+ counts (44 documented counts green); mutation guard: 36 mutations, 30
killed, 6 labeled-inert with reasons (the coherent-rung conjuncts each
outranked by their own pinned raising branch; the backstop carries its
OWN reason so a weakened branch surfaces as UNGRADED instead of
impersonating what it replaced), 0 survivors. Connector-discipline gate
reads 41 of 41. Manifest regenerated (kinds=32).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
… one (intake row 19)

The Omnissa Intelligence app-health blog (ledger row 19; the page is
bot-protected, assessed honestly via search snippets + Omnissa's own
techzone/product pages). Deployment validation, battery drain, and the
experience-analytics dashboards all map to existing surfaces, named in the
ledger — and DEEM/analytics platforms are a category SignalGrid CONSUMES,
not competes with: their computed figures arrive as caller-supplied
signals. The one genuine gap: the host app's RUNTIME STABILITY on this
device as a decision input — a med-pass starting in an EMR app that
crashed repeatedly this shift looked identical to one starting in a
healthy app.

The stability axis on app-update:
- The analytics plane (Omnissa Intelligence, Crashlytics-class SDKs and
  peers) reports crash_count + stability_window_hours — validated
  evidence, never reinterpreted. The CALLER poses maxCrashesInWindow.
  No threshold is tuned: the source counted, the operator bounded, the
  fabric graded.
- Over the bound -> step_up APP_UNSTABLE — the remedy is a challenge and
  a device swap, never a block.
- Unposed -> unassessed, carried visibly, never foreclosing the grant.
  Posed-but-unanswerable (count or window missing — a count without its
  window is uninterpretable) -> unknown, raises. Garbled figures or a
  zero-hour window -> malformed.
- The grant widens to current + managed + clean + stable-if-posed; the
  enumeration re-pins at 216 normalized states with exactly 3 grants.

proof:app-update 52 -> 57 checks (both doc count mentions updated in the
same commit, per the standing lesson); mutation guard 36 mutations, 32
killed, 4 labeled-inert, 0 survivors.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…o catch (fourth base reconciliation)

SignalGrid_Alpha moved again (e1891f7 -> d35160f: the absent-collection
law from three live vendors, the live Traccar location lane, and the macOS
harness web-build fix). The merge itself was conflict-free; the red CI run
on the PR merge ref was the ci<->preflight drift gate correctly refusing
the base's proof:absent-collection, which ran in preflight but in NO
workflow — the same one-list registration gap as the webhooks and
emit-gate proofs before it. Registered in review-hub-ci.yml beside its
siblings; the proof passes 10/10 here and the drift gate reads clean.
Their live-location proof stays out of CI like the other live lanes (it
needs a running Traccar; documented in the zero-cost matrix).

Full battery on the merged tree: typecheck, invariants, connector
discipline, counts, figures, orphans, mcp-surface, pagination guard,
live-sync (manifest regenerated) — all green.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…row-17 queue fully closed)

The Continuous Access Evaluation half the row-17 research report positioned:
telling COOPERATING applications that a session's context changed —
presence expired, posture dropped, a credential changed, assurance moved.
OpenID CAEP on the Shared Signals Framework is the standard shape; this
family is its outbound path, under emitter discipline from birth.

- format.ts builds an UNSIGNED SET claims set (RFC 8417): the five OpenID
  CAEP event types as an allowlist keyed by their URIs, reason codes
  traveling as reason_admin so the auditable why crosses with the event.
  TWO honesty rules pinned: no string in the output is even JWT-shaped —
  signing needs keys this public repository must not hold, so producing a
  transmissible SET is the injected transport's job in a private
  deployment; and the subject is an opaque PSEUDONYM with the same
  raw-identifier tripwire as the gateway projector (an email-shaped
  subject refuses). iat/event_timestamp from SUPPLIED instants, jti from
  an upstream decision id — no clock, no randomness.
- resolve.ts is the same four-clause unanimous gate as the other five
  families (tier + flag + CAEP_EMITTER_TOKEN + injected transport), with
  the fixture emitter whose records carry a literal delivered:false.

proof:caep-events 17/17, registered in CI + preflight + counts + the
figure guard (the registry drift gate caught the missing figure-guard
registration — fixed in this commit). proof:emitter-discipline grows
43 -> 51 with families=6; catalog prose and counts updated in the same
commit; manifest regenerated. Mutation guard: 6/6 killed, 0 survivors.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
An educational MCP explainer (AI app = client, tool provider = server,
USB analogy). Every decision-relevant element names an existing surface:
the fabric already SPEAKS MCP (artifacts/mcp-server + the on-device
signalgrid-mcp tools, drift-gated by check-mcp-surface) and already
GOVERNS the client side the poster stops short of — agent-identity (who
is acting), agent-behavior (whether the action shows judgment),
oauth-consent (the workload credential), token-binding (replayability).
Inline MCP-call brokering stays the agent-gateway category the fabric
consumes as a bridge (row-2 precedent).

Docs guards re-run green: proof-counts, doc-orphans, sync manifest +
live-sync (no manifest drift — the row carries no counts).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…nac rework

The base lane's new guard (the third face of the absent-collection law)
was written against its own tree. On the merged tree two things differ:

- nac/store.ts no longer has lookupEndpoint at all — this branch's nac
  discipline moved the endpoint read to ./index behind the gate, so the
  exemption was stale and is removed.
- deviceResolver.ts:resolveFromNAC is this branch's deliberate twin of
  the already-exempted resolveFromUEM: the catch reports the fault via
  onFault (console.warn by default, injectable) and returns null so
  resolution falls through to the next source. It gets the twin
  exemption, and both reasons now state the onFault reporting.

Full battery green on the merged tree: typecheck, review:invariants,
connector-discipline, ci-preflight-sync, proof-counts, doc-orphans,
mcp-surface, guard-registries, pagination-truncation, live-sync, the
new preflight-ci-parity and decision-port-parity guards, and the
absent-collection / vuln-scan / nac / simulator proofs.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…ow 21

From the PACS-modernization essay (PKOC/Aliro, cards + mobile + biometrics
coexisting, "evolutionary not revolutionary"). The unearned affirmative it
exposed: pacs-access recorded the credential MODALITY but nothing about the
TECHNOLOGY the reader verified — a cloned 125 kHz prox read and a PKOC/Aliro
cryptographic credential arrived as the same undifferentiated "granted".

The axis: the PACS reports what the reader verified (cryptographic
challenge-response vs replayable static_identifier; garbled → unknown); the
CALLER poses a per-workflow floor. Below it → step_up CREDENTIAL_BELOW_FLOOR —
a stronger challenge, deliberately never restrict/deny: the legacy estate is
graded, not condemned, and modernization stays the operator's pace. A posed
floor the PACS could not answer → step_up (silence is not a cryptographic
credential). Unposed → unassessed, forecloses nothing; posing the
static_identifier floor is itself a legitimate recorded choice.

proof:pacs-access 68 → 79 checks; the grant-safety enumeration widens to
24,300 combinations and is graded twice — unposed (the axis is invisible to
the grant) and under a posed cryptographic floor (the allow path additionally
demands the cryptographic read, exactly one third of the unposed grants; 0
mismatches both ways). Docs: catalog entry + figures, API_SIGNAL_DISCOVERY and
SELF_REVIEW enumeration figures, ledger row 21 (Omnissa Workspace one-pager
assessed in the same row: COVERED — workspace/UEM category the fabric
consumes; every element names an existing surface).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…built nothing

The vendor's own "hybrid isn't temporary" essay lands as external validation
of row 21, built hours earlier: pacs-access privileges no credential modality
(hybrid is the fabric's resting state), and the caller-posed per-workflow
technology floor is the blog's "each door moves at its own pace" made
mechanical. BYOD privacy refusers = the uem ownership axis; phone-free zones
stay the facility's call (the fabric reads the outcome); lifecycle
provisioning/revocation = access-governance + entitlement-binding +
link-usability with the cross-plane desync landing via composition — and the
verified HID Origo row in API_SIGNAL_DISCOVERY already names that revocation
lag as its "why it matters". Drivers page and github.com/hidglobal noted as
vendor references; no dependency taken.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…e row 23)

From the HID DigitalPersona Web Client API docs (an MFA execution stack:
fingerprint/face/card/OTP/FIDO ceremonies via a local device-access agent).
Ceremony execution stays with the HOST app per the embedded-UX law, and
completed ceremonies were already graded — but the docs exposed the session's
defect class surfacing in the REMEDY: the fabric chooses "step_up, never
block" everywhere, and every such verdict silently assumed the challenge
could be ANSWERED on this device by this worker. A step-up posed to a device
with no enrolled method, no authenticator, or a dead local agent is a deny
wearing a step_up label, and nobody chose it.

The family: a bridge reports per-method standing — credential ENROLLED for
the worker, AUTHENTICATOR present on the device, local CLIENT healthy — over
a six-method allowlist. The caller POSES which methods its workflow's
step-up would accept. READY is earned (posed + clean + bridge-affirmed + one
accepted method positively affirmed on all three axes); UNANSWERABLE is an
affirmative claim (EVERY accepted method positively broken — silence never
declares a dead end) and alerts at operator scale: fix enrollment or swap
the device before the doomed challenge; blind spots monitor; unposed is
unassessed and forecloses nothing; an unreadable pose refuses. Deliberately
never a step_up of its own — a dimension about challenge efficacy must not
prescribe the challenge it just reported impossible.

Gated from birth (tier + flag + CHALLENGE_CAPABILITY_ACCESS_TOKEN + injected
transport; fixture default). Fused as the 33rd composable kind; incidents
route to the Identity & Access queue. proof:challenge-capability 34/34 incl.
the exhaustive 81-cell single-method standing sweep (answerable in exactly
the all-affirmed cell); registered in preflight + CI + the mutation guard;
catalog entry + ledger row 23; sync manifest regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
… sweep clean)

The first sweep found four real-no-test gaps, all in the defensive shell:
a null report read as a quietly-empty clean read; a non-boolean
bridge_reachable coerced to silence instead of malformed; a non-iterable
methods slot that would have thrown instead of grading malformed; and a
hostile report whose key enumeration throws (Proxy ownKeys trap) trusted
instead of refused. Each now has a pinning check; the sweep reads 23
killed / 0 survivors, and the two doc mentions carry the live count.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
From DigitalPersona v4.4.0's what's-new: multiple device-bound passkeys so
"multiple people can access a shared account using their own unique
devices." Under that pattern the fabric's own core scenario had no honest
representation: sso-session's subject comparison is ground truth on an
individual account, but on a SHARED account (a nurse-station principal) the
subject IS the account — so a legitimate shared session either escalated as
a false leftover on every tap, or operators smuggled the account name into
expectedSubject and silently surrendered person-level attribution. Both are
unearned states.

The axis: new accountScope and credentialHolder wire facts. On a shared
account attribution moves to the CREDENTIAL level — a holder-matching
credential corroborates a first-class bound session; someone else's
credential is mismatched (live -> escalate; the leftover rule survives the
pattern); a live shared session with NO credential attribution is its own
posture, unattributed_shared -> step_up ("the account authenticated" is not
"this person is identified"; re-auth as yourself, never a lockout). The
credential-holder comparison only ever downgrades, on every scope; a
garbled scope is treated as individual so the subject rule stays
authoritative; the evaluator refuses the holderless-shared grant even on an
unnormalized "bound" label.

proof:sso-session 78 -> 88 checks; enumeration widened 768 -> 4,608
combinations with 0 mismatches. Catalog + SELF_REVIEW figures updated;
ledger row 24; sync manifest regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…hape rule

CI's docs↔proof figure guard checks every comma-formatted number in a
section that names a registered proof, and the ledger section names many.
"4,608" in row 24 is sso-session's figure, not caep-events' or
facility-trust-graph's, and the "768 → 4,608" arrow marker cannot exempt
it (the marker regex's word boundary never anchors on an arrow). Plain
digits are the guard's documented out-of-shape form for prose like this;
the proof still pins the real figure.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
From the "Top SSE Companies" ranking poster. The category itself is one the
fabric consumes, not competes with; the genuine gap was on the egress
plane: network-nac deliberately stops at LAN admission and edr-threat
grades the endpoint agent, so nothing asked whether a device's
internet/SaaS traffic was actually traversing the deployment's MANDATED
SWG/CASB/ZTNA edge. A frontline device with the SSE client bypassed,
toggled off, or never installed browsed raw while every console read
"protected" — the unearned affirmative on the egress plane, and the SSE's
own device API (Zscaler Client Connector, Netskope client inventory,
GlobalProtect are the reference shapes) already knows the difference.

The family: client state as a trusted allowlist (tunneled / bypassed /
disabled / not_installed) plus the edge's own affirmative traffic
observation — a "tunneled" claim is corroborated, never believed
(contradicted -> step_up + critical; unconfirmable -> unknown raises).
The caller poses whether the edge is mandated; unposed is unassessed and
forecloses nothing. Disabled and never-installed alert at operator scale
(the setup-bypassed precedent); a bypass is visible and steps up (a bypass
rule can be deliberate policy); silence on a mandated path steps up.

Gated from birth (tier + flag + SSE_EGRESS_ACCESS_TOKEN + injected
transport; fixture default). Fused as the 34th composable kind; routed
with the network plane. proof:sse-egress 30/30 incl. the exhaustive
45-cell standing sweep in both poses (protected in exactly one cell,
unposed always quiet); registered in preflight + CI + the mutation guard;
catalog entry + ledger row 25; sync manifest regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The first sweep found two real-no-test gaps in the defensive shell: a
report whose property getter throws read as a silent all-unknown default
instead of malformed, and a non-boolean bridge_reachable assertion coerced
to silence. Both now have pinning checks; the sweep reads 14 killed / 0
survivors, and the doc mentions carry the live count.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
catalog filed — intake row 26

The owner supplied a source-linked Physical Access Control API catalog
(61 vendor entries with access classifications, 24 OSS resources, 10
standards, P0-P2 SignalGrid sequencing with per-tier minimum signal
sets, and an implementation boundary). Filed verbatim as
docs/inspiration/PACS_VENDOR_API_CATALOG.md (row-17 precedent) — the
durable adapter roadmap; no dependency taken; the stated boundary is
already fabric law item for item.

Auditing the prescribed minimum signal set against pacs-access found two
unmodeled facts:

- EVENT TIMESTAMP: a badge-in from hours ago graded exactly like a
  current one — the row-11 recency doctrine had never reached the door.
  Now a caller-posed maxEventAgeSeconds + reference instant (no clock in
  any decision path): older-than-bound -> stale_evidence/step_up;
  posed-but-unanswerable (unreadable instants, a future-dated event — a
  contradiction never fresh — or a garbled pose) -> unknown raises;
  boundary inclusive; unposed forecloses nothing.
- READER/CONTROLLER HEALTH, distinct from bridge reachability: the
  bridge can answer perfectly about a door whose controller is offline,
  so the entry evidence may be blind. Explicit offline -> step_up,
  explicit degraded -> visible monitor; affirmative-only, so an
  unreported health never forecloses and pre-axis bridges keep their
  behavior.

proof:pacs-access 79 -> 92 checks; the grant-safety enumeration widens
to 97,200 combinations, still graded in both poses with 0 mismatches.
Catalog/discovery/self-review figures updated; ledger row 26; sync
manifest regenerated; figure guard verified green locally.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
…, part 1)

The ten-sheet workbook (135 API/platform entries across MDM/UEM/EMM, RMM,
DEX, security/telemetry and platform standards; 45 open-source resources;
31 SignalGrid repository mappings; launch sequencing; the canonical
normalized endpoint signal set; verified 2026-07-31), extracted
sheet-by-sheet to markdown and filed verbatim with a provenance preamble —
the row-26 precedent. Reference material only: no dependency taken, the
workbook's own boundary rules match fabric law, and its strategic
conclusion is adopted as stated (one Microsoft-backed shared-device
workflow first; breadth from design-partner demand; the legacy DEV
adapters are historical references whose write operations must not be
copied forward).

The ledger row lands with part 2 once the signal-set coverage audit
completes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3GJ2Fs8sVppPgzuJdnNLn
The owner's endpoint catalog carries a canonical normalized endpoint
signal set. A six-agent adversarial workflow audited it fact-by-fact
against the code on disk: 43 signals covered with file+type evidence,
and two gaps survived adversarial refutation.

Gap 1, BUILT here: joiner/mover context. access-governance's own comment
calls AccessAccountStatus "the runtime slice of JML", and it was
leaver-only — nothing anywhere carried new-hire or role-transfer state,
so a mover's stale pre-transfer entitlements could only surface as
generic drift. The new lifecycleStage axis (new_hire / established /
recent_transfer; affirmative-only, unreported forecloses nothing) gets
its teeth from composition: a recent transfer with over-privileged or
recert-due entitlements -> mover_stale_entitlement/ALERT with its own
queue-readable reason; a new hire already holding standing privilege ->
joiner_over_provisioned/ALERT (over-provisioned at birth); a clean
transition -> visible monitor, never a grant and never a nag; a leaver
still outranks everything (worst-wins pinned). Enumeration widened from
4,500 to 18,000 combinations, 0 mismatches; proof:access-governance
57 -> 65 checks.

Gap 2, QUEUED with evidence in docs/BUILD_BACKLOG.md: normalization-
version stamping — nothing records which version of the normalizer
produced a normalized record (only policyVersion is stamped). Its own PR,
because it touches the core decision/API contract.

Ledger row 27; catalog/self-review figures updated; figure guard verified
green locally.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants