stapes-adsa 1.0.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.
- package/AGENTS.md +244 -0
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/SECURITY.md +46 -0
- package/SKILL.md +198 -0
- package/dist/ast.d.ts +55 -0
- package/dist/ast.js +186 -0
- package/dist/ast.js.map +1 -0
- package/dist/checks/async-no-log.d.ts +19 -0
- package/dist/checks/async-no-log.js +77 -0
- package/dist/checks/async-no-log.js.map +1 -0
- package/dist/checks/decision-router.d.ts +16 -0
- package/dist/checks/decision-router.js +112 -0
- package/dist/checks/decision-router.js.map +1 -0
- package/dist/checks/fast-path-with-io.d.ts +19 -0
- package/dist/checks/fast-path-with-io.js +58 -0
- package/dist/checks/fast-path-with-io.js.map +1 -0
- package/dist/checks/index.d.ts +21 -0
- package/dist/checks/index.js +90 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/checks/log-schema.d.ts +18 -0
- package/dist/checks/log-schema.js +124 -0
- package/dist/checks/log-schema.js.map +1 -0
- package/dist/checks/slow-path-no-log.d.ts +16 -0
- package/dist/checks/slow-path-no-log.js +71 -0
- package/dist/checks/slow-path-no-log.js.map +1 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +186 -0
- package/dist/cli.js.map +1 -0
- package/dist/column.d.ts +10 -0
- package/dist/column.js +30 -0
- package/dist/column.js.map +1 -0
- package/dist/fix.d.ts +34 -0
- package/dist/fix.js +110 -0
- package/dist/fix.js.map +1 -0
- package/dist/hook-install.d.ts +36 -0
- package/dist/hook-install.js +94 -0
- package/dist/hook-install.js.map +1 -0
- package/dist/lib/instrument.d.ts +49 -0
- package/dist/lib/instrument.js +53 -0
- package/dist/lib/instrument.js.map +1 -0
- package/dist/patterns.d.ts +36 -0
- package/dist/patterns.js +72 -0
- package/dist/patterns.js.map +1 -0
- package/dist/runner.d.ts +35 -0
- package/dist/runner.js +104 -0
- package/dist/runner.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +32 -0
- package/dist/util.js +105 -0
- package/dist/util.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision-router.js","sourceRoot":"","sources":["../../src/checks/decision-router.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check #2 — fast-path-with-io
|
|
3
|
+
*
|
|
4
|
+
* A function is explicitly marked as a fast path (via @adsa:fast JSDoc,
|
|
5
|
+
* // adsa:fast inline comment, or `Fast`/`FastPath` name suffix)
|
|
6
|
+
* but its body contains an I/O call per IO_CALLEE_PATTERNS.
|
|
7
|
+
*
|
|
8
|
+
* This is the single biggest ADSA violation: the "cache" that calls
|
|
9
|
+
* `fetch()`, the "permission check" that hits the DB, the "validation"
|
|
10
|
+
* that makes an RPC. A fast path must be pure — no I/O.
|
|
11
|
+
*
|
|
12
|
+
* Severity: block.
|
|
13
|
+
* Fix: none. Extracting the I/O call into a separate slow path is a
|
|
14
|
+
* refactor, not a mechanical insertion. The runner will refuse to
|
|
15
|
+
* auto-fix this check (SPEC.md invariant #7).
|
|
16
|
+
*/
|
|
17
|
+
import type { CheckResult } from "../types.js";
|
|
18
|
+
import { AnyFunction } from "../ast.js";
|
|
19
|
+
export declare function checkFastPathWithIo(file: string, functions: AnyFunction[]): CheckResult;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check #2 — fast-path-with-io
|
|
3
|
+
*
|
|
4
|
+
* A function is explicitly marked as a fast path (via @adsa:fast JSDoc,
|
|
5
|
+
* // adsa:fast inline comment, or `Fast`/`FastPath` name suffix)
|
|
6
|
+
* but its body contains an I/O call per IO_CALLEE_PATTERNS.
|
|
7
|
+
*
|
|
8
|
+
* This is the single biggest ADSA violation: the "cache" that calls
|
|
9
|
+
* `fetch()`, the "permission check" that hits the DB, the "validation"
|
|
10
|
+
* that makes an RPC. A fast path must be pure — no I/O.
|
|
11
|
+
*
|
|
12
|
+
* Severity: block.
|
|
13
|
+
* Fix: none. Extracting the I/O call into a separate slow path is a
|
|
14
|
+
* refactor, not a mechanical insertion. The runner will refuse to
|
|
15
|
+
* auto-fix this check (SPEC.md invariant #7).
|
|
16
|
+
*/
|
|
17
|
+
import { SyntaxKind } from "ts-morph";
|
|
18
|
+
import { truncate } from "../util.js";
|
|
19
|
+
import { containsIoCall, getAdsaPathMarker } from "../ast.js";
|
|
20
|
+
import { startColumn } from "../column.js";
|
|
21
|
+
export function checkFastPathWithIo(file, functions) {
|
|
22
|
+
const findings = [];
|
|
23
|
+
for (const fn of functions) {
|
|
24
|
+
const marker = getAdsaPathMarker(fn);
|
|
25
|
+
if (marker !== "fast")
|
|
26
|
+
continue;
|
|
27
|
+
if (!containsIoCall(fn, "log"))
|
|
28
|
+
continue;
|
|
29
|
+
const name = getFnName(fn);
|
|
30
|
+
const line = fn.getStartLineNumber();
|
|
31
|
+
const column = startColumn(fn);
|
|
32
|
+
findings.push({
|
|
33
|
+
check: "fast-path-with-io",
|
|
34
|
+
file,
|
|
35
|
+
line,
|
|
36
|
+
column,
|
|
37
|
+
message: `Fast path "${name || "<anonymous>"}" performs I/O — fast paths must be pure (no network/db/cache/fs/queue)`,
|
|
38
|
+
severity: "block",
|
|
39
|
+
evidence: truncate(`function ${name}() { /* I/O inside fast path */ }`),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
name: "fast-path-with-io",
|
|
44
|
+
findings,
|
|
45
|
+
filesScanned: 1,
|
|
46
|
+
durationMs: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function getFnName(fn) {
|
|
50
|
+
if (fn.isKind(SyntaxKind.FunctionDeclaration)) {
|
|
51
|
+
return fn.getName() ?? "";
|
|
52
|
+
}
|
|
53
|
+
if (fn.isKind(SyntaxKind.MethodDeclaration)) {
|
|
54
|
+
return fn.getName();
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=fast-path-with-io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fast-path-with-io.js","sourceRoot":"","sources":["../../src/checks/fast-path-with-io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAA0C,UAAU,EAAE,MAAM,UAAU,CAAC;AAG9E,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAe,cAAc,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,SAAwB;IAExB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,MAAM;YAAE,SAAS;QAChC,IAAI,CAAC,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC;YAAE,SAAS;QAEzC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,EAAE,CAAC,kBAAkB,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;QAE/B,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,mBAAmB;YAC1B,IAAI;YACJ,IAAI;YACJ,MAAM;YACN,OAAO,EAAE,cAAc,IAAI,IAAI,aAAa,yEAAyE;YACrH,QAAQ,EAAE,OAAO;YACjB,QAAQ,EAAE,QAAQ,CAAC,YAAY,IAAI,mCAAmC,CAAC;SACxE,CAAC,CAAC;IACL,CAAC;IACD,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,QAAQ;QACR,YAAY,EAAE,CAAC;QACf,UAAU,EAAE,CAAC;KACd,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,EAAe;IAChC,IAAI,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC9C,OAAQ,EAA0B,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC5C,OAAQ,EAAwB,CAAC,OAAO,EAAE,CAAC;IAC7C,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check registry.
|
|
3
|
+
*
|
|
4
|
+
* The runner imports `CHECKS` and dispatches each one against every
|
|
5
|
+
* scanned file. Adding a new check = add an entry here + write the file.
|
|
6
|
+
*/
|
|
7
|
+
import { Project, SourceFile } from "ts-morph";
|
|
8
|
+
import type { CheckResult } from "../types.js";
|
|
9
|
+
export interface Check {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
defaultEnabled: boolean;
|
|
13
|
+
severityDefault: "block" | "warn";
|
|
14
|
+
/** Run this check against one source file. */
|
|
15
|
+
run: (sf: SourceFile) => CheckResult;
|
|
16
|
+
}
|
|
17
|
+
export declare const CHECKS: ReadonlyArray<Check>;
|
|
18
|
+
export declare function getCheck(name: string): Check | undefined;
|
|
19
|
+
export declare function listCheckNames(): string[];
|
|
20
|
+
/** Project factory — kept here so the runner and CLI share the same config. */
|
|
21
|
+
export declare function createProject(): Project;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check registry.
|
|
3
|
+
*
|
|
4
|
+
* The runner imports `CHECKS` and dispatches each one against every
|
|
5
|
+
* scanned file. Adding a new check = add an entry here + write the file.
|
|
6
|
+
*/
|
|
7
|
+
import { Project } from "ts-morph";
|
|
8
|
+
import { iterFunctions } from "../ast.js";
|
|
9
|
+
import { normalizePath } from "../util.js";
|
|
10
|
+
import { checkSlowPathNoLog } from "./slow-path-no-log.js";
|
|
11
|
+
import { checkFastPathWithIo } from "./fast-path-with-io.js";
|
|
12
|
+
import { checkDecisionRouter } from "./decision-router.js";
|
|
13
|
+
import { checkAsyncNoLog } from "./async-no-log.js";
|
|
14
|
+
import { checkLogSchema } from "./log-schema.js";
|
|
15
|
+
export const CHECKS = [
|
|
16
|
+
{
|
|
17
|
+
name: "slow-path-no-log",
|
|
18
|
+
description: "Function marked slow (JSDoc, inline comment, or name suffix) without {adsa_path: \"slow\"} log emission",
|
|
19
|
+
defaultEnabled: true,
|
|
20
|
+
severityDefault: "warn",
|
|
21
|
+
run: (sf) => {
|
|
22
|
+
const functions = Array.from(iterFunctions(sf));
|
|
23
|
+
const r = checkSlowPathNoLog(normalizePath(sf.getFilePath()), functions);
|
|
24
|
+
return r;
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "fast-path-with-io",
|
|
29
|
+
description: "Function marked fast (JSDoc, inline comment, or name suffix) that contains I/O (network/db/cache/fs/queue)",
|
|
30
|
+
defaultEnabled: true,
|
|
31
|
+
severityDefault: "block",
|
|
32
|
+
run: (sf) => {
|
|
33
|
+
const functions = Array.from(iterFunctions(sf));
|
|
34
|
+
const r = checkFastPathWithIo(normalizePath(sf.getFilePath()), functions);
|
|
35
|
+
return r;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "decision-router",
|
|
40
|
+
description: "Function marked fast with an if/else where one branch contains I/O — implicit fast/slow split hiding inside a conditional",
|
|
41
|
+
defaultEnabled: true,
|
|
42
|
+
severityDefault: "warn",
|
|
43
|
+
run: (sf) => {
|
|
44
|
+
const functions = Array.from(iterFunctions(sf));
|
|
45
|
+
const r = checkDecisionRouter(normalizePath(sf.getFilePath()), functions);
|
|
46
|
+
return r;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "async-no-log",
|
|
51
|
+
description: "Async function with >5 body lines and no {adsa_path: ...} log emission — likely a slow path not declaring itself",
|
|
52
|
+
defaultEnabled: true,
|
|
53
|
+
severityDefault: "warn",
|
|
54
|
+
run: (sf) => {
|
|
55
|
+
const functions = Array.from(iterFunctions(sf));
|
|
56
|
+
const r = checkAsyncNoLog(normalizePath(sf.getFilePath()), functions);
|
|
57
|
+
return r;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "log-schema",
|
|
62
|
+
description: "Validate {adsa_path, adsa_area} log object literals against the four-field contract",
|
|
63
|
+
defaultEnabled: true,
|
|
64
|
+
severityDefault: "block",
|
|
65
|
+
run: (sf) => checkLogSchema(sf),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
export function getCheck(name) {
|
|
69
|
+
return CHECKS.find((c) => c.name === name);
|
|
70
|
+
}
|
|
71
|
+
export function listCheckNames() {
|
|
72
|
+
return CHECKS.map((c) => c.name);
|
|
73
|
+
}
|
|
74
|
+
/** ts-morph ESNext target enum value (ScriptTarget.ESNext = 99). */
|
|
75
|
+
const ESNEXT_TARGET = 99;
|
|
76
|
+
/** Project factory — kept here so the runner and CLI share the same config. */
|
|
77
|
+
export function createProject() {
|
|
78
|
+
return new Project({
|
|
79
|
+
useInMemoryFileSystem: false,
|
|
80
|
+
skipAddingFilesFromTsConfig: true,
|
|
81
|
+
skipFileDependencyResolution: true,
|
|
82
|
+
compilerOptions: {
|
|
83
|
+
target: ESNEXT_TARGET,
|
|
84
|
+
allowJs: false,
|
|
85
|
+
skipLibCheck: true,
|
|
86
|
+
strict: false,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check #5 — adsa-log-schema
|
|
3
|
+
*
|
|
4
|
+
* Every `{adsa_path: ...}` object literal in the codebase must conform
|
|
5
|
+
* to the four-field contract:
|
|
6
|
+
*
|
|
7
|
+
* Required: {adsa_path: "fast" | "slow", adsa_area: <string>}
|
|
8
|
+
* Optional: {adsa_reason: <string>, adsa_save_pct: <number 0..100>}
|
|
9
|
+
* Forbidden: any other keys with the `adsa_` prefix
|
|
10
|
+
*
|
|
11
|
+
* "log call" = the call shape described in ast.ts (log/logger/console/... callee).
|
|
12
|
+
*
|
|
13
|
+
* Severity: block for required-field violations, warn for unknown adsa_ keys.
|
|
14
|
+
* Fix: none.
|
|
15
|
+
*/
|
|
16
|
+
import { SourceFile } from "ts-morph";
|
|
17
|
+
import type { CheckResult } from "../types.js";
|
|
18
|
+
export declare function checkLogSchema(sf: SourceFile): CheckResult;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check #5 — adsa-log-schema
|
|
3
|
+
*
|
|
4
|
+
* Every `{adsa_path: ...}` object literal in the codebase must conform
|
|
5
|
+
* to the four-field contract:
|
|
6
|
+
*
|
|
7
|
+
* Required: {adsa_path: "fast" | "slow", adsa_area: <string>}
|
|
8
|
+
* Optional: {adsa_reason: <string>, adsa_save_pct: <number 0..100>}
|
|
9
|
+
* Forbidden: any other keys with the `adsa_` prefix
|
|
10
|
+
*
|
|
11
|
+
* "log call" = the call shape described in ast.ts (log/logger/console/... callee).
|
|
12
|
+
*
|
|
13
|
+
* Severity: block for required-field violations, warn for unknown adsa_ keys.
|
|
14
|
+
* Fix: none.
|
|
15
|
+
*/
|
|
16
|
+
import { SyntaxKind, } from "ts-morph";
|
|
17
|
+
import { truncate } from "../util.js";
|
|
18
|
+
import { LOG_REQUIRED_FIELDS, LOG_OPTIONAL_FIELDS, ADSA_PATH_VALUES, } from "../patterns.js";
|
|
19
|
+
export function checkLogSchema(sf) {
|
|
20
|
+
const findings = [];
|
|
21
|
+
const file = sf.getBaseName() ?? "<unknown>";
|
|
22
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
23
|
+
const ce = call;
|
|
24
|
+
if (!isLogCallee(ce))
|
|
25
|
+
continue;
|
|
26
|
+
const args = ce.getArguments();
|
|
27
|
+
for (const arg of args) {
|
|
28
|
+
if (!arg.isKind(SyntaxKind.ObjectLiteralExpression))
|
|
29
|
+
continue;
|
|
30
|
+
validateObjectLiteral(arg, file, findings);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
name: "log-schema",
|
|
35
|
+
findings,
|
|
36
|
+
filesScanned: 1,
|
|
37
|
+
durationMs: 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function isLogCallee(ce) {
|
|
41
|
+
const text = ce.getExpression().getText();
|
|
42
|
+
return /\b(log|logger|console|pino|winston|bunyan)(\.|\(|$)/i.test(text);
|
|
43
|
+
}
|
|
44
|
+
function validateObjectLiteral(ol, file, findings) {
|
|
45
|
+
// Only inspect objects that contain an adsa_path property — otherwise
|
|
46
|
+
// it's just a regular log call and not in scope.
|
|
47
|
+
const present = new Map();
|
|
48
|
+
let hasAdsaPath = false;
|
|
49
|
+
for (const prop of ol.getProperties()) {
|
|
50
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment))
|
|
51
|
+
continue;
|
|
52
|
+
const pa = prop;
|
|
53
|
+
const name = pa.getName();
|
|
54
|
+
present.set(name, pa);
|
|
55
|
+
if (name === "adsa_path")
|
|
56
|
+
hasAdsaPath = true;
|
|
57
|
+
}
|
|
58
|
+
if (!hasAdsaPath)
|
|
59
|
+
return;
|
|
60
|
+
// Required fields
|
|
61
|
+
for (const required of LOG_REQUIRED_FIELDS) {
|
|
62
|
+
if (!present.has(required)) {
|
|
63
|
+
const line = ol.getStartLineNumber();
|
|
64
|
+
findings.push({
|
|
65
|
+
check: "log-schema",
|
|
66
|
+
file,
|
|
67
|
+
line,
|
|
68
|
+
column: 1,
|
|
69
|
+
message: `adsa log object is missing required field "${required}"`,
|
|
70
|
+
severity: "block",
|
|
71
|
+
evidence: truncate(ol.getText()),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Enum check on adsa_path
|
|
76
|
+
const adsaPathProp = present.get("adsa_path");
|
|
77
|
+
if (adsaPathProp) {
|
|
78
|
+
const init = adsaPathProp.getInitializer();
|
|
79
|
+
if (init && init.isKind(SyntaxKind.StringLiteral)) {
|
|
80
|
+
const v = init.getLiteralText();
|
|
81
|
+
if (!ADSA_PATH_VALUES.includes(v)) {
|
|
82
|
+
findings.push({
|
|
83
|
+
check: "log-schema",
|
|
84
|
+
file,
|
|
85
|
+
line: adsaPathProp.getStartLineNumber(),
|
|
86
|
+
column: 1,
|
|
87
|
+
message: `adsa_path must be "fast" or "slow" (got "${v}")`,
|
|
88
|
+
severity: "block",
|
|
89
|
+
evidence: truncate(adsaPathProp.getText()),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (init && !init.isKind(SyntaxKind.StringLiteral)) {
|
|
94
|
+
findings.push({
|
|
95
|
+
check: "log-schema",
|
|
96
|
+
file,
|
|
97
|
+
line: adsaPathProp.getStartLineNumber(),
|
|
98
|
+
column: 1,
|
|
99
|
+
message: `adsa_path must be a string literal`,
|
|
100
|
+
severity: "block",
|
|
101
|
+
evidence: truncate(adsaPathProp.getText()),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Forbidden adsa_ prefix keys not in the contract
|
|
106
|
+
for (const key of present.keys()) {
|
|
107
|
+
if (!key.startsWith("adsa_"))
|
|
108
|
+
continue;
|
|
109
|
+
if (LOG_REQUIRED_FIELDS.includes(key))
|
|
110
|
+
continue;
|
|
111
|
+
if (LOG_OPTIONAL_FIELDS.includes(key))
|
|
112
|
+
continue;
|
|
113
|
+
findings.push({
|
|
114
|
+
check: "log-schema",
|
|
115
|
+
file,
|
|
116
|
+
line: present.get(key).getStartLineNumber(),
|
|
117
|
+
column: 1,
|
|
118
|
+
message: `Unknown adsa_ field "${key}" — not in the four-field contract`,
|
|
119
|
+
severity: "warn",
|
|
120
|
+
evidence: truncate(present.get(key).getText()),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=log-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-schema.js","sourceRoot":"","sources":["../../src/checks/log-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check #1 — slow-path-no-log
|
|
3
|
+
*
|
|
4
|
+
* A function is explicitly marked as a slow path (via @adsa:slow JSDoc,
|
|
5
|
+
* // adsa:slow inline comment, or `Slow`/`SlowPath` name suffix)
|
|
6
|
+
* but its body does not emit a log call with `{adsa_path: "slow", ...}`.
|
|
7
|
+
*
|
|
8
|
+
* Without that log line, the path is invisible in observability — the
|
|
9
|
+
* fast_path_rate metric becomes meaningless.
|
|
10
|
+
*
|
|
11
|
+
* Severity: warn.
|
|
12
|
+
* Fix: insert the missing log line at the top of the body.
|
|
13
|
+
*/
|
|
14
|
+
import type { CheckResult } from "../types.js";
|
|
15
|
+
import { AnyFunction } from "../ast.js";
|
|
16
|
+
export declare function checkSlowPathNoLog(file: string, functions: AnyFunction[]): CheckResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check #1 — slow-path-no-log
|
|
3
|
+
*
|
|
4
|
+
* A function is explicitly marked as a slow path (via @adsa:slow JSDoc,
|
|
5
|
+
* // adsa:slow inline comment, or `Slow`/`SlowPath` name suffix)
|
|
6
|
+
* but its body does not emit a log call with `{adsa_path: "slow", ...}`.
|
|
7
|
+
*
|
|
8
|
+
* Without that log line, the path is invisible in observability — the
|
|
9
|
+
* fast_path_rate metric becomes meaningless.
|
|
10
|
+
*
|
|
11
|
+
* Severity: warn.
|
|
12
|
+
* Fix: insert the missing log line at the top of the body.
|
|
13
|
+
*/
|
|
14
|
+
import { SyntaxKind } from "ts-morph";
|
|
15
|
+
import { inferAdsaArea, truncate } from "../util.js";
|
|
16
|
+
import { startColumn } from "../column.js";
|
|
17
|
+
import { getAdsaPathMarker, hasAdsaLogCall, } from "../ast.js";
|
|
18
|
+
export function checkSlowPathNoLog(file, functions) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const fn of functions) {
|
|
21
|
+
const marker = getAdsaPathMarker(fn);
|
|
22
|
+
if (marker !== "slow")
|
|
23
|
+
continue;
|
|
24
|
+
if (hasAdsaLogCall(fn))
|
|
25
|
+
continue;
|
|
26
|
+
const name = getFnName(fn);
|
|
27
|
+
const line = fn.getStartLineNumber();
|
|
28
|
+
const column = startColumn(fn);
|
|
29
|
+
const body = fn.getBody();
|
|
30
|
+
const insertLine = body ? body.getStartLineNumber() : line + 1;
|
|
31
|
+
const insertColumn = body ? 1 : 1;
|
|
32
|
+
const area = inferAdsaArea(file);
|
|
33
|
+
// Expression-bodied arrows cannot be safely auto-fixed.
|
|
34
|
+
const isExpressionBody = fn.isKind(SyntaxKind.ArrowFunction) &&
|
|
35
|
+
!fn.getBody().isKind(SyntaxKind.Block);
|
|
36
|
+
const fix = isExpressionBody
|
|
37
|
+
? undefined
|
|
38
|
+
: {
|
|
39
|
+
description: `Insert slow-path log emission at top of ${name || "anonymous"}`,
|
|
40
|
+
replacement: `log.info({ adsa_path: "slow", adsa_area: "${area}" });\n`,
|
|
41
|
+
line: insertLine,
|
|
42
|
+
column: insertColumn,
|
|
43
|
+
};
|
|
44
|
+
findings.push({
|
|
45
|
+
check: "slow-path-no-log",
|
|
46
|
+
file,
|
|
47
|
+
line,
|
|
48
|
+
column,
|
|
49
|
+
message: `Function "${name || "<anonymous>"}" marked slow has no {adsa_path: "slow"} log emission`,
|
|
50
|
+
severity: "warn",
|
|
51
|
+
evidence: truncate(`async function ${name}() { /* slow path, no log */ }`),
|
|
52
|
+
suggestedFix: fix,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
name: "slow-path-no-log",
|
|
57
|
+
findings,
|
|
58
|
+
filesScanned: 1,
|
|
59
|
+
durationMs: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function getFnName(fn) {
|
|
63
|
+
if (fn.isKind(SyntaxKind.FunctionDeclaration)) {
|
|
64
|
+
return fn.getName() ?? "";
|
|
65
|
+
}
|
|
66
|
+
if (fn.isKind(SyntaxKind.MethodDeclaration)) {
|
|
67
|
+
return fn.getName();
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=slow-path-no-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slow-path-no-log.js","sourceRoot":"","sources":["../../src/checks/slow-path-no-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAyD,UAAU,EAAE,MAAM,UAAU,CAAC;AAG7F,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAEL,iBAAiB,EACjB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,SAAwB;IAExB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,MAAM;YAAE,SAAS;QAChC,IAAI,cAAc,CAAC,EAAE,CAAC;YAAE,SAAS;QAEjC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,EAAE,CAAC,kBAAkB,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QAC/D,MAAM,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAEjC,wDAAwD;QACxD,MAAM,gBAAgB,GACpB,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC;YACnC,CAAE,EAAoB,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAE5D,MAAM,GAAG,GAA6B,gBAAgB;YACpD,CAAC,CAAC,SAAS;YACX,CAAC,CAAC;gBACE,WAAW,EAAE,2CAA2C,IAAI,IAAI,WAAW,EAAE;gBAC7E,WAAW,EAAE,6CAA6C,IAAI,SAAS;gBACvE,IAAI,EAAE,UAAU;gBAChB,MAAM,EAAE,YAAY;aACrB,CAAC;QAEN,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,kBAAkB;YACzB,IAAI;YACJ,IAAI;YACJ,MAAM;YACN,OAAO,EAAE,aAAa,IAAI,IAAI,aAAa,uDAAuD;YAClG,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,IAAI,gCAAgC,CAAC;YAC1E,YAAY,EAAE,GAAG;SAClB,CAAC,CAAC;IACL,CAAC;IACD,OAAO;QACL,IAAI,EAAE,kBAAkB;QACxB,QAAQ;QACR,YAAY,EAAE,CAAC;QACf,UAAU,EAAE,CAAC;KACd,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,EAAe;IAChC,IAAI,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC9C,OAAQ,EAA0B,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC5C,OAAQ,EAAwB,CAAC,OAAO,EAAE,CAAC;IAC7C,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stapes-adsa CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Subcommands and flags:
|
|
6
|
+
* (default) run all enabled checks
|
|
7
|
+
* --check <name> run a single check by name
|
|
8
|
+
* --list list check names and exit
|
|
9
|
+
* --strict treat warnings as blocks
|
|
10
|
+
* --json emit the v1 JSON shape on stdout; stderr is empty
|
|
11
|
+
* --no-color disable ANSI colour in human output
|
|
12
|
+
* --fix dry-run: report what would be auto-fixed
|
|
13
|
+
* --fix-confirm apply the safe auto-fixes
|
|
14
|
+
* --init install the pre-commit hook (idempotent)
|
|
15
|
+
* --uninstall remove the pre-commit hook
|
|
16
|
+
* --root <path> project root (default: cwd)
|
|
17
|
+
* --version
|
|
18
|
+
* --help
|
|
19
|
+
*
|
|
20
|
+
* Exit codes per SPEC.md invariant #3:
|
|
21
|
+
* 0 clean (or warn-only without --strict)
|
|
22
|
+
* 1 blocked findings (or warn under --strict)
|
|
23
|
+
* 2 invalid args / runtime error
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stapes-adsa CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Subcommands and flags:
|
|
6
|
+
* (default) run all enabled checks
|
|
7
|
+
* --check <name> run a single check by name
|
|
8
|
+
* --list list check names and exit
|
|
9
|
+
* --strict treat warnings as blocks
|
|
10
|
+
* --json emit the v1 JSON shape on stdout; stderr is empty
|
|
11
|
+
* --no-color disable ANSI colour in human output
|
|
12
|
+
* --fix dry-run: report what would be auto-fixed
|
|
13
|
+
* --fix-confirm apply the safe auto-fixes
|
|
14
|
+
* --init install the pre-commit hook (idempotent)
|
|
15
|
+
* --uninstall remove the pre-commit hook
|
|
16
|
+
* --root <path> project root (default: cwd)
|
|
17
|
+
* --version
|
|
18
|
+
* --help
|
|
19
|
+
*
|
|
20
|
+
* Exit codes per SPEC.md invariant #3:
|
|
21
|
+
* 0 clean (or warn-only without --strict)
|
|
22
|
+
* 1 blocked findings (or warn under --strict)
|
|
23
|
+
* 2 invalid args / runtime error
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { Command } from "commander";
|
|
27
|
+
import { run, formatHuman } from "./runner.js";
|
|
28
|
+
import { applyFixes } from "./fix.js";
|
|
29
|
+
import { installHook, uninstallHook } from "./hook-install.js";
|
|
30
|
+
import { CHECKS, listCheckNames } from "./checks/index.js";
|
|
31
|
+
import { NAME_COL_WIDTH, SEVERITY_COL_WIDTH } from "./util.js";
|
|
32
|
+
const COLORS = {
|
|
33
|
+
red: (s) => `[31m${s}[0m`,
|
|
34
|
+
green: (s) => `[32m${s}[0m`,
|
|
35
|
+
yellow: (s) => `[33m${s}[0m`,
|
|
36
|
+
cyan: (s) => `[36m${s}[0m`,
|
|
37
|
+
dim: (s) => `[2m${s}[0m`,
|
|
38
|
+
};
|
|
39
|
+
const program = new Command();
|
|
40
|
+
program
|
|
41
|
+
.name("stapes-adsa")
|
|
42
|
+
.description("AST-based ADSA (Adaptive Dual-System Architecture) linter for TypeScript. " +
|
|
43
|
+
"Flags slow paths without logging, fast paths doing I/O, decision points without routing, " +
|
|
44
|
+
"async boundaries missing the adsa_path log line, and invalid log shapes.")
|
|
45
|
+
.version(readVersion())
|
|
46
|
+
.option("--root <path>", "project root", process.cwd())
|
|
47
|
+
.option("--check <name>", "run a single check by name (repeatable)", collect, [])
|
|
48
|
+
.option("--strict", "treat warnings as blocks", false)
|
|
49
|
+
.option("--json", "emit JSON on stdout (stable v1 schema)", false)
|
|
50
|
+
.option("--no-color", "disable ANSI colour in human output")
|
|
51
|
+
.option("--fix", "dry-run: report auto-fixable findings", false)
|
|
52
|
+
.option("--fix-confirm", "apply safe auto-fixes", false)
|
|
53
|
+
.option("--init", "install the pre-commit hook (idempotent)", false)
|
|
54
|
+
.option("--uninstall", "remove the pre-commit hook", false)
|
|
55
|
+
.option("--list", "list available check names", false)
|
|
56
|
+
.exitOverride();
|
|
57
|
+
try {
|
|
58
|
+
program.parse(process.argv);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// Commander throws on --help / --version (exitCode 0) and on unknown
|
|
62
|
+
// args (exitCode 1). Forward the code; emit message on stderr.
|
|
63
|
+
const code = err.exitCode ?? 2;
|
|
64
|
+
const msg = err.message ?? "";
|
|
65
|
+
if (msg)
|
|
66
|
+
process.stderr.write(msg + "\n");
|
|
67
|
+
process.exit(code === 0 ? 0 : 2);
|
|
68
|
+
}
|
|
69
|
+
const opts = program.opts();
|
|
70
|
+
function collect(value, prev) {
|
|
71
|
+
return prev.concat([value]);
|
|
72
|
+
}
|
|
73
|
+
// --list — exit early, no scan.
|
|
74
|
+
if (opts.list) {
|
|
75
|
+
for (const c of CHECKS) {
|
|
76
|
+
const sev = c.severityDefault === "block" ? "block" : "warn";
|
|
77
|
+
const on = c.defaultEnabled ? "on " : "off";
|
|
78
|
+
const line = ` ${c.name.padEnd(NAME_COL_WIDTH)} [${on}] [${sev.padEnd(SEVERITY_COL_WIDTH)}] ${c.description}`;
|
|
79
|
+
process.stdout.write(line + "\n");
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
// --init / --uninstall — operate on the hook, no scan.
|
|
84
|
+
if (opts.init) {
|
|
85
|
+
try {
|
|
86
|
+
const r = installHook(opts.root);
|
|
87
|
+
const msg = `[${r.status}] hook at ${r.hookPath}`;
|
|
88
|
+
if (opts.json) {
|
|
89
|
+
process.stdout.write(JSON.stringify({ op: "init", ...r }) + "\n");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
process.stdout.write(msg + "\n");
|
|
93
|
+
}
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
process.stdout.write(JSON.stringify({ op: "init", error: err.message }) + "\n");
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
process.stderr.write(err.message + "\n");
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (opts.uninstall) {
|
|
106
|
+
const r = uninstallHook(opts.root);
|
|
107
|
+
if (opts.json) {
|
|
108
|
+
process.stdout.write(JSON.stringify({ op: "uninstall", ...r }) + "\n");
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
process.stdout.write(`[${r.status}] hook at ${r.hookPath}\n`);
|
|
112
|
+
}
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
// Validate --check names up front.
|
|
116
|
+
if (opts.check.length > 0) {
|
|
117
|
+
const known = new Set(listCheckNames());
|
|
118
|
+
const unknown = opts.check.filter((n) => !known.has(n));
|
|
119
|
+
if (unknown.length > 0) {
|
|
120
|
+
const msg = `unknown check name(s): ${unknown.join(", ")}. Run --list to see available.`;
|
|
121
|
+
if (opts.json) {
|
|
122
|
+
process.stdout.write(JSON.stringify({ error: msg }) + "\n");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
process.stderr.write(msg + "\n");
|
|
126
|
+
}
|
|
127
|
+
process.exit(2);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Run.
|
|
131
|
+
let report;
|
|
132
|
+
try {
|
|
133
|
+
report = run({
|
|
134
|
+
root: opts.root,
|
|
135
|
+
checks: opts.check.length > 0 ? opts.check : undefined,
|
|
136
|
+
strict: opts.strict,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (opts.json) {
|
|
141
|
+
process.stdout.write(JSON.stringify({ error: err.message }) + "\n");
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
process.stderr.write(err.message + "\n");
|
|
145
|
+
process.exit(2);
|
|
146
|
+
}
|
|
147
|
+
// --fix / --fix-confirm.
|
|
148
|
+
if (opts.fix || opts.fixConfirm) {
|
|
149
|
+
const fixReport = applyFixes(report.findings, {
|
|
150
|
+
write: Boolean(opts.fixConfirm),
|
|
151
|
+
});
|
|
152
|
+
if (opts.json) {
|
|
153
|
+
process.stdout.write(JSON.stringify({ ...report, fix: fixReport }) + "\n");
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
process.stdout.write(`fix dry-run: ${fixReport.applied + fixReport.skipped + fixReport.failed} candidate(s)\n` +
|
|
157
|
+
` would-apply: ${fixReport.applied + fixReport.skipped} (run --fix-confirm to apply)\n` +
|
|
158
|
+
` failed: ${fixReport.failed}\n`);
|
|
159
|
+
}
|
|
160
|
+
// Exit code stays the same as the run report.
|
|
161
|
+
process.exit(report.exitCode);
|
|
162
|
+
}
|
|
163
|
+
// Output.
|
|
164
|
+
if (opts.json) {
|
|
165
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
166
|
+
process.exit(report.exitCode);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
process.stdout.write(formatHuman(report) + "\n");
|
|
170
|
+
process.exit(report.exitCode);
|
|
171
|
+
}
|
|
172
|
+
// --- helpers ---
|
|
173
|
+
function readVersion() {
|
|
174
|
+
try {
|
|
175
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
176
|
+
const raw = readFileSync(pkgPath.pathname, "utf8");
|
|
177
|
+
const pkg = JSON.parse(raw);
|
|
178
|
+
return pkg.version;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return "0.0.0";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Silence unused-import warnings for COLORS in --json mode.
|
|
185
|
+
void COLORS;
|
|
186
|
+
//# sourceMappingURL=cli.js.map
|