any-doctor 0.1.2 → 0.2.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/CONTEXT.md +65 -7
- package/README.md +4 -0
- package/bin/analysis-host.d.ts +20 -1
- package/bin/analysis-host.js +147 -23
- package/bin/analysis.d.ts +13 -0
- package/bin/analysis.js +60 -6
- package/bin/authoring.d.ts +97 -0
- package/bin/authoring.js +269 -0
- package/bin/call-structure.d.ts +4 -0
- package/bin/call-structure.js +575 -0
- package/bin/certify.d.ts +5 -1
- package/bin/certify.js +69 -17
- package/bin/cli.js +337 -39
- package/bin/cohort.js +1 -1
- package/bin/contract.d.ts +268 -4
- package/bin/contract.js +12 -8
- package/bin/dashboard.d.ts +11 -1
- package/bin/dashboard.js +64 -23
- package/bin/doctor-sdk.d.ts +41 -0
- package/bin/doctor-sdk.js +823 -0
- package/bin/doctor-tree.js +2 -1
- package/bin/file-scope.d.ts +17 -0
- package/bin/file-scope.js +72 -0
- package/bin/function-structure.d.ts +11 -0
- package/bin/function-structure.js +89 -0
- package/bin/palette.d.ts +1 -0
- package/bin/palette.js +1 -1
- package/bin/project-consumers.d.ts +27 -0
- package/bin/project-consumers.js +1054 -0
- package/bin/recipe-definitions.d.ts +27 -0
- package/bin/recipe-definitions.js +47 -0
- package/bin/recipes/forbidden-call.d.ts +15 -0
- package/bin/recipes/forbidden-call.js +147 -0
- package/bin/recipes/required-option.d.ts +8 -0
- package/bin/recipes/required-option.js +116 -0
- package/bin/recipes/resource-without-release.d.ts +12 -0
- package/bin/recipes/resource-without-release.js +182 -0
- package/bin/recipes/support.d.ts +12 -0
- package/bin/recipes/support.js +54 -0
- package/bin/recipes/types.d.ts +61 -0
- package/bin/recipes/types.js +1 -0
- package/bin/recipes/unhandled-value.d.ts +12 -0
- package/bin/recipes/unhandled-value.js +161 -0
- package/bin/report.js +18 -11
- package/bin/runner.d.ts +6 -1
- package/bin/runner.js +14 -2
- package/bin/score.d.ts +3 -0
- package/bin/score.js +6 -2
- package/bin/sdk.d.ts +4 -1
- package/bin/sdk.js +246 -40
- package/bin/search-host.js +5 -2
- package/bin/semantic-query-parsers.d.ts +3 -0
- package/bin/semantic-query-parsers.js +18 -0
- package/bin/summary.d.ts +1 -0
- package/bin/summary.js +38 -2
- package/bin/tty.js +49 -9
- package/bin/value-flow.d.ts +111 -0
- package/bin/value-flow.js +261 -0
- package/docs/call-structure.md +78 -0
- package/docs/decisions.md +82 -0
- package/docs/doctor-reliability.md +61 -15
- package/docs/doctor-sdk.md +666 -0
- package/docs/openrouter-reliability.md +75 -0
- package/docs/project-consumer-analysis.md +190 -0
- package/docs/research-openrouter-patterns.md +35 -39
- package/doctors/AGENTS.md +506 -0
- package/doctors/async.fixtures.mjs +106 -61
- package/doctors/async.mjs +47 -467
- package/doctors/convex.fixtures.mjs +81 -19
- package/doctors/convex.mjs +763 -487
- package/doctors/deepgram.fixtures.mjs +529 -0
- package/doctors/deepgram.mjs +637 -0
- package/doctors/openrouter.fixtures.mjs +67 -4
- package/doctors/openrouter.mjs +442 -135
- package/doctors/slop.fixtures.mjs +25 -12
- package/doctors/slop.mjs +45 -163
- package/fixtures/doctor-sdk-recipe-only.fixtures.mjs +2 -0
- package/fixtures/doctor-sdk-recipe-only.mjs +12 -0
- package/fixtures/doctor-sdk-reference.fixtures.mjs +14 -0
- package/fixtures/doctor-sdk-reference.mjs +23 -0
- package/package.json +6 -5
- package/skill/any-doctor.skill.md +44 -4
- package/skill/author-workflow.md +90 -0
- package/docs/evidence/repair-after-0.0.7/convex-verify.txt +0 -103
- package/docs/evidence/repair-after-0.0.7/sift-convex-packed.json +0 -583
- package/docs/evidence/repair-after-0.0.7/sift-convex-published.json +0 -1023
- package/docs/evidence/repair-after-0.0.7/sift-slop-packed.json +0 -956
- package/docs/evidence/repair-after-0.0.7/sift-slop-published.json +0 -951
- package/docs/evidence/repair-after-0.0.7/tests.txt +0 -307
- package/docs/plans/analysis-improvements.md +0 -234
- package/docs/plans/finding-lifecycle/design.md +0 -209
- package/docs/plans/finding-lifecycle/milestones.md +0 -120
- package/docs/plans/finding-lifecycle/proposal.md +0 -104
package/CONTEXT.md
CHANGED
|
@@ -163,6 +163,51 @@ The interface shared by doctor programs, the runner, the report, the
|
|
|
163
163
|
fixture harness, and the generator prompt. The single place where the
|
|
164
164
|
shape of ctx, meta, findings, and the runner protocol is defined.
|
|
165
165
|
|
|
166
|
+
## Doctor SDK
|
|
167
|
+
|
|
168
|
+
The deeper semantic portion of the DoctorCtx interface: host-owned queries for
|
|
169
|
+
identity, value disposition, resource lifetime and option presence. It hides the
|
|
170
|
+
parser/scope/type adapters and returns bounded evidence plus an explicit known or
|
|
171
|
+
unknown result. It is not initially an importable package; Confinement still gives
|
|
172
|
+
doctor programs one route to product capabilities through `ctx`. See the
|
|
173
|
+
[Doctor SDK design](docs/plans/doctor-sdk/design.md).
|
|
174
|
+
|
|
175
|
+
## Semantic result
|
|
176
|
+
|
|
177
|
+
The answer from one Doctor SDK query: a known value with supporting evidence, or
|
|
178
|
+
an unknown result with a named reason. Unknown never means absent, discarded,
|
|
179
|
+
unreleased or safe. A check that cannot support its claim from a known result
|
|
180
|
+
abstains and exposes narrowed coverage.
|
|
181
|
+
|
|
182
|
+
## Value Path
|
|
183
|
+
|
|
184
|
+
A bounded Doctor SDK query that establishes whether one static property path is
|
|
185
|
+
present or absent at one source observation. A present result identifies the
|
|
186
|
+
terminal expression and may include a primitive constant. Unsupported flow is
|
|
187
|
+
unknown; it never implies absence or safety.
|
|
188
|
+
|
|
189
|
+
## Check recipe
|
|
190
|
+
|
|
191
|
+
A host-owned composition of Doctor SDK queries for a recurring check family, such
|
|
192
|
+
as an unhandled value, a resource without release, or a required/recommended call
|
|
193
|
+
option. The recipe owns recurring mechanics, uncertainty, evidence and challenge
|
|
194
|
+
selection; the doctor owns technology-specific selectors, meaning and copy.
|
|
195
|
+
|
|
196
|
+
Each maintained recipe is locally complete under `src/recipes/`: its module owns
|
|
197
|
+
the host kind and implied needs, wire-query parser, semantic composition,
|
|
198
|
+
authoring catalog entry and generated challenge profile. `recipe-definitions.ts`
|
|
199
|
+
is the one registry consumed by the host, SDK, authoring catalog and certifier.
|
|
200
|
+
Adding or changing a recipe happens in its recipe module rather than by editing
|
|
201
|
+
parallel switches in those consumers.
|
|
202
|
+
|
|
203
|
+
## Challenge profile
|
|
204
|
+
|
|
205
|
+
A maintained certification corpus selected by a check recipe or declared semantic
|
|
206
|
+
capability. It pairs genuine positives with lookalikes, shadowing, aliases,
|
|
207
|
+
transfers, unknown cases, positive neighbors, unavailable analysis and exact
|
|
208
|
+
occurrence witnesses. It supplements author fixtures and independent evaluation;
|
|
209
|
+
it does not replace either.
|
|
210
|
+
|
|
166
211
|
## DoctorCtx (ctx)
|
|
167
212
|
|
|
168
213
|
The capability boundary a doctor program is expected to use: read-only,
|
|
@@ -174,16 +219,28 @@ writes, no subprocesses, no network.
|
|
|
174
219
|
(test-named code files — `*.test.*`/`*.spec.*` with a code extension —
|
|
175
220
|
and `test/`, `tests/`, `__tests__/` directories): tests mimic production
|
|
176
221
|
shapes without being production reads. The one law (`isTestPath`) and
|
|
177
|
-
the one derivation (`includeTestsFor`) live in contract.ts;
|
|
178
|
-
|
|
222
|
+
the one derivation (`includeTestsFor`) live in contract.ts; diagnostic
|
|
223
|
+
inventory and search apply them. Reference evidence includes tests. A Doctor run opts back in with
|
|
179
224
|
`--include-tests`; `ctx.files.read()` is never filtered — an explicit
|
|
180
225
|
path is a deliberate choice. `ctx.files.readMasked()` is the one
|
|
181
226
|
masking implementation (comments and strings blanked, offsets and
|
|
182
227
|
length preserved — a masked position addresses the same char in the
|
|
183
228
|
source); doctors carry no private copies — the bundled pack's remaining
|
|
184
|
-
copies migrate on the recorded triggers. Verify
|
|
185
|
-
|
|
186
|
-
|
|
229
|
+
copies migrate on the recorded triggers. Verify defaults to the deliberately seeded
|
|
230
|
+
test scope; fixtures can also exercise ordinary diagnostic defaults.
|
|
231
|
+
See [project consumer facts](docs/project-consumer-analysis.md) for the interfaces.
|
|
232
|
+
|
|
233
|
+
## Diagnostic scope
|
|
234
|
+
|
|
235
|
+
The authorized files eligible for findings in a run. Tests are opt-in and generated
|
|
236
|
+
modules are omitted; this scope is distinct from the evidence used to analyze them.
|
|
237
|
+
|
|
238
|
+
## Consumer evidence
|
|
239
|
+
|
|
240
|
+
An observed reference, declaration dependency, public exposure, or uncertainty
|
|
241
|
+
associated with an exported binding by module identity. Test and generated modules
|
|
242
|
+
may supply this evidence without becoming diagnostic targets; absence within
|
|
243
|
+
supported coverage is a review candidate, never deletion authorization.
|
|
187
244
|
|
|
188
245
|
## Rule query
|
|
189
246
|
|
|
@@ -217,8 +274,9 @@ the analysis they need on their CheckMeta (`needs` — vocabulary:
|
|
|
217
274
|
and the report renders "narrowed" — a degraded run is visible, never
|
|
218
275
|
silent. `ctx.analysis.calls(file)` provides immediate call use, receiver identity,
|
|
219
276
|
inline callback registration, and linked call ranges. These are syntax facts;
|
|
220
|
-
stored, passed, or returned is not a promise-settlement verdict.
|
|
221
|
-
|
|
277
|
+
stored, passed, or returned is not a promise-settlement verdict. A parse or
|
|
278
|
+
adapter error narrows the affected file while analyzable neighboring files
|
|
279
|
+
continue; the partial scan receives no score or grade. References answer by position, so analysis queries compose with
|
|
222
280
|
rule queries: shapes from one engine, identities from the other.
|
|
223
281
|
|
|
224
282
|
## Engine
|
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ Non-terminals and CI never see a prompt — output is stable and pipeable
|
|
|
46
46
|
| **convex** | Convex discipline: indexed reads, bounded collects, validated args, awaited writes, honest runtime boundaries | 15 |
|
|
47
47
|
| **effect-v4-kitlangton** | Effect v4 discipline — the mechanical rules of the [kitlangton Effect skill](https://www.ui-skills.com/skills/kitlangton/effect), enforced | 10 |
|
|
48
48
|
| **openrouter** | OpenRouter discipline: stream errors surfaced, keep-alives skipped, cancellations that stop billing | 5 |
|
|
49
|
+
| **deepgram** | Deepgram discipline: endpoint/model compatibility, streaming options, Read shapes, hosts, and proven browser key exposure | 5 |
|
|
49
50
|
| **async** | Async and concurrency: dropped promise results, uncleared timers, fetch hygiene | 3 |
|
|
50
51
|
|
|
51
52
|
Checks ship positive and innocent-lookalike fixtures. `verify` compares an
|
|
@@ -123,6 +124,9 @@ npx any-doctor@latest run --all --fail-on warning --base origin/main
|
|
|
123
124
|
|
|
124
125
|
## Docs
|
|
125
126
|
|
|
127
|
+
- [Doctor SDK semantic queries and recipes](docs/doctor-sdk.md) — confined,
|
|
128
|
+
evidence-bearing analysis and automatic recipe challenge profiles
|
|
129
|
+
|
|
126
130
|
| Doc | What it holds |
|
|
127
131
|
|---|---|
|
|
128
132
|
| [skill/any-doctor.skill.md](skill/any-doctor.skill.md) | The authoring contract — what your agent reads to write doctors |
|
package/bin/analysis-host.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProjectConsumers } from "./project-consumers.js";
|
|
2
|
+
import { FunctionStructure } from "./function-structure.js";
|
|
3
|
+
import { AnalysisFile, AnalysisSpans, AnalysisCalls, IdentityValue, Mode, OptionPresence, RecipeDecision, ResourceLifetime, SemanticResult, SourceRange, ValueDisposition, ValuePathValue } from "./contract.js";
|
|
2
4
|
import { analysisStatus, analyzeBindings, analyzeSpans, analyzeCalls } from "./analysis.js";
|
|
3
5
|
type Analyzer = typeof analyzeBindings;
|
|
4
6
|
type SpansAnalyzer = typeof analyzeSpans;
|
|
@@ -8,16 +10,33 @@ export interface AnalysisRequestBody {
|
|
|
8
10
|
kind?: unknown;
|
|
9
11
|
file?: unknown;
|
|
10
12
|
root?: unknown;
|
|
13
|
+
sourceDigest?: unknown;
|
|
14
|
+
expression?: unknown;
|
|
15
|
+
query?: unknown;
|
|
11
16
|
}
|
|
12
17
|
export type AnalysisResponse = {
|
|
18
|
+
project: ProjectConsumers;
|
|
19
|
+
} | {
|
|
20
|
+
structures: FunctionStructure[];
|
|
21
|
+
} | {
|
|
13
22
|
available: boolean;
|
|
14
23
|
reason?: string;
|
|
24
|
+
provider?: import("./contract.js").SemanticProviderProvenance;
|
|
15
25
|
} | {
|
|
16
26
|
file: AnalysisFile;
|
|
17
27
|
} | {
|
|
18
28
|
file: AnalysisSpans;
|
|
19
29
|
} | {
|
|
20
30
|
file: AnalysisCalls;
|
|
31
|
+
} | {
|
|
32
|
+
semantic: SemanticResult<IdentityValue | {
|
|
33
|
+
matches: boolean;
|
|
34
|
+
} | ValuePathValue | ValueDisposition | ResourceLifetime | OptionPresence | RecipeDecision>;
|
|
35
|
+
execution?: {
|
|
36
|
+
modelRequests: number;
|
|
37
|
+
modelCacheHits: number;
|
|
38
|
+
};
|
|
39
|
+
subject?: SourceRange;
|
|
21
40
|
} | {
|
|
22
41
|
error: string;
|
|
23
42
|
};
|
package/bin/analysis-host.js
CHANGED
|
@@ -1,22 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { projectConsumers } from "./project-consumers.js";
|
|
3
|
+
import { functionStructures } from "./function-structure.js";
|
|
1
4
|
import * as fs from "fs";
|
|
2
5
|
import * as path from "path";
|
|
3
|
-
import { searchBase, withinBase, withinDir } from "./contract.js";
|
|
4
|
-
import { analysisStatus, analyzeBindings, analyzeSpans, analyzeCalls } from "./analysis.js";
|
|
6
|
+
import { searchBase, SEMANTIC_RESULT_VERSION, withinBase, withinDir } from "./contract.js";
|
|
7
|
+
import { analysisStatus, analyzeBindings, analyzeSpans, analyzeCalls, semanticProviderProvenance } from "./analysis.js";
|
|
8
|
+
import { callIdentityResult, identityResult, optionPresenceResult, recipeEvaluationRuntime, resourceLifetimeResult, valueAtPathResult, valueDispositionResult } from "./doctor-sdk.js";
|
|
9
|
+
import { RECIPE_DEFINITIONS } from "./recipe-definitions.js";
|
|
10
|
+
import { parseIdentityQuery, parseOptionQuery } from "./semantic-query-parsers.js";
|
|
5
11
|
// One cache per host process. The host lives in the runner process, so
|
|
6
12
|
// the lifetime is the any-doctor invocation; across a cohort's doctors
|
|
7
13
|
// the same unchanged file answers from memory.
|
|
8
14
|
const modelCache = new Map();
|
|
9
15
|
const callsCache = new Map();
|
|
10
16
|
const spansCache = new Map();
|
|
11
|
-
// Test seam: the model cache is keyed by
|
|
17
|
+
// Test seam: the model cache is keyed by content digest for the process
|
|
12
18
|
// lifetime; tests bust it between cases. Invisible to slop's
|
|
13
|
-
// default run
|
|
19
|
+
// default run. Consumer graphs include test evidence independently.
|
|
14
20
|
export function clearAnalysisCache() {
|
|
15
21
|
modelCache.clear();
|
|
16
22
|
spansCache.clear();
|
|
17
23
|
callsCache.clear();
|
|
18
24
|
}
|
|
25
|
+
// Pair parsing with evaluation so each semantic kind has one typed owner.
|
|
26
|
+
function semanticHandler(parse, evaluate) {
|
|
27
|
+
return (value) => {
|
|
28
|
+
const query = parse(value);
|
|
29
|
+
return query === null ? null : (file, source, facts, expression) => evaluate(file, source, facts, expression, query);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const recipeSemanticHandlers = {};
|
|
33
|
+
for (const candidate of Object.values(RECIPE_DEFINITIONS)) {
|
|
34
|
+
const definition = candidate;
|
|
35
|
+
recipeSemanticHandlers[definition.kind] = semanticHandler(definition.parse, (file, source, facts, expression, query) => definition.evaluate(recipeEvaluationRuntime, file, source, facts, expression, query));
|
|
36
|
+
}
|
|
37
|
+
const semanticHandlers = {
|
|
38
|
+
"call-identity": semanticHandler(parseIdentityQuery, callIdentityResult),
|
|
39
|
+
identity: semanticHandler(parseIdentityQuery, identityResult),
|
|
40
|
+
"value-path": semanticHandler(parseValuePathQuery, valueAtPathResult),
|
|
41
|
+
"value-disposition": semanticHandler(parseDispositionQuery, valueDispositionResult),
|
|
42
|
+
"resource-lifetime": semanticHandler(parseResourceQuery, resourceLifetimeResult),
|
|
43
|
+
"option-presence": semanticHandler(parseOptionQuery, optionPresenceResult),
|
|
44
|
+
...recipeSemanticHandlers,
|
|
45
|
+
};
|
|
19
46
|
export function handleAnalysisRequest(req, mode, analyzer = analyzeBindings, status = analysisStatus, spansAnalyzer = analyzeSpans, callsAnalyzer = analyzeCalls) {
|
|
47
|
+
var _a;
|
|
20
48
|
const base = searchBase(mode);
|
|
21
49
|
const root = typeof req.root === "string" ? path.resolve(req.root) : "";
|
|
22
50
|
if (base === "" || !withinBase(root, base)) {
|
|
@@ -24,9 +52,56 @@ export function handleAnalysisRequest(req, mode, analyzer = analyzeBindings, sta
|
|
|
24
52
|
}
|
|
25
53
|
if (req.kind === "available") {
|
|
26
54
|
const s = status();
|
|
27
|
-
|
|
55
|
+
const provider = status === analysisStatus ? semanticProviderProvenance() : undefined;
|
|
56
|
+
return s.available
|
|
57
|
+
? { available: true, ...(provider ? { provider } : {}) }
|
|
58
|
+
: { available: false, reason: s.reason, ...(provider ? { provider } : {}) };
|
|
59
|
+
}
|
|
60
|
+
const semantic = typeof req.kind === "string" && Object.hasOwn(semanticHandlers, req.kind)
|
|
61
|
+
? semanticHandlers[req.kind] : undefined;
|
|
62
|
+
if (semantic && !status().available) {
|
|
63
|
+
return {
|
|
64
|
+
semantic: { version: SEMANTIC_RESULT_VERSION, status: "unknown", reason: "analysis-unavailable" },
|
|
65
|
+
execution: { modelRequests: 0, modelCacheHits: 0 },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (semantic && typeof req.sourceDigest !== "string") {
|
|
69
|
+
return {
|
|
70
|
+
semantic: { version: SEMANTIC_RESULT_VERSION, status: "unknown", reason: "source-changed" },
|
|
71
|
+
execution: { modelRequests: 0, modelCacheHits: 0 },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if ((req.kind === "structures") && typeof req.file === "string" && req.sourceDigest !== undefined) {
|
|
75
|
+
try {
|
|
76
|
+
const abs = path.resolve(root, req.file);
|
|
77
|
+
if (!withinDir(abs, root) || !withinDir(fs.realpathSync(abs), fs.realpathSync(root)))
|
|
78
|
+
return { error: "analysis file outside root" };
|
|
79
|
+
const digest = createHash("sha256").update(fs.readFileSync(abs)).digest("hex");
|
|
80
|
+
if (digest !== req.sourceDigest)
|
|
81
|
+
return { error: `source changed during analysis: ${req.file}` };
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return { error: String(e) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (req.kind === "project" || req.kind === "structures") {
|
|
88
|
+
try {
|
|
89
|
+
if (!status().available)
|
|
90
|
+
return { error: "consumer/structure analysis unavailable" };
|
|
91
|
+
if (req.kind === "project")
|
|
92
|
+
return { project: projectConsumers(root) };
|
|
93
|
+
if (typeof req.file !== "string" || !req.file)
|
|
94
|
+
return { error: "structures needs a file" };
|
|
95
|
+
const abs = path.resolve(root, req.file);
|
|
96
|
+
if (!withinDir(abs, root) || !withinDir(fs.realpathSync(abs), fs.realpathSync(root)))
|
|
97
|
+
return { error: "structure file is outside root" };
|
|
98
|
+
return { structures: functionStructures(req.file, fs.readFileSync(abs, "utf8")) };
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return { error: String(e) };
|
|
102
|
+
}
|
|
28
103
|
}
|
|
29
|
-
if (req.kind === "bindings" || req.kind === "spans" || req.kind === "calls") {
|
|
104
|
+
if (req.kind === "bindings" || req.kind === "spans" || req.kind === "calls" || semantic) {
|
|
30
105
|
if (typeof req.file !== "string" || req.file === "") {
|
|
31
106
|
return { error: `ctx.analysis.${req.kind} needs a "file" path` };
|
|
32
107
|
}
|
|
@@ -34,30 +109,76 @@ export function handleAnalysisRequest(req, mode, analyzer = analyzeBindings, sta
|
|
|
34
109
|
if (!withinDir(abs, root)) {
|
|
35
110
|
return { error: `ctx.analysis failed: file is outside the search root: ${req.file}` };
|
|
36
111
|
}
|
|
112
|
+
if (semantic) {
|
|
113
|
+
const expression = parseExpression(req.expression);
|
|
114
|
+
const evaluate = semantic(req.query);
|
|
115
|
+
if (!expression || !evaluate)
|
|
116
|
+
return {
|
|
117
|
+
semantic: { version: SEMANTIC_RESULT_VERSION, status: "unknown", reason: "unsupported-expression" },
|
|
118
|
+
execution: { modelRequests: 0, modelCacheHits: 0 },
|
|
119
|
+
};
|
|
120
|
+
const model = cachedModel(abs, root, callsCache, callsAnalyzer, req.file, req.sourceDigest);
|
|
121
|
+
if ("error" in model)
|
|
122
|
+
return {
|
|
123
|
+
semantic: { version: SEMANTIC_RESULT_VERSION, status: "unknown", reason: model.sourceChanged ? "source-changed" : "provider-failure" },
|
|
124
|
+
execution: { modelRequests: model.sourceChanged ? 0 : 1, modelCacheHits: 0 },
|
|
125
|
+
};
|
|
126
|
+
const execution = { modelRequests: model.cacheHit ? 0 : 1, modelCacheHits: model.cacheHit ? 1 : 0 };
|
|
127
|
+
const value = (_a = model.file.structure.flow.values.find(item => item.id === expression.id && item.start === expression.start && item.end === expression.end)) !== null && _a !== void 0 ? _a : model.file.structure.flow.values.find(item => item.start === expression.start && item.end === expression.end);
|
|
128
|
+
const subject = value === undefined ? undefined : rangeOf(value);
|
|
129
|
+
try {
|
|
130
|
+
return { semantic: evaluate(req.file, model.source, model.file, expression), execution, ...(subject ? { subject } : {}) };
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return { semantic: { version: SEMANTIC_RESULT_VERSION, status: "unknown", reason: "provider-failure" }, execution, ...(subject ? { subject } : {}) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
37
136
|
if (req.kind === "bindings") {
|
|
38
|
-
|
|
137
|
+
const model = cachedModel(abs, root, modelCache, analyzer, req.file, typeof req.sourceDigest === "string" ? req.sourceDigest : undefined);
|
|
138
|
+
return "error" in model ? model : { file: model.file };
|
|
139
|
+
}
|
|
140
|
+
if (req.kind === "calls") {
|
|
141
|
+
const model = cachedModel(abs, root, callsCache, callsAnalyzer, req.file, typeof req.sourceDigest === "string" ? req.sourceDigest : undefined);
|
|
142
|
+
return "error" in model ? model : { file: model.file };
|
|
39
143
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return cachedModel(abs, root, spansCache, spansAnalyzer, req.file);
|
|
144
|
+
const model = cachedModel(abs, root, spansCache, spansAnalyzer, req.file, typeof req.sourceDigest === "string" ? req.sourceDigest : undefined);
|
|
145
|
+
return "error" in model ? model : { file: model.file };
|
|
43
146
|
}
|
|
44
|
-
return { error: `unknown analysis kind ${JSON.stringify(req.kind)} — known kinds: available, bindings, spans, calls` };
|
|
147
|
+
return { error: `unknown analysis kind ${JSON.stringify(req.kind)} — known kinds: ${["available", "bindings", "spans", "calls", "project", "structures", ...Object.keys(semanticHandlers)].join(", ")}` };
|
|
148
|
+
}
|
|
149
|
+
function parseDispositionQuery(value) {
|
|
150
|
+
if (!value || typeof value !== "object")
|
|
151
|
+
return null;
|
|
152
|
+
const consumers = value.consumers;
|
|
153
|
+
return Array.isArray(consumers) && consumers.every(item => typeof item === "string") ? { consumers } : null;
|
|
154
|
+
}
|
|
155
|
+
function parseValuePathQuery(value) { if (!value || typeof value !== "object")
|
|
156
|
+
return null; const record = value, at = parseExpression(record.at), path = record.path; return at && Array.isArray(path) && path.length > 0 && path.every(item => typeof item === "string") ? { at, path: path } : null; }
|
|
157
|
+
function parseResourceQuery(value) { if (!value || typeof value !== 'object')
|
|
158
|
+
return null; const record = value, owner = parseExpression(record.owner), release = record.release; return owner && Array.isArray(release) && release.every(item => typeof item === 'string') ? { owner, release } : null; }
|
|
159
|
+
function parseExpression(value) {
|
|
160
|
+
if (!value || typeof value !== "object")
|
|
161
|
+
return null;
|
|
162
|
+
const ref = value;
|
|
163
|
+
return [ref.id, ref.start, ref.end].every(Number.isInteger) ? ref : null;
|
|
45
164
|
}
|
|
46
|
-
// The shared per-file model lifecycle:
|
|
165
|
+
// The shared per-file model lifecycle: read (cache hit on content digest),
|
|
47
166
|
// read, compute, cache. Bindings and spans are the same policy over two
|
|
48
167
|
// analyzers and two caches.
|
|
49
|
-
function cachedModel(abs, root, cache, compute, relFile) {
|
|
168
|
+
function cachedModel(abs, root, cache, compute, relFile, expectedDigest) {
|
|
50
169
|
let source;
|
|
51
|
-
let
|
|
52
|
-
let size;
|
|
170
|
+
let digest;
|
|
53
171
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
size = stat.size;
|
|
57
|
-
const cached = cache.get(abs);
|
|
58
|
-
if (cached && cached.mtimeMs === mtimeMs && cached.size === size)
|
|
59
|
-
return { file: cached.file };
|
|
172
|
+
if (!withinDir(fs.realpathSync(abs), fs.realpathSync(root)))
|
|
173
|
+
return { error: "analysis file outside root" };
|
|
60
174
|
source = fs.readFileSync(abs, "utf8");
|
|
175
|
+
digest = createHash("sha256").update(source).digest("hex");
|
|
176
|
+
if (expectedDigest !== undefined && digest !== expectedDigest) {
|
|
177
|
+
return { error: `source changed during analysis: ${relFile}`, sourceChanged: true };
|
|
178
|
+
}
|
|
179
|
+
const cached = cache.get(abs);
|
|
180
|
+
if (cached && cached.digest === digest)
|
|
181
|
+
return { file: cached.file, source, cacheHit: true };
|
|
61
182
|
}
|
|
62
183
|
catch {
|
|
63
184
|
return { error: `ctx.analysis failed: cannot read ${relFile}` };
|
|
@@ -66,6 +187,9 @@ function cachedModel(abs, root, cache, compute, relFile) {
|
|
|
66
187
|
const r = compute(rel, source);
|
|
67
188
|
if (!r.ok)
|
|
68
189
|
return { error: r.error };
|
|
69
|
-
cache.set(abs, {
|
|
70
|
-
return { file: r.file };
|
|
190
|
+
cache.set(abs, { digest, file: r.file });
|
|
191
|
+
return { file: r.file, source, cacheHit: false };
|
|
192
|
+
}
|
|
193
|
+
function rangeOf(value) {
|
|
194
|
+
return { start: value.start, end: value.end, line: value.line, column: value.column, endLine: value.endLine, endColumn: value.endColumn };
|
|
71
195
|
}
|
package/bin/analysis.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export type AnalysisStatusResult = AnalysisStatus | {
|
|
|
7
7
|
available: false;
|
|
8
8
|
reason: string;
|
|
9
9
|
};
|
|
10
|
+
export declare const SEMANTIC_PROVIDER_ID = "any-doctor/syntax-flow";
|
|
11
|
+
export declare const SEMANTIC_PROVIDER_VERSION = "1";
|
|
12
|
+
export declare function semanticProviderProvenance(): import("./contract.js").SemanticProviderProvenance;
|
|
10
13
|
export declare function analysisStatus(): AnalysisStatusResult;
|
|
11
14
|
export type AnalysisResult = {
|
|
12
15
|
ok: true;
|
|
@@ -31,4 +34,14 @@ export type SpansResult = {
|
|
|
31
34
|
};
|
|
32
35
|
export declare function analyzeSpans(file: string, source: string): SpansResult;
|
|
33
36
|
export declare function analyzeBindings(file: string, source: string): AnalysisResult;
|
|
37
|
+
export type Node = {
|
|
38
|
+
type: string;
|
|
39
|
+
range?: [number, number];
|
|
40
|
+
[k: string]: unknown;
|
|
41
|
+
};
|
|
34
42
|
export declare function analyzeCalls(file: string, source: string): CallsResult;
|
|
43
|
+
/** Host-only AST/scope seam. Doctors receive bounded derived facts, never ASTs. */
|
|
44
|
+
export declare function analyzeSyntax(file: string, source: string): {
|
|
45
|
+
program: Node;
|
|
46
|
+
scopes: import("@typescript-eslint/scope-manager").ScopeManager;
|
|
47
|
+
};
|
package/bin/analysis.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
|
+
import { callStructure } from "./call-structure.js";
|
|
1
2
|
import { createRequire } from "module";
|
|
2
3
|
let loaded = null;
|
|
3
4
|
const require_ = createRequire(import.meta.url);
|
|
5
|
+
export const SEMANTIC_PROVIDER_ID = "any-doctor/syntax-flow";
|
|
6
|
+
export const SEMANTIC_PROVIDER_VERSION = "1";
|
|
7
|
+
function dependencyVersion(id) {
|
|
8
|
+
try {
|
|
9
|
+
return require_(`${id}/package.json`).version;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return "unavailable";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function semanticProviderProvenance() {
|
|
16
|
+
const state = analysisStatus();
|
|
17
|
+
return {
|
|
18
|
+
id: SEMANTIC_PROVIDER_ID,
|
|
19
|
+
version: SEMANTIC_PROVIDER_VERSION,
|
|
20
|
+
available: state.available,
|
|
21
|
+
...(!state.available ? { reason: state.reason } : {}),
|
|
22
|
+
dependencies: [
|
|
23
|
+
{ id: "oxc-parser", version: dependencyVersion("oxc-parser") },
|
|
24
|
+
{ id: "@typescript-eslint/scope-manager", version: dependencyVersion("@typescript-eslint/scope-manager") },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
4
28
|
function loadStack() {
|
|
5
29
|
if (loaded !== null)
|
|
6
30
|
return loaded;
|
|
@@ -24,7 +48,12 @@ function parseProgram(stack, file, source) {
|
|
|
24
48
|
if (stack.error !== undefined)
|
|
25
49
|
return { ok: false, error: stack.error };
|
|
26
50
|
try {
|
|
27
|
-
const
|
|
51
|
+
const lang = /\.tsx$/i.test(file) ? "tsx"
|
|
52
|
+
: /\.(?:jsx?|mjs|cjs)$/i.test(file) ? "jsx"
|
|
53
|
+
: /\.d\.ts$/i.test(file) ? "dts"
|
|
54
|
+
: /\.(?:ts|mts|cts)$/i.test(file) ? "ts"
|
|
55
|
+
: "js";
|
|
56
|
+
const parsed = stack.parseSync(file, source, { sourceType: "module", lang });
|
|
28
57
|
if (parsed.errors !== undefined && parsed.errors.length > 0) {
|
|
29
58
|
return { ok: false, error: `analysis failed to parse ${file}: ${parsed.errors[0].message}` };
|
|
30
59
|
}
|
|
@@ -332,7 +361,7 @@ function lowerBound(sorted, value) {
|
|
|
332
361
|
// Call relationships and receiver identities are language facts. The doctor
|
|
333
362
|
// decides which imported factory and methods belong to its framework.
|
|
334
363
|
export function analyzeCalls(file, source) {
|
|
335
|
-
var _a, _b;
|
|
364
|
+
var _a, _b, _c;
|
|
336
365
|
const stack = loadStack();
|
|
337
366
|
if (stack.error !== undefined)
|
|
338
367
|
return { ok: false, error: stack.error };
|
|
@@ -401,14 +430,17 @@ export function analyzeCalls(file, source) {
|
|
|
401
430
|
var _a;
|
|
402
431
|
let n = unwrap(expr);
|
|
403
432
|
const members = [];
|
|
404
|
-
while (n.type === "MemberExpression" && !n.computed && isNode(n.object) && isNode(n.property)) {
|
|
405
|
-
const name = idName(n.property);
|
|
433
|
+
while ((n.type === "MemberExpression" || n.type === "JSXMemberExpression") && (!n.computed || unwrap(n.property).type === "Literal") && isNode(n.object) && isNode(n.property)) {
|
|
434
|
+
const name = n.computed ? propertyName(unwrap(n.property)) : idName(n.property);
|
|
406
435
|
if (!name)
|
|
407
436
|
break;
|
|
408
437
|
members.unshift(name);
|
|
409
438
|
n = unwrap(n.object);
|
|
410
439
|
}
|
|
411
|
-
if (n.type
|
|
440
|
+
if (n.type === "MetaProperty") {
|
|
441
|
+
return { root: `${idName(n.meta)}.${idName(n.property)}`, members, binding: null };
|
|
442
|
+
}
|
|
443
|
+
if (n.type !== "Identifier" && n.type !== "JSXIdentifier")
|
|
412
444
|
return { root: null, members, binding: null };
|
|
413
445
|
const identity = identities.get(n);
|
|
414
446
|
return { root: idName(n), members, binding: (_a = identity === null || identity === void 0 ? void 0 : identity.binding) !== null && _a !== void 0 ? _a : null,
|
|
@@ -478,7 +510,14 @@ export function analyzeCalls(file, source) {
|
|
|
478
510
|
...range(n), functionStart: functionStart(n), left: operand(n.left), right: operand(n.right),
|
|
479
511
|
});
|
|
480
512
|
}
|
|
481
|
-
|
|
513
|
+
const structure = callStructure(nodes, parents, target, range, unwrap, functionStart);
|
|
514
|
+
const receiverValues = new Map((_c = structure.flow) === null || _c === void 0 ? void 0 : _c.values.filter(value => value.kind === "call" && value.receiver !== undefined).map(value => [value.end, value.receiver]));
|
|
515
|
+
for (const call of calls) {
|
|
516
|
+
const receiverValue = receiverValues.get(call.end);
|
|
517
|
+
if (receiverValue !== undefined)
|
|
518
|
+
call.receiverValue = receiverValue;
|
|
519
|
+
}
|
|
520
|
+
return { ok: true, file: { file, calls, functions, differences, structure } };
|
|
482
521
|
}
|
|
483
522
|
catch (e) {
|
|
484
523
|
return { ok: false, error: `analysis failed for ${file}: ${e instanceof Error ? e.message : String(e)}` };
|
|
@@ -486,3 +525,18 @@ export function analyzeCalls(file, source) {
|
|
|
486
525
|
}
|
|
487
526
|
const FUNCTION_EXPRESSIONS = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
488
527
|
const TRANSPARENT_EXPRESSIONS = new Set(["TSAsExpression", "TSTypeAssertion", "TSNonNullExpression", "TSSatisfiesExpression", "ChainExpression", "ParenthesizedExpression"]);
|
|
528
|
+
/** Host-only AST/scope seam. Doctors receive bounded derived facts, never ASTs. */
|
|
529
|
+
export function analyzeSyntax(file, source) {
|
|
530
|
+
const stack = loadStack();
|
|
531
|
+
const parsed = parseProgram(stack, file, source);
|
|
532
|
+
if (!parsed.ok)
|
|
533
|
+
throw new Error(parsed.error);
|
|
534
|
+
if (stack.error !== undefined)
|
|
535
|
+
throw new Error(stack.error);
|
|
536
|
+
try {
|
|
537
|
+
return { program: parsed.program, scopes: stack.analyze(parsed.program, { sourceType: "module" }) };
|
|
538
|
+
}
|
|
539
|
+
catch (e) {
|
|
540
|
+
throw new Error(`analysis failed to resolve scopes in ${file}: ${String(e)}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export declare const AUTHORING_CATALOG_VERSION: 1;
|
|
2
|
+
export declare const AUTHORING_BOUNDARY = "Consumer agents use public facts and recipes; do not patch an installed Any Doctor package or build a private parser/resolver. When public facts are insufficient, narrow the affected check and produce a capability-gap report. Any Doctor maintainers may extend the shared provider only with a framework-neutral change and definite-positive, negative, uncertain, and mixed-neighbor regressions. A capability-gap report is evidence for future product work, not permission to guess or claim a clean result.";
|
|
3
|
+
export declare function authoringCatalog(): {
|
|
4
|
+
schema: 1;
|
|
5
|
+
tool: string;
|
|
6
|
+
command: string;
|
|
7
|
+
rule: string;
|
|
8
|
+
sourceAccess: {
|
|
9
|
+
api: string;
|
|
10
|
+
purpose: string;
|
|
11
|
+
}[];
|
|
12
|
+
capabilities: {
|
|
13
|
+
api: string;
|
|
14
|
+
purpose: string;
|
|
15
|
+
limits?: string[];
|
|
16
|
+
outcomes?: string[];
|
|
17
|
+
name: "bindings" | "calls" | "identity" | "option-presence" | "resource-lifetime" | "value-disposition" | "consumers" | "spans" | "value-path" | "structures";
|
|
18
|
+
}[];
|
|
19
|
+
recipes: {
|
|
20
|
+
requires: string[];
|
|
21
|
+
outcomes: readonly ["report", "clear", "unknown"];
|
|
22
|
+
api: string;
|
|
23
|
+
purpose: string;
|
|
24
|
+
limits: string[];
|
|
25
|
+
inputSchema: Record<string, unknown>;
|
|
26
|
+
example: string;
|
|
27
|
+
name: "forbidden-call" | "required-or-recommended-option" | "resource-without-release" | "unhandled-value";
|
|
28
|
+
}[];
|
|
29
|
+
customChecks: {
|
|
30
|
+
reference: string;
|
|
31
|
+
boundary: string;
|
|
32
|
+
procedure: string[];
|
|
33
|
+
gapReport: {
|
|
34
|
+
classifications: {
|
|
35
|
+
"authoring-error": string;
|
|
36
|
+
"reusable-sdk-gap": string;
|
|
37
|
+
"project-policy": string;
|
|
38
|
+
"runtime-dynamic": string;
|
|
39
|
+
};
|
|
40
|
+
requiredFields: string[];
|
|
41
|
+
validation: {
|
|
42
|
+
api: string;
|
|
43
|
+
certificationApi: string;
|
|
44
|
+
consumedByDoctorVerify: boolean;
|
|
45
|
+
stakes: readonly ["currentFailure", "definitePositive", "negativeControl", "uncertainControl"];
|
|
46
|
+
stakeFields: {
|
|
47
|
+
seed: string;
|
|
48
|
+
fixturePath: string;
|
|
49
|
+
findings: string;
|
|
50
|
+
narrowing: string;
|
|
51
|
+
score: string;
|
|
52
|
+
grade: string;
|
|
53
|
+
};
|
|
54
|
+
constraints: string;
|
|
55
|
+
};
|
|
56
|
+
completion: string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
reporting: {
|
|
60
|
+
api: string;
|
|
61
|
+
purpose: string;
|
|
62
|
+
}[];
|
|
63
|
+
};
|
|
64
|
+
export type AuthoringCatalog = ReturnType<typeof authoringCatalog>;
|
|
65
|
+
/** Validate authoring evidence without executing seeds or claiming their assertions passed. */
|
|
66
|
+
export declare function validateCapabilityGapReport(value: unknown): {
|
|
67
|
+
valid: boolean;
|
|
68
|
+
errors: string[];
|
|
69
|
+
};
|
|
70
|
+
export interface CapabilityGapCertificationOptions {
|
|
71
|
+
/** Doctor program whose accepted claim the report exercises. */
|
|
72
|
+
doctorPath: string;
|
|
73
|
+
/** Base for fixturePath stakes. Defaults to the current working directory. */
|
|
74
|
+
reportDir?: string;
|
|
75
|
+
/** Test seam for a candidate CLI build. Defaults to this package's CLI. */
|
|
76
|
+
cliPath?: string;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
export interface CapabilityGapCaseResult {
|
|
80
|
+
name: string;
|
|
81
|
+
ok: boolean;
|
|
82
|
+
errors: string[];
|
|
83
|
+
}
|
|
84
|
+
export interface CapabilityGapCertificationResult {
|
|
85
|
+
valid: boolean;
|
|
86
|
+
validationErrors: string[];
|
|
87
|
+
passed: number;
|
|
88
|
+
failed: number;
|
|
89
|
+
cases: CapabilityGapCaseResult[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Execute every declared capability-gap stake through the real CLI and compare
|
|
93
|
+
* findings, narrowing, scoring and run integrity. This remains separate from
|
|
94
|
+
* ordinary Doctor verification: a gap report is authoring evidence, not a
|
|
95
|
+
* Doctor fixture or a new runtime capability.
|
|
96
|
+
*/
|
|
97
|
+
export declare function certifyCapabilityGapReport(value: unknown, options: CapabilityGapCertificationOptions): CapabilityGapCertificationResult;
|