code-auditor-mcp 4.0.3 → 4.1.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.
Files changed (157) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +124 -0
  3. package/README.md +5 -5
  4. package/configs/hhra-compat.json +2 -2
  5. package/dist/analyzers/applicability.d.ts +25 -2
  6. package/dist/analyzers/applicability.d.ts.map +1 -1
  7. package/dist/analyzers/applicability.js +44 -50
  8. package/dist/analyzers/applicability.js.map +1 -1
  9. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -49
  10. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.js +22 -240
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  13. package/dist/analyzers/cross-language/SchemaValidator.d.ts +2 -14
  14. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  15. package/dist/analyzers/cross-language/SchemaValidator.js +0 -104
  16. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  17. package/dist/analyzers/orgFilterTiers.d.ts +89 -0
  18. package/dist/analyzers/orgFilterTiers.d.ts.map +1 -0
  19. package/dist/analyzers/orgFilterTiers.js +96 -0
  20. package/dist/analyzers/orgFilterTiers.js.map +1 -0
  21. package/dist/analyzers/ruleRegistry.d.ts +15 -0
  22. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  23. package/dist/analyzers/ruleRegistry.js +139 -185
  24. package/dist/analyzers/ruleRegistry.js.map +1 -1
  25. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +39 -0
  26. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  27. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +62 -41
  28. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  29. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +2 -2
  30. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  31. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +2 -2
  32. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  33. package/dist/analyzers/universal/UniversalSecurityAnalyzer.d.ts +86 -0
  34. package/dist/analyzers/universal/UniversalSecurityAnalyzer.d.ts.map +1 -0
  35. package/dist/analyzers/universal/UniversalSecurityAnalyzer.js +606 -0
  36. package/dist/analyzers/universal/UniversalSecurityAnalyzer.js.map +1 -0
  37. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +14 -0
  38. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  39. package/dist/analyzers/universal/schema/codeAnalysis.js +35 -18
  40. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  41. package/dist/analyzers/universal/schema/migrations.d.ts +14 -1
  42. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
  43. package/dist/analyzers/universal/schema/migrations.js +58 -14
  44. package/dist/analyzers/universal/schema/migrations.js.map +1 -1
  45. package/dist/auditRunner.d.ts.map +1 -1
  46. package/dist/auditRunner.js +70 -13
  47. package/dist/auditRunner.js.map +1 -1
  48. package/dist/churn/churnExtractor.js +5 -5
  49. package/dist/churn/churnExtractor.js.map +1 -1
  50. package/dist/cli.js +40 -9
  51. package/dist/cli.js.map +1 -1
  52. package/dist/codeIndexDB.d.ts +6 -0
  53. package/dist/codeIndexDB.d.ts.map +1 -1
  54. package/dist/codeIndexDB.js +33 -1
  55. package/dist/codeIndexDB.js.map +1 -1
  56. package/dist/config/configGeneratePath.d.ts +19 -0
  57. package/dist/config/configGeneratePath.d.ts.map +1 -0
  58. package/dist/config/configGeneratePath.js +29 -0
  59. package/dist/config/configGeneratePath.js.map +1 -0
  60. package/dist/config/configLoader.d.ts +36 -4
  61. package/dist/config/configLoader.d.ts.map +1 -1
  62. package/dist/config/configLoader.js +136 -15
  63. package/dist/config/configLoader.js.map +1 -1
  64. package/dist/config/lintConfigReader.d.ts +4 -0
  65. package/dist/config/lintConfigReader.d.ts.map +1 -1
  66. package/dist/config/lintConfigReader.js +35 -36
  67. package/dist/config/lintConfigReader.js.map +1 -1
  68. package/dist/config/staticObjectExtract.d.ts +35 -0
  69. package/dist/config/staticObjectExtract.d.ts.map +1 -0
  70. package/dist/config/staticObjectExtract.js +312 -0
  71. package/dist/config/staticObjectExtract.js.map +1 -0
  72. package/dist/constants.d.ts +1 -2
  73. package/dist/constants.d.ts.map +1 -1
  74. package/dist/constants.js +0 -2
  75. package/dist/constants.js.map +1 -1
  76. package/dist/daemon/core.js +1 -1
  77. package/dist/daemon/core.js.map +1 -1
  78. package/dist/daemon/main.js +1 -1
  79. package/dist/daemon/main.js.map +1 -1
  80. package/dist/dataPaths.d.ts +11 -0
  81. package/dist/dataPaths.d.ts.map +1 -1
  82. package/dist/dataPaths.js +24 -2
  83. package/dist/dataPaths.js.map +1 -1
  84. package/dist/generators/BaseConfigGenerator.d.ts +0 -4
  85. package/dist/generators/BaseConfigGenerator.d.ts.map +1 -1
  86. package/dist/generators/BaseConfigGenerator.js +1 -7
  87. package/dist/generators/BaseConfigGenerator.js.map +1 -1
  88. package/dist/installConfig.d.ts +3 -1
  89. package/dist/installConfig.d.ts.map +1 -1
  90. package/dist/installConfig.js +15 -2
  91. package/dist/installConfig.js.map +1 -1
  92. package/dist/installer.d.ts +10 -0
  93. package/dist/installer.d.ts.map +1 -1
  94. package/dist/installer.js +14 -2
  95. package/dist/installer.js.map +1 -1
  96. package/dist/languages/LanguageOrchestrator.js +6 -6
  97. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  98. package/dist/languages/RuntimeManager.d.ts +4 -2
  99. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  100. package/dist/languages/RuntimeManager.js +77 -44
  101. package/dist/languages/RuntimeManager.js.map +1 -1
  102. package/dist/languages/go/{analyzer → analyzer-darwin-amd64} +0 -0
  103. package/dist/languages/go/analyzer-darwin-arm64 +0 -0
  104. package/dist/languages/go/analyzer-linux-amd64 +0 -0
  105. package/dist/languages/go/analyzer-linux-arm64 +0 -0
  106. package/dist/languages/go/analyzer-windows-amd64.exe +0 -0
  107. package/dist/languages/goBinary.d.ts +8 -7
  108. package/dist/languages/goBinary.d.ts.map +1 -1
  109. package/dist/languages/goBinary.js +8 -7
  110. package/dist/languages/goBinary.js.map +1 -1
  111. package/dist/mcp-tools-shared.d.ts.map +1 -1
  112. package/dist/mcp-tools-shared.js +7 -2
  113. package/dist/mcp-tools-shared.js.map +1 -1
  114. package/dist/mcp-ui-simple.d.ts +5 -1
  115. package/dist/mcp-ui-simple.d.ts.map +1 -1
  116. package/dist/mcp-ui-simple.js +81 -29
  117. package/dist/mcp-ui-simple.js.map +1 -1
  118. package/dist/mcp.js +19 -4
  119. package/dist/mcp.js.map +1 -1
  120. package/dist/mcpAuditJobs.d.ts.map +1 -1
  121. package/dist/mcpAuditJobs.js +1 -0
  122. package/dist/mcpAuditJobs.js.map +1 -1
  123. package/dist/nextFileIncremental.js +1 -1
  124. package/dist/nextFileIncremental.js.map +1 -1
  125. package/dist/pipeline.d.ts.map +1 -1
  126. package/dist/pipeline.js +13 -2
  127. package/dist/pipeline.js.map +1 -1
  128. package/dist/pipelineAdapters.d.ts +15 -0
  129. package/dist/pipelineAdapters.d.ts.map +1 -1
  130. package/dist/pipelineAdapters.js +140 -6
  131. package/dist/pipelineAdapters.js.map +1 -1
  132. package/dist/reporting/htmlReportGenerator.js +2 -2
  133. package/dist/reporting/htmlReportGenerator.js.map +1 -1
  134. package/dist/ruleAliases.d.ts.map +1 -1
  135. package/dist/ruleAliases.js +45 -0
  136. package/dist/ruleAliases.js.map +1 -1
  137. package/dist/styles/tailwindConfigLoader.d.ts +4 -0
  138. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  139. package/dist/styles/tailwindConfigLoader.js +54 -36
  140. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  141. package/dist/types.d.ts +55 -2
  142. package/dist/types.d.ts.map +1 -1
  143. package/dist/types.js +15 -0
  144. package/dist/types.js.map +1 -1
  145. package/dist/version.generated.d.ts +1 -1
  146. package/dist/version.generated.js +1 -1
  147. package/examples/.auditrc.example.json +2 -2
  148. package/examples/.auditrc.json +3 -3
  149. package/examples/nextjs.auditrc.json +1 -1
  150. package/examples/node-api.auditrc.json +1 -1
  151. package/examples/react.auditrc.json +2 -2
  152. package/examples/tolerant-solid.auditrc.json +1 -1
  153. package/package.json +2 -1
  154. package/plugin/.claude-plugin/plugin.json +1 -1
  155. package/plugin/scripts/hook-common.sh +27 -27
  156. package/plugin/skills/code-auditor/SKILL.md +1 -1
  157. package/server.json +2 -2
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Tenant-scoping tier resolution (Spec 62 Amendment B).
3
+ *
4
+ * The single source of truth for "does a table require an org/tenant filter?"
5
+ * and "is the missing-org-filter rule applicable to this corpus?". Both the
6
+ * firing predicate (the Stage-4 missing-org-filter reducer) and the
7
+ * applicability predicate (pipeline.ts → applicability.ts) are derived from
8
+ * this one function, so the two can never drift to different tier sets.
9
+ *
10
+ * Three tiers, all read through {@link buildOrgFilterTierSet}:
11
+ *
12
+ * Tier 1 (config-primary) — `orgFilterTables`: the user's explicit list of
13
+ * multi-tenant tables. Tenancy is policy; this is the declaration of record.
14
+ *
15
+ * Tier 2 (schema-inference) — a configured `schemas` table that carries a
16
+ * column matching `orgFilterColumns` (default org_id/tenant_id/
17
+ * organization_id/workspace_id). Makes non-English table names detectable
18
+ * with zero explicit declaration.
19
+ *
20
+ * Tier 3 (DDL-discovery) — a table whose columns, extracted from SQL DDL
21
+ * (CREATE TABLE / ALTER TABLE … ADD COLUMN), carry a tenant-scoping column.
22
+ * This is the tier that was missing from the old firing predicate, which is
23
+ * what made `missing-org-filter` report a DDL-declared multi-tenant codebase
24
+ * `clean` (applicability un-suppressed the rule via Tier 3, but firing only
25
+ * read Tiers 1–2, so nothing fired).
26
+ *
27
+ * The firing predicate and the applicability predicate are NOT separate
28
+ * implementations that "agree by inspection" — they are two consumers of the
29
+ * same {@link OrgFilterTierSet}:
30
+ *
31
+ * - firing: `tableRequiresOrgFilter(queryTables, tierSet)`
32
+ * - applicability: `hasDeclaredTenancy(tierSet)`
33
+ *
34
+ * This module is deliberately free of analyzer and pipeline imports: it is
35
+ * imported statically by applicability.ts (which pipeline.ts imports
36
+ * statically) AND by the Stage-4 reducer in pipelineAdapters.ts (which reaches
37
+ * analyzers only through dynamic import()). Neither import path may pull in
38
+ * pipeline.js or an analyzer class.
39
+ */
40
+ /** Default tenant-scoping column names (lowercased by the consumer). */
41
+ export declare const DEFAULT_ORG_FILTER_COLUMNS: string[];
42
+ /** The loose config shape the tier resolver reads. Compatible with the
43
+ * data-access analyzer's `DataAccessAnalyzerConfig` and with the bare
44
+ * `Record<string, unknown>` the pipeline passes to applicability. */
45
+ export interface OrgFilterConfig {
46
+ orgFilterTables?: string[];
47
+ orgFilterColumns?: string[];
48
+ schemas?: Array<{
49
+ name?: string;
50
+ tables?: Array<{
51
+ name?: string;
52
+ columns?: Array<{
53
+ name?: unknown;
54
+ type?: unknown;
55
+ }>;
56
+ }>;
57
+ }>;
58
+ }
59
+ /** The resolved tenant-scoping picture across all three tiers. */
60
+ export interface OrgFilterTierSet {
61
+ /** Tier 1 — explicit tenant table names, lowercased. */
62
+ orgFilterTables: Set<string>;
63
+ /** Tier 2 — configured-schema tables carrying a tenant column, lowercased. */
64
+ schemaOrgTables: Set<string>;
65
+ /** Tier 3 — DDL-discovered tables carrying a tenant column, lowercased. */
66
+ ddlOrgTables: Set<string>;
67
+ /** The tenant-scoping column names this config treats as evidence, lowercased. */
68
+ tenantColumns: string[];
69
+ }
70
+ /**
71
+ * Build the full tenant-scoping tier set from config + DDL-discovered
72
+ * per-table columns. `ddlTableColumns` is the corpus-wide `Record<table,
73
+ * string[]>` folded by the schema reducer; it is undefined when the schema
74
+ * analyzer was not part of the run (Tier 3 is then empty — the rule can still
75
+ * fire on Tiers 1–2, which come from config alone).
76
+ */
77
+ export declare function buildOrgFilterTierSet(config: OrgFilterConfig | undefined, ddlTableColumns: Record<string, string[]> | undefined): OrgFilterTierSet;
78
+ /**
79
+ * Firing predicate — true when any of the query's referenced tables requires an
80
+ * org/tenant filter, across all three tiers.
81
+ */
82
+ export declare function tableRequiresOrgFilter(tables: string[], tierSet: OrgFilterTierSet): boolean;
83
+ /**
84
+ * Applicability predicate — true when any tier declares tenancy. When false,
85
+ * the rule is `notApplicable` (this corpus has no tenant-scoping input), never
86
+ * `clean` (the strongest claim the tool makes).
87
+ */
88
+ export declare function hasDeclaredTenancy(tierSet: OrgFilterTierSet): boolean;
89
+ //# sourceMappingURL=orgFilterTiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgFilterTiers.d.ts","sourceRoot":"","sources":["../../src/analyzers/orgFilterTiers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,wEAAwE;AACxE,eAAO,MAAM,0BAA0B,UAKtC,CAAC;AAEF;;sEAEsE;AACtE,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,KAAK,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,KAAK,CAAC;YACb,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,OAAO,CAAC,EAAE,KAAK,CAAC;gBAAE,IAAI,CAAC,EAAE,OAAO,CAAC;gBAAC,IAAI,CAAC,EAAE,OAAO,CAAA;aAAE,CAAC,CAAC;SACrD,CAAC,CAAC;KACJ,CAAC,CAAC;CACJ;AAED,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,2EAA2E;IAC3E,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,kFAAkF;IAClF,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,GAAG,SAAS,EACnC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,GACpD,gBAAgB,CA6BlB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAST;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAMrE"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Tenant-scoping tier resolution (Spec 62 Amendment B).
3
+ *
4
+ * The single source of truth for "does a table require an org/tenant filter?"
5
+ * and "is the missing-org-filter rule applicable to this corpus?". Both the
6
+ * firing predicate (the Stage-4 missing-org-filter reducer) and the
7
+ * applicability predicate (pipeline.ts → applicability.ts) are derived from
8
+ * this one function, so the two can never drift to different tier sets.
9
+ *
10
+ * Three tiers, all read through {@link buildOrgFilterTierSet}:
11
+ *
12
+ * Tier 1 (config-primary) — `orgFilterTables`: the user's explicit list of
13
+ * multi-tenant tables. Tenancy is policy; this is the declaration of record.
14
+ *
15
+ * Tier 2 (schema-inference) — a configured `schemas` table that carries a
16
+ * column matching `orgFilterColumns` (default org_id/tenant_id/
17
+ * organization_id/workspace_id). Makes non-English table names detectable
18
+ * with zero explicit declaration.
19
+ *
20
+ * Tier 3 (DDL-discovery) — a table whose columns, extracted from SQL DDL
21
+ * (CREATE TABLE / ALTER TABLE … ADD COLUMN), carry a tenant-scoping column.
22
+ * This is the tier that was missing from the old firing predicate, which is
23
+ * what made `missing-org-filter` report a DDL-declared multi-tenant codebase
24
+ * `clean` (applicability un-suppressed the rule via Tier 3, but firing only
25
+ * read Tiers 1–2, so nothing fired).
26
+ *
27
+ * The firing predicate and the applicability predicate are NOT separate
28
+ * implementations that "agree by inspection" — they are two consumers of the
29
+ * same {@link OrgFilterTierSet}:
30
+ *
31
+ * - firing: `tableRequiresOrgFilter(queryTables, tierSet)`
32
+ * - applicability: `hasDeclaredTenancy(tierSet)`
33
+ *
34
+ * This module is deliberately free of analyzer and pipeline imports: it is
35
+ * imported statically by applicability.ts (which pipeline.ts imports
36
+ * statically) AND by the Stage-4 reducer in pipelineAdapters.ts (which reaches
37
+ * analyzers only through dynamic import()). Neither import path may pull in
38
+ * pipeline.js or an analyzer class.
39
+ */
40
+ /** Default tenant-scoping column names (lowercased by the consumer). */
41
+ export const DEFAULT_ORG_FILTER_COLUMNS = [
42
+ 'org_id',
43
+ 'tenant_id',
44
+ 'organization_id',
45
+ 'workspace_id',
46
+ ];
47
+ /**
48
+ * Build the full tenant-scoping tier set from config + DDL-discovered
49
+ * per-table columns. `ddlTableColumns` is the corpus-wide `Record<table,
50
+ * string[]>` folded by the schema reducer; it is undefined when the schema
51
+ * analyzer was not part of the run (Tier 3 is then empty — the rule can still
52
+ * fire on Tiers 1–2, which come from config alone).
53
+ */
54
+ export function buildOrgFilterTierSet(config, ddlTableColumns) {
55
+ const tenantColumns = (config?.orgFilterColumns ?? DEFAULT_ORG_FILTER_COLUMNS).map((c) => String(c).toLowerCase());
56
+ const tenantSet = new Set(tenantColumns);
57
+ const orgFilterTables = new Set((config?.orgFilterTables ?? []).map((t) => String(t).toLowerCase()));
58
+ const schemaOrgTables = new Set();
59
+ for (const schema of config?.schemas ?? []) {
60
+ for (const table of schema.tables ?? []) {
61
+ if ((table.columns ?? []).some((c) => tenantSet.has(String(c.name).toLowerCase()))) {
62
+ schemaOrgTables.add(String(table.name).toLowerCase());
63
+ }
64
+ }
65
+ }
66
+ const ddlOrgTables = new Set();
67
+ for (const [table, columns] of Object.entries(ddlTableColumns ?? {})) {
68
+ if ((columns ?? []).some((c) => tenantSet.has(String(c).toLowerCase()))) {
69
+ ddlOrgTables.add(String(table).toLowerCase());
70
+ }
71
+ }
72
+ return { orgFilterTables, schemaOrgTables, ddlOrgTables, tenantColumns };
73
+ }
74
+ /**
75
+ * Firing predicate — true when any of the query's referenced tables requires an
76
+ * org/tenant filter, across all three tiers.
77
+ */
78
+ export function tableRequiresOrgFilter(tables, tierSet) {
79
+ return tables.some((t) => {
80
+ const lt = String(t).toLowerCase();
81
+ return (tierSet.orgFilterTables.has(lt) ||
82
+ tierSet.schemaOrgTables.has(lt) ||
83
+ tierSet.ddlOrgTables.has(lt));
84
+ });
85
+ }
86
+ /**
87
+ * Applicability predicate — true when any tier declares tenancy. When false,
88
+ * the rule is `notApplicable` (this corpus has no tenant-scoping input), never
89
+ * `clean` (the strongest claim the tool makes).
90
+ */
91
+ export function hasDeclaredTenancy(tierSet) {
92
+ return (tierSet.orgFilterTables.size > 0 ||
93
+ tierSet.schemaOrgTables.size > 0 ||
94
+ tierSet.ddlOrgTables.size > 0);
95
+ }
96
+ //# sourceMappingURL=orgFilterTiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgFilterTiers.js","sourceRoot":"","sources":["../../src/analyzers/orgFilterTiers.ts"],"names":[],"mappings":"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"}
@@ -155,6 +155,21 @@ export declare const ALL_ANALYZERS: readonly string[];
155
155
  * Referenced here so mcp.ts and mcp-tools-shared.ts don't each re-type the list.
