@dailephd/my-dev-kit-lab 0.2.2 → 0.3.1

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 (42) hide show
  1. package/README.md +17 -7
  2. package/dist/scripts/audits/runAudit.js +102 -0
  3. package/dist/src/audits/codeRot/codeRotDetectors.js +17 -0
  4. package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +271 -0
  5. package/dist/src/audits/codeRot/detectors/crossPlatformRotDetector.js +214 -0
  6. package/dist/src/audits/codeRot/detectors/deadCodeCandidateDetector.js +377 -0
  7. package/dist/src/audits/codeRot/detectors/dependencyEnvironmentRotDetector.js +244 -0
  8. package/dist/src/audits/codeRot/detectors/docsCodeMismatchDetector.js +219 -0
  9. package/dist/src/audits/codeRot/detectors/duplicateImplementationDetector.js +350 -0
  10. package/dist/src/audits/codeRot/detectors/packageReleaseRotDetector.js +325 -0
  11. package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +385 -0
  12. package/dist/src/audits/codeRot/detectors/staleCommandReferenceDetector.js +135 -0
  13. package/dist/src/audits/codeRot/detectors/testRotDetector.js +410 -0
  14. package/dist/src/audits/codeRot/utils/boundedRead.js +23 -0
  15. package/dist/src/audits/codeRot/utils/docClaimPatterns.js +62 -0
  16. package/dist/src/audits/codeRot/utils/docCommandReferences.js +64 -0
  17. package/dist/src/audits/codeRot/utils/filePatternUtils.js +79 -0
  18. package/dist/src/audits/codeRot/utils/issueFactories.js +38 -0
  19. package/dist/src/audits/codeRot/utils/sourceFactsLookup.js +53 -0
  20. package/dist/src/audits/codeRot/utils/textLines.js +12 -0
  21. package/dist/src/audits/core/auditConfig.js +125 -0
  22. package/dist/src/audits/core/auditExitCode.js +75 -0
  23. package/dist/src/audits/core/auditIssue.js +20 -0
  24. package/dist/src/audits/core/auditRegistry.js +40 -0
  25. package/dist/src/audits/core/auditRunner.js +74 -0
  26. package/dist/src/audits/core/auditTarget.js +19 -0
  27. package/dist/src/audits/core/auditTypes.js +30 -0
  28. package/dist/src/audits/core/collectSourceFacts.js +142 -0
  29. package/dist/src/audits/core/languageAnalyzerRegistry.js +33 -0
  30. package/dist/src/audits/core/projectInventory.js +458 -0
  31. package/dist/src/audits/core/sourceFacts.js +17 -0
  32. package/dist/src/audits/core/sourceOfTruth.js +339 -0
  33. package/dist/src/audits/core/textLines.js +24 -0
  34. package/dist/src/audits/core/typescriptJavaScriptAnalyzer.js +299 -0
  35. package/dist/src/audits/report/auditReportModel.js +253 -0
  36. package/dist/src/audits/report/auditReportPaths.js +13 -0
  37. package/dist/src/audits/report/renderAuditJsonReport.js +30 -0
  38. package/dist/src/audits/report/renderAuditTextReport.js +181 -0
  39. package/dist/src/audits/report/sanitizeAuditText.js +39 -0
  40. package/dist/src/audits/report/writeAuditReports.js +27 -0
  41. package/dist/src/core/resolveCommand.js +53 -2
  42. package/package.json +10 -6
