instar 1.3.1148 → 1.3.1150
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-dev-agent-dark-gate.js +45 -1
- package/scripts/lint-no-unreachable-messaging-gate.js +150 -4
- 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.1149.md +48 -0
- package/upgrades/1.3.1150.md +21 -0
- package/upgrades/side-effects/dev-agent-gate-alias.md +104 -0
- package/upgrades/side-effects/lint-split-key-messaging-gate.md +117 -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.1150",
|
|
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": "65b8fa203de000f59bf34d2f1ac5f30e0656f81da112a5f1dde281cf1be35b3c",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1150"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -72,6 +72,49 @@ const FUNNEL_ALLOWLIST = new Set([
|
|
|
72
72
|
// in the spec's Layer-1 "misses" row.)
|
|
73
73
|
const HANDROLLED_GATE =
|
|
74
74
|
/\?\?\s*(?:!{1,2}\s*|Boolean\s*\(\s*)?[A-Za-z_$][\w$.?]*(?:\.developmentAgent\b|\[\s*['"]developmentAgent['"]\s*\])/;
|
|
75
|
+
|
|
76
|
+
// ── 2026-08-14: the gate value does not have to be spelled at the `??`. ──
|
|
77
|
+
// The matcher above requires `.developmentAgent` LITERALLY after the `??`, so
|
|
78
|
+
// lifting it into a local const first walks past it while resolving the gate by
|
|
79
|
+
// hand exactly as before:
|
|
80
|
+
// const da = config.developmentAgent;
|
|
81
|
+
// return enabled ?? !!da; // exit 0 against the old matcher
|
|
82
|
+
// The header's declared limit ("cannot catch arbitrary aliases/wrapper helpers")
|
|
83
|
+
// was honest; this closes the LOCAL-CONST half of it, which is the shape a
|
|
84
|
+
// rename-defeat audit actually reproduced.
|
|
85
|
+
//
|
|
86
|
+
// Deliberately NOT closed, so it stays stated rather than implied: wrapper
|
|
87
|
+
// helpers (`isDevAgent(config)`), cross-module aliases, and any value that needs
|
|
88
|
+
// dataflow to resolve. Guessing at those would over-match, and this lint fails
|
|
89
|
+
// builds — flagging correct code is the more expensive error here.
|
|
90
|
+
//
|
|
91
|
+
// Scope-bounded on purpose: an alias binds only within its own file, so the map
|
|
92
|
+
// is rebuilt per file and never leaks between them.
|
|
93
|
+
const ALIAS_DECL =
|
|
94
|
+
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::\s*[^=]+?)?=\s*[A-Za-z_$][\w$.?]*(?:\.developmentAgent\b|\[\s*['"]developmentAgent['"]\s*\])/;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Local identifiers bound to `<expr>.developmentAgent` in THIS file.
|
|
98
|
+
* `config.developmentAgentName` does not qualify — the `\b` keeps the boundary
|
|
99
|
+
* through the alias exactly as it holds at the direct callsite.
|
|
100
|
+
*/
|
|
101
|
+
function collectDevAgentAliases(lines) {
|
|
102
|
+
const names = new Set();
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const code = codeOnly(line);
|
|
105
|
+
if (code === null) continue;
|
|
106
|
+
const m = ALIAS_DECL.exec(code);
|
|
107
|
+
if (m) names.add(m[1]);
|
|
108
|
+
}
|
|
109
|
+
return names;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `?? [!! | Boolean(] <alias>` — the aliased spelling of the same hand-rolled gate. */
|
|
113
|
+
function aliasGateMatcher(names) {
|
|
114
|
+
if (names.size === 0) return null;
|
|
115
|
+
const alts = [...names].join('|');
|
|
116
|
+
return new RegExp(`\\?\\?\\s*(?:!{1,2}\\s*|Boolean\\s*\\(\\s*)?(?:${alts})\\b`);
|
|
117
|
+
}
|
|
75
118
|
// A comment referencing the gate convention (for assertion B).
|
|
76
119
|
const GATE_MARKER = /developmentAgent/i;
|
|
77
120
|
const GATE_MARKER_QUALIFIER = /\b(dark|gate)\b/i;
|
|
@@ -171,10 +214,11 @@ for (const file of resolveTargets()) {
|
|
|
171
214
|
|
|
172
215
|
// ── Assertion A: funnel ──
|
|
173
216
|
if (!FUNNEL_ALLOWLIST.has(rel)) {
|
|
217
|
+
const aliasGate = aliasGateMatcher(collectDevAgentAliases(lines));
|
|
174
218
|
lines.forEach((line, i) => {
|
|
175
219
|
const code = codeOnly(line);
|
|
176
220
|
if (code === null) return;
|
|
177
|
-
if (HANDROLLED_GATE.test(code)) {
|
|
221
|
+
if (HANDROLLED_GATE.test(code) || (aliasGate !== null && aliasGate.test(code))) {
|
|
178
222
|
violations.push({
|
|
179
223
|
file: rel, line: i + 1, kind: 'A: hand-rolled gate',
|
|
180
224
|
text: line.trim(),
|
|
@@ -42,14 +42,160 @@ export const UNREACHABLE_OFF_GATE =
|
|
|
42
42
|
|
|
43
43
|
const SUPPRESS = /lint-allow-messaging-gate\s*:/;
|
|
44
44
|
|
|
45
|
+
function stripCommentsPreserveLines(text) {
|
|
46
|
+
let out = '';
|
|
47
|
+
let inBlock = false;
|
|
48
|
+
let quote = null;
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
51
|
+
const ch = text[i];
|
|
52
|
+
const next = text[i + 1];
|
|
53
|
+
|
|
54
|
+
if (inBlock) {
|
|
55
|
+
if (ch === '*' && next === '/') {
|
|
56
|
+
out += ' ';
|
|
57
|
+
i += 1;
|
|
58
|
+
inBlock = false;
|
|
59
|
+
} else {
|
|
60
|
+
out += ch === '\n' ? '\n' : ' ';
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (quote) {
|
|
66
|
+
out += ch;
|
|
67
|
+
if (ch === '\\' && i + 1 < text.length) {
|
|
68
|
+
out += text[i + 1];
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch === quote) quote = null;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (ch === '/' && next === '/') {
|
|
77
|
+
out += ' ';
|
|
78
|
+
i += 1;
|
|
79
|
+
while (i + 1 < text.length && text[i + 1] !== '\n') {
|
|
80
|
+
out += ' ';
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ch === '/' && next === '*') {
|
|
87
|
+
out += ' ';
|
|
88
|
+
i += 1;
|
|
89
|
+
inBlock = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (ch === "'" || ch === '"' || ch === '`') quote = ch;
|
|
94
|
+
out += ch;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function skipWs(s, i) {
|
|
101
|
+
while (i < s.length && /\s/.test(s[i])) i += 1;
|
|
102
|
+
return i;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseStringLiteral(s, start) {
|
|
106
|
+
const quote = s[start];
|
|
107
|
+
if (quote !== "'" && quote !== '"' && quote !== '`') return null;
|
|
108
|
+
|
|
109
|
+
let value = '';
|
|
110
|
+
for (let i = start + 1; i < s.length; i += 1) {
|
|
111
|
+
const ch = s[i];
|
|
112
|
+
if (ch === '\\' && i + 1 < s.length) {
|
|
113
|
+
value += s[i + 1];
|
|
114
|
+
i += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (quote === '`' && ch === '$' && s[i + 1] === '{') return null;
|
|
118
|
+
if (ch === quote) return { value, end: i + 1 };
|
|
119
|
+
value += ch;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseLiteralConcat(s, start) {
|
|
125
|
+
let i = skipWs(s, start);
|
|
126
|
+
let parsed = parseStringLiteral(s, i);
|
|
127
|
+
if (!parsed) return null;
|
|
128
|
+
|
|
129
|
+
let value = parsed.value;
|
|
130
|
+
i = skipWs(s, parsed.end);
|
|
131
|
+
while (s[i] === '+') {
|
|
132
|
+
i = skipWs(s, i + 1);
|
|
133
|
+
parsed = parseStringLiteral(s, i);
|
|
134
|
+
if (!parsed) return null;
|
|
135
|
+
value += parsed.value;
|
|
136
|
+
i = skipWs(s, parsed.end);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { value, end: i };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getCallArgStartAt(s, start) {
|
|
143
|
+
if (!s.startsWith('.get', start)) return null;
|
|
144
|
+
if (/[$_\p{ID_Continue}]/u.test(s[start + 4] ?? '')) return null;
|
|
145
|
+
|
|
146
|
+
let i = skipWs(s, start + 4);
|
|
147
|
+
if (s[i] === '<') {
|
|
148
|
+
const close = s.indexOf('>', i + 1);
|
|
149
|
+
if (close === -1) return null;
|
|
150
|
+
i = skipWs(s, close + 1);
|
|
151
|
+
}
|
|
152
|
+
if (s[i] !== '(') return null;
|
|
153
|
+
return i + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function lineHasUnreachableOffGate(line) {
|
|
157
|
+
let quote = null;
|
|
158
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
159
|
+
const ch = line[i];
|
|
160
|
+
if (quote) {
|
|
161
|
+
if (ch === '\\' && i + 1 < line.length) {
|
|
162
|
+
i += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (ch === quote) quote = null;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
170
|
+
quote = ch;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const firstArgStart = getCallArgStartAt(line, i);
|
|
175
|
+
if (firstArgStart === null) continue;
|
|
176
|
+
|
|
177
|
+
const firstArg = parseLiteralConcat(line, firstArgStart);
|
|
178
|
+
if (!firstArg || !firstArg.value.startsWith('messaging.')) continue;
|
|
179
|
+
|
|
180
|
+
let j = skipWs(line, firstArg.end);
|
|
181
|
+
if (line[j] !== ',') continue;
|
|
182
|
+
j = skipWs(line, j + 1);
|
|
183
|
+
if (line.startsWith('false', j) && !/[$_\p{ID_Continue}]/u.test(line[j + 5] ?? '')) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
45
190
|
/** Scan raw source text; return 1-indexed line numbers of un-suppressed offenders. */
|
|
46
191
|
export function scanText(text) {
|
|
47
|
-
const lines = text.split('\n');
|
|
192
|
+
const lines = stripCommentsPreserveLines(text).split('\n');
|
|
193
|
+
const originalLines = text.split('\n');
|
|
48
194
|
const hits = [];
|
|
49
195
|
lines.forEach((line, i) => {
|
|
50
|
-
if (!
|
|
51
|
-
if (SUPPRESS.test(
|
|
52
|
-
if (i > 0 && SUPPRESS.test(
|
|
196
|
+
if (!lineHasUnreachableOffGate(line)) return;
|
|
197
|
+
if (SUPPRESS.test(originalLines[i])) return;
|
|
198
|
+
if (i > 0 && SUPPRESS.test(originalLines[i - 1])) return;
|
|
53
199
|
hits.push(i + 1);
|
|
54
200
|
});
|
|
55
201
|
return hits;
|
|
@@ -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-15T00:12:54.107Z",
|
|
5
|
+
"instarVersion": "1.3.1150",
|
|
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.1150",
|
|
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": "65b8fa203de000f59bf34d2f1ac5f30e0656f81da112a5f1dde281cf1be35b3c",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1150"
|
|
5
5
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`scripts/lint-dev-agent-dark-gate.js` assertion A bans hand-rolled dev-agent gate resolution outside
|
|
9
|
+
`resolveDevAgentGate`. The matcher required `.developmentAgent` **literally after the `??`**, so lifting the
|
|
10
|
+
value into a local const first walked past it while resolving the gate by hand exactly as before:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const da = config.developmentAgent;
|
|
14
|
+
return enabled ?? !!da; // exit 0 against the shipped lint
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
instar-codey reproduced this while auditing rename-defeatable checks and pre-scoped the remedy ("low FP for
|
|
18
|
+
simple local alias folding around `developmentAgent` feeding `??`"). That is the scope implemented: local
|
|
19
|
+
`const`/`let`/`var` aliases, collected per file, matched at `?? [!!|Boolean(] <alias>`.
|
|
20
|
+
|
|
21
|
+
The lint's header already declared this limit ("cannot catch arbitrary aliases/wrapper helpers"). It is
|
|
22
|
+
closed for the shape that actually occurs; the declaration was honest, not wrong.
|
|
23
|
+
|
|
24
|
+
Four controls keep it from failing correct builds, each with a test: only a `developmentAgent` binding
|
|
25
|
+
counts; matching is whole-word so `developmentAgentName` is untouched; an alias never used at a `??` is
|
|
26
|
+
legal (reading the flag is fine — hand-rolling the fallback is what is banned); and a commented-out
|
|
27
|
+
declaration binds nothing.
|
|
28
|
+
|
|
29
|
+
Still not caught, stated in the source: wrapper helpers, cross-module aliases, and anything needing
|
|
30
|
+
dataflow. Guessing at those would over-match a build-failing lint.
|
|
31
|
+
|
|
32
|
+
## What to Tell Your User
|
|
33
|
+
|
|
34
|
+
None — internal change (no user-facing surface).
|
|
35
|
+
|
|
36
|
+
## Summary of New Capabilities
|
|
37
|
+
|
|
38
|
+
None — internal change (no user-facing surface).
|
|
39
|
+
|
|
40
|
+
## Evidence
|
|
41
|
+
|
|
42
|
+
- `tests/unit/lint-dev-agent-dark-gate.test.ts` — **31/31 green** (24 existing + 7 added).
|
|
43
|
+
- Negative control: tests written BEFORE the fix, run against the shipped lint — **3 of 31 fail** (const
|
|
44
|
+
alias, bracket-access alias, `Boolean(alias)`). The other 28 pass both ways.
|
|
45
|
+
- Reproduced by hand first with a positive control: direct form exits 1, aliased form exits 0.
|
|
46
|
+
- Real-tree verdict: `node scripts/lint-dev-agent-dark-gate.js` → exit 0, no new flags.
|
|
47
|
+
- Full `npm run lint` chain green.
|
|
48
|
+
- Side-effects: `upgrades/side-effects/dev-agent-gate-alias.md` · ELI16: `docs/specs/dev-agent-gate-alias.eli16.md`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- Strengthened the unreachable messaging gate lint so it detects default-off `messaging.*` LiveConfig keys built from literal string concatenation.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
None — internal change (no user-facing surface).
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
None — internal change (no user-facing surface).
|
|
17
|
+
|
|
18
|
+
## Evidence
|
|
19
|
+
|
|
20
|
+
- `npx vitest run tests/unit/lint-no-unreachable-messaging-gate.test.ts`
|
|
21
|
+
- `node scripts/lint-no-unreachable-messaging-gate.js`
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Side-Effects Review — dev-agent gate check follows a local alias
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dev-agent-gate-alias`
|
|
4
|
+
**Date:** `2026-08-14`
|
|
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 recognises one more spelling of the thing it already forbids.`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`scripts/lint-dev-agent-dark-gate.js` assertion A bans hand-rolled dev-agent gate resolution — anything
|
|
11
|
+
resolving `enabled ?? !!<x>.developmentAgent` outside `resolveDevAgentGate`. The matcher required
|
|
12
|
+
`.developmentAgent` (or `['developmentAgent']`) **literally after the `??`**, so lifting the value into a
|
|
13
|
+
local const first walked past it while resolving the gate by hand exactly as before:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const da = config.developmentAgent;
|
|
17
|
+
return enabled ?? !!da; // exit 0 against the shipped lint
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
instar-codey reproduced this while auditing rename-defeatable checks and scoped the remedy: *"low for
|
|
21
|
+
simple local alias folding around `developmentAgent` feeding `??`."* That is the scope implemented — local
|
|
22
|
+
`const`/`let`/`var` aliases only, rebuilt per file.
|
|
23
|
+
|
|
24
|
+
The lint's header **already declared this limit** ("cannot catch arbitrary aliases/wrapper helpers"). Like
|
|
25
|
+
the journal-actuation ban earlier today, it is closed because the declared gap is cheap to close for the
|
|
26
|
+
shape that actually occurs, not because the declaration was dishonest.
|
|
27
|
+
|
|
28
|
+
## Decision-point inventory
|
|
29
|
+
|
|
30
|
+
- `collectDevAgentAliases(lines)` — ADD — per-file identifiers bound to `<expr>.developmentAgent`.
|
|
31
|
+
- `aliasGateMatcher(names)` — ADD — `?? [!!|Boolean(] <alias>`; returns null when there are no aliases, so
|
|
32
|
+
a file without one is byte-identical to before.
|
|
33
|
+
- Assertion A predicate — WIDEN — `HANDROLLED_GATE || aliasGate`.
|
|
34
|
+
- Assertions B and C, the funnel allowlist, the comment-stripping (`codeOnly`), and the marker logic are
|
|
35
|
+
untouched.
|
|
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 fails builds, so flagging correct code costs more than missing a case.
|
|
41
|
+
Four controls, each with a test:
|
|
42
|
+
|
|
43
|
+
- **An alias of something else** (`config.somethingElse`) is not flagged — only `developmentAgent` binds.
|
|
44
|
+
- **A look-alike name** (`config.developmentAgentName`) is not flagged: the `\b` holds through the alias
|
|
45
|
+
exactly as it holds at the direct callsite.
|
|
46
|
+
- **An alias never used at a `??`** is not flagged. *Reading* the flag is legal; resolving the GATE by hand
|
|
47
|
+
is what is banned, and that distinction is preserved.
|
|
48
|
+
- **A comment describing the aliased pattern** is not flagged — `codeOnly` already strips comments and the
|
|
49
|
+
alias collector runs on the same stripped lines, so a commented-out declaration binds nothing.
|
|
50
|
+
|
|
51
|
+
Scope is per-file by construction: aliases cannot leak between files.
|
|
52
|
+
|
|
53
|
+
Verified against the real tree: exit 0 — the widened check introduces **no new flags on existing code**.
|
|
54
|
+
|
|
55
|
+
## 2. Under-block
|
|
56
|
+
|
|
57
|
+
Stated in the source rather than implied:
|
|
58
|
+
|
|
59
|
+
- **Wrapper helpers** (`isDevAgent(config)`) — still invisible.
|
|
60
|
+
- **Cross-module aliases** — an alias exported from another file is not followed.
|
|
61
|
+
- **Anything needing dataflow** to resolve.
|
|
62
|
+
|
|
63
|
+
Guessing at those would over-match. The header's original claim is narrowed, not erased: it now cannot
|
|
64
|
+
catch *arbitrary* aliases, having gained the local-const case.
|
|
65
|
+
|
|
66
|
+
## 3. Level-of-abstraction fit
|
|
67
|
+
|
|
68
|
+
Same layer as the existing check — line-oriented regex over comment-stripped source, no AST, no type
|
|
69
|
+
information, no new dependency. The alias map is the minimum needed to answer "what value is at this
|
|
70
|
+
`??`?" without climbing to a parser.
|
|
71
|
+
|
|
72
|
+
## 4. Signal vs authority compliance
|
|
73
|
+
|
|
74
|
+
Unchanged. A CI guard, not a runtime authority. It pushes callers toward `resolveDevAgentGate`; the funnel
|
|
75
|
+
allowlist still exempts the funnel itself.
|
|
76
|
+
|
|
77
|
+
## 5. Interactions
|
|
78
|
+
|
|
79
|
+
- `npm run lint` chain — position unchanged; full chain green.
|
|
80
|
+
- Assertions B/C unaffected; their env-fixture tests pass untouched.
|
|
81
|
+
- No source module, route, config key, or state file touched.
|
|
82
|
+
|
|
83
|
+
## 6. External surfaces
|
|
84
|
+
|
|
85
|
+
None. Developer tooling, not an agent capability; the Agent Awareness Standard does not apply.
|
|
86
|
+
|
|
87
|
+
## 7. Rollback cost
|
|
88
|
+
|
|
89
|
+
`git revert` of one script plus the appended tests. No migration, no state, no deployed artifact.
|
|
90
|
+
|
|
91
|
+
## Conclusion
|
|
92
|
+
|
|
93
|
+
Ship. One evasion closed at the scope a peer's audit recommended, four anti-over-block controls added, real
|
|
94
|
+
tree verified clean.
|
|
95
|
+
|
|
96
|
+
## Evidence pointers
|
|
97
|
+
|
|
98
|
+
- `tests/unit/lint-dev-agent-dark-gate.test.ts` — **31/31 green** (24 existing + 7 added).
|
|
99
|
+
- Negative control: tests written BEFORE the fix and run against the shipped lint — **3 of 31 fail** (const
|
|
100
|
+
alias, bracket-access alias, `Boolean(alias)`); the four new controls and all 24 existing tests pass both
|
|
101
|
+
ways, which is what makes them controls.
|
|
102
|
+
- Reproduced by hand first, with a positive control: the direct form exits 1, the aliased form exits 0.
|
|
103
|
+
- Real-tree verdict: `node scripts/lint-dev-agent-dark-gate.js` → exit 0.
|
|
104
|
+
- Full `npm run lint` chain green.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Side-Effects Review - Split-Key Messaging Gate Lint
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `lint-split-key-messaging-gate`
|
|
4
|
+
**Date:** `2026-08-14`
|
|
5
|
+
**Author:** `Instar-codey`
|
|
6
|
+
**Second-pass reviewer:** `Locke`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
This change strengthens `scripts/lint-no-unreachable-messaging-gate.js` so the existing unreachable default-off `messaging.*` LiveConfig lint also recognizes first-argument string literals joined with `+`, for example `liveConfig.get('messaging.' + 'actionClaim.enabled', false)`. The focused unit test file `tests/unit/lint-no-unreachable-messaging-gate.test.ts` now covers the reproduced split-key miss and the required controls. The release fragment is `upgrades/next/lint-split-key-messaging-gate.md`. Build location was a fresh worktree at `.worktrees/codey-lint-split-key-messaging-gate` on `JKHeadley/instar`, version `1.3.1146`, branched from current `origin/main`.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `scripts/lint-no-unreachable-messaging-gate.js` - modify - build-time decision that flags source lines where `LiveConfig.get` uses a `messaging.*` key with literal `false` default.
|
|
15
|
+
- Release note internal-only classification - pass-through - this is a script/test/docs-only change with no shipped `src/` runtime surface.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
|
|
21
|
+
The main legitimate input risk is a dynamic backtick template such as `` liveConfig.get(`messaging.${featureKey}`, false) `` being treated as a constant key. The implementation rejects template expressions during literal parsing, and the unit suite includes that negative control. Second-pass review also found that the first implementation could flag a `.get(...)` example embedded inside another string literal; the scanner now only recognizes `.get` tokens outside ordinary string literal text, and a regression test covers `const example = "liveConfig.get('messaging.' + 'actionClaim.enabled', false)"`. A literal-concat non-messaging key such as `liveConfig.get('monitoring.' + 'burnDetection.enabled', false)` is explicitly covered and remains accepted. A default-true split key remains accepted because the lint is specifically about default-off gates staying unreachable.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. Under-block
|
|
26
|
+
|
|
27
|
+
The lint still misses non-literal construction such as `liveConfig.get('messaging.' + featureKey, false)` or helper-returned keys. That is intentional for this change: widening into dataflow or general expression evaluation would raise false-positive risk and would be a different guard. The known reproduced bypass is literal-plus-literal construction, and that class is now covered.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 3. Level-of-abstraction fit
|
|
32
|
+
|
|
33
|
+
This is the right layer for the fix. The problem is a static source spelling that hides an unreachable configuration key. A build lint can catch the enumerable literal shape cheaply before release. A runtime gate would be later, noisier, and would still allow dead code to ship. The implementation uses a narrow scanner rather than general JavaScript evaluation, which fits the lint's existing design.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 4. Signal vs authority compliance
|
|
38
|
+
|
|
39
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
40
|
+
|
|
41
|
+
- [ ] No - this change produces a signal consumed by an existing smart gate.
|
|
42
|
+
- [ ] No - this change has no block/allow surface.
|
|
43
|
+
- [ ] Yes - but the logic is a smart gate with full conversational context (LLM-backed with recent history or equivalent).
|
|
44
|
+
- [x] Yes, but over a hard enumerable source invariant rather than a competing-signals judgment point.
|
|
45
|
+
|
|
46
|
+
This lint has blocking authority in the build, but it is not deciding from brittle runtime context. It enforces a concrete invariant: a literal `messaging.*` config key with literal `false` default is unreachable through the expected top-level config surface. The new part only folds literal string pieces before applying that same invariant.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
51
|
+
|
|
52
|
+
No new static heuristic at a competing-signals decision point. The domain is enumerable source text: literal string values, `+` separators, and the literal `false` default. There are no live signals such as ownership, urgency, recency, or user intent to arbitrate.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 5. Interactions
|
|
57
|
+
|
|
58
|
+
- **Shadowing:** This replaces the prior single-regex detection path inside the same lint. It does not run before another checker or suppress another result.
|
|
59
|
+
- **Double-fire:** One line produces one hit through `scanText`; the scanner returns line numbers as before.
|
|
60
|
+
- **Races:** No shared runtime state. The script reads the source tree and exits.
|
|
61
|
+
- **Feedback loops:** No feedback loop. The output feeds developer/build action only.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 6. External surfaces
|
|
66
|
+
|
|
67
|
+
No user-facing runtime surface changes. Other agents and users only see the effect when developing Instar: split-literal unreachable messaging gates fail the lint. No external service calls, no persistent state changes, no generated URLs, no timing dependency, and no operator-facing action surface.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
72
|
+
|
|
73
|
+
No operator surface - not applicable.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
78
|
+
|
|
79
|
+
Machine-local by design: this is a source-tree lint that runs in the checkout performing the build. It holds no durable agent state, emits no user-facing notices, and generates no URLs. Multi-machine coherence is handled by committing the script/tests/release notes to git so every machine receives the same lint behavior after update.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 8. Rollback cost
|
|
84
|
+
|
|
85
|
+
Pure code/test/docs change. Rollback is a hot-fix revert of the lint parser and its tests plus the release fragment. No data migration, no agent state repair, and no user-visible runtime regression during rollback.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Conclusion
|
|
90
|
+
|
|
91
|
+
The review narrowed the implementation to literal-only first-argument folding, added a negative test for template expressions, and resolved second-pass's string-example false-positive concern by scanning for `.get` only outside string literal text. The change is clear to ship with the known caveat that dynamic key construction remains out of scope for this lint.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Second-pass review (if required)
|
|
96
|
+
|
|
97
|
+
**Reviewer:** `Locke`
|
|
98
|
+
**Independent read of the artifact:** `concern, resolved`
|
|
99
|
+
|
|
100
|
+
Initial concern: the first implementation scanned for `.get` across preserved string contents, so a non-executed example string such as `const example = "liveConfig.get('messaging.' + 'actionClaim.enabled', false)"` could be reported. Resolution: `lineHasUnreachableOffGate` now locates `.get` only while outside string literal text, and `tests/unit/lint-no-unreachable-messaging-gate.test.ts` includes the example-string negative control. Dynamic concatenation and template expressions remained out of scope as intended.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Evidence pointers
|
|
105
|
+
|
|
106
|
+
- Shipped-lint reproduction before the fix: split key returned `hits: []`, counted as `failureCount: 1`.
|
|
107
|
+
- After fix direct probe: split key returns `hits: [1]`.
|
|
108
|
+
- Focused suite: `npx vitest run tests/unit/lint-no-unreachable-messaging-gate.test.ts` passed `15/15` after the second-pass fix.
|
|
109
|
+
- Real tree: `node scripts/lint-no-unreachable-messaging-gate.js` exited clean with no existing flags.
|
|
110
|
+
- Full lint: `npm run lint` exited clean.
|
|
111
|
+
- Full push suite: `npm run test:push` ran 44,154 tests; 44,121 passed, 27 skipped, 3 todo, and 3 failed. Two failures were live Gemini e2e tests requiring absent `GEMINI_API_KEY`; the third feedback-drain ordering failure reran green in isolation.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Class-Closure Declaration (display-only mirror)
|
|
116
|
+
|
|
117
|
+
No agent-authored-artifact defect - not applicable. This fixes a source-code lint detection gap and adds direct regression tests for the reproduced shape.
|