@chemx/starter-kit 26.9.12-93 → 26.9.13-272

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 (49) hide show
  1. package/cli/audit/ai-slop-detector.js +25 -41
  2. package/cli/audit/ast-visitors.js +10 -13
  3. package/cli/audit/history.js +45 -11
  4. package/cli/audit/pattern-detector.js +182 -0
  5. package/cli/audit/prompts.js +129 -72
  6. package/cli/audit/reporter-banner.js +4 -16
  7. package/cli/audit/reporter-grades.js +28 -16
  8. package/cli/audit/reporter-grouping.d.ts +6 -0
  9. package/cli/audit/reporter-grouping.js +48 -5
  10. package/cli/audit/reporter-markdown.js +31 -16
  11. package/cli/audit/reporter-sections.js +28 -12
  12. package/cli/audit/reporter-utils.js +88 -1
  13. package/cli/audit/reporter.d.ts +13 -4
  14. package/cli/audit/reporter.js +32 -5
  15. package/cli/audit/roadmap.js +236 -0
  16. package/cli/audit/rules-predicates.d.ts +17 -0
  17. package/cli/audit/rules-predicates.js +107 -0
  18. package/cli/audit/rules.js +6 -2
  19. package/cli/audit/social-gh.js +2 -6
  20. package/cli/audit/social-publisher.js +1 -1
  21. package/cli/audit/social.js +25 -9
  22. package/cli/audit/types.d.ts +52 -5
  23. package/cli/audit.js +76 -7
  24. package/cli/badge.js +9 -2
  25. package/cli/build/detector.js +68 -0
  26. package/cli/build/executor.js +58 -0
  27. package/cli/build/grouper.js +78 -0
  28. package/cli/build/parser-matchers.js +116 -0
  29. package/cli/build/parser.js +80 -0
  30. package/cli/build/reporter.js +66 -0
  31. package/cli/build/types.d.ts +58 -0
  32. package/cli/build/types.js +1 -0
  33. package/cli/build.d.ts +16 -0
  34. package/cli/build.js +55 -0
  35. package/cli/help.js +14 -0
  36. package/cli/index.js +39 -14
  37. package/cli/installer-templates.js +43 -4
  38. package/cli/license.js +10 -7
  39. package/cli/navigator-actions.js +34 -6
  40. package/cli/navigator-banner.js +4 -2
  41. package/cli/navigator-menu.js +18 -0
  42. package/cli/navigator-share.js +1 -1
  43. package/cli/navigator.d.ts +8 -1
  44. package/cli/navigator.js +3 -3
  45. package/cli/terminal.js +6 -5
  46. package/cli/theme.d.ts +41 -0
  47. package/cli/theme.js +84 -0
  48. package/docs/CHANGELOG.md +46 -0
  49. package/package.json +3 -2
