@nerviq/cli 1.8.9 → 1.10.0

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.
@@ -0,0 +1,34 @@
1
+ {
2
+ "audit.title": "nerviq audit",
3
+ "audit.quickScan": "nerviq quick scan",
4
+ "audit.codexTitle": "nerviq codex audit",
5
+ "audit.codexQuickScan": "nerviq codex quick scan",
6
+ "audit.scanning": "Scanning: {dir}",
7
+ "audit.platform": "Platform: {platform} ({version})",
8
+ "audit.domainPacks": "Domain packs: {packs}",
9
+ "audit.scope": "Scope: {message}",
10
+ "audit.score": "Live audit score: {score} ({passed}/{total} checks passing)",
11
+ "audit.found": "Found: {files}",
12
+ "audit.excellent": "Excellent setup — production-ready governance",
13
+ "audit.strong": "Strong setup — {count} critical items to address",
14
+ "audit.good": "Good foundation — {count} items need attention",
15
+ "audit.basic": "Basic setup — significant gaps in {category}",
16
+ "audit.early": "Early stage — run `nerviq setup` to bootstrap your config",
17
+ "audit.solid": "Your {platform} setup looks solid.",
18
+ "audit.next": "Next: {command}",
19
+ "audit.quickWins": "Quick wins:",
20
+ "audit.topActions": "Top actions:",
21
+ "audit.platformCaveats": "Platform caveats:",
22
+ "audit.largeFile": "Large file: {file} ({size}KB)",
23
+ "audit.categoryScores": "Category scores:",
24
+ "audit.passed": "Passed",
25
+ "audit.failed": "Failed",
26
+ "audit.skipped": "Skipped",
27
+ "audit.details": "Details:",
28
+ "cli.help.description": "The intelligent governance layer for AI coding agents",
29
+ "cli.help.usage": "Usage: nerviq <command> [options]",
30
+ "cli.help.commands": "Commands:",
31
+ "cli.help.options": "Options:",
32
+ "cli.error.unknownCommand": "Unknown command: {command}",
33
+ "cli.error.requiresValue": "{flag} requires a value"
34
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "audit.title": "nerviq auditoría",
3
+ "audit.quickScan": "nerviq escaneo rápido",
4
+ "audit.codexTitle": "nerviq codex auditoría",
5
+ "audit.codexQuickScan": "nerviq codex escaneo rápido",
6
+ "audit.scanning": "Escaneando: {dir}",
7
+ "audit.platform": "Plataforma: {platform} ({version})",
8
+ "audit.domainPacks": "Paquetes de dominio: {packs}",
9
+ "audit.scope": "Alcance: {message}",
10
+ "audit.score": "Puntuación de auditoría en vivo: {score} ({passed}/{total} verificaciones aprobadas)",
11
+ "audit.found": "Encontrado: {files}",
12
+ "audit.excellent": "Configuración excelente — gobernanza lista para producción",
13
+ "audit.strong": "Configuración sólida — {count} elementos críticos por resolver",
14
+ "audit.good": "Buena base — {count} elementos necesitan atención",
15
+ "audit.basic": "Configuración básica — brechas significativas en {category}",
16
+ "audit.early": "Etapa inicial — ejecuta `nerviq setup` para iniciar tu configuración",
17
+ "audit.solid": "Tu configuración de {platform} se ve sólida.",
18
+ "audit.next": "Siguiente: {command}",
19
+ "audit.quickWins": "Mejoras rápidas:",
20
+ "audit.topActions": "Acciones principales:",
21
+ "audit.platformCaveats": "Advertencias de plataforma:",
22
+ "audit.largeFile": "Archivo grande: {file} ({size}KB)",
23
+ "audit.categoryScores": "Puntuaciones por categoría:",
24
+ "audit.passed": "Aprobado",
25
+ "audit.failed": "Fallido",
26
+ "audit.skipped": "Omitido",
27
+ "audit.details": "Detalles:",
28
+ "cli.help.description": "La capa de gobernanza inteligente para agentes de codificación IA",
29
+ "cli.help.usage": "Uso: nerviq <comando> [opciones]",
30
+ "cli.help.commands": "Comandos:",
31
+ "cli.help.options": "Opciones:",
32
+ "cli.error.unknownCommand": "Comando desconocido: {command}",
33
+ "cli.error.requiresValue": "{flag} requiere un valor"
34
+ }
package/src/mcp-server.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Nerviq MCP Server
4
4
  *
