auditor-lambda 0.3.41 → 0.6.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 (78) hide show
  1. package/dist/cli/dispatch.js +5 -1
  2. package/dist/cli/prompts.d.ts +19 -0
  3. package/dist/cli/prompts.js +95 -0
  4. package/dist/cli/steps.d.ts +1 -1
  5. package/dist/cli.js +398 -78
  6. package/dist/extractors/analyzers/css.d.ts +2 -0
  7. package/dist/extractors/analyzers/css.js +101 -0
  8. package/dist/extractors/analyzers/html.d.ts +2 -0
  9. package/dist/extractors/analyzers/html.js +92 -0
  10. package/dist/extractors/analyzers/merge.d.ts +14 -0
  11. package/dist/extractors/analyzers/merge.js +85 -0
  12. package/dist/extractors/analyzers/python.d.ts +2 -0
  13. package/dist/extractors/analyzers/python.js +104 -0
  14. package/dist/extractors/analyzers/registry.d.ts +33 -0
  15. package/dist/extractors/analyzers/registry.js +100 -0
  16. package/dist/extractors/analyzers/resourceUrl.d.ts +7 -0
  17. package/dist/extractors/analyzers/resourceUrl.js +25 -0
  18. package/dist/extractors/analyzers/sql.d.ts +2 -0
  19. package/dist/extractors/analyzers/sql.js +19 -0
  20. package/dist/extractors/analyzers/treeSitter.d.ts +34 -0
  21. package/dist/extractors/analyzers/treeSitter.js +111 -0
  22. package/dist/extractors/analyzers/types.d.ts +53 -0
  23. package/dist/extractors/analyzers/types.js +1 -0
  24. package/dist/extractors/analyzers/typescript.d.ts +2 -0
  25. package/dist/extractors/analyzers/typescript.js +257 -0
  26. package/dist/extractors/disposition.js +8 -1
  27. package/dist/extractors/graph.d.ts +1 -0
  28. package/dist/extractors/graph.js +167 -1
  29. package/dist/extractors/graphPythonImports.d.ts +15 -0
  30. package/dist/extractors/graphPythonImports.js +36 -0
  31. package/dist/extractors/pathPatterns.d.ts +6 -0
  32. package/dist/extractors/pathPatterns.js +8 -0
  33. package/dist/io/artifacts.d.ts +13 -1
  34. package/dist/io/artifacts.js +19 -3
  35. package/dist/mcp/server.js +3 -3
  36. package/dist/orchestrator/advance.d.ts +20 -0
  37. package/dist/orchestrator/advance.js +61 -2
  38. package/dist/orchestrator/dependencyMap.js +27 -0
  39. package/dist/orchestrator/edgeReasoning.d.ts +39 -0
  40. package/dist/orchestrator/edgeReasoning.js +125 -0
  41. package/dist/orchestrator/executors.js +11 -1
  42. package/dist/orchestrator/graphEnrichmentExecutor.d.ts +29 -0
  43. package/dist/orchestrator/graphEnrichmentExecutor.js +196 -0
  44. package/dist/orchestrator/internalExecutors.d.ts +10 -1
  45. package/dist/orchestrator/internalExecutors.js +89 -11
  46. package/dist/orchestrator/localCommands.js +6 -25
  47. package/dist/orchestrator/nextStep.js +2 -0
  48. package/dist/orchestrator/reviewPackets.d.ts +37 -4
  49. package/dist/orchestrator/reviewPackets.js +93 -46
  50. package/dist/orchestrator/runtimeValidation.js +4 -31
  51. package/dist/orchestrator/scope.d.ts +62 -0
  52. package/dist/orchestrator/scope.js +227 -0
  53. package/dist/orchestrator/state.js +2 -0
  54. package/dist/reporting/synthesis.d.ts +37 -2
  55. package/dist/reporting/synthesis.js +95 -16
  56. package/dist/reporting/synthesisNarrativePrompt.d.ts +7 -0
  57. package/dist/reporting/synthesisNarrativePrompt.js +60 -0
  58. package/dist/reporting/workBlocks.d.ts +2 -10
  59. package/dist/supervisor/operatorHandoff.d.ts +1 -1
  60. package/dist/supervisor/operatorHandoff.js +26 -16
  61. package/dist/supervisor/sessionConfig.d.ts +8 -1
  62. package/dist/supervisor/sessionConfig.js +22 -1
  63. package/dist/types/analyzerCapability.d.ts +16 -0
  64. package/dist/types/analyzerCapability.js +1 -0
  65. package/dist/types/auditScope.d.ts +43 -0
  66. package/dist/types/auditScope.js +14 -0
  67. package/dist/types/synthesisNarrative.d.ts +7 -0
  68. package/dist/types/synthesisNarrative.js +5 -0
  69. package/dist/types.d.ts +2 -19
  70. package/dist/validation/artifacts.js +9 -0
  71. package/dist/validation/sessionConfig.js +24 -1
  72. package/docs/contracts.md +10 -3
  73. package/package.json +4 -2
  74. package/schemas/analyzer_capability.schema.json +47 -0
  75. package/schemas/audit_findings.schema.json +141 -0
  76. package/schemas/finding.schema.json +2 -1
  77. package/schemas/graph_bundle.schema.json +5 -0
  78. package/schemas/scope.schema.json +46 -0
