instar 1.3.1150 → 1.3.1152
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/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/package.json +1 -1
- package/scripts/lint-no-unfunneled-tmux-literal-send.js +159 -12
- package/scripts/lint-no-unfunneled-topic-creation.js +123 -25
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1151.md +62 -0
- package/upgrades/1.3.1152.md +54 -0
- package/upgrades/side-effects/tmux-send-lint-array-scope.md +122 -0
- package/upgrades/side-effects/topic-creation-lint-resolution.md +135 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1152",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "cb5b39009ce485f47b3010b819bd2a9c0dc4e2294204deaef41bf69db200fd41",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1152"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -22,9 +22,33 @@
|
|
|
22
22
|
* Structure > Willpower — a comment asking authors to remember is a wish; this
|
|
23
23
|
* is the guarantee.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* SCOPE CORRECTED 2026-08-15. The previous header said only that "a wrapper that
|
|
26
|
+
* builds the argv array dynamically could still evade it". That understated the
|
|
27
|
+
* gap: the check was LINE-oriented and required `send-keys` and `'-l'` on the
|
|
28
|
+
* SAME line, so four PLAIN literal forms — none of them dynamic, none of them a
|
|
29
|
+
* wrapper — walked straight past it. Measured against the shipped check, with the
|
|
30
|
+
* one-line form as a positive control firing in the same run:
|
|
31
|
+
*
|
|
32
|
+
* ["send-keys", "-l", p] CONTROL exit 1 (caught)
|
|
33
|
+
* [\n "send-keys",\n "-l",\n p,\n] exit 0 — EVADES
|
|
34
|
+
* const F = "-l"; ["send-keys", F, p] exit 0 — EVADES
|
|
35
|
+
* const C = "send-keys"; [C, "-l", p] exit 0 — EVADES
|
|
36
|
+
* ["send-keys","-l",p] // buildLiteralSendArgs exit 0 — EVADES
|
|
37
|
+
*
|
|
38
|
+
* The first is the one that matters: a multi-line argv array is simply how any
|
|
39
|
+
* formatter writes an array over the print width. The guard could be defeated by
|
|
40
|
+
* running prettier. The last is worse in kind — merely NAMING the funnel in a
|
|
41
|
+
* COMMENT on that line suppressed the check, so `// TODO: use buildLiteralSendArgs`
|
|
42
|
+
* beside a raw send silenced the guard that the TODO was admitting was needed.
|
|
43
|
+
*
|
|
44
|
+
* Now: comments are stripped quote-aware first, string constants are resolved
|
|
45
|
+
* per file, and the unit of matching is the ARRAY LITERAL (bracket-matched,
|
|
46
|
+
* bounded) rather than the line — which is what the original rule always meant.
|
|
47
|
+
*
|
|
48
|
+
* STILL not proof, and this is the honest remainder: an argv array assembled at
|
|
49
|
+
* RUNTIME (push(), concat(), spread of a computed list, a helper that returns the
|
|
50
|
+
* array) is invisible here. That is the gap the original header named, and it is
|
|
51
|
+
* the only one left. Closing it needs dataflow, not more patterns.
|
|
28
52
|
*/
|
|
29
53
|
import fs from 'node:fs';
|
|
30
54
|
import path from 'node:path';
|
|
@@ -46,20 +70,142 @@ function walk(dir, out = []) {
|
|
|
46
70
|
return out;
|
|
47
71
|
}
|
|
48
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Strip // and block comments WITHOUT touching string contents, replacing each
|
|
75
|
+
* removed character with a space so every byte offset — and therefore every
|
|
76
|
+
* reported line number — is preserved exactly.
|
|
77
|
+
*/
|
|
78
|
+
export function stripComments(src) {
|
|
79
|
+
let out = '';
|
|
80
|
+
let i = 0;
|
|
81
|
+
let quote = null;
|
|
82
|
+
while (i < src.length) {
|
|
83
|
+
const c = src[i];
|
|
84
|
+
const n = src[i + 1];
|
|
85
|
+
if (quote) {
|
|
86
|
+
out += c;
|
|
87
|
+
if (c === '\\') { out += n ?? ''; i += 2; continue; }
|
|
88
|
+
if (c === quote) quote = null;
|
|
89
|
+
i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (c === '"' || c === "'" || c === '`') { quote = c; out += c; i += 1; continue; }
|
|
93
|
+
if (c === '/' && n === '/') {
|
|
94
|
+
while (i < src.length && src[i] !== '\n') { out += ' '; i += 1; }
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (c === '/' && n === '*') {
|
|
98
|
+
out += ' '; i += 2;
|
|
99
|
+
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
100
|
+
out += src[i] === '\n' ? '\n' : ' ';
|
|
101
|
+
i += 1;
|
|
102
|
+
}
|
|
103
|
+
out += ' '; i += 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
out += c;
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Identifiers bound to a plain string literal in THIS file. An identifier bound
|
|
114
|
+
* more than once to DIFFERENT values is UNRESOLVABLE and dropped, so an ambiguous
|
|
115
|
+
* name can never be substituted into a match — ambiguity fails toward NOT flagging,
|
|
116
|
+
* because a guess that fails someone's build is the expensive direction.
|
|
117
|
+
*/
|
|
118
|
+
export function collectStringConsts(code) {
|
|
119
|
+
const seen = new Map();
|
|
120
|
+
const conflicting = new Set();
|
|
121
|
+
const DECL = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(['"`])([^'"`\n]*)\2/g;
|
|
122
|
+
let m;
|
|
123
|
+
while ((m = DECL.exec(code)) !== null) {
|
|
124
|
+
const [, name, , value] = m;
|
|
125
|
+
if (seen.has(name) && seen.get(name) !== value) conflicting.add(name);
|
|
126
|
+
else seen.set(name, value);
|
|
127
|
+
}
|
|
128
|
+
for (const name of conflicting) seen.delete(name);
|
|
129
|
+
return seen;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Every bracket-matched array literal in the source, as {text, line}. Bounded:
|
|
134
|
+
* an array longer than MAX_ARRAY_CHARS is truncated rather than scanned whole, so
|
|
135
|
+
* a pathological file cannot make this quadratic. Unbalanced brackets simply
|
|
136
|
+
* yield no region — the check fails toward NOT flagging.
|
|
137
|
+
*/
|
|
138
|
+
const MAX_ARRAY_CHARS = 4000;
|
|
139
|
+
export function arrayRegions(code) {
|
|
140
|
+
const regions = [];
|
|
141
|
+
for (let i = 0; i < code.length; i++) {
|
|
142
|
+
if (code[i] !== '[') continue;
|
|
143
|
+
let depth = 0;
|
|
144
|
+
let quote = null;
|
|
145
|
+
let j = i;
|
|
146
|
+
for (; j < code.length && j - i < MAX_ARRAY_CHARS; j++) {
|
|
147
|
+
const c = code[j];
|
|
148
|
+
if (quote) {
|
|
149
|
+
if (c === '\\') { j += 1; continue; }
|
|
150
|
+
if (c === quote) quote = null;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (c === '"' || c === "'" || c === '`') { quote = c; continue; }
|
|
154
|
+
if (c === '[') depth += 1;
|
|
155
|
+
else if (c === ']') { depth -= 1; if (depth === 0) break; }
|
|
156
|
+
}
|
|
157
|
+
if (depth !== 0) continue;
|
|
158
|
+
const text = code.slice(i, j + 1);
|
|
159
|
+
const line = code.slice(0, i).split('\n').length;
|
|
160
|
+
regions.push({ text, line });
|
|
161
|
+
}
|
|
162
|
+
return regions;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** True when the resolved region is a raw literal `send-keys -l` argv. */
|
|
166
|
+
export function regionViolates(regionText, consts) {
|
|
167
|
+
let t = regionText;
|
|
168
|
+
for (const [name, value] of consts) {
|
|
169
|
+
t = t.replace(new RegExp(`\\b${name}\\b`, 'g'), `'${value}'`);
|
|
170
|
+
}
|
|
171
|
+
if (!/['"`]send-keys['"`]/.test(t)) return false;
|
|
172
|
+
if (!/['"`]-l['"`]/.test(t)) return false;
|
|
173
|
+
// Already funnelled — checked on COMMENT-STRIPPED code, so merely naming the
|
|
174
|
+
// funnel in a comment can no longer silence the guard.
|
|
175
|
+
if (t.includes('buildLiteralSendArgs')) return false;
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function scanSource(code) {
|
|
180
|
+
const stripped = stripComments(code);
|
|
181
|
+
const consts = collectStringConsts(stripped);
|
|
182
|
+
const hits = [];
|
|
183
|
+
for (const region of arrayRegions(stripped)) {
|
|
184
|
+
if (regionViolates(region.text, consts)) {
|
|
185
|
+
hits.push({ line: region.line, text: region.text.replace(/\s+/g, ' ').trim() });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return hits;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// DIRECT-INVOCATION GUARD. Without it, importing this module to unit-test the
|
|
192
|
+
// helpers above runs the whole src/ scan and calls process.exit(1) the moment the
|
|
193
|
+
// repo has a real violation — killing the importing process. Four other lints hit
|
|
194
|
+
// exactly that this week; the guard is the same fix.
|
|
195
|
+
const invokedDirectly =
|
|
196
|
+
process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
197
|
+
|
|
198
|
+
if (invokedDirectly) runLint();
|
|
199
|
+
|
|
200
|
+
function runLint() {
|
|
49
201
|
const violations = [];
|
|
50
202
|
|
|
51
203
|
for (const file of walk(SRC)) {
|
|
52
204
|
const rel = path.relative(REPO, file);
|
|
53
205
|
if (EXEMPT.has(rel)) continue;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!line.includes('send-keys')) return;
|
|
58
|
-
if (!/['"]-l['"]/.test(line)) return;
|
|
59
|
-
// Already funnelled.
|
|
60
|
-
if (line.includes('buildLiteralSendArgs')) return;
|
|
61
|
-
violations.push({ rel, line: i + 1, text: line.trim() });
|
|
62
|
-
});
|
|
206
|
+
for (const hit of scanSource(fs.readFileSync(file, 'utf-8'))) {
|
|
207
|
+
violations.push({ rel, line: hit.line, text: hit.text });
|
|
208
|
+
}
|
|
63
209
|
}
|
|
64
210
|
|
|
65
211
|
if (violations.length > 0) {
|
|
@@ -76,3 +222,4 @@ if (violations.length > 0) {
|
|
|
76
222
|
}
|
|
77
223
|
|
|
78
224
|
console.log(`✓ tmux literal sends funnelled (scanned ${walk(SRC).length} files, 0 unfunneled)`);
|
|
225
|
+
}
|
|
@@ -64,6 +64,86 @@ const PATTERNS = [
|
|
|
64
64
|
/method\s*:\s*['"`]createForumTopic['"`]/,
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
+
// ── Name resolution (added 2026-08-15) ────────────────────────────────────
|
|
68
|
+
// The three patterns above each require `createForumTopic` as a string
|
|
69
|
+
// LITERAL adjacent to the seam. Measured against the shipped lint, all three
|
|
70
|
+
// of these reach the Bot API and NONE were caught:
|
|
71
|
+
//
|
|
72
|
+
// const M = 'createForumTopic'; apiCall(M, {...});
|
|
73
|
+
// apiCall('createForum' + 'Topic', {...});
|
|
74
|
+
// const M = 'createForumTopic'; ({ method: M, ... });
|
|
75
|
+
//
|
|
76
|
+
// None of that is evasive — lifting a repeated string into a named constant
|
|
77
|
+
// is ordinary tidying. Someone could step around the notification-flood
|
|
78
|
+
// ceiling while making code NICER, and this lint would say `clean`. So the
|
|
79
|
+
// method name is RESOLVED before the existing rules are applied; the rules
|
|
80
|
+
// themselves are unchanged, they simply see through one level of naming.
|
|
81
|
+
//
|
|
82
|
+
// Deliberately NOT a parser. Everything below is bounded, per-file, and
|
|
83
|
+
// fails toward NOT flagging, because this lint blocks commits and a check
|
|
84
|
+
// that flags correct code gets switched off — which would cost more than the
|
|
85
|
+
// hole it closes.
|
|
86
|
+
|
|
87
|
+
const METHOD = 'createForumTopic';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fold ADJACENT string literals only: `'a' + 'b'` -> `'ab'`.
|
|
91
|
+
* Never invents text and never folds across an identifier, so
|
|
92
|
+
* `'createForum' + suffix` stays unresolved rather than guessed at.
|
|
93
|
+
*/
|
|
94
|
+
export function foldAdjacentLiterals(text) {
|
|
95
|
+
let out = text;
|
|
96
|
+
for (let i = 0; i < 5; i++) {
|
|
97
|
+
const next = out.replace(
|
|
98
|
+
/(['"`])([^'"`\n]*)\1\s*\+\s*(['"`])([^'"`\n]*)\3/g,
|
|
99
|
+
(_m, q, a, _q2, b) => `${q}${a}${b}${q}`,
|
|
100
|
+
);
|
|
101
|
+
if (next === out) break;
|
|
102
|
+
out = next;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Identifiers bound to a string literal (after folding) in THIS file.
|
|
109
|
+
* An identifier bound more than once to DIFFERENT values is UNRESOLVABLE and
|
|
110
|
+
* is dropped, so an ambiguous name can never be substituted into a match.
|
|
111
|
+
*/
|
|
112
|
+
export function collectStringConsts(lines) {
|
|
113
|
+
const seen = new Map();
|
|
114
|
+
const conflicting = new Set();
|
|
115
|
+
const DECL = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+)/;
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const m = DECL.exec(line);
|
|
118
|
+
if (!m) continue;
|
|
119
|
+
const name = m[1];
|
|
120
|
+
const folded = foldAdjacentLiterals(m[2].trim());
|
|
121
|
+
const lit = /^(['"`])([^'"`\n]*)\1\s*$/.exec(folded);
|
|
122
|
+
if (!lit) continue;
|
|
123
|
+
const value = lit[2];
|
|
124
|
+
if (seen.has(name) && seen.get(name) !== value) conflicting.add(name);
|
|
125
|
+
else seen.set(name, value);
|
|
126
|
+
}
|
|
127
|
+
for (const name of conflicting) seen.delete(name);
|
|
128
|
+
return seen;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Produce a view of one line with the method name resolved, for matching only.
|
|
133
|
+
* Substitution happens ONLY in the two seam positions the rules look at —
|
|
134
|
+
* never globally — so an unrelated identifier sharing a value is untouched.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveLine(line, consts) {
|
|
137
|
+
let out = foldAdjacentLiterals(line);
|
|
138
|
+
out = out.replace(/(apiCall\(\s*)([A-Za-z_$][\w$]*)/g, (m, head, name) =>
|
|
139
|
+
consts.get(name) === METHOD ? `${head}'${METHOD}'` : m,
|
|
140
|
+
);
|
|
141
|
+
out = out.replace(/(method\s*:\s*)([A-Za-z_$][\w$]*)/g, (m, head, name) =>
|
|
142
|
+
consts.get(name) === METHOD ? `${head}'${METHOD}'` : m,
|
|
143
|
+
);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
67
147
|
function listFiles() {
|
|
68
148
|
const staged = process.argv.includes('--staged');
|
|
69
149
|
if (staged) {
|
|
@@ -94,36 +174,54 @@ function listFiles() {
|
|
|
94
174
|
return files;
|
|
95
175
|
}
|
|
96
176
|
|
|
97
|
-
|
|
98
|
-
for (const rel of listFiles()) {
|
|
99
|
-
const normalized = rel.split(path.sep).join('/');
|
|
100
|
-
if (ALLOWLIST.has(normalized)) continue;
|
|
101
|
-
if (!EXTENSIONS.has(path.extname(normalized))) continue;
|
|
102
|
-
const full = path.join(ROOT, normalized);
|
|
103
|
-
let content;
|
|
104
|
-
try {
|
|
105
|
-
content = fs.readFileSync(full, 'utf-8');
|
|
106
|
-
} catch {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
177
|
+
export function scanFile(normalized, content) {
|
|
109
178
|
const lines = content.split('\n');
|
|
179
|
+
const consts = collectStringConsts(lines);
|
|
180
|
+
const hits = [];
|
|
110
181
|
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
const resolved = resolveLine(lines[i], consts);
|
|
111
183
|
for (const pattern of PATTERNS) {
|
|
112
|
-
if (pattern.test(
|
|
113
|
-
console.error(
|
|
114
|
-
`${normalized}:${i + 1} — raw createForumTopic invocation outside the budgeted funnel. ` +
|
|
115
|
-
`Route through TelegramAdapter.createForumTopic / findOrCreateForumTopic (declare an origin/label), ` +
|
|
116
|
-
`or add an allowlist entry here with a bounded-volume justification.`,
|
|
117
|
-
);
|
|
118
|
-
violations++;
|
|
119
|
-
}
|
|
184
|
+
if (pattern.test(resolved)) hits.push({ file: normalized, line: i + 1 });
|
|
120
185
|
}
|
|
121
186
|
}
|
|
187
|
+
return hits;
|
|
122
188
|
}
|
|
123
189
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
190
|
+
function runLint() {
|
|
191
|
+
let violations = 0;
|
|
192
|
+
for (const rel of listFiles()) {
|
|
193
|
+
const normalized = rel.split(path.sep).join('/');
|
|
194
|
+
if (ALLOWLIST.has(normalized)) continue;
|
|
195
|
+
if (!EXTENSIONS.has(path.extname(normalized))) continue;
|
|
196
|
+
const full = path.join(ROOT, normalized);
|
|
197
|
+
let content;
|
|
198
|
+
try {
|
|
199
|
+
content = fs.readFileSync(full, 'utf-8');
|
|
200
|
+
} catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const hit of scanFile(normalized, content)) {
|
|
204
|
+
console.error(
|
|
205
|
+
`${hit.file}:${hit.line} — raw createForumTopic invocation outside the budgeted funnel. ` +
|
|
206
|
+
`Route through TelegramAdapter.createForumTopic / findOrCreateForumTopic (declare an origin/label), ` +
|
|
207
|
+
`or add an allowlist entry here with a bounded-volume justification.`,
|
|
208
|
+
);
|
|
209
|
+
violations++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (violations > 0) {
|
|
214
|
+
console.error(`\nlint-no-unfunneled-topic-creation: ${violations} violation(s). ` +
|
|
215
|
+
`See docs/STANDARDS-REGISTRY.md "Bounded Notification Surface".`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
console.log('lint-no-unfunneled-topic-creation: clean');
|
|
128
219
|
}
|
|
129
|
-
|
|
220
|
+
|
|
221
|
+
// DIRECT-INVOCATION GUARD. Without this, importing this module to unit-test the
|
|
222
|
+
// resolution helpers runs the whole repo scan — and calls process.exit(1) the
|
|
223
|
+
// moment the repo has a real violation, killing the test runner. Three other
|
|
224
|
+
// lints hit exactly that this window; the guard is the same fix.
|
|
225
|
+
const invokedDirectly =
|
|
226
|
+
process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename);
|
|
227
|
+
if (invokedDirectly) runLint();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-08-15T01:02:42.043Z",
|
|
5
|
+
"instarVersion": "1.3.1152",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1152",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "cb5b39009ce485f47b3010b819bd2a9c0dc4e2294204deaef41bf69db200fd41",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1152"
|
|
5
5
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`scripts/lint-no-unfunneled-topic-creation.js` now resolves the Telegram method name before applying
|
|
9
|
+
its rules.
|
|
10
|
+
|
|
11
|
+
That lint enforces the **Bounded Notification Surface** standard — the last-resort budget on
|
|
12
|
+
automatically-created forum topics, added after the third topic-spam incident. Its three patterns each
|
|
13
|
+
required `createForumTopic` as a string LITERAL adjacent to the seam, so all three of these reached the
|
|
14
|
+
Bot API uncounted while the build reported `clean`:
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
const M = 'createForumTopic'; apiCall(M, { name: 'x' });
|
|
18
|
+
apiCall('createForum' + 'Topic', { name: 'x' });
|
|
19
|
+
const M = 'createForumTopic'; ({ method: M, name: 'x' });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Measured against the shipped lint with a positive control (the bare literal) firing in the same run:
|
|
23
|
+
control caught, all three EVADE.
|
|
24
|
+
|
|
25
|
+
None of that is evasive — lifting a repeated string into a named constant is ordinary tidying. Someone
|
|
26
|
+
could step around a notification-flood ceiling while making code *nicer*, and nothing would say a word.
|
|
27
|
+
|
|
28
|
+
Added: `foldAdjacentLiterals` (folds `'a' + 'b'` only, never across an identifier),
|
|
29
|
+
`collectStringConsts` (per-file identifier → literal; an identifier bound twice to different values is
|
|
30
|
+
dropped as unresolvable), `resolveLine` (substitutes ONLY at the two seam positions), and `scanFile`
|
|
31
|
+
(extracted so matching is testable). `PATTERNS`, `ALLOWLIST` and the violation message are unchanged.
|
|
32
|
+
|
|
33
|
+
Also added: a **direct-invocation guard**. Importing this module previously ran the whole repo scan and
|
|
34
|
+
called `process.exit(1)` on the first real violation, killing the importing process — which is why its
|
|
35
|
+
internals had never been unit-tested. Three other lints hit the same hazard this week.
|
|
36
|
+
|
|
37
|
+
## What to Tell Your User
|
|
38
|
+
|
|
39
|
+
Nothing changes for you. A build-time check that stops features from creating unlimited Telegram topics
|
|
40
|
+
could be walked past by giving a string a name first — an ordinary bit of tidying, not a trick. It now
|
|
41
|
+
sees through that. Nothing you use behaves differently; a category of notification flood just got harder
|
|
42
|
+
to ship by accident.
|
|
43
|
+
|
|
44
|
+
## Summary of New Capabilities
|
|
45
|
+
|
|
46
|
+
None. No new command, endpoint, setting, or runtime behaviour. A CI guard that was defeatable by a local
|
|
47
|
+
constant is no longer defeatable by one.
|
|
48
|
+
|
|
49
|
+
## Evidence
|
|
50
|
+
|
|
51
|
+
- `tests/unit/topic-creation-lint-resolution.test.ts` — 18/18 green.
|
|
52
|
+
- **Negative control: 5 of 18 fail** against the shipped matching behaviour; the other 13 pass both ways
|
|
53
|
+
and are the controls. Source restored byte-exact afterwards (sha match, zero markers left).
|
|
54
|
+
- Six anti-over-block controls, because this lint blocks commits: a const bound elsewhere, an identifier
|
|
55
|
+
that never reaches a seam, an identifier with conflicting bindings, a concatenation involving an
|
|
56
|
+
identifier, a longer look-alike method name, and seam-local (not global) substitution.
|
|
57
|
+
- Real tree: `exit 0` before AND after — no new flags on existing code.
|
|
58
|
+
- Full `npm run lint` chain green (57 steps); lint-chain membership verified explicitly rather than
|
|
59
|
+
inferred from a `package.json` reference.
|
|
60
|
+
- **Declared open in the source:** cross-module names, runtime-built names, and anything needing
|
|
61
|
+
dataflow. Also named: the lint does not strip comments, which is pre-existing and neither fixed nor
|
|
62
|
+
worsened here.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`scripts/lint-no-unfunneled-tmux-literal-send.js` now matches the bracket-matched ARGV ARRAY rather
|
|
9
|
+
than a single line.
|
|
10
|
+
|
|
11
|
+
That lint guards the `send-keys -l` argv ceiling (~16.2 KB) — the class behind the 2026-08-04 incident
|
|
12
|
+
where a ~40 KB prompt blew the ceiling, the circuit breaker misread the opaque send error as a provider
|
|
13
|
+
rate-limit and tripped 14 consecutive times, and ten LLM-backed components sat at 76-100% error rate.
|
|
14
|
+
|
|
15
|
+
It required `send-keys` and `'-l'` on the SAME line, so four PLAIN literal forms evaded it — measured
|
|
16
|
+
against the shipped check with the one-line form as a positive control firing in the same run: a
|
|
17
|
+
multi-line array (what any formatter produces), the flag lifted into a const, the verb lifted into a
|
|
18
|
+
const, and — worst in kind — merely NAMING the funnel in a comment on that line, so
|
|
19
|
+
`// TODO: use buildLiteralSendArgs` beside a raw send silenced the guard the TODO was admitting was
|
|
20
|
+
needed.
|
|
21
|
+
|
|
22
|
+
The first is the one that matters: **the guard could be defeated by running prettier.**
|
|
23
|
+
|
|
24
|
+
Added: `stripComments` (quote-aware, line-number preserving), `collectStringConsts` (per-file;
|
|
25
|
+
conflicting bindings dropped as unresolvable), `arrayRegions` (bracket-matched, string-aware, bounded),
|
|
26
|
+
`regionViolates` and `scanSource`. The rule, the exemption and the message are unchanged. Also added a
|
|
27
|
+
direct-invocation guard — importing the module previously ran the whole `src/` scan and could
|
|
28
|
+
`process.exit(1)`, which is why its internals had never been unit-tested.
|
|
29
|
+
|
|
30
|
+
## What to Tell Your User
|
|
31
|
+
|
|
32
|
+
Nothing changes for you. A build-time check that stops a known way of breaking terminal sends could be
|
|
33
|
+
walked past just by letting a code formatter split a list across lines — no intent required. It now
|
|
34
|
+
reads the whole list instead of one line. Nothing you use behaves differently; a failure mode that once
|
|
35
|
+
took down ten components for hours got harder to reintroduce by accident.
|
|
36
|
+
|
|
37
|
+
## Summary of New Capabilities
|
|
38
|
+
|
|
39
|
+
None. No new command, endpoint, setting, or runtime behaviour. A CI guard that a code formatter could
|
|
40
|
+
defeat no longer can be.
|
|
41
|
+
|
|
42
|
+
## Evidence
|
|
43
|
+
|
|
44
|
+
- `tests/unit/tmux-send-lint-array-scope.test.ts` — 18/18 green.
|
|
45
|
+
- **Negative control: 6 of 18 fail** against the shipped line-oriented behaviour; the other 12 pass both
|
|
46
|
+
ways and are the controls. Source restored byte-exact afterwards (sha match, zero markers left).
|
|
47
|
+
- Six anti-over-block controls, because this lint blocks commits: send-keys without the flag; a genuinely
|
|
48
|
+
funnelled call; two SEPARATE arrays never joined; a const bound to a different flag; an example living
|
|
49
|
+
entirely inside a comment; and an identifier with conflicting bindings left unresolvable.
|
|
50
|
+
- Real tree: `exit 0` before AND after, 1,631 files scanned.
|
|
51
|
+
- Full `npm run lint` chain green; chain membership verified explicitly rather than inferred.
|
|
52
|
+
- **Scope CORRECTED, not restated:** the old header claimed only that a dynamically-built argv could
|
|
53
|
+
evade it. None of the four measured evasions is dynamic. The header now names what was actually
|
|
54
|
+
missing, and the genuine remainder (a runtime-assembled array — push/concat/spread/helper).
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Side-Effects Review — tmux send-keys guard matches the array, not the line
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `tmux-send-lint-array-scope`
|
|
4
|
+
**Date:** `2026-08-15`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required — Tier 1 (CI-only lint script; no runtime path). The rule is unchanged; the check now reads the unit it always meant (the argv array) instead of a single line.`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`scripts/lint-no-unfunneled-tmux-literal-send.js` guards the `send-keys -l` argv ceiling (~16.2 KB).
|
|
11
|
+
The 2026-08-04 incident it exists to prevent: a ~40 KB prompt blew the ceiling, `LlmCircuitBreaker`
|
|
12
|
+
misclassified the opaque send error as a provider rate-limit and tripped 14 consecutive times while ten
|
|
13
|
+
LLM-backed components sat at 76-100% error rate.
|
|
14
|
+
|
|
15
|
+
The check required `send-keys` AND `'-l'` on the SAME LINE. Measured against the shipped check, with the
|
|
16
|
+
one-line form as a positive control firing in the same run:
|
|
17
|
+
|
|
18
|
+
| form | shipped |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `["send-keys", "-l", p]` — POSITIVE CONTROL | exit 1 (caught) |
|
|
21
|
+
| the same array across 5 lines | **exit 0 — EVADES** |
|
|
22
|
+
| `const F = "-l"; ["send-keys", F, p]` | **exit 0 — EVADES** |
|
|
23
|
+
| `const C = "send-keys"; [C, "-l", p]` | **exit 0 — EVADES** |
|
|
24
|
+
| `["send-keys","-l",p] // buildLiteralSendArgs` | **exit 0 — EVADES** |
|
|
25
|
+
|
|
26
|
+
The unit of matching is now the bracket-matched ARRAY LITERAL, over comment-stripped, const-resolved
|
|
27
|
+
source. The rule, the exemption and the message are unchanged.
|
|
28
|
+
|
|
29
|
+
## Decision-point inventory
|
|
30
|
+
|
|
31
|
+
- `stripComments(src)` — ADD — quote-aware; replaces removed bytes with spaces so line numbers are exact.
|
|
32
|
+
- `collectStringConsts(code)` — ADD — per-file identifier → literal; conflicting bindings dropped.
|
|
33
|
+
- `arrayRegions(code)` — ADD — bracket-matched, string-aware, bounded at 4000 chars per region.
|
|
34
|
+
- `regionViolates(text, consts)` — ADD — resolves then applies the EXISTING two patterns.
|
|
35
|
+
- `scanSource(code)` — ADD — the composed scan, extracted so matching is testable.
|
|
36
|
+
- Direct-invocation guard — ADD — the scan runs only when invoked directly.
|
|
37
|
+
- The `EXEMPT` set, the two patterns, the violation message and exit codes — UNCHANGED.
|
|
38
|
+
- No runtime block/allow decision added or modified. CI-time only.
|
|
39
|
+
|
|
40
|
+
## 1. Over-block
|
|
41
|
+
|
|
42
|
+
The failure that matters: this lint blocks commits, and a check that flags correct code gets switched
|
|
43
|
+
off. Six controls, each with a test, all passing under BOTH old and new behaviour:
|
|
44
|
+
|
|
45
|
+
- `send-keys` WITHOUT `-l` is not flagged (only the literal form has the ceiling).
|
|
46
|
+
- A genuinely funnelled `buildLiteralSendArgs(...)` call is not flagged.
|
|
47
|
+
- **Two separate arrays are never joined** — an unrelated `["ls", "-l"]` cannot complete a send-keys
|
|
48
|
+
array. This is the whole reason the unit is a bracket-matched region rather than a line window.
|
|
49
|
+
- A const bound to a different flag is not flagged.
|
|
50
|
+
- An example living entirely inside a comment is not flagged — which is what makes it safe to document
|
|
51
|
+
the bad pattern, including in this file and the ELI16.
|
|
52
|
+
- An identifier bound twice to DIFFERENT values is unresolvable and never substituted.
|
|
53
|
+
|
|
54
|
+
Unbalanced brackets yield no region, so a syntax error elsewhere cannot become a false accusation.
|
|
55
|
+
|
|
56
|
+
**Verified against the real tree: exit 0 before AND after**, scanning 1,631 files. No new flags.
|
|
57
|
+
|
|
58
|
+
## 2. Under-block
|
|
59
|
+
|
|
60
|
+
Stated in the source, and it is a CORRECTION rather than a restatement. The old header claimed only
|
|
61
|
+
that "a wrapper that builds the argv array dynamically could still evade it" — true, but it understated
|
|
62
|
+
the gap, since none of the four measured evasions is dynamic or a wrapper. The header now names what was
|
|
63
|
+
actually missing and what genuinely remains:
|
|
64
|
+
|
|
65
|
+
- An argv array assembled at RUNTIME — `push()`, `concat()`, spread of a computed list, or a helper that
|
|
66
|
+
returns the array. That is the original declared gap and it is the only one left.
|
|
67
|
+
- Closing it needs dataflow, not more patterns.
|
|
68
|
+
|
|
69
|
+
## 3. Level-of-abstraction fit
|
|
70
|
+
|
|
71
|
+
Same layer as the existing check — regex over source text, no AST, no type information, no new
|
|
72
|
+
dependency. Bracket matching is the minimum needed to read an array literal as one unit; going further
|
|
73
|
+
(a parser) would be a different check at a different layer.
|
|
74
|
+
|
|
75
|
+
## 4. Signal vs authority compliance
|
|
76
|
+
|
|
77
|
+
Unchanged. A CI guard, not a runtime authority. It pushes callers toward `buildLiteralSendArgs()`; the
|
|
78
|
+
funnel itself stays exempt.
|
|
79
|
+
|
|
80
|
+
## 5. Interactions
|
|
81
|
+
|
|
82
|
+
- `npm run lint` chain — membership verified explicitly (in the `lint` chain CI runs, not merely
|
|
83
|
+
referenced by a standalone entry); full chain green.
|
|
84
|
+
- The direct-invocation guard changes import behaviour from "runs the src/ scan and may exit(1)" to
|
|
85
|
+
"exports only". Nothing imported this module before, so no caller changes.
|
|
86
|
+
- No source module, route, config key, or state file touched.
|
|
87
|
+
|
|
88
|
+
## 6. External surfaces
|
|
89
|
+
|
|
90
|
+
None. Developer tooling, not an agent capability; the Agent Awareness Standard does not apply.
|
|
91
|
+
|
|
92
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
93
|
+
|
|
94
|
+
**Machine-local BY DESIGN, and correctly so — not an unexamined assumption.** A CI-time source scan with
|
|
95
|
+
no runtime surface: it reads files in one checkout and exits. No durable state, no user-facing notice, no
|
|
96
|
+
generated URL, no runtime decision — so nothing to replicate, nothing to merge on read, nothing that can
|
|
97
|
+
strand on a topic transfer. Every machine runs it over its own checkout of the same tracked source and
|
|
98
|
+
reaches the same verdict; determinism comes from the source tree, not from coordination. Resolution is
|
|
99
|
+
explicitly per-file, so it cannot depend on the rest of the checkout, let alone another machine.
|
|
100
|
+
|
|
101
|
+
## 8. Rollback cost
|
|
102
|
+
|
|
103
|
+
`git revert` of one script plus the added test file. No migration, no state, no deployed artifact.
|
|
104
|
+
|
|
105
|
+
## Conclusion
|
|
106
|
+
|
|
107
|
+
Ship. Four evasions closed on a guard with a real production incident behind it, six anti-over-block
|
|
108
|
+
controls added, the stated scope corrected rather than restated, and the import hazard removed.
|
|
109
|
+
|
|
110
|
+
## Evidence pointers
|
|
111
|
+
|
|
112
|
+
- `tests/unit/tmux-send-lint-array-scope.test.ts` — **18/18 green**.
|
|
113
|
+
- **Negative control: 6 of 18 fail** against the shipped line-oriented behaviour. The other 12 pass both
|
|
114
|
+
ways — which is what makes them controls. Source restored byte-exact after the mutation (sha match,
|
|
115
|
+
zero markers left).
|
|
116
|
+
- Reproduced by hand FIRST with a positive control firing in the same run; the control is what makes the
|
|
117
|
+
four EVADES verdicts mean anything.
|
|
118
|
+
- Real-tree verdict: exit 0, 1,631 files scanned, before and after.
|
|
119
|
+
- Full `npm run lint` chain green.
|
|
120
|
+
- Tier **1** declared: risk floor 1, verified with a directional control (`SessionReaper.ts` → floor 2
|
|
121
|
+
with its reason named). The size heuristic suggests 2 on added LOC alone; stated openly, since the tier
|
|
122
|
+
I choose is the one that lets my own change through.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Side-Effects Review — topic-creation guard resolves the method name
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `topic-creation-lint-resolution`
|
|
4
|
+
**Date:** `2026-08-15`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required — Tier 1 (CI-only lint script; no runtime path). The rule is unchanged; the check now resolves one level of naming before applying the rules it already had.`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`scripts/lint-no-unfunneled-topic-creation.js` enforces the **Bounded Notification Surface** standard —
|
|
11
|
+
the last-resort budget on automatically-created forum topics, added after the THIRD topic-spam incident
|
|
12
|
+
(2026-06-05). All three of its patterns required `createForumTopic` as a string LITERAL adjacent to the
|
|
13
|
+
seam, so a resolved name walked past a safety floor.
|
|
14
|
+
|
|
15
|
+
Measured against the shipped lint, with a positive control (the bare literal) firing in the same run:
|
|
16
|
+
|
|
17
|
+
| form | shipped |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `apiCall("createForumTopic", …)` — POSITIVE CONTROL | exit 1 (caught) |
|
|
20
|
+
| `const M = "createForumTopic"; apiCall(M, …)` | **exit 0 — EVADES** |
|
|
21
|
+
| `apiCall("createForum" + "Topic", …)` | **exit 0 — EVADES** |
|
|
22
|
+
| `const M = "createForumTopic"; { method: M }` | **exit 0 — EVADES** |
|
|
23
|
+
|
|
24
|
+
The method name is now RESOLVED before the existing patterns are applied. The patterns are byte-identical;
|
|
25
|
+
only what they can see changed.
|
|
26
|
+
|
|
27
|
+
## Decision-point inventory
|
|
28
|
+
|
|
29
|
+
- `foldAdjacentLiterals(text)` — ADD — folds `'a' + 'b'` only; never folds across an identifier.
|
|
30
|
+
- `collectStringConsts(lines)` — ADD — per-file identifier → string-literal map; an identifier bound twice
|
|
31
|
+
to DIFFERENT values is dropped as unresolvable.
|
|
32
|
+
- `resolveLine(line, consts)` — ADD — substitutes ONLY at the two seam positions (`apiCall(` and `method:`).
|
|
33
|
+
- `scanFile(normalized, content)` — ADD — extracted so the matching behaviour is testable without the CLI.
|
|
34
|
+
- Direct-invocation guard — ADD — the scan runs only when the script is invoked directly.
|
|
35
|
+
- `PATTERNS`, `ALLOWLIST`, `SCAN_DIRS`, `EXTENSIONS`, the violation message and exit codes — UNCHANGED.
|
|
36
|
+
- No runtime block/allow decision added or modified. CI-time only.
|
|
37
|
+
|
|
38
|
+
## 1. Over-block
|
|
39
|
+
|
|
40
|
+
The failure that matters. This lint blocks commits, and a check that flags correct code gets switched
|
|
41
|
+
off — which would cost more than the hole it closes. Six controls, each with a test, all passing under
|
|
42
|
+
BOTH the old and new behaviour:
|
|
43
|
+
|
|
44
|
+
- **A const bound to a different method** (`"sendMessage"`) is not flagged.
|
|
45
|
+
- **An identifier that never reaches a seam** is not flagged — holding the value is not calling with it.
|
|
46
|
+
- **An identifier bound twice to different values** is unresolvable and never substituted. Ambiguity
|
|
47
|
+
fails toward NOT flagging, because a guess that fails someone's build is the expensive direction.
|
|
48
|
+
- **A concatenation involving an identifier** (`"createForum" + suffix`) is left unresolved. Folding it
|
|
49
|
+
would mean inventing text the source does not contain.
|
|
50
|
+
- **A longer name starting with the method** (`createForumTopicIconStickers`) is not flagged — the
|
|
51
|
+
existing quote-delimited patterns already bound this, and resolution preserves it.
|
|
52
|
+
- **Substitution is seam-local, not global** — an unrelated `logger.debug(M)` is untouched.
|
|
53
|
+
|
|
54
|
+
Resolution is per-file by construction: one file's names cannot affect another's.
|
|
55
|
+
|
|
56
|
+
**Verified against the real tree: exit 0 before AND after.** No new flags on existing code.
|
|
57
|
+
|
|
58
|
+
## 2. Under-block
|
|
59
|
+
|
|
60
|
+
Stated in the source rather than implied:
|
|
61
|
+
|
|
62
|
+
- **Cross-module names** — a method name imported from another file is not followed.
|
|
63
|
+
- **Runtime-built names** — from a variable, a function call, or a template literal.
|
|
64
|
+
- **Anything needing dataflow** to resolve.
|
|
65
|
+
|
|
66
|
+
Guessing at those would over-match.
|
|
67
|
+
|
|
68
|
+
**One residual I want named rather than absorbed:** the lint does not strip comments, so a comment
|
|
69
|
+
containing the literal form is flagged. That is PRE-EXISTING behaviour (it is why this script is on its
|
|
70
|
+
own allowlist), and this change neither fixes nor worsens it — resolution runs on the same lines the
|
|
71
|
+
patterns already ran on. I am not changing it here because loosening a flood guard to be tidier is the
|
|
72
|
+
wrong direction, and doing it unrequested is the unrequested-tightening mistake in reverse.
|
|
73
|
+
|
|
74
|
+
## 3. Level-of-abstraction fit
|
|
75
|
+
|
|
76
|
+
Same layer as the existing check — line-oriented regex over raw source, no AST, no type information, no
|
|
77
|
+
new dependency. The resolution map is the minimum needed to answer "what method name is at this seam?"
|
|
78
|
+
without climbing to a parser.
|
|
79
|
+
|
|
80
|
+
## 4. Signal vs authority compliance
|
|
81
|
+
|
|
82
|
+
Unchanged. A CI guard, not a runtime authority. It pushes callers toward the budgeted funnel; the
|
|
83
|
+
allowlist still exempts the funnel itself, the lifeline's fixed-cardinality system topic, and the
|
|
84
|
+
setup-wizard doc string.
|
|
85
|
+
|
|
86
|
+
## 5. Interactions
|
|
87
|
+
|
|
88
|
+
- `npm run lint` chain — membership verified explicitly (the script is in the `lint` chain CI runs, not
|
|
89
|
+
merely referenced by a standalone entry); full chain exit 0 across 57 steps.
|
|
90
|
+
- The direct-invocation guard changes import behaviour from "runs the repo scan and may exit(1)" to
|
|
91
|
+
"exports only". Nothing imported this module before, so no caller changes.
|
|
92
|
+
- No source module, route, config key, or state file touched.
|
|
93
|
+
|
|
94
|
+
## 6. External surfaces
|
|
95
|
+
|
|
96
|
+
None. Developer tooling, not an agent capability; the Agent Awareness Standard does not apply.
|
|
97
|
+
|
|
98
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
99
|
+
|
|
100
|
+
**Machine-local BY DESIGN, and it is the correct posture — not an unexamined assumption.** This is a
|
|
101
|
+
CI-time source scan with no runtime surface: it reads files in one checkout and exits. It holds no
|
|
102
|
+
durable state, emits no user-facing notice, generates no URL, and makes no runtime decision, so there
|
|
103
|
+
is nothing to replicate, nothing to merge on read, and nothing that could strand on a topic transfer.
|
|
104
|
+
|
|
105
|
+
Every machine that runs the lint runs it over its own checkout of the same tracked source and reaches
|
|
106
|
+
the same verdict — determinism comes from the source tree, not from coordination. Resolution is
|
|
107
|
+
explicitly per-file, so it cannot even depend on the rest of the checkout, let alone another machine.
|
|
108
|
+
|
|
109
|
+
Worth stating because the audit that added this question found ~20 features shipped machine-blind: the
|
|
110
|
+
thing that makes THIS one genuinely local is that its input is version-controlled and its output is an
|
|
111
|
+
exit code, not that I could not think of a cross-machine concern.
|
|
112
|
+
|
|
113
|
+
## 8. Rollback cost
|
|
114
|
+
|
|
115
|
+
`git revert` of one script plus the added test file. No migration, no state, no deployed artifact.
|
|
116
|
+
|
|
117
|
+
## Conclusion
|
|
118
|
+
|
|
119
|
+
Ship. Three evasions closed on a safety floor, six anti-over-block controls added, real tree verified
|
|
120
|
+
clean in both directions, and the import hazard that blocked testing it removed.
|
|
121
|
+
|
|
122
|
+
## Evidence pointers
|
|
123
|
+
|
|
124
|
+
- `tests/unit/topic-creation-lint-resolution.test.ts` — **18/18 green**.
|
|
125
|
+
- **Negative control: 5 of 18 fail** against the shipped matching behaviour (local const, concatenation,
|
|
126
|
+
`method:` const, let/var bindings, const-declared-after-use). The other 13 pass both ways — which is
|
|
127
|
+
what makes them controls. Source restored byte-exact after the mutation (sha match, 0 markers left).
|
|
128
|
+
- Reproduced by hand FIRST with a positive control firing in the same run; the control is what makes the
|
|
129
|
+
three EVADES verdicts mean anything.
|
|
130
|
+
- Real-tree verdict: `node scripts/lint-no-unfunneled-topic-creation.js` → exit 0, before and after.
|
|
131
|
+
- Full `npm run lint` chain green, 57 steps.
|
|
132
|
+
- Tier **1** declared: risk floor 1 (no safety-invariant path match — verified with two directional
|
|
133
|
+
controls, `SessionReaper.ts` → floor 2 with its reason named, `devClaimCheck.ts` → floor 1). The size
|
|
134
|
+
heuristic suggests 2 on added LOC alone; stated openly rather than left silent, since the tier I chose
|
|
135
|
+
is the one that lets the change through.
|