@@ -1,158 +1,158 @@
1
- /**
2
- * OpenCode Freshness Operationalization
3
- *
4
- * Release gates, recurring probes, propagation checklists,
5
- * and staleness blocking for OpenCode surfaces.
6
- */
7
-
8
- const { version } = require('../../package.json');
9
-
10
- const P0_SOURCES = [
11
- {
12
- key: 'opencode-docs',
13
- label: 'OpenCode Official Docs',
14
- url: 'https://opencode.ai/docs',
15
- stalenessThresholdDays: 30,
16
- verifiedAt: null,
17
- },
18
- {
19
- key: 'opencode-config-reference',
20
- label: 'OpenCode Config Reference',
21
- url: 'https://opencode.ai/config',
22
- stalenessThresholdDays: 30,
23
- verifiedAt: null,
24
- },
25
- {
26
- key: 'opencode-github-releases',
27
- label: 'OpenCode GitHub Releases',
28
- url: 'https://github.com/sst/opencode/releases',
29
- stalenessThresholdDays: 14,
30
- verifiedAt: null,
31
- },
32
- {
33
- key: 'opencode-plugin-api',
34
- label: 'OpenCode Plugin API',
35
- url: 'https://opencode.ai/plugins',
36
- stalenessThresholdDays: 30,
37
- verifiedAt: null,
38
- },
39
- {
40
- key: 'opencode-permissions-docs',
41
- label: 'OpenCode Permissions Documentation',
42
- url: 'https://opencode.ai/permissions',
43
- stalenessThresholdDays: 30,
44
- verifiedAt: null,
45
- },
46
- ];
47
-
48
- const PROPAGATION_CHECKLIST = [
49
- {
50
- trigger: 'OpenCode release with config changes',
51
- targets: [
52
- 'src/opencode/techniques.js — update DEPRECATED_CONFIG_KEYS if keys renamed/removed',
53
- 'src/opencode/config-parser.js — update JSONC validation',
54
- 'src/opencode/governance.js — update caveats if behavior changes',
55
- 'test/opencode-check-matrix.js — update check expectations',
56
- ],
57
- },
58
- {
59
- trigger: 'New OpenCode plugin event type added',
60
- targets: [
61
- 'src/opencode/techniques.js — add to VALID_PLUGIN_EVENTS',
62
- 'src/opencode/governance.js — add to OPENCODE_PLUGIN_GOVERNANCE',
63
- 'src/opencode/setup.js — update plugins starter template',
64
- ],
65
- },
66
- {
67
- trigger: 'New OpenCode permission tool added',
68
- targets: [
69
- 'src/opencode/techniques.js — add to PERMISSIONED_TOOLS',
70
- 'src/opencode/governance.js — update permission profiles',
71
- 'src/opencode/setup.js — update default permission config',
72
- ],
73
- },
74
- {
75
- trigger: 'OpenCode MCP schema change',
76
- targets: [
77
- 'src/opencode/mcp-packs.js — update JSONC projections',
78
- 'src/opencode/techniques.js — update MCP checks',
79
- ],
80
- },
81
- {
82
- trigger: 'Known security bug fixed or new bug reported',
83
- targets: [
84
- 'src/opencode/techniques.js — update security checks (E02, E03, D05)',
85
- 'src/opencode/governance.js — update platformCaveats',
86
- 'src/opencode/freshness.js — verify against latest release',
87
- ],
88
- },
89
- ];
90
-
91
- function checkReleaseGate(sourceVerifications = {}) {
92
- const now = new Date();
93
- const results = P0_SOURCES.map(source => {
94
- const verifiedAt = sourceVerifications[source.key]
95
- ? new Date(sourceVerifications[source.key])
96
- : source.verifiedAt ? new Date(source.verifiedAt) : null;
97
-
98
- if (!verifiedAt) {
99
- return { ...source, status: 'unverified', daysStale: null };
100
- }
101
-
102
- const daysSince = Math.floor((now - verifiedAt) / (1000 * 60 * 60 * 24));
103
- const isStale = daysSince > source.stalenessThresholdDays;
104
-
105
- return {
106
- ...source,
107
- verifiedAt: verifiedAt.toISOString(),
108
- daysStale: daysSince,
109
- status: isStale ? 'stale' : 'fresh',
110
- };
111
- });
112
-
113
- return {
114
- ready: results.every(r => r.status === 'fresh'),
115
- stale: results.filter(r => r.status === 'stale' || r.status === 'unverified'),
116
- fresh: results.filter(r => r.status === 'fresh'),
117
- results,
118
- };
119
- }
120
-
121
- function formatReleaseGate(gateResult) {
122
- const lines = [
123
- `OpenCode Freshness Gate (nerviq v${version})`,
124
- '═══════════════════════════════════════',
125
- '',
126
- `Status: ${gateResult.ready ? 'READY' : 'BLOCKED'}`,
127
- `Fresh: ${gateResult.fresh.length}/${gateResult.results.length}`,
128
- '',
129
- ];
130
-
131
- for (const result of gateResult.results) {
132
- const icon = result.status === 'fresh' ? '✓' : result.status === 'stale' ? '✗' : '?';
133
- const age = result.daysStale !== null ? ` (${result.daysStale}d ago)` : ' (unverified)';
134
- lines.push(` ${icon} ${result.label}${age} — threshold: ${result.stalenessThresholdDays}d`);
135
- }
136
-
137
- if (!gateResult.ready) {
138
- lines.push('');
139
- lines.push('Action required: verify stale/unverified sources before claiming release freshness.');
140
- }
141
-
142
- return lines.join('\n');
143
- }
144
-
145
- function getPropagationTargets(triggerKeyword) {
146
- const keyword = triggerKeyword.toLowerCase();
147
- return PROPAGATION_CHECKLIST.filter(item =>
148
- item.trigger.toLowerCase().includes(keyword)
149
- );
150
- }
151
-
152
- module.exports = {
153
- P0_SOURCES,
154
- PROPAGATION_CHECKLIST,
155
- checkReleaseGate,
156
- formatReleaseGate,
157
- getPropagationTargets,
158
- };
1
+ /**
2
+ * OpenCode Freshness Operationalization
3
+ *
4
+ * Release gates, recurring probes, propagation checklists,
5
+ * and staleness blocking for OpenCode surfaces.
6
+ */
7
+
8
+ const { version } = require('../../package.json');
9
+
10
+ const P0_SOURCES = [
11
+ {
12
+ key: 'opencode-docs',
13
+ label: 'OpenCode Official Docs',
14
+ url: 'https://opencode.ai/docs',
15
+ stalenessThresholdDays: 30,
16
+ verifiedAt: '2026-04-07',
17
+ },
18
+ {
19
+ key: 'opencode-config-reference',
20
+ label: 'OpenCode Config Reference',
21
+ url: 'https://opencode.ai/docs/config/',
22
+ stalenessThresholdDays: 30,
23
+ verifiedAt: '2026-04-07',
24
+ },
25
+ {
26
+ key: 'opencode-github-releases',
27
+ label: 'OpenCode GitHub Releases',
28
+ url: 'https://github.com/sst/opencode/releases',
29
+ stalenessThresholdDays: 14,
30
+ verifiedAt: '2026-04-07',
31
+ },
32
+ {
33
+ key: 'opencode-plugin-api',
34
+ label: 'OpenCode Plugin API',
35
+ url: 'https://opencode.ai/docs/plugins/',
36
+ stalenessThresholdDays: 30,
37
+ verifiedAt: '2026-04-07',
38
+ },
39
+ {
40
+ key: 'opencode-permissions-docs',
41
+ label: 'OpenCode Permissions Documentation',
42
+ url: 'https://opencode.ai/docs/permissions/',
43
+ stalenessThresholdDays: 30,
44
+ verifiedAt: '2026-04-07',
45
+ },
46
+ ];
47
+
48
+ const PROPAGATION_CHECKLIST = [
49
+ {
50
+ trigger: 'OpenCode release with config changes',
51
+ targets: [
52
+ 'src/opencode/techniques.js — update DEPRECATED_CONFIG_KEYS if keys renamed/removed',
53
+ 'src/opencode/config-parser.js — update JSONC validation',
54
+ 'src/opencode/governance.js — update caveats if behavior changes',
55
+ 'test/opencode-check-matrix.js — update check expectations',
56
+ ],
57
+ },
58
+ {
59
+ trigger: 'New OpenCode plugin event type added',
60
+ targets: [
61
+ 'src/opencode/techniques.js — add to VALID_PLUGIN_EVENTS',
62
+ 'src/opencode/governance.js — add to OPENCODE_PLUGIN_GOVERNANCE',
63
+ 'src/opencode/setup.js — update plugins starter template',
64
+ ],
65
+ },
66
+ {
67
+ trigger: 'New OpenCode permission tool added',
68
+ targets: [
69
+ 'src/opencode/techniques.js — add to PERMISSIONED_TOOLS',
70
+ 'src/opencode/governance.js — update permission profiles',
71
+ 'src/opencode/setup.js — update default permission config',
72
+ ],
73
+ },
74
+ {
75
+ trigger: 'OpenCode MCP schema change',
76
+ targets: [
77
+ 'src/opencode/mcp-packs.js — update JSONC projections',
78
+ 'src/opencode/techniques.js — update MCP checks',
79
+ ],
80
+ },
81
+ {
82
+ trigger: 'Known security bug fixed or new bug reported',
83
+ targets: [
84
+ 'src/opencode/techniques.js — update security checks (E02, E03, D05)',
85
+ 'src/opencode/governance.js — update platformCaveats',
86
+ 'src/opencode/freshness.js — verify against latest release',
87
+ ],
88
+ },
89
+ ];
90
+
91
+ function checkReleaseGate(sourceVerifications = {}) {
92
+ const now = new Date();
93
+ const results = P0_SOURCES.map(source => {
94
+ const verifiedAt = sourceVerifications[source.key]
95
+ ? new Date(sourceVerifications[source.key])
96
+ : source.verifiedAt ? new Date(source.verifiedAt) : null;
97
+
98
+ if (!verifiedAt) {
99
+ return { ...source, status: 'unverified', daysStale: null };
100
+ }
101
+
102
+ const daysSince = Math.floor((now - verifiedAt) / (1000 * 60 * 60 * 24));
103
+ const isStale = daysSince > source.stalenessThresholdDays;
104
+
105
+ return {
106
+ ...source,
107
+ verifiedAt: verifiedAt.toISOString(),
108
+ daysStale: daysSince,
109
+ status: isStale ? 'stale' : 'fresh',
110
+ };
111
+ });
112
+
113
+ return {
114
+ ready: results.every(r => r.status === 'fresh'),
115
+ stale: results.filter(r => r.status === 'stale' || r.status === 'unverified'),
116
+ fresh: results.filter(r => r.status === 'fresh'),
117
+ results,
118
+ };
119
+ }
120
+
121
+ function formatReleaseGate(gateResult) {
122
+ const lines = [
123
+ `OpenCode Freshness Gate (nerviq v${version})`,
124
+ '═══════════════════════════════════════',
125
+ '',
126
+ `Status: ${gateResult.ready ? 'READY' : 'BLOCKED'}`,
127
+ `Fresh: ${gateResult.fresh.length}/${gateResult.results.length}`,
128
+ '',
129
+ ];
130
+
131
+ for (const result of gateResult.results) {
132
+ const icon = result.status === 'fresh' ? '✓' : result.status === 'stale' ? '✗' : '?';
133
+ const age = result.daysStale !== null ? ` (${result.daysStale}d ago)` : ' (unverified)';
134
+ lines.push(` ${icon} ${result.label}${age} — threshold: ${result.stalenessThresholdDays}d`);
135
+ }
136
+
137
+ if (!gateResult.ready) {
138
+ lines.push('');
139
+ lines.push('Action required: verify stale/unverified sources before claiming release freshness.');
140
+ }
141
+
142
+ return lines.join('\n');
143
+ }
144
+
145
+ function getPropagationTargets(triggerKeyword) {
146
+ const keyword = triggerKeyword.toLowerCase();
147
+ return PROPAGATION_CHECKLIST.filter(item =>
148
+ item.trigger.toLowerCase().includes(keyword)
149
+ );
150
+ }
151
+
152
+ module.exports = {
153
+ P0_SOURCES,
154
+ PROPAGATION_CHECKLIST,
155
+ checkReleaseGate,
156
+ formatReleaseGate,
157
+ getPropagationTargets,
158
+ };
@@ -358,7 +358,7 @@ function buildStackChecks({ platform, objectPrefix, idPrefix, docs }) {
358
358
  impact: 'high',
359
359
  fix: 'Document the XCTest or `xcodebuild test` workflow so iOS verification is part of the default path.',
360
360
  check: (ctx) => hasSwiftSurface(ctx)
361
- ? /xctest|swift test|xcodebuild test|test target/i.test(projectText(ctx, docs)) ||
361
+ ? /xctest|swift test|xcodebuild[^\n\r]{0,200}\btest\b|test target/i.test(projectText(ctx, docs)) ||
362
362
  hasMatchingFile(ctx, /(^|[\\/])Tests([\\/]|$)|XCTestCase/i)
363
363
  : null,
364
364
  }),
