@tekyzinc/gsd-t 4.9.14 → 4.10.11

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +1 -1
  3. package/bin/gsd-t-competition-judge.cjs +7 -1
  4. package/bin/gsd-t-context-brief-kinds/impact.cjs +91 -4
  5. package/bin/gsd-t-context-brief-kinds/partition.cjs +95 -0
  6. package/bin/gsd-t-context-brief-kinds/plan.cjs +110 -4
  7. package/bin/gsd-t-file-disjointness.cjs +319 -7
  8. package/bin/gsd-t-graph-anti-grep-lint.cjs +515 -0
  9. package/bin/gsd-t-graph-edge-extract.cjs +622 -0
  10. package/bin/gsd-t-graph-freshness.cjs +506 -0
  11. package/bin/gsd-t-graph-index.cjs +540 -0
  12. package/bin/gsd-t-graph-k1-sqlite-stream.cjs +251 -0
  13. package/bin/gsd-t-graph-query-cli.cjs +1182 -0
  14. package/bin/gsd-t-graph-scip-upgrade.cjs +440 -0
  15. package/bin/gsd-t-graph-store-bakeoff.cjs +889 -0
  16. package/bin/gsd-t-graph-synthetic-gen.cjs +304 -0
  17. package/bin/gsd-t-graph-ts-throughput.cjs +587 -0
  18. package/bin/gsd-t-require-store.cjs +89 -0
  19. package/bin/gsd-t-scip-reader.cjs +167 -0
  20. package/bin/gsd-t.js +170 -48
  21. package/commands/gsd-t-debug.md +10 -0
  22. package/commands/gsd-t-design-build.md +10 -0
  23. package/commands/gsd-t-execute.md +15 -0
  24. package/commands/gsd-t-feature.md +15 -7
  25. package/commands/gsd-t-gap-analysis.md +17 -7
  26. package/commands/gsd-t-impact.md +25 -0
  27. package/commands/gsd-t-integrate.md +25 -0
  28. package/commands/gsd-t-partition.md +18 -0
  29. package/commands/gsd-t-plan.md +16 -0
  30. package/commands/gsd-t-populate.md +16 -5
  31. package/commands/gsd-t-prd.md +18 -0
  32. package/commands/gsd-t-project.md +19 -0
  33. package/commands/gsd-t-promote-debt.md +16 -5
  34. package/commands/gsd-t-qa.md +20 -8
  35. package/commands/gsd-t-quick.md +10 -0
  36. package/commands/gsd-t-scan.md +21 -3
  37. package/commands/gsd-t-test-sync.md +10 -0
  38. package/commands/gsd-t-verify.md +25 -0
  39. package/commands/gsd-t-wave.md +8 -0
  40. package/package.json +12 -2
  41. package/templates/workflows/gsd-t-debug.workflow.js +81 -0
  42. package/templates/workflows/gsd-t-integrate.workflow.js +47 -1
  43. package/templates/workflows/gsd-t-phase.workflow.js +84 -0
  44. package/templates/workflows/gsd-t-quick.workflow.js +64 -0
  45. package/templates/workflows/gsd-t-scan.workflow.js +200 -9
  46. package/templates/workflows/gsd-t-verify.workflow.js +50 -1
  47. package/bin/graph-cgc.js +0 -510
  48. package/bin/graph-indexer.js +0 -147
  49. package/bin/graph-overlay.js +0 -195
  50. package/bin/graph-parsers.js +0 -327
  51. package/bin/graph-query.js +0 -453
  52. package/bin/graph-store.js +0 -154
