instar 1.3.734 → 1.3.736

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.
Files changed (34) hide show
  1. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  2. package/dist/config/ConfigDefaults.js +9 -0
  3. package/dist/config/ConfigDefaults.js.map +1 -1
  4. package/dist/core/DefectClassRegistry.d.ts +152 -0
  5. package/dist/core/DefectClassRegistry.d.ts.map +1 -0
  6. package/dist/core/DefectClassRegistry.js +256 -0
  7. package/dist/core/DefectClassRegistry.js.map +1 -0
  8. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  9. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  10. package/dist/core/PostUpdateMigrator.js +61 -0
  11. package/dist/core/PostUpdateMigrator.js.map +1 -1
  12. package/dist/core/StandardsEnforcementAuditor.d.ts +24 -0
  13. package/dist/core/StandardsEnforcementAuditor.d.ts.map +1 -1
  14. package/dist/core/StandardsEnforcementAuditor.js +50 -1
  15. package/dist/core/StandardsEnforcementAuditor.js.map +1 -1
  16. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  17. package/dist/core/devGatedFeatures.js +5 -0
  18. package/dist/core/devGatedFeatures.js.map +1 -1
  19. package/dist/core/types.d.ts +13 -0
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/core/types.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/class-closure-declare.mjs +191 -0
  24. package/scripts/class-closure-lint.mjs +346 -0
  25. package/scripts/lib/class-closure-grader.mjs +139 -0
  26. package/scripts/lib/defect-class-registry.mjs +236 -0
  27. package/skills/instar-dev/templates/side-effects-artifact.md +39 -0
  28. package/skills/spec-converge/SKILL.md +2 -1
  29. package/skills/spec-converge/templates/reviewer-integration.md +8 -6
  30. package/src/data/builtin-manifest.json +19 -19
  31. package/upgrades/1.3.735.md +67 -0
  32. package/upgrades/1.3.736.md +57 -0
  33. package/upgrades/side-effects/class-closure-gate.md +111 -0
  34. package/upgrades/side-effects/three-standards-enforcement.md +131 -0