156
156
  */
157
157
  export declare const MCP_DEFAULT_ANALYZERS: readonly string[];
158
+ /**
159
+ * Analyzers the pipeline can run that emit *no* registry rule. Their rules come
160
+ * from a runtime source other than {@link RULE_REGISTRY}: `invariants` reads its
161
+ * rules from the `.codeauditor.json` `rules` array (the seven invariant rule
162
+ * kinds), not from registry rows, so it is absent from {@link ALL_ANALYZERS}.
163
+ */
164
+ export declare const PIPELINE_ONLY_ANALYZERS: readonly string[];
165
+ /**
166
+ * Every analyzer `enabledAnalyzers` may legitimately name — {@link ALL_ANALYZERS}
167
+ * (registry rule emitters) plus {@link PIPELINE_ONLY_ANALYZERS}. This is the
168
+ * single source of truth for "can this analyzer actually run?" — used by config
169
+ * validation, which must not reject `invariants` (a valid pipeline analyzer that
170
+ * happens to carry no registry rule).
171
+ */
172
+ export declare const RUNNABLE_ANALYZERS: readonly string[];
158
173
  /**
159
174
  * A violation in the loose shape the gate and baseline code handle (a Violation
160
175
  * with the transitional analyzer/type/violationType/etc. fields still present).
@@ -1 +1 @@
1
- {"version":3,"file":"ruleRegistry.d.ts","sourceRoot":"","sources":["../../src/analyzers/ruleRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC/F;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CA4+DrE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAEnC,CAAC;AAET;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAMlD,CAAC;AAEF;;;GAGG;AACH,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,aAAa,GAAG,QAAQ,CAAC,iBAAiB,CAAC,GAAG,SAAS,CAQ/F"}
1
+ {"version":3,"file":"ruleRegistry.d.ts","sourceRoot":"","sources":["../../src/analyzers/ruleRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC/F;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,iBAAiB,CAAC,CA26DrE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAEnC,CAAC;AAET;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAOlD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAAmB,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAExC,CAAC;AAET;;;GAGG;AACH,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,aAAa,GAAG,QAAQ,CAAC,iBAAiB,CAAC,GAAG,SAAS,CAQ/F"}
@@ -43,7 +43,7 @@ export const RULE_REGISTRY = {
43
43
  { code: 'function simple(x) {\n if (x > 0) return x;\n return -x;\n}', nearMiss: true },
44
44
  ],
45
45
  invalid: [
46
- { code: 'function complex(x) {\n if (a && b) { if (c) { while (d) { if (e) return 1; } } }\n if (f || g) { for (;;) { if (h) break; } }\n return 0;\n}' },
46
+ { code: 'function complex(x) {\n if (x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x && x) return 1;\n return 0;\n}' },
47
47
  ],
48
48
  },
49
49
  },
@@ -142,7 +142,7 @@ export const RULE_REGISTRY = {
142
142
  { code: 'interface Printer {\n print(doc) { return doc; }\n}', nearMiss: true },
143
143
  ],
144
144
  invalid: [
145
- { code: 'interface Machine {\n op1() {}\n op2() {}\n op3() {}\n op4() {}\n op5() {}\n op6() {}\n op7() {}\n op8() {}\n op9() {}\n op10() {}\n op11() {}\n op12() {}\n op13() {}\n op14() {}\n op15() {}\n op16() {}\n op17() {}\n op18() {}\n op19() {}\n op20() {}\n op21() {}\n op22() {}\n op23() {}\n op24() {}\n op25() {}\n op26() {}\n}' },
145
+ { code: 'interface Machine {\n op1(): void;\n op2(): void;\n op3(): void;\n op4(): void;\n op5(): void;\n op6(): void;\n op7(): void;\n op8(): void;\n op9(): void;\n op10(): void;\n op11(): void;\n op12(): void;\n op13(): void;\n op14(): void;\n op15(): void;\n op16(): void;\n op17(): void;\n op18(): void;\n op19(): void;\n op20(): void;\n op21(): void;\n op22(): void;\n op23(): void;\n op24(): void;\n op25(): void;\n op26(): void;\n}' },
146
146
  ],
147
147
  },
148
148
  },
@@ -395,8 +395,13 @@ export const RULE_REGISTRY = {
395
395
  ],
396
396
  invalid: [
397
397
  {
398
- code: 'function a() {\n const x = compute(1);\n const y = compute(2);\n return x + y;\n}\nfunction b() {\n const x = compute(1);\n const y = compute(2);\n return x + y;\n}',
399
- resolution: { action: 'extract-shared', summary: 'Extract the duplicated block into a shared helper and call it from both sites.', symbols: ['a', 'b'] },
398
+ // Two IDENTICAL ≥15-line for-loops — `dry/duplicate` is an *exact*
399
+ // token match (normalizeCode keeps identifiers), so the two blocks must
400
+ // be byte-identical, not merely same-shape. The old sample (`function a`
401
+ // / `function b` with identical bodies) was doubly dead: its names differ
402
+ // (no exact hash match) and it was under minLineThreshold.
403
+ code: 'function process(rows) {\n for (const row of rows) {\n const id = row.id;\n const name = row.name;\n const value = row.value;\n const category = row.category;\n const tags = row.tags;\n const meta = row.meta;\n const score = computeScore(row);\n const rank = computeRank(score);\n const label = formatLabel(name);\n const bucket = assignBucket(rank);\n const flags = extractFlags(row);\n const audit = buildAudit(flags);\n const record = { id, name, value, category, tags, meta, score, rank, label, bucket, audit };\n push(record);\n notify(record.id);\n }\n for (const row of rows) {\n const id = row.id;\n const name = row.name;\n const value = row.value;\n const category = row.category;\n const tags = row.tags;\n const meta = row.meta;\n const score = computeScore(row);\n const rank = computeRank(score);\n const label = formatLabel(name);\n const bucket = assignBucket(rank);\n const flags = extractFlags(row);\n const audit = buildAudit(flags);\n const record = { id, name, value, category, tags, meta, score, rank, label, bucket, audit };\n push(record);\n notify(record.id);\n }\n}',
404
+ resolution: { action: 'extract-shared', summary: 'Extract the duplicated block into a shared helper and call it from both sites.', symbols: ['process'] },
400
405
  },
401
406
  ],
402
407
  },
@@ -415,7 +420,14 @@ export const RULE_REGISTRY = {
415
420
  { code: 'function a(x) {\n return x + 1;\n}\nfunction b(x) {\n return x * 2;\n}', nearMiss: true },
416
421
  ],
417
422
  invalid: [
418
- { code: 'function a(x) {\n return fetch(x).then((r) => r.json());\n}\nfunction b(y) {\n return fetch(y).then((r) => r.json());\n}' },
423
+ {
424
+ // Two structurally-identical ≥15-line functions with different
425
+ // identifiers/literals. `dry/structural-similarity` compares the
426
+ // token-kind skeleton (normalizeCodeForStructure), so the two must share
427
+ // structure but differ in names — the old two-line `fetch().then()`
428
+ // pair was under minLineThreshold and never emitted.
429
+ code: 'function computeA(rows) {\n for (const item of rows) {\n const id = item.id;\n const name = item.name;\n const value = item.value;\n const category = item.category;\n const tags = item.tags;\n const meta = item.meta;\n const score = scoreA(item);\n const rank = rankA(score);\n const label = labelA(name);\n const bucket = bucketA(rank);\n const flags = flagsA(item);\n const audit = auditA(flags);\n const record = { id, name, value, category, tags, meta, score, rank, label, bucket, audit };\n appendA(record);\n emitA(record.id);\n }\n}\nfunction computeB(rows) {\n for (const entry of rows) {\n const id = entry.id;\n const name = entry.name;\n const value = entry.value;\n const category = entry.category;\n const tags = entry.tags;\n const meta = entry.meta;\n const score = scoreB(entry);\n const rank = rankB(score);\n const label = labelB(name);\n const bucket = bucketB(rank);\n const flags = flagsB(entry);\n const audit = auditB(flags);\n const record = { id, name, value, category, tags, meta, score, rank, label, bucket, audit };\n appendB(record);\n emitB(record.id);\n }\n}',
430
+ },
419
431
  ],
420
432
  },
421
433
  },
@@ -536,11 +548,16 @@ export const RULE_REGISTRY = {
536
548
  },
537
549
  },
538
550
  'missing-org-filter': {
539
- analyzer: 'data-access',
551
+ analyzer: 'data-access-org-filter',
540
552
  field: 'rule',
541
- input: ['schema'],
542
- resolvable: false,
543
- message: 'Query on {tables} is missing an organization/tenant filter.',
553
+ input: ['data-access', 'schema'],
554
+ resolvable: true,
555
+ // The claim is "no organization/tenant *predicate*", NOT "no filter". A
556
+ // query scoped by primary key (`WHERE id = $1`) still fires, because it is
557
+ // scoped by id but not by tenant — an IDOR surface if the id is
558
+ // request-reachable. The fix is to add the tenant column to the predicate,
559
+ // not to add a filter (one already exists in the PK-scoped case).
560
+ message: 'Query on {tables} has no organization/tenant predicate.',
544
561
  docs: 'missing-org-filter',
545
562
  thresholds: [],
546
563
  samples: {
@@ -548,7 +565,14 @@ export const RULE_REGISTRY = {
548
565
  { code: 'db.query("SELECT * FROM projects WHERE org_id = ?", [orgId])', nearMiss: true },
549
566
  ],
550
567
  invalid: [
551
- { code: 'db.query("SELECT * FROM projects WHERE id = ?", [id])' },
568
+ {
569
+ code: 'db.query("SELECT * FROM projects WHERE id = ?", [id])',
570
+ resolution: {
571
+ action: 'add-tenant-predicate',
572
+ summary: 'Add the tenant column (organization_id / org_id) to the WHERE predicate so the query is scoped to the current organization, not just by primary key.',
573
+ symbols: ['db', 'query'],
574
+ },
575
+ },
552
576
  ],
553
577
  },
554
578
  },
@@ -575,7 +599,7 @@ export const RULE_REGISTRY = {
575
599
  field: 'rule',
576
600
  input: ['files'],
577
601
  resolvable: false,
578
- message: 'Unfiltered write on {tables} has no WHERE/HAVING/LIMIT.',
602
+ message: 'Unfiltered write or tenant-scoped read on {tables} has no WHERE/HAVING/LIMIT.',
579
603
  docs: 'unfiltered-query',
580
604
  thresholds: [],
581
605
  samples: {
@@ -720,10 +744,21 @@ export const RULE_REGISTRY = {
720
744
  thresholds: [],
721
745
  samples: {
722
746
  valid: [
723
- { code: '/**\n * Computes.\n * @returns the result\n */\nfunction compute() {}', nearMiss: true },
747
+ {
748
+ // return-documentation only applies to a typed, non-void return
749
+ // (`func.returnType && !== 'void'`). The old sample returned nothing,
750
+ // so the guard was vacuous; this one returns `number` and documents it.
751
+ code: '/**\n * Computes the result.\n * @returns the result.\n */\nfunction compute(): number { return 1; }',
752
+ nearMiss: true,
753
+ },
724
754
  ],
