@nerviq/cli 1.5.3 → 1.6.1
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/README.md +1 -0
- package/bin/cli.js +12 -1
- package/package.json +1 -1
- package/src/activity.js +167 -0
- package/src/audit.js +36 -4
- package/src/harmony/cli.js +17 -62
- package/src/techniques.js +66 -9
package/README.md
CHANGED
|
@@ -310,6 +310,7 @@ Every write command supports `--snapshot` for automatic backup before changes.
|
|
|
310
310
|
- **npm**: [@nerviq/cli](https://www.npmjs.com/package/@nerviq/cli)
|
|
311
311
|
- **GitHub**: [github.com/nerviq/nerviq](https://github.com/nerviq/nerviq)
|
|
312
312
|
- **Website**: [nerviq.net](https://nerviq.net)
|
|
313
|
+
- **Discord**: [Join the community](https://discord.gg/nerviq)
|
|
313
314
|
|
|
314
315
|
## What Nerviq Is — and Isn't
|
|
315
316
|
|
package/bin/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ const COMMAND_ALIASES = {
|
|
|
26
26
|
gov: 'governance',
|
|
27
27
|
outcome: 'feedback',
|
|
28
28
|
};
|
|
29
|
-
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
|
|
29
|
+
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
|
|
30
30
|
|
|
31
31
|
function levenshtein(a, b) {
|
|
32
32
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -305,6 +305,7 @@ const HELP = `
|
|
|
305
305
|
nerviq setup Generate starter-safe baseline config files
|
|
306
306
|
nerviq setup --auto Apply all generated files without prompts
|
|
307
307
|
nerviq interactive Step-by-step guided wizard
|
|
308
|
+
nerviq check-health Detect regressions + platform format changes between snapshots
|
|
308
309
|
nerviq doctor Self-diagnostics: Node, deps, freshness, platform detection
|
|
309
310
|
|
|
310
311
|
FIX
|
|
@@ -1041,6 +1042,16 @@ async function main() {
|
|
|
1041
1042
|
console.log(output);
|
|
1042
1043
|
}
|
|
1043
1044
|
process.exit(0);
|
|
1045
|
+
} else if (normalizedCommand === 'check-health') {
|
|
1046
|
+
const { checkHealth, formatCheckHealth } = require('../src/activity');
|
|
1047
|
+
const report = checkHealth(options.dir);
|
|
1048
|
+
if (options.json) {
|
|
1049
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1050
|
+
} else {
|
|
1051
|
+
console.log('');
|
|
1052
|
+
console.log(formatCheckHealth(report));
|
|
1053
|
+
}
|
|
1054
|
+
process.exit(0);
|
|
1044
1055
|
} else if (normalizedCommand === 'freshness') {
|
|
1045
1056
|
const { TECHNIQUES } = require('../src/techniques');
|
|
1046
1057
|
const stats = getVerificationStats();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "The intelligent nervous system for AI coding agents — 2,431 checks across 8 platforms, 10 languages, and 62 domain packs. Audit, align, and amplify.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/activity.js
CHANGED
|
@@ -533,6 +533,170 @@ function formatRecommendationOutcomeSummary(dir) {
|
|
|
533
533
|
return lines.join('\n');
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Load the full payload of a snapshot by its index entry.
|
|
538
|
+
* @param {string} dir - Project root directory.
|
|
539
|
+
* @param {Object} indexEntry - Snapshot index entry with relativePath.
|
|
540
|
+
* @returns {Object|null} Full snapshot envelope, or null if unreadable.
|
|
541
|
+
*/
|
|
542
|
+
function loadSnapshotPayload(dir, indexEntry) {
|
|
543
|
+
if (!indexEntry || !indexEntry.relativePath) return null;
|
|
544
|
+
const filePath = path.join(dir, indexEntry.relativePath);
|
|
545
|
+
try {
|
|
546
|
+
const envelope = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
547
|
+
return envelope.payload || null;
|
|
548
|
+
} catch {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Analyze check health by comparing the two most recent audit snapshots.
|
|
555
|
+
* Detects checks that regressed (passed → failed), improved (failed → passed),
|
|
556
|
+
* and flags sudden drops that may indicate platform format changes.
|
|
557
|
+
*
|
|
558
|
+
* @param {string} dir - Project root directory.
|
|
559
|
+
* @returns {Object|null} Health report, or null if fewer than 2 audit snapshots exist.
|
|
560
|
+
*/
|
|
561
|
+
function checkHealth(dir) {
|
|
562
|
+
const history = getHistory(dir, 2);
|
|
563
|
+
if (history.length < 2) return null;
|
|
564
|
+
|
|
565
|
+
const currentPayload = loadSnapshotPayload(dir, history[0]);
|
|
566
|
+
const previousPayload = loadSnapshotPayload(dir, history[1]);
|
|
567
|
+
if (!currentPayload || !previousPayload) return null;
|
|
568
|
+
|
|
569
|
+
const currentResults = currentPayload.results || [];
|
|
570
|
+
const previousResults = previousPayload.results || [];
|
|
571
|
+
|
|
572
|
+
// Build maps: key → passed (true/false/null)
|
|
573
|
+
const prevMap = {};
|
|
574
|
+
for (const r of previousResults) {
|
|
575
|
+
if (r.key) prevMap[r.key] = r.passed;
|
|
576
|
+
}
|
|
577
|
+
const currMap = {};
|
|
578
|
+
for (const r of currentResults) {
|
|
579
|
+
if (r.key) currMap[r.key] = r.passed;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const regressions = []; // was passing → now failing
|
|
583
|
+
const improvements = []; // was failing → now passing
|
|
584
|
+
const newChecks = []; // not in previous
|
|
585
|
+
const removedChecks = []; // not in current
|
|
586
|
+
|
|
587
|
+
for (const r of currentResults) {
|
|
588
|
+
if (!r.key) continue;
|
|
589
|
+
const prev = prevMap[r.key];
|
|
590
|
+
const curr = r.passed;
|
|
591
|
+
if (prev === undefined) {
|
|
592
|
+
if (curr !== null) newChecks.push({ key: r.key, name: r.name, impact: r.impact, passed: curr });
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (prev === true && curr === false) {
|
|
596
|
+
regressions.push({ key: r.key, name: r.name, impact: r.impact, category: r.category });
|
|
597
|
+
} else if (prev === false && curr === true) {
|
|
598
|
+
improvements.push({ key: r.key, name: r.name, impact: r.impact, category: r.category });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const r of previousResults) {
|
|
603
|
+
if (r.key && currMap[r.key] === undefined) {
|
|
604
|
+
removedChecks.push({ key: r.key, name: r.name });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Detect potential platform format changes:
|
|
609
|
+
// If 3+ checks in the same category regressed, flag it
|
|
610
|
+
const regressionsByCategory = {};
|
|
611
|
+
for (const r of regressions) {
|
|
612
|
+
if (!regressionsByCategory[r.category]) regressionsByCategory[r.category] = [];
|
|
613
|
+
regressionsByCategory[r.category].push(r);
|
|
614
|
+
}
|
|
615
|
+
const platformAlerts = [];
|
|
616
|
+
for (const [cat, items] of Object.entries(regressionsByCategory)) {
|
|
617
|
+
if (items.length >= 3) {
|
|
618
|
+
platformAlerts.push({
|
|
619
|
+
category: cat,
|
|
620
|
+
regressionCount: items.length,
|
|
621
|
+
message: `${items.length} checks in '${cat}' regressed — possible platform format change`,
|
|
622
|
+
checks: items.map(i => i.key),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
currentDate: history[0].createdAt,
|
|
629
|
+
previousDate: history[1].createdAt,
|
|
630
|
+
scoreDelta: (currentPayload.score || 0) - (previousPayload.score || 0),
|
|
631
|
+
regressions,
|
|
632
|
+
improvements,
|
|
633
|
+
newChecks,
|
|
634
|
+
removedChecks,
|
|
635
|
+
platformAlerts,
|
|
636
|
+
summary: {
|
|
637
|
+
regressionsCount: regressions.length,
|
|
638
|
+
improvementsCount: improvements.length,
|
|
639
|
+
newChecksCount: newChecks.length,
|
|
640
|
+
removedChecksCount: removedChecks.length,
|
|
641
|
+
alertsCount: platformAlerts.length,
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Format check-health report for CLI display.
|
|
648
|
+
*/
|
|
649
|
+
function formatCheckHealth(healthReport) {
|
|
650
|
+
if (!healthReport) return 'Need at least 2 audit snapshots. Run `nerviq audit --snapshot` twice.';
|
|
651
|
+
|
|
652
|
+
const lines = [];
|
|
653
|
+
const { scoreDelta, regressions, improvements, platformAlerts, newChecks, summary } = healthReport;
|
|
654
|
+
const sign = scoreDelta >= 0 ? '+' : '';
|
|
655
|
+
|
|
656
|
+
lines.push(` Check Health Report`);
|
|
657
|
+
lines.push(` ─────────────────────────────────────`);
|
|
658
|
+
lines.push(` Period: ${healthReport.previousDate?.split('T')[0]} → ${healthReport.currentDate?.split('T')[0]}`);
|
|
659
|
+
lines.push(` Score delta: ${sign}${scoreDelta}`);
|
|
660
|
+
lines.push('');
|
|
661
|
+
|
|
662
|
+
if (platformAlerts.length > 0) {
|
|
663
|
+
lines.push(` ⚠️ PLATFORM ALERTS (${platformAlerts.length})`);
|
|
664
|
+
for (const alert of platformAlerts) {
|
|
665
|
+
lines.push(` ${alert.message}`);
|
|
666
|
+
lines.push(` Checks: ${alert.checks.join(', ')}`);
|
|
667
|
+
}
|
|
668
|
+
lines.push('');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (regressions.length > 0) {
|
|
672
|
+
lines.push(` 🔴 Regressions (${regressions.length} checks now failing)`);
|
|
673
|
+
for (const r of regressions) {
|
|
674
|
+
lines.push(` ${r.name} [${r.impact}]`);
|
|
675
|
+
}
|
|
676
|
+
lines.push('');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (improvements.length > 0) {
|
|
680
|
+
lines.push(` ✅ Improvements (${improvements.length} checks now passing)`);
|
|
681
|
+
for (const r of improvements) {
|
|
682
|
+
lines.push(` ${r.name}`);
|
|
683
|
+
}
|
|
684
|
+
lines.push('');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (newChecks.length > 0) {
|
|
688
|
+
lines.push(` 🆕 New checks (${newChecks.length})`);
|
|
689
|
+
lines.push('');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (regressions.length === 0 && platformAlerts.length === 0) {
|
|
693
|
+
lines.push(` ✅ All checks stable. No regressions detected.`);
|
|
694
|
+
lines.push('');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return lines.join('\n');
|
|
698
|
+
}
|
|
699
|
+
|
|
536
700
|
module.exports = {
|
|
537
701
|
getUserId,
|
|
538
702
|
ensureArtifactDirs,
|
|
@@ -550,4 +714,7 @@ module.exports = {
|
|
|
550
714
|
getRecommendationOutcomeSummary,
|
|
551
715
|
getRecommendationAdjustment,
|
|
552
716
|
formatRecommendationOutcomeSummary,
|
|
717
|
+
checkHealth,
|
|
718
|
+
formatCheckHealth,
|
|
719
|
+
loadSnapshotPayload,
|
|
553
720
|
};
|
package/src/audit.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS } = require('./techniques');
|
|
6
|
+
const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS, STACK_CATEGORY_DETECTORS } = require('./techniques');
|
|
7
7
|
const { ProjectContext } = require('./context');
|
|
8
8
|
const { CODEX_TECHNIQUES } = require('./codex/techniques');
|
|
9
9
|
const { detectCodexDomainPacks } = require('./codex/domain-packs');
|
|
@@ -907,9 +907,9 @@ function printLiteAudit(result, dir) {
|
|
|
907
907
|
return;
|
|
908
908
|
}
|
|
909
909
|
|
|
910
|
-
// Urgency summary line
|
|
911
|
-
const criticalCount = (result.results || []).filter(r =>
|
|
912
|
-
const highCount = (result.results || []).filter(r =>
|
|
910
|
+
// Urgency summary line (only count actual failures, not skipped/null)
|
|
911
|
+
const criticalCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
|
|
912
|
+
const highCount = (result.results || []).filter(r => r.passed === false && r.impact === 'high').length;
|
|
913
913
|
const mediumCount = result.failed - criticalCount - highCount;
|
|
914
914
|
const urgencyParts = [];
|
|
915
915
|
if (criticalCount > 0) urgencyParts.push(colorize(`🔴 ${criticalCount} critical`, 'red'));
|
|
@@ -964,8 +964,40 @@ async function audit(options) {
|
|
|
964
964
|
? mergePluginChecks(spec.techniques, plugins)
|
|
965
965
|
: spec.techniques;
|
|
966
966
|
|
|
967
|
+
// Pre-compute which stack categories are active for this project
|
|
968
|
+
const activeStackCategories = new Set();
|
|
969
|
+
for (const [category, detector] of Object.entries(STACK_CATEGORY_DETECTORS)) {
|
|
970
|
+
if (detector(ctx)) activeStackCategories.add(category);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Generic quality categories that are NOT about AI agent configuration.
|
|
974
|
+
// These are only included with --verbose or --full --verbose (deep quality mode).
|
|
975
|
+
const GENERIC_QUALITY_CATEGORIES = new Set([
|
|
976
|
+
'observability', 'accessibility', 'i18n', 'privacy', 'error-tracking',
|
|
977
|
+
'supply-chain', 'api-versioning', 'caching', 'rate-limiting', 'feature-flags',
|
|
978
|
+
'docs-quality', 'monorepo', 'performance-budget', 'realtime', 'graphql',
|
|
979
|
+
'testing-strategy', 'code-quality', 'api-design', 'database', 'authentication',
|
|
980
|
+
'monitoring', 'dependency-management', 'cost-optimization',
|
|
981
|
+
]);
|
|
982
|
+
const includeGenericQuality = options.verbose;
|
|
983
|
+
|
|
967
984
|
// Run all technique checks
|
|
968
985
|
for (const [key, technique] of Object.entries(techniques)) {
|
|
986
|
+
// Skip entire stack category if the stack is not detected at a core location
|
|
987
|
+
// Skip generic quality categories unless --verbose is set
|
|
988
|
+
const cat = technique.category;
|
|
989
|
+
if ((!includeGenericQuality && GENERIC_QUALITY_CATEGORIES.has(cat)) ||
|
|
990
|
+
(STACK_CATEGORY_DETECTORS[cat] && !activeStackCategories.has(cat))) {
|
|
991
|
+
results.push({
|
|
992
|
+
key,
|
|
993
|
+
...technique,
|
|
994
|
+
file: null,
|
|
995
|
+
line: null,
|
|
996
|
+
passed: null, // not applicable
|
|
997
|
+
});
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
|
|
969
1001
|
const passed = technique.check(ctx);
|
|
970
1002
|
const file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
|
|
971
1003
|
const line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
|
package/src/harmony/cli.js
CHANGED
|
@@ -28,66 +28,21 @@ function resolveDir(options) {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Collect audit results from all detectable platforms.
|
|
31
|
-
*
|
|
31
|
+
* Runs audit() for each platform in sequence (audit is async).
|
|
32
32
|
*/
|
|
33
|
-
function collectPlatformAudits(dir) {
|
|
33
|
+
async function collectPlatformAudits(dir) {
|
|
34
|
+
const { audit } = require('../audit');
|
|
35
|
+
const platforms = ['claude', 'codex', 'gemini', 'copilot', 'cursor', 'windsurf', 'aider', 'opencode'];
|
|
34
36
|
const results = [];
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const { audit } = require('../audit');
|
|
46
|
-
const result = audit({ dir, silent: true, platform: 'codex' });
|
|
47
|
-
if (result) results.push({ platform: 'codex', ...result });
|
|
48
|
-
} catch (_e) { /* platform not available */ }
|
|
49
|
-
|
|
50
|
-
// Try Gemini audit
|
|
51
|
-
try {
|
|
52
|
-
const { audit } = require('../audit');
|
|
53
|
-
const result = audit({ dir, silent: true, platform: 'gemini' });
|
|
54
|
-
if (result) results.push({ platform: 'gemini', ...result });
|
|
55
|
-
} catch (_e) { /* platform not available */ }
|
|
56
|
-
|
|
57
|
-
// Try Copilot audit
|
|
58
|
-
try {
|
|
59
|
-
const { audit } = require('../audit');
|
|
60
|
-
const result = audit({ dir, silent: true, platform: 'copilot' });
|
|
61
|
-
if (result) results.push({ platform: 'copilot', ...result });
|
|
62
|
-
} catch (_e) { /* platform not available */ }
|
|
63
|
-
|
|
64
|
-
// Try Cursor audit
|
|
65
|
-
try {
|
|
66
|
-
const { audit } = require('../audit');
|
|
67
|
-
const result = audit({ dir, silent: true, platform: 'cursor' });
|
|
68
|
-
if (result) results.push({ platform: 'cursor', ...result });
|
|
69
|
-
} catch (_e) { /* platform not available */ }
|
|
70
|
-
|
|
71
|
-
// Try Windsurf audit
|
|
72
|
-
try {
|
|
73
|
-
const { audit } = require('../audit');
|
|
74
|
-
const result = audit({ dir, silent: true, platform: 'windsurf' });
|
|
75
|
-
if (result) results.push({ platform: 'windsurf', ...result });
|
|
76
|
-
} catch (_e) { /* platform not available */ }
|
|
77
|
-
|
|
78
|
-
// Try Aider audit
|
|
79
|
-
try {
|
|
80
|
-
const { audit } = require('../audit');
|
|
81
|
-
const result = audit({ dir, silent: true, platform: 'aider' });
|
|
82
|
-
if (result) results.push({ platform: 'aider', ...result });
|
|
83
|
-
} catch (_e) { /* platform not available */ }
|
|
84
|
-
|
|
85
|
-
// Try OpenCode audit
|
|
86
|
-
try {
|
|
87
|
-
const { audit } = require('../audit');
|
|
88
|
-
const result = audit({ dir, silent: true, platform: 'opencode' });
|
|
89
|
-
if (result) results.push({ platform: 'opencode', ...result });
|
|
90
|
-
} catch (_e) { /* platform not available */ }
|
|
38
|
+
for (const platform of platforms) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await audit({ dir, silent: true, platform });
|
|
41
|
+
if (result && typeof result.score === 'number') {
|
|
42
|
+
results.push({ platform, ...result });
|
|
43
|
+
}
|
|
44
|
+
} catch (_e) { /* platform not available */ }
|
|
45
|
+
}
|
|
91
46
|
|
|
92
47
|
return results;
|
|
93
48
|
}
|
|
@@ -99,7 +54,7 @@ function collectPlatformAudits(dir) {
|
|
|
99
54
|
*/
|
|
100
55
|
async function runHarmonyAudit(options) {
|
|
101
56
|
const dir = resolveDir(options);
|
|
102
|
-
const platformAudits = collectPlatformAudits(dir);
|
|
57
|
+
const platformAudits = await collectPlatformAudits(dir);
|
|
103
58
|
|
|
104
59
|
if (options.json) {
|
|
105
60
|
console.log(JSON.stringify({ dir, platforms: platformAudits }, null, 2));
|
|
@@ -147,7 +102,7 @@ async function runHarmonyAudit(options) {
|
|
|
147
102
|
*/
|
|
148
103
|
async function runHarmonySync(options) {
|
|
149
104
|
const dir = resolveDir(options);
|
|
150
|
-
const platformAudits = collectPlatformAudits(dir);
|
|
105
|
+
const platformAudits = await collectPlatformAudits(dir);
|
|
151
106
|
|
|
152
107
|
// Load or build canonical model from memory
|
|
153
108
|
const state = loadHarmonyState(dir);
|
|
@@ -234,7 +189,7 @@ async function runHarmonyDrift(options) {
|
|
|
234
189
|
*/
|
|
235
190
|
async function runHarmonyAdvise(options) {
|
|
236
191
|
const dir = resolveDir(options);
|
|
237
|
-
const platformAudits = collectPlatformAudits(dir);
|
|
192
|
+
const platformAudits = await collectPlatformAudits(dir);
|
|
238
193
|
const state = loadHarmonyState(dir);
|
|
239
194
|
const canonicalModel = state.canon || null;
|
|
240
195
|
|
|
@@ -309,7 +264,7 @@ async function runHarmonyWatch(options) {
|
|
|
309
264
|
// Logged by watch module itself
|
|
310
265
|
},
|
|
311
266
|
runAudit: options.noAudit ? null : async (auditDir) => {
|
|
312
|
-
const audits = collectPlatformAudits(auditDir);
|
|
267
|
+
const audits = await collectPlatformAudits(auditDir);
|
|
313
268
|
const result = {};
|
|
314
269
|
for (const audit of audits) {
|
|
315
270
|
result[audit.platform] = audit;
|
|
@@ -326,7 +281,7 @@ async function runHarmonyWatch(options) {
|
|
|
326
281
|
*/
|
|
327
282
|
async function runHarmonyGovernance(options) {
|
|
328
283
|
const dir = resolveDir(options);
|
|
329
|
-
const platformAudits = collectPlatformAudits(dir);
|
|
284
|
+
const platformAudits = await collectPlatformAudits(dir);
|
|
330
285
|
const state = loadHarmonyState(dir);
|
|
331
286
|
const canonicalModel = state.canon || null;
|
|
332
287
|
|
package/src/techniques.js
CHANGED
|
@@ -95,6 +95,26 @@ function hasProjectFile(ctx, pattern) {
|
|
|
95
95
|
return findProjectFiles(ctx, pattern).length > 0;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Check if a stack-indicator file exists at a "core" location (root, src/, lib/, app/, packages/)
|
|
100
|
+
* rather than inside examples/, docs/, test/, vendor/, or deeply nested paths.
|
|
101
|
+
* This prevents false stack detection from example/demo code.
|
|
102
|
+
*/
|
|
103
|
+
const EXCLUDED_STACK_DIRS = /^(examples?|docs?|test|tests|fixtures?|samples?|demo|vendor|third[_-]?party|\.github)\//i;
|
|
104
|
+
|
|
105
|
+
function hasCoreProjectFile(ctx, pattern) {
|
|
106
|
+
return findProjectFiles(ctx, pattern).some(file => !EXCLUDED_STACK_DIRS.test(file));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasCoreRootFile(ctx, pattern) {
|
|
110
|
+
// Only match files at project root (no / in path) or one level deep (src/X, lib/X, app/X)
|
|
111
|
+
return findProjectFiles(ctx, pattern).some(file => {
|
|
112
|
+
if (EXCLUDED_STACK_DIRS.test(file)) return false;
|
|
113
|
+
const depth = (file.match(/\//g) || []).length;
|
|
114
|
+
return depth <= 1;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
function readProjectFiles(ctx, pattern, limit = 60) {
|
|
99
119
|
return findProjectFiles(ctx, pattern)
|
|
100
120
|
.slice(0, limit)
|
|
@@ -105,40 +125,42 @@ function readProjectFiles(ctx, pattern, limit = 60) {
|
|
|
105
125
|
|
|
106
126
|
function isPythonProject(ctx) {
|
|
107
127
|
if (ctx.__nerviqIsPython !== undefined) return ctx.__nerviqIsPython;
|
|
128
|
+
// Require a Python config file (pyproject.toml, requirements.txt, setup.py) at a core location.
|
|
129
|
+
// Stray .py files in examples/ or docs/ don't make it a Python project.
|
|
108
130
|
ctx.__nerviqIsPython =
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
hasCoreRootFile(ctx, /(^|\/)(pyproject\.toml|setup\.py|Pipfile)$/i) ||
|
|
132
|
+
hasCoreRootFile(ctx, /(^|\/)requirements\.txt$/i);
|
|
111
133
|
return ctx.__nerviqIsPython;
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
function isGoProject(ctx) {
|
|
115
137
|
if (ctx.__nerviqIsGo !== undefined) return ctx.__nerviqIsGo;
|
|
116
|
-
ctx.__nerviqIsGo =
|
|
138
|
+
ctx.__nerviqIsGo = hasCoreRootFile(ctx, /(^|\/)go\.mod$/i);
|
|
117
139
|
return ctx.__nerviqIsGo;
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
function isRustProject(ctx) {
|
|
121
143
|
if (ctx.__nerviqIsRust !== undefined) return ctx.__nerviqIsRust;
|
|
122
|
-
ctx.__nerviqIsRust =
|
|
144
|
+
ctx.__nerviqIsRust = hasCoreRootFile(ctx, /(^|\/)Cargo\.toml$/i);
|
|
123
145
|
return ctx.__nerviqIsRust;
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
function isJavaProject(ctx) {
|
|
127
149
|
if (ctx.__nerviqIsJava !== undefined) return ctx.__nerviqIsJava;
|
|
128
|
-
ctx.__nerviqIsJava =
|
|
150
|
+
ctx.__nerviqIsJava = hasCoreRootFile(ctx, /(^|\/)(pom\.xml|build\.gradle|build\.gradle\.kts)$/i);
|
|
129
151
|
return ctx.__nerviqIsJava;
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
function isFlutterProject(ctx) {
|
|
133
155
|
if (ctx.__nerviqIsFlutter !== undefined) return ctx.__nerviqIsFlutter;
|
|
134
|
-
ctx.__nerviqIsFlutter =
|
|
156
|
+
ctx.__nerviqIsFlutter = hasCoreRootFile(ctx, /(^|\/)pubspec\.yaml$/i);
|
|
135
157
|
return ctx.__nerviqIsFlutter;
|
|
136
158
|
}
|
|
137
159
|
|
|
138
160
|
function isSwiftProject(ctx) {
|
|
139
161
|
if (ctx.__nerviqIsSwift !== undefined) return ctx.__nerviqIsSwift;
|
|
140
|
-
ctx.__nerviqIsSwift =
|
|
141
|
-
|
|
162
|
+
ctx.__nerviqIsSwift = hasCoreRootFile(ctx, /(^|\/)Package\.swift$/i) ||
|
|
163
|
+
hasCoreProjectFile(ctx, /\.xcodeproj/i);
|
|
142
164
|
return ctx.__nerviqIsSwift;
|
|
143
165
|
}
|
|
144
166
|
|
|
@@ -149,6 +171,41 @@ function isKotlinProject(ctx) {
|
|
|
149
171
|
return ctx.__nerviqIsKotlin;
|
|
150
172
|
}
|
|
151
173
|
|
|
174
|
+
function isRubyProject(ctx) {
|
|
175
|
+
if (ctx.__nerviqIsRuby !== undefined) return ctx.__nerviqIsRuby;
|
|
176
|
+
ctx.__nerviqIsRuby = hasCoreRootFile(ctx, /(^|\/)Gemfile$/i);
|
|
177
|
+
return ctx.__nerviqIsRuby;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isPhpProject(ctx) {
|
|
181
|
+
if (ctx.__nerviqIsPhp !== undefined) return ctx.__nerviqIsPhp;
|
|
182
|
+
ctx.__nerviqIsPhp = hasCoreRootFile(ctx, /(^|\/)composer\.json$/i);
|
|
183
|
+
return ctx.__nerviqIsPhp;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isDotnetProject(ctx) {
|
|
187
|
+
if (ctx.__nerviqIsDotnet !== undefined) return ctx.__nerviqIsDotnet;
|
|
188
|
+
ctx.__nerviqIsDotnet = hasCoreProjectFile(ctx, /(^|\/)(.*\.csproj|.*\.sln|global\.json)$/i);
|
|
189
|
+
return ctx.__nerviqIsDotnet;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Map category names to their project detection function.
|
|
194
|
+
* Used by the audit to skip entire categories when the stack isn't detected.
|
|
195
|
+
*/
|
|
196
|
+
const STACK_CATEGORY_DETECTORS = {
|
|
197
|
+
python: isPythonProject,
|
|
198
|
+
go: isGoProject,
|
|
199
|
+
rust: isRustProject,
|
|
200
|
+
java: isJavaProject,
|
|
201
|
+
flutter: isFlutterProject,
|
|
202
|
+
swift: isSwiftProject,
|
|
203
|
+
kotlin: isKotlinProject,
|
|
204
|
+
ruby: isRubyProject,
|
|
205
|
+
php: isPhpProject,
|
|
206
|
+
dotnet: isDotnetProject,
|
|
207
|
+
};
|
|
208
|
+
|
|
152
209
|
function getPythonFiles(ctx) {
|
|
153
210
|
if (ctx.__nerviqPythonFiles) return ctx.__nerviqPythonFiles;
|
|
154
211
|
ctx.__nerviqPythonFiles = findProjectFiles(ctx, /\.py$/i);
|
|
@@ -5434,4 +5491,4 @@ const STACKS = {
|
|
|
5434
5491
|
|
|
5435
5492
|
attachSourceUrls('claude', TECHNIQUES);
|
|
5436
5493
|
|
|
5437
|
-
module.exports = { TECHNIQUES, STACKS, containsEmbeddedSecret };
|
|
5494
|
+
module.exports = { TECHNIQUES, STACKS, STACK_CATEGORY_DETECTORS, containsEmbeddedSecret };
|