@@ -0,0 +1,38 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 3 — shared code-rot issue construction helpers.
3
+ //
4
+ // Issue ids are content-addressed (detectorId + a stable, relative-path-only
5
+ // cue) rather than counter-based, so the same underlying problem always
6
+ // produces the same id across runs -- this is what makes de-duplication and
7
+ // "stable issue ids" (Batch 3 spec 3.8) trivial: two candidate issues with
8
+ // the same id are the same issue.
9
+ // ---------------------------------------------------------------------------
10
+ // Builds a deterministic id from a detector id and one or more relative-path
11
+ // or short-string cues. Never embeds an absolute path -- callers must pass
12
+ // already-relative paths (e.g. from InventoryFileEntry.relativePath).
13
+ export function makeIssueId(detectorId, ...cues) {
14
+ const sanitizedCues = cues.map((cue) => cue.replace(/\\/g, "/"));
15
+ return [detectorId, ...sanitizedCues].join(":");
16
+ }
17
+ export function makeCodeRotIssue(input) {
18
+ const { idCues, evidence, affectedFiles, ...rest } = input;
19
+ return {
20
+ ...rest,
21
+ id: makeIssueId(rest.detectorId, ...idCues),
22
+ evidence: evidence ?? [],
23
+ affectedFiles: affectedFiles ?? [],
24
+ };
25
+ }
26
+ // Deduplicates a list of already-built issues by id, keeping the first
27
+ // occurrence and preserving overall array order (stable de-duplication).
28
+ export function deduplicateIssuesById(issues) {
29
+ const seen = new Set();
30
+ const result = [];
31
+ for (const issue of issues) {
32
+ if (seen.has(issue.id))
33
+ continue;
34
+ seen.add(issue.id);
35
+ result.push(issue);
36
+ }
37
+ return result;
38
+ }
@@ -0,0 +1,53 @@
1
+ import { baseNameNoExt } from "./filePatternUtils.js";
2
+ // ---------------------------------------------------------------------------
3
+ // v0.3.1 Batch 4 -- shared source-facts lookup helpers for code-rot
4
+ // detectors.
5
+ //
6
+ // ctx.sourceFacts is optional (see auditRegistry.ts) precisely so existing
7
+ // detector tests that build a literal AuditDetectorContext without it keep
8
+ // working unchanged -- every helper here accepts `undefined` and degrades to
9
+ // an empty result rather than throwing, so a caller never needs its own
10
+ // null-check before using them.
11
+ // ---------------------------------------------------------------------------
12
+ // Indexes a SourceFactsSnapshot's files by relativePath for O(1) per-file
13
+ // lookups. Returns an empty map when sourceFacts is undefined.
14
+ export function indexSourceFactsByPath(sourceFacts) {
15
+ const index = new Map();
16
+ if (!sourceFacts)
17
+ return index;
18
+ for (const file of sourceFacts.files)
19
+ index.set(file.relativePath, file);
20
+ return index;
21
+ }
22
+ // Returns the facts for relativePath only when parseStatus is "parsed" --
23
+ // callers that want analyzer-derived evidence (imports/exports/declarations)
24
+ // should use this rather than reading the index directly, since
25
+ // file-level-only/parse-error/skipped/unsupported entries carry empty fact
26
+ // arrays that would silently look like "no imports" rather than "unknown".
27
+ export function getParsedSourceFacts(index, relativePath) {
28
+ const facts = index.get(relativePath);
29
+ return facts && facts.parseStatus === "parsed" ? facts : null;
30
+ }
31
+ // Basenames referenced by any parsed file's relative import/re-export
32
+ // specifier, lowercased. Mirrors deadCodeCandidateDetector.ts's regex-based
33
+ // buildReverseReferenceIndex() basename matching, but sourced from
34
+ // structured analyzer facts -- so it also catches import forms the regex
35
+ // misses (e.g. multi-line import statements) for files a registered
36
+ // analyzer actually parsed. Returns an empty set when sourceFacts is
37
+ // undefined, so merging it into an existing Set is always safe.
38
+ export function collectRelativeImportBasenames(sourceFacts) {
39
+ const basenames = new Set();
40
+ if (!sourceFacts)
41
+ return basenames;
42
+ for (const file of sourceFacts.files) {
43
+ if (file.parseStatus !== "parsed")
44
+ continue;
45
+ for (const imp of file.imports) {
46
+ if (!imp.source.startsWith("."))
47
+ continue;
48
+ const base = baseNameNoExt(imp.source.split("?")[0]);
49
+ basenames.add(base.toLowerCase());
50
+ }
51
+ }
52
+ return basenames;
53
+ }
@@ -0,0 +1,12 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 3 (moved in Batch 6) — shared line-splitting helper.
3
+ //
4
+ // The canonical implementation now lives in src/audits/core/textLines.ts
5
+ // (core is lower-level than codeRot, and src/audits/core/sourceOfTruth.ts
6
+ // needed the same helper -- see that file's header comment). This module is
7
+ // kept as a transparent re-export so every existing codeRot detector/util
8
+ // that imports `splitLines` from "../utils/textLines.js" (or
9
+ // "./textLines.js") keeps working unchanged -- named-export re-exports are
10
+ // transparent to importers, there is no behavior difference.
11
+ // ---------------------------------------------------------------------------
12
+ export { splitLines } from "../../core/textLines.js";
@@ -0,0 +1,125 @@
1
+ import path from "node:path";
2
+ import { AUDIT_FAIL_ON_THRESHOLDS, AUDIT_INCLUDE_AREAS, AUDIT_OUTPUT_FORMATS, AUDIT_TYPES, DEFAULT_AUDIT_FAIL_ON_THRESHOLD, DEFAULT_AUDIT_INCLUDE_AREAS, DEFAULT_AUDIT_OUTPUT_FORMATS, DEFAULT_AUDIT_TYPES, PLANNED_AUDIT_TYPES, } from "./auditTypes.js";
3
+ const FLAGS_WITH_VALUE = {
4
+ "--target": "target",
5
+ "--types": "types",
6
+ "--include": "include",
7
+ "--format": "format",
8
+ "--fail-on": "failOn",
9
+ "--out": "out",
10
+ };
11
+ export function parseAuditArgs(argv) {
12
+ const result = {};
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const arg = argv[i];
15
+ const key = FLAGS_WITH_VALUE[arg];
16
+ if (!key)
17
+ continue;
18
+ if (i + 1 >= argv.length) {
19
+ throw new Error(`Missing value for ${arg}. Expected a value after ${arg}.`);
20
+ }
21
+ result[key] = argv[++i];
22
+ }
23
+ return result;
24
+ }
25
+ export function normalizeAuditConfig(raw, toolRoot) {
26
+ const types = parseTypesOption(raw.types);
27
+ const include = parseIncludeOption(raw.include);
28
+ const formats = parseFormatOption(raw.format);
29
+ const failOn = parseFailOnOption(raw.failOn);
30
+ const out = raw.out !== undefined ? resolveOutDir(raw.out) : defaultOutDir(toolRoot, types.value);
31
+ return {
32
+ targetPathArg: raw.target,
33
+ targetMode: raw.target === undefined ? "self" : "external",
34
+ types: types.value,
35
+ typesWereDefault: types.wasDefault,
36
+ include: include.value,
37
+ includeWereDefault: include.wasDefault,
38
+ formats: formats.value,
39
+ formatsWereDefault: formats.wasDefault,
40
+ failOn: failOn.value,
41
+ failOnWasDefault: failOn.wasDefault,
42
+ out,
43
+ outWasDefault: raw.out === undefined,
44
+ isDefaultRun: raw.target === undefined &&
45
+ raw.types === undefined &&
46
+ raw.include === undefined &&
47
+ raw.format === undefined &&
48
+ raw.failOn === undefined &&
49
+ raw.out === undefined,
50
+ };
51
+ }
52
+ function splitAndNormalizeList(raw, flagName) {
53
+ const parts = raw.split(",").map((p) => p.trim());
54
+ if (parts.some((p) => p.length === 0)) {
55
+ throw new Error(`Invalid ${flagName} value: empty entries are not allowed. Got "${raw}".`);
56
+ }
57
+ return parts;
58
+ }
59
+ function parseTypesOption(raw) {
60
+ if (raw === undefined) {
61
+ return { value: [...DEFAULT_AUDIT_TYPES], wasDefault: true };
62
+ }
63
+ const parts = splitAndNormalizeList(raw, "--types");
64
+ const unknown = parts.filter((p) => !AUDIT_TYPES.includes(p));
65
+ if (unknown.length > 0) {
66
+ throw new Error(`Invalid --types value(s): ${unknown.join(", ")}. Valid values are: ${AUDIT_TYPES.join(", ")}.`);
67
+ }
68
+ const planned = parts.filter((p) => PLANNED_AUDIT_TYPES.includes(p));
69
+ if (planned.length > 0) {
70
+ throw new Error(`--types value(s) planned but not implemented: ${planned.join(", ")}. Only "code-rot" is implemented in this version.`);
71
+ }
72
+ const unique = new Set(parts);
73
+ const ordered = AUDIT_TYPES.filter((id) => unique.has(id));
74
+ return { value: ordered, wasDefault: false };
75
+ }
76
+ function parseIncludeOption(raw) {
77
+ if (raw === undefined) {
78
+ return { value: [...DEFAULT_AUDIT_INCLUDE_AREAS], wasDefault: true };
79
+ }
80
+ const parts = splitAndNormalizeList(raw, "--include");
81
+ const invalid = parts.filter((p) => !AUDIT_INCLUDE_AREAS.includes(p));
82
+ if (invalid.length > 0) {
83
+ throw new Error(`Invalid --include value(s): ${invalid.join(", ")}. Valid values are: ${AUDIT_INCLUDE_AREAS.join(", ")}.`);
84
+ }
85
+ const unique = new Set(parts);
86
+ const ordered = AUDIT_INCLUDE_AREAS.filter((id) => unique.has(id));
87
+ return { value: ordered, wasDefault: false };
88
+ }
89
+ function parseFormatOption(raw) {
90
+ if (raw === undefined) {
91
+ return { value: [...DEFAULT_AUDIT_OUTPUT_FORMATS], wasDefault: true };
92
+ }
93
+ const parts = splitAndNormalizeList(raw, "--format");
94
+ const invalid = parts.filter((p) => !AUDIT_OUTPUT_FORMATS.includes(p));
95
+ if (invalid.length > 0) {
96
+ throw new Error(`Invalid --format value(s): ${invalid.join(", ")}. Valid values are: ${AUDIT_OUTPUT_FORMATS.join(", ")}.`);
97
+ }
98
+ const unique = new Set(parts);
99
+ const ordered = AUDIT_OUTPUT_FORMATS.filter((id) => unique.has(id));
100
+ return { value: ordered, wasDefault: false };
101
+ }
102
+ function parseFailOnOption(raw) {
103
+ if (raw === undefined) {
104
+ return { value: DEFAULT_AUDIT_FAIL_ON_THRESHOLD, wasDefault: true };
105
+ }
106
+ const value = raw.trim();
107
+ if (!AUDIT_FAIL_ON_THRESHOLDS.includes(value)) {
108
+ throw new Error(`Invalid --fail-on value: "${raw}". Valid values are: ${AUDIT_FAIL_ON_THRESHOLDS.join(", ")}.`);
109
+ }
110
+ return { value: value, wasDefault: false };
111
+ }
112
+ function resolveOutDir(raw) {
113
+ const trimmed = raw.trim();
114
+ if (trimmed.length === 0) {
115
+ throw new Error("Invalid --out value: path must not be empty.");
116
+ }
117
+ return path.resolve(trimmed);
118
+ }
119
+ function defaultOutDir(toolRoot, types) {
120
+ // Batch 1 only ever has "code-rot" as a valid resolved type, but this stays
121
+ // written generically so later batches (quality/security/project) don't
122
+ // need to touch this function, only extend the audit-type vocabulary.
123
+ const primaryType = types[0] ?? "code-rot";
124
+ return path.join(path.resolve(toolRoot), "reports", "audits", primaryType);
125
+ }
@@ -0,0 +1,75 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 1 — audit exit code policy.
3
+ //
4
+ // 0 = no issue met the --fail-on threshold (and no config/target/runtime
5
+ // error occurred).
6
+ // 1 = at least one issue met or exceeded the --fail-on threshold.
7
+ // 2 = invalid config, invalid target, or an audit runtime failure. This
8
+ // code is never returned by calculateAuditExitCode() below (a pure,
9
+ // issue-driven function) -- it is the caller's (scripts/audits/
10
+ // runAudit.ts) responsibility to catch parse/config/target errors and
11
+ // exit with AUDIT_EXIT_CODES.FATAL_ERROR directly, mirroring how
12
+ // scripts/security/validate.ts handles its own parse/target try/catch.
13
+ // ---------------------------------------------------------------------------
14
+ export const AUDIT_EXIT_CODES = {
15
+ SUCCESS: 0,
16
+ THRESHOLD_BREACHED: 1,
17
+ FATAL_ERROR: 2,
18
+ };
19
+ // "info" is a valid issue severity but is intentionally below every
20
+ // selectable --fail-on threshold (including "low") -- it can never breach.
21
+ const SEVERITY_RANK = {
22
+ info: 0,
23
+ low: 1,
24
+ medium: 2,
25
+ high: 3,
26
+ blocker: 4,
27
+ };
28
+ // "none" ranks above every severity so it can never be breached.
29
+ const THRESHOLD_RANK = {
30
+ low: 1,
31
+ medium: 2,
32
+ high: 3,
33
+ blocker: 4,
34
+ none: Number.POSITIVE_INFINITY,
35
+ };
36
+ export function issueBreachesFailOnThreshold(severity, failOn) {
37
+ if (failOn === "none")
38
+ return false;
39
+ return SEVERITY_RANK[severity] >= THRESHOLD_RANK[failOn];
40
+ }
41
+ // v0.3.0 Batch 5 — additive export. Report-building code (src/audits/report/
42
+ // auditReportModel.ts) needs the same severity ranking to compute a
43
+ // "highest severity present" summary field, and must not hardcode a second
44
+ // copy of this table (see Batch 5 spec 3.2). Signature/behavior of the two
45
+ // functions above is unchanged.
46
+ export function getHighestSeverity(issues) {
47
+ if (issues.length === 0)
48
+ return null;
49
+ let highest = issues[0].severity;
50
+ for (const issue of issues) {
51
+ if (SEVERITY_RANK[issue.severity] > SEVERITY_RANK[highest]) {
52
+ highest = issue.severity;
53
+ }
54
+ }
55
+ return highest;
56
+ }
57
+ // Pure function: given a final issue list and the configured --fail-on
58
+ // threshold, decides between SUCCESS and THRESHOLD_BREACHED. Never returns
59
+ // FATAL_ERROR -- see file header.
60
+ export function calculateAuditExitCode(issues, failOn) {
61
+ if (failOn === "none") {
62
+ return { exitCode: AUDIT_EXIT_CODES.SUCCESS, reason: "--fail-on none: findings never block the command." };
63
+ }
64
+ const breaching = issues.filter((issue) => issueBreachesFailOnThreshold(issue.severity, failOn));
65
+ if (breaching.length > 0) {
66
+ return {
67
+ exitCode: AUDIT_EXIT_CODES.THRESHOLD_BREACHED,
68
+ reason: `${breaching.length} issue(s) met or exceeded the --fail-on ${failOn} threshold.`,
69
+ };
70
+ }
71
+ return {
72
+ exitCode: AUDIT_EXIT_CODES.SUCCESS,
73
+ reason: `No issues met the --fail-on ${failOn} threshold.`,
74
+ };
75
+ }
@@ -0,0 +1,20 @@
1
+ export const AUDIT_EVIDENCE_KINDS = [
2
+ "file",
3
+ "command",
4
+ "diff",
5
+ "reference",
6
+ "observation",
7
+ ];
8
+ const MAX_EXCERPT_LENGTH = 400;
9
+ // Bounds an excerpt to a safe preview length so evidence never embeds huge
10
+ // file contents. Does not attempt secret redaction here — detectors that
11
+ // scan for secret-shaped content are responsible for redacting before this
12
+ // point, same division of responsibility documented in exploitEvidence.ts.
13
+ export function boundExcerpt(raw, maxLength = MAX_EXCERPT_LENGTH) {
14
+ if (raw.length <= maxLength)
15
+ return raw;
16
+ return `${raw.slice(0, maxLength)}... [truncated, ${raw.length} chars total]`;
17
+ }
18
+ export function makeAuditEvidence(evidence) {
19
+ return evidence.excerpt !== undefined ? { ...evidence, excerpt: boundExcerpt(evidence.excerpt) } : evidence;
20
+ }
@@ -0,0 +1,40 @@
1
+ import { STALE_COMMAND_REFERENCE_DETECTOR, DOCS_CODE_MISMATCH_DETECTOR, PACKAGE_RELEASE_ROT_DETECTOR, DUPLICATE_IMPLEMENTATION_DETECTOR, DEAD_CODE_CANDIDATE_DETECTOR, TEST_ROT_DETECTOR, ARCHITECTURE_DRIFT_DETECTOR, DEPENDENCY_ENVIRONMENT_ROT_DETECTOR, CROSS_PLATFORM_ROT_DETECTOR, SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR, } from "../codeRot/codeRotDetectors.js";
2
+ // v0.3.0 Batch 3/4 — code-rot detectors. Batch 4 completes the full
3
+ // code-rot detector family planned in docs/ROADMAP.md's v0.3.0 section:
4
+ // stale command/workflow reference, docs/code mismatch, package/release
5
+ // rot (Batch 3), plus duplicate-implementation, dead-code, test-rot,
6
+ // architecture-drift, dependency/environment-rot, cross-platform-rot, and
7
+ // security/validation-assumption-rot (Batch 4).
8
+ export const DEFAULT_AUDIT_REGISTRY = createAuditRegistry([
9
+ STALE_COMMAND_REFERENCE_DETECTOR,
10
+ DOCS_CODE_MISMATCH_DETECTOR,
11
+ PACKAGE_RELEASE_ROT_DETECTOR,
12
+ DUPLICATE_IMPLEMENTATION_DETECTOR,
13
+ DEAD_CODE_CANDIDATE_DETECTOR,
14
+ TEST_ROT_DETECTOR,
15
+ ARCHITECTURE_DRIFT_DETECTOR,
16
+ DEPENDENCY_ENVIRONMENT_ROT_DETECTOR,
17
+ CROSS_PLATFORM_ROT_DETECTOR,
18
+ SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR,
19
+ ]);
20
+ // Validates a detector list has no duplicate ids. Later batches call this
21
+ // when assembling their own registries; Batch 1's empty default trivially
22
+ // passes.
23
+ export function createAuditRegistry(detectors) {
24
+ const seen = new Set();
25
+ for (const detector of detectors) {
26
+ if (seen.has(detector.id)) {
27
+ throw new Error(`Duplicate audit detector id: "${detector.id}".`);
28
+ }
29
+ seen.add(detector.id);
30
+ }
31
+ return detectors;
32
+ }
33
+ // Filters a registry down to detectors matching the selected audit types and
34
+ // at least one selected include area. Order is preserved from the input
35
+ // registry (deterministic, not re-sorted).
36
+ export function selectDetectors(registry, types, include) {
37
+ const typeSet = new Set(types);
38
+ const includeSet = new Set(include);
39
+ return registry.filter((detector) => typeSet.has(detector.auditType) && detector.supportedIncludeAreas.some((area) => includeSet.has(area)));
40
+ }
@@ -0,0 +1,74 @@
1
+ import { calculateAuditExitCode } from "./auditExitCode.js";
2
+ import { DEFAULT_AUDIT_REGISTRY, selectDetectors } from "./auditRegistry.js";
3
+ import { resolveAuditTarget } from "./auditTarget.js";
4
+ import { scanProjectInventory } from "./projectInventory.js";
5
+ import { collectSourceOfTruth } from "./sourceOfTruth.js";
6
+ import { collectSourceFacts } from "./collectSourceFacts.js";
7
+ export async function runAudit(options) {
8
+ const { config, toolRoot } = options;
9
+ const registry = options.registry ?? DEFAULT_AUDIT_REGISTRY;
10
+ const target = options.target ?? resolveAuditTarget(config.targetPathArg, toolRoot);
11
+ const inventory = scanProjectInventory(target.rootPath);
12
+ const sourceOfTruth = collectSourceOfTruth(target.rootPath, inventory);
13
+ const sourceFacts = await collectSourceFacts(target.rootPath, inventory);
14
+ const selected = selectDetectors(registry, config.types, config.include);
15
+ const ctx = { target, config, inventory, sourceOfTruth, sourceFacts };
16
+ const issues = [];
17
+ const skippedDetectors = [];
18
+ const detectorErrors = [];
19
+ // Sequential, in registry order -- keeps issue ordering deterministic
20
+ // without needing a secondary sort step.
21
+ for (const detector of selected) {
22
+ try {
23
+ if (detector.shouldSkip) {
24
+ const decision = detector.shouldSkip(ctx);
25
+ if (decision.skip) {
26
+ skippedDetectors.push({ id: detector.id, reason: decision.reason });
27
+ continue;
28
+ }
29
+ }
30
+ const detectorIssues = await detector.run(ctx);
31
+ issues.push(...detectorIssues);
32
+ }
33
+ catch (err) {
34
+ detectorErrors.push({
35
+ id: detector.id,
36
+ message: err instanceof Error ? err.message : String(err),
37
+ });
38
+ }
39
+ }
40
+ const issueCounts = countIssuesBySeverity(issues);
41
+ const { exitCode, reason } = calculateAuditExitCode(issues, config.failOn);
42
+ return {
43
+ configSummary: {
44
+ types: config.types,
45
+ include: config.include,
46
+ formats: config.formats,
47
+ failOn: config.failOn,
48
+ out: config.out,
49
+ isDefaultRun: config.isDefaultRun,
50
+ },
51
+ targetSummary: {
52
+ rootPath: target.rootPath,
53
+ displayName: target.displayName,
54
+ isSelf: target.isSelf,
55
+ },
56
+ issues,
57
+ skippedDetectors,
58
+ detectorErrors,
59
+ issueCounts,
60
+ exitCode,
61
+ exitReason: reason,
62
+ inventory,
63
+ sourceOfTruth,
64
+ sourceFacts,
65
+ noDetectorsRegistered: selected.length === 0,
66
+ };
67
+ }
68
+ function countIssuesBySeverity(issues) {
69
+ const counts = { blocker: 0, high: 0, medium: 0, low: 0, info: 0 };
70
+ for (const issue of issues) {
71
+ counts[issue.severity] += 1;
72
+ }
73
+ return counts;
74
+ }
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ import { resolveLocalProjectTarget } from "../../core/localProjectTarget.js";
3
+ // Resolves and validates an audit target. Throws a descriptive Error (no
4
+ // stack trace wrapper) for a missing path or a non-directory path. Does not
5
+ // modify the target in any way.
6
+ export function resolveAuditTarget(targetPathArg, toolRoot) {
7
+ const metadata = resolveLocalProjectTarget(targetPathArg, toolRoot);
8
+ const displayName = metadata.packageName ?? path.basename(metadata.targetRoot);
9
+ return {
10
+ rootPath: metadata.targetRoot,
11
+ displayName,
12
+ exists: true,
13
+ isDirectory: true,
14
+ packageJsonPath: metadata.hasPackageJson ? path.join(metadata.targetRoot, "package.json") : null,
15
+ gitRoot: metadata.hasGit ? metadata.targetRoot : null,
16
+ isSelf: metadata.isSelf,
17
+ safeReportOutputRoot: path.join(path.resolve(toolRoot), "reports", "audits"),
18
+ };
19
+ }
@@ -0,0 +1,30 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 1 — generic audit framework core vocabulary.
3
+ //
4
+ // Deliberately kept separate from src/securityValidation/types.ts. Audit and
5
+ // security-validation are two distinct frameworks (see docs/ROADMAP.md); a
6
+ // later v0.3.2 batch may integrate their results into a unified report, but
7
+ // this batch keeps the audit issue schema independent even where a
8
+ // vocabulary word (e.g. "blocker") happens to be shared.
9
+ // ---------------------------------------------------------------------------
10
+ // Recognized --types values. Only "code-rot" has real detector coverage in
11
+ // Batch 1 — the rest are known, named future audit types that must fail
12
+ // cleanly with a "planned but not implemented" message rather than being
13
+ // silently accepted or treated as an unknown/garbage value.
14
+ export const AUDIT_TYPES = ["code-rot", "quality", "security", "project", "all"];
15
+ export const IMPLEMENTED_AUDIT_TYPES = ["code-rot"];
16
+ export const PLANNED_AUDIT_TYPES = ["quality", "security", "project", "all"];
17
+ export const DEFAULT_AUDIT_TYPES = IMPLEMENTED_AUDIT_TYPES;
18
+ export const AUDIT_INCLUDE_AREAS = ["docs", "tests", "package", "architecture", "cli"];
19
+ export const DEFAULT_AUDIT_INCLUDE_AREAS = AUDIT_INCLUDE_AREAS;
20
+ export const AUDIT_OUTPUT_FORMATS = ["text", "json"];
21
+ export const DEFAULT_AUDIT_OUTPUT_FORMATS = ["text", "json"];
22
+ // "none" is a valid, explicit opt-out — findings never block the command.
23
+ export const AUDIT_FAIL_ON_THRESHOLDS = ["blocker", "high", "medium", "low", "none"];
24
+ export const DEFAULT_AUDIT_FAIL_ON_THRESHOLD = "blocker";
25
+ // Issue severity. Distinct from AuditFailOnThreshold: "info" is a valid
26
+ // issue severity but not a selectable --fail-on threshold (an info-severity
27
+ // issue can never breach any threshold, including "low").
28
+ export const AUDIT_SEVERITIES = ["blocker", "high", "medium", "low", "info"];
29
+ export const AUDIT_CONFIDENCE_LEVELS = ["high", "medium", "low"];
30
+ export const AUDIT_FALSE_POSITIVE_RISK_LEVELS = ["high", "medium", "low"];
@@ -0,0 +1,142 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { NORMALIZED_LANGUAGES } from "./projectInventory.js";
4
+ import { DEFAULT_LANGUAGE_ANALYZER_REGISTRY, selectLanguageAnalyzer, } from "./languageAnalyzerRegistry.js";
5
+ import { SOURCE_FACT_PARSE_STATUSES, } from "./sourceFacts.js";
6
+ // ---------------------------------------------------------------------------
7
+ // v0.3.1 Batch 2 -- source facts collector.
8
+ //
9
+ // Consumes the Batch 1 ProjectInventorySnapshot (never re-walks the
10
+ // filesystem) and produces a SourceFactsSnapshot: analyzer facts where a
11
+ // registered analyzer exists, deterministic fallback facts otherwise. Same
12
+ // spirit as sourceOfTruth.ts -- pure data collection, never an AuditIssue,
13
+ // never a write to a target file.
14
+ //
15
+ // Eligibility / omission policy (documented here since it's the single
16
+ // source of truth for the decision):
17
+ // - Only inventory entries with role "source" or "test" are considered.
18
+ // docs/config/package/generated/vendor/build-output/report-output/
19
+ // unknown-role files are omitted entirely from `files` -- source facts
20
+ // are meaningless for them, and omitting (rather than emitting a
21
+ // "skipped" placeholder for every doc/config file in the repo) keeps
22
+ // the snapshot proportional to actual candidate source files.
23
+ // - For an eligible file in a *known* language (typescript, javascript,
24
+ // python, java, kotlin) with no registered analyzer: parseStatus
25
+ // "file-level-only" -- we know the language and already have
26
+ // inventory-level evidence (lineCount), just no analyzer parsed it yet.
27
+ // - For an eligible file in an *unknown* language: parseStatus
28
+ // "unsupported" -- no file-level evidence attempted either.
29
+ // - File content is only read when a registered analyzer actually claims
30
+ // the file -- with an empty default registry (Batch 2), no file is ever
31
+ // read by this collector.
32
+ // ---------------------------------------------------------------------------
33
+ const ANALYZABLE_ROLES = new Set(["source", "test"]);
34
+ const KNOWN_LANGUAGES = new Set(["typescript", "javascript", "python", "java", "kotlin"]);
35
+ export async function collectSourceFacts(targetRoot, inventory, registry = DEFAULT_LANGUAGE_ANALYZER_REGISTRY) {
36
+ const resolvedRoot = path.resolve(targetRoot);
37
+ const files = [];
38
+ const analyzerDiagnostics = [];
39
+ const warnings = [];
40
+ // inventory.files is already sorted by relativePath (Batch 1 guarantee) --
41
+ // filtering preserves that order, so no re-sort is needed here.
42
+ const eligible = inventory.files.filter((entry) => ANALYZABLE_ROLES.has(entry.role));
43
+ for (const entry of eligible) {
44
+ const analyzer = selectLanguageAnalyzer(registry, entry.language, entry.extension);
45
+ if (!analyzer) {
46
+ files.push(buildFallbackFacts(entry));
47
+ continue;
48
+ }
49
+ const absolutePath = path.join(resolvedRoot, entry.relativePath);
50
+ try {
51
+ const content = fs.readFileSync(absolutePath, "utf8");
52
+ const facts = await analyzer.analyzeFile({
53
+ targetRoot: resolvedRoot,
54
+ relativePath: entry.relativePath,
55
+ absolutePath,
56
+ content,
57
+ inventoryEntry: entry,
58
+ language: entry.language,
59
+ role: entry.role,
60
+ });
61
+ files.push(facts);
62
+ }
63
+ catch (err) {
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ const diagnostic = {
66
+ severity: "error",
67
+ code: "analyzer-error",
68
+ message: `Analyzer "${analyzer.id}" failed on "${entry.relativePath}": ${message}`,
69
+ path: entry.relativePath,
70
+ analyzerId: analyzer.id,
71
+ };
72
+ analyzerDiagnostics.push(diagnostic);
73
+ files.push({
74
+ relativePath: entry.relativePath,
75
+ language: entry.language,
76
+ role: entry.role,
77
+ parseStatus: "parse-error",
78
+ analyzerId: analyzer.id,
79
+ lineCount: entry.lineCount,
80
+ imports: [],
81
+ exports: [],
82
+ declarations: [],
83
+ references: [],
84
+ diagnostics: [diagnostic],
85
+ });
86
+ }
87
+ }
88
+ return {
89
+ targetRoot: resolvedRoot,
90
+ files,
91
+ filesByLanguage: countByLanguage(files),
92
+ filesByParseStatus: countByParseStatus(files),
93
+ analyzerDiagnostics,
94
+ warnings,
95
+ };
96
+ }
97
+ function buildFallbackFacts(entry) {
98
+ const known = KNOWN_LANGUAGES.has(entry.language);
99
+ const parseStatus = known ? "file-level-only" : "unsupported";
100
+ const diagnostic = known
101
+ ? {
102
+ severity: "info",
103
+ code: "no-analyzer-registered",
104
+ message: `No language analyzer is registered for "${entry.language}" yet; only inventory-level facts are available.`,
105
+ path: entry.relativePath,
106
+ analyzerId: null,
107
+ }
108
+ : {
109
+ severity: "info",
110
+ code: "unsupported-language",
111
+ message: `Language "${entry.language}" is not supported for source-fact extraction.`,
112
+ path: entry.relativePath,
113
+ analyzerId: null,
114
+ };
115
+ return {
116
+ relativePath: entry.relativePath,
117
+ language: entry.language,
118
+ role: entry.role,
119
+ parseStatus,
120
+ analyzerId: null,
121
+ lineCount: entry.lineCount,
122
+ imports: [],
123
+ exports: [],
124
+ declarations: [],
125
+ references: [],
126
+ diagnostics: [diagnostic],
127
+ };
128
+ }
129
+ function countByLanguage(files) {
130
+ const counts = Object.fromEntries(NORMALIZED_LANGUAGES.map((l) => [l, 0]));
131
+ for (const file of files) {
132
+ counts[file.language] += 1;
133
+ }
134
+ return counts;
135
+ }
136
+ function countByParseStatus(files) {
137
+ const counts = Object.fromEntries(SOURCE_FACT_PARSE_STATUSES.map((s) => [s, 0]));
138
+ for (const file of files) {
139
+ counts[file.parseStatus] += 1;
140
+ }
141
+ return counts;
142
+ }