725
755
  invalid: [
726
- { code: '/** Computes. */\nfunction compute() {}' },
756
+ {
757
+ // Missing @returns on a typed, non-void function. The old sample
758
+ // (`function compute() {}`) had no return type, so the return-doc guard
759
+ // never applied.
760
+ code: '/** Computes the result. */\nfunction compute(): number { return 1; }',
761
+ },
727
762
  ],
728
763
  },
729
764
  },
@@ -848,23 +883,6 @@ export const RULE_REGISTRY = {
848
883
  ],
849
884
  },
850
885
  },
851
- 'file-error': {
852
- analyzer: 'schema',
853
- field: 'rule',
854
- input: ['schema-json'],
855
- resolvable: false,
856
- message: 'Error processing schema file: {error}.',
857
- docs: 'file-error',
858
- thresholds: [],
859
- samples: {
860
- valid: [
861
- { code: '{"valid": true}', nearMiss: true },
862
- ],
863
- invalid: [
864
- { code: 'not a readable schema' },
865
- ],
866
- },
867
- },
868
886
  'type-mismatch': {
869
887
  analyzer: 'schema',
870
888
  field: 'rule',
@@ -1214,7 +1232,13 @@ export const RULE_REGISTRY = {
1214
1232
  { code: 'function Simple({ x }) {\n return <div>{x}</div>;\n}', nearMiss: true },
1215
1233
  ],
1216
1234
  invalid: [
1217
- { code: 'function Complex(props) {\n if (a) { if (b) { if (c) { if (d) { return <X/>; } } } }\n if (e) { if (f) { return <Y/>; } }\n return <Z/>;\n}' },
1235
+ {
1236
+ // 22 `if` statements — component complexity (base 1 + one per branch)
1237
+ // must exceed maxComponentComplexity (20). The old sample had 6
1238
+ // branches (complexity 7), under the recalibrated ceiling, so it never
1239
+ // fired.
1240
+ code: 'function Complex(props) {\n if (props.x === 1) return <C1 />;\n if (props.x === 2) return <C2 />;\n if (props.x === 3) return <C3 />;\n if (props.x === 4) return <C4 />;\n if (props.x === 5) return <C5 />;\n if (props.x === 6) return <C6 />;\n if (props.x === 7) return <C7 />;\n if (props.x === 8) return <C8 />;\n if (props.x === 9) return <C9 />;\n if (props.x === 10) return <C10 />;\n if (props.x === 11) return <C11 />;\n if (props.x === 12) return <C12 />;\n if (props.x === 13) return <C13 />;\n if (props.x === 14) return <C14 />;\n if (props.x === 15) return <C15 />;\n if (props.x === 16) return <C16 />;\n if (props.x === 17) return <C17 />;\n if (props.x === 18) return <C18 />;\n if (props.x === 19) return <C19 />;\n if (props.x === 20) return <C20 />;\n if (props.x === 21) return <C21 />;\n if (props.x === 22) return <C22 />;\n return <Z />;\n}',
1241
+ },
1218
1242
  ],
1219
1243
  },
