auditor-lambda 0.2.16 → 0.2.17

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.
@@ -0,0 +1,38 @@
1
+ {
2
+ "correctness": {
3
+ "description": "Logic errors, incorrect algorithm implementations, off-by-one bugs, type mismatches, wrong return values, incorrect state transitions, missing null/undefined guards, misuse of APIs. Focus on code that does the wrong thing.",
4
+ "do_not_report": "Style issues, naming problems, missing tests, or findings that belong to other lenses."
5
+ },
6
+ "maintainability": {
7
+ "description": "Code that is hard to change safely: excessive function length, deep nesting, tight coupling between unrelated modules, poor naming, magic constants, duplicated logic, inconsistent abstractions, unclear public APIs.",
8
+ "do_not_report": "Correctness bugs, test gaps, or operational concerns."
9
+ },
10
+ "tests": {
11
+ "description": "Test coverage gaps for important paths, tests that assert incorrect behavior (pinning bugs as expected), fragile or non-deterministic tests, missing negative/edge-case tests, tests that silently pass on stale builds (e.g. importing compiled dist/ rather than source).",
12
+ "do_not_report": "Source code bugs — report only issues with the tests themselves."
13
+ },
14
+ "security": {
15
+ "description": "Injection vulnerabilities (SQL, shell, path traversal), authentication/authorization flaws, secret exposure, insecure deserialization, privilege escalation, unsafe use of eval or child processes with user input.",
16
+ "do_not_report": "Performance or correctness issues that are not security-relevant."
17
+ },
18
+ "reliability": {
19
+ "description": "Failure modes without recovery, missing timeouts, unhandled promise rejections, race conditions, resource leaks (file handles, sockets, timers), incorrect retry logic, cascading failure risks.",
20
+ "do_not_report": "Correctness bugs that do not affect reliability under failure conditions."
21
+ },
22
+ "performance": {
23
+ "description": "Algorithmic inefficiencies (O(n²) where O(n) is possible), unnecessary re-computation, missing caching, synchronous blocking in hot paths, excessive memory allocation.",
24
+ "do_not_report": "Correctness bugs unrelated to performance."
25
+ },
26
+ "data_integrity": {
27
+ "description": "Missing input validation at trust boundaries, schema violations, inconsistent field naming across related schemas, data loss scenarios, missing required fields, enum values that are present in some schemas but not others.",
28
+ "do_not_report": "UI or presentation issues; operational or deployment concerns."
29
+ },
30
+ "operability": {
31
+ "description": "Missing or low-quality log output, error messages that don't help operators diagnose problems, missing progress indicators for long operations, no elapsed-time reporting, lack of dry-run or preview modes for destructive operations.",
32
+ "do_not_report": "Correctness bugs or deployment configuration."
33
+ },
34
+ "config_deployment": {
35
+ "description": "CI/CD pipeline correctness (wrong triggers, missing branch filters, floating version pins), deployment safety (no gate before publish, missing rollback), insecure secret handling in configs, mutable action tags that should be pinned to commit SHAs.",
36
+ "do_not_report": "Runtime code issues; findings that belong to other lenses."
37
+ }
38
+ }
@@ -0,0 +1,84 @@
1
+ import { dirname, resolve, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
4
+ import { validateResult } from "./validate.mjs";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const PROJECT_ROOT = resolve(__dirname, "..");
9
+
10
+ // Parse --run-id
11
+ const runIdIdx = process.argv.indexOf("--run-id");
12
+ if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
13
+ console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id>");
14
+ process.exit(1);
15
+ }
16
+ const run_id = process.argv[runIdIdx + 1];
17
+
18
+ const artifactsDir = join(PROJECT_ROOT, ".audit-artifacts");
19
+ const taskResultsDir = join(artifactsDir, "runs", run_id, "task-results");
20
+ const auditResultsPath = join(artifactsDir, "runs", run_id, "audit-results.json");
21
+ const failedTasksPath = join(artifactsDir, "runs", run_id, "failed-tasks.json");
22
+ const tasksPath = join(artifactsDir, "runs", run_id, "pending-audit-tasks.json");
23
+
24
+ // Build fileLineCounts map
25
+ const lineCounts = {};
26
+ if (existsSync(tasksPath)) {
27
+ try {
28
+ const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
29
+ for (const task of tasks) {
30
+ lineCounts[task.task_id] = task.file_line_counts;
31
+ }
32
+ } catch {
33
+ // proceed with empty map
34
+ }
35
+ }
36
+
37
+ if (!existsSync(taskResultsDir)) {
38
+ console.error(`task-results directory not found: ${taskResultsDir}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ const files = readdirSync(taskResultsDir).filter(f => f.endsWith(".json"));
43
+
44
+ const passing = [];
45
+ const failing = [];
46
+
47
+ for (const filename of files) {
48
+ const filePath = join(taskResultsDir, filename);
49
+ let resultObj;
50
+ try {
51
+ resultObj = JSON.parse(readFileSync(filePath, "utf8"));
52
+ } catch (e) {
53
+ failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
54
+ continue;
55
+ }
56
+
57
+ const taskId = resultObj?.task_id;
58
+ const fileLineCounts = (taskId && lineCounts[taskId]) ? lineCounts[taskId] : {};
59
+ const { valid, errors } = validateResult(resultObj, fileLineCounts);
60
+
61
+ if (valid) {
62
+ passing.push(resultObj);
63
+ } else {
64
+ failing.push({ task_id: taskId ?? filename, errors });
65
+ }
66
+ }
67
+
68
+ writeFileSync(auditResultsPath, JSON.stringify(passing, null, 2));
69
+
70
+ if (failing.length > 0) {
71
+ writeFileSync(failedTasksPath, JSON.stringify(failing, null, 2));
72
+ console.warn(`${failing.length} task(s) failed validation and were excluded:`);
73
+ for (const f of failing) {
74
+ console.warn(` ✗ ${f.task_id}: ${f.errors[0]}`);
75
+ }
76
+ }
77
+
78
+ const total = files.length;
79
+ console.log(`✓ ${passing.length}/${total} tasks valid → ${auditResultsPath}`);
80
+ if (failing.length > 0) {
81
+ console.log(" Re-run those tasks in the next cycle.");
82
+ }
83
+
84
+ process.exit(0);
@@ -0,0 +1,155 @@
1
+ import { dirname, resolve, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const PROJECT_ROOT = resolve(__dirname, "..");
8
+
9
+ // Parse --run-id
10
+ const runIdIdx = process.argv.indexOf("--run-id");
11
+ if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
12
+ console.error("Usage: node dispatch/prepare-dispatch.mjs --run-id <run_id>");
13
+ process.exit(1);
14
+ }
15
+ const run_id = process.argv[runIdIdx + 1];
16
+
17
+ const artifactsDir = join(PROJECT_ROOT, ".audit-artifacts");
18
+ const runDir = join(artifactsDir, "runs", run_id);
19
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
20
+ const dispatchPlanPath = join(runDir, "dispatch-plan.json");
21
+
22
+ if (!existsSync(tasksPath)) {
23
+ console.error(`File not found: ${tasksPath}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
28
+ const lensDefinitions = JSON.parse(
29
+ readFileSync(join(__dirname, "lens-definitions.json"), "utf8")
30
+ );
31
+ const auditResultSchema = JSON.parse(
32
+ readFileSync(join(PROJECT_ROOT, "schemas", "audit_result.schema.json"), "utf8")
33
+ );
34
+ const findingSchema = JSON.parse(
35
+ readFileSync(join(PROJECT_ROOT, "schemas", "finding.schema.json"), "utf8")
36
+ );
37
+
38
+ function buildPrompt(task, lensDef, auditResultSchema, findingSchema, outputPath, runId, artifactsDir) {
39
+ const fallback = {
40
+ task_id: task.task_id,
41
+ unit_id: task.unit_id,
42
+ pass_id: task.pass_id,
43
+ lens: task.lens,
44
+ file_coverage: task.file_paths.map(p => ({ path: p, total_lines: task.file_line_counts[p] })),
45
+ findings: [],
46
+ notes: ["Validation failed after 3 attempts — empty result written as fallback."]
47
+ };
48
+
49
+ return `You are a code auditor. Perform a bounded audit of the files listed below under the specified lens.
50
+
51
+ ## Task metadata
52
+ ${JSON.stringify(task, null, 2)}
53
+
54
+ ## Files to read
55
+ Read each path in task.file_paths using your Read tool. The repo root is the current working directory — paths are repo-relative (e.g. "src/foo.ts").
56
+
57
+ file_line_counts gives the expected total line count for each file. Use those exact values for file_coverage[].total_lines in your result.
58
+
59
+ ## Lens: ${task.lens}
60
+ ${lensDef.description}
61
+
62
+ Do NOT report: ${lensDef.do_not_report}
63
+
64
+ ## Output format
65
+ Write your result as a single JSON **object** (not an array) to this exact path:
66
+ ${outputPath}
67
+
68
+ The result must conform to the following schema:
69
+
70
+ ### audit_result.schema.json
71
+ ${JSON.stringify(auditResultSchema, null, 2)}
72
+
73
+ ### finding.schema.json
74
+ ${JSON.stringify(findingSchema, null, 2)}
75
+
76
+ ## Hard constraints (violations will fail validation)
77
+ 1. NEVER set line_end higher than the file's actual line count.
78
+ Use file_line_counts as your reference. If in doubt, leave line_end omitted.
79
+ 2. Every finding MUST have ALL required fields:
80
+ id, title, category, severity, confidence, lens, summary, affected_files, evidence
81
+ 3. lens on every finding must be exactly "${task.lens}"
82
+ 4. No fields outside the schema. Forbidden: "recommendation", "tags", "description" (use "summary").
83
+ 5. evidence[] must contain at least one specific file:line reference.
84
+ Format: "path/to/file.ts:42 - brief description of what you see there"
85
+ 6. affected_files[] entries are OBJECTS with a "path" key — NOT plain strings.
86
+ Example: {"path": "src/foo.ts", "line_start": 10, "line_end": 20, "symbol": "myFunc"}
87
+ 7. Only reference file paths that appear in this task's file_paths.
88
+ 8. findings: [] is correct when you genuinely find nothing. Do not invent findings.
89
+
90
+ ## Validation step (required)
91
+ After writing your result, run:
92
+ node dispatch/validate-result.mjs ${runId} ${task.task_id}
93
+
94
+ - If it exits 0: you are done. Stop.
95
+ - If it exits non-zero: read the error output, fix the JSON, rewrite the file, run again.
96
+ - Repeat up to 3 times.
97
+
98
+ If you cannot produce a valid result after 3 attempts, write this fallback (substituting real values):
99
+ ${JSON.stringify(fallback, null, 2)}
100
+
101
+ Then validate the fallback passes before finishing.`;
102
+ }
103
+
104
+ mkdirSync(join(runDir, "task-results"), { recursive: true });
105
+
106
+ const plan = [];
107
+ let largestTask = null;
108
+ let largestLines = 0;
109
+
110
+ for (const task of tasks) {
111
+ const sanitizedId = task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
112
+ const outputPath = join(runDir, "task-results", sanitizedId + ".json");
113
+ const lensDef = lensDefinitions[task.lens];
114
+
115
+ if (!lensDef) {
116
+ console.warn(`Warning: no lens definition for '${task.lens}' (task ${task.task_id})`);
117
+ }
118
+
119
+ const totalFileLines = Object.values(task.file_line_counts).reduce((a, b) => a + b, 0);
120
+ const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalFileLines} lines) — ${task.lens} lens`;
121
+ const prompt = buildPrompt(
122
+ task,
123
+ lensDef ?? { description: task.lens, do_not_report: "N/A" },
124
+ auditResultSchema,
125
+ findingSchema,
126
+ outputPath,
127
+ run_id,
128
+ artifactsDir
129
+ );
130
+
131
+ if (totalFileLines > largestLines) {
132
+ largestLines = totalFileLines;
133
+ largestTask = task.task_id;
134
+ }
135
+
136
+ if (totalFileLines > 1500) {
137
+ console.warn(`Warning: large task ${task.task_id} (~${totalFileLines} lines) may hit quota limits`);
138
+ }
139
+
140
+ plan.push({ task_id: task.task_id, description, output_path: outputPath, prompt });
141
+ }
142
+
143
+ writeFileSync(dispatchPlanPath, JSON.stringify(plan, null, 2));
144
+
145
+ console.log(`Wrote dispatch-plan.json — ${plan.length} tasks ready for dispatch`);
146
+ if (largestTask) {
147
+ console.log(`Largest task: ${largestTask} (~${largestLines} lines)`);
148
+ }
149
+ console.log("");
150
+ console.log("--- ORCHESTRATOR INSTRUCTIONS ---");
151
+ console.log("Read dispatch-plan.json. For each entry, fire one Agent call with:");
152
+ console.log(" description: <entry.description>");
153
+ console.log(" prompt: <entry.prompt>");
154
+ console.log(`Fire all ${plan.length} calls in a single message for parallel execution.`);
155
+ console.log(`When all complete, run: node dispatch/merge-results.mjs --run-id ${run_id}`);
@@ -0,0 +1,67 @@
1
+ import { dirname, resolve, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { validateResult } from "./validate.mjs";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const PROJECT_ROOT = resolve(__dirname, "..");
9
+
10
+ const run_id = process.argv[2];
11
+ const task_id = process.argv[3];
12
+
13
+ if (!run_id || !task_id) {
14
+ console.error("Usage: node dispatch/validate-result.mjs <run_id> <task_id>");
15
+ process.exit(1);
16
+ }
17
+
18
+ // Locate artifacts_dir
19
+ let artifactsDir = join(PROJECT_ROOT, ".audit-artifacts");
20
+ const sessionConfigPath = join(artifactsDir, "session-config.json");
21
+ if (existsSync(sessionConfigPath)) {
22
+ try {
23
+ const cfg = JSON.parse(readFileSync(sessionConfigPath, "utf8"));
24
+ if (cfg.artifacts_dir) artifactsDir = cfg.artifacts_dir;
25
+ } catch {
26
+ // use default
27
+ }
28
+ }
29
+
30
+ const sanitized = task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
31
+ const resultPath = join(artifactsDir, "runs", run_id, "task-results", sanitized + ".json");
32
+
33
+ if (!existsSync(resultPath)) {
34
+ console.error(`File not found: ${resultPath}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ let resultObj;
39
+ try {
40
+ resultObj = JSON.parse(readFileSync(resultPath, "utf8"));
41
+ } catch (e) {
42
+ console.error(`Invalid JSON in ${resultPath}: ${e.message}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const tasksPath = join(artifactsDir, "runs", run_id, "pending-audit-tasks.json");
47
+ let fileLineCounts = {};
48
+ if (existsSync(tasksPath)) {
49
+ try {
50
+ const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
51
+ const task = tasks.find(t => t.task_id === task_id);
52
+ fileLineCounts = task?.file_line_counts ?? {};
53
+ } catch {
54
+ // use empty
55
+ }
56
+ }
57
+
58
+ const { valid, errors } = validateResult(resultObj, fileLineCounts);
59
+
60
+ if (valid) {
61
+ console.log("✓ valid:", task_id);
62
+ process.exit(0);
63
+ } else {
64
+ console.error("✗ invalid:", task_id);
65
+ console.error(JSON.stringify(errors, null, 2));
66
+ process.exit(1);
67
+ }
@@ -0,0 +1,88 @@
1
+ import { dirname, resolve, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { readFileSync } from "node:fs";
4
+ import Ajv2020 from "ajv/dist/2020.js";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const PROJECT_ROOT = resolve(__dirname, "..");
9
+ const SCHEMAS_DIR = join(PROJECT_ROOT, "schemas");
10
+
11
+ function loadSchema(name) {
12
+ return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), "utf8"));
13
+ }
14
+
15
+ let _ajv = null;
16
+ let _validateFn = null;
17
+
18
+ function getValidator() {
19
+ if (_validateFn) return _validateFn;
20
+ _ajv = new Ajv2020({ strict: false, allErrors: true });
21
+ _ajv.addSchema(loadSchema("finding.schema.json"));
22
+ _validateFn = _ajv.compile(loadSchema("audit_result.schema.json"));
23
+ return _validateFn;
24
+ }
25
+
26
+ function formatAjvError(e) {
27
+ const path = e.instancePath || "(root)";
28
+ return `${path}: ${e.message}${e.params ? " (" + JSON.stringify(e.params) + ")" : ""}`;
29
+ }
30
+
31
+ /**
32
+ * @param {object} resultObj — parsed JSON from a task-results file
33
+ * @param {Record<string, number>} fileLineCounts — from the task's file_line_counts
34
+ * @returns {{ valid: boolean, errors: string[] }}
35
+ */
36
+ export function validateResult(resultObj, fileLineCounts) {
37
+ const validate = getValidator();
38
+ const schemaValid = validate(resultObj);
39
+
40
+ if (!schemaValid) {
41
+ return { valid: false, errors: validate.errors.map(formatAjvError) };
42
+ }
43
+
44
+ const errors = [];
45
+
46
+ // Line range constraint
47
+ for (const finding of resultObj.findings) {
48
+ for (const entry of finding.affected_files) {
49
+ if (entry.line_end !== undefined) {
50
+ const coverage = resultObj.file_coverage.find(fc => fc.path === entry.path);
51
+ if (!coverage) {
52
+ errors.push(`affected_files path '${entry.path}' not in file_coverage`);
53
+ } else if (entry.line_end > coverage.total_lines) {
54
+ errors.push(
55
+ `finding '${finding.id}': line_end ${entry.line_end} exceeds total_lines ${coverage.total_lines} for ${entry.path}`
56
+ );
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ // Lens consistency
63
+ for (const finding of resultObj.findings) {
64
+ if (finding.lens !== resultObj.lens) {
65
+ errors.push(
66
+ `finding '${finding.id}': lens '${finding.lens}' does not match task lens '${resultObj.lens}'`
67
+ );
68
+ }
69
+ }
70
+
71
+ // affected_files paths in scope
72
+ const allowedPaths = new Set(resultObj.file_coverage.map(fc => fc.path));
73
+ for (const finding of resultObj.findings) {
74
+ for (const entry of finding.affected_files) {
75
+ if (!allowedPaths.has(entry.path)) {
76
+ errors.push(
77
+ `finding '${finding.id}': affected path '${entry.path}' not in task file_coverage`
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ if (errors.length > 0) {
84
+ return { valid: false, errors };
85
+ }
86
+
87
+ return { valid: true, errors: [] };
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  "dist/**",
12
12
  "audit-code.mjs",
13
13
  "audit-code-wrapper-lib.mjs",
14
+ "dispatch/**",
14
15
  "schemas/**",
15
16
  "skills/audit-code/**",
16
17
  "scripts/postinstall.mjs",
@@ -1,19 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir } from 'os';
3
3
  import { join, dirname } from 'path';
4
- import { mkdirSync, copyFileSync, existsSync } from 'fs';
4
+ import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
5
5
  import { fileURLToPath } from 'url';
6
6
 
7
- const pkgRoot = dirname(fileURLToPath(new URL('.', import.meta.url)));
8
- const sourceFile = join(pkgRoot, '..', 'skills', 'audit-code', 'audit-code.prompt.md');
7
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ const sourceFile = join(pkgRoot, 'skills', 'audit-code', 'audit-code.prompt.md');
9
9
  const destDir = join(homedir(), '.claude', 'commands');
10
10
  const destFile = join(destDir, 'audit-code.md');
11
11
 
12
+ if (!existsSync(sourceFile)) {
13
+ console.warn(`audit-code: skill source not found at ${sourceFile} — skipping Claude command install`);
14
+ process.exit(0);
15
+ }
16
+
12
17
  try {
13
18
  mkdirSync(destDir, { recursive: true });
14
- copyFileSync(sourceFile, destFile);
15
- console.log(`audit-code: installed /audit-code Claude command → ${destFile}`);
19
+ const action = existsSync(destFile) ? 'updated' : 'installed';
20
+ // Read then write to avoid Windows file-lock issues with copyFileSync
21
+ writeFileSync(destFile, readFileSync(sourceFile));
22
+ console.log(`audit-code: ${action} /audit-code Claude command → ${destFile}`);
16
23
  } catch (err) {
17
- // Non-fatal — CLI still works, just no slash command autocomplete
18
24
  console.warn(`audit-code: could not install Claude command (${err.message})`);
25
+ console.warn(` To install manually, run:`);
26
+ console.warn(` cp "${sourceFile}" "${destFile}"`);
19
27
  }