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.
Files changed (55) hide show
  1. package/AGENTS.md +244 -0
  2. package/CHANGELOG.md +52 -0
  3. package/LICENSE +21 -0
  4. package/README.md +291 -0
  5. package/SECURITY.md +46 -0
  6. package/SKILL.md +198 -0
  7. package/dist/ast.d.ts +55 -0
  8. package/dist/ast.js +186 -0
  9. package/dist/ast.js.map +1 -0
  10. package/dist/checks/async-no-log.d.ts +19 -0
  11. package/dist/checks/async-no-log.js +77 -0
  12. package/dist/checks/async-no-log.js.map +1 -0
  13. package/dist/checks/decision-router.d.ts +16 -0
  14. package/dist/checks/decision-router.js +112 -0
  15. package/dist/checks/decision-router.js.map +1 -0
  16. package/dist/checks/fast-path-with-io.d.ts +19 -0
  17. package/dist/checks/fast-path-with-io.js +58 -0
  18. package/dist/checks/fast-path-with-io.js.map +1 -0
  19. package/dist/checks/index.d.ts +21 -0
  20. package/dist/checks/index.js +90 -0
  21. package/dist/checks/index.js.map +1 -0
  22. package/dist/checks/log-schema.d.ts +18 -0
  23. package/dist/checks/log-schema.js +124 -0
  24. package/dist/checks/log-schema.js.map +1 -0
  25. package/dist/checks/slow-path-no-log.d.ts +16 -0
  26. package/dist/checks/slow-path-no-log.js +71 -0
  27. package/dist/checks/slow-path-no-log.js.map +1 -0
  28. package/dist/cli.d.ts +25 -0
  29. package/dist/cli.js +186 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/column.d.ts +10 -0
  32. package/dist/column.js +30 -0
  33. package/dist/column.js.map +1 -0
  34. package/dist/fix.d.ts +34 -0
  35. package/dist/fix.js +110 -0
  36. package/dist/fix.js.map +1 -0
  37. package/dist/hook-install.d.ts +36 -0
  38. package/dist/hook-install.js +94 -0
  39. package/dist/hook-install.js.map +1 -0
  40. package/dist/lib/instrument.d.ts +49 -0
  41. package/dist/lib/instrument.js +53 -0
  42. package/dist/lib/instrument.js.map +1 -0
  43. package/dist/patterns.d.ts +36 -0
  44. package/dist/patterns.js +72 -0
  45. package/dist/patterns.js.map +1 -0
  46. package/dist/runner.d.ts +35 -0
  47. package/dist/runner.js +104 -0
  48. package/dist/runner.js.map +1 -0
  49. package/dist/types.d.ts +52 -0
  50. package/dist/types.js +9 -0
  51. package/dist/types.js.map +1 -0
  52. package/dist/util.d.ts +32 -0
  53. package/dist/util.js +105 -0
  54. package/dist/util.js.map +1 -0
  55. package/package.json +66 -0
