instar 1.3.519 → 1.3.521

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,7 @@ import { checkEli16Overview, MIN_ELI16_CHARS } from './eli16-overview-check.mjs'
32
32
  import { verifyProposalDerivedRunbooks } from '../skills/instar-dev/scripts/verify-proposal-derived-runbook.mjs';
33
33
  import { classifyTier, decideRequirementSet } from './lib/classify-tier.mjs';
34
34
  import { recognizeConvergence } from './lib/convergence-recognition.mjs';
35
+ import { isOperatorSurfaceFile, artifactAddressesOperatorSurfaceQuality } from './lib/operator-surface.mjs';
35
36
 
36
37
  // Report-Backed Converging Audit (docs/specs/CONVERGING-AUDIT-DEFAULT.md, Part B).
37
38
  // The precommit reads NO config file and runs pre-compile, so it cannot import
@@ -884,6 +885,7 @@ if (!promotionGateResult.ok) {
884
885
  // ─── Pass ────────────────────────────────────────────────────────────────
885
886
 
886
887
  assertFrameworkGenerality(inScopeFiles, validTrace.trace);
888
+ assertOperatorSurfaceQuality(staged, validTrace.trace);
887
889
 
888
890
  console.error(
889
891
  `[instar-dev-precommit] OK — trace ${path.basename(validTrace.entry.file)} covers ${inScopeFiles.length} in-scope file(s), artifact ${validTrace.trace.artifactPath} verified, spec ${spec} is converged + approved` +
@@ -930,6 +932,49 @@ function assertFrameworkGenerality(inScopeFiles, trace) {
930
932
  }
931
933
  }
932
934
 
935
+ // Operator-Surface Quality review gate (docs/STANDARDS-REGISTRY.md →
936
+ // "Operator-Surface Quality", CMT-1434). A change touching an operator surface
937
+ // (dashboard renderers/markup, approval pages, grant/secret forms) must answer
938
+ // the operator-surface-quality question in the side-effects artifact IN WRITING:
939
+ // does the surface lead with its primary action, expose zero raw internals,
940
+ // de-emphasize destructive actions, and work at phone width? A "reachable but
941
+ // bad" surface passes Mobile-Complete and still fails the operator (the
942
+ // 2026-06-12 "abysmal" Mandates-grant-form lesson) — this makes the quality
943
+ // question unskippable. Operator-surface files are NOT in the gate's inScope set
944
+ // (they live under dashboard/), so we scan the full STAGED set, not inScopeFiles.
945
+ // Scoped tight so a non-surface commit pays nothing. Companion to
946
+ // assertFrameworkGenerality.
947
+ function assertOperatorSurfaceQuality(stagedFiles, trace) {
948
+ const touched = (stagedFiles || []).filter(isOperatorSurfaceFile);
949
+ if (touched.length === 0) return;
950
+ const artifactRel = trace.sideEffectsPath || trace.artifactPath;
951
+ if (!artifactRel) return; // a missing artifact is already blocked upstream
952
+ const artifactAbs = path.resolve(ROOT, artifactRel);
953
+ if (!fs.existsSync(artifactAbs)) return;
954
+ const content = fs.readFileSync(artifactAbs, 'utf8');
955
+ // The artifact must engage the operator-surface-quality question (the §6b
956
+ // section seeded by skills/instar-dev/templates/side-effects-artifact.md).
957
+ if (!artifactAddressesOperatorSurfaceQuality(content)) {
958
+ blockCommit(
959
+ touched,
960
+ [
961
+ 'Operator-Surface Quality review gate:',
962
+ ` ${touched.join(', ')} change an OPERATOR SURFACE,`,
963
+ ' but the side-effects artifact never answers the operator-surface-quality',
964
+ ' question — a surface can be phone-reachable (Mobile-Complete) and still be',
965
+ ' unusable (the 2026-06-12 "abysmal" Mandates grant-form lesson, CMT-1434).',
966
+ '',
967
+ ' Add the "## 6b. Operator-surface quality" section and answer in writing:',
968
+ ' does the surface (1) lead with its primary action, (2) expose zero raw',
969
+ ' internals as primary content, (3) de-emphasize destructive actions, and',
970
+ ' (4) read in plain language at phone width? (Standard:',
971
+ ' docs/STANDARDS-REGISTRY.md → "Operator-Surface Quality". Template:',
972
+ ' skills/instar-dev/templates/side-effects-artifact.md §6b.)',
973
+ ].join('\n'),
974
+ );
975
+ }
976
+ }
977
+
933
978
  function blockCommit(files, reason) {
934
979
  console.error('');
935
980
  console.error('╔════════════════════════════════════════════════════════════════════╗');
@@ -1125,6 +1170,7 @@ function enforceTier1(trace, traceFile) {
1125
1170
  }
1126
1171
 
1127
1172
  assertFrameworkGenerality(inScopeFiles, trace);
1173
+ assertOperatorSurfaceQuality(staged, trace);
1128
1174
 
1129
1175
  console.error(
1130
1176
  `[instar-dev-precommit] OK (Tier 1) — trace ${traceName} covers ${inScopeFiles.length} in-scope file(s), ` +
@@ -0,0 +1,54 @@
1
+ /**
2
+ * operator-surface.mjs — pure decision logic for the Operator-Surface Quality
3
+ * review gate in the instar-dev commit gate (docs/STANDARDS-REGISTRY.md →
4
+ * "Operator-Surface Quality", CMT-1434).
5
+ *
6
+ * Two pure predicates, extracted so the gate's load-bearing decisions are
7
+ * unit-testable without git/fs mocking (the classify-tier.mjs pattern):
8
+ *
9
+ * - isOperatorSurfaceFile(path): is this staged file an operator surface — a
10
+ * dashboard renderer/markup file, an approval page, or a grant/secret form?
11
+ * - artifactAddressesOperatorSurfaceQuality(content): does the side-effects
12
+ * artifact engage the operator-surface-quality question in writing?
13
+ *
14
+ * SIGNAL-FREE: these never block; the gate (instar-dev-precommit.js) reads them
15
+ * and decides. Keeping them pure means both sides of every boundary are pinned
16
+ * by tests (Testing Integrity → semantic-correctness).
17
+ */
18
+
19
+ /**
20
+ * An operator surface is anything a person uses to authorize, decide, or act:
21
+ * - dashboard renderer / markup files (dashboard/*.js, dashboard/*.html), AND
22
+ * - approval pages / one-time-approval links / secret-drop forms (a file whose
23
+ * basename starts with approval / operator-approval / secret-drop).
24
+ * Build/test/spec siblings (*.test.js, *.spec.js) are NOT surfaces.
25
+ */
26
+ export const OPERATOR_SURFACE_RE =
27
+ /^dashboard\/.+\.(?:js|html)$|(?:^|\/)(?:approval|operator-approval|secret-drop)[^/]*\.(?:js|html|ts)$/i;
28
+
29
+ /**
30
+ * True when a staged file path is an operator surface.
31
+ * @param {string} file repo-relative path
32
+ * @returns {boolean}
33
+ */
34
+ export function isOperatorSurfaceFile(file) {
35
+ const f = String(file ?? '');
36
+ if (!f) return false;
37
+ // A test/spec file that happens to live under dashboard/ or match the approval
38
+ // basename is NOT itself an operator surface — it's a guard for one.
39
+ if (/\.(?:test|spec)\.(?:js|ts|mjs)$/.test(f)) return false;
40
+ return OPERATOR_SURFACE_RE.test(f);
41
+ }
42
+
43
+ /**
44
+ * The artifact engages the operator-surface-quality question when it carries the
45
+ * §6b heading/phrase (seeded by side-effects-artifact.md). The gate's job is to
46
+ * ensure the question is structurally PRESENT for every operator-surface change —
47
+ * an agent that deletes/skips the section (or writes a bespoke artifact omitting
48
+ * it) is blocked. (Same strength as the framework-generality gate.)
49
+ * @param {string} content the side-effects artifact text
50
+ * @returns {boolean}
51
+ */
52
+ export function artifactAddressesOperatorSurfaceQuality(content) {
53
+ return /operator[- ]surface quality/i.test(String(content ?? ''));
54
+ }
@@ -101,6 +101,21 @@
101
101
 
102
102
  ---
103
103
 
104
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
105
+
106
+ **REQUIRED whenever this change touches an operator surface** — a dashboard renderer/markup file (`dashboard/*.js`, `dashboard/*.html`), an approval page, or a grant/revoke/secret-drop form. The pre-commit gate (`scripts/instar-dev-precommit.js`) refuses the commit if this section is missing when an operator-surface file is staged. Reachable-but-bad still fails the operator (the 2026-06-12 "abysmal" Mandates-grant-form lesson, CMT-1434): Mobile-Complete asks *can they do it from a phone?*; this asks *is it good when they do?*
107
+
108
+ Answer each in writing (a "no" or unjustified "n/a" blocks the commit):
109
+
110
+ 1. **Leads with the primary action?** The thing the operator came to do is visible and actionable on arrival — never collapsed behind a toggle, below the fold, or after explanatory prose.
111
+ 2. **Zero raw internals as primary content?** No JSON blobs, fingerprints, UUIDs, hashes, or enum/slug values shown as headline content — only human language; identifiers de-emphasized as support metadata when genuinely needed.
112
+ 3. **Destructive actions de-emphasized?** Revoke/delete/stop is visually quieter than the constructive primary action and never appears above it.
113
+ 4. **Plain language + phone width?** Labels/states read the way a non-engineer would say them; verified at phone width — real tap targets, readable type, no horizontal scroll, no truncated table hiding the answer.
114
+
115
+ [Specific findings per criterion. "Grant form renders open as the card's primary block (mnd-grant-block); Revoke demoted to a collapsed mnd-revoke-details below it; bounds/fingerprints humanized, raw ids kept only on the muted 'For support' line; audit table stacks at ≤640px." If this change touches NO operator surface, state: "No operator surface — not applicable."]
116
+
117
+ ---
118
+
104
119
  ## 7. Multi-machine posture (Cross-Machine Coherence)
105
120
 
106
121
  **When this agent runs on MORE THAN ONE machine, what is this feature's posture?**
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-13T09:03:12.807Z",
5
- "instarVersion": "1.3.519",
4
+ "generatedAt": "2026-06-13T09:25:14.093Z",
5
+ "instarVersion": "1.3.521",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,69 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ The generic machinery that future cross-machine memory stores (preferences, relationships, learnings, …) will plug into. This step builds the reusable substrate ONLY — it adds no concrete store kind (that lands with the first store, WS2.1).
9
+
10
+ - **Replicated-record envelope** (`src/core/ReplicatedRecordEnvelope.ts`) — the fields every replicated change carries on top of its store-specific data: `recordKey`, the `HybridLogicalClock` stamp (`hlc`), a `put`/`delete` `op`, the author machine (`origin`), and the single prior stamp the author had already merged for that key (`observed` — the last-writer-witness; absent ⇒ "no prior witness" ⇒ flag-on-conflict, the safe direction). A strict, parameterizable validator mirrors the coherence-journal typed-schema discipline (rejects free text, drops + counts unknown fields, jails path-shaped fields, validates the HLC fields). A `ReplicatedKindRegistry` (ships EMPTY) is the registration mechanism each store will use.
11
+ - **Flag-gated emission (dark per store)** — a store emits its kind only when `multiMachine.stateSync.<store>.enabled` is on (default false). When off, no journal traffic — a strict no-op.
12
+ - **Flag-coherence-gated emission** — a kind is forwarded to a peer ONLY when that peer advertises (`seamlessnessFlags.stateSyncReceive`) it can receive it. Emitting a new kind to an older peer would be silently dropped by the applier — the named data-loss skew mode this gate prevents. The per-peer decision is correct for N peers; a boot-time pool-flag-coherence check surfaces a mixed-flag pool ONCE, coalesced.
13
+ - **Config + invariants** (`src/core/stateSyncConfig.ts`, `ConfigDefaults.ts`, `types.ts`) — the foundation-level `multiMachine.stateSync` knobs (journal budget, the §3.4 HLC drift ceiling, snapshot-cache bounds), validated at startup by `validateStateSyncInvariants()` (an out-of-range value is REJECTED, not silently coerced), backfilled to existing agents via the add-missing migration path.
14
+
15
+ Pure MECHANISM, dark by default. The only two refusal surfaces are at the receive door (the validator rejects malformed data) and the emission door (don't forward to a non-advertising peer); neither blocks a user-initiated action. A single-machine install is a strict no-op.
16
+
17
+ Two cohesive changes (topic 22367, CMT-1434):
18
+
19
+ 1. **Mandates dashboard tab redesign.** The Mandates card now leads with the Grant
20
+ form as its primary, always-open block; Revoke is demoted to a quiet, collapsed
21
+ control beneath it. Raw JSON bounds, agent fingerprints, and scope slugs are
22
+ replaced with a plain-language summary sentence; identifiers survive only on a
23
+ muted "For support" line. Existing grants read as plain English ("Adam Admin can
24
+ deploy to production until 9:37 PM — authorized by you"), and the decision-audit
25
+ table stacks into labelled rows at phone width so the reason column is never
26
+ truncated. Renderer + markup + CSS only — the mandate API, payloads, and
27
+ PIN-never-retained discipline are unchanged. Ships via `dashboard/` static
28
+ serving, so it reaches every agent through the normal update path.
29
+
30
+ 2. **New constitutional standard: Operator-Surface Quality** (sibling to
31
+ Mobile-Complete Operator Actions) in `docs/STANDARDS-REGISTRY.md`. Where
32
+ Mobile-Complete asks "can the operator do this from a phone?", this asks "is it
33
+ actually good when they do?" — lead with the primary action, zero raw internals,
34
+ de-emphasized destructive actions, plain language, phone-width layout. It lands
35
+ with teeth: a new operator-surface-quality question in the instar-dev
36
+ side-effects review template, and a pre-commit gate
37
+ (`scripts/instar-dev-precommit.js`) that blocks any commit touching an operator
38
+ surface unless the review answers it. The standard names that gate, so the
39
+ Standards-Enforcement-Coverage audit classifies it as an enforced gate.
40
+
41
+ ## What to Tell Your User
42
+
43
+ None — internal substrate (no user-facing surface). The replicated cross-machine memory stores that this foundation enables ship later, store by store, each with its own announcement when its user-facing surface (conflict viewing, rollback) lands.
44
+
45
+ - **A cleaner Mandates screen on your phone**: "I gave the Mandates tab a real
46
+ overhaul. The action you actually came to do — granting someone permission — is
47
+ now front and centre, the risky Revoke button is tucked quietly out of the way,
48
+ and the screen reads in plain English instead of raw data. The history list also
49
+ finally fits your phone without cutting off the reason column."
50
+ - **A new quality bar for everything you touch**: "I also turned this into a
51
+ standing rule for myself: any screen you use to approve or decide something has to
52
+ be genuinely good to use on your phone, not just technically reachable. I added an
53
+ automatic check that holds me to it whenever I build one of those screens."
54
+
55
+ ## Summary of New Capabilities
56
+
57
+ None — internal change. This is the reusable substrate the first concrete replicated store (the cross-machine preferences pool, WS2.1) will register a journal kind onto; it exposes no new endpoint, config a user would set, or behavior a user would notice until a store turns its flag on.
58
+
59
+ | Capability | How to Use |
60
+ |-----------|-----------|
61
+ | Redesigned Mandates tab (humanized, mobile-first) | Open the Mandates tab in the dashboard |
62
+ | Operator-Surface Quality standard | docs/STANDARDS-REGISTRY.md (constitution) |
63
+ | Operator-surface-quality pre-commit gate | automatic during instar-dev work |
64
+
65
+ ## Evidence
66
+
67
+ Tier-1 unit tests in `tests/unit/ReplicatedRecordEnvelope.test.ts` (46 tests, all green) cover both sides of every boundary: the validator (valid put/delete; missing recordKey/hlc; malformed hlc; observed present-valid / present-malformed-rejected / absent-legal; unknown-field-dropped-and-counted; path-shaped field jailed; non-object/free-text rejected; store-schema rejection), the registry (empty default, unregistered-kind absent, conflict-throws, idempotent re-register), flag-gated emission (enabled=false ⇒ no emission; enabled=true ⇒ emits), flag-coherence (advertising ⇒ emit; non-advertising ⇒ withhold + surface; 3+-peer mix ⇒ per-peer correctness + ONE coalesced surface), `validateStateSyncInvariants` (maxDriftMs floor/ceiling/in-range), and a wiring-integrity case for the advert self-report (driven by registered+enabled stores, never a hardcoded true).
68
+
69
+ Gate-parity (run in the worktree): `tsc --noEmit` clean; `no-silent-fallbacks` = 471 (= BASELINE, no bump — the new files carry no un-tagged silent catches); `docs-coverage --check` PASS (class floor 55% held — `ReplicatedRecordEnvelope` documented in `multi-machine.md` + `under-the-hood.md`); `feature-delivery-completeness` PASS; `lint-dev-agent-dark-gate` PASS (the config line-map recomputed for the +17 cartographer shift); focused suites `CoherenceJournal*` / `JournalSyncApplier*` / `CoherenceJournalReader` + the new test all green.
@@ -0,0 +1,104 @@
1
+ # Side-Effects Review — Replicated-store foundation Step 2 (journal-kind tagging + flag-gated emission)
2
+
3
+ **Version / slug:** `multi-machine-replicated-store-foundation-step2-journal-kind`
4
+ **Date:** `2026-06-13`
5
+ **Author:** `Echo (instar-dev subagent)`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Step 2 of the multi-machine replicated-store foundation builds the GENERIC machinery that the concrete replicated stores (preferences, relationships, learnings, …) will layer a journal kind onto — it adds NO concrete store kind itself. New module `src/core/ReplicatedRecordEnvelope.ts` defines (A) the replicated-record envelope type (`recordKey`, `hlc`, `op`, `origin`, `observed`) plus a strict, parameterizable validator that mirrors the CoherenceJournal typed-schema discipline (reject free text, drop+count unknown fields, jail path-shaped fields, validate HLC fields); (B) a `ReplicatedKindRegistry` (ships empty — the registration mechanism, no concrete kind); (C) flag-gated emission (`isStoreEmissionEnabled` — a store emits only when `multiMachine.stateSync.<store>.enabled`, default false); and (D) flag-coherence-gated emission (`shouldEmitToPeer` + `checkPoolFlagCoherence` — never forward a kind to a peer that does not advertise it can receive it, N-peer-correct, one coalesced surface). New module `src/core/stateSyncConfig.ts` resolves + validates the foundation-level `stateSync` knobs (`validateStateSyncInvariants`, the §3.4 maxDriftMs clamp). Config defaults (`ConfigDefaults.ts` `multiMachine.stateSync`), a `StateSyncConfig` type + the `stateSyncReceive` advert flag (`types.ts`), and the server wiring (startup invariant assertion, the empty registry, the advert self-report, the boot-time coherence check) complete it. The change is pure MECHANISM, dark by default; a single-machine install is a strict no-op.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `validateReplicatedEnvelope` (RECEIVE door) — **add** — rejects malformed replicated-record data before it can enter a stream. The only "block" surface, and it blocks DATA, never a user action.
15
+ - `isStoreEmissionEnabled` / `shouldEmitToPeer` (EMISSION door) — **add** — withholds emitting a store's kind when the store is dark or a peer can't receive it. Withholds JOURNAL TRAFFIC, never a user action.
16
+ - `validateStateSyncInvariants` (startup config gate) — **add** — rejects an out-of-range foundation knob at boot (loud, not silent), mirroring `validateSeamlessnessInvariants`.
17
+ - `seamlessnessFlags.stateSyncReceive` (capability advert) — **add** — self-reported per-store receive capability in the capacity heartbeat.
18
+
19
+ ---
20
+
21
+ ## 1. Over-block
22
+
23
+ The validator rejects: a non-object/array; a missing/empty/oversized/path-shaped `recordKey`; a malformed/missing `hlc`; an `op` outside `{put,delete}`; a missing/empty/path-shaped `origin`; a PRESENT-but-malformed `observed`. These are all genuinely-malformed records — there is no legitimate replicated record that fails them. The one deliberate design choice that could look like over-block is the path-shape jail on `recordKey`/`origin`: a store whose primary key legitimately contains a `/` would be rejected. This is intentional (§4 "jail any path-shaped field") — the envelope carries identifiers, not paths, and a concrete store (WS2.1) must choose a non-path key. ABSENT `observed` is explicitly NOT rejected (legal ⇒ "no prior witness" ⇒ flag-on-conflict, the safe direction). No user-message surface exists, so no user input is over-blocked.
24
+
25
+ ---
26
+
27
+ ## 2. Under-block
28
+
29
+ The validator does NOT semantically validate store-specific fields beyond delegating to the supplied `StoreFieldSchema` — a store that supplies a permissive schema could let through store data the envelope can't reason about. This is by design (parameterizable substrate; the store owns its field discipline) — BUT the envelope's own LOAD-BEARING fields are now protected against a buggy/hostile store by construction (post-adversarial-review hardening, findings #1–#3):
30
+ - **A store can NEVER override a reserved envelope field on `data`.** `validateReplicatedEnvelope` strips every `RESERVED_ENVELOPE_FIELDS` key (`op`/`recordKey`/`hlc`/`origin`/`observed`) from the store's returned object (counting each as a dropped field) and spreads the store fields FIRST, the VALIDATED envelope fields LAST — so `data.op/recordKey/hlc/origin/observed` always equal the validated `envelope.*`. The earlier `...storeFields`-last ordering let a store's un-validated, un-jailed copy win on a key collision; that divergence is closed (finding #1).
31
+ - **A schema cannot claim a reserved field name.** `ReplicatedKindRegistry.register()` throws (a wiring-time programmer error, like the conflict throws) if `knownFields` intersects `RESERVED_ENVELOPE_FIELDS` — the reserved constant is now ENFORCED, not merely documented for self-check (finding #2).
32
+ - **Reusable store-field path-jail.** The §4 path-jail is now reusable machinery for store fields too: a store declares `pathSensitiveFields` (auto-jailed by the validator before its `validate()`, rejecting the whole record with `store-field-path-shaped` + a jail-counter bump) and/or calls the exported `jailStoreStringField(value, ctx)` helper, which feeds the SAME `jailRejects` counter via the new `StoreValidateContext.countJailReject` (finding #3). Structure > Willpower instead of every store re-implementing (and possibly forgetting) the jail.
33
+
34
+ The emission gate's flag-coherence check trusts the peer's advert: a peer that advertises `stateSyncReceive[store]=true` but is actually unable to apply the kind would still be sent to. This is the same trust model as the existing `ws11DeliverReceive`/`ws12DrainReceive` adverts — the advert is self-reported from machinery presence, and a lying peer is out of scope for this foundation (the applier's per-record validation at the RECEIVE door is the backstop). The validator never throws on bad data (it counts + rejects); a thrown error is reserved for a programmer error (a registration conflict OR a reserved-field knownFields collision), which is the correct loud-fail.
35
+
36
+ ---
37
+
38
+ ## 3. Level-of-abstraction fit
39
+
40
+ Correct layer. This is a low-level, deterministic PRIMITIVE (a validator + a registry + pure decision functions) with no reasoning and no context — exactly right for a substrate that runs on every replicated record. It does NOT hold smart-gate authority; it produces typed verdicts (`EnvelopeValidationResult`, `shouldEmitToPeer` decision, `PoolFlagCoherenceResult`) that the eventual consumers (the concrete stores, the apply path) act on. It USES the existing lower-level primitive (`HybridLogicalClock.coerceHlc` for HLC validation) rather than re-implementing HLC parsing, and it MIRRORS — does not duplicate — the CoherenceJournal typed-schema discipline (the journal's own `validate()`/jail stays the authority for the existing kinds; this is the parallel discipline for the generic replicated envelope a concrete store will register).
41
+
42
+ ---
43
+
44
+ ## 4. Signal vs authority compliance
45
+
46
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
47
+
48
+ - [x] No — this change has no block/allow surface **over a user action**.
49
+
50
+ The two refusal surfaces (validator at the RECEIVE door, emission gate at the EMISSION door) are mechanism: they protect DATA integrity and prevent the named silent-drop skew mode. Neither blocks, delays, or rewrites a user-initiated action — there is no user-message path through this code at all. The validator's logic is deterministic-but-NOT-brittle-with-authority-over-a-user: it is a structural schema check (the same class as the existing journal typed-schema validation, which is the accepted pattern), and its "authority" is only over whether a malformed REPLICATED RECORD enters a stream. The emission gate is a conservative "don't forward what the peer can't receive" — withholding traffic, the safe direction. No brittle detector owns block authority over anything a user does.
51
+
52
+ ---
53
+
54
+ ## 5. Interactions
55
+
56
+ - **Shadowing:** none. The new validator runs on the replicated-record envelope path, which does not yet exist on disk (the registry is empty); no concrete kind is emitted, so it shadows no existing check. The startup `assertStateSyncInvariants` runs alongside `assertSeamlessnessInvariants` (independent config sub-trees) — neither shadows the other.
57
+ - **PULL transport + dual-registry coupling (adversarial finding #4).** The real journal-sync transport is RECEIVER-DRIVEN PULL (`PeerPresencePuller.driveJournalDelta` iterates the SENDER's advert from `CoherenceJournal.getOwnAdvert()`, which enumerates the static `JOURNAL_KINDS`; serve + apply both gate on `JOURNAL_KINDS`). There is NO push-forward step, so `shouldEmitToPeer` is intentionally UNWIRED in Step 2 (no concrete kind to serve; it is the pure per-peer decision the WS2.1 serve/pull chokepoint will consult). The named "emit a new kind to an OLD peer → silently dropped" mode manifests on this PULL transport as "an old peer never PULLS a kind absent from its own JOURNAL_KINDS." CRITICAL COUPLING for the consumer PRs: a replicated kind MUST be added to BOTH `ReplicatedKindRegistry` (read by the gate + the `stateSyncReceive` advert) AND the static `JOURNAL_KINDS` (gates serve + apply, enumerated by `getOwnAdvert`) — registering into only the former yields a store that advertises receive=true yet serves/applies/pulls nothing (a silent no-replication). A wiring-integrity ratchet (`tests/unit/ReplicatedRecordEnvelope.test.ts`) asserts every registered replicated kind is present in `JOURNAL_KINDS`. Documented in spec §4 + the `ReplicatedRecordEnvelope.ts` module header.
58
+ - **Double-fire:** the boot-time coherence check is guarded (`stateSyncCoherenceSurfaced` one-shot) so a mixed-flag pool surfaces ONCE, not per-tick — explicitly the anti-double-fire design (§4 "surfaces ONCE, coalesced"). With an empty registry it never fires at all.
59
+ - **Races:** the registry is constructed once at boot and (in Step 2) never mutated after; the advert self-report reads config + the registry's store list (immutable in this step). No shared mutable state with concurrent code. The validator + decision functions are pure (no shared state).
60
+ - **Feedback loops:** none — the coherence check is observe-only (logs a line); it never auto-enables/disables a store or changes the advert.
61
+
62
+ ---
63
+
64
+ ## 6. External surfaces
65
+
66
+ - **Other agents on the same machine:** none — no new shared files, no new routes.
67
+ - **Other users of the install base:** the capacity heartbeat now carries an additional `seamlessnessFlags.stateSyncReceive` field (`{}` until a store is registered+enabled). Older peers that don't know the field ignore it (forward-compat); newer peers read it. Additive, bounded, no breakage.
68
+ - **External systems:** none.
69
+ - **Persistent state:** config gains a `multiMachine.stateSync` block (foundation knobs only, no `enabled` key) via the existing `applyDefaults`/`migrateConfig` add-missing path — no new on-disk stream, no DB, no ledger. The replicated-record streams themselves land with WS2.1.
70
+ - **Timing/runtime:** the boot coherence check timer (60s, unref'd) is inert while the registry is empty.
71
+ - **Operator surface (Mobile-Complete Operator Actions):** no operator-facing actions — this is internal substrate with no PIN-gated or approval-class route. N/A.
72
+
73
+ ---
74
+
75
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
76
+
77
+ **Posture: replicated (journal-kind emission + flag-coherence-gated).** This IS the replication machinery — its entire purpose is to let a store's state follow the agent across machines via flag-gated, flag-coherence-gated journal-kind emission on top of the coherence-journal replication path (the same first-hop replication the existing kinds use). The named replication path is: a concrete store (WS2.1) registers a kind onto `ReplicatedKindRegistry`, the per-record envelope rides the coherence journal's existing serve/apply transport, and emission is gated per-peer on the `seamlessnessFlags.stateSyncReceive` advert so a kind is NEVER forwarded to a peer that would silently drop it (the named data-loss skew mode this step exists to prevent).
78
+
79
+ - **User-facing notices:** the boot coherence check logs ONE coalesced line to the server log on a mixed-flag pool; it emits no user-facing Telegram notice (a richer surface is the store PR's to add). No one-voice gating needed.
80
+ - **Durable state on topic transfer:** Step 2 holds no per-topic durable state (the registry is in-memory, config is per-machine). No strand-on-transfer risk.
81
+ - **URLs across machine boundaries:** none generated.
82
+ - **Single-machine:** strict no-op — emission is gated on a peer advertising the matching flag, so with no peers nothing is ever emitted; the registry is empty regardless.
83
+
84
+ ---
85
+
86
+ ## 8. Rollback cost
87
+
88
+ - **Hot-fix release:** pure code change — revert the two new modules + the types/config/server additions and ship as the next patch. No persistent state to undo.
89
+ - **Data migration:** none. The `multiMachine.stateSync` config block is add-missing and inert (no per-store `enabled` key shipped); leaving it on an agent after a code revert is harmless (the consuming code is gone). No new on-disk stream is created in this step.
90
+ - **Agent state repair:** none — no agent needs notifying or resetting; the advert field simply stops being emitted after a revert (peers tolerate its absence).
91
+ - **User visibility:** none — the feature is dark with no user-facing surface; a rollback is invisible to users.
92
+
93
+ ---
94
+
95
+ ## Conclusion
96
+
97
+ This review produced no design changes — the change is, by construction, pure dark-by-default mechanism with its only two refusal surfaces at the receive door (malformed-data rejection) and the emission door (don't forward to a non-advertising peer), neither of which touches a user action. The N-peer correctness of the flag-coherence check and the coalesced single-surface design were verified by unit tests (3+-peer mixed cases). The line-map golden test was recomputed by hand for the +17 cartographer shift the `stateSync` config block introduced. The change is clear to ship as the substrate WS2.1 will register a concrete kind onto.
98
+
99
+ ---
100
+
101
+ ## Evidence pointers
102
+
103
+ - `tests/unit/ReplicatedRecordEnvelope.test.ts` (46 tests) — validator both-sides, registry, flag-gated + flag-coherence (incl. 3+-peer mix), config invariants, wiring-integrity advert.
104
+ - Gate-parity (run in the worktree): `npx tsc --noEmit` clean; `no-silent-fallbacks` = 471 (= BASELINE, no bump, my files absent from the flagged list); `docs-coverage --check` PASS (class 55%); `feature-delivery-completeness` PASS (no new CLAUDE.md section); `lint-dev-agent-dark-gate` PASS (line-map updated); focused suites `CoherenceJournal*` / `JournalSyncApplier*` / new test all green.
@@ -0,0 +1,200 @@
1
+ # Side-Effects Review — Operator-Surface Quality standard + Mandates-tab redesign
2
+
3
+ **Version / slug:** `operator-surface-quality-and-mandates-redesign`
4
+ **Date:** `2026-06-12`
5
+ **Author:** `Echo (instar dev agent)`
6
+ **Second-pass reviewer:** `not required (Tier 1)`
7
+
8
+ ## Summary of the change
9
+
10
+ Two cohesive changes from one operator finding (2026-06-12, topic 22367, CMT-1434):
11
+ the freshly-shipped Mandates grant form was mobile-*reachable* (per Mobile-Complete
12
+ Operator Actions) yet unusable ("abysmal"). This (A) redesigns the Mandates
13
+ dashboard card to be genuinely good, and (B) lands a new constitutional standard,
14
+ **Operator-Surface Quality**, with a real enforcement gate so reachable-but-bad
15
+ cannot ship again.
16
+
17
+ Files touched:
18
+ - `dashboard/mandates.js` — renderers: grant form is now the card's primary,
19
+ always-open block (was wrapped in a collapsed `<details>`); revoke demoted to a
20
+ quiet collapsed control ordered after grant; JSON bounds / fingerprints / scope
21
+ slugs replaced by a plain-language summary, with ids kept only on a muted "For
22
+ support" line; grants list humanized; audit cells carry `data-label` for
23
+ mobile stacking.
24
+ - `dashboard/index.html` — shortened the explanatory wall to one line + a
25
+ collapsible "What is this?"; added CSS for the primary grant block, the demoted
26
+ revoke, the plain summary, and a `@media (max-width:640px)` rule that stacks the
27
+ audit table so the reason column is never truncated.
28
+ - `docs/STANDARDS-REGISTRY.md` — new "Operator-Surface Quality" article (Interaction
29
+ family), naming `scripts/instar-dev-precommit.js` as its gate.
30
+ - `skills/instar-dev/templates/side-effects-artifact.md` — new §6b operator-surface
31
+ quality question.
32
+ - `scripts/instar-dev-precommit.js` + `scripts/lib/operator-surface.mjs` — the gate
33
+ (`assertOperatorSurfaceQuality`) + its pure decision predicates.
34
+ - Tests: `tests/unit/dashboard-mandateGrantForm.test.ts`,
35
+ `tests/unit/dashboard-mandatesTab.test.ts`,
36
+ `tests/unit/standards-enforcement-auditor.test.ts`,
37
+ `tests/unit/operator-surface-gate.test.ts`.
38
+
39
+ ## Decision-point inventory
40
+
41
+ - `assertOperatorSurfaceQuality` (scripts/instar-dev-precommit.js) — **add** — a new
42
+ scoped review gate: when a commit touches an operator surface, the side-effects
43
+ artifact must answer the operator-surface-quality question or the commit blocks.
44
+ - `isOperatorSurfaceFile` / `artifactAddressesOperatorSurfaceQuality`
45
+ (scripts/lib/operator-surface.mjs) — **add** — the gate's two pure predicates.
46
+ - Mandates renderers (dashboard/mandates.js) — **modify** — presentation only; no
47
+ change to the grant/revoke/issue API calls, payloads, or PIN discipline.
48
+
49
+ ---
50
+
51
+ ## 1. Over-block
52
+
53
+ **What legitimate inputs does this change reject that it shouldn't?**
54
+
55
+ The only block/allow surface added is the pre-commit gate. It fires ONLY when a
56
+ staged file matches an operator surface (`dashboard/*.js|html`, or an
57
+ approval/secret-drop/operator-approval form) AND the side-effects artifact lacks
58
+ the operator-surface-quality phrase. A legitimate operator-surface change whose
59
+ review genuinely answers §6b passes. A change touching no operator surface is never
60
+ evaluated. The detector explicitly excludes `*.test.*`/`*.spec.*` siblings so a
61
+ test guarding a surface isn't itself treated as the surface.
62
+
63
+ ## 2. Under-block
64
+
65
+ **What failure modes does this still miss?**
66
+
67
+ A commit that touches an operator surface but stages NO in-scope file (src/,
68
+ scripts/, .husky/, skill) never reaches the gate at all — the precommit only runs
69
+ its body when an in-scope file is staged. This is the SAME boundary the sibling
70
+ `assertFrameworkGenerality` gate has (it fires only when its src surface is
71
+ touched). A pure dashboard-only commit (dashboard/ is not in-scope) would not be
72
+ gated. Accepted for v1: the dashboard surface is authored by instar-dev work that
73
+ near-always co-stages an in-scope file, and the standard + template question still
74
+ guide the review. Widening the gate's trigger to operator surfaces directly is a
75
+ tracked follow-up, not silently dropped.
76
+
77
+ The gate also checks for *presence* of the §6b engagement, not the substantive
78
+ quality of the four answers — that judgment stays with the reviewer (Signal vs.
79
+ Authority: the gate signals the question must be present; the mind answers it).
80
+
81
+ ## 3. Level-of-abstraction fit
82
+
83
+ **Is this at the right layer?**
84
+
85
+ Yes. The gate is a brittle path/text detector that ENFORCES a written answer; it
86
+ holds no semantic judgment about whether the UI is actually good — that is the
87
+ reviewer's call. This is the correct split: the detector ensures the question is
88
+ asked; the full-context mind answers it. It feeds the existing side-effects review
89
+ flow rather than running parallel to it, mirroring `assertFrameworkGenerality`.
90
+
91
+ ## 4. Signal vs authority compliance
92
+
93
+ **Required reference:** docs/signal-vs-authority.md
94
+
95
+ - [x] No — this change has no block/allow surface that makes a *semantic* judgment.
96
+ The gate's only authority is "the operator-surface-quality section must be present
97
+ in the artifact for an operator-surface change" — a structural presence check, not
98
+ a content-quality verdict. The quality verdict stays with the human/agent reviewer.
99
+
100
+ ## 5. Interactions
101
+
102
+ - **Shadowing:** the new gate runs alongside `assertFrameworkGenerality` at both the
103
+ Tier-1 and Tier-2 pass sites; neither shadows the other (different surfaces,
104
+ different artifacts checks). Both run before `process.exit(0)`.
105
+ - **Double-fire:** the gate is invoked once per pass path; only one pass path
106
+ executes per commit (Tier-1 lite exits, Tier-2 falls through). No double-fire.
107
+ - **Races:** none — pure synchronous file reads at commit time.
108
+ - **Feedback loops:** none.
109
+
110
+ The dashboard renderer changes are pure presentation: the grant/revoke/issue
111
+ controller, fetch calls, payloads, and PIN-never-retained discipline are untouched
112
+ (verified — all existing controller tests pass unchanged).
113
+
114
+ ## 6. External surfaces
115
+
116
+ - Other agents on the same machine? No.
117
+ - Other users of the install base? Yes — the Mandates dashboard tab is the visible
118
+ change. It ships via the package's `dashboard/` (served by `express.static`), so
119
+ it reaches every agent through the normal release → auto-update path. No
120
+ agent-installed-file rewrite is involved for the dashboard.
121
+ - External systems? No.
122
+ - Persistent state? No.
123
+ - **Operator surface (Mobile-Complete Operator Actions):** the Mandates grant/revoke
124
+ actions already have a phone-completable surface (this very tab). This change
125
+ improves that surface; it adds no new API-only operator action.
126
+
127
+ **Migration parity (the one agent-installed file I touched —
128
+ `skills/instar-dev/templates/side-effects-artifact.md`):** this template is deployed
129
+ into dev-agent homes and is already migrated by
130
+ `migrateMultiMachinePostureReviewDimension`. I deliberately did NOT add a new
131
+ `PostUpdateMigrator` migration, and this is a recorded decision, not a silent skip:
132
+ (1) the BINDING enforcement is the pre-commit gate in `scripts/`, which is
133
+ instar-source-repo tooling run from `.husky` — it is NOT a deployed runtime file and
134
+ works for ALL instar-dev work regardless of any installed template copy; (2) the gate
135
+ is self-teaching — its block message names the exact §6b section to add, so no
136
+ developer is left in a silent-fail state (the Migration Parity standard's actual
137
+ concern: "a feature that only works for new agents"); (3) new agents get §6b via
138
+ `installBuiltinSkills`; existing agents NOT yet on the multi-machine template get it
139
+ automatically because the bundled file the existing migration re-copies now contains
140
+ §6b; only an already-multi-machine dev agent misses the template PROMPT, and the gate
141
+ covers it. Touching `PostUpdateMigrator` (fleet machinery the codebase declares
142
+ "never Tier-1") to ship a convenience prompt to that narrow residual is
143
+ disproportionate. Tracked: CMT-1434.
144
+
145
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
146
+
147
+ **machine-local BY DESIGN.** The change is a stateless dashboard renderer + a
148
+ commit-time repo gate. The dashboard reads pool-wide mandate/audit state from the
149
+ server it is served by (no new per-machine state); the gate runs in whichever
150
+ checkout the developer commits from. It emits no user-facing notices, holds no
151
+ durable state, and generates no URLs — so there is nothing to replicate, proxy, or
152
+ strand on a topic transfer. Framework generality: not applicable — this touches
153
+ neither the session launch/inject abstraction nor messaging delivery.
154
+
155
+ ## 8. Rollback cost
156
+
157
+ Pure code change. Revert the renderer/markup/CSS and the gate; ship as a patch. No
158
+ persistent state, no data migration, no agent-state repair. During the rollback
159
+ window users would simply see the prior Mandates card again — no functional
160
+ regression (the underlying mandate API is unchanged throughout).
161
+
162
+ ## Conclusion
163
+
164
+ The review produced one design refinement: extracting the gate's decision logic into
165
+ a pure, unit-tested lib (`scripts/lib/operator-surface.mjs`) rather than inlining
166
+ regexes in the precommit, so both sides of each boundary are pinned by tests and the
167
+ gate's wiring is verifiable. The recorded migration-parity decision (no new
168
+ `PostUpdateMigrator` migration, with explicit reasoning) is the one item flagged for
169
+ the operator's awareness. Clear to ship as a Tier-1 change.
170
+
171
+ ## Operator-surface quality (Operator-Surface Quality standard) — §6b self-review
172
+
173
+ This change IS an operator surface (`dashboard/mandates.js`, `dashboard/index.html`),
174
+ so it is held to the standard it introduces:
175
+
176
+ 1. **Leads with the primary action?** Yes. The Grant form renders as an open,
177
+ titled `mnd-grant-block` — never inside a collapsed `<details>`. On a mandate
178
+ with no grants it is the visible call to action.
179
+ 2. **Zero raw internals as primary content?** Yes. The card headline is a
180
+ de-slugified scope title; the body is a plain-language summary. No JSON bounds,
181
+ no fingerprints, no slugs as primary content (asserted: the card body contains no
182
+ `{"` substring and no raw bounds keys). Ids/fingerprints/slugs survive only on a
183
+ muted "For support" line.
184
+ 3. **Destructive actions de-emphasized?** Yes. Revoke is a quiet, collapsed
185
+ `mnd-revoke-details` ordered AFTER the Grant block in the DOM; the danger button
186
+ is no longer bold/featured (asserted: grant index < revoke index).
187
+ 4. **Plain language + phone width?** Yes. Grants read "Adam Admin can deploy to
188
+ production until … — authorized by you"; action slugs are humanized; the audit
189
+ table stacks into labelled rows at ≤640px so the reason column is never
190
+ truncated.
191
+
192
+ ## Evidence pointers
193
+
194
+ - `tests/unit/dashboard-mandateGrantForm.test.ts` — grant form NOT in collapsed
195
+ `<details>`; no `{"` JSON in card body; revoke ordered after grant; humanized
196
+ grants; mobile audit labels.
197
+ - `tests/unit/operator-surface-gate.test.ts` — both predicates (both sides of each
198
+ boundary) + wiring-integrity (the gate is called on both pass paths, not a no-op).
199
+ - `tests/unit/standards-enforcement-auditor.test.ts` — the new standard classifies as
200
+ `gate` with zero dangling refs over the real registry.