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.
- package/dashboard/index.html +40 -10
- package/dashboard/mandates.js +115 -34
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +75 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +17 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/ReplicatedRecordEnvelope.d.ts +380 -0
- package/dist/core/ReplicatedRecordEnvelope.d.ts.map +1 -0
- package/dist/core/ReplicatedRecordEnvelope.js +464 -0
- package/dist/core/ReplicatedRecordEnvelope.js.map +1 -0
- package/dist/core/stateSyncConfig.d.ts +94 -0
- package/dist/core/stateSyncConfig.d.ts.map +1 -0
- package/dist/core/stateSyncConfig.js +126 -0
- package/dist/core/stateSyncConfig.js.map +1 -0
- package/dist/core/types.d.ts +59 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +46 -0
- package/scripts/lib/operator-surface.mjs +54 -0
- package/skills/instar-dev/templates/side-effects-artifact.md +15 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.521.md +69 -0
- package/upgrades/side-effects/multi-machine-replicated-store-foundation-step2-journal-kind.md +104 -0
- package/upgrades/side-effects/operator-surface-quality-and-mandates-redesign.md +200 -0
|
@@ -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:
|
|
5
|
-
"instarVersion": "1.3.
|
|
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.
|