code-auditor-mcp 3.4.14 → 3.4.16

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 (93) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +83 -0
  3. package/dist/analyzers/applicability.d.ts +41 -0
  4. package/dist/analyzers/applicability.d.ts.map +1 -0
  5. package/dist/analyzers/applicability.js +96 -0
  6. package/dist/analyzers/applicability.js.map +1 -0
  7. package/dist/analyzers/ruleRegistry.js +1 -1
  8. package/dist/analyzers/ruleRegistry.js.map +1 -1
  9. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +2 -2
  10. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  11. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +2 -2
  12. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  13. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  14. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +20 -9
  15. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  16. package/dist/analyzers/universal/schema/migrations.d.ts +11 -0
  17. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
  18. package/dist/analyzers/universal/schema/migrations.js +127 -4
  19. package/dist/analyzers/universal/schema/migrations.js.map +1 -1
  20. package/dist/auditRunner.d.ts.map +1 -1
  21. package/dist/auditRunner.js +62 -23
  22. package/dist/auditRunner.js.map +1 -1
  23. package/dist/cli.js +406 -1
  24. package/dist/cli.js.map +1 -1
  25. package/dist/codeIndexDB.d.ts +40 -1
  26. package/dist/codeIndexDB.d.ts.map +1 -1
  27. package/dist/codeIndexDB.js +222 -50
  28. package/dist/codeIndexDB.js.map +1 -1
  29. package/dist/config/defaults.js +1 -1
  30. package/dist/config/defaults.js.map +1 -1
  31. package/dist/config/pathProfiles.d.ts +7 -0
  32. package/dist/config/pathProfiles.d.ts.map +1 -1
  33. package/dist/config/pathProfiles.js +7 -1
  34. package/dist/config/pathProfiles.js.map +1 -1
  35. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  36. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1 -1
  37. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  38. package/dist/ledger.d.ts +163 -2
  39. package/dist/ledger.d.ts.map +1 -1
  40. package/dist/ledger.js +528 -7
  41. package/dist/ledger.js.map +1 -1
  42. package/dist/mcp.js +3 -3
  43. package/dist/mcp.js.map +1 -1
  44. package/dist/mcpAuditJobs.d.ts +24 -2
  45. package/dist/mcpAuditJobs.d.ts.map +1 -1
  46. package/dist/mcpAuditJobs.js +364 -58
  47. package/dist/mcpAuditJobs.js.map +1 -1
  48. package/dist/pipeline.d.ts +9 -2
  49. package/dist/pipeline.d.ts.map +1 -1
  50. package/dist/pipeline.js +165 -18
  51. package/dist/pipeline.js.map +1 -1
  52. package/dist/pipelineAdapters.d.ts.map +1 -1
  53. package/dist/pipelineAdapters.js +45 -10
  54. package/dist/pipelineAdapters.js.map +1 -1
  55. package/dist/scripts/benchmarkWorkers.js +1 -1
  56. package/dist/scripts/benchmarkWorkers.js.map +1 -1
  57. package/dist/services/auditJobService.d.ts +22 -5
  58. package/dist/services/auditJobService.d.ts.map +1 -1
  59. package/dist/services/auditJobService.js +67 -26
  60. package/dist/services/auditJobService.js.map +1 -1
  61. package/dist/services/fileAccounting.d.ts +83 -0
  62. package/dist/services/fileAccounting.d.ts.map +1 -0
  63. package/dist/services/fileAccounting.js +215 -0
  64. package/dist/services/fileAccounting.js.map +1 -0
  65. package/dist/styles/styleExtractor.d.ts +2 -2
  66. package/dist/styles/styleExtractor.d.ts.map +1 -1
  67. package/dist/styles/styleExtractor.js +83 -11
  68. package/dist/styles/styleExtractor.js.map +1 -1
  69. package/dist/styles/styleIndexer.d.ts +8 -0
  70. package/dist/styles/styleIndexer.d.ts.map +1 -1
  71. package/dist/styles/styleIndexer.js +103 -15
  72. package/dist/styles/styleIndexer.js.map +1 -1
  73. package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
  74. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  75. package/dist/styles/tailwindUtilityExpander.js +35 -1
  76. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  77. package/dist/styles/types.d.ts +11 -0
  78. package/dist/styles/types.d.ts.map +1 -1
  79. package/dist/types.d.ts +133 -0
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js.map +1 -1
  82. package/dist/utils/fileDiscovery.d.ts +98 -0
  83. package/dist/utils/fileDiscovery.d.ts.map +1 -1
  84. package/dist/utils/fileDiscovery.js +246 -29
  85. package/dist/utils/fileDiscovery.js.map +1 -1
  86. package/dist/workers/auditJobRunner.d.ts +2 -0
  87. package/dist/workers/auditJobRunner.d.ts.map +1 -0
  88. package/dist/workers/auditJobRunner.js +37 -0
  89. package/dist/workers/auditJobRunner.js.map +1 -0
  90. package/dist/workers/auditWorker.js +26 -0
  91. package/dist/workers/auditWorker.js.map +1 -1
  92. package/package.json +2 -2
  93. package/plugin/.claude-plugin/plugin.json +1 -1
