@dailephd/my-dev-kit-lab 0.2.2 → 0.3.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 (37) hide show
  1. package/README.md +3 -2
  2. package/dist/scripts/audits/runAudit.js +101 -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 +344 -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 +273 -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 +348 -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 +47 -0
  18. package/dist/src/audits/codeRot/utils/issueFactories.js +38 -0
  19. package/dist/src/audits/codeRot/utils/textLines.js +12 -0
  20. package/dist/src/audits/core/auditConfig.js +125 -0
  21. package/dist/src/audits/core/auditExitCode.js +75 -0
  22. package/dist/src/audits/core/auditIssue.js +20 -0
  23. package/dist/src/audits/core/auditRegistry.js +40 -0
  24. package/dist/src/audits/core/auditRunner.js +71 -0
  25. package/dist/src/audits/core/auditTarget.js +19 -0
  26. package/dist/src/audits/core/auditTypes.js +30 -0
  27. package/dist/src/audits/core/projectInventory.js +308 -0
  28. package/dist/src/audits/core/sourceOfTruth.js +339 -0
  29. package/dist/src/audits/core/textLines.js +24 -0
  30. package/dist/src/audits/report/auditReportModel.js +241 -0
  31. package/dist/src/audits/report/auditReportPaths.js +13 -0
  32. package/dist/src/audits/report/renderAuditJsonReport.js +29 -0
  33. package/dist/src/audits/report/renderAuditTextReport.js +165 -0
  34. package/dist/src/audits/report/sanitizeAuditText.js +39 -0
  35. package/dist/src/audits/report/writeAuditReports.js +27 -0
  36. package/dist/src/core/resolveCommand.js +53 -2
  37. package/package.json +3 -2
@@ -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,71 @@
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
+ export async function runAudit(options) {
7
+ const { config, toolRoot } = options;
8
+ const registry = options.registry ?? DEFAULT_AUDIT_REGISTRY;
9
+ const target = options.target ?? resolveAuditTarget(config.targetPathArg, toolRoot);
10
+ const inventory = scanProjectInventory(target.rootPath);
11
+ const sourceOfTruth = collectSourceOfTruth(target.rootPath, inventory);
12
+ const selected = selectDetectors(registry, config.types, config.include);
13
+ const ctx = { target, config, inventory, sourceOfTruth };
14
+ const issues = [];
15
+ const skippedDetectors = [];
16
+ const detectorErrors = [];
17
+ // Sequential, in registry order -- keeps issue ordering deterministic
18
+ // without needing a secondary sort step.
19
+ for (const detector of selected) {
20
+ try {
21
+ if (detector.shouldSkip) {
22
+ const decision = detector.shouldSkip(ctx);
23
+ if (decision.skip) {
24
+ skippedDetectors.push({ id: detector.id, reason: decision.reason });
25
+ continue;
26
+ }
27
+ }
28
+ const detectorIssues = await detector.run(ctx);
29
+ issues.push(...detectorIssues);
30
+ }
31
+ catch (err) {
32
+ detectorErrors.push({
33
+ id: detector.id,
34
+ message: err instanceof Error ? err.message : String(err),
35
+ });
36
+ }
37
+ }
38
+ const issueCounts = countIssuesBySeverity(issues);
39
+ const { exitCode, reason } = calculateAuditExitCode(issues, config.failOn);
40
+ return {
41
+ configSummary: {
42
+ types: config.types,
43
+ include: config.include,
44
+ formats: config.formats,
45
+ failOn: config.failOn,
46
+ out: config.out,
47
+ isDefaultRun: config.isDefaultRun,
48
+ },
49
+ targetSummary: {
50
+ rootPath: target.rootPath,
51
+ displayName: target.displayName,
52
+ isSelf: target.isSelf,
53
+ },
54
+ issues,
55
+ skippedDetectors,
56
+ detectorErrors,
57
+ issueCounts,
58
+ exitCode,
59
+ exitReason: reason,
60
+ inventory,
61
+ sourceOfTruth,
62
+ noDetectorsRegistered: selected.length === 0,
63
+ };
64
+ }
65
+ function countIssuesBySeverity(issues) {
66
+ const counts = { blocker: 0, high: 0, medium: 0, low: 0, info: 0 };
67
+ for (const issue of issues) {
68
+ counts[issue.severity] += 1;
69
+ }
70
+ return counts;
71
+ }
@@ -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"];