@@ -0,0 +1,116 @@
1
+ const TS_PATTERN_PARENS = /^([^(]+)\((\d+),(\d+)\):\s*(error|warning)\s*(TS\d+):\s*(.+)$/;
2
+ const TS_PATTERN_COLONS = /^([^:]+):(\d+):(\d+)\s*-\s*(error|warning)\s*(TS\d+):\s*(.+)$/;
3
+ const ROLLUP_RESOLVE_PATTERN = /Could not resolve ["']([^"']+)["'] from ["']([^"']+)["']/;
4
+ const VITE_ERROR_PATTERN = /\[(vite|rollup|plugin:[^\]]+)\]\s*(.+)/;
5
+ const MODULE_NOT_FOUND_PATTERN = /Module not found:\s*Can['’]t resolve ['"]([^'"]+)['"] in ['"]([^'"]+)['"]/;
6
+ const SASS_POSTCSS_PATTERN = /(CssSyntaxError|SassError):\s*(.+)/;
7
+
8
+ const resolveTsSuggestion = (code) => {
9
+ if (code === 'TS2304' || code === 'TS2552') {
10
+ return 'Ensure the identifier is imported or declared in scope.';
11
+ }
12
+ if (code === 'TS2307') {
13
+ return 'Verify package installation or check tsconfig path aliases.';
14
+ }
15
+ if (code === 'TS2322' || code === 'TS2345') {
16
+ return 'Check property types or update interface declarations.';
17
+ }
18
+ if (code === 'TS2339') {
19
+ return 'Verify property naming or extend type definition.';
20
+ }
21
+ return 'Review TypeScript compiler diagnostic.';
22
+ };
23
+
24
+ export const matchTypeScriptError = (line) => {
25
+ const parensMatch = line.match(TS_PATTERN_PARENS);
26
+ if (parensMatch) {
27
+ const [, file, lineNum, colNum, severityStr, code, message] = parensMatch;
28
+ const severity = severityStr.toLowerCase() === 'warning' ? 'WARNING' : 'ERROR';
29
+ return {
30
+ category: 'TYPE_ERROR',
31
+ severity,
32
+ file: file.trim(),
33
+ line: parseInt(lineNum, 10),
34
+ column: parseInt(colNum, 10),
35
+ code,
36
+ message: message.trim(),
37
+ suggestion: resolveTsSuggestion(code)
38
+ };
39
+ }
40
+
41
+ const colonsMatch = line.match(TS_PATTERN_COLONS);
42
+ if (colonsMatch) {
43
+ const [, file, lineNum, colNum, severityStr, code, message] = colonsMatch;
44
+ const severity = severityStr.toLowerCase() === 'warning' ? 'WARNING' : 'ERROR';
45
+ return {
46
+ category: 'TYPE_ERROR',
47
+ severity,
48
+ file: file.trim(),
49
+ line: parseInt(lineNum, 10),
50
+ column: parseInt(colNum, 10),
51
+ code,
52
+ message: message.trim(),
53
+ suggestion: resolveTsSuggestion(code)
54
+ };
55
+ }
56
+
57
+ return null;
58
+ };
59
+
60
+ export const matchResolveError = (line) => {
61
+ const rollupMatch = line.match(ROLLUP_RESOLVE_PATTERN);
62
+ if (rollupMatch) {
63
+ const [, moduleName, importer] = rollupMatch;
64
+ return {
65
+ category: 'RESOLVE_ERROR',
66
+ severity: 'ERROR',
67
+ file: importer.trim(),
68
+ code: 'MODULE_NOT_FOUND',
69
+ message: `Could not resolve module "${moduleName}"`,
70
+ suggestion: 'Verify import path alias, file extension, or run package install.'
71
+ };
72
+ }
73
+
74
+ const moduleMatch = line.match(MODULE_NOT_FOUND_PATTERN);
75
+ if (moduleMatch) {
76
+ const [, moduleName, importer] = moduleMatch;
77
+ return {
78
+ category: 'RESOLVE_ERROR',
79
+ severity: 'ERROR',
80
+ file: importer.trim(),
81
+ code: 'MODULE_NOT_FOUND',
82
+ message: `Can't resolve "${moduleName}"`,
83
+ suggestion: 'Verify dependency is listed in package.json and installed.'
84
+ };
85
+ }
86
+
87
+ return null;
88
+ };
89
+
90
+ export const matchBundlerOrStyleError = (line) => {
91
+ const sassMatch = line.match(SASS_POSTCSS_PATTERN);
92
+ if (sassMatch) {
93
+ const [, errorType, message] = sassMatch;
94
+ return {
95
+ category: 'STYLE_ERROR',
96
+ severity: 'ERROR',
97
+ code: errorType,
98
+ message: message.trim(),
99
+ suggestion: 'Check stylesheet syntax, missing variable, or mixin import.'
100
+ };
101
+ }
102
+
103
+ const viteMatch = line.match(VITE_ERROR_PATTERN);
104
+ if (viteMatch) {
105
+ const [, plugin, message] = viteMatch;
106
+ return {
107
+ category: 'SYNTAX_ERROR',
108
+ severity: 'ERROR',
109
+ code: plugin,
110
+ message: message.trim(),
111
+ suggestion: 'Check component template syntax or plugin configuration.'
112
+ };
113
+ }
114
+
115
+ return null;
116
+ };
@@ -0,0 +1,80 @@
1
+ import { stripAnsi } from '../terminal.js';
2
+ import {
3
+ matchTypeScriptError,
4
+ matchResolveError,
5
+ matchBundlerOrStyleError
6
+ } from './parser-matchers.js';
7
+
8
+ const CHUNK_BUDGET_PATTERN = /chunks are larger than \d+\s*kBs/i;
9
+ const GENERIC_ERROR_PATTERN = /^(SyntaxError|ReferenceError|TypeError|Error):\s*(.+)$/;
10
+
11
+ const cleanAnsiText = (text) => {
12
+ return stripAnsi(text).replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
13
+ };
14
+
15
+ const matchBudgetWarning = (line) => {
16
+ const isBudgetIssue = CHUNK_BUDGET_PATTERN.test(line);
17
+ if (!isBudgetIssue) return null;
18
+ return {
19
+ category: 'BUDGET_WARNING',
20
+ severity: 'WARNING',
21
+ code: 'CHUNK_OVER_BUDGET',
22
+ message: line.trim(),
23
+ suggestion: 'Use dynamic imports or configure build.rollupOptions.output.manualChunks.'
24
+ };
25
+ };
26
+
27
+ const matchGenericError = (line) => {
28
+ const match = line.match(GENERIC_ERROR_PATTERN);
29
+ if (!match) return null;
30
+ const [, code, message] = match;
31
+ return {
32
+ category: 'RUNTIME_ERROR',
33
+ severity: 'ERROR',
34
+ code,
35
+ message: message.trim(),
36
+ suggestion: 'Inspect stack trace or surrounding code for unhandled exceptions.'
37
+ };
38
+ };
39
+
40
+ export const parseBuildOutput = (stdout, stderr) => {
41
+ const combined = `${stdout}\n${stderr}`;
42
+ const lines = combined.split(/\r?\n/);
43
+ const diagnostics = [];
44
+
45
+ for (const rawLine of lines) {
46
+ const cleanLine = cleanAnsiText(rawLine).trim();
47
+ if (!cleanLine) continue;
48
+
49
+ const tsDiagnostic = matchTypeScriptError(cleanLine);
50
+ if (tsDiagnostic) {
51
+ diagnostics.push({ ...tsDiagnostic, rawSnippet: cleanLine });
52
+ continue;
53
+ }
54
+
55
+ const resolveDiagnostic = matchResolveError(cleanLine);
56
+ if (resolveDiagnostic) {
57
+ diagnostics.push({ ...resolveDiagnostic, rawSnippet: cleanLine });
58
+ continue;
59
+ }
60
+
61
+ const bundlerDiagnostic = matchBundlerOrStyleError(cleanLine);
62
+ if (bundlerDiagnostic) {
63
+ diagnostics.push({ ...bundlerDiagnostic, rawSnippet: cleanLine });
64
+ continue;
65
+ }
66
+
67
+ const budgetDiagnostic = matchBudgetWarning(cleanLine);
68
+ if (budgetDiagnostic) {
69
+ diagnostics.push({ ...budgetDiagnostic, rawSnippet: cleanLine });
70
+ continue;
71
+ }
72
+
73
+ const genericDiagnostic = matchGenericError(cleanLine);
74
+ if (genericDiagnostic) {
75
+ diagnostics.push({ ...genericDiagnostic, rawSnippet: cleanLine });
76
+ }
77
+ }
78
+
79
+ return diagnostics;
80
+ };
@@ -0,0 +1,66 @@
1
+ import { ANSI } from '../theme.js';
2
+
3
+ const formatDiagnosticItem = (item) => {
4
+ const loc = item.file
5
+ ? `${ANSI.CYAN}${item.file}${item.line ? `:${item.line}` : ''}${item.column ? `:${item.column}` : ''}${ANSI.RESET}`
6
+ : `${ANSI.DIM}unknown location${ANSI.RESET}`;
7
+ const codeTag = item.code ? ` ${ANSI.DIM}[${item.code}]${ANSI.RESET}` : '';
8
+ const hint = item.suggestion ? `\n ${ANSI.MINT}↳ Hint: ${item.suggestion}${ANSI.RESET}` : '';
9
+ return ` • ${loc}${codeTag} ${item.message}${hint}`;
10
+ };
11
+
12
+ const formatCategoryGroup = (category, items) => {
13
+ const title = `${ANSI.BOLD}${ANSI.PINK}[${category}]${ANSI.RESET} ${ANSI.DIM}(${items.length})${ANSI.RESET}`;
14
+ const lines = items.map(formatDiagnosticItem);
15
+ return `${title}\n${lines.join('\n')}`;
16
+ };
17
+
18
+ export const formatTerminalBuildReport = (report, options = {}) => {
19
+ const isSilent = Boolean(options.silent);
20
+ const durationSec = (report.durationMs / 1000).toFixed(2);
21
+
22
+ if (report.success) {
23
+ if (isSilent) return '';
24
+ const successHeader = `${ANSI.BOLD}${ANSI.LIME}✔ Build Succeeded${ANSI.RESET} ${ANSI.DIM}(${durationSec}s)${ANSI.RESET}`;
25
+ if (report.counts.warnings === 0) return `${successHeader}\n`;
26
+
27
+ const warningLines = report.diagnostics
28
+ .filter((d) => d.severity === 'WARNING')
29
+ .map(formatDiagnosticItem)
30
+ .join('\n');
31
+ return `${successHeader}\n${ANSI.GOLD}Warnings (${report.counts.warnings}):${ANSI.RESET}\n${warningLines}\n`;
32
+ }
33
+
34
+ const out = [];
35
+ out.push(`${ANSI.BOLD}\x1b[31m✕ Build Failed${ANSI.RESET} ${ANSI.DIM}(exit code ${report.exitCode}, ${durationSec}s)${ANSI.RESET}`);
36
+ out.push(`${ANSI.DIM}Command: ${report.command}${ANSI.RESET}\n`);
37
+
38
+ if (report.diagnostics.length > 0) {
39
+ const grouped = {};
40
+ for (const d of report.diagnostics) {
41
+ if (!grouped[d.category]) grouped[d.category] = [];
42
+ grouped[d.category].push(d);
43
+ }
44
+ for (const [cat, items] of Object.entries(grouped)) {
45
+ out.push(formatCategoryGroup(cat, items));
46
+ out.push('');
47
+ }
48
+ } else if (report.rawTail.length > 0) {
49
+ out.push(`${ANSI.BOLD}${ANSI.GOLD}[UNCATALOGED BUILD OUTPUT]${ANSI.RESET}`);
50
+ for (const line of report.rawTail) {
51
+ out.push(` ${ANSI.DIM}${line}${ANSI.RESET}`);
52
+ }
53
+ out.push('');
54
+ }
55
+
56
+ out.push(
57
+ `${ANSI.BOLD}Audit Summary:${ANSI.RESET} ${report.counts.errors} error(s), ${report.counts.warnings} warning(s) across ${report.counts.files} file(s).`
58
+ );
59
+ out.push(`${ANSI.DIM}Tip: Run with --json to emit structured machine-readable diagnostics for AI agents.${ANSI.RESET}\n`);
60
+
61
+ return out.join('\n');
62
+ };
63
+
64
+ export const formatJsonBuildReport = (report) => {
65
+ return JSON.stringify(report, null, 2);
66
+ };
@@ -0,0 +1,58 @@
1
+ export type BuildDiagnosticCategory =
2
+ | 'TYPE_ERROR'
3
+ | 'RESOLVE_ERROR'
4
+ | 'SYNTAX_ERROR'
5
+ | 'STYLE_ERROR'
6
+ | 'BUDGET_WARNING'
7
+ | 'COMPILER_WARNING'
8
+ | 'RUNTIME_ERROR'
9
+ | 'UNKNOWN';
10
+
11
+ export type BuildDiagnosticSeverity = 'ERROR' | 'WARNING' | 'INFO';
12
+
13
+ export interface BuildDiagnostic {
14
+ readonly category: BuildDiagnosticCategory;
15
+ readonly severity: BuildDiagnosticSeverity;
16
+ readonly file?: string;
17
+ readonly line?: number;
18
+ readonly column?: number;
19
+ readonly code?: string;
20
+ readonly message: string;
21
+ readonly suggestion?: string;
22
+ readonly rawSnippet?: string;
23
+ }
24
+
25
+ export interface BuildSummaryCounts {
26
+ readonly total: number;
27
+ readonly errors: number;
28
+ readonly warnings: number;
29
+ readonly files: number;
30
+ }
31
+
32
+ export interface BuildAuditReport {
33
+ readonly success: boolean;
34
+ readonly exitCode: number;
35
+ readonly durationMs: number;
36
+ readonly command: string;
37
+ readonly counts: BuildSummaryCounts;
38
+ readonly categories: Record<string, number>;
39
+ readonly diagnostics: readonly BuildDiagnostic[];
40
+ readonly rawTail: readonly string[];
41
+ }
42
+
43
+ export interface BuildOptions {
44
+ readonly command?: string;
45
+ readonly silent?: boolean;
46
+ readonly json?: boolean;
47
+ readonly summary?: boolean;
48
+ readonly raw?: boolean;
49
+ readonly cwd?: string;
50
+ }
51
+
52
+ export interface BuildExecutionResult {
53
+ readonly exitCode: number;
54
+ readonly stdout: string;
55
+ readonly stderr: string;
56
+ readonly durationMs: number;
57
+ readonly command: string;
58
+ }
@@ -0,0 +1 @@
1
+ export {};
package/cli/build.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { BuildAuditReport, BuildOptions } from './build/types.js';
2
+
3
+ export function runBuildAudit(
4
+ rawArgs?: readonly string[],
5
+ isCli?: boolean
6
+ ): Promise<BuildAuditReport>;
7
+
8
+ export {
9
+ BuildDiagnostic,
10
+ BuildDiagnosticCategory,
11
+ BuildDiagnosticSeverity,
12
+ BuildSummaryCounts,
13
+ BuildAuditReport,
14
+ BuildOptions,
15
+ BuildExecutionResult
16
+ } from './build/types.js';
package/cli/build.js ADDED
@@ -0,0 +1,55 @@
1
+ import { detectProjectBuildCommand } from './build/detector.js';
2
+ import { executeBuild } from './build/executor.js';
3
+ import { parseBuildOutput } from './build/parser.js';
4
+ import { groupBuildDiagnostics } from './build/grouper.js';
5
+ import { formatTerminalBuildReport, formatJsonBuildReport } from './build/reporter.js';
6
+ import { ANSI } from './theme.js';
7
+
8
+ const parseCommandFromArgs = (args) => {
9
+ const dashDashIndex = args.indexOf('--');
10
+ if (dashDashIndex !== -1) {
11
+ const afterDash = args.slice(dashDashIndex + 1).join(' ').trim();
12
+ if (afterDash.length > 0) return afterDash;
13
+ }
14
+
15
+ const candidate = args.find((a) => !a.startsWith('-') && a !== 'build' && a !== 'run' && a !== 'wrap');
16
+ return candidate ? candidate.trim() : null;
17
+ };
18
+
19
+ export const runBuildAudit = async (rawArgs = [], isCli = false) => {
20
+ const isJson = rawArgs.includes('--json');
21
+ const isSilent = rawArgs.includes('--silent');
22
+ const isRaw = rawArgs.includes('--raw');
23
+ const isSummary = rawArgs.includes('--summary');
24
+
25
+ const customCommand = parseCommandFromArgs(rawArgs);
26
+ const command = detectProjectBuildCommand(customCommand, process.cwd());
27
+
28
+ const shouldPrintStart = !isJson && !isSilent && !isRaw;
29
+ if (shouldPrintStart) {
30
+ process.stdout.write(`${ANSI.CYAN}Auditing build:${ANSI.RESET} ${ANSI.DIM}${command}${ANSI.RESET}\n`);
31
+ }
32
+
33
+ const executionResult = await executeBuild(command, process.cwd(), { raw: isRaw });
34
+ const rawDiagnostics = parseBuildOutput(executionResult.stdout, executionResult.stderr);
35
+ const report = groupBuildDiagnostics(rawDiagnostics, executionResult);
36
+
37
+ if (isJson) {
38
+ process.stdout.write(formatJsonBuildReport(report) + '\n');
39
+ if (isCli) process.exit(report.exitCode);
40
+ return report;
41
+ }
42
+
43
+ const terminalOutput = formatTerminalBuildReport(report, { silent: isSilent, summary: isSummary });
44
+ if (terminalOutput) {
45
+ process.stdout.write(terminalOutput);
46
+ }
47
+
48
+ if (isCli) {
49
+ process.exit(report.exitCode);
50
+ }
51
+
52
+ return report;
53
+ };
54
+
55
+ export * from './build/types.js';
package/cli/help.js CHANGED
@@ -16,6 +16,14 @@ export const printHelp = () => {
16
16
  ` ${CYAN}npm create chemx${RESET} [directory]`,
17
17
  '',
18
18
  `${BOLD}COMMANDS${RESET}`,
19
+ ` ${CYAN}build${RESET} [options] [-- <command>] ${DIM}(aliases: run, wrap)${RESET}`,
20
+ ` Wrap and audit arbitrary build scripts with token-conserving silent execution.`,
21
+ ` Suppresses multi-thousand-line compiler noise, catalogs diagnostics into`,
22
+ ` grouped categories (TypeScript, Vite/Rollup, Style, Budget), and provides`,
23
+ ` actionable remediation hints and machine-readable JSON for AI agents.`,
24
+ ` Flags: ${CYAN}--json${RESET}, ${CYAN}--silent${RESET}, ${CYAN}--raw${RESET}, ${CYAN}--summary${RESET}`,
25
+ ` Examples: ${CYAN}npx chemx build${RESET} or ${CYAN}npx chemx build -- pnpm build${RESET}`,
26
+ '',
19
27
  ` ${CYAN}audit${RESET} [options]`,
20
28
  ` Run the 7-Pillar Molecular Architecture static analysis audit.`,
21
29
  ` Scans AST for line budgets, control flow booleans, nested ternaries,`,
@@ -139,6 +147,12 @@ export const printHelp = () => {
139
147
  '',
140
148
  ` ${DIM}# Scaffold a new project:${RESET}`,
141
149
  ` ${CYAN}npm create chemx my-awesome-app${RESET}`,
150
+ '',
151
+ ` ${DIM}# Run token-conserving silent build audit with structured diagnostics:${RESET}`,
152
+ ` ${CYAN}npx chemx build --silent${RESET}`,
153
+ '',
154
+ ` ${DIM}# Wrap arbitrary build command and output AI-ready JSON:${RESET}`,
155
+ ` ${CYAN}npx chemx build --json -- pnpm run build${RESET}`,
142
156
  ''
143
157
  ].join('\n');
144
158
 
package/cli/index.js CHANGED
@@ -9,7 +9,8 @@ import {
9
9
  generateMarkdownReport,
10
10
  saveAuditSnapshot,
11
11
  copyToClipboard,
12
- buildMasterPrompt
12
+ buildMasterPrompt,
13
+ groupViolationsBySeverity
13
14
  } from './audit.js';
14
15
  import {
15
16
  runScaffold,
@@ -23,8 +24,15 @@ import {
23
24
  showConversionMenu,
24
25
  handleShareToDiscussions
25
26
  } from './navigator.js';
27
+ import { renderDashboardBanner } from './navigator-banner.js';
28
+ import {
29
+ isGradeBelowMinimum,
30
+ evaluateAuditFailure,
31
+ isNonInteractiveSession
32
+ } from './audit/rules-predicates.js';
26
33
  import { runInstallWizard } from './installer.js';
27
34
  import { runBadgeCommand } from './badge.js';
35
+ import { runBuildAudit } from './build.js';
28
36
 
29
37
  const rawArgs = process.argv.slice(2);
30
38
  const invokedBin = path.basename(process.argv[1] || '');
@@ -79,12 +87,11 @@ export const runAudit = async (customDir = null, isCli = false) => {
79
87
  const report = executeAstAudit(targetDir, { outputFile, model, costPerMillion });
80
88
  saveAuditSnapshot(report);
81
89
 
82
- const GRADE_RANKS = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'F': 0 };
83
90
  const hasCriticalOrHigh = report.violations.some((v) => v.severity === 'CRITICAL' || v.severity === 'HIGH');
84
91
  const isStrictFail = isStrict && report.violations.length > 0;
85
- const isGradeFail = Boolean(minGrade && GRADE_RANKS[report.health.grade] !== undefined && GRADE_RANKS[minGrade] !== undefined && GRADE_RANKS[report.health.grade] < GRADE_RANKS[minGrade]);
92
+ const isGradeFail = isGradeBelowMinimum(report.health.grade, minGrade);
86
93
  const isScoreFail = minScore !== null && !isNaN(minScore) && report.health.score < minScore;
87
- const hasFailingViolations = isStrictFail || hasCriticalOrHigh || isGradeFail || isScoreFail;
94
+ const hasFailingViolations = evaluateAuditFailure([isStrictFail, hasCriticalOrHigh, isGradeFail, isScoreFail]);
88
95
 
89
96
  if (isJson) {
90
97
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -92,24 +99,21 @@ export const runAudit = async (customDir = null, isCli = false) => {
92
99
  return report;
93
100
  }
94
101
 
95
- if (isMarkdown) {
102
+ if (isMarkdown && !outputFile) {
96
103
  const md = generateMarkdownReport(report);
97
104
  process.stdout.write(md + '\n');
98
105
  if (isCli) process.exit(hasFailingViolations ? 1 : 0);
99
106
  return report;
100
107
  }
101
108
 
102
- if (outputFile) {
103
- process.stdout.write(`\x1b[32m✔ Exported markdown audit report to:\x1b[0m ${outputFile}\n\n`);
104
- }
105
-
106
109
  if (isCli) {
107
110
  if (isShare) {
108
111
  await handleShareToDiscussions(report);
109
112
  process.exit(0);
110
113
  }
111
114
 
112
- const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
115
+ const isNonInteractive = isNonInteractiveSession(rawArgs);
116
+ const isInteractive = !isNonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
113
117
 
114
118
  if (isInteractive && !isUnroll) {
115
119
  const handleReAudit = () => {
@@ -123,9 +127,25 @@ export const runAudit = async (customDir = null, isCli = false) => {
123
127
  handleReAudit
124
128
  );
125
129
  } else {
126
- process.stdout.write(formatTerminalReport(report));
127
- if (isInteractive && isUnroll) {
128
- await showConversionMenu(() => runScaffold(undefined, rawArgs, runAudit));
130
+ if (isUnroll) {
131
+ process.stdout.write(formatTerminalReport(report));
132
+ if (isInteractive) {
133
+ await showConversionMenu(() => runScaffold(undefined, rawArgs, runAudit));
134
+ }
135
+ } else {
136
+ const { critical, high, medium, low } = groupViolationsBySeverity(report.violations);
137
+ const highMediumCount = high.length + medium.length;
138
+ renderDashboardBanner(
139
+ report.health,
140
+ report.metrics,
141
+ report.violations,
142
+ critical,
143
+ highMediumCount,
144
+ low,
145
+ report.contextAnalysis,
146
+ report.aiSlop,
147
+ { clear: false }
148
+ );
129
149
  }
130
150
  }
131
151
 
@@ -145,7 +165,7 @@ export const runAudit = async (customDir = null, isCli = false) => {
145
165
  return report;
146
166
  };
147
167
 
148
- export { auditFile };
168
+ export { auditFile, runBuildAudit };
149
169
 
150
170
  const main = async () => {
151
171
  const firstArg = rawArgs[0];
@@ -157,6 +177,11 @@ const main = async () => {
157
177
  }
158
178
 
159
179
  switch (firstArg) {
180
+ case 'build':
181
+ case 'run':
182
+ case 'wrap':
183
+ await runBuildAudit(rawArgs.slice(1), true);
184
+ break;
160
185
  case 'audit':
161
186
  await runAudit(null, true);
162
187
  break;
@@ -44,7 +44,7 @@ fi
44
44
 
45
45
  if [ -n "\$AUDIT_BIN" ]; then
46
46
  printf "\\033[38;2;98;201;255m[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...\\033[0m\\n" "\$MIN_GRADE" "\$MIN_SCORE"
47
- if ! \$AUDIT_BIN audit --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --prompt-on-fail; then
47
+ if ! \$AUDIT_BIN audit --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --non-interactive < /dev/null; then
48
48
  printf "\\n\\033[1m\\033[31m[Chemical X] Commit Blocked: Codebase falls below required Grade %s (Score %s)\\033[0m\\n" "\$MIN_GRADE" "\$MIN_SCORE"
49
49
  printf "\\033[36m💡 Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol\\033[0m\\n\\n"
50
50
  exit 1
@@ -78,14 +78,53 @@ jobs:
78
78
  - name: Setup Node.js
79
79
  uses: actions/setup-node@v4
80
80
  with:
81
- node-version: 20
82
- cache: npm
81
+ node-version: 22
82
+
83
+ - name: Install dependencies
84
+ run: |
85
+ if [ -f package-lock.json ]; then
86
+ npm ci
87
+ elif [ -f package.json ]; then
88
+ npm install
89
+ fi
90
+
83
91
  - name: Run Chemical X Architectural Audit
84
- run: npx chemx audit --min-grade=\${{ vars.CHEMX_MIN_GRADE || '${minGrade}' }} --min-score=\${{ vars.CHEMX_MIN_SCORE || ${minScore} }} --markdown --output=AUDIT_REPORT.md
92
+ run: npx --yes chemx audit --min-grade=\${{ vars.CHEMX_MIN_GRADE || '${minGrade}' }} --min-score=\${{ vars.CHEMX_MIN_SCORE || ${minScore} }} --markdown --output=AUDIT_REPORT.md
85
93
  - name: Upload Audit Scorecard Artifact
86
94
  if: always()
87
95
  uses: actions/upload-artifact@v4
88
96
  with:
89
97
  name: chemical-x-audit-report
90
98
  path: AUDIT_REPORT.md
99
+
100
+ - name: Post Architectural Audit Comment on PR
101
+ if: always() && github.event_name == 'pull_request' && hashFiles('AUDIT_REPORT.md') != ''
102
+ continue-on-error: true
103
+ env:
104
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
105
+ PR_NUMBER: \${{ github.event.pull_request.number }}
106
+ REPO: \${{ github.repository }}
107
+ run: |
108
+ DISCUSSION_URL="\${{ vars.CHEMX_DISCUSSION_URL }}"
109
+ if [ -z "\$DISCUSSION_URL" ] && [ -f .chemx/discussion.json ]; then
110
+ DISCUSSION_URL=\$(node -e "try { const d = JSON.parse(require('fs').readFileSync('.chemx/discussion.json')); console.log(d.url || ''); } catch (e) {}")
111
+ fi
112
+ if [ -z "\$DISCUSSION_URL" ]; then
113
+ DISCUSSION_URL="https://github.com/orgs/Chemical-X-Protocol/discussions/categories/npx-chemx-audit"
114
+ fi
115
+
116
+ {
117
+ echo "<!-- chemical-x-audit-pr-comment -->"
118
+ echo "> 💬 **Architectural Discussion Topic:** [View Discussion & Community Showcase](\$DISCUSSION_URL)"
119
+ echo ""
120
+ cat AUDIT_REPORT.md
121
+ } > PR_COMMENT.md
122
+
123
+ EXISTING_COMMENT_ID=\$(gh api "repos/\$REPO/issues/\$PR_NUMBER/comments" --jq '.[] | select(.body | contains("<!-- chemical-x-audit-pr-comment -->")) | .id' | head -n 1)
124
+
125
+ if [ -n "\$EXISTING_COMMENT_ID" ]; then
126
+ gh api --method PATCH "repos/\$REPO/issues/comments/\$EXISTING_COMMENT_ID" -F body=@PR_COMMENT.md
127
+ else
128
+ gh pr comment "\$PR_NUMBER" --body-file PR_COMMENT.md
129
+ fi
91
130
  `;
package/cli/license.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  promptQuestion,
10
10
  openBrowser
11
11
  } from './terminal.js';
12
+ import { toResultSync } from './audit/rules-helpers.js';
12
13
 
13
14
  export const CONFIG_DIR = path.join(os.homedir(), '.chemical-x');
14
15
  export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
@@ -20,14 +21,14 @@ export const URL_SPONSOR = 'https://github.com/sponsors/Chemical-X-Protocol';
20
21
  export const URL_STANDARD = 'https://mycompassconsulting.com/buy/chemical-x/standard';
21
22
  export const URL_MASTER = 'https://mycompassconsulting.com/buy/chemical-x/master';
22
23
 
23
- if (!fs.existsSync(CONFIG_DIR)) {
24
- try {
25
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
- } catch (err) {
27
- const error = err instanceof Error ? err : new Error(String(err));
28
- process.stderr.write(`Failed to create config directory: ${error.message}\n`);
24
+ export const ensureConfigDir = () => {
25
+ if (fs.existsSync(CONFIG_DIR)) return true;
26
+ const [, err] = toResultSync(() => fs.mkdirSync(CONFIG_DIR, { recursive: true }));
27
+ if (err) {
28
+ return false;
29
29
  }
30
- }
30
+ return true;
31
+ };
31
32
 
32
33
  export const getOrCreateDeviceId = () => {
33
34
  if (fs.existsSync(DEVICE_FILE)) {
@@ -40,6 +41,7 @@ export const getOrCreateDeviceId = () => {
40
41
  }
41
42
  }
42
43
  const newId = `cli_${Math.random().toString(36).substring(2, 12)}_${Date.now()}`;
44
+ ensureConfigDir();
43
45
  try {
44
46
  fs.writeFileSync(DEVICE_FILE, newId, 'utf-8');
45
47
  } catch (err) {
@@ -62,6 +64,7 @@ export const getCachedLicenseKey = () => {
62
64
  };
63
65
 
64
66
  export const saveLicenseKey = (licenseKey) => {
67
+ ensureConfigDir();
65
68
  try {
66
69
  fs.writeFileSync(
67
70
  CONFIG_FILE,