instar 1.3.478 → 1.3.480
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/.claude/skills/autonomous/SKILL.md +10 -0
- package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +418 -23
- package/.claude/skills/autonomous/scripts/setup-autonomous.sh +29 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +10 -7
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +39 -8
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +121 -3
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts +31 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +121 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.js +1 -1
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/lib/dark-gate-attribution.js +148 -0
- package/scripts/lint-dev-agent-dark-gate.js +159 -14
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +1 -0
- package/upgrades/1.3.479.md +53 -0
- package/upgrades/1.3.480.md +35 -0
- package/upgrades/side-effects/autonomous-completion-real-checks.md +88 -0
- package/upgrades/side-effects/dev-agent-dark-gate-enforcement.md +95 -0
package/package.json
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dark-gate-attribution.js — the path-attribution + registry-extraction core
|
|
3
|
+
* shared by scripts/lint-dev-agent-dark-gate.js (assertion C) and its golden-path
|
|
4
|
+
* drift-canary test. Extracted so there is ONE attributor implementation: the
|
|
5
|
+
* lint and the test agree by construction (the test asserts THIS resolver
|
|
6
|
+
* reproduces a hand-authored map — never the resolver's own output).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
|
|
11
|
+
/** Strip a `//` line comment; return null if the line is a pure comment line. */
|
|
12
|
+
export function codeOnly(line) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
|
|
15
|
+
return null; // comment line — no code
|
|
16
|
+
}
|
|
17
|
+
const idx = line.indexOf('//');
|
|
18
|
+
return idx >= 0 ? line.slice(0, idx) : line;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Does a code-stripped line contain a `{` or `}` inside a string or template
|
|
23
|
+
* literal? codeOnly() does not strip string contents, so such a brace would
|
|
24
|
+
* desync the depth counter.
|
|
25
|
+
*/
|
|
26
|
+
export function braceInString(code) {
|
|
27
|
+
let inStr = false;
|
|
28
|
+
let quote = '';
|
|
29
|
+
for (let i = 0; i < code.length; i++) {
|
|
30
|
+
const ch = code[i];
|
|
31
|
+
if (inStr) {
|
|
32
|
+
if (ch === '\\') { i++; continue; } // skip escaped char
|
|
33
|
+
if (ch === quote) { inStr = false; quote = ''; }
|
|
34
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
35
|
+
inStr = true;
|
|
36
|
+
quote = ch;
|
|
37
|
+
}
|
|
38
|
+
if (inStr && (ch === '{' || ch === '}')) return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Attribute every `enabled: false` line in a ConfigDefaults.ts to a dotted config
|
|
45
|
+
* path by brace-tracking from the top of the SHARED_DEFAULTS object literal.
|
|
46
|
+
* Returns { paths: [{ line, dottedPath }], error }.
|
|
47
|
+
*/
|
|
48
|
+
export function attributeEnabledFalsePaths(absPath) {
|
|
49
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
50
|
+
const startIdx = lines.findIndex((l) => /const\s+SHARED_DEFAULTS\b[^=]*=\s*{/.test(l));
|
|
51
|
+
if (startIdx < 0) {
|
|
52
|
+
return { paths: [], error: 'could not locate `const SHARED_DEFAULTS = {` in ConfigDefaults.ts' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Stack of { key } — key is null for an anonymous `{` (array element object
|
|
56
|
+
// etc.). depth starts at 0; the SHARED_DEFAULTS `{` takes it to 1 (the root
|
|
57
|
+
// object body lives at depth 1).
|
|
58
|
+
const stack = [];
|
|
59
|
+
let depth = 0;
|
|
60
|
+
const results = [];
|
|
61
|
+
const ENABLED_FALSE = /(["']?)enabled\1\s*:\s*false\b/;
|
|
62
|
+
const KEY_LINE = /^\s*(["']?)([A-Za-z_$][\w$-]*)\1\s*:/;
|
|
63
|
+
|
|
64
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
65
|
+
const code = codeOnly(lines[i]);
|
|
66
|
+
if (code === null) continue; // pure comment line
|
|
67
|
+
|
|
68
|
+
if (braceInString(code)) {
|
|
69
|
+
return {
|
|
70
|
+
paths: [],
|
|
71
|
+
error: `brace-in-string in defaults block can desync path attribution (line ${i + 1}) — split the value or extend the parser`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isEnabledFalse = ENABLED_FALSE.test(code);
|
|
76
|
+
const keyMatch = code.match(KEY_LINE);
|
|
77
|
+
const keyCandidate = keyMatch ? keyMatch[2] : null;
|
|
78
|
+
|
|
79
|
+
if (isEnabledFalse) {
|
|
80
|
+
const dottedPath = stack.map((s) => s.key).filter(Boolean).join('.') + '.enabled';
|
|
81
|
+
results.push({ line: i + 1, dottedPath });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let firstOpenOnLine = true;
|
|
85
|
+
for (const ch of code) {
|
|
86
|
+
if (ch === '{') {
|
|
87
|
+
if (firstOpenOnLine && keyCandidate && depth >= 1) {
|
|
88
|
+
stack.push({ key: keyCandidate });
|
|
89
|
+
} else {
|
|
90
|
+
stack.push({ key: null });
|
|
91
|
+
}
|
|
92
|
+
firstOpenOnLine = false;
|
|
93
|
+
depth++;
|
|
94
|
+
} else if (ch === '}') {
|
|
95
|
+
depth--;
|
|
96
|
+
stack.pop();
|
|
97
|
+
if (depth <= 0) {
|
|
98
|
+
return { paths: results, error: null };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { paths: results, error: null };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const VALID_CATEGORIES = new Set([
|
|
107
|
+
'destructive',
|
|
108
|
+
'optional-integration',
|
|
109
|
+
'cost-bearing',
|
|
110
|
+
'structural-stub',
|
|
111
|
+
'deliberate-fleet-default',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
export { VALID_CATEGORIES };
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract configPath literals + exclusion entries from a devGatedFeatures.ts.
|
|
118
|
+
* The file is hand-authored with a stable shape; the lint runs as plain JS
|
|
119
|
+
* (importing the TS registry directly won't work), so we regex the source.
|
|
120
|
+
*/
|
|
121
|
+
export function extractRegistry(absPath) {
|
|
122
|
+
const src = fs.readFileSync(absPath, 'utf-8');
|
|
123
|
+
const configPathOf = (arrName) => {
|
|
124
|
+
const m = src.match(new RegExp(`export const ${arrName}[^=]*=\\s*\\[([\\s\\S]*?)\\n\\];`));
|
|
125
|
+
if (!m) return [];
|
|
126
|
+
const body = m[1];
|
|
127
|
+
const paths = [];
|
|
128
|
+
const re = /configPath:\s*(['"])([^'"]+)\1/g;
|
|
129
|
+
let mm;
|
|
130
|
+
while ((mm = re.exec(body)) !== null) paths.push(mm[2]);
|
|
131
|
+
return paths;
|
|
132
|
+
};
|
|
133
|
+
const exclBody = (() => {
|
|
134
|
+
const m = src.match(/export const DARK_GATE_EXCLUSIONS[^=]*=\s*\[([\s\S]*?)\n\];/);
|
|
135
|
+
return m ? m[1] : '';
|
|
136
|
+
})();
|
|
137
|
+
const exclusionEntries = [];
|
|
138
|
+
const entryRe = /\{\s*configPath:\s*(['"])([^'"]+)\1\s*,\s*category:\s*(['"])([^'"]+)\3\s*,\s*reason:\s*(['"])((?:[^'"\\]|\\.)*)\5\s*,?\s*\}/g;
|
|
139
|
+
let em;
|
|
140
|
+
while ((em = entryRe.exec(exclBody)) !== null) {
|
|
141
|
+
exclusionEntries.push({ configPath: em[2], category: em[4], reason: em[6] });
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
gatedPaths: configPathOf('DEV_GATED_FEATURES'),
|
|
145
|
+
exclusionPaths: configPathOf('DARK_GATE_EXCLUSIONS'),
|
|
146
|
+
exclusionEntries,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (dev agents included), silently contradicting the standard. Caught only by
|
|
9
9
|
* operator review — there was no structural guard. This is that guard.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Three assertions over `src/`:
|
|
12
12
|
*
|
|
13
13
|
* A. FUNNEL — every dev-agent gate resolution must go through
|
|
14
14
|
* `resolveDevAgentGate` (src/core/devAgentGate.ts). A hand-rolled
|
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
* is NOT flagged — that is an allowed deliberate fleet-flip. Comment prose is
|
|
26
26
|
* skipped so the convention's own documentation never trips the check.
|
|
27
27
|
*
|
|
28
|
+
* C. NO UNCLASSIFIED DARK DEFAULT (DEV-AGENT-DARK-GATE-ENFORCEMENT B2) — every
|
|
29
|
+
* literal `enabled: false` in src/config/ConfigDefaults.ts must be a DECLARED
|
|
30
|
+
* choice: its brace-attributed config path is EITHER in DEV_GATED_FEATURES
|
|
31
|
+
* (and then must NOT also hardcode false — a dev-gated feature OMITS enabled)
|
|
32
|
+
* OR in DARK_GATE_EXCLUSIONS with a valid category + a ≥12-char reason. This
|
|
33
|
+
* closes the hole assertion B left: a marker-less hardcoded `enabled: false`
|
|
34
|
+
* (exactly the cartographer specs) shipped dark for everyone, invisibly.
|
|
35
|
+
* LIMITATION: C matches the literal `enabled: false` spelling only.
|
|
36
|
+
*
|
|
28
37
|
* Exit codes:
|
|
29
38
|
* 0 — no violations.
|
|
30
39
|
* 1 — at least one violation.
|
|
@@ -39,6 +48,12 @@ import fs from 'node:fs';
|
|
|
39
48
|
import path from 'node:path';
|
|
40
49
|
import { execSync } from 'node:child_process';
|
|
41
50
|
import { fileURLToPath } from 'node:url';
|
|
51
|
+
import {
|
|
52
|
+
codeOnly,
|
|
53
|
+
attributeEnabledFalsePaths,
|
|
54
|
+
extractRegistry,
|
|
55
|
+
VALID_CATEGORIES,
|
|
56
|
+
} from './lib/dark-gate-attribution.js';
|
|
42
57
|
|
|
43
58
|
const __filename = fileURLToPath(import.meta.url);
|
|
44
59
|
const __dirname = path.dirname(__filename);
|
|
@@ -69,16 +84,6 @@ const HARDCODED_ENABLED = /(["']?)enabled\1\s*:\s*false\b/;
|
|
|
69
84
|
const BLOCK_OPEN_SEARCH = 15; // max non-code lines between marker and the block's `{`
|
|
70
85
|
const BLOCK_MAX_LINES = 120; // safety bound on block-body scan
|
|
71
86
|
|
|
72
|
-
/** Strip a `//` line comment; return null if the line is a pure comment line. */
|
|
73
|
-
function codeOnly(line) {
|
|
74
|
-
const trimmed = line.trim();
|
|
75
|
-
if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
|
|
76
|
-
return null; // comment line — no code
|
|
77
|
-
}
|
|
78
|
-
const idx = line.indexOf('//');
|
|
79
|
-
return idx >= 0 ? line.slice(0, idx) : line;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
87
|
/** Is this line (trimmed) a comment line? */
|
|
83
88
|
function isCommentLine(line) {
|
|
84
89
|
const t = line.trim();
|
|
@@ -118,6 +123,47 @@ function resolveTargets() {
|
|
|
118
123
|
|
|
119
124
|
const violations = [];
|
|
120
125
|
|
|
126
|
+
// Paths used by assertions B (skip-classified) and C (classification check).
|
|
127
|
+
// Tests inject fixtures via env overrides (the C-assertion reads the registry +
|
|
128
|
+
// ConfigDefaults directly, independent of the file args, so without an override a
|
|
129
|
+
// test could not exercise C's failure modes). Absolute paths only; default to the
|
|
130
|
+
// real repo files.
|
|
131
|
+
const CONFIG_DEFAULTS_REL = 'src/config/ConfigDefaults.ts';
|
|
132
|
+
const DEV_GATED_FEATURES_REL = 'src/core/devGatedFeatures.ts';
|
|
133
|
+
const CONFIG_DEFAULTS_ABS = process.env.INSTAR_DARKGATE_CONFIG_DEFAULTS
|
|
134
|
+
? path.resolve(process.env.INSTAR_DARKGATE_CONFIG_DEFAULTS)
|
|
135
|
+
: path.join(ROOT, CONFIG_DEFAULTS_REL);
|
|
136
|
+
const REGISTRY_ABS = process.env.INSTAR_DARKGATE_REGISTRY
|
|
137
|
+
? path.resolve(process.env.INSTAR_DARKGATE_REGISTRY)
|
|
138
|
+
: path.join(ROOT, DEV_GATED_FEATURES_REL);
|
|
139
|
+
|
|
140
|
+
// Precompute the registry + path attribution so assertion B can SKIP a hardcoded
|
|
141
|
+
// `enabled: false` whose attributed path is a DELIBERATE classification (a nested
|
|
142
|
+
// DARK_GATE_EXCLUSIONS entry under a gate-marker comment, e.g. the cost-bearing
|
|
143
|
+
// cartographer sweep). Without this, B false-flags a declared dark default that a
|
|
144
|
+
// parent block's gate-marker comment happens to enclose. The full classification
|
|
145
|
+
// check is assertion C below; B only needs the exclusion line-set to stay quiet on
|
|
146
|
+
// already-classified lines.
|
|
147
|
+
const _configDefaultsAbs = CONFIG_DEFAULTS_ABS;
|
|
148
|
+
const _registryAbs = REGISTRY_ABS;
|
|
149
|
+
let _exclusionClassifiedLines = new Set(); // 1-based ConfigDefaults.ts line numbers
|
|
150
|
+
if (fs.existsSync(_configDefaultsAbs) && fs.existsSync(_registryAbs)) {
|
|
151
|
+
try {
|
|
152
|
+
const { exclusionPaths } = extractRegistry(_registryAbs);
|
|
153
|
+
const exclSet = new Set(exclusionPaths);
|
|
154
|
+
const { paths: attributed, error } = attributeEnabledFalsePaths(_configDefaultsAbs);
|
|
155
|
+
if (!error) {
|
|
156
|
+
for (const { line, dottedPath } of attributed) {
|
|
157
|
+
if (exclSet.has(dottedPath)) _exclusionClassifiedLines.add(line);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// If precompute fails, B falls back to its original behavior (flag all);
|
|
162
|
+
// assertion C reports the real desync/error loudly.
|
|
163
|
+
_exclusionClassifiedLines = new Set();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
121
167
|
for (const file of resolveTargets()) {
|
|
122
168
|
if (!fs.existsSync(file)) continue;
|
|
123
169
|
const rel = path.relative(ROOT, file);
|
|
@@ -163,7 +209,15 @@ for (const file of resolveTargets()) {
|
|
|
163
209
|
for (let j = openIdx; j <= Math.min(openIdx + BLOCK_MAX_LINES, lines.length - 1); j++) {
|
|
164
210
|
const codeJ = codeOnly(lines[j]);
|
|
165
211
|
if (codeJ === null) continue;
|
|
166
|
-
|
|
212
|
+
// Skip a line that is a DELIBERATE DARK_GATE_EXCLUSIONS classification
|
|
213
|
+
// (a nested dark default that a parent block's gate-marker comment merely
|
|
214
|
+
// encloses — e.g. the cost-bearing cartographer sweep). Assertion C still
|
|
215
|
+
// requires it to be classified; B should not double-flag it.
|
|
216
|
+
if (
|
|
217
|
+
HARDCODED_ENABLED.test(codeJ) &&
|
|
218
|
+
!reportedB.has(j) &&
|
|
219
|
+
!(rel === CONFIG_DEFAULTS_REL && _exclusionClassifiedLines.has(j + 1))
|
|
220
|
+
) {
|
|
167
221
|
reportedB.add(j);
|
|
168
222
|
violations.push({
|
|
169
223
|
file: rel, line: j + 1, kind: 'B: hardcoded enabled under gate marker',
|
|
@@ -181,16 +235,107 @@ for (const file of resolveTargets()) {
|
|
|
181
235
|
}
|
|
182
236
|
}
|
|
183
237
|
|
|
238
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
239
|
+
// Assertion C — NO UNCLASSIFIED DARK DEFAULT (DEV-AGENT-DARK-GATE-ENFORCEMENT B2)
|
|
240
|
+
//
|
|
241
|
+
// Every literal `enabled: false` in src/config/ConfigDefaults.ts must be a
|
|
242
|
+
// DECLARED choice: its attributed config path is EITHER registered in
|
|
243
|
+
// DEV_GATED_FEATURES (→ but then it must NOT also hardcode `enabled: false`; a
|
|
244
|
+
// dev-gated feature OMITS enabled) OR classified in DARK_GATE_EXCLUSIONS with a
|
|
245
|
+
// category + reason. Neither → violation. This closes the cartographer hole:
|
|
246
|
+
// assertion B only fires under a comment marker; a marker-less hardcoded
|
|
247
|
+
// `enabled: false` (exactly the cartographer specs) was invisible.
|
|
248
|
+
//
|
|
249
|
+
// LIMITATION (P2 Signal-vs-Authority — do NOT claim full closure): assertion C
|
|
250
|
+
// matches the LITERAL `enabled: false` spelling ONLY. A non-literal default
|
|
251
|
+
// (`enabled: someFlag ?? false`) evades it — the same miss named in the prior
|
|
252
|
+
// conformance spec's Layer-2 row. C closes the literal-false hole (cartographer +
|
|
253
|
+
// #1001), not the non-literal-expression hole.
|
|
254
|
+
//
|
|
255
|
+
// Path attribution reuses codeOnly() for depth (shared with the golden-path test
|
|
256
|
+
// via scripts/lib/dark-gate-attribution.js — ONE attributor implementation).
|
|
257
|
+
// codeOnly() strips `//` comments but does NOT skip braces inside string/template
|
|
258
|
+
// literals — so a loud-fail guard in attributeEnabledFalsePaths errors if any line
|
|
259
|
+
// in the defaults-block region carries a `{`/`}` inside a string, rather than
|
|
260
|
+
// silently desyncing.
|
|
261
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
262
|
+
|
|
263
|
+
// Run assertion C only on a full-tree / explicit run that includes ConfigDefaults
|
|
264
|
+
// (the path attribution needs the whole file; a --staged run that doesn't touch
|
|
265
|
+
// it is a no-op for C, which is fine — CI runs the full tree).
|
|
266
|
+
(() => {
|
|
267
|
+
const configDefaultsAbs = CONFIG_DEFAULTS_ABS;
|
|
268
|
+
const registryAbs = REGISTRY_ABS;
|
|
269
|
+
if (!fs.existsSync(configDefaultsAbs) || !fs.existsSync(registryAbs)) return;
|
|
270
|
+
|
|
271
|
+
const { gatedPaths, exclusionPaths, exclusionEntries } = extractRegistry(registryAbs);
|
|
272
|
+
const gatedSet = new Set(gatedPaths);
|
|
273
|
+
const exclusionSet = new Set(exclusionPaths);
|
|
274
|
+
|
|
275
|
+
// Validate exclusion-entry quality (closed enum + reason length).
|
|
276
|
+
for (const e of exclusionEntries) {
|
|
277
|
+
if (!VALID_CATEGORIES.has(e.category)) {
|
|
278
|
+
violations.push({
|
|
279
|
+
file: DEV_GATED_FEATURES_REL, line: 0, kind: 'C: invalid exclusion category',
|
|
280
|
+
text: `${e.configPath} → category '${e.category}'`,
|
|
281
|
+
fix: `category must be one of: ${[...VALID_CATEGORIES].join(', ')}`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const reasonLen = (e.reason || '').replace(/\s/g, '').length;
|
|
285
|
+
if (reasonLen < 12) {
|
|
286
|
+
violations.push({
|
|
287
|
+
file: DEV_GATED_FEATURES_REL, line: 0, kind: 'C: exclusion reason too short',
|
|
288
|
+
text: `${e.configPath} → reason '${e.reason}' (${reasonLen} non-ws chars)`,
|
|
289
|
+
fix: 'a DARK_GATE_EXCLUSIONS reason must be ≥12 non-whitespace chars (defeats placeholder reasons)',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const { paths: attributed, error } = attributeEnabledFalsePaths(configDefaultsAbs);
|
|
295
|
+
if (error) {
|
|
296
|
+
violations.push({
|
|
297
|
+
file: CONFIG_DEFAULTS_REL, line: 0, kind: 'C: path-attribution error',
|
|
298
|
+
text: error,
|
|
299
|
+
fix: 'resolve the desync condition before the lint can attribute dark defaults',
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const { line, dottedPath } of attributed) {
|
|
305
|
+
const inGated = gatedSet.has(dottedPath);
|
|
306
|
+
const inExcluded = exclusionSet.has(dottedPath);
|
|
307
|
+
if (inGated) {
|
|
308
|
+
// Registered as dev-gated but still hardcodes `enabled: false` — the #1001
|
|
309
|
+
// shape. A dev-gated feature OMITS enabled.
|
|
310
|
+
violations.push({
|
|
311
|
+
file: CONFIG_DEFAULTS_REL, line, kind: 'C: registered but hardcodes false',
|
|
312
|
+
text: `${dottedPath} is in DEV_GATED_FEATURES but still hardcodes \`enabled: false\``,
|
|
313
|
+
fix: 'OMIT `enabled` from the default so the gate decides (resolved as enabled ?? !!developmentAgent)',
|
|
314
|
+
});
|
|
315
|
+
} else if (!inExcluded) {
|
|
316
|
+
violations.push({
|
|
317
|
+
file: CONFIG_DEFAULTS_REL, line, kind: 'C: unclassified dark default',
|
|
318
|
+
text: `${dottedPath} has \`enabled: false\` but is in NEITHER DEV_GATED_FEATURES NOR DARK_GATE_EXCLUSIONS`,
|
|
319
|
+
fix: 'dev-gate it (omit `enabled` + register in DEV_GATED_FEATURES) OR add it to DARK_GATE_EXCLUSIONS with a category+reason',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
})();
|
|
324
|
+
|
|
184
325
|
if (violations.length === 0) {
|
|
185
326
|
console.log('lint-dev-agent-dark-gate: clean');
|
|
186
327
|
process.exit(0);
|
|
187
328
|
}
|
|
188
329
|
|
|
189
330
|
console.error('\n❌ lint-dev-agent-dark-gate found violations of the developmentAgent dark-feature gate standard:\n');
|
|
331
|
+
console.error('NOTE: assertion C matches the literal `enabled: false` spelling only — a non-literal');
|
|
332
|
+
console.error('default (`enabled: someFlag ?? false`) evades it. C closes the literal-false hole');
|
|
333
|
+
console.error('(cartographer + #1001), not the non-literal-expression hole.\n');
|
|
190
334
|
for (const v of violations) {
|
|
191
|
-
|
|
335
|
+
const loc = v.line ? `${v.file}:${v.line}` : v.file;
|
|
336
|
+
console.error(` ${loc} [${v.kind}]`);
|
|
192
337
|
console.error(` ${v.text}`);
|
|
193
338
|
console.error(` fix: ${v.fix}\n`);
|
|
194
339
|
}
|
|
195
|
-
console.error('Standard: a dev-gated feature OMITS `enabled` and resolves it through resolveDevAgentGate so it runs LIVE on dev agents and DARK on the fleet. Spec: docs/specs/DEV-AGENT-DARK-GATE-
|
|
340
|
+
console.error('Standard: a dev-gated feature OMITS `enabled` and resolves it through resolveDevAgentGate so it runs LIVE on dev agents and DARK on the fleet; every other `enabled: false` default must be classified in DARK_GATE_EXCLUSIONS. Spec: docs/specs/DEV-AGENT-DARK-GATE-ENFORCEMENT-SPEC.md\n');
|
|
196
341
|
process.exit(1);
|