@@ -0,0 +1,236 @@
1
+ // defect-class-registry.mjs — the SELF-CONTAINED ESM mirror of the pure
2
+ // registry/derive/threshold logic in src/core/DefectClassRegistry.ts, used by
3
+ // the class-closure gate's CI lint (scripts/class-closure-lint.mjs).
4
+ //
5
+ // WHY a mirror: PR-gate lints run on a fresh checkout with NO build step
6
+ // (decision-audit-gate.yml precedent), so the lint cannot import the TS module
7
+ // from dist/. This file re-implements the SAME pure functions the lint needs —
8
+ // loadRegistry, validateRegistry, readDecisionDeclarations, deriveClassData,
9
+ // computeEscalation (+ the small helpers/constants they lean on).
10
+ //
11
+ // tests/unit/class-closure-registry-parity.test.ts pins this mirror's outputs
12
+ // EQUAL to src/core/DefectClassRegistry.ts for the same inputs, so the two
13
+ // implementations cannot drift (Structure > Willpower). If you edit one, edit
14
+ // the other and the parity test will hold you to it.
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ export const DEFAULT_SPREAD_N = 3; // ≥N across ≥2 components (normal severity)
20
+ export const DEFAULT_SINGLE_K = 5; // ≥K within one component (normal severity)
21
+ export const DEFAULT_GAP_MAX_AGE_DAYS = 45;
22
+
23
+ export const REGISTRY_REL_PATH = path.join('docs', 'defect-classes.json');
24
+
25
+ /** Is `repoRoot` an instar checkout carrying the class registry (repo-gate)? */
26
+ export function isClassClosureRepo(repoRoot) {
27
+ try {
28
+ return (
29
+ fs.existsSync(path.join(repoRoot, '.git')) &&
30
+ fs.existsSync(path.join(repoRoot, 'package.json')) &&
31
+ fs.existsSync(path.join(repoRoot, REGISTRY_REL_PATH))
32
+ );
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ /** Load + parse the registry file. Throws on missing/malformed JSON or invalid shape. */
39
+ export function loadRegistry(repoRoot) {
40
+ const p = path.join(repoRoot, REGISTRY_REL_PATH);
41
+ const raw = fs.readFileSync(p, 'utf-8');
42
+ const parsed = JSON.parse(raw);
43
+ const v = validateRegistry(parsed);
44
+ if (!v.ok) {
45
+ throw new Error(`defect-classes.json is invalid: ${v.errors.join('; ')}`);
46
+ }
47
+ return parsed;
48
+ }
49
+
50
+ /** Structural validation (used by the lint + tests). Never throws. */
51
+ export function validateRegistry(reg) {
52
+ const errors = [];
53
+ if (!reg || typeof reg !== 'object') return { ok: false, errors: ['registry is not an object'] };
54
+ const r = reg;
55
+ if (typeof r.version !== 'number') errors.push('version must be a number');
56
+ if (!Array.isArray(r.classes)) {
57
+ errors.push('classes must be an array');
58
+ return { ok: errors.length === 0, errors };
59
+ }
60
+ const ids = new Set();
61
+ for (const [i, c] of r.classes.entries()) {
62
+ const where = `class[${i}]`;
63
+ if (!c || typeof c !== 'object') { errors.push(`${where} is not an object`); continue; }
64
+ const e = c;
65
+ if (!e.id || typeof e.id !== 'string') errors.push(`${where}.id missing`);
66
+ else {
67
+ if (ids.has(e.id)) errors.push(`${where}.id duplicate: ${e.id}`);
68
+ ids.add(e.id);
69
+ if (!/^[a-z0-9-]+$/.test(e.id)) errors.push(`${where}.id must match ^[a-z0-9-]+$`);
70
+ }
71
+ if (!e.description || typeof e.description !== 'string') errors.push(`${where}.description missing`);
72
+ if (!Array.isArray(e.includes) || e.includes.length < 1) errors.push(`${where}.includes needs ≥1 entry`);
73
+ if (!Array.isArray(e.excludes) || e.excludes.length < 1) errors.push(`${where}.excludes needs ≥1 entry`);
74
+ if (!Array.isArray(e.canonicalExamples)) errors.push(`${where}.canonicalExamples must be an array`);
75
+ if (e.status !== 'confirmed' && e.status !== 'unconfirmed') errors.push(`${where}.status invalid`);
76
+ if (e.severity !== 'critical' && e.severity !== 'normal') errors.push(`${where}.severity invalid`);
77
+ if (typeof e.instanceCount !== 'number') errors.push(`${where}.instanceCount must be a number`);
78
+ if (typeof e.evidenceCountAtLastAck !== 'number') errors.push(`${where}.evidenceCountAtLastAck must be a number`);
79
+ if (!('closureStandard' in e)) errors.push(`${where}.closureStandard missing (nullable ok)`);
80
+ if (!('proposalId' in e)) errors.push(`${where}.proposalId missing (nullable ok)`);
81
+ // A novel class enters unconfirmed and REQUIRES a nearestExistingClass.
82
+ if (e.status === 'unconfirmed' && !e.nearestExistingClass) {
83
+ errors.push(`${where} is unconfirmed but has no nearestExistingClass`);
84
+ }
85
+ }
86
+ return { ok: errors.length === 0, errors };
87
+ }
88
+
89
+ /**
90
+ * Read the class-closure declarations from committed decision-audit entries.
91
+ * The decision-audit entry is the SINGLE machine-readable counting host — the
92
+ * side-effects artifact mirror is display-only and is NOT read here (C1 fix).
93
+ * @returns {Array<object>} declarations, each with `.source` (filename) + `.ts`.
94
+ */
95
+ export function readDecisionDeclarations(repoRoot) {
96
+ const dir = path.join(repoRoot, '.instar', 'instar-dev-decisions');
97
+ let files;
98
+ try {
99
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
100
+ } catch {
101
+ return [];
102
+ }
103
+ const out = [];
104
+ for (const f of files) {
105
+ let entry;
106
+ try {
107
+ entry = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
108
+ } catch {
109
+ continue;
110
+ }
111
+ if (entry && entry.classClosure && typeof entry.classClosure === 'object') {
112
+ out.push({ ...entry.classClosure, source: f, ts: entry.ts });
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /**
119
+ * Derive per-class counts from declarations, deduped by PR number. Two
120
+ * declarations citing the same PR + class count ONCE. A declaration with no
121
+ * prNumber falls back to its source filename as the dedup key (still counts once).
122
+ * @returns {Map<string, object>} classId → DerivedClassData.
123
+ */
124
+ export function deriveClassData(declarations) {
125
+ const byClass = new Map();
126
+ for (const d of declarations) {
127
+ if (!d.defectClass) continue;
128
+ const list = byClass.get(d.defectClass) ?? [];
129
+ list.push(d);
130
+ byClass.set(d.defectClass, list);
131
+ }
132
+ const result = new Map();
133
+ for (const [classId, list] of byClass) {
134
+ const dedupKey = (d) => (d.prNumber != null ? `pr:${d.prNumber}` : `src:${d.source}`);
135
+ const seen = new Set();
136
+ const prs = new Set();
137
+ const components = new Set();
138
+ const perComponentKeys = new Map();
139
+ let hasOpenGap = false;
140
+ for (const d of list) {
141
+ const key = dedupKey(d);
142
+ seen.add(key);
143
+ if (d.prNumber != null) prs.add(d.prNumber);
144
+ const comp = d.component ?? '<unspecified>';
145
+ components.add(comp);
146
+ const compSet = perComponentKeys.get(comp) ?? new Set();
147
+ compSet.add(key);
148
+ perComponentKeys.set(comp, compSet);
149
+ if (d.closure === 'gap') hasOpenGap = true;
150
+ }
151
+ let maxSingleComponentCount = 0;
152
+ for (const s of perComponentKeys.values()) {
153
+ maxSingleComponentCount = Math.max(maxSingleComponentCount, s.size);
154
+ }
155
+ result.set(classId, {
156
+ dedupedCount: seen.size,
157
+ componentCount: components.size,
158
+ maxSingleComponentCount,
159
+ hasOpenGap,
160
+ prs: [...prs].sort((a, b) => a - b),
161
+ components: [...components].sort(),
162
+ });
163
+ }
164
+ return result;
165
+ }
166
+
167
+ /**
168
+ * Deterministic threshold logic. Fires ONLY when there is NEW evidence past the
169
+ * last-ack baseline (seeded-closed suppression + dedup re-raise) AND a severity
170
+ * arm crosses:
171
+ * - critical ⇒ escalates at ≥1 confirmed instance (any new evidence).
172
+ * - normal ⇒ ≥N across ≥2 components, OR ≥2 + an open gap, OR ≥K in one component.
173
+ * @param {{ severity: string, evidenceCountAtLastAck: number }} entry
174
+ * @param {object} derived DerivedClassData
175
+ * @param {{ spreadN?: number, singleK?: number }} thresholds
176
+ */
177
+ export function computeEscalation(entry, derived, thresholds = {}) {
178
+ const N = thresholds.spreadN ?? DEFAULT_SPREAD_N;
179
+ const K = thresholds.singleK ?? DEFAULT_SINGLE_K;
180
+ const newEvidence = derived.dedupedCount > entry.evidenceCountAtLastAck;
181
+ if (!newEvidence) {
182
+ return {
183
+ shouldEscalate: false,
184
+ arm: null,
185
+ reason: `no new evidence past ack baseline (derived ${derived.dedupedCount} ≤ ack ${entry.evidenceCountAtLastAck})`,
186
+ newEvidence: false,
187
+ };
188
+ }
189
+ if (entry.severity === 'critical') {
190
+ return {
191
+ shouldEscalate: true,
192
+ arm: 'critical-1',
193
+ reason: `critical class recurred (derived ${derived.dedupedCount} > ack ${entry.evidenceCountAtLastAck}); escalates at 1`,
194
+ newEvidence: true,
195
+ };
196
+ }
197
+ // normal severity — three arms.
198
+ if (derived.dedupedCount >= N && derived.componentCount >= 2) {
199
+ return {
200
+ shouldEscalate: true,
201
+ arm: 'spread',
202
+ reason: `≥${N} instances (${derived.dedupedCount}) across ≥2 components (${derived.componentCount})`,
203
+ newEvidence: true,
204
+ };
205
+ }
206
+ if (derived.dedupedCount >= 2 && derived.hasOpenGap) {
207
+ return {
208
+ shouldEscalate: true,
209
+ arm: 'gap-plus',
210
+ reason: `≥2 instances (${derived.dedupedCount}) + an open gap item`,
211
+ newEvidence: true,
212
+ };
213
+ }
214
+ if (derived.maxSingleComponentCount >= K) {
215
+ return {
216
+ shouldEscalate: true,
217
+ arm: 'single-component',
218
+ reason: `≥${K} instances (${derived.maxSingleComponentCount}) within one component`,
219
+ newEvidence: true,
220
+ };
221
+ }
222
+ return {
223
+ shouldEscalate: false,
224
+ arm: null,
225
+ reason: `no normal-severity arm crossed (count ${derived.dedupedCount}, components ${derived.componentCount}, maxSingle ${derived.maxSingleComponentCount})`,
226
+ newEvidence: true,
227
+ };
228
+ }
229
+
230
+ /** Whether a gap item is open past the max-age escalation ceiling. */
231
+ export function gapOpenPastMaxAge(gapOpenedAtIso, now = Date.now(), maxAgeDays = DEFAULT_GAP_MAX_AGE_DAYS) {
232
+ const opened = Date.parse(gapOpenedAtIso);
233
+ if (Number.isNaN(opened)) return false;
234
+ const ageDays = (now - opened) / (24 * 60 * 60 * 1000);
235
+ return ageDays > maxAgeDays;
236
+ }
@@ -176,3 +176,42 @@ pool-wide question is answered by a proxied-on-read merged view." Not abstractio
176
176
  ## Evidence pointers
177
177
 
178
178
  [Optional. Links or file paths to the live verification artifacts produced during `/build` — reproduction steps, before/after logs, test output. These feed the "Evidence" section in the upgrade notes if the change is shipping as a release.]
179
+
180
+ ---
181
+
182
+ ## Class-Closure Declaration (display-only mirror)
183
+
184
+ **REQUIRED whenever this change FIXES a defect in an agent-authored artifact** (an
185
+ LLM prompt, hook, config, skill, or standards text — see
186
+ `docs/specs/class-closure-gate.md`). This section is the human-readable MIRROR of
187
+ the machine-readable `classClosure` block in the commit's decision-audit entry
188
+ (the host the CI lint validates). **Display-only:** the lint counts the
189
+ decision-audit host ONLY and NEVER sums this mirror — the two are asserted to
190
+ AGREE, never added (C1). If this change fixes no agent-authored-artifact defect,
191
+ state: "No agent-authored-artifact defect — not applicable."
192
+
193
+ - **`defectClass`** — a class id from `docs/defect-classes.json`, or `novel`. A
194
+ `novel` class is not a free pass: it REQUIRES a full new registry entry in the
195
+ same change carrying `nearestExistingClass` + ≥1 `includes` + ≥1 `excludes` +
196
+ `severity`, and it enters `status: "unconfirmed"` (an unconfirmed class CANNOT
197
+ satisfy `closure: guard` — its fix carries `closure: gap` until the operator
198
+ confirms it).
199
+ - **`closure`** — either `guard` (the standard/test/lint that makes the class's
200
+ recurrence structurally refused or detected, cited by path/symbol) or `gap` (a
201
+ tracked standards-gap evolution-action id when the class-level guard is out of
202
+ this fix's scope).
203
+ - **`guardEvidence`** (required with `closure: guard`) — the guard's enforcement
204
+ type as graded by the coverage audit's grader (`ratchet` / `gate` / `lint`),
205
+ the citation, and one line on *how this guard would have caught THIS defect*. A
206
+ citation that does not resolve to a LIVE enforcing guard on disk automatically
207
+ downgrades the declaration to `closure: gap` (G3 — a dark/spec-only artifact
208
+ guards nothing).
209
+ - **`gap`** (with `closure: gap`) — the evolution-action id tracking the missing
210
+ guard. A gap is not fire-and-forget: it counts as escalation evidence and
211
+ re-surfaces on the evolution-action cadence.
212
+
213
+ [Fill in the four fields (or "not applicable"). Example: "`defectClass:
214
+ injection-credulity`, `closure: guard`, `guardEvidence: {enforcementType: gate,
215
+ citation: src/core/promptClauses.ts#authorityClause, howCaught: the authority
216
+ clause separates the trusted instruction surface from the quoted untrusted
217
+ transcript excerpt, so the injected instruction is data not command}`."]
@@ -91,7 +91,8 @@ The skill spawns reviewers in parallel:
91
91
  - **Security.** Attack surfaces, leaks, privilege escalation, auth on endpoints, prompt injection vectors, rotation races.
92
92
  - **Scalability/performance.** Hot-path cost, concurrent writes, memory churn, fail-open semantics, hook latency.
93
93
  - **Adversarial.** Misbehaving-session scenarios — bad-entry poisoning, self-reinforcing loops, stale claims, authority ambiguity, kind gaming.
94
- - **Integration/deployment.** Migration, backup/restore, multi-machine, config knobs, dashboard surface, rollback. **Multi-machine posture (Cross-Machine Coherence — mandatory check):** every feature/state surface the spec introduces must declare its posture when the agent runs on more than one machine — replicated (named replication path) / proxied-on-read (named merged read) / machine-local BY DESIGN (with the reason). A spec whose features silently assume a single machine is a MATERIAL finding: ~20 features shipped machine-blind before this check existed (2026-06-12 audit, topic 13481; MULTI-MACHINE-SEAMLESSNESS-SPEC is the cleanup bill). Probe specifically: user-facing notices (one-voice gating?), durable state (strands on topic transfer?), generated URLs (survive machine boundaries?).
94
+ - **Integration/deployment.** Migration, backup/restore, multi-machine, config knobs, dashboard surface, rollback. **Multi-machine posture (Cross-Machine Coherence — mandatory check):** every feature/state surface the spec introduces must declare its posture when the agent runs on more than one machine — replicated (named replication path) / proxied-on-read (named merged read) / machine-local BY DESIGN (with the reason). A spec whose features silently assume a single machine is a MATERIAL finding: ~20 features shipped machine-blind before this check existed (2026-06-12 audit, topic 13481; MULTI-MACHINE-SEAMLESSNESS-SPEC is the cleanup bill). Probe specifically: user-facing notices (one-voice gating?), durable state (strands on topic transfer?), generated URLs (survive machine boundaries?). **Standard A — reject an UNDEFENDED machine-local (default is `unified`):** a *declared* posture is no longer enough — the DEFAULT posture is `unified`, and an undefended `machine-local` is itself a **MATERIAL FINDING** (this is the upgrade from "accepts a declaration" to "requires the correct default"; the tiered-intelligence consult-memory shipped machine-local-by-design and survived seven rounds because the check only tested for an *answer*). A machine-local surface is allowed ONLY with a `machine-local-justification: <key>` marker — one labeled `key: value` line per machine-local surface inside the spec's `## Multi-machine posture` section — whose `<key>` is drawn from a **CLOSED taxonomy**: `physical-credential-locality` (a login / key / token / service-binding that physically lives on one disk — e.g. a Claude OAuth login, a browser profile's cookies, a Telegram bot token + the forum/topic ids it namespaces), `hardware-bound-resource` (bound to specific physical hardware), or `operator-ratified-exception` (the operator explicitly ratified it — this key MUST cite a machine-verifiable, existence-checkable artifact ref: a commit SHA, a registry key, or a resolvable URL, NEVER a bare topic+date). Other locality reasons (availability, privacy/data-residency, cost/latency, an intentional per-machine cache) are DENIED by default — each is either not a real locality *requirement* or must be routed through `operator-ratified-exception`; a fourth key is a constitution-bound operator decision, never an author's convenience. The check is **BIDIRECTIONAL**: an *infeasible* `unified` (a surface that is inherently credential/hardware-bound but declared — or left to default — `unified`) is EQUALLY a MATERIAL FINDING, because "just claim unified" would otherwise be the trivial dodge (and invites an unsafe attempt to replicate a credential). The reviewer INDEPENDENTLY CONTESTS correctness in BOTH directions: a marker whose key is *present* but substantively *wrong* is still a finding — the marker's PRESENCE never satisfies the CORRECTNESS check (this spec's own first draft mislabeled its per-machine hub `hardware-bound-resource`; the correct key was `physical-credential-locality`, and this rule is what catches it). `operator-ratified-exception` is the escape hatch — treat each use as worth surfacing for operator visibility so the hatch can't quietly become the path of least resistance. (Signal vs. Authority: the `machine-local-justification` marker is the cheap deterministic signal; THIS reviewer holds the semantic authority. The purely-deterministic marker LINT lands with the registry ship, hard-sequenced — until then this reviewer + the per-spec `POST /spec/conformance-check` gate surface a missing/wrong marker; do NOT claim a no-LLM deterministic parse exists yet.)
95
+ - **Self-Heal Before Notify (Standard B — escalation-gate review-check).** A spec that adds a **monitor / watcher / recurring-or-automated notice source** which raises operator notices MUST place its operator-facing attention-raise DOWNSTREAM of a `selfHealAttempted && selfHealExhausted` signal — the escalation path is UNREACHABLE on first detection. A first-detection escalation on a `recoverable` degradation is a **MATERIAL FINDING**. (Scope: a one-shot conversational reply is NOT a watcher and pays no self-heal cost.) The watcher MUST DECLARE, and the reviewer INDEPENDENTLY CONTESTS: (a) its self-heal step + `remediation-actions` — the concrete operations it invokes (re-register / restart / re-deliver); a no-op that merely flips `selfHealAttempted = true` to unlock escalation is a MATERIAL FINDING, and each side-effecting action MUST carry an idempotency guard + a compensation/rollback (a heal retried over a half-completed action is how these loops corrupt state); (b) that step's P19 brakes (per *No Unbounded Loops*) — `max-attempts`, `max-wall-clock`, `backoff`, `dedupe-key`, `breaker` (INCLUDING flapping: N heals of the same break within a window auto-reclassify to critical and escalate — a structural backstop a `recoverable` label can NEVER waive, per *Distrust Temporary Success*), `max-notification-latency` (an explicit duration WITH units, e.g. `120s`, and ≤ the constitutional ceiling at the registry key `standards.selfHealBeforeNotify.recoverableLatencyCeiling` — a recoverable watcher MUST tell the operator even while self-heal is still running once this ceiling passes; a missing/unitless value is a MATERIAL FINDING and a missing ceiling fails CLOSED = escalate-sooner), `audit-location` (a scrubbed, metadata-only trail — never raw secrets); (c) the severity `class` of each degradation — an **irreversible / data-loss / security** class escalates IMMEDIATELY (on the SAME detection tick, no heal-gate delay interposed) and self-heals CONCURRENTLY (notify-and-heal), while everything else is `recoverable`; a critical degradation MISLABELED `recoverable` to unlock the heal-first path is a MATERIAL FINDING (presence of a class field never satisfies the CORRECTNESS check — symmetric to Standard A's marker-correctness rule). Composes with *No Silent Degradation*: routing to self-heal is NEVER swallowing — every detection + heal attempt is audited, the audit trail IS the report, and the operator is the last resort, not the silent-drop alternative. The FIRST runtime application MUST extract the pattern into a reusable `SelfHealGate` — a THIN declaration + assertion layer over Instar's EXISTING in-process P19 breaker primitives (CrashLoopPauser and the breakers already threaded through the monitors), NEVER a new external workflow engine — plus a shared conformance fixture that exercises the stateful paths (unreachable-before-exhaustion, observable remediation evidence, flapping auto-escalation, and the latency backstop firing mid-heal). That runtime application is a downstream build task registered under Close the Loop, deliberately outside this review-check's own boundary — this bullet ships the review lens + the pattern, the code application follows.
95
96
  - **Decision-Completeness.** (Autonomy Principle 2 — `docs/specs/AUTONOMY-PRINCIPLES-ENFORCEMENT-SPEC.md` Piece 2.) Enumerates every point where the building agent would have to **stop mid-run and ask the user**. Each must be either **frontloaded** into a `## Frontloaded Decisions` section or explicitly tagged **cheap-to-change-after** because the work ships behind a named dark/dry-run/read-only phase. The reviewer **CONTESTS every cheap tag** — it independently asserts reversibility, and a closed non-cheap taxonomy overrides any tag: anything touching **durable external side-effects, money, identity, or a published/user-visible interface is NEVER cheap**, regardless of a "ships dark" label. A contested tag the reviewer rejects is a **material finding that blocks convergence**. Prompt: `templates/reviewer-decision-completeness.md`. Applies to ALL specs through this skill (D7) — no size gate, no per-spec override (D11; the rejected `disposition: override` escape hatch would reopen the exact skip-hatch Principle 2 closes).
96
97
  - **Lessons-aware.** Loads the canonical Instar Design Principles + Lessons Learned index (`docs/INSTAR-DESIGN-PRINCIPLES-AND-LESSONS.md`) plus the running agent's local `.instar/memory/feedback_*.md` entries, then checks the spec for (a) direct contradictions of documented principles/lessons, (b) applicable lessons the spec fails to engage with, (c) behavioral lessons violated by agent-facing surfaces the spec proposes, and **(d) FOUNDATION/SUBSYSTEM AUDIT — the design the spec TESTS, EXTENDS, or BUILDS ON, not just the spec's own surface: does that foundation violate a known standard or repeat a known mistake?** The audit MUST reach one layer below the spec boundary. A spec can be internally clean while faithfully testing or extending a flawed foundation — e.g. a test-harness spec that correctly proves a permission gate which *itself* holds brittle blocking authority in violation of Signal-vs-Authority, or an extension spec built on a subsystem with an unaddressed gap. Taking the underlying subsystem "as given" is exactly how a standards violation survives review: the spec passes, the foundation's flaw is never weighed. When the foundation is flawed, the finding is "this spec is sound but the subsystem it depends on violates standard X / repeats mistake Y — surface it before building on/around it." Catches the "Phase 2" anti-pattern, the spec-converge-pre-auth-circular failure mode (see `feedback_spec_converge_pre_auth_circular`), and the foundation-not-audited gap (the Slack-permission-gate brittle-enum-as-authority that the harness convergence cleared because it audited only the harness, 2026-06-09).
97
98
 
@@ -19,17 +19,19 @@ Specifically check:
19
19
 
20
20
  3. **Auto-update path** — when a user pulls a new version of instar, what automatically propagates? What needs manual intervention?
21
21
 
22
- 4. **Multi-machine**if instar agents are paired across machines, does the new state stay coherent across both, or does each machine develop its own divergent view?
22
+ 4. **Multi-machine — Standard A: reject an UNDEFENDED machine-local (default is `unified`).** For EVERY state surface the spec introduces, the DEFAULT posture is `unified` across the agent's machines. A *declaration* of "machine-local BY DESIGN" is NOT sufficient — an undefended `machine-local` is a **MATERIAL FINDING**. A machine-local surface is allowed ONLY with a `machine-local-justification: <key>` marker (one labeled `key: value` line per surface, in the spec's `## Multi-machine posture` section) whose `<key>` is from the **CLOSED taxonomy**: `physical-credential-locality` (a login / key / token / service-binding that physically lives on one disk), `hardware-bound-resource` (bound to specific hardware), or `operator-ratified-exception` (operator-ratified, and MUST cite a machine-verifiable, existence-checkable ref a commit SHA, registry key, or resolvable URL, never a bare topic+date). Other reasons (availability, privacy/data-residency, cost/latency, per-machine cache) are DENIED by default. The check is **BIDIRECTIONAL**: an *infeasible* `unified` (a credential/hardware-bound surface declared or defaulted `unified`) is EQUALLY a MATERIAL FINDING. CONTEST the justification INDEPENDENTLY in both directions — a marker whose key is present but substantively WRONG is still a finding; the marker's PRESENCE never satisfies the CORRECTNESS check. (Absence of any posture declaration defaults to `unified`-required.)
23
23
 
24
- 5. **Backup/restore** — is new persistent state included in the backup manifest? If a user runs a snapshot/restore cycle, does the state survive?
24
+ 5. **Self-Heal Before Notify — Standard B (escalation-gate).** If the spec adds a **monitor / watcher / recurring-or-automated notice source** that raises operator notices, its operator-facing raise MUST be DOWNSTREAM of `selfHealAttempted && selfHealExhausted` UNREACHABLE on first detection. A first-detection escalation on a `recoverable` degradation is a **MATERIAL FINDING** (a one-shot conversational reply is out of scope). The watcher MUST declare, and you CONTEST: its self-heal step + `remediation-actions` (concrete operations — a no-op flag-flip is a finding; each side-effecting action needs an idempotency guard + compensation); its P19 brakes — `max-attempts`, `max-wall-clock`, `backoff`, `dedupe-key`, `breaker` (incl. flapping auto-escalation), `max-notification-latency` (explicit units, ≤ `standards.selfHealBeforeNotify.recoverableLatencyCeiling`), `audit-location` (scrubbed); and the severity `class` of each degradation (an irreversible/data-loss/security class escalates IMMEDIATELY and heals concurrently; a critical-as-`recoverable` mislabel is a MATERIAL FINDING). This composes with *No Silent Degradation* — routing to self-heal is auditing, never swallowing.
25
25
 
26
- 6. **Rollback** — if the feature is reverted, what happens to state files, config entries, and background jobs? Is cleanup provided?
26
+ 6. **Backup/restore** — is new persistent state included in the backup manifest? If a user runs a snapshot/restore cycle, does the state survive?
27
27
 
28
- 7. **Dashboard / observability** — is there a UI surface where users can see what's happening? A feature affecting every session should be visible somewhere.
28
+ 7. **Rollback** — if the feature is reverted, what happens to state files, config entries, and background jobs? Is cleanup provided?
29
29
 
30
- 8. **Config knob** — is there a way to disable the feature if it turns out to be harmful? Default on or off?
30
+ 8. **Dashboard / observability** — is there a UI surface where users can see what's happening? A feature affecting every session should be visible somewhere.
31
31
 
32
- 9. **Anything else** about deployment, operations, or integration that might surprise in production.
32
+ 9. **Config knob** is there a way to disable the feature if it turns out to be harmful? Default on or off?
33
+
34
+ 10. **Anything else** about deployment, operations, or integration that might surprise in production.
33
35
 
34
36
  Produce a SHORT report (under 400 words):
35
37
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-03T19:15:15.540Z",
5
- "instarVersion": "1.3.734",
4
+ "generatedAt": "2026-07-03T21:33:39.596Z",
5
+ "instarVersion": "1.3.736",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -11,7 +11,7 @@
11
11
  "domain": "identity",
12
12
  "sourcePath": "src/core/PostUpdateMigrator.ts",
13
13
  "installedPath": ".instar/hooks/instar/session-start.sh",
14
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
14
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
15
15
  "since": "2025-01-01"
16
16
  },
17
17
  "hook:dangerous-command-guard": {
@@ -20,7 +20,7 @@
20
20
  "domain": "safety",
21
21
  "sourcePath": "src/core/PostUpdateMigrator.ts",
22
22
  "installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
23
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
23
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
24
24
  "since": "2025-01-01"
25
25
  },
26
26
  "hook:grounding-before-messaging": {
@@ -29,7 +29,7 @@
29
29
  "domain": "safety",
30
30
  "sourcePath": "src/core/PostUpdateMigrator.ts",
31
31
  "installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
32
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
32
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
33
33
  "since": "2025-01-01"
34
34
  },
35
35
  "hook:compaction-recovery": {
@@ -38,7 +38,7 @@
38
38
  "domain": "identity",
39
39
  "sourcePath": "src/core/PostUpdateMigrator.ts",
40
40
  "installedPath": ".instar/hooks/instar/compaction-recovery.sh",
41
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
41
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
42
42
  "since": "2025-01-01"
43
43
  },
44
44
  "hook:external-operation-gate": {
@@ -47,7 +47,7 @@
47
47
  "domain": "safety",
48
48
  "sourcePath": "src/core/PostUpdateMigrator.ts",
49
49
  "installedPath": ".instar/hooks/instar/external-operation-gate.js",
50
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
50
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
51
51
  "since": "2025-01-01"
52
52
  },
53
53
  "hook:deferral-detector": {
@@ -56,7 +56,7 @@
56
56
  "domain": "safety",
57
57
  "sourcePath": "src/core/PostUpdateMigrator.ts",
58
58
  "installedPath": ".instar/hooks/instar/deferral-detector.js",
59
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
59
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
60
60
  "since": "2025-01-01"
61
61
  },
62
62
  "hook:self-stop-guard": {
@@ -65,7 +65,7 @@
65
65
  "domain": "coherence",
66
66
  "sourcePath": "src/core/PostUpdateMigrator.ts",
67
67
  "installedPath": ".instar/hooks/instar/self-stop-guard.js",
68
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
68
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
69
69
  "since": "2025-01-01"
70
70
  },
71
71
  "hook:post-action-reflection": {
@@ -74,7 +74,7 @@
74
74
  "domain": "evolution",
75
75
  "sourcePath": "src/core/PostUpdateMigrator.ts",
76
76
  "installedPath": ".instar/hooks/instar/post-action-reflection.js",
77
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
77
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
78
78
  "since": "2025-01-01"
79
79
  },
80
80
  "hook:external-communication-guard": {
@@ -83,7 +83,7 @@
83
83
  "domain": "safety",
84
84
  "sourcePath": "src/core/PostUpdateMigrator.ts",
85
85
  "installedPath": ".instar/hooks/instar/external-communication-guard.js",
86
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
86
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
87
87
  "since": "2025-01-01"
88
88
  },
89
89
  "hook:scope-coherence-collector": {
@@ -92,7 +92,7 @@
92
92
  "domain": "coherence",
93
93
  "sourcePath": "src/core/PostUpdateMigrator.ts",
94
94
  "installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
95
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
95
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
96
96
  "since": "2025-01-01"
97
97
  },
98
98
  "hook:scope-coherence-checkpoint": {
@@ -101,7 +101,7 @@
101
101
  "domain": "coherence",
102
102
  "sourcePath": "src/core/PostUpdateMigrator.ts",
103
103
  "installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
104
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
104
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
105
105
  "since": "2025-01-01"
106
106
  },
107
107
  "hook:free-text-guard": {
@@ -110,7 +110,7 @@
110
110
  "domain": "safety",
111
111
  "sourcePath": "src/core/PostUpdateMigrator.ts",
112
112
  "installedPath": ".instar/hooks/instar/free-text-guard.sh",
113
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
113
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
114
114
  "since": "2025-01-01"
115
115
  },
116
116
  "hook:claim-intercept": {
@@ -119,7 +119,7 @@
119
119
  "domain": "coherence",
120
120
  "sourcePath": "src/core/PostUpdateMigrator.ts",
121
121
  "installedPath": ".instar/hooks/instar/claim-intercept.js",
122
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
122
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
123
123
  "since": "2025-01-01"
124
124
  },
125
125
  "hook:claim-intercept-response": {
@@ -128,7 +128,7 @@
128
128
  "domain": "coherence",
129
129
  "sourcePath": "src/core/PostUpdateMigrator.ts",
130
130
  "installedPath": ".instar/hooks/instar/claim-intercept-response.js",
131
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
131
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
132
132
  "since": "2025-01-01"
133
133
  },
134
134
  "hook:stop-gate-router": {
@@ -137,7 +137,7 @@
137
137
  "domain": "safety",
138
138
  "sourcePath": "src/core/PostUpdateMigrator.ts",
139
139
  "installedPath": ".instar/hooks/instar/stop-gate-router.js",
140
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
140
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
141
141
  "since": "2025-01-01"
142
142
  },
143
143
  "hook:auto-approve-permissions": {
@@ -146,7 +146,7 @@
146
146
  "domain": "safety",
147
147
  "sourcePath": "src/core/PostUpdateMigrator.ts",
148
148
  "installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
149
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
149
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
150
150
  "since": "2025-01-01"
151
151
  },
152
152
  "job:health-check": {
@@ -1562,7 +1562,7 @@
1562
1562
  "type": "subsystem",
1563
1563
  "domain": "updates",
1564
1564
  "sourcePath": "src/core/PostUpdateMigrator.ts",
1565
- "contentHash": "57d09bfcdaad6aa9e466cfadf972a7fd27f55d5011fac7f82495ee44856f42c7",
1565
+ "contentHash": "73d28859114fc58c042a25f2cb15703265118eb07a46cc26f333a43653658b1a",
1566
1566
  "since": "2025-01-01"
1567
1567
  },
1568
1568
  "subsystem:scheduler": {
@@ -0,0 +1,67 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Increment 1 of the Class-Closure Gate (`docs/specs/class-closure-gate.md`, converged +
9
+ approved). This is the mechanical arm of the constitutional principle **"Distrust Temporary
10
+ Success: A Recurrence Is a Root Cause"** — until now nothing at fix-time asked *"what CLASS
11
+ of defect is this, and what structural change makes the whole class unrepresentable?"* (the
12
+ question only ever fired because the operator asked it).
13
+
14
+ Increment 1 ships **CI tooling only — no runtime gates**:
15
+
16
+ - A **defect-class registry** (`docs/defect-classes.json`) seeded with the four measured
17
+ classes, each carrying real semantics (includes/excludes/severity/closure-standard).
18
+ - A pure count/threshold **library** (`src/core/DefectClassRegistry.ts`) that derives each
19
+ class's recurrence count from committed instar-dev decision-audit declarations, deduped by
20
+ PR number, and computes deterministic escalation-threshold crossings (critical-at-1;
21
+ normal: ≥3-across-2-components, ≥2+open-gap, or ≥5-in-one-component; seeded-closed
22
+ suppresses historical backfill).
23
+ - A self-contained **guard grader** (`scripts/lib/class-closure-grader.mjs`, mirroring
24
+ `StandardsEnforcementAuditor` with a pinned parity test) that downgrades a `closure:guard`
25
+ declaration to `gap` when the cited guard does not resolve to a live enforcing guard (G3:
26
+ a dark/spec-only artifact guards nothing).
27
+ - A **report-only PR-gate lint** (`scripts/class-closure-lint.mjs`) + CI workflow
28
+ (`.github/workflows/class-closure-gate.yml`, check name `class-closure`) that validates the
29
+ class-declaration field-set on fixes touching agent-authored artifacts and LOGS threshold
30
+ crossings.
31
+ - An author declaration helper (`scripts/class-closure-declare.mjs`) and the side-effects
32
+ template's mirrored declaration section.
33
+
34
+ Config-gated behind `prGate.classClosure = {enabled, dryRun, escalatorDrafting}` (defaults
35
+ off/dry-run) and repo-gated (no-op on installs without `docs/defect-classes.json`). The
36
+ escalator's LLM drafting arm, the proposals writer, the attention-item producer, the runtime
37
+ read route, and the consolidated axis-requirements ratchet are the spec's OWN dark
38
+ **increment 3** — deliberately not in this increment.
39
+
40
+ ## What to Tell Your User
41
+
42
+ Nothing changes for you right now — this ships **dark and report-only**, and it is
43
+ maintainer-only CI machinery (a no-op on your install unless you develop instar itself). It
44
+ is the first brick of a system that will, later and only after an operator sign-off, notice
45
+ when the same *kind* of bug keeps recurring and propose a structural fix for the whole class
46
+ rather than patching instances one at a time. No behavior, message, or command changes today.
47
+
48
+ ## Summary of New Capabilities
49
+
50
+ None active for end users in this increment — everything ships disabled/dry-run and
51
+ repo-gated. (For instar maintainers: a report-only CI lint that logs missing/invalid
52
+ defect-class declarations and deterministic class-recurrence threshold crossings, plus the
53
+ class registry and grading libraries it rides on.)
54
+
55
+ ## Evidence
56
+
57
+ - `npx vitest run tests/unit/class-closure-registry.test.ts tests/unit/class-closure-grader.test.ts tests/unit/class-closure-grader-parity.test.ts tests/unit/class-closure-registry-parity.test.ts tests/integration/class-closure-lint.test.ts`
58
+ → **5 files, 55 tests, 0 failures** (all four escalation arms + seeded-closed suppression +
59
+ newEvidence gate; guard-upheld vs 3 downgrade paths; report-only-exits-0 vs
60
+ enforcing-exits-nonzero for both hard-violation types; both mirror-parity suites).
61
+ - `npx tsc --noEmit` → exit 0, zero errors.
62
+ - Report-only guarantee is structural: `scripts/class-closure-lint.mjs` computes
63
+ `enforcing = config.enabled && !config.dryRun` and returns a nonzero exit ONLY when
64
+ `enforcing && hardViolations.length > 0` — so under the shipped defaults (disabled +
65
+ dry-run) it always exits 0 and cannot fail a PR.
66
+ - Side-effects review: `upgrades/side-effects/class-closure-gate.md` (8 questions + Q7
67
+ multi-machine posture + signal-vs-authority), second-pass reviewed.
@@ -0,0 +1,57 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Enforcement machinery for three operator-ratified constitutional standards, per
9
+ `docs/specs/three-standards-enforcement.md` (converged). This ships the STRUCTURE that makes
10
+ each standard stick — the standard TEXTS ship separately under the already-granted ratification.
11
+
12
+ - **Standard A — reject an undefended machine-local.** The `/spec-converge` integration reviewer
13
+ is upgraded from "a cross-machine posture is *declared*" to "the default posture is `unified`;
14
+ an undefended `machine-local` is a MATERIAL FINDING." A machine-local surface is now allowed
15
+ only with a `machine-local-justification: <key>` marker drawn from a CLOSED taxonomy
16
+ (`physical-credential-locality` / `hardware-bound-resource` / `operator-ratified-exception`),
17
+ and the check is bidirectional (an infeasible `unified` is equally a finding). The marker is the
18
+ cheap deterministic signal; the LLM reviewer holds the semantic authority (Signal vs. Authority).
19
+ - **Standard B — self-heal before notify.** A new `/spec-converge` review-check that flags any
20
+ monitor/watcher/recurring-notice-source whose operator-facing escalation is reachable on first
21
+ detection. A watcher must declare its self-heal step, its `remediation-actions`, its P19 brakes
22
+ (including flapping detection and a `max-notification-latency`), and a contested severity class.
23
+ - **Standard C — alerts-topic routing default.** A table-driven routing CONTRACT test at the
24
+ Telegram adapter boundary proving a topic-less non-critical notice routes to the one hub topic by
25
+ default, HIGH/URGENT keep their own topic, an existing-owning-topic send mints no new topic, and
26
+ an unresolvable hub falls back safely — plus a guard that the hub id is never a baked-in constant.
27
+ - **Migration Parity.** `migrateThreeStandardsReviewChecks` re-copies the two upgraded spec-converge
28
+ skill files to already-installed agents, mirroring the existing multi-machine-posture migration.
29
+
30
+ Honest framing: the purely-deterministic marker/field lints for A and B are hard-sequenced to land
31
+ WITH each standard's registry guard, not in this change; until then A/B enforcement is the per-spec
32
+ conformance-check gate plus the LLM review-lens (a semantic audit), never a no-LLM guarantee.
33
+
34
+ ## What to Tell Your User
35
+
36
+ Nothing changes in how you talk to me day to day. This is internal development plumbing: when I
37
+ design a new capability, my own spec-review now pushes back harder on two mistakes it used to let
38
+ slide — quietly assuming a feature only ever runs on one of your machines, and building a watchdog
39
+ that pings you the instant it sees a problem instead of trying to fix it first and telling you only
40
+ when it genuinely cannot. It also adds a test proving stray housekeeping notices land in your one
41
+ alerts topic instead of spawning a new topic each time. You do not need to do anything.
42
+
43
+ ## Summary of New Capabilities
44
+
45
+ No new user-facing capability. Internal enforcement only: stronger spec-review checks for
46
+ cross-machine posture and self-heal-before-notify, an idempotent migration so existing agents
47
+ receive the upgraded checks, and a routing contract test for topic-less notices.
48
+
49
+ ## Evidence
50
+
51
+ - `npx vitest run tests/unit/PostUpdateMigrator-threeStandardsReviewChecks.test.ts` — 7 passed
52
+ (content-presence of the A/B review-lenses in the shipped prompts + migration idempotency and
53
+ fingerprint-guard).
54
+ - `npx vitest run tests/integration/notification-flood-burst-invariant.test.ts` — 12 passed
55
+ (5 new Standard C routing-contract cases + 7 existing burst-bound cases).
56
+ - `npx tsc --noEmit` — clean.
57
+ - Side-effects review: `upgrades/side-effects/three-standards-enforcement.md`.