@@ -0,0 +1,515 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * gsd-t-graph-anti-grep-lint.cjs
5
+ *
6
+ * M94-D8-T2 — Anti-grep lint engine (deterministic, structural, manifest-driven)
7
+ *
8
+ * [RULE] anti-grep-lint-structural-not-substring
9
+ * [RULE] anti-grep-lint-reads-manifest
10
+ * [RULE] consumer-structural-grep-removed
11
+ *
12
+ * Reads the wired-file set from the §Consumer Manifest table in
13
+ * .gsd-t/contracts/graph-consumer-wiring-contract.md (NOT a hardcoded list),
14
+ * scans each listed command-file + workflow-file for a
15
+ * `try graph-query → catch/else → grep (structural)` fallback,
16
+ * returns a JSON envelope {ok, violations:[{file, line, evidence}]},
17
+ * exits non-zero on any violation.
18
+ *
19
+ * Node built-ins only (zero-dep invariant).
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ const CONTRACT_PATH = '.gsd-t/contracts/graph-consumer-wiring-contract.md';
26
+
27
+ // Structural verb set — the graph query CLI verbs that answer STRUCTURAL questions.
28
+ // A grep that answers one of these is a structural-grep fallback.
29
+ const STRUCTURAL_VERBS = [
30
+ 'who-imports',
31
+ 'who-calls',
32
+ 'blast-radius',
33
+ 'dependents',
34
+ 'dead-code',
35
+ 'orphan',
36
+ 'cycles',
37
+ 'cluster',
38
+ 'test-impl',
39
+ ];
40
+
41
+ // Files exempted from the lint (the announced-fallback carve-outs).
42
+ // Relative to projectDir, POSIX-normalised.
43
+ const SCAN_EXEMPT_FILES = new Set([
44
+ 'commands/gsd-t-scan.md',
45
+ 'templates/workflows/gsd-t-scan.workflow.js',
46
+ ]);
47
+
48
+ const VERIFY_INTEGRATE_EXEMPT_FILES = new Set([
49
+ 'commands/gsd-t-verify.md',
50
+ 'templates/workflows/gsd-t-verify.workflow.js',
51
+ 'commands/gsd-t-integrate.md',
52
+ 'templates/workflows/gsd-t-integrate.workflow.js',
53
+ ]);
54
+
55
+ // ─── Manifest parser ────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Parse the §Consumer Manifest table from the wiring contract.
59
+ * Returns an array of { commandFile, workflowFile } objects for files to lint.
60
+ * The manifest table starts with:
61
+ * | Command File | Workflow File | Role | Structural Verbs Used | Replaces Structural Grep For |
62
+ * Data rows follow (non-header, non-separator, non-placeholder).
63
+ *
64
+ * @param {string} contractText — full text of the contract file
65
+ * @returns {{ commandFile: string, workflowFile: string }[]}
66
+ */
67
+ function parseManifest(contractText) {
68
+ const rows = [];
69
+ const lines = contractText.split(/\r?\n/);
70
+
71
+ // Find the manifest table section — look for the header row.
72
+ let inTable = false;
73
+ let headerSeen = false;
74
+
75
+ for (const line of lines) {
76
+ const trimmed = line.trim();
77
+
78
+ // Detect the manifest table header (case-insensitive column name check).
79
+ if (
80
+ /^\|\s*Command\s+File\s*\|.*Workflow\s+File\s*\|.*Role\s*\|/i.test(trimmed)
81
+ ) {
82
+ inTable = true;
83
+ headerSeen = true;
84
+ continue;
85
+ }
86
+
87
+ // We're in the table once we've seen the header.
88
+ if (!inTable || !headerSeen) continue;
89
+
90
+ // Stop at a non-table line (empty line or heading).
91
+ if (!trimmed.startsWith('|')) {
92
+ // Stop if it's a blank line or a heading, continue through empty rows.
93
+ if (trimmed === '' || trimmed.startsWith('#')) {
94
+ inTable = false;
95
+ }
96
+ continue;
97
+ }
98
+
99
+ // Skip separator rows (--- rows).
100
+ if (/^\|[-| ]+\|$/.test(trimmed)) continue;
101
+
102
+ // Parse data row: | command-file | workflow-file | role | verbs | description |
103
+ const cols = trimmed
104
+ .split('|')
105
+ .slice(1, -1) // strip leading/trailing pipes
106
+ .map((c) => c.trim());
107
+
108
+ if (cols.length < 2) continue;
109
+
110
+ const commandFile = cols[0];
111
+ const workflowFile = cols[1];
112
+
113
+ // Skip placeholder rows.
114
+ if (!commandFile || commandFile.startsWith('_(') || commandFile === '') continue;
115
+ if (!workflowFile || workflowFile.startsWith('_(') || workflowFile === '') {
116
+ // Command file alone is still worth linting if workflow is absent/placeholder.
117
+ if (commandFile && !commandFile.startsWith('_(')) {
118
+ rows.push({ commandFile, workflowFile: null });
119
+ }
120
+ continue;
121
+ }
122
+
123
+ rows.push({ commandFile, workflowFile });
124
+ }
125
+
126
+ return rows;
127
+ }
128
+
129
+ // ─── Structural grep detector ───────────────────────────────────────────────
130
+
131
+ /**
132
+ * Strip JS/MD line comments and block comments from source.
133
+ * Returns text with comments replaced by whitespace (to preserve line numbers).
134
+ *
135
+ * @param {string} source
136
+ * @returns {string}
137
+ */
138
+ function stripComments(source) {
139
+ // Replace block comments /* … */ with spaces (preserving newlines for line counts).
140
+ let out = source.replace(/\/\*[\s\S]*?\*\//g, (m) =>
141
+ m.replace(/[^\n]/g, ' ')
142
+ );
143
+ // Replace // line comments.
144
+ out = out.replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
145
+ // Replace markdown code-fence block content (triple-backtick fences).
146
+ // We blank these so a code example in docs doesn't trip the lint.
147
+ out = out.replace(/^```[\s\S]*?^```/gm, (m) => m.replace(/[^\n]/g, ' '));
148
+ return out;
149
+ }
150
+
151
+ /**
152
+ * Detect whether a line/region of source contains a structural-grep fallback.
153
+ *
154
+ * A structural-grep fallback is:
155
+ * a grep call (exec/execSync/spawn/spawnSync with 'grep' as the command,
156
+ * or a shell command string starting with 'grep …')
157
+ * appearing in a catch/else/fallback position AFTER a `gsd-t graph <structural-verb>` call.
158
+ *
159
+ * This is STRUCTURAL, not a substring scan:
160
+ * - A comment mentioning grep → NOT flagged (comments stripped first).
161
+ * - A legitimate text-search grep (TODO, config value) → NOT flagged (no structural verb in context).
162
+ * - An actual exec('grep …') after a graph query verb in a catch/else → FLAGGED.
163
+ *
164
+ * Detection approach (conservative — better to report a false positive than miss a real violation):
165
+ * 1. Strip comments from source.
166
+ * 2. Identify "structural query regions" — spans containing a `gsd-t graph <verb>` call.
167
+ * 3. Within or after those regions, detect catch/else blocks that invoke grep.
168
+ * 4. A grep in a catch/else that is contextually NEAR (within 50 lines after) a structural verb call
169
+ * AND the grep pattern looks structural (not a text search) is a violation.
170
+ *
171
+ * @param {string} source — source text of the file
172
+ * @param {string} filePath — for reporting (relative)
173
+ * @returns {{ found: boolean, violations: { file: string, line: number, evidence: string }[] }}
174
+ */
175
+ function detectStructuralGrepFallback(source, filePath) {
176
+ const violations = [];
177
+
178
+ const clean = stripComments(source);
179
+ const lines = clean.split('\n');
180
+ const rawLines = source.split('\n');
181
+
182
+ // Step 1: Find all lines containing a structural graph verb call.
183
+ // Pattern: gsd-t graph <verb> OR graph <verb> OR graphQuery(<verb>) OR queryGraph(<verb>)
184
+ // Also: strings like 'who-imports', 'blast-radius' etc. passed to exec/spawn as graph CLI args.
185
+ const structuralVerbPattern = new RegExp(
186
+ '(?:gsd-t\\s+graph|graph(?:Query|Cli)?|queryGraph)\\s+(?:' +
187
+ STRUCTURAL_VERBS.map((v) => v.replace('-', '[-_]')).join('|') +
188
+ ')|[\'"`](?:' +
189
+ STRUCTURAL_VERBS.map((v) => v.replace('-', '[-_]')).join('|') +
190
+ ')[\'"`]',
191
+ 'i'
192
+ );
193
+
194
+ // Collect line numbers of structural verb calls.
195
+ const verbLineNums = new Set();
196
+ for (let i = 0; i < lines.length; i++) {
197
+ if (structuralVerbPattern.test(lines[i])) {
198
+ verbLineNums.add(i);
199
+ }
200
+ }
201
+
202
+ // If no structural verb found in this file, no structural-grep fallback possible.
203
+ if (verbLineNums.size === 0) return { found: false, violations: [] };
204
+
205
+ // Step 2: Find catch/else blocks that contain a grep subprocess call.
206
+ // Pattern: catch (...) { ... grep ... } or else { ... grep ... }
207
+ // We look for:
208
+ // - exec*('grep …') / spawnSync('grep', …) / exec("grep …")
209
+ // - shell strings like 'grep -r …' passed to exec
210
+ // in lines that are within a catch/else context.
211
+ const grepCallPattern =
212
+ /\bexec(?:Sync|File(?:Sync)?|File)?\s*\(\s*['"`]grep\b|spawn(?:Sync)?\s*\(\s*['"`]grep\b/;
213
+
214
+ // Shell command strings: exec(`grep …`), or shell.exec('grep …'), or spawnSync('sh', ['-c', 'grep …'])
215
+ const shellGrepPattern = /['"`]grep\s+/;
216
+
217
+ // Track if we're inside a catch or else block.
218
+ // Simple brace-counting heuristic (not a full AST — good enough for our conservative approach).
219
+ let catchElseDepth = 0;
220
+ let inCatchElse = false;
221
+ let lastVerbLine = -1;
222
+
223
+ for (let i = 0; i < lines.length; i++) {
224
+ const line = lines[i];
225
+ const rawLine = rawLines[i];
226
+
227
+ // Update last seen structural verb line.
228
+ if (verbLineNums.has(i)) {
229
+ lastVerbLine = i;
230
+ }
231
+
232
+ // Detect entering a catch or else block.
233
+ if (/\b(catch|else)\s*[\({]/.test(line) || /\belse\s*$/.test(line.trim())) {
234
+ // Only flag if we're within 50 lines of a structural verb call.
235
+ if (lastVerbLine >= 0 && i - lastVerbLine <= 50) {
236
+ inCatchElse = true;
237
+ catchElseDepth = 1;
238
+ }
239
+ }
240
+
241
+ // Track brace depth to know when we leave the catch/else.
242
+ if (inCatchElse) {
243
+ const opens = (line.match(/\{/g) || []).length;
244
+ const closes = (line.match(/\}/g) || []).length;
245
+ // First line of catch/else already counted one brace above.
246
+ if (i > lastVerbLine) {
247
+ catchElseDepth += opens - closes;
248
+ if (catchElseDepth <= 0) {
249
+ inCatchElse = false;
250
+ catchElseDepth = 0;
251
+ }
252
+ }
253
+
254
+ // Check for grep in this line (within the catch/else).
255
+ if (
256
+ i > lastVerbLine &&
257
+ (grepCallPattern.test(line) || shellGrepPattern.test(line))
258
+ ) {
259
+ // Classify: is this a structural grep (structural pattern) or text grep?
260
+ // If the grep pattern is about structural questions (imports, calls, deps),
261
+ // it's a violation. If it looks like a text search (TODO, config key), it's NOT.
262
+ const isStructuralGrep = isStructuralGrepPattern(rawLine);
263
+ if (isStructuralGrep) {
264
+ violations.push({
265
+ file: filePath,
266
+ line: i + 1,
267
+ evidence: rawLine.trim().slice(0, 200),
268
+ });
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ // Step 3: Also check for simple inline grep-fallback patterns not in explicit catch/else:
275
+ // Pattern: const result = graphQuery(verb) || execSync('grep …')
276
+ // Pattern: try { ... } catch { grep … } on the same line or consecutive lines.
277
+ // We already handle multi-line catch above; add single-line alternatives.
278
+ const inlineFallbackPattern =
279
+ /(?:gsd-t\s+graph|graphQuery|queryGraph)\s*\(.*\)\s*\|\|\s*.*(?:exec|spawn).*grep/i;
280
+ for (let i = 0; i < lines.length; i++) {
281
+ if (inlineFallbackPattern.test(lines[i])) {
282
+ violations.push({
283
+ file: filePath,
284
+ line: i + 1,
285
+ evidence: rawLines[i].trim().slice(0, 200),
286
+ });
287
+ }
288
+ }
289
+
290
+ return { found: violations.length > 0, violations };
291
+ }
292
+
293
+ /**
294
+ * Determine if a grep call in source code is answering a STRUCTURAL question.
295
+ *
296
+ * Structural grep patterns include:
297
+ * - grep for import/require patterns (who-imports structural question)
298
+ * - grep for function call patterns (who-calls structural question)
299
+ * - grep for dependency/export patterns
300
+ *
301
+ * Non-structural (text-search) grep patterns:
302
+ * - grep for TODO, FIXME, NOTE
303
+ * - grep for config keys / magic strings / string literals
304
+ * - grep for environment variables
305
+ *
306
+ * This is a heuristic — it should have LOW false-positive rate.
307
+ *
308
+ * @param {string} line
309
+ * @returns {boolean}
310
+ */
311
+ function isStructuralGrepPattern(line) {
312
+ // Import/require pattern grepping → structural.
313
+ if (/grep.*(?:import|require|from\s+['"`])/.test(line)) return true;
314
+ // Function-call pattern grepping → structural.
315
+ if (/grep.*\w+\s*\(/.test(line) && !/grep.*(?:TODO|FIXME|NOTE|todo|fixme)/.test(line)) {
316
+ // Only flag if it looks like function-call pattern search, not a general text search.
317
+ // e.g. grep for "foo(" across the codebase is structural.
318
+ if (/grep.*\w+\s*\\\(/.test(line) || /grep.*-[rRE].*\w+\s*\(/.test(line)) return true;
319
+ }
320
+ // Dead-code / orphan / cycle grepping — structural. Hard to pattern-match generically,
321
+ // so we rely on the catch/else positioning in the caller rather than pattern-matching here.
322
+ // Return true if this grep is clearly inside a structural-fallback block (the caller already
323
+ // established we're in a catch/else after a structural verb call).
324
+ // For conservative detection: a grep in a catch/else after a structural verb call IS a violation
325
+ // unless it's clearly a text search.
326
+ if (isTextSearchGrep(line)) return false;
327
+ // Default for a grep in a catch/else after a structural verb: flag it.
328
+ return true;
329
+ }
330
+
331
+ /**
332
+ * Returns true if the grep is clearly a text-search (not structural).
333
+ * Text-search greps look for literal string content, not code structure.
334
+ *
335
+ * Conservative: only returns true when there is STRONG POSITIVE evidence
336
+ * that the grep is searching for text content (TODO, a named constant, a
337
+ * quoted literal string known to be non-structural). When in doubt (e.g. the
338
+ * search pattern is a variable or ambiguous), returns false — the caller
339
+ * already established structural context (catch/else after a structural verb),
340
+ * so the default is to flag, not to pass.
341
+ *
342
+ * @param {string} line
343
+ * @returns {boolean}
344
+ */
345
+ function isTextSearchGrep(line) {
346
+ // TODO / FIXME / NOTE / HACK / BUG → unambiguously text search.
347
+ if (/grep.*(?:TODO|FIXME|NOTE|HACK|XXX|BUG)/i.test(line)) return true;
348
+ // All-caps constants (MAX_RETRIES, MY_CONFIG_KEY, etc.) with no structural keywords → text search.
349
+ if (/grep.*\b[A-Z][A-Z0-9_]{3,}\b/.test(line) && !/grep.*(?:import|require|export)/.test(line)) return true;
350
+ // Quoted LITERAL strings that are clearly non-structural content:
351
+ // A grep for a quoted string that is NOT a structural pattern (import/require/export/call).
352
+ // The quoted string must be a LITERAL (no string concatenation with + variable) and
353
+ // must not look like a code-structure pattern.
354
+ const quotedLiteralMatch = line.match(/grep.*(['"`])([^'"`+$]+)\1/);
355
+ if (quotedLiteralMatch) {
356
+ const pattern = quotedLiteralMatch[2];
357
+ // Skip if the pattern contains structural keywords.
358
+ if (/import|require|from\s|export|function\s/.test(pattern)) return false;
359
+ // Skip if the pattern looks like a file path or structural identifier.
360
+ if (/\.(ts|js|cjs|mjs|tsx|jsx)\b/.test(pattern)) return false;
361
+ // A clearly non-structural literal (contains spaces without structural keywords,
362
+ // or is a plain word that's not a code identifier) → text search.
363
+ if (/\s/.test(pattern) || /^[A-Z][A-Z0-9_]+$/.test(pattern)) return true;
364
+ }
365
+ return false;
366
+ }
367
+
368
+ // ─── Main lint runner ────────────────────────────────────────────────────────
369
+
370
+ /**
371
+ * Read the consumer manifest from the wiring contract and return the set of
372
+ * wired files to lint. Returns { files: string[], error: string|null }.
373
+ *
374
+ * @param {string} projectDir
375
+ * @returns {{ files: string[], manifestRows: number, error: string|null }}
376
+ */
377
+ function readManifest(projectDir) {
378
+ const contractPath = path.join(projectDir, CONTRACT_PATH);
379
+ let contractText;
380
+ try {
381
+ contractText = fs.readFileSync(contractPath, 'utf8');
382
+ } catch (err) {
383
+ return {
384
+ files: [],
385
+ manifestRows: 0,
386
+ error: `Cannot read wiring contract at ${CONTRACT_PATH}: ${err.message}`,
387
+ };
388
+ }
389
+
390
+ const rows = parseManifest(contractText);
391
+ const files = new Set();
392
+ for (const row of rows) {
393
+ if (row.commandFile) files.add(row.commandFile);
394
+ if (row.workflowFile) files.add(row.workflowFile);
395
+ }
396
+
397
+ return { files: [...files], manifestRows: rows.length, error: null };
398
+ }
399
+
400
+ /**
401
+ * Run the anti-grep lint over the wired-file set.
402
+ *
403
+ * @param {object} opts
404
+ * @param {string} opts.projectDir — absolute path to the project root
405
+ * @param {string[]} [opts.files] — override the file list (for testing)
406
+ * @returns {{ ok: boolean, violations: { file: string, line: number, evidence: string }[], manifestRows: number, scannedFiles: string[], skippedFiles: string[], error: string|null }}
407
+ */
408
+ function runLint(opts = {}) {
409
+ const projectDir = opts.projectDir || process.cwd();
410
+
411
+ let filesToScan;
412
+ let manifestRows = 0;
413
+
414
+ if (opts.files) {
415
+ // Override mode (for tests passing fixture files).
416
+ filesToScan = opts.files;
417
+ manifestRows = opts.files.length;
418
+ } else {
419
+ const manifest = readManifest(projectDir);
420
+ if (manifest.error) {
421
+ return {
422
+ ok: false,
423
+ violations: [],
424
+ manifestRows: 0,
425
+ scannedFiles: [],
426
+ skippedFiles: [],
427
+ error: manifest.error,
428
+ };
429
+ }
430
+ filesToScan = manifest.files;
431
+ manifestRows = manifest.manifestRows;
432
+ }
433
+
434
+ const allViolations = [];
435
+ const scannedFiles = [];
436
+ const skippedFiles = [];
437
+
438
+ for (const relPath of filesToScan) {
439
+ const normalised = relPath.replace(/\\/g, '/');
440
+
441
+ // Check exemptions.
442
+ if (SCAN_EXEMPT_FILES.has(normalised) || VERIFY_INTEGRATE_EXEMPT_FILES.has(normalised)) {
443
+ skippedFiles.push(relPath);
444
+ continue;
445
+ }
446
+
447
+ const absPath = path.isAbsolute(relPath) ? relPath : path.join(projectDir, relPath);
448
+ let source;
449
+ try {
450
+ source = fs.readFileSync(absPath, 'utf8');
451
+ } catch (err) {
452
+ // File not found — not a violation (file may not exist yet during d10/d11 wiring).
453
+ skippedFiles.push(relPath);
454
+ continue;
455
+ }
456
+
457
+ scannedFiles.push(relPath);
458
+ const { violations } = detectStructuralGrepFallback(source, relPath);
459
+ allViolations.push(...violations);
460
+ }
461
+
462
+ return {
463
+ ok: allViolations.length === 0,
464
+ violations: allViolations,
465
+ manifestRows,
466
+ scannedFiles,
467
+ skippedFiles,
468
+ error: null,
469
+ };
470
+ }
471
+
472
+ // ─── CLI entry point ─────────────────────────────────────────────────────────
473
+
474
+ if (require.main === module) {
475
+ const projectDir = process.argv[2] || process.cwd();
476
+ const result = runLint({ projectDir });
477
+
478
+ if (result.error) {
479
+ // Contract not found or unreadable — fail loudly (manifest-required).
480
+ process.stderr.write(`[anti-grep-lint] ERROR: ${result.error}\n`);
481
+ process.stdout.write(JSON.stringify({ ok: false, violations: [], error: result.error }, null, 2) + '\n');
482
+ process.exit(1);
483
+ }
484
+
485
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
486
+
487
+ if (!result.ok) {
488
+ process.stderr.write(
489
+ `[anti-grep-lint] FAIL: ${result.violations.length} structural-grep-fallback violation(s) found.\n`
490
+ );
491
+ for (const v of result.violations) {
492
+ process.stderr.write(` ${v.file}:${v.line} — ${v.evidence}\n`);
493
+ }
494
+ process.exit(1);
495
+ } else {
496
+ process.stderr.write(
497
+ `[anti-grep-lint] PASS: ${result.scannedFiles.length} wired files scanned, 0 violations.\n`
498
+ );
499
+ process.exit(0);
500
+ }
501
+ }
502
+
503
+ // ─── Exports ─────────────────────────────────────────────────────────────────
504
+
505
+ module.exports = {
506
+ runLint,
507
+ parseManifest,
508
+ detectStructuralGrepFallback,
509
+ isStructuralGrepPattern,
510
+ isTextSearchGrep,
511
+ stripComments,
512
+ STRUCTURAL_VERBS,
513
+ SCAN_EXEMPT_FILES,
514
+ VERIFY_INTEGRATE_EXEMPT_FILES,
515
+ };