instar 1.3.490 → 1.3.492
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/index.html +30 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +199 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +11 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CartographerSweepEngine.d.ts +55 -11
- package/dist/core/CartographerSweepEngine.d.ts.map +1 -1
- package/dist/core/CartographerSweepEngine.js +276 -95
- package/dist/core/CartographerSweepEngine.js.map +1 -1
- package/dist/core/CartographerTree.d.ts +77 -1
- package/dist/core/CartographerTree.d.ts.map +1 -1
- package/dist/core/CartographerTree.js +257 -5
- package/dist/core/CartographerTree.js.map +1 -1
- package/dist/core/GuardPostureStore.d.ts +38 -0
- package/dist/core/GuardPostureStore.d.ts.map +1 -0
- package/dist/core/GuardPostureStore.js +87 -0
- package/dist/core/GuardPostureStore.js.map +1 -0
- package/dist/core/MachinePoolRegistry.d.ts +16 -0
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.js +24 -1
- package/dist/core/MachinePoolRegistry.js.map +1 -1
- package/dist/core/PeerPresencePuller.d.ts +9 -0
- package/dist/core/PeerPresencePuller.d.ts.map +1 -1
- package/dist/core/PeerPresencePuller.js +1 -1
- package/dist/core/PeerPresencePuller.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +43 -2
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/cartographerDetect.d.ts +133 -0
- package/dist/core/cartographerDetect.d.ts.map +1 -0
- package/dist/core/cartographerDetect.js +431 -0
- package/dist/core/cartographerDetect.js.map +1 -0
- package/dist/core/cartographerDetect.worker.d.ts +2 -0
- package/dist/core/cartographerDetect.worker.d.ts.map +1 -0
- package/dist/core/cartographerDetect.worker.js +37 -0
- package/dist/core/cartographerDetect.worker.js.map +1 -0
- package/dist/core/types.d.ts +29 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +8 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
- package/dist/monitoring/ActiveWorkSilenceSentinel.js +8 -0
- package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
- package/dist/monitoring/ContextWedgeSentinel.d.ts +8 -0
- package/dist/monitoring/ContextWedgeSentinel.d.ts.map +1 -1
- package/dist/monitoring/ContextWedgeSentinel.js +8 -0
- package/dist/monitoring/ContextWedgeSentinel.js.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.d.ts +3 -27
- package/dist/monitoring/GuardPostureTripwire.d.ts.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.js +8 -76
- package/dist/monitoring/GuardPostureTripwire.js.map +1 -1
- package/dist/monitoring/GuardRegistry.d.ts +48 -0
- package/dist/monitoring/GuardRegistry.d.ts.map +1 -0
- package/dist/monitoring/GuardRegistry.js +49 -0
- package/dist/monitoring/GuardRegistry.js.map +1 -0
- package/dist/monitoring/SessionReaper.d.ts +7 -0
- package/dist/monitoring/SessionReaper.d.ts.map +1 -1
- package/dist/monitoring/SessionReaper.js +9 -0
- package/dist/monitoring/SessionReaper.js.map +1 -1
- package/dist/monitoring/SessionWatchdog.d.ts +8 -0
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
- package/dist/monitoring/SessionWatchdog.js +8 -0
- package/dist/monitoring/SessionWatchdog.js.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.d.ts +8 -0
- package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -1
- package/dist/monitoring/SocketDisconnectSentinel.js +8 -0
- package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts +67 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -0
- package/dist/monitoring/guardManifest.js +536 -0
- package/dist/monitoring/guardManifest.js.map +1 -0
- package/dist/monitoring/guardPosture.d.ts +77 -0
- package/dist/monitoring/guardPosture.d.ts.map +1 -0
- package/dist/monitoring/guardPosture.js +198 -0
- package/dist/monitoring/guardPosture.js.map +1 -0
- package/dist/monitoring/guardPostureView.d.ts +100 -0
- package/dist/monitoring/guardPostureView.d.ts.map +1 -0
- package/dist/monitoring/guardPostureView.js +294 -0
- package/dist/monitoring/guardPostureView.js.map +1 -0
- package/dist/monitoring/probes/GuardPostureProbe.d.ts +82 -0
- package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/GuardPostureProbe.js +384 -0
- package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +9 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts +10 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +12 -0
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/AgentServer.d.ts +8 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +11 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/peerUrlGuard.d.ts +35 -0
- package/dist/server/peerUrlGuard.d.ts.map +1 -0
- package/dist/server/peerUrlGuard.js +93 -0
- package/dist/server/peerUrlGuard.js.map +1 -0
- package/dist/server/routes.d.ts +14 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +239 -25
- package/dist/server/routes.js.map +1 -1
- package/package.json +3 -2
- package/scripts/lint-guard-manifest.js +264 -0
- package/scripts/lint-no-direct-destructive.js +4 -0
- package/scripts/lint-no-mainthread-cartographer-walk.js +101 -0
- package/src/data/builtin-manifest.json +65 -65
- package/src/data/state-coherence-registry.json +54 -18
- package/src/scaffold/templates.ts +9 -0
- package/upgrades/1.3.491.md +37 -0
- package/upgrades/1.3.492.md +31 -0
- package/upgrades/side-effects/cartographer-sweep-eventloop-safety.md +110 -0
- package/upgrades/side-effects/guard-posture-endpoint.md +101 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instar",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.492",
|
|
4
4
|
"description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
"test:contract": "node scripts/run-contract-tests.js",
|
|
29
29
|
"test:contract:raw": "vitest run --config vitest.contract.config.ts",
|
|
30
30
|
"test:all": "vitest run && vitest run --config vitest.integration.config.ts && vitest run --config vitest.e2e.config.ts",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-state-registry.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-llm-attribution.js && node scripts/check-codex-rule1-drift.js",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-state-registry.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/check-codex-rule1-drift.js",
|
|
32
32
|
"lint:destructive": "node scripts/lint-no-direct-destructive.js",
|
|
33
33
|
"lint:dev-agent-dark-gate": "node scripts/lint-dev-agent-dark-gate.js",
|
|
34
|
+
"lint:guard-manifest": "node scripts/lint-guard-manifest.js",
|
|
34
35
|
"lint:dev-agent-dark-gate:staged": "node scripts/lint-dev-agent-dark-gate.js --staged",
|
|
35
36
|
"lint:destructive:staged": "node scripts/lint-no-direct-destructive.js --staged",
|
|
36
37
|
"lint:llm-http": "node scripts/lint-no-direct-llm-http.js",
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-guard-manifest.js — the guard-inventory CI ratchet
|
|
4
|
+
* (GUARD-POSTURE-ENDPOINT-SPEC §2.1, "CI ratchet (manifest-driven, repo-wide)").
|
|
5
|
+
*
|
|
6
|
+
* THE RULE: every guard-shaped component in src/monitoring, src/messaging,
|
|
7
|
+
* src/lifeline, src/core must be CLASSIFIED — it appears either as a
|
|
8
|
+
* `component:` in GUARD_MANIFEST (it IS an inventory guard) or in NOT_A_GUARD
|
|
9
|
+
* (it deliberately is not, with a real reason). A future guard cannot be
|
|
10
|
+
* forgotten from the /guards inventory: the moment its file lands, this lint
|
|
11
|
+
* fails until someone makes the classification decision explicitly
|
|
12
|
+
* (Structure > Willpower — same contract as lint-dev-agent-dark-gate.js +
|
|
13
|
+
* DARK_GATE_EXCLUSIONS).
|
|
14
|
+
*
|
|
15
|
+
* Three assertions:
|
|
16
|
+
*
|
|
17
|
+
* A. EVERY CANDIDATE CLASSIFIED — a candidate component is (1) a .ts file in
|
|
18
|
+
* one of the four dirs whose basename matches the guard-shape suffix
|
|
19
|
+
* pattern (Sentinel|Reaper|Watchdog|Guard|Tripwire|Monitor|Beacon|
|
|
20
|
+
* Detector|Promoter|Pauser|Backstop), component name = basename, OR
|
|
21
|
+
* (2) a name in ADDITIONAL_CANDIDATES (enabled-gated boot constructs
|
|
22
|
+
* whose filenames don't match the pattern, e.g. QuotaTracker, PromptGate).
|
|
23
|
+
* Each candidate must appear in GUARD_MANIFEST's `component:` fields or
|
|
24
|
+
* in NOT_A_GUARD.
|
|
25
|
+
*
|
|
26
|
+
* B. REAL REASONS — every NOT_A_GUARD entry's reason must be ≥12
|
|
27
|
+
* non-whitespace chars (defeats placeholder reasons; same bar as
|
|
28
|
+
* DARK_GATE_EXCLUSIONS).
|
|
29
|
+
*
|
|
30
|
+
* C. EXACTLY ONE LIST — a component must not appear in BOTH GUARD_MANIFEST
|
|
31
|
+
* and NOT_A_GUARD (a dual classification is a contradiction the
|
|
32
|
+
* inventory cannot resolve).
|
|
33
|
+
*
|
|
34
|
+
* LIMITATIONS (P2 Signal-vs-Authority — do NOT claim full closure):
|
|
35
|
+
* - Detection is the file-basename suffix pattern + the explicit
|
|
36
|
+
* ADDITIONAL_CANDIDATES list. A guard-shaped component in a file whose
|
|
37
|
+
* name matches neither (e.g. a guard class defined inside server.ts)
|
|
38
|
+
* evades detection until someone names it in ADDITIONAL_CANDIDATES.
|
|
39
|
+
* Deliberate: cheap, deterministic, explainable — the precedent's
|
|
40
|
+
* pragmatism over an AST-grade analyzer.
|
|
41
|
+
* - The manifest is parsed STATICALLY (regex over the source — this lint
|
|
42
|
+
* must not import TS). `component:` and `reason:` must be single-line
|
|
43
|
+
* 'single-' or "double-quoted" string literals, which is the file's
|
|
44
|
+
* enforced house style; a template-literal or multi-line reason would be
|
|
45
|
+
* invisible to the parse (and the entry would then fail assertion A,
|
|
46
|
+
* loudly, not silently pass).
|
|
47
|
+
*
|
|
48
|
+
* Exit codes:
|
|
49
|
+
* 0 — no violations.
|
|
50
|
+
* 1 — at least one violation.
|
|
51
|
+
*
|
|
52
|
+
* Usage:
|
|
53
|
+
* node scripts/lint-guard-manifest.js # full scan of the four dirs
|
|
54
|
+
* node scripts/lint-guard-manifest.js path1 path2 # specific candidate files
|
|
55
|
+
*
|
|
56
|
+
* Test override:
|
|
57
|
+
* INSTAR_GUARDLINT_MANIFEST=<path> # point the parse at a fixture manifest
|
|
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 __dirname = path.dirname(__filename);
|
|
66
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
67
|
+
|
|
68
|
+
const MANIFEST_REL = 'src/monitoring/guardManifest.ts';
|
|
69
|
+
const MANIFEST_ABS = process.env.INSTAR_GUARDLINT_MANIFEST
|
|
70
|
+
? path.resolve(process.env.INSTAR_GUARDLINT_MANIFEST)
|
|
71
|
+
: path.join(ROOT, MANIFEST_REL);
|
|
72
|
+
|
|
73
|
+
// The repo-wide scan boundary (spec §2.1: "in ANY of src/monitoring,
|
|
74
|
+
// src/messaging, src/lifeline, src/core" — path-pattern linting scoped to one
|
|
75
|
+
// directory would miss the spec's own canonical examples).
|
|
76
|
+
const GUARD_DIRS = ['src/monitoring', 'src/messaging', 'src/lifeline', 'src/core'];
|
|
77
|
+
|
|
78
|
+
// Guard-shape detection 1: the basename suffix pattern.
|
|
79
|
+
const GUARD_SHAPE_BASENAME =
|
|
80
|
+
/(Sentinel|Reaper|Watchdog|Guard|Tripwire|Monitor|Beacon|Detector|Promoter|Pauser|Backstop)\.ts$/;
|
|
81
|
+
|
|
82
|
+
// Guard-shape detection 2: explicit additional candidates — enabled-gated
|
|
83
|
+
// boot-constructed components whose FILENAMES do not match the suffix pattern.
|
|
84
|
+
// Append here when a new guard-shaped component ships under a non-matching
|
|
85
|
+
// name (the lint then forces its classification like any other candidate).
|
|
86
|
+
const ADDITIONAL_CANDIDATES = [
|
|
87
|
+
'QuotaTracker', // monitoring.quotaTracking
|
|
88
|
+
'ResourceLedger', // monitoring.resourceLedger.enabled
|
|
89
|
+
'TelemetryCollector', // monitoring.telemetry.enabled
|
|
90
|
+
'StallTriageNurse', // monitoring.triage.enabled
|
|
91
|
+
'TriageOrchestrator', // monitoring.triageOrchestrator.enabled
|
|
92
|
+
'GrowthMilestoneAnalyst',// monitoring.growthAnalyst.enabled (dev-gated)
|
|
93
|
+
'BlockerLedger', // monitoring.blockerLedger.enabled (dev-gated)
|
|
94
|
+
'SecretSync', // multiMachine.secretSync.enabled
|
|
95
|
+
'CoherenceJournal', // multiMachine.coherenceJournal.enabled
|
|
96
|
+
'PromptGate', // monitoring.promptGate.enabled
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const MIN_REASON_NON_WS = 12;
|
|
100
|
+
|
|
101
|
+
/** Strip line + block comments so commented-out entries can't satisfy the rule. */
|
|
102
|
+
function stripComments(src) {
|
|
103
|
+
return src
|
|
104
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
105
|
+
.replace(/^\s*\/\/.*$/gm, '');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Extract the source slice of `export const NAME ... = [ ... ] as const;`. */
|
|
109
|
+
function extractArrayBlock(src, name) {
|
|
110
|
+
const declRe = new RegExp(`export\\s+const\\s+${name}\\b[\\s\\S]*?=\\s*\\[`);
|
|
111
|
+
const m = declRe.exec(src);
|
|
112
|
+
if (!m) return null;
|
|
113
|
+
const start = m.index + m[0].length;
|
|
114
|
+
const end = src.indexOf('] as const', start);
|
|
115
|
+
if (end < 0) return null;
|
|
116
|
+
return src.slice(start, end);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Statically parse the manifest file (regex over source — never imports TS).
|
|
121
|
+
* Returns { manifestComponents: Set, notAGuard: [{component, reason, line}] }.
|
|
122
|
+
*/
|
|
123
|
+
function parseManifest(manifestAbs) {
|
|
124
|
+
const raw = fs.readFileSync(manifestAbs, 'utf-8');
|
|
125
|
+
const src = stripComments(raw);
|
|
126
|
+
|
|
127
|
+
const manifestBlock = extractArrayBlock(src, 'GUARD_MANIFEST');
|
|
128
|
+
const notAGuardBlock = extractArrayBlock(src, 'NOT_A_GUARD');
|
|
129
|
+
if (manifestBlock === null || notAGuardBlock === null) {
|
|
130
|
+
return {
|
|
131
|
+
error: `could not locate ${manifestBlock === null ? 'GUARD_MANIFEST' : 'NOT_A_GUARD'} array in ${manifestAbs}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const manifestComponents = new Set();
|
|
136
|
+
const componentRe = /component:\s*(['"])([^'"]+)\1/g;
|
|
137
|
+
for (const m of manifestBlock.matchAll(componentRe)) manifestComponents.add(m[2]);
|
|
138
|
+
|
|
139
|
+
// NOT_A_GUARD entries: `{ component: '...', reason: '...' }` (single-line
|
|
140
|
+
// string literals — the file's house style; see header LIMITATIONS).
|
|
141
|
+
const notAGuard = [];
|
|
142
|
+
const entryRe = /component:\s*(['"])([^'"]+)\1\s*,\s*reason:\s*(['"])((?:[^\\]|\\.)*?)\3/g;
|
|
143
|
+
for (const m of notAGuardBlock.matchAll(entryRe)) {
|
|
144
|
+
notAGuard.push({ component: m[2], reason: m[4] });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { manifestComponents, notAGuard };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Walk the four guard dirs for candidate .ts files (skip tests/declarations). */
|
|
151
|
+
function listCandidateFiles() {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const rel of GUARD_DIRS) {
|
|
154
|
+
const dir = path.join(ROOT, rel);
|
|
155
|
+
if (!fs.existsSync(dir)) continue;
|
|
156
|
+
const walk = (d) => {
|
|
157
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
158
|
+
const full = path.join(d, e.name);
|
|
159
|
+
if (e.isDirectory()) {
|
|
160
|
+
if (e.name === 'node_modules') continue;
|
|
161
|
+
walk(full);
|
|
162
|
+
} else if (
|
|
163
|
+
e.name.endsWith('.ts') &&
|
|
164
|
+
!e.name.endsWith('.test.ts') &&
|
|
165
|
+
!e.name.endsWith('.d.ts')
|
|
166
|
+
) {
|
|
167
|
+
out.push(full);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
walk(dir);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveCandidateFiles() {
|
|
177
|
+
const explicit = process.argv.slice(2).filter((a) => !a.startsWith('--'));
|
|
178
|
+
if (explicit.length) return explicit.map((f) => path.resolve(ROOT, f));
|
|
179
|
+
return listCandidateFiles();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Run ──────────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
const violations = [];
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(MANIFEST_ABS)) {
|
|
187
|
+
console.error(`lint-guard-manifest: manifest not found at ${MANIFEST_ABS}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const parsed = parseManifest(MANIFEST_ABS);
|
|
192
|
+
if (parsed.error) {
|
|
193
|
+
console.error(`lint-guard-manifest: ${parsed.error}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const { manifestComponents, notAGuard } = parsed;
|
|
197
|
+
const notAGuardComponents = new Set(notAGuard.map((e) => e.component));
|
|
198
|
+
|
|
199
|
+
// ── Assertion B: every NOT_A_GUARD reason is real (≥12 non-ws chars) ──
|
|
200
|
+
for (const e of notAGuard) {
|
|
201
|
+
const reasonLen = (e.reason || '').replace(/\s/g, '').length;
|
|
202
|
+
if (reasonLen < MIN_REASON_NON_WS) {
|
|
203
|
+
violations.push({
|
|
204
|
+
file: MANIFEST_REL, kind: 'B: NOT_A_GUARD reason too short',
|
|
205
|
+
text: `${e.component} → reason '${e.reason}' (${reasonLen} non-ws chars)`,
|
|
206
|
+
fix: `a NOT_A_GUARD reason must be ≥${MIN_REASON_NON_WS} non-whitespace chars (defeats placeholder reasons)`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Assertion C: exactly one list ──
|
|
212
|
+
for (const c of notAGuardComponents) {
|
|
213
|
+
if (manifestComponents.has(c)) {
|
|
214
|
+
violations.push({
|
|
215
|
+
file: MANIFEST_REL, kind: 'C: dual classification',
|
|
216
|
+
text: `${c} appears in BOTH GUARD_MANIFEST and NOT_A_GUARD`,
|
|
217
|
+
fix: 'a component is a guard or it is not — remove it from one list',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Assertion A: every candidate component is classified ──
|
|
223
|
+
const explicitArgs = process.argv.slice(2).filter((a) => !a.startsWith('--'));
|
|
224
|
+
const candidates = new Map(); // component name → where it was detected
|
|
225
|
+
for (const file of resolveCandidateFiles()) {
|
|
226
|
+
if (!fs.existsSync(file)) continue;
|
|
227
|
+
const base = path.basename(file);
|
|
228
|
+
if (!GUARD_SHAPE_BASENAME.test(base)) continue;
|
|
229
|
+
candidates.set(base.replace(/\.ts$/, ''), path.relative(ROOT, file));
|
|
230
|
+
}
|
|
231
|
+
// ADDITIONAL_CANDIDATES are repo-global names — only meaningful on a full
|
|
232
|
+
// scan (an explicit-args run, e.g. a test fixture, scopes to those files).
|
|
233
|
+
if (explicitArgs.length === 0) {
|
|
234
|
+
for (const name of ADDITIONAL_CANDIDATES) {
|
|
235
|
+
if (!candidates.has(name)) candidates.set(name, '(ADDITIONAL_CANDIDATES)');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const [component, where] of [...candidates.entries()].sort()) {
|
|
240
|
+
if (manifestComponents.has(component)) continue;
|
|
241
|
+
if (notAGuardComponents.has(component)) continue;
|
|
242
|
+
violations.push({
|
|
243
|
+
file: where, kind: 'A: unclassified guard-shaped component',
|
|
244
|
+
text: `${component} is in NEITHER GUARD_MANIFEST (component:) NOR NOT_A_GUARD`,
|
|
245
|
+
fix: `declare it in GUARD_MANIFEST (it joins the /guards inventory) OR classify it in NOT_A_GUARD with a ≥${MIN_REASON_NON_WS}-char reason (${MANIFEST_REL})`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (violations.length === 0) {
|
|
250
|
+
console.log('lint-guard-manifest: clean');
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.error('\n❌ lint-guard-manifest found violations of the guard-inventory classification standard:\n');
|
|
255
|
+
console.error('NOTE: detection is the file-basename suffix pattern + ADDITIONAL_CANDIDATES only —');
|
|
256
|
+
console.error('a guard-shaped component under a non-matching filename evades it until named in');
|
|
257
|
+
console.error('ADDITIONAL_CANDIDATES. This closes the forgotten-file hole, not every hole.\n');
|
|
258
|
+
for (const v of violations) {
|
|
259
|
+
console.error(` ${v.file} [${v.kind}]`);
|
|
260
|
+
console.error(` ${v.text}`);
|
|
261
|
+
console.error(` fix: ${v.fix}\n`);
|
|
262
|
+
}
|
|
263
|
+
console.error('Standard: every guard-shaped component is CLASSIFIED — declared in GUARD_MANIFEST (it appears in the /guards inventory) or in NOT_A_GUARD with a real reason. A future guard cannot be silently forgotten. Spec: docs/specs/GUARD-POSTURE-ENDPOINT-SPEC.md §2.1\n');
|
|
264
|
+
process.exit(1);
|
|
@@ -88,6 +88,10 @@ const ALLOWLIST = new Set([
|
|
|
88
88
|
// completeness) — read-only `git diff --cached --name-only` for --staged.
|
|
89
89
|
// Cannot depend on the TS funnel because TS is not compiled at pre-push.
|
|
90
90
|
'scripts/lint-llm-attribution.js',
|
|
91
|
+
// Same bootstrap-escape pattern (cartographer main-thread-walk lint, fix
|
|
92
|
+
// instar#1069) — read-only `git diff --cached --name-only` for --staged.
|
|
93
|
+
// Cannot depend on the TS funnel because TS is not compiled at pre-push.
|
|
94
|
+
'scripts/lint-no-mainthread-cartographer-walk.js',
|
|
91
95
|
// Postinstall bootstrap script — runs before TypeScript is compiled and
|
|
92
96
|
// before SafeFsExecutor is available. CommonJS, can't use ESM imports.
|
|
93
97
|
'scripts/fix-better-sqlite3.cjs',
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-no-mainthread-cartographer-walk.js — refuses the O(nodeCount)/67MB
|
|
4
|
+
* synchronous cartographer operations on the server's main-thread surfaces.
|
|
5
|
+
*
|
|
6
|
+
* Earned by instar#1069: enabling the doc-freshness sweep on a real tree
|
|
7
|
+
* (366,757 nodes / 67MB index) put the AgentServer into a supervisor kill-loop.
|
|
8
|
+
* The "what's stale?" detect read + walked the whole index ON THE MAIN EVENT
|
|
9
|
+
* LOOP, starving /health for ~35s at a stretch, so the supervisor declared the
|
|
10
|
+
* (alive) server dead and force-restarted it — every ~10-15 min.
|
|
11
|
+
*
|
|
12
|
+
* The fix moved the detect + index writes off the event loop (a worker thread)
|
|
13
|
+
* and made every /cartographer/* route serve a cheap snapshot. This lint stops
|
|
14
|
+
* the regression from returning: "Structure > Willpower", not "enforced by review".
|
|
15
|
+
*
|
|
16
|
+
* Rule (PATH-ALLOWLIST — a callsite lint matches FILES, not runtime threads):
|
|
17
|
+
* FORBIDDEN in the main-thread surfaces:
|
|
18
|
+
* - src/server/routes.ts (the request thread)
|
|
19
|
+
* - src/core/CartographerSweepEngine.ts (the sweep poller thread = main loop)
|
|
20
|
+
* the heavy calls: .staleNodes( .loadIndex( .freshnessHealth( .scaffold(
|
|
21
|
+
* PERMITTED everywhere else, specifically:
|
|
22
|
+
* - src/core/cartographerDetect.ts (the pure module — legitimately parses, OFF-thread)
|
|
23
|
+
* - src/core/CartographerTree.ts (the definitions of these methods)
|
|
24
|
+
* - tests/ (tests scaffold/load directly)
|
|
25
|
+
* The bounded request loader (`.loadIndexBounded(`) is NOT matched — it stats
|
|
26
|
+
* the file and refuses before parsing, so it is safe on the request thread.
|
|
27
|
+
*
|
|
28
|
+
* Escape hatch (closed, reviewed): a genuinely off-thread / boot-only call may
|
|
29
|
+
* carry an inline justification on the same line or directly above:
|
|
30
|
+
* // lint-allow-carto-heavy: <why this can't starve the event loop>
|
|
31
|
+
*
|
|
32
|
+
* Exit codes: 0 — clean; 1 — at least one violation.
|
|
33
|
+
* Usage:
|
|
34
|
+
* node scripts/lint-no-mainthread-cartographer-walk.js # full repo
|
|
35
|
+
* node scripts/lint-no-mainthread-cartographer-walk.js --staged # staged files
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import { execSync } from 'node:child_process';
|
|
41
|
+
import { fileURLToPath } from 'node:url';
|
|
42
|
+
|
|
43
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
44
|
+
const ROOT = path.resolve(path.dirname(__filename), '..');
|
|
45
|
+
|
|
46
|
+
// The main-thread surfaces where these heavy calls re-introduce #1069.
|
|
47
|
+
const FORBIDDEN_FILES = new Set([
|
|
48
|
+
'src/server/routes.ts',
|
|
49
|
+
'src/core/CartographerSweepEngine.ts',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// .staleNodes( / .loadIndex( / .freshnessHealth( / .scaffold( — note the trailing
|
|
53
|
+
// `\(` means `.loadIndexBounded(` (the safe byte-bounded loader) is NOT matched.
|
|
54
|
+
const VIOLATION = /\.(staleNodes|loadIndex|freshnessHealth|scaffold)\(/;
|
|
55
|
+
const ALLOW = /lint-allow-carto-heavy:/;
|
|
56
|
+
|
|
57
|
+
function listFiles() {
|
|
58
|
+
if (process.argv.includes('--staged')) {
|
|
59
|
+
const out = execSync('git diff --cached --name-only', { cwd: ROOT, encoding: 'utf-8' });
|
|
60
|
+
return out.split('\n').filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
const explicit = process.argv.slice(2).filter((a) => !a.startsWith('--'));
|
|
63
|
+
if (explicit.length) return explicit;
|
|
64
|
+
return [...FORBIDDEN_FILES];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let violations = 0;
|
|
68
|
+
for (const rel of listFiles()) {
|
|
69
|
+
const normalized = rel.split(path.sep).join('/');
|
|
70
|
+
if (!FORBIDDEN_FILES.has(normalized)) continue; // only the main-thread surfaces are enforced
|
|
71
|
+
const full = path.isAbsolute(normalized) ? normalized : path.join(ROOT, normalized);
|
|
72
|
+
let content;
|
|
73
|
+
try { content = fs.readFileSync(full, 'utf-8'); } catch { continue; }
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const trimmed = lines[i].trimStart();
|
|
77
|
+
if (/^(\/\/|\*|\/\*)/.test(trimmed)) continue; // comment-only mention
|
|
78
|
+
if (!VIOLATION.test(lines[i])) continue;
|
|
79
|
+
let allowed = false;
|
|
80
|
+
for (let j = i; j >= Math.max(0, i - 6); j--) {
|
|
81
|
+
if (ALLOW.test(lines[j])) { allowed = true; break; }
|
|
82
|
+
}
|
|
83
|
+
if (allowed) continue;
|
|
84
|
+
const m = lines[i].match(VIOLATION);
|
|
85
|
+
console.error(
|
|
86
|
+
`${normalized}:${i + 1} — main-thread cartographer ${m ? m[1] + '()' : 'walk'} ` +
|
|
87
|
+
`(O(nodeCount)/67MB synchronous) on a server hot path. This is the instar#1069 ` +
|
|
88
|
+
`kill-loop regression. Use the off-event-loop detect (cartographerDetect.runDetect) ` +
|
|
89
|
+
`or serve the snapshot / use loadIndexBounded(). If genuinely off-thread/boot-only, ` +
|
|
90
|
+
`add "// lint-allow-carto-heavy: <reason>".`,
|
|
91
|
+
);
|
|
92
|
+
violations++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (violations > 0) {
|
|
97
|
+
console.error(`\nlint-no-mainthread-cartographer-walk: ${violations} violation(s). ` +
|
|
98
|
+
`See docs/specs/CARTOGRAPHER-SWEEP-EVENTLOOP-SAFETY.md (fix instar#1069).`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log('lint-no-mainthread-cartographer-walk: clean');
|