1220
1244
  },
@@ -1318,23 +1342,6 @@ export const RULE_REGISTRY = {
1318
1342
  // `clean` (which would assert the tool checked something it no longer checks
1319
1343
  // as a finding).
1320
1344
  // ── schema-validator (SchemaValidator) ──────────────────────────────────
1321
- 'field-mismatch': {
1322
- analyzer: 'schema-validator',
1323
- field: 'rule',
1324
- input: ['cross-language-entities'],
1325
- resolvable: false,
1326
- message: 'Field mismatch: {detail}.',
1327
- docs: 'field-mismatch',
1328
- thresholds: [],
1329
- samples: {
1330
- valid: [
1331
- { code: 'model User { id Int @id }', nearMiss: true },
1332
- ],
1333
- invalid: [
1334
- { code: 'model User { id String @id }' },
1335
- ],
1336
- },
1337
- },
1338
1345
  'schema-field-mismatch': {
1339
1346
  analyzer: 'schema-validator',
1340
1347
  field: 'rule',
@@ -1386,143 +1393,6 @@ export const RULE_REGISTRY = {
1386
1393
  ],
1387
1394
  },
1388
1395
  },
1389
- 'constraint-mismatch': {
1390
- analyzer: 'schema-validator',
1391
- field: 'rule',
1392
- input: ['cross-language-entities'],
1393
- resolvable: false,
1394
- message: 'Constraint mismatch: {detail}.',
1395
- docs: 'constraint-mismatch',
1396
- thresholds: [],
1397
- samples: {
1398
- valid: [
1399
- { code: 'model User { id Int @id @default(autoincrement()) }', nearMiss: true },
1400
- ],
1401
- invalid: [
1402
- { code: 'model User { id Int @id }' },
1403
- ],
1404
- },
1405
- },
1406
- 'version-mismatch': {
1407
- analyzer: 'schema-validator',
1408
- field: 'rule',
1409
- input: ['cross-language-entities'],
1410
- resolvable: false,
1411
- message: 'Version mismatch: {detail}.',
1412
- docs: 'version-mismatch',
1413
- thresholds: [],
1414
- samples: {
1415
- valid: [
1416
- { code: 'export const version = "1.2.3";', nearMiss: true },
1417
- ],
1418
- invalid: [
1419
- { code: 'export const version = "0.0.1";' },
1420
- ],
1421
- },
1422
- },
1423
- // ── api-contract (APIContractAnalyzer) ──────────────────────────────────
1424
- 'api-type-mismatch': {
1425
- analyzer: 'api-contract',
1426
- field: 'rule',
1427
- input: ['cross-language-entities'],
1428
- resolvable: false,
1429
- message: 'API type mismatch: {detail}.',
1430
- docs: 'api-type-mismatch',
1431
- thresholds: [],
1432
- samples: {
1433
- valid: [
1434
- { code: 'function getUser(id: number): User { return {} as User; }', nearMiss: true },
1435
- ],
1436
- invalid: [
1437
- { code: 'function getUser(id: number): string { return ""; }' },
1438
- ],
1439
- },
1440
- },
1441
- 'missing-endpoint': {
1442
- analyzer: 'api-contract',
1443
- field: 'rule',
1444
- input: ['cross-language-entities'],
1445
- resolvable: false,
1446
- message: 'Missing API endpoint: {endpoint}.',
1447
- docs: 'missing-endpoint',
1448
- thresholds: [],
1449
- samples: {
1450
- valid: [
1451
- { code: 'app.get("/users", handler);', nearMiss: true },
1452
- ],
1453
- invalid: [
1454
- { code: 'app.get("/unlisted", handler);' },
1455
- ],
1456
- },
1457
- },
1458
- 'api-extra-field': {
1459
- analyzer: 'api-contract',
1460
- field: 'rule',
1461
- input: ['cross-language-entities'],
1462
- resolvable: false,
1463
- message: 'Extra field in API response: {field}.',
1464
- docs: 'api-extra-field',
1465
- thresholds: [],
1466
- samples: {
1467
- valid: [
1468
- { code: 'return res.json({ id, name });', nearMiss: true },
1469
- ],
1470
- invalid: [
1471
- { code: 'return res.json({ id, name, internal });' },
1472
- ],
1473
- },
1474
- },
1475
- 'api-missing-field': {
1476
- analyzer: 'api-contract',
1477
- field: 'rule',
1478
- input: ['cross-language-entities'],
1479
- resolvable: false,
1480
- message: 'Missing field in API response: {field}.',
1481
- docs: 'api-missing-field',
1482
- thresholds: [],
1483
- samples: {
1484
- valid: [
1485
- { code: 'return res.json({ id, name });', nearMiss: true },
1486
- ],
1487
- invalid: [
1488
- { code: 'return res.json({ id });' },
1489
- ],
1490
- },
1491
- },
1492
- 'method-mismatch': {
1493
- analyzer: 'api-contract',
1494
- field: 'rule',
1495
- input: ['cross-language-entities'],
1496
- resolvable: false,
1497
- message: 'HTTP method mismatch: {detail}.',
1498
- docs: 'method-mismatch',
1499
- thresholds: [],
1500
- samples: {
1501
- valid: [
1502
- { code: 'app.post("/users", createUser);', nearMiss: true },
1503
- ],
1504
- invalid: [
1505
- { code: 'app.get("/users", createUser);' },
1506
- ],
1507
- },
1508
- },
1509
- 'auth-mismatch': {
1510
- analyzer: 'api-contract',
1511
- field: 'rule',
1512
- input: ['cross-language-entities'],
1513
- resolvable: false,
1514
- message: 'Authentication mismatch: {detail}.',
1515
- docs: 'auth-mismatch',
1516
- thresholds: [],
1517
- samples: {
1518
- valid: [
1519
- { code: 'app.get("/users", auth, handler);', nearMiss: true },
1520
- ],
1521
- invalid: [
1522
- { code: 'app.get("/users", handler);' },
1523
- ],
1524
- },
1525
- },
1526
1396
  // ── dependency-graph (DependencyGraphBuilder) ───────────────────────────