@@ -1,36 +1,77 @@
1
1
  /**
2
- * In-memory audit job queue. Jobs are lost on process restart.
2
+ * DB-backed audit job model.
3
+ *
4
+ * A job is a view over a `findings_ledger_runs` row: `jobId` IS `run_id`, and
5
+ * lifecycle state (`queued → running → completed/failed`, progress, error,
6
+ * resultId) lives on the run row. There is no second job model and no
7
+ * in-memory Map — this delegates to `ledger.ts`, so synchronous and detached
8
+ * audits share one write path.
9
+ *
10
+ * The database handle is passed in explicitly (matching `ledger.ts`); callers
11
+ * resolve the per-project DB via `CodeIndexDB.getInstance(undefined, projectRoot)`.
3
12
  */
4
- const jobs = new Map();
5
- function randomId() {
6
- return `job_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
7
- }
8
- export function createAuditJob(resolvedPath) {
9
- const jobId = randomId();
10
- const rec = {
11
- jobId,
12
- status: 'queued',
13
- path: resolvedPath,
14
- createdAt: new Date().toISOString(),
13
+ import path from 'node:path';
14
+ import { readFileSync } from 'node:fs';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { createLedgerRun, detectRunInput, getLedgerRun, patchLedgerRun, } from '../ledger.js';
17
+ // Package version — read once at module load (mirrors auditRunner.ts).
18
+ const __svcDirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const _pkg = JSON.parse(readFileSync(path.join(__svcDirname, '..', '..', 'package.json'), 'utf-8'));
20
+ const TOOL_VERSION = String(_pkg.version || '0.0.0');
21
+ function toAuditJobRecord(detail) {
22
+ let progress;
23
+ if (detail.progressJson) {
24
+ try {
25
+ progress = JSON.parse(detail.progressJson);
26
+ }
27
+ catch {
28
+ progress = undefined;
29
+ }
30
+ }
31
+ return {
32
+ jobId: detail.runId,
33
+ status: detail.status,
34
+ path: detail.projectRoot ?? detail.target ?? '',
35
+ createdAt: detail.timestamp,
36
+ ...(detail.startedAt ? { startedAt: detail.startedAt } : {}),
37
+ ...(detail.finishedAt ? { finishedAt: detail.finishedAt } : {}),
38
+ ...(detail.resultId ? { resultId: detail.resultId } : {}),
39
+ ...(detail.error ? { error: detail.error } : {}),
40
+ ...(progress ? { progress } : {}),
15
41
  };
16
- jobs.set(jobId, rec);
17
- return rec;
18
42
  }
19
- export function getAuditJob(jobId) {
20
- return jobs.get(jobId);
43
+ export function createAuditJob(db, projectRoot, meta = {}) {
44
+ const runInput = detectRunInput(meta.command ?? 'audit.start', meta.surface ?? 'mcp', meta.scope ?? 'all', projectRoot, TOOL_VERSION);
45
+ const runId = createLedgerRun(db, runInput, { status: 'queued', projectRoot });
46
+ // getLedgerRun cannot miss a row we just created; the non-null assertion is safe.
47
+ return toAuditJobRecord(getLedgerRun(db, runId));
48
+ }
49
+ export function getAuditJob(db, jobId) {
50
+ const detail = getLedgerRun(db, jobId);
51
+ return detail ? toAuditJobRecord(detail) : undefined;
21
52
  }
22
- export function patchAuditJob(jobId, patch) {
23
- const cur = jobs.get(jobId);
24
- if (!cur)
53
+ export function patchAuditJob(db, jobId, patch) {
54
+ if (!getLedgerRun(db, jobId))
25
55
  return undefined;
26
- const next = { ...cur, ...patch };
27
- jobs.set(jobId, next);
28
- return next;
56
+ const ledgerPatch = {};
57
+ if (patch.status !== undefined)
58
+ ledgerPatch.status = patch.status;
59
+ if (patch.startedAt !== undefined)
60
+ ledgerPatch.startedAt = patch.startedAt ?? null;
61
+ if (patch.finishedAt !== undefined)
62
+ ledgerPatch.finishedAt = patch.finishedAt ?? null;
63
+ if (patch.error !== undefined)
64
+ ledgerPatch.error = patch.error ?? null;
65
+ if (patch.resultId !== undefined)
66
+ ledgerPatch.resultId = patch.resultId ?? null;
67
+ if (patch.progress !== undefined)
68
+ ledgerPatch.progressJson = JSON.stringify(patch.progress);
69
+ patchLedgerRun(db, jobId, ledgerPatch);
70
+ return getAuditJob(db, jobId);
29
71
  }
30
- export function setAuditJobProgress(jobId, progress) {
31
- const cur = jobs.get(jobId);
32
- if (!cur)
72
+ export function setAuditJobProgress(db, jobId, progress) {
73
+ if (!getLedgerRun(db, jobId))
33
74
  return;
34
- jobs.set(jobId, { ...cur, progress });
75
+ patchLedgerRun(db, jobId, { progressJson: JSON.stringify(progress) });
35
76
  }
36
77
  //# sourceMappingURL=auditJobService.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auditJobService.js","sourceRoot":"","sources":["../../src/services/auditJobService.ts"],"names":[],"mappings":"AAAA;;GAEG;AAyBH,MAAM,IAAI,GAAG,IAAI,GAAG,EAA0B,CAAC;AAE/C,SAAS,QAAQ;IACf,OAAO,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,YAAoB;IACjD,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,GAAG,GAAmB;QAC1B,KAAK;QACL,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,YAAY;QAClB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACrB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,KAA8B;IACzE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,IAAI,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IAClC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACtB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,QAA0B;IAC3E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;AACxC,CAAC"}
1
+ {"version":3,"file":"auditJobService.js","sourceRoot":"","sources":["../../src/services/auditJobService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * File accounting accumulator (Spec 44).
3
+ *
4
+ * Makes file handling *total*: every file the discovery walk touches lands in
5
+ * exactly one terminal state — `analyzed` (reached >= 1 stage-2 visitor),
6
+ * `partially analyzed` (dropped at stage 2 as `no adapter` / `no visitor
7
+ * matched` yet still consumed/read by a later layer — the style indexer or a
8
+ * stage-3 `readSource`), or `dropped` (with one of eight named reasons) — and
9
+ * the accounting must balance
10
+ * (`analyzed + partiallyAnalyzed + dropped === touched`), asserted at the end
11
+ * of the run.
12
+ *
13
+ * The accumulator is owned by the run and threaded discovery -> stage 1 ->
14
+ * stage 2. Files inside infrastructure directories (node_modules, .git, …) are
15
+ * NOT in the universe — they are recorded as an aggregate `infraPruned` line,
16
+ * never per-file. Reason 8 (`unsupported dialect`) is a sub-layer: style-level
17
+ * unread sources that do NOT affect the file-level balance, reported under the
18
+ * reason in metadata but excluded from `dropped`.
19
+ */
20
+ import type { FileDropReason, FileAccountingSummary, FileAccountingFileEntry } from '../types.js';
21
+ /** Detail attached to a drop (everything except the filePath, which is the key). */
22
+ export type FileDropDetail = Omit<FileAccountingFileEntry, 'filePath'>;
23
+ /**
24
+ * Thrown when the accounting does not balance: a file was touched but never
25
+ * classified (a silent-drop leak), or classified more than once. Surfaced by
26
+ * `assertBalanced()` at the end of the run and converted to a non-zero exit.
27
+ */
28
+ export declare class AccountingBalanceError extends Error {
29
+ readonly name = "AccountingBalanceError";
30
+ constructor(message: string);
31
+ }
32
+ export declare class FileAccounting {
33
+ private readonly entries;
34
+ private readonly infraPrunedMap;
35
+ private readonly unsupportedDialects;
36
+ private readonly conflicts;
37
+ /** Mark a candidate file as part of the touched universe. */
38
+ recordTouched(filePath: string): void;
39
+ /** Mark a file as analyzed (its tuple reached >= 1 stage-2 visitor). */
40
+ recordAnalyzed(filePath: string): void;
41
+ /** Mark a file as dropped with exactly one of the eight reasons. */
42
+ recordDropped(reason: FileDropReason, filePath: string, detail?: FileDropDetail): void;
43
+ /**
44
+ * Sanctioned transition used only for the size-threshold path: an oversized
45
+ * `.sql` orphan reaches the schema-sql visitor (and is therefore first
46
+ * recorded `analyzed`) before being skipped. Reclassify it as dropped.
47
+ */
48
+ reclassifyAnalyzedToDropped(reason: FileDropReason, filePath: string, detail?: FileDropDetail): void;
49
+ /**
50
+ * Reclassify a file dropped at stage 2 as "partially analyzed": it reached
51
+ * zero stage-2 visitors (hence `no adapter` / `no visitor matched`) yet was
52
+ * still *consumed* (read) by a later layer — the style indexer readFileSyncs
53
+ * every discovered file, stage-3 reducers pull source via `readSource`. The
54
+ * trigger is consumption, not output: a clean file the indexer read is just as
55
+ * "partially analyzed" as one that happened to produce findings. The original
56
+ * reason is retained — it still explains why the file wasn't *fully* analyzed.
57
+ *
58
+ * Only `no adapter` and `no visitor matched` are reclassifiable: the other
59
+ * drop reasons mean the file never reached (directory pruned, extension not
60
+ * known, parse failed) or was deliberately removed from (size threshold, path
61
+ * profile excluded) every analysis layer, so a read from one of them would be
62
+ * a genuine accounting bug, not a partial analysis. No-ops otherwise, so the
63
+ * pipeline can call it blindly over every consumed file path.
64
+ */
65
+ reclassifyDroppedToPartiallyAnalyzed(filePath: string): void;
66
+ /** Record an infrastructure directory prune (aggregate, not per-file). */
67
+ recordInfraPruned(directory: string, rule: string): void;
68
+ /**
69
+ * Reason 8 sub-layer: a style-level unread source (Spec 42 R2). Recorded in
70
+ * the summary under `unsupported dialect` but does NOT touch the file-level
71
+ * `analyzed + dropped === touched` balance.
72
+ */
73
+ recordUnsupportedDialect(filePath: string, reason?: string): void;
74
+ /**
75
+ * Verify the balance. Throws `AccountingBalanceError` if any file was touched
76
+ * but never classified (a leak) or classified more than once. Called at the
77
+ * end of the run before metadata assembly.
78
+ */
79
+ assertBalanced(): void;
80
+ /** Produce the summary (touched/analyzed/partiallyAnalyzed/dropped counts + per-reason lists). */
81
+ summary(): FileAccountingSummary;
82
+ }
83
+ //# sourceMappingURL=fileAccounting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileAccounting.d.ts","sourceRoot":"","sources":["../../src/services/fileAccounting.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAElG,oFAAoF;AACpF,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC,CAAC;AAUvE;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,SAAkB,IAAI,4BAA4B;IAClD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACF;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+E;IAC9G,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAoD;IACxF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAE1C,6DAA6D;IAC7D,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIpC;IAED,wEAAwE;IACxE,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYrC;IAED,oEAAoE;IACpE,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAmBrF;IAED;;;;OAIG;IACH,2BAA2B,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CASnG;IAED;;;;;;;;;;;;;;;OAeG;IACH,oCAAoC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAK3D;IAED,0EAA0E;IAC1E,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAKvD;IAED;;;;OAIG;IACH,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAEhE;IAED;;;;OAIG;IACH,cAAc,IAAI,IAAI,CAqBrB;IAED,kGAAkG;IAClG,OAAO,IAAI,qBAAqB,CAmD/B;CACF"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * File accounting accumulator (Spec 44).
3
+ *
4
+ * Makes file handling *total*: every file the discovery walk touches lands in
5
+ * exactly one terminal state — `analyzed` (reached >= 1 stage-2 visitor),
6
+ * `partially analyzed` (dropped at stage 2 as `no adapter` / `no visitor
7
+ * matched` yet still consumed/read by a later layer — the style indexer or a
8
+ * stage-3 `readSource`), or `dropped` (with one of eight named reasons) — and
9
+ * the accounting must balance
10
+ * (`analyzed + partiallyAnalyzed + dropped === touched`), asserted at the end
11
+ * of the run.
12
+ *
13
+ * The accumulator is owned by the run and threaded discovery -> stage 1 ->
14
+ * stage 2. Files inside infrastructure directories (node_modules, .git, …) are
15
+ * NOT in the universe — they are recorded as an aggregate `infraPruned` line,
16
+ * never per-file. Reason 8 (`unsupported dialect`) is a sub-layer: style-level
17
+ * unread sources that do NOT affect the file-level balance, reported under the
18
+ * reason in metadata but excluded from `dropped`.
19
+ */
20
+ /**
21
+ * Thrown when the accounting does not balance: a file was touched but never
22
+ * classified (a silent-drop leak), or classified more than once. Surfaced by
23
+ * `assertBalanced()` at the end of the run and converted to a non-zero exit.
24
+ */
25
+ export class AccountingBalanceError extends Error {
26
+ name = 'AccountingBalanceError';
27
+ constructor(message) {
28
+ super(message);
29
+ }
30
+ }
31
+ export class FileAccounting {
32
+ entries = new Map();
33
+ infraPrunedMap = new Map();
34
+ unsupportedDialects = [];
35
+ conflicts = [];
36
+ /** Mark a candidate file as part of the touched universe. */
37
+ recordTouched(filePath) {
38
+ if (!this.entries.has(filePath)) {
39
+ this.entries.set(filePath, { state: 'touched' });
40
+ }
41
+ }
42
+ /** Mark a file as analyzed (its tuple reached >= 1 stage-2 visitor). */
43
+ recordAnalyzed(filePath) {
44
+ const entry = this.entries.get(filePath);
45
+ if (!entry) {
46
+ this.entries.set(filePath, { state: 'analyzed' });
47
+ return;
48
+ }
49
+ if (entry.state === 'analyzed')
50
+ return; // idempotent — a file can reach several visitors
51
+ if (entry.state === 'dropped') {
52
+ this.conflicts.push(`${filePath}: analyzed after dropped(${entry.reason})`);
53
+ return;
54
+ }
55
+ entry.state = 'analyzed';
56
+ }
57
+ /** Mark a file as dropped with exactly one of the eight reasons. */
58
+ recordDropped(reason, filePath, detail) {
59
+ const entry = this.entries.get(filePath);
60
+ if (!entry) {
61
+ this.entries.set(filePath, { state: 'dropped', reason, detail });
62
+ return;
63
+ }
64
+ if (entry.state === 'dropped') {
65
+ if (entry.reason !== reason) {
66
+ this.conflicts.push(`${filePath}: dropped(${reason}) after dropped(${entry.reason})`);
67
+ }
68
+ return;
69
+ }
70
+ if (entry.state === 'analyzed') {
71
+ this.conflicts.push(`${filePath}: dropped(${reason}) after analyzed`);
72
+ return;
73
+ }
74
+ entry.state = 'dropped';
75
+ entry.reason = reason;
76
+ entry.detail = detail;
77
+ }
78
+ /**
79
+ * Sanctioned transition used only for the size-threshold path: an oversized
80
+ * `.sql` orphan reaches the schema-sql visitor (and is therefore first
81
+ * recorded `analyzed`) before being skipped. Reclassify it as dropped.
82
+ */
83
+ reclassifyAnalyzedToDropped(reason, filePath, detail) {
84
+ const entry = this.entries.get(filePath);
85
+ if (!entry || entry.state !== 'analyzed') {
86
+ this.conflicts.push(`${filePath}: reclassify(${reason}) when state=${entry?.state ?? 'absent'}`);
87
+ return;
88
+ }
89
+ entry.state = 'dropped';
90
+ entry.reason = reason;
91
+ entry.detail = detail;
92
+ }
93
+ /**
94
+ * Reclassify a file dropped at stage 2 as "partially analyzed": it reached
95
+ * zero stage-2 visitors (hence `no adapter` / `no visitor matched`) yet was
96
+ * still *consumed* (read) by a later layer — the style indexer readFileSyncs
97
+ * every discovered file, stage-3 reducers pull source via `readSource`. The
98
+ * trigger is consumption, not output: a clean file the indexer read is just as
99
+ * "partially analyzed" as one that happened to produce findings. The original
100
+ * reason is retained — it still explains why the file wasn't *fully* analyzed.
101
+ *
102
+ * Only `no adapter` and `no visitor matched` are reclassifiable: the other
103
+ * drop reasons mean the file never reached (directory pruned, extension not
104
+ * known, parse failed) or was deliberately removed from (size threshold, path
105
+ * profile excluded) every analysis layer, so a read from one of them would be
106
+ * a genuine accounting bug, not a partial analysis. No-ops otherwise, so the
107
+ * pipeline can call it blindly over every consumed file path.
108
+ */
109
+ reclassifyDroppedToPartiallyAnalyzed(filePath) {
110
+ const entry = this.entries.get(filePath);
111
+ if (!entry || entry.state !== 'dropped')
112
+ return;
113
+ if (entry.reason !== 'no adapter' && entry.reason !== 'no visitor matched')
114
+ return;
115
+ entry.state = 'partially analyzed';
116
+ }
117
+ /** Record an infrastructure directory prune (aggregate, not per-file). */
118
+ recordInfraPruned(directory, rule) {
119
+ const key = `${directory}::${rule}`;
120
+ const existing = this.infraPrunedMap.get(key);
121
+ if (existing)
122
+ existing.directories++;
123
+ else
124
+ this.infraPrunedMap.set(key, { directory, rule, directories: 1 });
125
+ }
126
+ /**
127
+ * Reason 8 sub-layer: a style-level unread source (Spec 42 R2). Recorded in
128
+ * the summary under `unsupported dialect` but does NOT touch the file-level
129
+ * `analyzed + dropped === touched` balance.
130
+ */
131
+ recordUnsupportedDialect(filePath, reason) {
132
+ this.unsupportedDialects.push({ filePath, reason });
133
+ }
134
+ /**
135
+ * Verify the balance. Throws `AccountingBalanceError` if any file was touched
136
+ * but never classified (a leak) or classified more than once. Called at the
137
+ * end of the run before metadata assembly.
138
+ */
139
+ assertBalanced() {
140
+ const leaked = [];
141
+ for (const [filePath, entry] of this.entries) {
142
+ if (entry.state === 'touched')
143
+ leaked.push(filePath);
144
+ }
145
+ if (leaked.length === 0 && this.conflicts.length === 0)
146
+ return;
147
+ const lines = [];
148
+ if (leaked.length > 0) {
149
+ lines.push(`${leaked.length} file(s) touched but never classified (silent-drop leak):`);
150
+ leaked.sort();
151
+ for (const f of leaked.slice(0, 20))
152
+ lines.push(` - ${f}`);
153
+ if (leaked.length > 20)
154
+ lines.push(` … and ${leaked.length - 20} more`);
155
+ }
156
+ if (this.conflicts.length > 0) {
157
+ lines.push(`${this.conflicts.length} file(s) classified more than once:`);
158
+ for (const c of this.conflicts.slice(0, 20))
159
+ lines.push(` - ${c}`);
160
+ if (this.conflicts.length > 20)
161
+ lines.push(` … and ${this.conflicts.length - 20} more`);
162
+ }
163
+ throw new AccountingBalanceError(lines.join('\n'));
164
+ }
165
+ /** Produce the summary (touched/analyzed/partiallyAnalyzed/dropped counts + per-reason lists). */
166
+ summary() {
167
+ let analyzed = 0;
168
+ let partiallyAnalyzed = 0;
169
+ let dropped = 0;
170
+ const reasons = new Map();
171
+ for (const [filePath, entry] of this.entries) {
172
+ if (entry.state === 'analyzed') {
173
+ analyzed++;
174
+ }
175
+ else if ((entry.state === 'dropped' || entry.state === 'partially analyzed') &&
176
+ entry.reason) {
177
+ if (entry.state === 'partially analyzed')
178
+ partiallyAnalyzed++;
179
+ else
180
+ dropped++;
181
+ let list = reasons.get(entry.reason);
182
+ if (!list) {
183
+ list = [];
184
+ reasons.set(entry.reason, list);
185
+ }
186
+ const detail = entry.detail ?? {};
187
+ list.push({
188
+ filePath,
189
+ ...detail,
190
+ ...(entry.state === 'partially analyzed' ? { partial: true } : {}),
191
+ });
192
+ }
193
+ }
194
+ const reasonsOut = {};
195
+ for (const [reason, list] of reasons) {
196
+ list.sort((a, b) => a.filePath.localeCompare(b.filePath));
197
+ reasonsOut[reason] = { count: list.length, files: list };
198
+ }
199
+ if (this.unsupportedDialects.length > 0) {
200
+ const list = this.unsupportedDialects
201
+ .map(({ filePath, reason }) => ({ filePath, ...(reason !== undefined ? { reason } : {}) }))
202
+ .sort((a, b) => a.filePath.localeCompare(b.filePath));
203
+ reasonsOut['unsupported dialect'] = { count: list.length, files: list };
204
+ }
205
+ return {
206
+ touched: this.entries.size,
207
+ analyzed,
208
+ partiallyAnalyzed,
209
+ dropped,
210
+ infraPruned: [...this.infraPrunedMap.values()].sort((a, b) => b.directories - a.directories || a.directory.localeCompare(b.directory)),
211
+ reasons: reasonsOut,
212
+ };
213
+ }
214
+ }
215
+ //# sourceMappingURL=fileAccounting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileAccounting.js","sourceRoot":"","sources":["../../src/services/fileAccounting.ts"],"names":[],"mappings":"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"}
@@ -11,7 +11,7 @@
11
11
  * Single entry point: extractDeclarations(filePath, adapter, sourceCode, ast?)
12
12
  */
13
13
  import type { AST, LanguageAdapter } from '../languages/types.js';
14
- import type { NormalizedDeclaration, StyleMechanism, StyleToken } from './types.js';
14
+ import type { NormalizedDeclaration, StyleMechanism, StyleToken, UnreadStyleSource } from './types.js';
15
15
  import type { TailwindThemeTokens } from './tailwindConfigLoader.js';
16
16
  /**
17
17
  * Extract all style declarations from a file.
@@ -25,7 +25,7 @@ import type { TailwindThemeTokens } from './tailwindConfigLoader.js';
25
25
  * @param ast - Optional pre-parsed AST (avoids re-parsing)
26
26
  * @param tailwindTokens - Optional pre-loaded Tailwind tokens (avoids re-loading per file)
27
27
  */
28
- export declare function extractDeclarations(filePath: string, adapter: LanguageAdapter, sourceCode: string, ast?: AST, tailwindTokens?: TailwindThemeTokens): NormalizedDeclaration[];
28
+ export declare function extractDeclarations(filePath: string, adapter: LanguageAdapter, sourceCode: string, ast?: AST, tailwindTokens?: TailwindThemeTokens, unreadSources?: UnreadStyleSource[]): NormalizedDeclaration[];
29
29
  /**
30
30
  * Extract CSS custom property definitions from all CSS/SCSS files
31
31
  * and produce design tokens.
@@ -1 +1 @@
1
- {"version":3,"file":"styleExtractor.d.ts","sourceRoot":"","sources":["../../src/styles/styleExtractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAW,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAIpF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAMrE;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,GAAG,EACT,cAAc,CAAC,EAAE,mBAAmB,GACnC,qBAAqB,EAAE,CAsBzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,UAAU,EAAE,CAwBd;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,mBAAmB,CAGrB;AA0ND,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,YAAY,EAAE,qBAAqB,EAAE,EACrC,QAAQ,EAAE,MAAM,GACf,IAAI,CAuFN"}
1
+ {"version":3,"file":"styleExtractor.d.ts","sourceRoot":"","sources":["../../src/styles/styleExtractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAW,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAIvG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAOrE;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,GAAG,EACT,cAAc,CAAC,EAAE,mBAAmB,EACpC,aAAa,CAAC,EAAE,iBAAiB,EAAE,GAClC,qBAAqB,EAAE,CAuCzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,UAAU,EAAE,CAwBd;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,mBAAmB,CAGrB;AA2ND,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,YAAY,EAAE,qBAAqB,EAAE,EACrC,QAAQ,EAAE,MAAM,GACf,IAAI,CAuFN"}
@@ -13,6 +13,7 @@
13
13
  import { normalizeValue, expandShorthand } from './normalizer.js';
14
14
  import { expandUtility } from './tailwindExpander.js';
15
15
  import { loadTailwindConfig } from './tailwindConfigLoader.js';
16
+ import { STYLE_MARKUP_EXTENSIONS, KNOWN_SOURCE_EXTENSIONS } from '../utils/fileDiscovery.js';
16
17
  // ---------------------------------------------------------------------------
17
18
  // Public API
18
19
  // ---------------------------------------------------------------------------
@@ -28,8 +29,16 @@ import { loadTailwindConfig } from './tailwindConfigLoader.js';
28
29
  * @param ast - Optional pre-parsed AST (avoids re-parsing)
29
30
  * @param tailwindTokens - Optional pre-loaded Tailwind tokens (avoids re-loading per file)
30
31
  */
31
- export function extractDeclarations(filePath, adapter, sourceCode, ast, tailwindTokens) {
32
+ export function extractDeclarations(filePath, adapter, sourceCode, ast, tailwindTokens, unreadSources) {
32
33
  const ext = filePath.includes('.') ? filePath.slice(filePath.lastIndexOf('.')) : '';
34
+ // Markup/component files — extract class attributes + embedded <style> blocks.
35
+ // Membership is derived from the shared STYLE_MARKUP_EXTENSIONS (single source
36
+ // of truth) so a newly supported dialect is handled here automatically rather
37
+ // than silently dropped from a hand-maintained `case` list (the `.astro`
38
+ // silent-drop bug, Spec 42 R2).
39
+ if (STYLE_MARKUP_EXTENSIONS.includes(ext)) {
40
+ return extractFromHTML(filePath, sourceCode, tailwindTokens, unreadSources);
41
+ }
33
42
  switch (ext) {
34
43
  // .css files are handled by the AST pipeline (cssAstExtractor.ts +
35
44
  // createStylesCssVisitor in pipelineAdapters.ts). SCSS stays on the
@@ -40,13 +49,21 @@ export function extractDeclarations(filePath, adapter, sourceCode, ast, tailwind
40
49
  case '.jsx':
41
50
  case '.ts':
42
51
  case '.js':
52
+ case '.mts':
53
+ case '.cts':
54
+ case '.mjs':
55
+ case '.cjs':
43
56
  return extractFromTypeScript(filePath, adapter, sourceCode, ast, tailwindTokens);
44
- case '.html':
45
- case '.vue':
46
- case '.svelte':
47
- // HTML/Vue/Svelte: extract class attributes only (limited support)
48
- return extractFromHTML(filePath, sourceCode, tailwindTokens);
49
57
  default:
58
+ // Not a style-bearing extension. Known source (`.css`/`.scss` — handled by
59
+ // the AST pipeline — plus JSON/Go/SQL/TOML/Prisma owned by other analyzers)
60
+ // is skipped silently; any *other* extension is unhandled — record it so
61
+ // undefined-class surfaces the gap (whole-run notApplicable) instead of
62
+ // silently dropping the file type. This is the loud backstop for the next
63
+ // dialect that isn't added to the known set.
64
+ if (unreadSources && ext && !KNOWN_SOURCE_EXTENSIONS.includes(ext)) {
65
+ unreadSources.push({ filePath, reason: `unsupported source extension: ${ext}` });
66
+ }
50
67
  return [];
51
68
  }
52
69
  }
@@ -100,7 +117,7 @@ function extractRuleSetsFromSCSS(filePath, sourceCode) {
100
117
  * parsing for declaration extraction. The tree-sitter adapter handles structural
101
118
  * analysis when needed.
102
119
  */
103
- function extractRuleSets(css, filePath, declarations) {
120
+ function extractRuleSets(css, filePath, declarations, mechanism = 'scss') {
104
121
  // Track current context (selector, at-rule)
105
122
  const lines = css.split('\n');
106
123
  // Regex approach: find rule blocks with their selectors
@@ -182,7 +199,7 @@ function extractRuleSets(css, filePath, declarations) {
182
199
  }
183
200
  // Extract declarations inside this block
184
201
  const block = css.slice(braceOpen + 1, braceClose);
185
- extractDeclarationsFromBlock(block, filePath, 'scss', selector, atRuleStack.length > 0 ? atRuleStack.join(', ') : null, declarations, css.slice(0, braceOpen).split('\n').length);
202
+ extractDeclarationsFromBlock(block, filePath, mechanism, selector, atRuleStack.length > 0 ? atRuleStack.join(', ') : null, declarations, css.slice(0, braceOpen).split('\n').length);
186
203
  i = braceClose + 1;
187
204
  }
188
205
  }
@@ -524,8 +541,13 @@ function parseStyleObjectExpression(expr) {
524
541
  // CSS-in-JS (styled-components / emotion)
525
542
  // ---------------------------------------------------------------------------
526
543
  function extractFromCSSinJS(node, sourceCode, filePath, declarations) {
527
- // Tagged template expression: styled.div`...` or css`...`
528
- if (node.type !== 'tagged_template_expression')
544
+ // Tagged templates (styled.div`...` / css`...`) parse as a `call_expression`
545
+ // whose argument is a `template_string` — the WASM grammar emits no
546
+ // `tagged_template_expression` node. Guard on the template argument so a
547
+ // plain function call never reaches the tag-match below.
548
+ if (node.type !== 'call_expression')
549
+ return false;
550
+ if (!findChildByType(node, 'template_string'))
529
551
  return false;
530
552
  const nodeText = sourceCode.slice(node.range[0], node.range[1]);
531
553
  const line = getLine(sourceCode, node.range[0]);
@@ -558,8 +580,12 @@ function extractFromCSSinJS(node, sourceCode, filePath, declarations) {
558
580
  // ---------------------------------------------------------------------------
559
581
  // Sub-extractor: HTML (limited)
560
582
  // ---------------------------------------------------------------------------
561
- function extractFromHTML(filePath, sourceCode, tailwindTokens) {
583
+ function extractFromHTML(filePath, sourceCode, tailwindTokens, unreadSources) {
562
584
  const declarations = [];
585
+ // Embedded <style> blocks (Astro/Vue/Svelte) — extract the CSS text and feed
586
+ // it to the same rule-set parser the .scss path uses (Spec 42 R1). Blocks with
587
+ // an unsupported dialect (sass/less/stylus/…) are recorded as unread (R2).
588
+ extractStyleBlocks(filePath, sourceCode, declarations, unreadSources);
563
589
  // Simple class attribute extraction from HTML
564
590
  const classRegex = /class(Name)?\s*=\s*"([^"]*)"/g;
565
591
  let match;
@@ -620,6 +646,52 @@ function extractFromHTML(filePath, sourceCode, tailwindTokens) {
620
646
  }
621
647
  return declarations;
622
648
  }
649
+ /**
650
+ * Extract `<style>…</style>` blocks from a markup source (Astro/Vue/Svelte).
651
+ *
652
+ * Each block's CSS text is fed to `extractRuleSets`, the same parser the
653
+ * `.scss` path uses, so selectors (including `:global(.foo)`), at-rules, and
654
+ * declarations are extracted identically. The dialect is chosen from the
655
+ * `lang`/`type` attribute:
656
+ * - `css` (or absent) → `css`
657
+ * - `scss` → `scss` (nesting handled by the existing SCSS path)
658
+ * - anything else (`sass`, `less`, `styl`, `postcss`, …) → recorded as an
659
+ * unread source so `undefined-class` never asserts a class defined only here.
660
+ */
661
+ function extractStyleBlocks(filePath, sourceCode, declarations, unreadSources) {
662
+ const styleRegex = /<style\b([^>]*)>([\s\S]*?)<\/style\s*>/gi;
663
+ let match;
664
+ while ((match = styleRegex.exec(sourceCode)) !== null) {
665
+ const attrs = match[1];
666
+ const body = match[2];
667
+ // Index of the first character of the block body (right after the opening
668
+ // `>`), used to map body line numbers back to the source file.
669
+ const bodyStart = match.index + match[0].indexOf('>') + 1;
670
+ const langAttr = attrs.match(/\blang\s*=\s*["']?([a-zA-Z0-9-]+)/);
671
+ const typeAttr = attrs.match(/\btype\s*=\s*["']?([a-zA-Z0-9-]+)/);
672
+ const lang = (langAttr?.[1] ?? typeAttr?.[1] ?? '').toLowerCase();
673
+ let mechanism;
674
+ if (lang === '' || lang === 'css') {
675
+ mechanism = 'css';
676
+ }
677
+ else if (lang === 'scss') {
678
+ mechanism = 'scss';
679
+ }
680
+ else {
681
+ unreadSources?.push({
682
+ filePath,
683
+ reason: `unsupported style dialect: ${lang}`,
684
+ });
685
+ continue;
686
+ }
687
+ // Pad with newlines so that `extractRuleSets`' line arithmetic (which counts
688
+ // newlines up to each `{`) maps onto source-file line numbers. Newlines are
689
+ // whitespace, so selector extraction and at-rule parsing are unaffected.
690
+ const bodyLine0 = sourceCode.slice(0, bodyStart).split('\n').length;
691
+ const padded = '\n'.repeat(bodyLine0 - 1) + body;
692
+ extractRuleSets(padded, filePath, declarations, mechanism);
693
+ }
694
+ }
623
695
  // ---------------------------------------------------------------------------
624
696
  // AST Helpers
625
697
  // ---------------------------------------------------------------------------