@@ -0,0 +1,101 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { graphEdge, normalizeGraphPath } from "../graphPathUtils.js";
4
+ import { getTreeSitterParser } from "./treeSitter.js";
5
+ import { resolveResourceUrl } from "./resourceUrl.js";
6
+ const CSS_IMPORT_EDGE_CONFIDENCE = 0.9;
7
+ const CSS_URL_EDGE_CONFIDENCE = 0.82;
8
+ const MAX_CSS_SOURCE_BYTES = 512 * 1024;
9
+ function supports(file) {
10
+ return normalizeGraphPath(file).toLowerCase().endsWith(".css");
11
+ }
12
+ function unquote(value) {
13
+ const trimmed = value.trim();
14
+ if (trimmed.length >= 2 &&
15
+ ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
16
+ (trimmed.startsWith("'") && trimmed.endsWith("'")))) {
17
+ return trimmed.slice(1, -1);
18
+ }
19
+ return trimmed;
20
+ }
21
+ /** First quoted or unquoted literal value beneath a node (string / url arg). */
22
+ function literalValue(node) {
23
+ // tree-sitter-css string_value includes the surrounding quotes; plain_value
24
+ // (unquoted url() args) does not.
25
+ const value = node.descendantsOfType(["string_value", "plain_value"])[0];
26
+ return value ? unquote(value.text) : undefined;
27
+ }
28
+ function collectFileEdges(fromPath, root, pathLookup, edges) {
29
+ const importTargets = new Set();
30
+ // @import "x.css"; and @import url("x.css");
31
+ for (const statement of root.descendantsOfType("import_statement")) {
32
+ const url = literalValue(statement);
33
+ if (!url)
34
+ continue;
35
+ const target = resolveResourceUrl(fromPath, url, pathLookup);
36
+ if (!target || target === fromPath || importTargets.has(target))
37
+ continue;
38
+ importTargets.add(target);
39
+ edges.push(graphEdge({
40
+ from: fromPath,
41
+ to: target,
42
+ kind: "css-import",
43
+ confidence: CSS_IMPORT_EDGE_CONFIDENCE,
44
+ reason: `CSS @import references '${target}'.`,
45
+ }));
46
+ }
47
+ // url(...) references in declarations (background images, fonts, …).
48
+ const seenUrls = new Set(importTargets);
49
+ for (const call of root.descendantsOfType("call_expression")) {
50
+ if (!/^\s*url\s*\(/i.test(call.text))
51
+ continue;
52
+ const url = literalValue(call);
53
+ if (!url)
54
+ continue;
55
+ const target = resolveResourceUrl(fromPath, url, pathLookup);
56
+ if (!target || target === fromPath || seenUrls.has(target))
57
+ continue;
58
+ seenUrls.add(target);
59
+ edges.push(graphEdge({
60
+ from: fromPath,
61
+ to: target,
62
+ kind: "css-url",
63
+ confidence: CSS_URL_EDGE_CONFIDENCE,
64
+ reason: `CSS url() references '${target}'.`,
65
+ }));
66
+ }
67
+ }
68
+ async function analyze(files, context) {
69
+ if (files.length === 0)
70
+ return { edges: [] };
71
+ const parser = await getTreeSitterParser("css", context.dependencyPath);
72
+ if (!parser)
73
+ return { edges: [] };
74
+ const root = resolve(context.root);
75
+ const edges = [];
76
+ for (const file of files) {
77
+ let content;
78
+ try {
79
+ content = await readFile(resolve(root, file), "utf8");
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ if (content.length > MAX_CSS_SOURCE_BYTES)
85
+ continue;
86
+ try {
87
+ const tree = parser.parse(content);
88
+ collectFileEdges(normalizeGraphPath(file), tree.rootNode, context.pathLookup, edges);
89
+ }
90
+ catch {
91
+ // Degrade to the floor for this file.
92
+ }
93
+ }
94
+ return { edges };
95
+ }
96
+ export const cssAnalyzer = {
97
+ id: "css",
98
+ dependency: "web-tree-sitter",
99
+ supports,
100
+ analyze,
101
+ };
@@ -0,0 +1,2 @@
1
+ import type { LanguageAnalyzer } from "./types.js";
2
+ export declare const htmlAnalyzer: LanguageAnalyzer;
@@ -0,0 +1,92 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { graphEdge, normalizeGraphPath } from "../graphPathUtils.js";
4
+ import { getTreeSitterParser } from "./treeSitter.js";
5
+ import { resolveResourceUrl } from "./resourceUrl.js";
6
+ // Above the regex floor's html-resource-link so the merge prefers the parsed
7
+ // edge for the same (from, to).
8
+ const HTML_RESOURCE_EDGE_CONFIDENCE = 0.96;
9
+ const MAX_HTML_SOURCE_BYTES = 512 * 1024;
10
+ const HTML_EXTENSIONS = [".html", ".htm"];
11
+ // tag → the attribute carrying its resource reference.
12
+ const RESOURCE_ATTRIBUTE = {
13
+ script: "src",
14
+ link: "href",
15
+ img: "src",
16
+ };
17
+ function supports(file) {
18
+ const lower = normalizeGraphPath(file).toLowerCase();
19
+ return HTML_EXTENSIONS.some((extension) => lower.endsWith(extension));
20
+ }
21
+ function attributeValue(tag, name) {
22
+ for (const attribute of tag.descendantsOfType("attribute")) {
23
+ const attributeName = attribute
24
+ .descendantsOfType("attribute_name")[0]
25
+ ?.text?.toLowerCase();
26
+ if (attributeName !== name)
27
+ continue;
28
+ const value = attribute.descendantsOfType("attribute_value")[0]?.text;
29
+ if (value && value.trim().length > 0)
30
+ return value;
31
+ }
32
+ return undefined;
33
+ }
34
+ function collectFileEdges(fromPath, root, pathLookup, edges) {
35
+ const seen = new Set();
36
+ for (const tag of root.descendantsOfType("start_tag")) {
37
+ const tagName = tag.descendantsOfType("tag_name")[0]?.text?.toLowerCase();
38
+ if (!tagName)
39
+ continue;
40
+ const attribute = RESOURCE_ATTRIBUTE[tagName];
41
+ if (!attribute)
42
+ continue;
43
+ const url = attributeValue(tag, attribute);
44
+ if (!url)
45
+ continue;
46
+ const target = resolveResourceUrl(fromPath, url, pathLookup);
47
+ if (!target || target === fromPath || seen.has(target))
48
+ continue;
49
+ seen.add(target);
50
+ edges.push(graphEdge({
51
+ from: fromPath,
52
+ to: target,
53
+ kind: "html-resource",
54
+ confidence: HTML_RESOURCE_EDGE_CONFIDENCE,
55
+ reason: `HTML <${tagName} ${attribute}> references '${target}'.`,
56
+ }));
57
+ }
58
+ }
59
+ async function analyze(files, context) {
60
+ if (files.length === 0)
61
+ return { edges: [] };
62
+ const parser = await getTreeSitterParser("html", context.dependencyPath);
63
+ if (!parser)
64
+ return { edges: [] };
65
+ const root = resolve(context.root);
66
+ const edges = [];
67
+ for (const file of files) {
68
+ let content;
69
+ try {
70
+ content = await readFile(resolve(root, file), "utf8");
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ if (content.length > MAX_HTML_SOURCE_BYTES)
76
+ continue;
77
+ try {
78
+ const tree = parser.parse(content);
79
+ collectFileEdges(normalizeGraphPath(file), tree.rootNode, context.pathLookup, edges);
80
+ }
81
+ catch {
82
+ // Degrade to the floor for this file.
83
+ }
84
+ }
85
+ return { edges };
86
+ }
87
+ export const htmlAnalyzer = {
88
+ id: "html",
89
+ dependency: "web-tree-sitter",
90
+ supports,
91
+ analyze,
92
+ };
@@ -0,0 +1,14 @@
1
+ import type { GraphEdge } from "@audit-tools/shared";
2
+ export declare const TS_IMPORT_EDGE_CONFIDENCE = 0.99;
3
+ export declare const TS_REEXPORT_EDGE_CONFIDENCE = 0.99;
4
+ export declare const TS_EXTENDS_EDGE_CONFIDENCE = 0.97;
5
+ export declare const TS_IMPLEMENTS_EDGE_CONFIDENCE = 0.97;
6
+ export declare const TS_CALL_EDGE_CONFIDENCE = 0.9;
7
+ /**
8
+ * Merge analyzer edges into an existing floor bucket (e.g. `graphs.imports`).
9
+ * Within a known relationship group, edges sharing (from, to) collapse to the
10
+ * highest-confidence one (ties favour the later/analyzer edge); ungrouped kinds
11
+ * keep their per-kind identity. Self-edges are dropped. Result is deduped and
12
+ * sorted, matching the floor's ordering contract.
13
+ */
14
+ export declare function mergeAnalyzerEdges(floor: GraphEdge[], analyzer: GraphEdge[]): GraphEdge[];
@@ -0,0 +1,85 @@
1
+ // Analyzer edge confidences are set above their regex-floor counterparts so the
2
+ // group-aware merge below prefers the compiler-derived edge for the same
3
+ // (from, to) relationship. Floor import kinds sit at 0.95 (see graph.ts).
4
+ export const TS_IMPORT_EDGE_CONFIDENCE = 0.99;
5
+ export const TS_REEXPORT_EDGE_CONFIDENCE = 0.99;
6
+ export const TS_EXTENDS_EDGE_CONFIDENCE = 0.97;
7
+ export const TS_IMPLEMENTS_EDGE_CONFIDENCE = 0.97;
8
+ export const TS_CALL_EDGE_CONFIDENCE = 0.9;
9
+ /**
10
+ * Kinds that represent the same underlying relationship collapse together during
11
+ * a merge (highest confidence wins). Kinds with no group keep their distinct
12
+ * (from, to, kind) identity — so floor-only relationships such as
13
+ * `heuristic-container-edge` or `heuristic-auth-session-link` are never dropped
14
+ * by an analyzer that happens to connect the same two nodes a different way.
15
+ */
16
+ const EDGE_GROUP = {
17
+ // import relationship
18
+ esm: "import",
19
+ "re-export": "import",
20
+ "dynamic-import": "import",
21
+ commonjs: "import",
22
+ "ts-import": "import",
23
+ "ts-reexport": "import",
24
+ // Python imports: the tree-sitter analyzer (py-*) supersedes the regex floor
25
+ // (python-*) for the same (from, to).
26
+ "python-import": "import",
27
+ "python-from-import": "import",
28
+ "py-import": "import",
29
+ "py-from-import": "import",
30
+ // HTML resource references: the tree-sitter analyzer (html-resource)
31
+ // supersedes the regex floor (html-resource-link).
32
+ "html-resource-link": "html-resource",
33
+ "html-resource": "html-resource",
34
+ // inheritance relationship
35
+ "ts-extends": "inheritance",
36
+ "ts-implements": "inheritance",
37
+ // call relationship
38
+ "ts-call": "call",
39
+ };
40
+ function edgeGroupOf(edge) {
41
+ return edge.kind ? EDGE_GROUP[edge.kind] : undefined;
42
+ }
43
+ function confidenceOf(edge) {
44
+ return typeof edge.confidence === "number" && Number.isFinite(edge.confidence)
45
+ ? edge.confidence
46
+ : 0;
47
+ }
48
+ function groupedKey(edge, group) {
49
+ return `${edge.from}\0${edge.to}\0${group}`;
50
+ }
51
+ function ungroupedKey(edge) {
52
+ return `${edge.from}\0${edge.to}\0${edge.kind ?? ""}`;
53
+ }
54
+ function sortEdges(edges) {
55
+ return edges.sort((a, b) => a.from.localeCompare(b.from) ||
56
+ a.to.localeCompare(b.to) ||
57
+ (a.kind ?? "").localeCompare(b.kind ?? ""));
58
+ }
59
+ /**
60
+ * Merge analyzer edges into an existing floor bucket (e.g. `graphs.imports`).
61
+ * Within a known relationship group, edges sharing (from, to) collapse to the
62
+ * highest-confidence one (ties favour the later/analyzer edge); ungrouped kinds
63
+ * keep their per-kind identity. Self-edges are dropped. Result is deduped and
64
+ * sorted, matching the floor's ordering contract.
65
+ */
66
+ export function mergeAnalyzerEdges(floor, analyzer) {
67
+ const grouped = new Map();
68
+ const ungrouped = new Map();
69
+ for (const edge of [...floor, ...analyzer]) {
70
+ if (edge.from === edge.to)
71
+ continue;
72
+ const group = edgeGroupOf(edge);
73
+ if (group) {
74
+ const key = groupedKey(edge, group);
75
+ const existing = grouped.get(key);
76
+ if (!existing || confidenceOf(edge) >= confidenceOf(existing)) {
77
+ grouped.set(key, edge);
78
+ }
79
+ }
80
+ else {
81
+ ungrouped.set(ungroupedKey(edge), edge);
82
+ }
83
+ }
84
+ return sortEdges([...grouped.values(), ...ungrouped.values()]);
85
+ }
@@ -0,0 +1,2 @@
1
+ import type { LanguageAnalyzer } from "./types.js";
2
+ export declare const pythonAnalyzer: LanguageAnalyzer;
@@ -0,0 +1,104 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { graphEdge, normalizeGraphPath } from "../graphPathUtils.js";
4
+ import { isPythonSourcePath, resolvePythonFromImportTargets, resolvePythonImportTarget, } from "../graphPythonImports.js";
5
+ import { getTreeSitterParser } from "./treeSitter.js";
6
+ // Set above the regex floor's 0.95 so the merge prefers the AST-resolved edge
7
+ // for the same (from, to). Resolution itself is shared with the floor, so the
8
+ // only difference is parse-grade extraction.
9
+ const PY_IMPORT_EDGE_CONFIDENCE = 0.97;
10
+ const MAX_PYTHON_SOURCE_BYTES = 512 * 1024;
11
+ function supports(file) {
12
+ return isPythonSourcePath(file);
13
+ }
14
+ /** The bare module text for one `import` name (unwrapping `x.y as z`). */
15
+ function importModuleText(nameNode) {
16
+ const moduleNode = nameNode.type === "aliased_import"
17
+ ? nameNode.childForFieldName("name")
18
+ : nameNode;
19
+ const text = moduleNode?.text?.trim();
20
+ return text && text.length > 0 ? text : undefined;
21
+ }
22
+ function collectFileEdges(fromPath, root, pathLookup, edges) {
23
+ const seen = new Set();
24
+ const push = (target, kind, specifier) => {
25
+ if (target === fromPath)
26
+ return;
27
+ const key = `${kind}\0${target}`;
28
+ if (seen.has(key))
29
+ return;
30
+ seen.add(key);
31
+ edges.push(graphEdge({
32
+ from: fromPath,
33
+ to: target,
34
+ kind,
35
+ confidence: PY_IMPORT_EDGE_CONFIDENCE,
36
+ reason: `tree-sitter resolved Python import '${specifier}' to '${target}'.`,
37
+ }));
38
+ };
39
+ for (const node of root.descendantsOfType("import_statement")) {
40
+ for (const nameNode of childrenForField(node, "name")) {
41
+ const specifier = importModuleText(nameNode);
42
+ if (!specifier)
43
+ continue;
44
+ const target = resolvePythonImportTarget(fromPath, specifier, pathLookup);
45
+ if (target)
46
+ push(target, "py-import", specifier);
47
+ }
48
+ }
49
+ for (const node of root.descendantsOfType("import_from_statement")) {
50
+ const moduleNode = node.childForFieldName("module_name");
51
+ const moduleSpecifier = moduleNode?.text?.trim();
52
+ if (!moduleSpecifier)
53
+ continue;
54
+ const importedNames = childrenForField(node, "name")
55
+ .map((nameNode) => importModuleText(nameNode))
56
+ .filter((name) => Boolean(name));
57
+ for (const { specifier, target } of resolvePythonFromImportTargets(fromPath, moduleSpecifier, importedNames, pathLookup)) {
58
+ push(target, "py-from-import", specifier);
59
+ }
60
+ }
61
+ }
62
+ /** web-tree-sitter Node#childrenForFieldName, guarded for older runtimes. */
63
+ function childrenForField(node, field) {
64
+ const fn = node.childrenForFieldName;
65
+ if (typeof fn === "function") {
66
+ return fn.call(node, field) ?? [];
67
+ }
68
+ const single = node.childForFieldName(field);
69
+ return single ? [single] : [];
70
+ }
71
+ async function analyze(files, context) {
72
+ if (files.length === 0)
73
+ return { edges: [] };
74
+ const parser = await getTreeSitterParser("python", context.dependencyPath);
75
+ if (!parser)
76
+ return { edges: [] };
77
+ const root = resolve(context.root);
78
+ const edges = [];
79
+ for (const file of files) {
80
+ let content;
81
+ try {
82
+ content = await readFile(resolve(root, file), "utf8");
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ if (content.length > MAX_PYTHON_SOURCE_BYTES)
88
+ continue;
89
+ try {
90
+ const tree = parser.parse(content);
91
+ collectFileEdges(normalizeGraphPath(file), tree.rootNode, context.pathLookup, edges);
92
+ }
93
+ catch {
94
+ // A parse failure on one file degrades to the floor for that file.
95
+ }
96
+ }
97
+ return { edges };
98
+ }
99
+ export const pythonAnalyzer = {
100
+ id: "python",
101
+ dependency: "web-tree-sitter",
102
+ supports,
103
+ analyze,
104
+ };
@@ -0,0 +1,33 @@
1
+ import { type AnalyzerSetting } from "@audit-tools/shared";
2
+ import type { AnalyzerPlanEntry, LanguageAnalyzer } from "./types.js";
3
+ /**
4
+ * Registered language analyzers, in within-phase order (seam → TS/JS →
5
+ * Python → HTML → CSS). SQL is a registry stub (recognises `.sql`, emits no
6
+ * edges yet). The tree-sitter analyzers (Python/HTML/CSS) load their grammar
7
+ * from the optional `web-tree-sitter` dependency and degrade to the regex
8
+ * floor when it cannot be resolved.
9
+ */
10
+ export declare const ANALYZER_REGISTRY: LanguageAnalyzer[];
11
+ export declare function getAnalyzerById(id: string): LanguageAnalyzer | undefined;
12
+ /**
13
+ * Deterministically resolve, without installing anything, how each registered
14
+ * analyzer would run for this repo. The conversation-first CLI uses this to
15
+ * decide whether to propose an install before the enrichment executor runs.
16
+ *
17
+ * Resolution rules:
18
+ * - 0 supported in-scope files → `not_applicable`
19
+ * - setting `skip` → `skip`
20
+ * - dependency resolves (repo|cache) → `repo` | `cache`
21
+ * - dependency absent → `absent` (executor installs only
22
+ * for `ephemeral`/`permanent`; `auto` may prompt; `repo` falls to the floor)
23
+ */
24
+ export declare function resolveAnalyzerPlan(root: string, analyzers: Record<string, AnalyzerSetting> | undefined, includedFiles: string[], options?: {
25
+ cacheRoot?: string;
26
+ }): AnalyzerPlanEntry[];
27
+ /**
28
+ * A plan entry whose dependency is absent and whose setting is `auto` (or unset)
29
+ * with in-scope files — the only case that warrants proposing an install in the
30
+ * conversation-first flow. (ephemeral/permanent install silently; repo/skip fall
31
+ * to the floor silently.)
32
+ */
33
+ export declare function needsInstallDecision(entry: AnalyzerPlanEntry): boolean;
@@ -0,0 +1,100 @@
1
+ import { resolveAnalyzerDep, } from "@audit-tools/shared";
2
+ import { typescriptAnalyzer } from "./typescript.js";
3
+ import { pythonAnalyzer } from "./python.js";
4
+ import { htmlAnalyzer } from "./html.js";
5
+ import { cssAnalyzer } from "./css.js";
6
+ import { sqlAnalyzer } from "./sql.js";
7
+ /**
8
+ * Registered language analyzers, in within-phase order (seam → TS/JS →
9
+ * Python → HTML → CSS). SQL is a registry stub (recognises `.sql`, emits no
10
+ * edges yet). The tree-sitter analyzers (Python/HTML/CSS) load their grammar
11
+ * from the optional `web-tree-sitter` dependency and degrade to the regex
12
+ * floor when it cannot be resolved.
13
+ */
14
+ export const ANALYZER_REGISTRY = [
15
+ typescriptAnalyzer,
16
+ pythonAnalyzer,
17
+ htmlAnalyzer,
18
+ cssAnalyzer,
19
+ sqlAnalyzer,
20
+ ];
21
+ export function getAnalyzerById(id) {
22
+ return ANALYZER_REGISTRY.find((analyzer) => analyzer.id === id);
23
+ }
24
+ function settingFor(analyzers, id) {
25
+ return analyzers?.[id] ?? "auto";
26
+ }
27
+ /**
28
+ * Deterministically resolve, without installing anything, how each registered
29
+ * analyzer would run for this repo. The conversation-first CLI uses this to
30
+ * decide whether to propose an install before the enrichment executor runs.
31
+ *
32
+ * Resolution rules:
33
+ * - 0 supported in-scope files → `not_applicable`
34
+ * - setting `skip` → `skip`
35
+ * - dependency resolves (repo|cache) → `repo` | `cache`
36
+ * - dependency absent → `absent` (executor installs only
37
+ * for `ephemeral`/`permanent`; `auto` may prompt; `repo` falls to the floor)
38
+ */
39
+ export function resolveAnalyzerPlan(root, analyzers, includedFiles, options = {}) {
40
+ const depOptions = options.cacheRoot ? { cacheRoot: options.cacheRoot } : {};
41
+ return ANALYZER_REGISTRY.map((analyzer) => {
42
+ const setting = settingFor(analyzers, analyzer.id);
43
+ const supportedCount = includedFiles.filter((file) => analyzer.supports(file)).length;
44
+ if (supportedCount === 0) {
45
+ return {
46
+ id: analyzer.id,
47
+ dependency: analyzer.dependency,
48
+ setting,
49
+ resolution: "not_applicable",
50
+ supportedCount,
51
+ };
52
+ }
53
+ if (setting === "skip") {
54
+ return {
55
+ id: analyzer.id,
56
+ dependency: analyzer.dependency,
57
+ setting,
58
+ resolution: "skip",
59
+ supportedCount,
60
+ };
61
+ }
62
+ if (!analyzer.dependency) {
63
+ // No dependency required: always available.
64
+ return {
65
+ id: analyzer.id,
66
+ dependency: analyzer.dependency,
67
+ setting,
68
+ resolution: "repo",
69
+ supportedCount,
70
+ };
71
+ }
72
+ const resolved = resolveAnalyzerDep(analyzer.dependency, root, depOptions);
73
+ if (resolved.via === "repo" || resolved.via === "cache") {
74
+ return {
75
+ id: analyzer.id,
76
+ dependency: analyzer.dependency,
77
+ setting,
78
+ resolution: resolved.via,
79
+ path: resolved.path,
80
+ supportedCount,
81
+ };
82
+ }
83
+ return {
84
+ id: analyzer.id,
85
+ dependency: analyzer.dependency,
86
+ setting,
87
+ resolution: "absent",
88
+ supportedCount,
89
+ };
90
+ });
91
+ }
92
+ /**
93
+ * A plan entry whose dependency is absent and whose setting is `auto` (or unset)
94
+ * with in-scope files — the only case that warrants proposing an install in the
95
+ * conversation-first flow. (ephemeral/permanent install silently; repo/skip fall
96
+ * to the floor silently.)
97
+ */
98
+ export function needsInstallDecision(entry) {
99
+ return entry.resolution === "absent" && entry.setting === "auto";
100
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolve an HTML/CSS resource reference (script src, link href, @import, url())
3
+ * to a repo-relative file path, or undefined. Root-relative URLs ("/assets/x")
4
+ * resolve from the repo root; everything else is relative to the referencing
5
+ * file. Query strings and fragments are stripped before resolution.
6
+ */
7
+ export declare function resolveResourceUrl(fromPath: string, url: string, pathLookup: Map<string, string>): string | undefined;
@@ -0,0 +1,25 @@
1
+ import { posix } from "node:path";
2
+ import { normalizeGraphPath, resolveCandidate } from "../graphPathUtils.js";
3
+ // Protocol (http:, data:, mailto:, …), protocol-relative (//), and fragment (#)
4
+ // URLs point outside the repository and are never resolved to a local file.
5
+ const EXTERNAL_URL_PATTERN = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
6
+ /**
7
+ * Resolve an HTML/CSS resource reference (script src, link href, @import, url())
8
+ * to a repo-relative file path, or undefined. Root-relative URLs ("/assets/x")
9
+ * resolve from the repo root; everything else is relative to the referencing
10
+ * file. Query strings and fragments are stripped before resolution.
11
+ */
12
+ export function resolveResourceUrl(fromPath, url, pathLookup) {
13
+ const trimmed = url.trim();
14
+ if (trimmed.length === 0 || EXTERNAL_URL_PATTERN.test(trimmed)) {
15
+ return undefined;
16
+ }
17
+ const withoutQuery = trimmed.split(/[?#]/, 1)[0] ?? "";
18
+ if (withoutQuery.length === 0) {
19
+ return undefined;
20
+ }
21
+ const candidate = withoutQuery.startsWith("/")
22
+ ? withoutQuery.slice(1)
23
+ : posix.join(posix.dirname(normalizeGraphPath(fromPath)), withoutQuery);
24
+ return resolveCandidate(candidate, pathLookup);
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { LanguageAnalyzer } from "./types.js";
2
+ export declare const sqlAnalyzer: LanguageAnalyzer;
@@ -0,0 +1,19 @@
1
+ import { normalizeGraphPath } from "../graphPathUtils.js";
2
+ /**
3
+ * SQL is intentionally a registry stub (per the refactor plan): the seam
4
+ * recognises `.sql` files but emits no edges yet. Registering it keeps the
5
+ * capability surface honest — `analyzer_capability.json` records SQL as
6
+ * applicable-but-empty rather than silently invisible — and leaves a single
7
+ * place to add cross-file resolution (views/foreign keys) later.
8
+ */
9
+ function supports(file) {
10
+ return normalizeGraphPath(file).toLowerCase().endsWith(".sql");
11
+ }
12
+ function analyze() {
13
+ return { edges: [] };
14
+ }
15
+ export const sqlAnalyzer = {
16
+ id: "sql",
17
+ supports,
18
+ analyze,
19
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared web-tree-sitter loader for the Tier-S parser analyzers (Python / HTML /
3
+ * CSS). web-tree-sitter is a pure-WASM runtime — no native compilation — so a
4
+ * grammar parses identically on every platform; grammars ship as `.wasm` files
5
+ * in `tree-sitter-wasms` (out/tree-sitter-<lang>.wasm).
6
+ *
7
+ * Everything here degrades to `undefined` on any failure (missing dependency,
8
+ * missing grammar, init error) so a caller simply keeps the deterministic regex
9
+ * floor. The runtime is initialised once and parsed grammars are cached.
10
+ */
11
+ export interface TsNode {
12
+ type: string;
13
+ text: string;
14
+ namedChildren: TsNode[];
15
+ childForFieldName(field: string): TsNode | null;
16
+ descendantsOfType(type: string | string[]): TsNode[];
17
+ }
18
+ export interface TsTree {
19
+ rootNode: TsNode;
20
+ }
21
+ export interface TsLanguage {
22
+ __brand?: "tree-sitter-language";
23
+ }
24
+ export interface TsParser {
25
+ setLanguage(language: TsLanguage): void;
26
+ parse(source: string): TsTree;
27
+ }
28
+ /**
29
+ * Obtain a parser bound to `grammar` (e.g. "python", "html", "css"), or
30
+ * `undefined` if web-tree-sitter or the grammar wasm cannot be loaded.
31
+ */
32
+ export declare function getTreeSitterParser(grammar: string, dependencyPath?: string): Promise<TsParser | undefined>;
33
+ /** Test seam: reset the memoised runtime/grammar caches. */
34
+ export declare function __resetTreeSitterForTests(): void;