1527
1397
  'circular-dependency': {
1528
1398
  analyzer: 'dependency-graph',
@@ -2021,6 +1891,72 @@ export const RULE_REGISTRY = {
2021
1891
  ],
2022
1892
  },
2023
1893
  },
1894
+ // ── security (UniversalSecurityAnalyzer) — Spec 61 R6 ─────────────────────
1895
+ 'command-injection-risk': {
1896
+ analyzer: 'security',
1897
+ field: 'rule',
1898
+ input: ['files'],
1899
+ resolvable: true,
1900
+ message: 'Unsafe process invocation: {method} is passed a command built by interpolation/concatenation.',
1901
+ docs: 'command-injection-risk',
1902
+ thresholds: [],
1903
+ samples: {
1904
+ valid: [
1905
+ { code: "execFileSync('git', ['diff', '--name-only', ref])", nearMiss: true },
1906
+ ],
1907
+ invalid: [
1908
+ { code: "execSync(`git diff --name-only ${ref}`)", resolution: { action: 'use-argv-array', summary: 'Replace execSync with execFileSync/spawn whose command is a string literal and whose arguments are separate array elements, so no shell interprets them.', symbols: ['execSync'] } },
1909
+ ],
1910
+ },
1911
+ },
1912
+ 'dynamic-require-of-project-path': {
1913
+ analyzer: 'security',
1914
+ field: 'rule',
1915
+ input: ['files'],
1916
+ resolvable: true,
1917
+ message: 'Dynamic require/import of a project config path: {path}.',
1918
+ docs: 'dynamic-require-of-project-path',
1919
+ thresholds: [],
1920
+ samples: {
1921
+ valid: [
1922
+ { code: "import('./builtin-helper.js')", nearMiss: true },
1923
+ { code: "require('tailwindcss')", nearMiss: true },
1924
+ ],
1925
+ invalid: [
1926
+ { code: "require(configPath)", resolution: { action: 'static-config-extraction', summary: 'Read the config without executing it (static extraction) rather than require()/import() a path discovered from the project tree.', symbols: ['require'] } },
1927
+ ],
1928
+ },
1929
+ },
1930
+ 'unescaped-html-interpolation': {
1931
+ analyzer: 'security',
1932
+ field: 'rule',
1933
+ input: ['files'],
1934
+ resolvable: true,
1935
+ message: 'Unescaped HTML interpolation: {field} is inserted into an HTML template without an escaping call.',
1936
+ docs: 'unescaped-html-interpolation',
1937
+ thresholds: [],
1938
+ samples: {
1939
+ valid: [
1940
+ {
1941
+ // Same sink (`.innerHTML`) + same member-access interpolation, but the
1942
+ // value is escape-wrapped — the fix, not the finding. The old near-miss
1943
+ // (`const html = ...` with no sink) "passed" only because there was no
1944
+ // sink to read.
1945
+ code: 'el.innerHTML = `<p>${escapeHtml(user.name)}</p>`;',
1946
+ nearMiss: true,
1947
+ },
1948
+ ],
1949
+ invalid: [
1950
+ {
1951
+ // Needs BOTH a sink (`.innerHTML` assignment) and a member-expression
1952
+ // interpolation (`user.name`, not a bare identifier). The old sample
1953
+ // (`const html = `${userName}``) had neither, so it never emitted.
1954
+ code: 'el.innerHTML = `<p>${user.name}</p>`;',
1955
+ resolution: { action: 'escape-html-interpolation', summary: 'Wrap the interpolation in an escaping call (e.g. ${escapeHtml(user.name)}) before it reaches the HTML template.', symbols: ['user.name'] },
1956
+ },
1957
+ ],
1958
+ },
1959
+ },
2024
1960
  };
