instar 1.3.768 → 1.3.769
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/package.json +1 -1
- package/scripts/lint-machine-local-justification.js +273 -0
- package/scripts/lint-self-heal-fields.js +259 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.769.md +62 -0
- package/upgrades/eli16/act1174-lint-floors.eli16.md +75 -0
- package/upgrades/side-effects/act1174-lint-floors.md +124 -0
package/package.json
CHANGED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-machine-local-justification.js — the DETERMINISTIC marker floor for
|
|
4
|
+
* Standard A ("An Instar Agent Is Always a Multi-Machine Entity",
|
|
5
|
+
* docs/STANDARDS-REGISTRY.md), built per docs/specs/three-standards-enforcement.md
|
|
6
|
+
* §178-202 ("The marker's location + parse contract").
|
|
7
|
+
*
|
|
8
|
+
* ── What it is (Signal vs. Authority — the BODY, not the MIND) ─────────────
|
|
9
|
+
* A no-LLM static parser that grades the PRESENCE + well-formedness of the
|
|
10
|
+
* `machine-local-justification: <taxonomy-key>` marker a spec must carry for each
|
|
11
|
+
* machine-local state surface it introduces. It is the cheap deterministic SIGNAL
|
|
12
|
+
* that front-runs the /spec-converge integration reviewer; the reviewer holds the
|
|
13
|
+
* semantic AUTHORITY (is the justification actually TRUE?) that a parser cannot
|
|
14
|
+
* make (§194-202). Neither alone is the enforcement — together they are.
|
|
15
|
+
*
|
|
16
|
+
* ── The contract it checks (bidirectional, §170) ──────────────────────────
|
|
17
|
+
* A1 — UNDEFENDED machine-local: the spec's `## Multi-machine posture` section
|
|
18
|
+
* ASSERTS a machine-local posture (contains the token `machine-local`) but
|
|
19
|
+
* carries NO well-formed `machine-local-justification:` marker. §163: "a
|
|
20
|
+
* bare 'machine-local BY DESIGN' with no taxonomy-jailed justification is a
|
|
21
|
+
* MATERIAL FINDING."
|
|
22
|
+
* A2 — SPURIOUS / MALFORMED marker (the reverse direction, §170): a
|
|
23
|
+
* `machine-local-justification:` marker whose key is NOT in the closed
|
|
24
|
+
* taxonomy {physical-credential-locality, hardware-bound-resource,
|
|
25
|
+
* operator-ratified-exception}; OR an `operator-ratified-exception` marker
|
|
26
|
+
* that cites no machine-verifiable, existence-checkable ref (a commit SHA,
|
|
27
|
+
* a dotted registry key, or a URL — §155-162: a bare topic+date fails
|
|
28
|
+
* DETERMINISTICALLY); OR a well-formed marker that sits OUTSIDE the
|
|
29
|
+
* `## Multi-machine posture` section (the location contract, §178-181).
|
|
30
|
+
*
|
|
31
|
+
* ── Honest deterministic scope (§194-202, §242-245) ───────────────────────
|
|
32
|
+
* PRESENCE + well-formedness only. It does NOT judge whether a declared posture
|
|
33
|
+
* is the CORRECT one for the surface (that is the reviewer's semantic audit), and
|
|
34
|
+
* it does NOT flag a spec that simply omits a posture section — §168's
|
|
35
|
+
* "absence defaults to unified-required" is a semantic call the reviewer owns, not
|
|
36
|
+
* a deterministic one. A `<placeholder>` marker value (template prose like
|
|
37
|
+
* `machine-local-justification: <taxonomy-key>`) is IGNORED — it is documentation,
|
|
38
|
+
* not a real marker.
|
|
39
|
+
*
|
|
40
|
+
* ── Rollout MODE — REPORT-FIRST (graduated rollout) ───────────────────────
|
|
41
|
+
* Per the spec's honesty / hard-sequencing clause (§197-202, §563-573) and the
|
|
42
|
+
* dark-first Maturation convention: the deterministic marker lint's grade is
|
|
43
|
+
* "inert until the registry ship" — this floor lands as a SIGNAL first, not a new
|
|
44
|
+
* brittle blocking gate. Default run REPORTS findings and exits 0 (never blocks;
|
|
45
|
+
* existing specs predate the marker convention and are swept separately, §242-245).
|
|
46
|
+
* `--strict` makes findings exit non-zero — the FAIL capability the spec describes
|
|
47
|
+
* (§197-199), used by this lint's tests and available for a later CI graduation.
|
|
48
|
+
*
|
|
49
|
+
* Exit codes:
|
|
50
|
+
* 0 — clean, OR findings in report mode (default)
|
|
51
|
+
* 1 — findings in --strict mode, OR a usage error
|
|
52
|
+
*
|
|
53
|
+
* Usage:
|
|
54
|
+
* node scripts/lint-machine-local-justification.js # report, scans docs/specs
|
|
55
|
+
* node scripts/lint-machine-local-justification.js FILE... # report specific files
|
|
56
|
+
* node scripts/lint-machine-local-justification.js --strict FILE # FAIL on any finding
|
|
57
|
+
* node scripts/lint-machine-local-justification.js --json FILE # machine-readable findings
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import fs from 'node:fs';
|
|
61
|
+
import path from 'node:path';
|
|
62
|
+
import { fileURLToPath } from 'node:url';
|
|
63
|
+
|
|
64
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
65
|
+
const ROOT = path.resolve(path.dirname(__filename), '..');
|
|
66
|
+
|
|
67
|
+
// ── The closed taxonomy (§148-162). Widening it is a constitution-bound
|
|
68
|
+
// operator decision, never an author's convenience (§205-219). ──────────
|
|
69
|
+
export const TAXONOMY_KEYS = new Set([
|
|
70
|
+
'physical-credential-locality',
|
|
71
|
+
'hardware-bound-resource',
|
|
72
|
+
'operator-ratified-exception',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const POSTURE_SECTION_RE = /^#{1,6}\s+Multi-machine posture\b.*$/im;
|
|
76
|
+
|
|
77
|
+
// A standalone marker line: start-of-line (optional bullet / backtick), the
|
|
78
|
+
// label, a colon, then `key rest`. Anchored so a mid-sentence backticked prose
|
|
79
|
+
// mention (e.g. "declares it with a `machine-local-justification: <key>` marker")
|
|
80
|
+
// does NOT match — only a real declaration line does.
|
|
81
|
+
const MARKER_LINE_RE =
|
|
82
|
+
/^[ \t]*(?:[-*+][ \t]+)?`?machine-local-justification`?[ \t]*:[ \t]*`?([^\s`]+)`?[ \t]*(.*?)`?[ \t]*$/gim;
|
|
83
|
+
|
|
84
|
+
// A machine-verifiable, existence-checkable ref for operator-ratified-exception
|
|
85
|
+
// (§155-162): a commit SHA (7-40 hex), a URL, or a dotted registry key.
|
|
86
|
+
const REF_SHA_RE = /\b[0-9a-f]{7,40}\b/i;
|
|
87
|
+
const REF_URL_RE = /https?:\/\/\S+/i;
|
|
88
|
+
const REF_REGISTRY_KEY_RE = /\b[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_]+){1,}\b/;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Locate the bounds of the `## Multi-machine posture` section in a spec's text.
|
|
92
|
+
* Returns { start, end } character offsets, or null when the section is absent.
|
|
93
|
+
* The section runs from its heading to the next heading of equal-or-higher level
|
|
94
|
+
* (or EOF).
|
|
95
|
+
*/
|
|
96
|
+
export function findPostureSection(text) {
|
|
97
|
+
const m = POSTURE_SECTION_RE.exec(text);
|
|
98
|
+
if (!m) return null;
|
|
99
|
+
const headingLevel = (m[0].match(/^#+/) || ['#'])[0].length;
|
|
100
|
+
const start = m.index;
|
|
101
|
+
const afterHeading = start + m[0].length;
|
|
102
|
+
// Find the next heading at <= headingLevel after the section starts.
|
|
103
|
+
const rest = text.slice(afterHeading);
|
|
104
|
+
const nextHeadingRe = new RegExp(`^#{1,${headingLevel}}\\s+\\S`, 'im');
|
|
105
|
+
const nm = nextHeadingRe.exec(rest);
|
|
106
|
+
const end = nm ? afterHeading + nm.index : text.length;
|
|
107
|
+
return { start, end };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse every standalone `machine-local-justification:` marker line in the text.
|
|
112
|
+
* `<placeholder>` values (template prose) are skipped. Returns
|
|
113
|
+
* [{ key, rest, index }].
|
|
114
|
+
*/
|
|
115
|
+
export function parseMarkers(text) {
|
|
116
|
+
const out = [];
|
|
117
|
+
MARKER_LINE_RE.lastIndex = 0;
|
|
118
|
+
let m;
|
|
119
|
+
while ((m = MARKER_LINE_RE.exec(text)) !== null) {
|
|
120
|
+
const key = m[1];
|
|
121
|
+
const rest = (m[2] || '').trim();
|
|
122
|
+
// Skip template placeholders like `<taxonomy-key>` / `<key>` — documentation,
|
|
123
|
+
// not a real declaration.
|
|
124
|
+
if (/^<.*>$/.test(key)) continue;
|
|
125
|
+
out.push({ key, rest, index: m.index });
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** True if `rest` carries a machine-verifiable, existence-checkable ref. */
|
|
131
|
+
export function hasResolvableRef(rest) {
|
|
132
|
+
if (!rest) return false;
|
|
133
|
+
return REF_SHA_RE.test(rest) || REF_URL_RE.test(rest) || REF_REGISTRY_KEY_RE.test(rest);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Grade one spec's text against Standard A's marker contract. Pure — no I/O.
|
|
138
|
+
* Returns { findings: [{ rule, message }] }.
|
|
139
|
+
*/
|
|
140
|
+
export function gradeMachineLocalMarkers(text) {
|
|
141
|
+
const findings = [];
|
|
142
|
+
const markers = parseMarkers(text);
|
|
143
|
+
const posture = findPostureSection(text);
|
|
144
|
+
|
|
145
|
+
// ── A2 — spurious / malformed markers (both directions, §170) ──
|
|
146
|
+
for (const marker of markers) {
|
|
147
|
+
if (!TAXONOMY_KEYS.has(marker.key)) {
|
|
148
|
+
findings.push({
|
|
149
|
+
rule: 'A2-invalid-taxonomy-key',
|
|
150
|
+
message:
|
|
151
|
+
`machine-local-justification key "${marker.key}" is not in the closed taxonomy ` +
|
|
152
|
+
`{${[...TAXONOMY_KEYS].join(', ')}}. A key outside the set is a MATERIAL FINDING ` +
|
|
153
|
+
`(a fourth reason is an operator-ratified constitutional decision, §205-219).`,
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (marker.key === 'operator-ratified-exception' && !hasResolvableRef(marker.rest)) {
|
|
158
|
+
findings.push({
|
|
159
|
+
rule: 'A2-unresolvable-ratification-ref',
|
|
160
|
+
message:
|
|
161
|
+
`operator-ratified-exception must cite a machine-verifiable, existence-checkable ref ` +
|
|
162
|
+
`(a commit SHA, a URL, or a dotted registry key) — a bare topic+date fails ` +
|
|
163
|
+
`deterministically (§155-162). Marker value: "${marker.rest || '(empty)'}".`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Location contract (§178-181): a real marker belongs inside `## Multi-machine posture`.
|
|
167
|
+
if (posture && (marker.index < posture.start || marker.index >= posture.end)) {
|
|
168
|
+
findings.push({
|
|
169
|
+
rule: 'A2-marker-outside-posture-section',
|
|
170
|
+
message:
|
|
171
|
+
`machine-local-justification marker ("${marker.key}") sits OUTSIDE the ` +
|
|
172
|
+
`## Multi-machine posture section — the marker's fixed location is what makes ` +
|
|
173
|
+
`PRESENCE machine-checkable (§178-181).`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── A1 — undefended machine-local assertion (§163) ──
|
|
179
|
+
// Scope the trigger to the posture section so a mere prose mention of
|
|
180
|
+
// "machine-local" elsewhere in the spec is not a false positive.
|
|
181
|
+
if (posture) {
|
|
182
|
+
const sectionText = text.slice(posture.start, posture.end);
|
|
183
|
+
const assertsMachineLocal = /machine-local/i.test(sectionText);
|
|
184
|
+
const hasValidMarker = markers.some(
|
|
185
|
+
(mk) =>
|
|
186
|
+
TAXONOMY_KEYS.has(mk.key) &&
|
|
187
|
+
mk.index >= posture.start &&
|
|
188
|
+
mk.index < posture.end &&
|
|
189
|
+
(mk.key !== 'operator-ratified-exception' || hasResolvableRef(mk.rest)),
|
|
190
|
+
);
|
|
191
|
+
if (assertsMachineLocal && !hasValidMarker) {
|
|
192
|
+
findings.push({
|
|
193
|
+
rule: 'A1-undefended-machine-local',
|
|
194
|
+
message:
|
|
195
|
+
`the ## Multi-machine posture section asserts a machine-local posture but carries no ` +
|
|
196
|
+
`well-formed machine-local-justification: <taxonomy-key> marker. A bare ` +
|
|
197
|
+
`"machine-local BY DESIGN" with no taxonomy-jailed justification is a MATERIAL FINDING ` +
|
|
198
|
+
`(§163). Default posture is unified (§148).`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { findings };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── CLI ────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function listSpecFiles() {
|
|
209
|
+
const dir = path.join(ROOT, 'docs', 'specs');
|
|
210
|
+
const out = [];
|
|
211
|
+
const walk = (d) => {
|
|
212
|
+
let entries;
|
|
213
|
+
try {
|
|
214
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
215
|
+
} catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const e of entries) {
|
|
219
|
+
const p = path.join(d, e.name);
|
|
220
|
+
if (e.isDirectory()) walk(p);
|
|
221
|
+
else if (e.isFile() && e.name.endsWith('.md')) out.push(p);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
walk(dir);
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function main() {
|
|
229
|
+
const argv = process.argv.slice(2);
|
|
230
|
+
const strict = argv.includes('--strict');
|
|
231
|
+
const json = argv.includes('--json');
|
|
232
|
+
const files = argv.filter((a) => !a.startsWith('--'));
|
|
233
|
+
const targets = files.length ? files : listSpecFiles();
|
|
234
|
+
|
|
235
|
+
const allFindings = [];
|
|
236
|
+
for (const file of targets) {
|
|
237
|
+
let text;
|
|
238
|
+
try {
|
|
239
|
+
text = fs.readFileSync(file, 'utf8');
|
|
240
|
+
} catch {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const { findings } = gradeMachineLocalMarkers(text);
|
|
244
|
+
for (const f of findings) allFindings.push({ file: path.relative(ROOT, path.resolve(file)), ...f });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (json) {
|
|
248
|
+
process.stdout.write(JSON.stringify({ findings: allFindings, strict }, null, 2) + '\n');
|
|
249
|
+
} else if (allFindings.length === 0) {
|
|
250
|
+
console.log('lint-machine-local-justification: clean — no undefended or malformed markers.');
|
|
251
|
+
} else {
|
|
252
|
+
const header = strict
|
|
253
|
+
? 'lint-machine-local-justification: FINDINGS (strict — blocking):'
|
|
254
|
+
: 'lint-machine-local-justification: findings (report-first — non-blocking signal):';
|
|
255
|
+
console.error(header);
|
|
256
|
+
for (const f of allFindings) {
|
|
257
|
+
console.error(` • [${f.rule}] ${f.file}`);
|
|
258
|
+
console.error(` ${f.message}`);
|
|
259
|
+
}
|
|
260
|
+
console.error(
|
|
261
|
+
`\n ${allFindings.length} finding(s). Standard A: docs/STANDARDS-REGISTRY.md ` +
|
|
262
|
+
`("An Instar Agent Is Always a Multi-Machine Entity").`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (strict && allFindings.length > 0) process.exit(1);
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Only run the CLI when invoked directly (not when imported by a test).
|
|
271
|
+
const invokedDirectly =
|
|
272
|
+
process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(__filename);
|
|
273
|
+
if (invokedDirectly) main();
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-self-heal-fields.js — the DETERMINISTIC field-schema floor for Standard B
|
|
4
|
+
* ("Self-Heal Before Notify", docs/STANDARDS-REGISTRY.md), built per
|
|
5
|
+
* docs/specs/three-standards-enforcement.md §256-289, §343-361.
|
|
6
|
+
*
|
|
7
|
+
* ── What it is (Signal vs. Authority — the BODY, not the MIND) ─────────────
|
|
8
|
+
* A no-LLM static parser over a spec's self-heal DECLARATION. When a spec adds a
|
|
9
|
+
* watcher/monitor that can escalate to the operator, it must declare a bounded
|
|
10
|
+
* self-heal step with the P19 brake fields (§265-283). `remediation-actions` is
|
|
11
|
+
* the deterministic anti-no-op SIGNAL (§270): listing the concrete operations the
|
|
12
|
+
* heal invokes makes "did this heal actually DO something?" machine-inspectable,
|
|
13
|
+
* so the /spec-converge reviewer's substance judgment becomes a semantic AUDIT
|
|
14
|
+
* over a deterministic floor rather than the sole guard. This lint IS that floor;
|
|
15
|
+
* the reviewer holds the authority (is the heal SUBSTANTIVE? is the severity class
|
|
16
|
+
* CORRECT?) a parser cannot judge.
|
|
17
|
+
*
|
|
18
|
+
* ── The contract it checks ────────────────────────────────────────────────
|
|
19
|
+
* B0 — IN SCOPE: a spec declares a self-heal block iff it carries a
|
|
20
|
+
* `remediation-actions:` labeled line (the anchor field, §270). No anchor →
|
|
21
|
+
* not in scope (a spec with no watcher pays no self-heal cost, §353-355).
|
|
22
|
+
* B1 — REQUIRED FIELDS present (§265-283, §592): max-attempts, max-wall-clock,
|
|
23
|
+
* backoff, dedupe-key, breaker, max-notification-latency, audit-location,
|
|
24
|
+
* remediation-actions, class. A missing field is a MATERIAL FINDING.
|
|
25
|
+
* B2 — NO-OP heal: remediation-actions must be non-empty (inline value OR ≥1
|
|
26
|
+
* following bullet). An empty list is the no-op that merely flips a flag to
|
|
27
|
+
* unlock the escalation path (§286-291) — a MATERIAL FINDING.
|
|
28
|
+
* B3 — LATENCY UNITS: max-notification-latency must be a duration WITH units
|
|
29
|
+
* (e.g. `120s`, `5m`) — never a bare number or an adjective (§318-323).
|
|
30
|
+
* B4 — SEVERITY CLASS value: class must be a recognized class
|
|
31
|
+
* {recoverable, critical, irreversible, data-loss, security}. (The lint
|
|
32
|
+
* grades the value's WELL-FORMEDNESS; the reviewer CONTESTS its
|
|
33
|
+
* correctness, §306-310.)
|
|
34
|
+
*
|
|
35
|
+
* ── Honest deterministic scope (§357-361, §194-202) ───────────────────────
|
|
36
|
+
* PRESENCE + well-formedness of the declared fields only. It does NOT judge
|
|
37
|
+
* whether a heal is genuinely SUBSTANTIVE, whether a `recoverable` label is honest,
|
|
38
|
+
* or whether exhaustion is truly reachable — those are the reviewer's semantic
|
|
39
|
+
* audit and (for the runtime gate) the downstream SelfHealGate fixture (§374-389).
|
|
40
|
+
*
|
|
41
|
+
* ── Rollout MODE — REPORT-FIRST (graduated rollout) ───────────────────────
|
|
42
|
+
* Per the spec's honesty / hard-sequencing clause (§357-361, §563-573) and the
|
|
43
|
+
* dark-first Maturation convention: this floor lands as a SIGNAL first, not a new
|
|
44
|
+
* brittle blocking gate. Default run REPORTS findings and exits 0 (never blocks).
|
|
45
|
+
* `--strict` makes findings exit non-zero — the FAIL capability, used by this
|
|
46
|
+
* lint's tests and available for a later CI graduation.
|
|
47
|
+
*
|
|
48
|
+
* Exit codes:
|
|
49
|
+
* 0 — clean, OR findings in report mode (default)
|
|
50
|
+
* 1 — findings in --strict mode, OR a usage error
|
|
51
|
+
*
|
|
52
|
+
* Usage:
|
|
53
|
+
* node scripts/lint-self-heal-fields.js # report, scans docs/specs
|
|
54
|
+
* node scripts/lint-self-heal-fields.js FILE... # report specific files
|
|
55
|
+
* node scripts/lint-self-heal-fields.js --strict FILE # FAIL on any finding
|
|
56
|
+
* node scripts/lint-self-heal-fields.js --json FILE # machine-readable findings
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import fs from 'node:fs';
|
|
60
|
+
import path from 'node:path';
|
|
61
|
+
import { fileURLToPath } from 'node:url';
|
|
62
|
+
|
|
63
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
64
|
+
const ROOT = path.resolve(path.dirname(__filename), '..');
|
|
65
|
+
|
|
66
|
+
// The full P19-brake + declaration field set a self-heal watcher must declare
|
|
67
|
+
// (§265-283, §592). `remediation-actions` is the in-scope anchor (§270).
|
|
68
|
+
export const REQUIRED_FIELDS = [
|
|
69
|
+
'max-attempts',
|
|
70
|
+
'max-wall-clock',
|
|
71
|
+
'backoff',
|
|
72
|
+
'dedupe-key',
|
|
73
|
+
'breaker',
|
|
74
|
+
'max-notification-latency',
|
|
75
|
+
'audit-location',
|
|
76
|
+
'remediation-actions',
|
|
77
|
+
'class',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const ANCHOR_FIELD = 'remediation-actions';
|
|
81
|
+
|
|
82
|
+
// The severity classes an author may declare (§306). The reviewer CONTESTS
|
|
83
|
+
// correctness; the lint only grades that the declared value is well-formed.
|
|
84
|
+
export const SEVERITY_CLASSES = new Set([
|
|
85
|
+
'recoverable',
|
|
86
|
+
'critical',
|
|
87
|
+
'irreversible',
|
|
88
|
+
'data-loss',
|
|
89
|
+
'security',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
// A duration WITH units: an integer/decimal followed by ms | s | m | h (§318-323).
|
|
93
|
+
const DURATION_WITH_UNITS_RE = /^\d+(?:\.\d+)?\s*(?:ms|s|m|h)$/i;
|
|
94
|
+
|
|
95
|
+
function fieldLineRe(name) {
|
|
96
|
+
const escaped = name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
97
|
+
// start-of-line, optional bullet, optional backticks, label, colon, value.
|
|
98
|
+
return new RegExp(`^[ \\t]*(?:[-*+][ \\t]+)?\`?${escaped}\`?[ \\t]*:[ \\t]*(.*)$`, 'im');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Return the inline value string for a labeled field, or null if absent. */
|
|
102
|
+
export function findField(text, name) {
|
|
103
|
+
const m = fieldLineRe(name).exec(text);
|
|
104
|
+
if (!m) return null;
|
|
105
|
+
// Strip a trailing backtick and surrounding whitespace/backticks from the value.
|
|
106
|
+
return m[1].replace(/`/g, '').trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* True if the field's list value is non-empty: either an inline value after the
|
|
111
|
+
* colon, OR at least one bullet line following the field's declaration line.
|
|
112
|
+
*/
|
|
113
|
+
export function listFieldIsNonEmpty(text, name) {
|
|
114
|
+
const re = fieldLineRe(name);
|
|
115
|
+
const m = re.exec(text);
|
|
116
|
+
if (!m) return false;
|
|
117
|
+
const inline = m[1].replace(/[`[\]]/g, '').trim();
|
|
118
|
+
if (inline.length > 0) return true;
|
|
119
|
+
// Scan the lines immediately after the field line for bullets, stopping at a
|
|
120
|
+
// blank line or the next labeled field / heading. Strip the single newline
|
|
121
|
+
// that terminates the field line itself so it doesn't read as an empty line.
|
|
122
|
+
const after = text.slice(m.index + m[0].length).replace(/^\r?\n/, '');
|
|
123
|
+
const lines = after.split('\n');
|
|
124
|
+
for (const raw of lines) {
|
|
125
|
+
const line = raw.replace(/\r$/, '');
|
|
126
|
+
if (/^\s*$/.test(line)) break; // blank line ends the block
|
|
127
|
+
if (/^#{1,6}\s/.test(line)) break; // next heading
|
|
128
|
+
if (/^[ \t]*[-*+][ \t]+\S/.test(line)) return true; // a real bullet with content
|
|
129
|
+
if (/^[ \t]*\S+[ \t]*:/.test(line)) break; // next labeled field
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Grade one spec's text against Standard B's self-heal field schema. Pure — no
|
|
136
|
+
* I/O. Returns { inScope, findings: [{ rule, message }] }.
|
|
137
|
+
*/
|
|
138
|
+
export function gradeSelfHealFields(text) {
|
|
139
|
+
const findings = [];
|
|
140
|
+
const inScope = findField(text, ANCHOR_FIELD) !== null;
|
|
141
|
+
if (!inScope) return { inScope: false, findings };
|
|
142
|
+
|
|
143
|
+
// B1 — required fields present.
|
|
144
|
+
for (const field of REQUIRED_FIELDS) {
|
|
145
|
+
if (findField(text, field) === null) {
|
|
146
|
+
findings.push({
|
|
147
|
+
rule: 'B1-missing-field',
|
|
148
|
+
message:
|
|
149
|
+
`self-heal declaration is missing required field "${field}". A watcher's bounded ` +
|
|
150
|
+
`self-heal must declare its full P19 brake set (§265-283) — an unbounded or ` +
|
|
151
|
+
`undeclared brake is the compounding-loop failure the standard forbids.`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// B2 — remediation-actions must not be a no-op (non-empty).
|
|
157
|
+
if (!listFieldIsNonEmpty(text, ANCHOR_FIELD)) {
|
|
158
|
+
findings.push({
|
|
159
|
+
rule: 'B2-noop-remediation',
|
|
160
|
+
message:
|
|
161
|
+
`remediation-actions is empty. A heal that names no concrete operation is a no-op that ` +
|
|
162
|
+
`merely unlocks the escalation path (§286-291) — the anti-no-op floor requires ≥1 ` +
|
|
163
|
+
`substantive action (e.g. re-register-flag, restart-tracker, re-deliver-report).`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// B3 — max-notification-latency must carry units.
|
|
168
|
+
const latency = findField(text, 'max-notification-latency');
|
|
169
|
+
if (latency !== null && latency.length > 0 && !DURATION_WITH_UNITS_RE.test(latency)) {
|
|
170
|
+
findings.push({
|
|
171
|
+
rule: 'B3-latency-unitless',
|
|
172
|
+
message:
|
|
173
|
+
`max-notification-latency "${latency}" must be an explicit duration WITH units ` +
|
|
174
|
+
`(e.g. 120s, 5m) — never a bare number or an adjective (§318-323).`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// B4 — severity class value well-formed.
|
|
179
|
+
const cls = findField(text, 'class');
|
|
180
|
+
if (cls !== null && cls.length > 0 && !SEVERITY_CLASSES.has(cls.toLowerCase())) {
|
|
181
|
+
findings.push({
|
|
182
|
+
rule: 'B4-unknown-severity-class',
|
|
183
|
+
message:
|
|
184
|
+
`severity class "${cls}" is not a recognized class ` +
|
|
185
|
+
`{${[...SEVERITY_CLASSES].join(', ')}}. (The reviewer CONTESTS whether the class is ` +
|
|
186
|
+
`CORRECT for the degradation, §306-310; the lint checks it is well-formed.)`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { inScope: true, findings };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── CLI ────────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function listSpecFiles() {
|
|
196
|
+
const dir = path.join(ROOT, 'docs', 'specs');
|
|
197
|
+
const out = [];
|
|
198
|
+
const walk = (d) => {
|
|
199
|
+
let entries;
|
|
200
|
+
try {
|
|
201
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
202
|
+
} catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const e of entries) {
|
|
206
|
+
const p = path.join(d, e.name);
|
|
207
|
+
if (e.isDirectory()) walk(p);
|
|
208
|
+
else if (e.isFile() && e.name.endsWith('.md')) out.push(p);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
walk(dir);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function main() {
|
|
216
|
+
const argv = process.argv.slice(2);
|
|
217
|
+
const strict = argv.includes('--strict');
|
|
218
|
+
const json = argv.includes('--json');
|
|
219
|
+
const files = argv.filter((a) => !a.startsWith('--'));
|
|
220
|
+
const targets = files.length ? files : listSpecFiles();
|
|
221
|
+
|
|
222
|
+
const allFindings = [];
|
|
223
|
+
for (const file of targets) {
|
|
224
|
+
let text;
|
|
225
|
+
try {
|
|
226
|
+
text = fs.readFileSync(file, 'utf8');
|
|
227
|
+
} catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const { findings } = gradeSelfHealFields(text);
|
|
231
|
+
for (const f of findings) allFindings.push({ file: path.relative(ROOT, path.resolve(file)), ...f });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (json) {
|
|
235
|
+
process.stdout.write(JSON.stringify({ findings: allFindings, strict }, null, 2) + '\n');
|
|
236
|
+
} else if (allFindings.length === 0) {
|
|
237
|
+
console.log('lint-self-heal-fields: clean — every self-heal declaration carries its field set.');
|
|
238
|
+
} else {
|
|
239
|
+
const header = strict
|
|
240
|
+
? 'lint-self-heal-fields: FINDINGS (strict — blocking):'
|
|
241
|
+
: 'lint-self-heal-fields: findings (report-first — non-blocking signal):';
|
|
242
|
+
console.error(header);
|
|
243
|
+
for (const f of allFindings) {
|
|
244
|
+
console.error(` • [${f.rule}] ${f.file}`);
|
|
245
|
+
console.error(` ${f.message}`);
|
|
246
|
+
}
|
|
247
|
+
console.error(
|
|
248
|
+
`\n ${allFindings.length} finding(s). Standard B: docs/STANDARDS-REGISTRY.md ` +
|
|
249
|
+
`("Self-Heal Before Notify").`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (strict && allFindings.length > 0) process.exit(1);
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const invokedDirectly =
|
|
258
|
+
process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(__filename);
|
|
259
|
+
if (invokedDirectly) main();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-05T02:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-05T02:48:30.887Z",
|
|
5
|
+
"instarVersion": "1.3.769",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Adds two enforcement lints — deterministic, no-LLM parser scripts — that back the two
|
|
9
|
+
already-ratified constitutional standards A ("An Instar Agent Is Always a Multi-Machine
|
|
10
|
+
Entity") and B ("Self-Heal Before Notify") with a cheap structural signal, per
|
|
11
|
+
`docs/specs/three-standards-enforcement.md` (§178-202 for A, §256-289 / §343-361 for B).
|
|
12
|
+
|
|
13
|
+
- `scripts/lint-machine-local-justification.js` grades a spec's `machine-local-justification: <taxonomy-key>`
|
|
14
|
+
marker: an undefended machine-local surface is a finding, and — bidirectionally — a
|
|
15
|
+
spurious/out-of-taxonomy marker, or an `operator-ratified-exception` citing no
|
|
16
|
+
machine-verifiable ref, is a finding too.
|
|
17
|
+
- `scripts/lint-self-heal-fields.js` grades a spec's self-heal declaration (anchored on
|
|
18
|
+
the `remediation-actions` field): it requires the full P19 brake set, a non-empty
|
|
19
|
+
`remediation-actions` list (the anti-no-op floor), a units-carrying
|
|
20
|
+
`max-notification-latency`, and a well-formed severity class.
|
|
21
|
+
|
|
22
|
+
Both ship REPORT-FIRST — they print findings and exit 0 by default (a non-blocking
|
|
23
|
+
signal); a `--strict` flag is the FAIL capability used by their tests and available for a
|
|
24
|
+
later CI graduation. Each is registered in `docs/STANDARDS-REGISTRY.md` so the
|
|
25
|
+
Standards Enforcement-Coverage auditor now grades Standards A and B as enforced by a
|
|
26
|
+
deterministic lint (verified: two more standards move from unenforced to lint-enforced).
|
|
27
|
+
No constitutional text is minted or re-ratified; no runtime `src/` code changes.
|
|
28
|
+
|
|
29
|
+
## What to Tell Your User
|
|
30
|
+
|
|
31
|
+
Nothing proactive — this is instar-developing-agent tooling and nothing changes for a
|
|
32
|
+
user. If a user asks how the constitution's "always multi-machine" and "self-heal before
|
|
33
|
+
telling me" rules are actually enforced now: two small automatic checkers read a design
|
|
34
|
+
document's plain text and flag when it forgets to say which machine a piece of state
|
|
35
|
+
lives on, or writes a self-repair plan that would do nothing or never stop trying. For now
|
|
36
|
+
these run in report-only mode — they point out the problem but do not block anything — and
|
|
37
|
+
they sit underneath the smart reviewer that still makes the real judgment call, rather than
|
|
38
|
+
replacing it.
|
|
39
|
+
|
|
40
|
+
## Summary of New Capabilities
|
|
41
|
+
|
|
42
|
+
- **Standard A marker floor** — a no-AI checker that flags an undefended machine-local
|
|
43
|
+
state surface in a spec, and also flags the reverse: a made-up justification key or an
|
|
44
|
+
operator-ratified claim with no verifiable proof.
|
|
45
|
+
- **Standard B self-heal floor** — a no-AI checker that flags a watchdog self-heal plan
|
|
46
|
+
missing its brakes, listing no real repair actions, or writing a notification deadline
|
|
47
|
+
with no time units.
|
|
48
|
+
- **Report-first rollout** — both checkers signal without blocking by default; a strict
|
|
49
|
+
mode exists for their tests and a future graduation.
|
|
50
|
+
- **Constitution coverage** — Standards A and B are now graded as lint-enforced by the
|
|
51
|
+
Standards Enforcement-Coverage auditor, not merely documented.
|
|
52
|
+
|
|
53
|
+
## Evidence
|
|
54
|
+
|
|
55
|
+
- `tests/unit/lint-machine-local-justification.test.ts` + `tests/unit/lint-self-heal-fields.test.ts`
|
|
56
|
+
— 13 unit tests (7 A + 6 B): positive/defended cases pass; undefended (A1),
|
|
57
|
+
spurious-key (A2), and no-ref (A2) fail under strict; missing-brakes (B1), no-op +
|
|
58
|
+
unitless + unknown-class (B2/B3/B4) fail under strict; out-of-scope is clean; and report
|
|
59
|
+
mode exits 0 on every bad fixture.
|
|
60
|
+
- `node scripts/standards-coverage.mjs` reports the lint-enforced standard count rising
|
|
61
|
+
from 0 to 2 with Standards A and B removed from the unenforced-gaps list and zero
|
|
62
|
+
dangling references; the coverage ratchet check passes.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ELI16 — Two deterministic lint floors for Standards A and B
|
|
2
|
+
|
|
3
|
+
## The one-sentence version
|
|
4
|
+
|
|
5
|
+
Two tiny no-AI checkers now read a design spec's plain text and flag when it forgets
|
|
6
|
+
the "which machine does this live on?" tag (Standard A) or writes a broken
|
|
7
|
+
"try to fix it yourself before bugging the operator" plan (Standard B) — cheap
|
|
8
|
+
signals that back up the smart reviewer instead of replacing it.
|
|
9
|
+
|
|
10
|
+
## Why this exists
|
|
11
|
+
|
|
12
|
+
Instar's constitution has two rules that used to live only in prose and reviewer
|
|
13
|
+
habit:
|
|
14
|
+
|
|
15
|
+
- **Standard A — "always multi-machine":** every piece of state a feature adds should
|
|
16
|
+
be SHARED across all the agent's machines by default. Keeping it on one machine is
|
|
17
|
+
only allowed for a short list of concrete reasons (a login/key that physically sits
|
|
18
|
+
on one disk, a piece of hardware, or something the operator explicitly signed off on).
|
|
19
|
+
This rule was ratified *because* a spec once wrongly kept its memory on one machine
|
|
20
|
+
and it survived SEVEN review rounds — only the operator caught it.
|
|
21
|
+
- **Standard B — "self-heal before notify":** a watchdog should try a bounded, logged
|
|
22
|
+
self-repair FIRST and only ping the operator when that repair genuinely runs out of
|
|
23
|
+
road. A repair plan that does nothing, or that can retry forever, is exactly the trap
|
|
24
|
+
the rule forbids.
|
|
25
|
+
|
|
26
|
+
The smart `/spec-converge` reviewer already reads for these. But "a reviewer will
|
|
27
|
+
notice" is willpower, and Instar's root rule is **structure beats willpower**. So each
|
|
28
|
+
rule gets a cheap deterministic floor that runs first and never needs to remember.
|
|
29
|
+
|
|
30
|
+
## What actually shipped
|
|
31
|
+
|
|
32
|
+
Two small parser scripts (no AI, just text rules) plus their tests:
|
|
33
|
+
|
|
34
|
+
- **`scripts/lint-machine-local-justification.js` (Standard A).** It looks in a spec's
|
|
35
|
+
`## Multi-machine posture` section. If the spec says a surface is "machine-local" but
|
|
36
|
+
carries no `machine-local-justification: <reason>` tag, that's a finding. It also
|
|
37
|
+
checks the OTHER direction: a tag with a made-up reason (not one of the three allowed),
|
|
38
|
+
or an "operator ratified it" claim that cites no checkable proof (a commit SHA, a URL,
|
|
39
|
+
or a registry key), is also a finding.
|
|
40
|
+
- **`scripts/lint-self-heal-fields.js` (Standard B).** When a spec declares a watchdog's
|
|
41
|
+
self-heal, it must list all its brakes (max attempts, time limit, backoff, dedupe key,
|
|
42
|
+
a breaker, a notification deadline with real units like "300s", an audit location, and
|
|
43
|
+
the concrete repair actions), plus the severity class. An empty repair-actions list
|
|
44
|
+
(the fake heal that does nothing) or a deadline written as a bare "300" with no units
|
|
45
|
+
is a finding.
|
|
46
|
+
|
|
47
|
+
Both are registered in `docs/STANDARDS-REGISTRY.md` so Instar's own conformance auditor
|
|
48
|
+
now grades Standards A and B as enforced by a real deterministic `lint` on disk, not just
|
|
49
|
+
"documented" (the auditor confirmed: 0 → 2 lint-enforced standards).
|
|
50
|
+
|
|
51
|
+
## The important nuance: report-first
|
|
52
|
+
|
|
53
|
+
These lints ship in **report mode** — they PRINT their findings but exit 0 (they do not
|
|
54
|
+
block anything yet). A `--strict` flag makes them exit non-zero; that is the FAIL
|
|
55
|
+
capability the tests exercise and the hook a future graduation can flip on. This is
|
|
56
|
+
deliberate: the marker convention is brand new, existing specs predate it, and the spec's
|
|
57
|
+
own honesty clause says the deterministic floor's grade is "inert until the registry
|
|
58
|
+
ship." Shipping a brand-new blocking gate over the whole spec corpus would be a wall of
|
|
59
|
+
false failures. Report-first is the graduated-rollout path Instar always takes with a new
|
|
60
|
+
enforcement.
|
|
61
|
+
|
|
62
|
+
## Signal vs. authority
|
|
63
|
+
|
|
64
|
+
Neither lint decides the hard question. Standard A's lint cannot tell whether a
|
|
65
|
+
justification is TRUE; Standard B's lint cannot tell whether a repair is genuinely
|
|
66
|
+
SUBSTANTIVE or a severity label is HONEST. Those calls stay with the smart `/spec-converge`
|
|
67
|
+
reviewer. The lint is the cheap body; the reviewer is the mind. Together they enforce the
|
|
68
|
+
rule; neither alone does.
|
|
69
|
+
|
|
70
|
+
## What did NOT change
|
|
71
|
+
|
|
72
|
+
No constitutional text was written or re-ratified — Standards A and B were already
|
|
73
|
+
ratified by the operator on 2026-07-03. This change is purely the enforcement machinery
|
|
74
|
+
(two lints + tests + the registry rows that register them as guards). It touches no
|
|
75
|
+
runtime server code and changes nothing for a user.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Side-Effects Review — ACT-1174: two deterministic lint floors (Standards A + B)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `act1174-lint-floors`
|
|
4
|
+
**Date:** `2026-07-04`
|
|
5
|
+
**Author:** `echo (build hand)`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier 1)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships the two DEFERRED deterministic lint floors named in
|
|
11
|
+
`docs/specs/three-standards-enforcement.md` (§178-202 for A, §256-289/§343-361 for B) —
|
|
12
|
+
the "registry ship" the spec hard-sequences against each standard's registered guard.
|
|
13
|
+
Two no-LLM parser scripts + their unit tests + fixtures, plus the STANDARDS-REGISTRY
|
|
14
|
+
guard-registration rows that let the conformance auditor grade Standards A and B as
|
|
15
|
+
enforced by a deterministic `lint`.
|
|
16
|
+
|
|
17
|
+
Files touched:
|
|
18
|
+
- `scripts/lint-machine-local-justification.js` — Standard A marker floor (no-LLM).
|
|
19
|
+
- `scripts/lint-self-heal-fields.js` — Standard B self-heal field-schema floor (no-LLM).
|
|
20
|
+
- `tests/unit/lint-machine-local-justification.test.ts`, `tests/unit/lint-self-heal-fields.test.ts` — self-tests.
|
|
21
|
+
- `tests/fixtures/spec-lint/*.md` — positive / negative / bidirectional / out-of-scope fixtures.
|
|
22
|
+
- `docs/STANDARDS-REGISTRY.md` — the two `**Applied through.**` rows now cite the lints (enforcement registration, NOT new ratification prose).
|
|
23
|
+
- `upgrades/eli16/act1174-lint-floors.eli16.md`, `upgrades/side-effects/act1174-lint-floors.md`, `upgrades/next/act1174-lint-floors.md` — gate artifacts.
|
|
24
|
+
|
|
25
|
+
No constitutional text is minted or re-ratified (the standard TEXTS were ratified by the operator 2026-07-03). No runtime `src/` code changes.
|
|
26
|
+
|
|
27
|
+
## Decision-point inventory
|
|
28
|
+
|
|
29
|
+
- `Standards Enforcement-Coverage auditor classification (StandardsEnforcementAuditor)` — pass-through — the audit reads the registry and now classifies A + B as `lint` because the two rows cite `scripts/lint-*.js` (verified: baseline lint 0 → 2, A/B out of the gaps list, dangling still 0).
|
|
30
|
+
- `/spec-converge integration reviewer` — pass-through — the semantic authority is unchanged; these lints are the deterministic SIGNAL beneath it.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. Over-block
|
|
35
|
+
|
|
36
|
+
The lints DO have a block/allow surface, but ONLY under `--strict`. In report mode (the shipped default) they never block. Under `--strict` the plausible over-block is: a spec that legitimately mentions "machine-local" inside its `## Multi-machine posture` section as prose discussion (not a real surface) would trip A1. This is mitigated by scoping A1's trigger to the posture section AND skipping `<placeholder>` marker values, and by the report-first default — a false positive is a printed line, never a blocked commit. `--strict` is opt-in (tests + a future graduation), so no live over-block exists today.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Under-block
|
|
41
|
+
|
|
42
|
+
The deterministic floor is PRESENCE + well-formedness only, by design (Signal vs. Authority). It misses: a marker whose key is valid but substantively WRONG (A), and a self-heal whose fields are all present but whose remediation is a plausible-but-ineffective no-op or whose severity class is dishonestly `recoverable` (B). Those are exactly the semantic calls left to the `/spec-converge` reviewer — the lint is not meant to catch them. A spec that omits a posture section entirely is also not flagged (that is the reviewer's §168 call).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Level-of-abstraction fit
|
|
47
|
+
|
|
48
|
+
Correct layer: a cheap, brittle DETECTOR that produces a signal, explicitly paired with the existing high-context `/spec-converge` reviewer that owns the semantic authority. This is the constitutional Signal-vs-Authority / Body-and-Mind split the spec mandates (§87-95). The lint does NOT hold blocking authority over a merge in its shipped (report) mode.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 4. Signal vs authority compliance
|
|
53
|
+
|
|
54
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
55
|
+
|
|
56
|
+
- [x] No — this change produces a signal consumed by an existing smart gate (the `/spec-converge` integration reviewer), and in its shipped mode it does not block at all.
|
|
57
|
+
|
|
58
|
+
The lints are deterministic signals. Their `--strict` FAIL capability exists for tests and a future, separately-decided CI graduation; it is not wired into the blocking `npm run lint` chain. The reviewer holds semantic authority.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 5. Interactions
|
|
63
|
+
|
|
64
|
+
- **Shadowing:** none. The lints are standalone scripts, not wired into the blocking lint `&&`-chain, so they cannot shadow an existing lint or be shadowed.
|
|
65
|
+
- **Double-fire:** none. No runtime path invokes them; they are dev/CI tooling run explicitly or by their tests.
|
|
66
|
+
- **Races:** none. Pure fs-read parsers, no shared state.
|
|
67
|
+
- **Feedback loops:** none.
|
|
68
|
+
|
|
69
|
+
The one real interaction is the Standards Enforcement-Coverage auditor: the registry rows now resolve to on-disk `lint` guards, which RAISES the enforced ratio (0.4429 → 0.4714). That is the intended effect, not a regression; the coverage ratchet (`standards-coverage.mjs --check`) passes (floors: ratio ≥ floor, dangling ≤ 0).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 6. External surfaces
|
|
74
|
+
|
|
75
|
+
- Other agents on the same machine? No.
|
|
76
|
+
- Install base? These are instar-repo dev/CI tooling; they do not ship into agent homes as runtime behavior.
|
|
77
|
+
- External systems? No.
|
|
78
|
+
- Persistent state? No (the coverage script's `.instar/standards-coverage.json` output is untracked runtime state, never a committed baseline).
|
|
79
|
+
- Operator surface (Mobile-Complete): No operator-facing actions — dev/CI tooling only.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 6b. Operator-surface quality
|
|
84
|
+
|
|
85
|
+
No operator surface — not applicable.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
90
|
+
|
|
91
|
+
**machine-local BY DESIGN — pure per-machine dev/CI tooling with no durable state.** These lint scripts + tests + fixtures are repo source, replicated to every machine via git like all source (so there is no per-machine divergence to reconcile). They emit no user-facing notices (no one-voice gating needed), hold no durable state (nothing to strand on a topic transfer), and generate no URLs. The registry rows they add are likewise git-replicated source. There is no runtime state surface introduced by this change, so the multi-machine posture question is satisfied structurally: source is uniform across machines by construction.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 8. Rollback cost
|
|
96
|
+
|
|
97
|
+
Pure additive dev/CI change — revert the commit and ship as the next patch. No persistent state, no data migration, no agent-state repair, no user-visible regression. The registry rows revert cleanly (the conformance auditor simply re-grades A/B back to their prior kind). Because the lints ship report-first and are not in the blocking chain, a rollback cannot un-block anything that was blocking.
|
|
98
|
+
|
|
99
|
+
## Conclusion
|
|
100
|
+
|
|
101
|
+
The review produced no design changes. The two lints are correctly scoped as deterministic signals beneath the existing reviewer authority, ship report-first per the spec's honesty / hard-sequencing clause, and are registered so the conformance auditor grades Standards A and B as `lint`-enforced. Clear to ship as a Tier-1 change.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Second-pass review (if required)
|
|
106
|
+
|
|
107
|
+
**Reviewer:** not required (Tier 1)
|
|
108
|
+
**Independent read of the artifact: [concur]**
|
|
109
|
+
|
|
110
|
+
Tier-1 low-risk additive tooling; no independent reviewer required per the tier policy.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Evidence pointers
|
|
115
|
+
|
|
116
|
+
- `npx vitest run tests/unit/lint-machine-local-justification.test.ts tests/unit/lint-self-heal-fields.test.ts` → 13 passed (7 A + 6 B).
|
|
117
|
+
- `node scripts/standards-coverage.mjs` → `lint 2` (baseline 0), A/B absent from the gaps list, dangling 0, enforced-ratio 0.4429 → 0.4714.
|
|
118
|
+
- Manual fixture runs: A good-defended/good-ratified pass `--strict`; A-bad-undefended (A1), A-bad-spurious-key (A2), A-bad-ratified-noref (A2) fail `--strict`; B-good-complete + B-out-of-scope pass; B-bad-missing-fields (B1), B-bad-noop-and-unitless (B2/B3/B4) fail `--strict`; report mode exits 0 on all bad fixtures.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Class-Closure Declaration (display-only mirror)
|
|
123
|
+
|
|
124
|
+
No agent-authored-artifact defect and no self-triggered controller — not applicable. This change adds enforcement lints; it does not fix a defect in an LLM prompt/hook/config/skill/standards-text, and it introduces no loop/monitor/sentinel/reaper/scheduler/recovery path.
|