@@ -54,6 +54,7 @@ function formatSynergyReport(options) {
54
54
  lines.push(c(' ║ SYNERGY DASHBOARD [EXPERIMENTAL] ║', 'blue'));
55
55
  lines.push(c(' ╚══════════════════════════════════════════════════╝', 'blue'));
56
56
  lines.push(c(' Static routing rules. Learned routing planned for v2.0.', 'dim'));
57
+ lines.push(c(' Harmony is the GA cross-platform surface. Treat Synergy as advisory research output.', 'dim'));
57
58
  lines.push('');
58
59
 
59
60
  // Compound audit
package/src/techniques.js CHANGED
@@ -4,8 +4,15 @@
4
4
  * Each technique includes: what to check, how to fix, impact level.
5
5
  */
6
6
 
7
- const fs = require('fs');
8
- const path = require('path');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const {
10
+ getClaudeInstructionBundle,
11
+ hasDocumentedVerificationGuidance,
12
+ hasDocumentedTestCommand,
13
+ hasDocumentedLintCommand,
14
+ hasDocumentedBuildCommand,
15
+ } = require('./instruction-surfaces');
9
16
 
10
17
  function hasFrontendSignals(ctx) {
11
18
  const pkg = ctx.fileContent('package.json') || '';
@@ -444,62 +451,58 @@ const TECHNIQUES = {
444
451
  // === QUALITY & TESTING (category: 'quality') ================
445
452
  // ============================================================
446
453
 
447
- verificationLoop: {
448
- id: 93,
449
- name: 'Verification criteria in CLAUDE.md',
450
- check: (ctx) => {
451
- const md = ctx.claudeMdContent() || '';
452
- return /\b(npm test|yarn test|pnpm test|pytest|go test|make test|npm run lint|yarn lint|npx |ruff |eslint)\b/i.test(md) ||
453
- /\b(test command|lint command|build command|verify|run tests|run lint)\b/i.test(md);
454
- },
455
- impact: 'critical',
456
- rating: 5,
457
- category: 'quality',
458
- fix: 'Add test/lint/build commands to CLAUDE.md so Claude can verify its own work.',
459
- template: null
460
- },
461
-
462
- testCommand: {
463
- id: 93001,
464
- name: 'CLAUDE.md contains a test command',
465
- check: (ctx) => {
466
- const md = ctx.claudeMdContent() || '';
467
- return /npm test|pytest|jest|vitest|cargo test|go test|mix test|rspec/.test(md);
468
- },
469
- impact: 'high',
470
- rating: 5,
471
- category: 'quality',
472
- fix: 'Add an explicit test command to CLAUDE.md (e.g. "Run `npm test` before committing").',
473
- template: null
474
- },
475
-
476
- lintCommand: {
477
- id: 93002,
478
- name: 'CLAUDE.md contains a lint command',
479
- check: (ctx) => {
480
- const md = ctx.claudeMdContent() || '';
481
- return /eslint|prettier|ruff|black|clippy|golangci-lint|rubocop|npm run lint|yarn lint|pnpm lint|bun lint/.test(md);
482
- },
483
- impact: 'high',
484
- rating: 4,
485
- category: 'quality',
486
- fix: 'Add a lint command to CLAUDE.md so Claude auto-formats and checks code style.',
487
- template: null
488
- },
489
-
490
- buildCommand: {
491
- id: 93003,
492
- name: 'CLAUDE.md contains a build command',
493
- check: (ctx) => {
494
- const md = ctx.claudeMdContent() || '';
495
- return /npm run build|cargo build|go build|make|tsc|gradle build|mvn compile/.test(md);
496
- },
497
- impact: 'medium',
498
- rating: 4,
499
- category: 'quality',
500
- fix: 'Add a build command to CLAUDE.md so Claude can verify compilation before committing.',
501
- template: null
502
- },
454
+ verificationLoop: {
455
+ id: 93,
456
+ name: 'Claude instruction surfaces include verification criteria',
457
+ check: (ctx) => {
458
+ const docs = getClaudeInstructionBundle(ctx);
459
+ return hasDocumentedVerificationGuidance(docs);
460
+ },
461
+ impact: 'critical',
462
+ rating: 5,
463
+ category: 'quality',
464
+ fix: 'Add canonical test/lint/build commands to your Claude instruction surfaces (CLAUDE.md, imported docs, or .claude/commands) so Claude can verify its own work.',
465
+ template: null
466
+ },
467
+
468
+ testCommand: {
469
+ id: 93001,
470
+ name: 'Claude instruction surfaces include a test command',
471
+ check: (ctx) => {
472
+ return hasDocumentedTestCommand(getClaudeInstructionBundle(ctx));
473
+ },
474
+ impact: 'high',
475
+ rating: 5,
476
+ category: 'quality',
477
+ fix: 'Add an explicit test command to your Claude instruction surfaces (for example "Run `npm test` before committing").',
478
+ template: null
479
+ },
480
+
481
+ lintCommand: {
482
+ id: 93002,
483
+ name: 'Claude instruction surfaces include a lint command',
484
+ check: (ctx) => {
485
+ return hasDocumentedLintCommand(getClaudeInstructionBundle(ctx));
486
+ },
487
+ impact: 'high',
488
+ rating: 4,
489
+ category: 'quality',
490
+ fix: 'Add a lint command to your Claude instruction surfaces so Claude can check style and static quality automatically.',
491
+ template: null
492
+ },
493
+
494
+ buildCommand: {
495
+ id: 93003,
496
+ name: 'Claude instruction surfaces include a build command',
497
+ check: (ctx) => {
498
+ return hasDocumentedBuildCommand(getClaudeInstructionBundle(ctx));
499
+ },
500
+ impact: 'medium',
501
+ rating: 4,
502
+ category: 'quality',
503
+ fix: 'Add a build command to your Claude instruction surfaces so Claude can verify compilation before committing.',
504
+ template: null
505
+ },
503
506
 
504
507
  // ============================================================
505
508
  // === GIT SAFETY (category: 'git') ===========================
@@ -954,6 +957,119 @@ const TECHNIQUES = {
954
957
  template: null
955
958
  },
956
959
 
960
+ // --- Dockerfile best practices (Issue #8) ---
961
+
962
+ dockerMultiStage: {
963
+ id: 39902,
964
+ name: 'Dockerfile uses multi-stage build',
965
+ check: (ctx) => {
966
+ const df = findProjectFiles(ctx, /^Dockerfile$/i);
967
+ if (df.length === 0) return null;
968
+ const content = ctx.fileContent(df[0]) || '';
969
+ return (content.match(/^FROM\s/gim) || []).length >= 2;
970
+ },
971
+ impact: 'medium',
972
+ rating: 3,
973
+ category: 'devops',
974
+ fix: 'Use multi-stage builds in Dockerfile to reduce image size and avoid leaking build tools into production.',
975
+ template: null
976
+ },
977
+
978
+ dockerignoreExists: {
979
+ id: 39903,
980
+ name: '.dockerignore includes node_modules and .env',
981
+ check: (ctx) => {
982
+ if (!ctx.files.some(f => /^Dockerfile/i.test(f))) return null;
983
+ const di = ctx.fileContent('.dockerignore') || '';
984
+ return di.includes('node_modules') && /\.env/i.test(di);
985
+ },
986
+ impact: 'high',
987
+ rating: 4,
988
+ category: 'devops',
989
+ fix: 'Add .dockerignore with node_modules, .env, and other sensitive/large files to keep images small and secure.',
990
+ template: null
991
+ },
992
+
993
+ dockerNoSecrets: {
994
+ id: 39904,
995
+ name: 'Dockerfile has no secrets in build args',
996
+ check: (ctx) => {
997
+ const df = findProjectFiles(ctx, /^Dockerfile$/i);
998
+ if (df.length === 0) return null;
999
+ const content = ctx.fileContent(df[0]) || '';
1000
+ return !/ARG\s+(PASSWORD|SECRET|TOKEN|API_KEY|PRIVATE_KEY)/i.test(content);
1001
+ },
1002
+ impact: 'critical',
1003
+ rating: 5,
1004
+ category: 'devops',
1005
+ fix: 'Never pass secrets via ARG in Dockerfile — use runtime environment variables or secret mounts instead.',
1006
+ template: null
1007
+ },
1008
+
1009
+ // --- Terraform checks (Issue #10) ---
1010
+
1011
+ terraformFmt: {
1012
+ id: 39705,
1013
+ name: 'Terraform formatting configured',
1014
+ check: (ctx) => {
1015
+ if (!ctx.files.some(f => /\.tf$/.test(f))) return null;
1016
+ const ci = readProjectFiles(ctx, /\.(yml|yaml)$/i, 10);
1017
+ const makefileContent = ctx.fileContent('Makefile') || '';
1018
+ const preCommit = ctx.fileContent('.pre-commit-config.yaml') || '';
1019
+ return /terraform\s+fmt/i.test(ci) || /terraform\s+fmt/i.test(makefileContent) || /terraform_fmt/i.test(preCommit);
1020
+ },
1021
+ impact: 'medium',
1022
+ rating: 3,
1023
+ category: 'devops',
1024
+ fix: 'Add `terraform fmt` to CI or pre-commit hooks to enforce consistent formatting.',
1025
+ template: null
1026
+ },
1027
+
1028
+ terraformDirIgnored: {
1029
+ id: 39706,
1030
+ name: '.terraform directory in .gitignore',
1031
+ check: (ctx) => {
1032
+ if (!ctx.files.some(f => /\.tf$/.test(f))) return null;
1033
+ const gi = ctx.fileContent('.gitignore') || '';
1034
+ return /\.terraform/i.test(gi);
1035
+ },
1036
+ impact: 'high',
1037
+ rating: 4,
1038
+ category: 'devops',
1039
+ fix: 'Add .terraform/ to .gitignore — it contains provider binaries and should not be committed.',
1040
+ template: null
1041
+ },
1042
+
1043
+ terraformStateNotCommitted: {
1044
+ id: 39707,
1045
+ name: 'Terraform state file not committed',
1046
+ check: (ctx) => {
1047
+ if (!ctx.files.some(f => /\.tf$/.test(f))) return null;
1048
+ return !ctx.files.some(f => /terraform\.tfstate$/i.test(f));
1049
+ },
1050
+ impact: 'critical',
1051
+ rating: 5,
1052
+ category: 'devops',
1053
+ fix: 'Never commit terraform.tfstate — it may contain secrets. Use a remote backend (S3, GCS, Terraform Cloud).',
1054
+ template: null
1055
+ },
1056
+
1057
+ terraformBackendConfigured: {
1058
+ id: 39708,
1059
+ name: 'Terraform remote backend configured',
1060
+ check: (ctx) => {
1061
+ const tfFiles = findProjectFiles(ctx, /\.tf$/);
1062
+ if (tfFiles.length === 0) return null;
1063
+ const allTf = tfFiles.slice(0, 10).map(f => ctx.fileContent(f) || '').join('\n');
1064
+ return /backend\s+"(s3|gcs|azurerm|remote|cloud|consul|http)"/i.test(allTf);
1065
+ },
1066
+ impact: 'high',
1067
+ rating: 4,
1068
+ category: 'devops',
1069
+ fix: 'Configure a remote backend in Terraform (S3, GCS, Terraform Cloud) for team collaboration and state locking.',
1070
+ template: null
1071
+ },
1072
+
957
1073
  // ============================================================
958
1074
  // === PROJECT HYGIENE (category: 'hygiene') ==================
959
1075
  // ============================================================