package/SKILL.md ADDED
@@ -0,0 +1,198 @@
1
+ ---
2
+ name: adsa
3
+ description: Apply ADSA (Adaptive Dual-System Architecture) fast/slow path splitting to TypeScript code. Use when designing or reviewing a function, module, or service that has a decision point which could be cheap (cache, regex, JWT verify) vs expensive (DB, network, FS), when adding observability for fast_path_rate, or when fixing a "this should be fast but it isn't" performance bug. Triggers on phrases like "ADSA", "fast path", "slow path", "fast/slow", "router between", "cache vs compute", "the slow path is too slow", "permission check", "auth lookup", "validation rule", "rate limit", "ingestion pipeline". Outputs the four-field log contract {adsa_path, adsa_area, adsa_reason?, adsa_save_pct?}. Pairs with `stapes-adsa` (the AST linter) for automated enforcement.
4
+ ---
5
+
6
+ # ADSA — Adaptive Dual-System Architecture
7
+
8
+ ADSA is a thinking pattern. Every code path is either **fast** (cheap, deterministic, no I/O) or **slow** (full context, expensive). Routing between them is explicit. The path taken is logged.
9
+
10
+ This skill is the framework reference. The linter (`stapes-adsa` on npm) enforces it.
11
+
12
+ ## The four-question self-assessment
13
+
14
+ Before writing a function, code path, or service, ask:
15
+
16
+ 1. **Does this code make a decision based on input?**
17
+ 2. **Is there a cheap way to make that decision?** (cache, JWT, regex, simple calc, type check)
18
+ 3. **Is a minority of cases that need the expensive path?**
19
+ 4. **Would logging the path taken help observability?**
20
+
21
+ If 2 or more are YES — apply ADSA. Structure the code as `FastPath → Router → SlowPath → UnifiedOutput + log(adsa_path)`.
22
+
23
+ ## The four-field log contract
24
+
25
+ Every ADSA-coded path emits one structured log line at the moment routing happens:
26
+
27
+ ```json
28
+ {
29
+ "adsa_path": "fast" | "slow",
30
+ "adsa_area": "<scope>",
31
+ "adsa_reason": "<why this path>", // optional
32
+ "adsa_save_pct": 0..100 // optional, percentage saved vs slow
33
+ }
34
+ ```
35
+
36
+ Field rules:
37
+
38
+ | Field | Required | Values | Notes |
39
+ |---|---|---|---|
40
+ | `adsa_path` | yes | `"fast"` \| `"slow"` | Case-sensitive. Other values fail the linter. |
41
+ | `adsa_area` | yes | string | Scope. Use directory or module name. |
42
+ | `adsa_reason` | no | string | Human-readable why. No PII. |
43
+ | `adsa_save_pct` | no | number 0..100 | Saved fraction vs the slow path. |
44
+
45
+ Log schema violations are blocked by `stapes-adsa check log-schema`.
46
+
47
+ ## Path markers (optional, for linter detection)
48
+
49
+ You can declare a path explicitly so the linter can verify it:
50
+
51
+ | Marker | Form | Effect |
52
+ |---|---|---|
53
+ | JSDoc tag | `/** @adsa:fast */` or `/** @adsa:slow */` | Function classified |
54
+ | Inline comment | `// adsa:fast` immediately preceding | Function classified |
55
+ | Name suffix | `verifyFast`, `resolveSlow`, `lookupCacheFast` | Function classified |
56
+
57
+ If no marker is present, the linter applies a heuristic (async + >5 body lines → candidate slow path).
58
+
59
+ ## Worked examples
60
+
61
+ ### 1. Auth — the most common ADSA case
62
+
63
+ ```ts
64
+ import { withAdsaLogging } from "stapes-adsa/lib/instrument";
65
+
66
+ // FAST: verify JWT signature. Pure. No I/O.
67
+ function verifyTokenFast(token: string): UserClaims | null {
68
+ try {
69
+ return jwt.verify(token, PUBLIC_KEY) as UserClaims;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ // SLOW: full session resolution. Hits DB.
76
+ async function resolveSessionSlow(claims: UserClaims): Promise<Session> {
77
+ return db.sessions.findUnique({ where: { userId: claims.sub } });
78
+ }
79
+
80
+ // Router
81
+ export async function authenticate(token: string) {
82
+ return withAdsaLogging({
83
+ area: "auth",
84
+ predicate: async () => verifyTokenFast(token), // fast path
85
+ slowPath: async () => {
86
+ const claims = jwt.decode(token); // decode only, still cheap
87
+ if (!claims) throw new AuthError("invalid");
88
+ return resolveSessionSlow(claims as UserClaims);
89
+ },
90
+ });
91
+ }
92
+ ```
93
+
94
+ Why: `verifyTokenFast` needs no I/O. It runs in microseconds. The 99% of tokens that are valid JWTs never touch the DB. The slow path runs only on tokens that fail signature verification.
95
+
96
+ ### 2. Cache — wrap a lookup
97
+
98
+ ```ts
99
+ async function getUserPermissions(userId: string): Promise<Permission[]> {
100
+ return withAdsaLogging({
101
+ area: "permission",
102
+ predicate: async () => cache.get(`perms:${userId}`), // fast: in-memory hit
103
+ slowPath: async () => {
104
+ const perms = await db.permissions.findMany({ where: { userId } });
105
+ cache.set(`perms:${userId}`, perms, { ttl: 60 });
106
+ return perms;
107
+ },
108
+ });
109
+ }
110
+ ```
111
+
112
+ Why: when the cache hits, the function returns in single-digit microseconds and emits `adsa_path: "fast"`. When the cache misses, it computes, sets the cache, and emits `adsa_path: "slow"`. The fast-path-rate metric becomes the cache hit ratio.
113
+
114
+ ### 3. Search — Meilisearch vs DB
115
+
116
+ ```ts
117
+ async function searchProducts(query: string): Promise<Hit[]> {
118
+ return withAdsaLogging({
119
+ area: "search",
120
+ predicate: async () => meili.index("products").search(query, { limit: 20 }),
121
+ slowPath: async () => db.products.findMany({ where: { name: { contains: query } } }),
122
+ });
123
+ }
124
+ ```
125
+
126
+ Why: Meilisearch handles typo-tolerance, ranking, and facet aggregation in one shot. The DB fallback exists for index-stale or empty-index cases. The log line tells ops which path the request took.
127
+
128
+ ### 4. Rate limit — Redis counter vs request rejection
129
+
130
+ ```ts
131
+ async function checkRateLimit(userId: string): Promise<boolean> {
132
+ return withAdsaLogging({
133
+ area: "rate-limit",
134
+ predicate: async () => {
135
+ const count = await redis.incr(`rl:${userId}`);
136
+ if (count === 1) await redis.expire(`rl:${userId}`, 60);
137
+ return count <= 100;
138
+ },
139
+ // slow path: degrade to memory-only counter if Redis is down
140
+ slowPath: async () => memCounter.incr(userId) <= 100,
141
+ });
142
+ }
143
+ ```
144
+
145
+ Why: Redis is fast but still I/O. The fast path is the normal operation. The slow path is a degraded mode that runs only when Redis is unavailable — and logs it so ops sees the degradation.
146
+
147
+ ## Anti-patterns
148
+
149
+ - **Uniformly cheap code.** Don't apply ADSA to a pure function with no decision point. Over-engineering.
150
+ - **Uniformly expensive code.** If every call hits the DB, splitting fast/slow is the wrong split. Use caching or batching instead.
151
+ - **PII in `adsa_reason`.** The reason field is logged to your observability stack. Treat it as user-visible.
152
+ - **Logging on the wrong side.** Emit `adsa_path` once, at the routing point. Don't log inside the slow path's I/O calls.
153
+ - **Silent failures.** The router must surface the fast/slow distinction in both the response shape and the log. A failure mode that returns success without distinguishing fast/slow is a bug.
154
+
155
+ ## Self-assessment output (fire at task start)
156
+
157
+ When this skill is loaded, the next user-visible line should be one of:
158
+
159
+ ```
160
+ ADSA: YES — fast/slow split in [paths] | NO — monolithic OK
161
+ ```
162
+
163
+ Followed by the framework's coverage summary across the four areas:
164
+
165
+ | Area | Fast Path | Slow Path |
166
+ |---|---|---|
167
+ | Auth | JWT verify, claim extract | DB session lookup |
168
+ | Cache | In-memory hit | DB/API call + cache set |
169
+ | Search | Meilisearch query | DB full-text scan |
170
+ | Permissions | Role bit, cached perm | RBAC/ACL gRPC |
171
+ | Validation | Regex, literal check | Rule engine, external policy |
172
+ | Data ingestion | Single write/read | Multi-step transform, enrichment |
173
+ | Rate limiting | In-memory counter | Distributed counter (Redis) |
174
+ | Config / flags | Local eval | Remote config service |
175
+
176
+ ## Pairing with `stapes-adsa`
177
+
178
+ This skill is the framework reference. The linter is the enforcement:
179
+
180
+ ```bash
181
+ npx stapes-adsa --root . # scan, exit 1 on block
182
+ npx stapes-adsa --json # agent-friendly output
183
+ npx stapes-adsa --init # install pre-commit hook
184
+ npx stapes-adsa --fix # dry-run safe auto-fixes
185
+ npx stapes-adsa --fix-confirm # apply safe auto-fixes
186
+ ```
187
+
188
+ The linter catches:
189
+
190
+ | Check | What it catches |
191
+ |---|---|
192
+ | `slow-path-no-log` | Function marked slow without `{adsa_path: "slow"}` log |
193
+ | `fast-path-with-io` | Function marked fast that contains network/db/cache/fs/queue calls |
194
+ | `decision-router` | Function marked fast with an if/else where one branch does I/O |
195
+ | `async-no-log` | Async function >5 lines without an `{adsa_path}` log line |
196
+ | `log-schema` | Invalid `{adsa_path}` value, missing `adsa_area`, unknown `adsa_*` field |
197
+
198
+ Use the linter in pre-commit + CI. Use this skill when designing the structure.
package/dist/ast.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * AST helpers shared across checks.
3
+ *
4
+ * All checks operate on `ts-morph` SourceFile nodes. These helpers
5
+ * extract the ADSA-relevant signals:
6
+ *
7
+ * - `getFunctionName` — name for marker regex matching
8
+ * - `getAdsaPathMarker` — "fast" | "slow" | undefined, from JSDoc/name/comment
9
+ * - `isAsyncFunction` — function is declared `async`
10
+ * - `hasAdsaLogCall` — body contains a log call with {adsa_path: "fast"|"slow"}
11
+ * - `containsIoCall` — body contains a callee matching IO_CALLEE_PATTERNS
12
+ * - `iterFunctions` — walk all function-like nodes in a source file
13
+ */
14
+ import { ArrowFunction, FunctionDeclaration, FunctionExpression, MethodDeclaration, SourceFile } from "ts-morph";
15
+ /** A function-like node we care about. */
16
+ export type AnyFunction = FunctionDeclaration | MethodDeclaration | FunctionExpression | ArrowFunction;
17
+ /**
18
+ * Get the textual name of a function. Anonymous arrows return "".
19
+ */
20
+ export declare function getFunctionName(fn: AnyFunction): string;
21
+ /**
22
+ * Read the ADSA path marker attached to a function.
23
+ *
24
+ * Resolution order (most explicit wins):
25
+ * 1. JSDoc tag `@adsa:fast` / `@adsa:slow`
26
+ * 2. Inline comment `// adsa:fast` / `// adsa:slow` immediately preceding
27
+ * 3. Function name suffix `Fast` / `Slow`
28
+ *
29
+ * Returns undefined when no marker is present.
30
+ */
31
+ export declare function getAdsaPathMarker(fn: AnyFunction): "fast" | "slow" | undefined;
32
+ /** True if the function is declared `async`. */
33
+ export declare function isAsyncFunction(fn: AnyFunction): boolean;
34
+ /**
35
+ * True if the function body contains a log call with an `{adsa_path: "fast"|"slow"}` shape.
36
+ *
37
+ * "log call" = a CallExpression whose callee identifier / member name matches
38
+ * ./^log(_|$)|logger|console$|pino|winston|bunyan/i
39
+ *
40
+ * The first object-literal argument is inspected for an `adsa_path` property
41
+ * with a string-literal value of "fast" or "slow".
42
+ */
43
+ export declare function hasAdsaLogCall(fn: AnyFunction): boolean;
44
+ /**
45
+ * True if the function body contains an I/O call per IO_CALLEE_PATTERNS.
46
+ *
47
+ * `ignoreCallee` allows a check to skip its own log emission when scanning.
48
+ */
49
+ export declare function containsIoCall(fn: AnyFunction, ignoreCallee?: string): boolean;
50
+ /**
51
+ * Walk every function-like node in a SourceFile. Yields each one exactly once.
52
+ */
53
+ export declare function iterFunctions(sf: SourceFile): Generator<AnyFunction>;
54
+ /** Count lines in a function body (approximate; uses node text). */
55
+ export declare function functionLineCount(fn: AnyFunction): number;
package/dist/ast.js ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * AST helpers shared across checks.
3
+ *
4
+ * All checks operate on `ts-morph` SourceFile nodes. These helpers
5
+ * extract the ADSA-relevant signals:
6
+ *
7
+ * - `getFunctionName` — name for marker regex matching
8
+ * - `getAdsaPathMarker` — "fast" | "slow" | undefined, from JSDoc/name/comment
9
+ * - `isAsyncFunction` — function is declared `async`
10
+ * - `hasAdsaLogCall` — body contains a log call with {adsa_path: "fast"|"slow"}
11
+ * - `containsIoCall` — body contains a callee matching IO_CALLEE_PATTERNS
12
+ * - `iterFunctions` — walk all function-like nodes in a source file
13
+ */
14
+ import { SyntaxKind, } from "ts-morph";
15
+ import { ADSA_FAST_TAG, ADSA_SLOW_TAG, ADSA_INLINE_FAST, ADSA_INLINE_SLOW, FAST_NAME_REGEX, SLOW_NAME_REGEX, IO_CALLEE_PATTERNS, } from "./patterns.js";
16
+ /**
17
+ * Get the textual name of a function. Anonymous arrows return "".
18
+ */
19
+ export function getFunctionName(fn) {
20
+ if (fn.isKind(SyntaxKind.FunctionDeclaration)) {
21
+ return fn.getName() ?? "";
22
+ }
23
+ if (fn.isKind(SyntaxKind.MethodDeclaration)) {
24
+ return fn.getName();
25
+ }
26
+ const parent = fn.getParent();
27
+ if (parent && parent.isKind(SyntaxKind.VariableDeclaration)) {
28
+ return parent.getName();
29
+ }
30
+ return "";
31
+ }
32
+ /**
33
+ * Read the ADSA path marker attached to a function.
34
+ *
35
+ * Resolution order (most explicit wins):
36
+ * 1. JSDoc tag `@adsa:fast` / `@adsa:slow`
37
+ * 2. Inline comment `// adsa:fast` / `// adsa:slow` immediately preceding
38
+ * 3. Function name suffix `Fast` / `Slow`
39
+ *
40
+ * Returns undefined when no marker is present.
41
+ */
42
+ export function getAdsaPathMarker(fn) {
43
+ // 1. JSDoc tags
44
+ for (const tag of collectJsDocTags(fn)) {
45
+ const t = tag.getText();
46
+ if (t.includes(ADSA_FAST_TAG))
47
+ return "fast";
48
+ if (t.includes(ADSA_SLOW_TAG))
49
+ return "slow";
50
+ }
51
+ // 2. Inline leading comment
52
+ const leading = fn.getLeadingCommentRanges() ?? [];
53
+ for (const range of leading) {
54
+ const text = range.getText();
55
+ if (ADSA_INLINE_FAST.test(text))
56
+ return "fast";
57
+ if (ADSA_INLINE_SLOW.test(text))
58
+ return "slow";
59
+ }
60
+ // 3. Name pattern
61
+ const name = getFunctionName(fn);
62
+ if (!name)
63
+ return undefined;
64
+ if (FAST_NAME_REGEX.test(name))
65
+ return "fast";
66
+ if (SLOW_NAME_REGEX.test(name))
67
+ return "slow";
68
+ return undefined;
69
+ }
70
+ /** Collect all JSDoc tag nodes attached to a function, or its container. */
71
+ function collectJsDocTags(fn) {
72
+ const tags = [];
73
+ for (const jsdoc of fn.getJsDocs()) {
74
+ tags.push(...jsdoc.getTags());
75
+ }
76
+ return tags;
77
+ }
78
+ /** True if the function is declared `async`. */
79
+ export function isAsyncFunction(fn) {
80
+ return fn.isAsync();
81
+ }
82
+ /**
83
+ * True if the function body contains a log call with an `{adsa_path: "fast"|"slow"}` shape.
84
+ *
85
+ * "log call" = a CallExpression whose callee identifier / member name matches
86
+ * ./^log(_|$)|logger|console$|pino|winston|bunyan/i
87
+ *
88
+ * The first object-literal argument is inspected for an `adsa_path` property
89
+ * with a string-literal value of "fast" or "slow".
90
+ */
91
+ export function hasAdsaLogCall(fn) {
92
+ let found = false;
93
+ fn.forEachDescendant((node) => {
94
+ if (found)
95
+ return;
96
+ if (!node.isKind(SyntaxKind.CallExpression))
97
+ return;
98
+ const call = node;
99
+ if (!isLogCallee(call))
100
+ return;
101
+ const args = call.getArguments();
102
+ for (const arg of args) {
103
+ if (arg.isKind(SyntaxKind.ObjectLiteralExpression)) {
104
+ if (objectLiteralHasAdsaPath(arg)) {
105
+ found = true;
106
+ return;
107
+ }
108
+ }
109
+ }
110
+ });
111
+ return found;
112
+ }
113
+ /** True if the callee of `call` looks like a logging function. */
114
+ function isLogCallee(call) {
115
+ const expr = call.getExpression();
116
+ const text = expr.getText();
117
+ return /\b(log|logger|console|pino|winston|bunyan)(\.|\(|$)/i.test(text);
118
+ }
119
+ /** True if the object literal contains `adsa_path: "fast"|"slow"`. */
120
+ function objectLiteralHasAdsaPath(ol) {
121
+ for (const prop of ol.getProperties()) {
122
+ if (!prop.isKind(SyntaxKind.PropertyAssignment))
123
+ continue;
124
+ const pa = prop;
125
+ if (pa.getName() !== "adsa_path")
126
+ continue;
127
+ const init = pa.getInitializer();
128
+ if (!init)
129
+ return false;
130
+ if (init.isKind(SyntaxKind.StringLiteral)) {
131
+ const v = init.getLiteralText();
132
+ return v === "fast" || v === "slow";
133
+ }
134
+ return false;
135
+ }
136
+ return false;
137
+ }
138
+ /**
139
+ * True if the function body contains an I/O call per IO_CALLEE_PATTERNS.
140
+ *
141
+ * `ignoreCallee` allows a check to skip its own log emission when scanning.
142
+ */
143
+ export function containsIoCall(fn, ignoreCallee) {
144
+ let found = false;
145
+ fn.forEachDescendant((node) => {
146
+ if (found)
147
+ return;
148
+ if (!node.isKind(SyntaxKind.CallExpression))
149
+ return;
150
+ const call = node;
151
+ const calleeText = call.getExpression().getText();
152
+ if (ignoreCallee && calleeText === ignoreCallee)
153
+ return;
154
+ const fullText = call.getText();
155
+ for (const rx of IO_CALLEE_PATTERNS) {
156
+ if (rx.test(fullText) || rx.test(calleeText)) {
157
+ found = true;
158
+ return;
159
+ }
160
+ }
161
+ });
162
+ return found;
163
+ }
164
+ /**
165
+ * Walk every function-like node in a SourceFile. Yields each one exactly once.
166
+ */
167
+ export function* iterFunctions(sf) {
168
+ for (const node of sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration)) {
169
+ yield node;
170
+ }
171
+ for (const node of sf.getDescendantsOfKind(SyntaxKind.MethodDeclaration)) {
172
+ yield node;
173
+ }
174
+ for (const node of sf.getDescendantsOfKind(SyntaxKind.FunctionExpression)) {
175
+ yield node;
176
+ }
177
+ for (const node of sf.getDescendantsOfKind(SyntaxKind.ArrowFunction)) {
178
+ yield node;
179
+ }
180
+ }
181
+ /** Count lines in a function body (approximate; uses node text). */
182
+ export function functionLineCount(fn) {
183
+ const text = fn.getBody()?.getText() ?? fn.getText();
184
+ return text.split("\n").length;
185
+ }
186
+ //# sourceMappingURL=ast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Check #4 — async-no-log
3
+ *
4
+ * An async function whose body is non-trivial (>5 lines), does not
5
+ * already have a `@adsa:slow` marker, and does not emit a log call
6
+ * with `{adsa_path: "slow", ...}`. Such a function is a "candidate"
7
+ * slow path that has not declared itself.
8
+ *
9
+ * This is a heuristic — small async helpers (e.g. `async function id(x)
10
+ * { return x }`) are excluded by the line-count gate. The intent is
11
+ * to surface the common case of "I wrote an async function, it does
12
+ * real work, but I forgot to log which path it is."
13
+ *
14
+ * Severity: warn.
15
+ * Fix: insert the slow-path log line at the top of the body.
16
+ */
17
+ import type { CheckResult } from "../types.js";
18
+ import { AnyFunction } from "../ast.js";
19
+ export declare function checkAsyncNoLog(file: string, functions: AnyFunction[]): CheckResult;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Check #4 — async-no-log
3
+ *
4
+ * An async function whose body is non-trivial (>5 lines), does not
5
+ * already have a `@adsa:slow` marker, and does not emit a log call
6
+ * with `{adsa_path: "slow", ...}`. Such a function is a "candidate"
7
+ * slow path that has not declared itself.
8
+ *
9
+ * This is a heuristic — small async helpers (e.g. `async function id(x)
10
+ * { return x }`) are excluded by the line-count gate. The intent is
11
+ * to surface the common case of "I wrote an async function, it does
12
+ * real work, but I forgot to log which path it is."
13
+ *
14
+ * Severity: warn.
15
+ * Fix: insert the slow-path log line at the top of the body.
16
+ */
17
+ import { SyntaxKind } from "ts-morph";
18
+ import { inferAdsaArea, truncate } from "../util.js";
19
+ import { startColumn } from "../column.js";
20
+ import { functionLineCount, getAdsaPathMarker, hasAdsaLogCall, isAsyncFunction, } from "../ast.js";
21
+ /** Minimum body lines to count as a non-trivial async function. */
22
+ const MIN_BODY_LINES = 5;
23
+ export function checkAsyncNoLog(file, functions) {
24
+ const findings = [];
25
+ for (const fn of functions) {
26
+ if (!isAsyncFunction(fn))
27
+ continue;
28
+ if (functionLineCount(fn) < MIN_BODY_LINES)
29
+ continue;
30
+ if (hasAdsaLogCall(fn))
31
+ continue;
32
+ if (getAdsaPathMarker(fn) === "fast")
33
+ continue;
34
+ const name = getFnName(fn);
35
+ const line = fn.getStartLineNumber();
36
+ const column = startColumn(fn);
37
+ const body = fn.getBody();
38
+ const insertLine = body ? body.getStartLineNumber() : line + 1;
39
+ const area = inferAdsaArea(file);
40
+ const isExpressionBody = fn.isKind(SyntaxKind.ArrowFunction) &&
41
+ !fn.getBody().isKind(SyntaxKind.Block);
42
+ const fix = isExpressionBody
43
+ ? undefined
44
+ : {
45
+ description: `Insert slow-path log emission at top of ${name || "anonymous"}`,
46
+ replacement: `log.info({ adsa_path: "slow", adsa_area: "${area}" });\n`,
47
+ line: insertLine,
48
+ column: 1,
49
+ };
50
+ findings.push({
51
+ check: "async-no-log",
52
+ file,
53
+ line,
54
+ column,
55
+ message: `Async function "${name || "<anonymous>"}" has no {adsa_path: "slow"} log emission — likely a slow path that is not declaring itself`,
56
+ severity: "warn",
57
+ evidence: truncate(`async function ${name}() { /* ${functionLineCount(fn)} lines, no log */ }`),
58
+ suggestedFix: fix,
59
+ });
60
+ }
61
+ return {
62
+ name: "async-no-log",
63
+ findings,
64
+ filesScanned: 1,
65
+ durationMs: 0,
66
+ };
67
+ }
68
+ function getFnName(fn) {
69
+ if (fn.isKind(SyntaxKind.FunctionDeclaration)) {
70
+ return fn.getName() ?? "";
71
+ }
72
+ if (fn.isKind(SyntaxKind.MethodDeclaration)) {
73
+ return fn.getName();
74
+ }
75
+ return "";
76
+ }
77
+ //# sourceMappingURL=async-no-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-no-log.js","sourceRoot":"","sources":["../../src/checks/async-no-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;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,iBAAiB,EACjB,cAAc,EACd,eAAe,GAChB,MAAM,WAAW,CAAC;AAEnB,mEAAmE;AACnE,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,SAAwB;IAExB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;YAAE,SAAS;QACnC,IAAI,iBAAiB,CAAC,EAAE,CAAC,GAAG,cAAc;YAAE,SAAS;QACrD,IAAI,cAAc,CAAC,EAAE,CAAC;YAAE,SAAS;QACjC,IAAI,iBAAiB,CAAC,EAAE,CAAC,KAAK,MAAM;YAAE,SAAS;QAE/C,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,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAEjC,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,CAAC;aACV,CAAC;QAEN,QAAQ,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,cAAc;YACrB,IAAI;YACJ,IAAI;YACJ,MAAM;YACN,OAAO,EAAE,mBAAmB,IAAI,IAAI,aAAa,6FAA6F;YAC9I,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,IAAI,WAAW,iBAAiB,CAAC,EAAE,CAAC,qBAAqB,CAAC;YAC/F,YAAY,EAAE,GAAG;SAClB,CAAC,CAAC;IACL,CAAC;IACD,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,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,16 @@
1
+ /**
2
+ * Check #3 — decision-point-no-router
3
+ *
4
+ * A function marked fast has an `if` statement whose condition references
5
+ * one of the function's parameters, and one of the branches contains
6
+ * an I/O call. This is the pattern where the implicit fast/slow split
7
+ * is hiding inside a conditional — the fast path is doing I/O on one
8
+ * branch without declaring itself.
9
+ *
10
+ * Severity: warn.
11
+ * Fix: none. Splitting the function into an explicit fast/slow pair
12
+ * is a refactor.
13
+ */
14
+ import type { CheckResult } from "../types.js";
15
+ import { AnyFunction } from "../ast.js";
16
+ export declare function checkDecisionRouter(file: string, functions: AnyFunction[]): CheckResult;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Check #3 — decision-point-no-router
3
+ *
4
+ * A function marked fast has an `if` statement whose condition references
5
+ * one of the function's parameters, and one of the branches contains
6
+ * an I/O call. This is the pattern where the implicit fast/slow split
7
+ * is hiding inside a conditional — the fast path is doing I/O on one
8
+ * branch without declaring itself.
9
+ *
10
+ * Severity: warn.
11
+ * Fix: none. Splitting the function into an explicit fast/slow pair
12
+ * is a refactor.
13
+ */
14
+ import { SyntaxKind, } from "ts-morph";
15
+ import { truncate } from "../util.js";
16
+ import { getAdsaPathMarker } from "../ast.js";
17
+ import { IO_CALLEE_PATTERNS } from "../patterns.js";
18
+ import { startColumn } from "../column.js";
19
+ export function checkDecisionRouter(file, functions) {
20
+ const findings = [];
21
+ for (const fn of functions) {
22
+ const marker = getAdsaPathMarker(fn);
23
+ if (marker !== "fast")
24
+ continue;
25
+ const params = collectParamNames(fn);
26
+ if (params.size === 0)
27
+ continue;
28
+ const ifStatements = fn.getDescendantsOfKind(SyntaxKind.IfStatement);
29
+ for (const ifStmt of ifStatements) {
30
+ if (!ifTestReferencesParam(ifStmt, params))
31
+ continue;
32
+ const consequentIo = ifContainsIo(ifStmt.getThenStatement());
33
+ const alternateIo = ifStmt.getElseStatement() != null
34
+ ? ifContainsIo(ifStmt.getElseStatement())
35
+ : false;
36
+ if (!consequentIo && !alternateIo)
37
+ continue;
38
+ const name = getFnName(fn);
39
+ const line = ifStmt.getStartLineNumber();
40
+ const column = startColumn(ifStmt);
41
+ findings.push({
42
+ check: "decision-router",
43
+ file,
44
+ line,
45
+ column,
46
+ message: `Fast path "${name || "<anonymous>"}" has an implicit fast/slow split — extract the I/O branch into a separate slow path`,
47
+ severity: "warn",
48
+ evidence: truncate(ifStmt.getText()),
49
+ });
50
+ }
51
+ }
52
+ return {
53
+ name: "decision-router",
54
+ findings,
55
+ filesScanned: 1,
56
+ durationMs: 0,
57
+ };
58
+ }
59
+ function collectParamNames(fn) {
60
+ const names = new Set();
61
+ for (const p of fn.getParameters()) {
62
+ names.add(p.getName());
63
+ }
64
+ return names;
65
+ }
66
+ function ifTestReferencesParam(ifStmt, params) {
67
+ const test = ifStmt.getExpression();
68
+ // forEachDescendant does not visit the root; check the root first so
69
+ // simple `if (paramName)` cases are caught.
70
+ if (test.isKind(SyntaxKind.Identifier)) {
71
+ if (params.has(test.getText()))
72
+ return true;
73
+ }
74
+ let referenced = false;
75
+ test.forEachDescendant((node) => {
76
+ if (referenced)
77
+ return;
78
+ if (node.isKind(SyntaxKind.Identifier)) {
79
+ const id = node;
80
+ if (params.has(id.getText()))
81
+ referenced = true;
82
+ }
83
+ });
84
+ return referenced;
85
+ }
86
+ function ifContainsIo(stmt) {
87
+ let found = false;
88
+ stmt.forEachDescendant((node) => {
89
+ if (found)
90
+ return;
91
+ if (!node.isKind(SyntaxKind.CallExpression))
92
+ return;
93
+ const text = node.getText();
94
+ for (const rx of IO_CALLEE_PATTERNS) {
95
+ if (rx.test(text)) {
96
+ found = true;
97
+ return;
98
+ }
99
+ }
100
+ });
101
+ return found;
102
+ }
103
+ function getFnName(fn) {
104
+ if (fn.isKind(SyntaxKind.FunctionDeclaration)) {
105
+ return fn.getName() ?? "";
106
+ }
107
+ if (fn.isKind(SyntaxKind.MethodDeclaration)) {
108
+ return fn.getName();
109
+ }
110
+ return "";
111
+ }
112
+ //# sourceMappingURL=decision-router.js.map