any-doctor 0.0.4 → 0.0.7
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/CONTEXT.md +16 -6
- package/bin/analysis-host.d.ts +6 -3
- package/bin/analysis-host.js +36 -25
- package/bin/analysis.d.ts +10 -2
- package/bin/analysis.js +181 -7
- package/bin/certify.d.ts +15 -0
- package/bin/certify.js +377 -0
- package/bin/cli.js +2 -2
- package/bin/cohort.d.ts +1 -0
- package/bin/cohort.js +1 -3
- package/bin/contract.d.ts +32 -1
- package/bin/doctor-loader.mjs +17 -79
- package/bin/mask.js +7 -2
- package/bin/sdk.js +12 -1
- package/docs/HANDOFF.md +64 -0
- package/docs/decisions.md +347 -0
- package/doctors/async-doctor.mjs +7 -0
- package/doctors/convex-doctor.fixtures.mjs +462 -599
- package/doctors/convex-doctor.mjs +162 -100
- package/doctors/effect-v4-doctor.mjs +21 -46
- package/doctors/openrouter-doctor.mjs +11 -46
- package/doctors/slop-doctor.fixtures.mjs +86 -0
- package/doctors/slop-doctor.mjs +151 -63
- package/fixtures/innocent/dynamic-import.ts +6 -0
- package/fixtures/innocent/index.ts +22 -0
- package/fixtures/innocent/jsx-use.tsx +13 -0
- package/fixtures/innocent/namespaced-api.ts +9 -0
- package/fixtures/innocent/normalize-dash.ts +4 -0
- package/fixtures/innocent/normalize-underscore.ts +4 -0
- package/fixtures/innocent/parts.tsx +3 -0
- package/fixtures/innocent/promise-batching.ts +22 -0
- package/fixtures/innocent/qa-guard.ts +6 -0
- package/fixtures/innocent/rest-exclusion.ts +6 -0
- package/fixtures/innocent/type-semicolons.ts +3 -0
- package/fixtures/innocent/widget.ts +3 -0
- package/fixtures/sensitivity/billing-triple/billing.ts +32 -0
- package/fixtures/sensitivity/billing-triple/expect.json +11 -0
- package/fixtures/sensitivity/dead-prompt-builders/expect.json +10 -0
- package/fixtures/sensitivity/dead-prompt-builders/prompts.ts +13 -0
- package/fixtures/sensitivity/ratelimiter-unbounded-collect/expect.json +9 -0
- package/fixtures/sensitivity/ratelimiter-unbounded-collect/rateLimiter.ts +18 -0
- package/package.json +3 -2
- package/skill/any-doctor.skill.md +93 -33
package/CONTEXT.md
CHANGED
|
@@ -39,9 +39,9 @@ call: the runner's bounded pool, the crash fold (a crash is data — id
|
|
|
39
39
|
plus full error detail, never a throw), the process-wide analysis fold,
|
|
40
40
|
the per-doctor paths, the file-count policy, and the timing. Progress
|
|
41
41
|
events (settle order) are the only side channel — the live line renders
|
|
42
|
-
them, it never joins the fold. Skips are a discovery fact, so
|
|
43
|
-
command layer, which owns discovery,
|
|
44
|
-
outcome.
|
|
42
|
+
them, it never joins the fold. Skips are a discovery fact, so they ride
|
|
43
|
+
in with the CohortSpec: the command layer, which owns discovery, hands
|
|
44
|
+
them to the run and the outcome comes back complete.
|
|
45
45
|
|
|
46
46
|
## Summary
|
|
47
47
|
|
|
@@ -160,9 +160,14 @@ An identity question asked through `ctx.analysis`: which **Binding** a
|
|
|
160
160
|
name resolves to, and every **Reference** to it. `ctx.analysis.bindings(file)`
|
|
161
161
|
returns the file's whole identity model in one answer — every binding
|
|
162
162
|
with its declaration span and its references (positions in ctx.search's
|
|
163
|
-
convention, plus read/write).
|
|
163
|
+
convention, plus read/write). `ctx.analysis.spans(file)` (D26) returns
|
|
164
|
+
every function-like **Span** — functions, methods, arrows, classes with
|
|
165
|
+
name, asyncness, and extent — as an AST fact, replacing the brace-counting
|
|
166
|
+
scans doctors once carried privately (the audit truncation bugs lived in
|
|
167
|
+
those approximations). The engine is optional (oxc-parser +
|
|
164
168
|
eslint-scope behind the Engine seam's second adapter): checks declare
|
|
165
|
-
the analysis they need on their CheckMeta (`needs`
|
|
169
|
+
the analysis they need on their CheckMeta (`needs` — vocabulary:
|
|
170
|
+
"bindings", "spans"), narrow without it,
|
|
166
171
|
and the report renders "narrowed" — a degraded run is visible, never
|
|
167
172
|
silent. References answer by position, so analysis queries compose with
|
|
168
173
|
rule queries: shapes from one engine, identities from the other.
|
|
@@ -242,7 +247,12 @@ workflow's second pass; a doctor ships only after surviving its attack.
|
|
|
242
247
|
|
|
243
248
|
Running a doctor program against its fixtures and diffing the findings.
|
|
244
249
|
The trust gate: a doctor program is not considered working until verify
|
|
245
|
-
passes.
|
|
250
|
+
passes. The policies that compose the gate — the claim contract, the
|
|
251
|
+
per-fixture diff, the shared innocent corpus, the duplicate-location
|
|
252
|
+
probe, the shared sensitivity corpus — live in one module, the
|
|
253
|
+
Certification harness (`src/certify.ts`), behind one interface:
|
|
254
|
+
`certify(mod, fixtures) -> result rows`. The doctor loader calls it once;
|
|
255
|
+
prevention tiers land there, not in loader choreography.
|
|
246
256
|
|
|
247
257
|
## Runner
|
|
248
258
|
|
package/bin/analysis-host.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AnalysisFile, Mode } from "./contract.js";
|
|
2
|
-
import { analysisStatus, analyzeBindings } from "./analysis.js";
|
|
1
|
+
import { AnalysisFile, AnalysisSpans, Mode } from "./contract.js";
|
|
2
|
+
import { analysisStatus, analyzeBindings, analyzeSpans } from "./analysis.js";
|
|
3
3
|
type Analyzer = typeof analyzeBindings;
|
|
4
|
+
type SpansAnalyzer = typeof analyzeSpans;
|
|
4
5
|
type Status = typeof analysisStatus;
|
|
5
6
|
export declare function clearAnalysisCache(): void;
|
|
6
7
|
export interface AnalysisRequestBody {
|
|
@@ -13,8 +14,10 @@ export type AnalysisResponse = {
|
|
|
13
14
|
reason?: string;
|
|
14
15
|
} | {
|
|
15
16
|
file: AnalysisFile;
|
|
17
|
+
} | {
|
|
18
|
+
file: AnalysisSpans;
|
|
16
19
|
} | {
|
|
17
20
|
error: string;
|
|
18
21
|
};
|
|
19
|
-
export declare function handleAnalysisRequest(req: AnalysisRequestBody, mode: Mode, analyzer?: Analyzer, status?: Status): AnalysisResponse;
|
|
22
|
+
export declare function handleAnalysisRequest(req: AnalysisRequestBody, mode: Mode, analyzer?: Analyzer, status?: Status, spansAnalyzer?: SpansAnalyzer): AnalysisResponse;
|
|
20
23
|
export {};
|
package/bin/analysis-host.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { analysisStatus, analyzeBindings } from "./analysis.js";
|
|
3
|
+
import { analysisStatus, analyzeBindings, analyzeSpans } from "./analysis.js";
|
|
4
4
|
import { searchBase, withinBase } from "./search-host.js";
|
|
5
5
|
// One cache per host process. The host lives in the runner process, so
|
|
6
6
|
// the lifetime is the any-doctor invocation; across a cohort's doctors
|
|
7
7
|
// the same unchanged file answers from memory.
|
|
8
8
|
const modelCache = new Map();
|
|
9
|
+
const spansCache = new Map();
|
|
9
10
|
// Test seam: the model cache is keyed by mtime+size for the process
|
|
10
11
|
// lifetime; tests bust it between cases. Invisible to slop-doctor's
|
|
11
12
|
// default run (test-file consumers are the documented narrowing).
|
|
12
13
|
export function clearAnalysisCache() {
|
|
13
14
|
modelCache.clear();
|
|
15
|
+
spansCache.clear();
|
|
14
16
|
}
|
|
15
|
-
export function handleAnalysisRequest(req, mode, analyzer = analyzeBindings, status = analysisStatus) {
|
|
17
|
+
export function handleAnalysisRequest(req, mode, analyzer = analyzeBindings, status = analysisStatus, spansAnalyzer = analyzeSpans) {
|
|
16
18
|
const base = searchBase(mode);
|
|
17
19
|
const root = typeof req.root === "string" ? path.resolve(req.root) : "";
|
|
18
20
|
if (base === "" || !withinBase(root, base)) {
|
|
@@ -22,35 +24,44 @@ export function handleAnalysisRequest(req, mode, analyzer = analyzeBindings, sta
|
|
|
22
24
|
const s = status();
|
|
23
25
|
return s.available ? { available: true } : { available: false, reason: s.reason };
|
|
24
26
|
}
|
|
25
|
-
if (req.kind === "bindings") {
|
|
27
|
+
if (req.kind === "bindings" || req.kind === "spans") {
|
|
26
28
|
if (typeof req.file !== "string" || req.file === "") {
|
|
27
|
-
return { error:
|
|
29
|
+
return { error: `ctx.analysis.${req.kind} needs a "file" path` };
|
|
28
30
|
}
|
|
29
31
|
const abs = path.resolve(root, req.file);
|
|
30
32
|
if (!withinBase(abs, root)) {
|
|
31
33
|
return { error: `ctx.analysis failed: file is outside the search root: ${req.file}` };
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let size;
|
|
36
|
-
try {
|
|
37
|
-
const stat = fs.statSync(abs);
|
|
38
|
-
mtimeMs = stat.mtimeMs;
|
|
39
|
-
size = stat.size;
|
|
40
|
-
const cached = modelCache.get(abs);
|
|
41
|
-
if (cached && cached.mtimeMs === mtimeMs && cached.size === size)
|
|
42
|
-
return { file: cached.file };
|
|
43
|
-
source = fs.readFileSync(abs, "utf8");
|
|
35
|
+
if (req.kind === "bindings") {
|
|
36
|
+
return cachedModel(abs, root, modelCache, analyzer, req.file);
|
|
44
37
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
return cachedModel(abs, root, spansCache, spansAnalyzer, req.file);
|
|
39
|
+
}
|
|
40
|
+
return { error: `unknown analysis kind ${JSON.stringify(req.kind)} — known kinds: available, bindings, spans` };
|
|
41
|
+
}
|
|
42
|
+
// The shared per-file model lifecycle: stat (cache hit on mtime+size),
|
|
43
|
+
// read, compute, cache. Bindings and spans are the same policy over two
|
|
44
|
+
// analyzers and two caches.
|
|
45
|
+
function cachedModel(abs, root, cache, compute, relFile) {
|
|
46
|
+
let source;
|
|
47
|
+
let mtimeMs;
|
|
48
|
+
let size;
|
|
49
|
+
try {
|
|
50
|
+
const stat = fs.statSync(abs);
|
|
51
|
+
mtimeMs = stat.mtimeMs;
|
|
52
|
+
size = stat.size;
|
|
53
|
+
const cached = cache.get(abs);
|
|
54
|
+
if (cached && cached.mtimeMs === mtimeMs && cached.size === size)
|
|
55
|
+
return { file: cached.file };
|
|
56
|
+
source = fs.readFileSync(abs, "utf8");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { error: `ctx.analysis failed: cannot read ${relFile}` };
|
|
54
60
|
}
|
|
55
|
-
|
|
61
|
+
const rel = path.relative(root, abs);
|
|
62
|
+
const r = compute(rel, source);
|
|
63
|
+
if (!r.ok)
|
|
64
|
+
return { error: r.error };
|
|
65
|
+
cache.set(abs, { mtimeMs, size, file: r.file });
|
|
66
|
+
return { file: r.file };
|
|
56
67
|
}
|
package/bin/analysis.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AnalysisFile, BindingInfo, BindingRef } from "./contract.js";
|
|
2
|
-
export type { AnalysisFile, BindingInfo, BindingRef };
|
|
1
|
+
import { AnalysisFile, AnalysisSpans, BindingInfo, BindingRef, SpanInfo } from "./contract.js";
|
|
2
|
+
export type { AnalysisFile, AnalysisSpans, BindingInfo, BindingRef, SpanInfo };
|
|
3
3
|
export interface AnalysisStatus {
|
|
4
4
|
available: true;
|
|
5
5
|
}
|
|
@@ -15,4 +15,12 @@ export type AnalysisResult = {
|
|
|
15
15
|
ok: false;
|
|
16
16
|
error: string;
|
|
17
17
|
};
|
|
18
|
+
export type SpansResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
file: AnalysisSpans;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function analyzeSpans(file: string, source: string): SpansResult;
|
|
18
26
|
export declare function analyzeBindings(file: string, source: string): AnalysisResult;
|
package/bin/analysis.js
CHANGED
|
@@ -8,10 +8,8 @@ function loadStack() {
|
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
9
|
const { parseSync } = require_("oxc-parser");
|
|
10
10
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
-
const { analyze } = require_("eslint-
|
|
12
|
-
|
|
13
|
-
const keys = require_("eslint-visitor-keys");
|
|
14
|
-
loaded = { parseSync, analyze, keys };
|
|
11
|
+
const { analyze } = require_("@typescript-eslint/scope-manager");
|
|
12
|
+
loaded = { parseSync, analyze };
|
|
15
13
|
}
|
|
16
14
|
catch (e) {
|
|
17
15
|
loaded = { error: `the analysis engine is not installed (${e instanceof Error ? e.message : String(e)}) — npm install oxc-parser` };
|
|
@@ -22,6 +20,89 @@ export function analysisStatus() {
|
|
|
22
20
|
const stack = loadStack();
|
|
23
21
|
return stack.error !== undefined ? { available: false, reason: stack.error } : { available: true };
|
|
24
22
|
}
|
|
23
|
+
// One file in, every function-like span out (D26): an AST fact answering
|
|
24
|
+
// what the doctors' private brace-counting copies could only approximate.
|
|
25
|
+
// Semicolons inside multi-line callbacks, strings containing braces, JSX —
|
|
26
|
+
// none of it can truncate a span that the parser already knows. Named
|
|
27
|
+
// declarations carry their id; arrows and anonymous expressions carry null.
|
|
28
|
+
export function analyzeSpans(file, source) {
|
|
29
|
+
const stack = loadStack();
|
|
30
|
+
if (stack.error !== undefined)
|
|
31
|
+
return { ok: false, error: stack.error };
|
|
32
|
+
let program;
|
|
33
|
+
try {
|
|
34
|
+
program = stack.parseSync(file, source, { sourceType: "module" }).program;
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return { ok: false, error: `analysis failed to parse ${file}: ${e instanceof Error ? e.message : String(e)}` };
|
|
38
|
+
}
|
|
39
|
+
const pos = positioner(source);
|
|
40
|
+
const spans = [];
|
|
41
|
+
const visit = (node) => {
|
|
42
|
+
if (!node || typeof node !== "object")
|
|
43
|
+
return;
|
|
44
|
+
const n = node;
|
|
45
|
+
let span = null;
|
|
46
|
+
if (n.type === "FunctionDeclaration" || n.type === "TSDeclareFunction") {
|
|
47
|
+
span = spanOf(n, "function", idName(n.id));
|
|
48
|
+
}
|
|
49
|
+
else if (n.type === "FunctionExpression") {
|
|
50
|
+
span = spanOf(n, "function-expression", idName(n.id));
|
|
51
|
+
}
|
|
52
|
+
else if (n.type === "ArrowFunctionExpression") {
|
|
53
|
+
span = spanOf(n, "arrow", null);
|
|
54
|
+
}
|
|
55
|
+
else if (n.type === "ClassDeclaration" || n.type === "ClassExpression") {
|
|
56
|
+
span = spanOf(n, "class", idName(n.id));
|
|
57
|
+
}
|
|
58
|
+
else if (n.type === "MethodDefinition" || n.type === "TSAbstractMethodDefinition") {
|
|
59
|
+
span = spanOf(n.value, "method", propertyName(n.key));
|
|
60
|
+
}
|
|
61
|
+
if (span !== null)
|
|
62
|
+
spans.push(span);
|
|
63
|
+
for (const key of Object.keys(n)) {
|
|
64
|
+
if (key === "range" || key === "start" || key === "end")
|
|
65
|
+
continue;
|
|
66
|
+
const v = n[key];
|
|
67
|
+
if (Array.isArray(v)) {
|
|
68
|
+
for (const child of v)
|
|
69
|
+
if (isNode(child))
|
|
70
|
+
visit(child);
|
|
71
|
+
}
|
|
72
|
+
else if (isNode(v))
|
|
73
|
+
visit(v);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
visit(program);
|
|
77
|
+
return { ok: true, file: { file, spans } };
|
|
78
|
+
function spanOf(n, kind, name) {
|
|
79
|
+
if (typeof n.start !== "number" || typeof n.end !== "number")
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
kind,
|
|
83
|
+
name,
|
|
84
|
+
async: n.async === true,
|
|
85
|
+
line: pos.line(n.start),
|
|
86
|
+
column: pos.column(n.start),
|
|
87
|
+
endLine: pos.line(n.end),
|
|
88
|
+
endColumn: pos.column(n.end),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function idName(id) {
|
|
93
|
+
return id && typeof id.name === "string" ? id.name : null;
|
|
94
|
+
}
|
|
95
|
+
function propertyName(key) {
|
|
96
|
+
const k = key;
|
|
97
|
+
if (k && typeof k.name === "string")
|
|
98
|
+
return k.name;
|
|
99
|
+
if (k && typeof k.value === "string")
|
|
100
|
+
return k.value; // computed / literal keys
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function isNode(v) {
|
|
104
|
+
return Boolean(v) && typeof v === "object" && typeof v.type === "string";
|
|
105
|
+
}
|
|
25
106
|
// One file in, one identity model out: every binding (declarations,
|
|
26
107
|
// parameters, imports) with the span of its declaring node and every
|
|
27
108
|
// reference to it, read or write. Type-position identifiers never become
|
|
@@ -45,8 +126,6 @@ export function analyzeBindings(file, source) {
|
|
|
45
126
|
try {
|
|
46
127
|
scopeManager = stack.analyze(program, {
|
|
47
128
|
sourceType: "module",
|
|
48
|
-
ecmaVersion: 2026,
|
|
49
|
-
childVisitorKeys: stack.keys.KEYS,
|
|
50
129
|
});
|
|
51
130
|
}
|
|
52
131
|
catch (e) {
|
|
@@ -57,6 +136,10 @@ export function analyzeBindings(file, source) {
|
|
|
57
136
|
const global = scopeManager.globalScope;
|
|
58
137
|
if (global === null)
|
|
59
138
|
return { ok: false, error: `analysis failed to resolve scopes in ${file}` };
|
|
139
|
+
// Language facts computed from the AST once, so doctors never re-derive
|
|
140
|
+
// them with regexes: what is exported, and what is an intentional
|
|
141
|
+
// object-rest exclusion.
|
|
142
|
+
const facts = languageFacts(program);
|
|
60
143
|
for (const scope of allScopes(global)) {
|
|
61
144
|
for (const variable of scope.variables) {
|
|
62
145
|
const def = variable.defs[0];
|
|
@@ -64,7 +147,7 @@ export function analyzeBindings(file, source) {
|
|
|
64
147
|
continue; // builtins and implicit globals carry no def
|
|
65
148
|
// The declaration's own extent: for variables the declarator (so a
|
|
66
149
|
// binding's span contains its initializer), for parameters the
|
|
67
|
-
// identifier itself (
|
|
150
|
+
// identifier itself (scope managers hand the whole function node for
|
|
68
151
|
// params, which would swallow the body).
|
|
69
152
|
const node = def.node;
|
|
70
153
|
const span = def.type === "Parameter" ? def.name.range : ((_a = node === null || node === void 0 ? void 0 : node.range) !== null && _a !== void 0 ? _a : def.name.range);
|
|
@@ -86,11 +169,102 @@ export function analyzeBindings(file, source) {
|
|
|
86
169
|
endColumn: pos.column(r.identifier.range[1]),
|
|
87
170
|
write: r.isWrite(),
|
|
88
171
|
})),
|
|
172
|
+
exported: facts.exported.has(variable.name) || undefined,
|
|
173
|
+
excluded: facts.excluded.has(variable.name) || undefined,
|
|
89
174
|
});
|
|
90
175
|
}
|
|
91
176
|
}
|
|
92
177
|
return { ok: true, file: { file, bindings } };
|
|
93
178
|
}
|
|
179
|
+
function languageFacts(program) {
|
|
180
|
+
const exported = new Set();
|
|
181
|
+
const excluded = new Set();
|
|
182
|
+
const visit = (node) => {
|
|
183
|
+
if (!node || typeof node !== "object")
|
|
184
|
+
return;
|
|
185
|
+
const n = node;
|
|
186
|
+
if (n.type === "ExportNamedDeclaration" && n.declaration) {
|
|
187
|
+
collectDeclaredNames(n.declaration, exported);
|
|
188
|
+
}
|
|
189
|
+
if (n.type === "ExportNamedDeclaration" && Array.isArray(n.specifiers)) {
|
|
190
|
+
for (const spec of n.specifiers) {
|
|
191
|
+
if (spec.local && typeof spec.local.name === "string") {
|
|
192
|
+
exported.add(spec.local.name);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (n.type === "ExportDefaultDeclaration") {
|
|
197
|
+
const d = n.declaration;
|
|
198
|
+
if (d && typeof d.id === "object" && d.id && typeof d.id.name === "string") {
|
|
199
|
+
exported.add(d.id.name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (n.type === "ObjectPattern" && Array.isArray(n.properties)) {
|
|
203
|
+
const hasRest = n.properties.some((p) => p.type === "RestElement");
|
|
204
|
+
if (hasRest) {
|
|
205
|
+
for (const p of n.properties) {
|
|
206
|
+
if (p.type === "Property" && p.value && p.value.type === "Identifier") {
|
|
207
|
+
excluded.add(p.value.name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const key of Object.keys(n)) {
|
|
213
|
+
if (key === "range" || key === "start" || key === "end" || key === "tokens" || key === "comments")
|
|
214
|
+
continue;
|
|
215
|
+
const v = n[key];
|
|
216
|
+
if (Array.isArray(v)) {
|
|
217
|
+
for (const child of v) {
|
|
218
|
+
if (child && typeof child === "object" && typeof child.type === "string")
|
|
219
|
+
visit(child);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (v && typeof v === "object" && typeof v.type === "string") {
|
|
223
|
+
visit(v);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
visit(program);
|
|
228
|
+
return { exported, excluded };
|
|
229
|
+
}
|
|
230
|
+
// Every name a declaration binds: function/class ids, every declarator of
|
|
231
|
+
// a variable statement (multi-declarator, destructured patterns), and the
|
|
232
|
+
// property names of nested object/array patterns.
|
|
233
|
+
function collectDeclaredNames(decl, into) {
|
|
234
|
+
if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration" || decl.type === "TSDeclareFunction") {
|
|
235
|
+
if (decl.id && typeof decl.id.name === "string") {
|
|
236
|
+
into.add(decl.id.name);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (decl.type === "VariableDeclaration" && Array.isArray(decl.declarations)) {
|
|
241
|
+
for (const d of decl.declarations) {
|
|
242
|
+
collectPatternNames(d.id, into);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function collectPatternNames(pattern, into) {
|
|
247
|
+
var _a, _b;
|
|
248
|
+
if (pattern.type === "Identifier") {
|
|
249
|
+
into.add(pattern.name);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if ((pattern.type === "ObjectPattern" || pattern.type === "ArrayPattern") && Array.isArray((_a = pattern.properties) !== null && _a !== void 0 ? _a : pattern.elements)) {
|
|
253
|
+
const items = ((_b = pattern.properties) !== null && _b !== void 0 ? _b : pattern.elements);
|
|
254
|
+
for (const item of items) {
|
|
255
|
+
if (!item)
|
|
256
|
+
continue;
|
|
257
|
+
if (item.type === "Property")
|
|
258
|
+
collectPatternNames(item.value, into);
|
|
259
|
+
else if (item.type === "RestElement")
|
|
260
|
+
collectPatternNames(item.argument, into);
|
|
261
|
+
else
|
|
262
|
+
collectPatternNames(item, into);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (pattern.type === "AssignmentPattern")
|
|
266
|
+
collectPatternNames(pattern.left, into);
|
|
267
|
+
}
|
|
94
268
|
// eslint-scope expects `range: [start, end]` on nodes; oxc emits start/end.
|
|
95
269
|
function addRanges(node) {
|
|
96
270
|
if (!node || typeof node !== "object")
|
package/bin/certify.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buildCtx } from "./sdk.js";
|
|
2
|
+
import type { Fixture, FixtureResult, RunResult } from "./contract.js";
|
|
3
|
+
export interface DoctorModule {
|
|
4
|
+
meta?: unknown;
|
|
5
|
+
doctor?: (ctx: ReturnType<typeof buildCtx>["ctx"]) => unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function runOnce(root: string, mod: DoctorModule, opts: {
|
|
8
|
+
includeTests: boolean;
|
|
9
|
+
}): Promise<RunResult>;
|
|
10
|
+
export declare class ClaimContractViolation extends Error {
|
|
11
|
+
readonly problems: string[];
|
|
12
|
+
constructor(problems: string[]);
|
|
13
|
+
}
|
|
14
|
+
export declare function validateClaimContract(mod: DoctorModule): void;
|
|
15
|
+
export declare function certify(mod: DoctorModule, fixtures: Fixture[]): Promise<FixtureResult[]>;
|