2025
1961
  /**
2026
1962
  * Canonical set of every analyzer ID that emits at least one rule in
@@ -2050,7 +1986,25 @@ export const MCP_DEFAULT_ANALYZERS = [
2050
1986
  'documentation',
2051
1987
  'react',
2052
1988
  'data-access',
1989
+ 'data-access-org-filter',
2053
1990
  ];
1991
+ /**
1992
+ * Analyzers the pipeline can run that emit *no* registry rule. Their rules come
1993
+ * from a runtime source other than {@link RULE_REGISTRY}: `invariants` reads its
1994
+ * rules from the `.codeauditor.json` `rules` array (the seven invariant rule
1995
+ * kinds), not from registry rows, so it is absent from {@link ALL_ANALYZERS}.
1996
+ */
1997
+ export const PIPELINE_ONLY_ANALYZERS = ['invariants'];
1998
+ /**
1999
+ * Every analyzer `enabledAnalyzers` may legitimately name — {@link ALL_ANALYZERS}
2000
+ * (registry rule emitters) plus {@link PIPELINE_ONLY_ANALYZERS}. This is the
2001
+ * single source of truth for "can this analyzer actually run?" — used by config
2002
+ * validation, which must not reject `invariants` (a valid pipeline analyzer that
2003
+ * happens to carry no registry rule).
2004
+ */
2005
+ export const RUNNABLE_ANALYZERS = [
2006
+ ...new Set([...ALL_ANALYZERS, ...PIPELINE_ONLY_ANALYZERS]),
2007
+ ].sort();
2054
2008
  /**
2055
2009
  * Resolve a violation to its canonical {@link RuleRegistryEntry}, if any.
2056
2010
  *