any-doctor 0.1.2 → 0.2.1
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 +87 -27
- package/README.md +25 -11
- 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/HANDOFF.md +90 -113
- package/docs/call-structure.md +78 -0
- package/docs/decisions.md +89 -0
- package/docs/doctor-modernization.md +44 -0
- package/docs/doctor-reliability.md +68 -20
- package/docs/doctor-sdk.md +666 -0
- package/docs/features.md +83 -71
- package/docs/openrouter-reliability.md +75 -0
- package/docs/project-consumer-analysis.md +195 -0
- package/docs/research-openrouter-patterns.md +35 -39
- package/docs/vision.md +13 -8
- package/doctors/AGENTS.md +507 -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 +527 -0
- package/doctors/deepgram.mjs +622 -0
- package/doctors/effect-v4-kitlangton.fixtures.mjs +91 -4
- package/doctors/effect-v4-kitlangton.mjs +234 -273
- package/doctors/openrouter.fixtures.mjs +67 -4
- package/doctors/openrouter.mjs +442 -135
- package/doctors/slop.fixtures.mjs +68 -12
- package/doctors/slop.mjs +171 -248
- 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 +47 -6
- 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
|
@@ -5,11 +5,10 @@ When a term here conflicts with language elsewhere, this file wins.
|
|
|
5
5
|
|
|
6
6
|
Current product intent lives in [docs/vision.md](docs/vision.md). For work on
|
|
7
7
|
persistent decisions, history, identity, or team convergence, read the
|
|
8
|
-
[lifecycle design](docs/plans/finding-lifecycle/design.md)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
unless a term is explicitly marked planned.
|
|
8
|
+
[lifecycle design](docs/plans/finding-lifecycle/design.md). Identity (M1) and
|
|
9
|
+
remembered local decisions (M2) have landed; Git-shared decisions (M3) and
|
|
10
|
+
bounded history (M4) remain planned. The glossary below describes current
|
|
11
|
+
behavior unless a term is explicitly marked planned.
|
|
13
12
|
|
|
14
13
|
## Doctor program
|
|
15
14
|
|
|
@@ -88,8 +87,8 @@ tree without rebuilding it. A SiteFinding's readKey
|
|
|
88
87
|
Text the dashboard copies to the clipboard as one unit of agent work: one
|
|
89
88
|
finding (fixPrompt), every finding of one check (checkFixPrompt), or a
|
|
90
89
|
doctor's whole batch (doctorFixPrompt) — pure functions of Doctor-tree
|
|
91
|
-
types plus the verify command, no terminal required. The
|
|
92
|
-
(M2)
|
|
90
|
+
types plus the verify command, no terminal required. The local-decision
|
|
91
|
+
delivery (M2) reworked this family toward investigation-first framing and
|
|
93
92
|
authorized decision paths.
|
|
94
93
|
|
|
95
94
|
## Gate
|
|
@@ -163,6 +162,51 @@ The interface shared by doctor programs, the runner, the report, the
|
|
|
163
162
|
fixture harness, and the generator prompt. The single place where the
|
|
164
163
|
shape of ctx, meta, findings, and the runner protocol is defined.
|
|
165
164
|
|
|
165
|
+
## Doctor SDK
|
|
166
|
+
|
|
167
|
+
The deeper semantic portion of the DoctorCtx interface: host-owned queries for
|
|
168
|
+
identity, value disposition, resource lifetime and option presence. It hides the
|
|
169
|
+
parser/scope/type adapters and returns bounded evidence plus an explicit known or
|
|
170
|
+
unknown result. It is not initially an importable package; Confinement still gives
|
|
171
|
+
doctor programs one route to product capabilities through `ctx`. See the
|
|
172
|
+
[Doctor SDK design](docs/plans/doctor-sdk/design.md).
|
|
173
|
+
|
|
174
|
+
## Semantic result
|
|
175
|
+
|
|
176
|
+
The answer from one Doctor SDK query: a known value with supporting evidence, or
|
|
177
|
+
an unknown result with a named reason. Unknown never means absent, discarded,
|
|
178
|
+
unreleased or safe. A check that cannot support its claim from a known result
|
|
179
|
+
abstains and exposes narrowed coverage.
|
|
180
|
+
|
|
181
|
+
## Value Path
|
|
182
|
+
|
|
183
|
+
A bounded Doctor SDK query that establishes whether one static property path is
|
|
184
|
+
present or absent at one source observation. A present result identifies the
|
|
185
|
+
terminal expression and may include a primitive constant. Unsupported flow is
|
|
186
|
+
unknown; it never implies absence or safety.
|
|
187
|
+
|
|
188
|
+
## Check recipe
|
|
189
|
+
|
|
190
|
+
A host-owned composition of Doctor SDK queries for a recurring check family, such
|
|
191
|
+
as an unhandled value, a resource without release, or a required/recommended call
|
|
192
|
+
option. The recipe owns recurring mechanics, uncertainty, evidence and challenge
|
|
193
|
+
selection; the doctor owns technology-specific selectors, meaning and copy.
|
|
194
|
+
|
|
195
|
+
Each maintained recipe is locally complete under `src/recipes/`: its module owns
|
|
196
|
+
the host kind and implied needs, wire-query parser, semantic composition,
|
|
197
|
+
authoring catalog entry and generated challenge profile. `recipe-definitions.ts`
|
|
198
|
+
is the one registry consumed by the host, SDK, authoring catalog and certifier.
|
|
199
|
+
Adding or changing a recipe happens in its recipe module rather than by editing
|
|
200
|
+
parallel switches in those consumers.
|
|
201
|
+
|
|
202
|
+
## Challenge profile
|
|
203
|
+
|
|
204
|
+
A maintained certification corpus selected by a check recipe or declared semantic
|
|
205
|
+
capability. It pairs genuine positives with lookalikes, shadowing, aliases,
|
|
206
|
+
transfers, unknown cases, positive neighbors, unavailable analysis and exact
|
|
207
|
+
occurrence witnesses. It supplements author fixtures and independent evaluation;
|
|
208
|
+
it does not replace either.
|
|
209
|
+
|
|
166
210
|
## DoctorCtx (ctx)
|
|
167
211
|
|
|
168
212
|
The capability boundary a doctor program is expected to use: read-only,
|
|
@@ -174,16 +218,28 @@ writes, no subprocesses, no network.
|
|
|
174
218
|
(test-named code files — `*.test.*`/`*.spec.*` with a code extension —
|
|
175
219
|
and `test/`, `tests/`, `__tests__/` directories): tests mimic production
|
|
176
220
|
shapes without being production reads. The one law (`isTestPath`) and
|
|
177
|
-
the one derivation (`includeTestsFor`) live in contract.ts;
|
|
178
|
-
|
|
221
|
+
the one derivation (`includeTestsFor`) live in contract.ts; diagnostic
|
|
222
|
+
inventory and search apply them. Reference evidence includes tests. A Doctor run opts back in with
|
|
179
223
|
`--include-tests`; `ctx.files.read()` is never filtered — an explicit
|
|
180
224
|
path is a deliberate choice. `ctx.files.readMasked()` is the one
|
|
181
225
|
masking implementation (comments and strings blanked, offsets and
|
|
182
226
|
length preserved — a masked position addresses the same char in the
|
|
183
227
|
source); doctors carry no private copies — the bundled pack's remaining
|
|
184
|
-
copies migrate on the recorded triggers. Verify
|
|
185
|
-
|
|
186
|
-
|
|
228
|
+
copies migrate on the recorded triggers. Verify defaults to the deliberately seeded
|
|
229
|
+
test scope; fixtures can also exercise ordinary diagnostic defaults.
|
|
230
|
+
See [project consumer facts](docs/project-consumer-analysis.md) for the interfaces.
|
|
231
|
+
|
|
232
|
+
## Diagnostic scope
|
|
233
|
+
|
|
234
|
+
The authorized files eligible for findings in a run. Tests are opt-in and generated
|
|
235
|
+
modules are omitted; this scope is distinct from the evidence used to analyze them.
|
|
236
|
+
|
|
237
|
+
## Consumer evidence
|
|
238
|
+
|
|
239
|
+
An observed reference, declaration dependency, public exposure, or uncertainty
|
|
240
|
+
associated with an exported binding by module identity. Test and generated modules
|
|
241
|
+
may supply this evidence without becoming diagnostic targets; absence within
|
|
242
|
+
supported coverage is a review candidate, never deletion authorization.
|
|
187
243
|
|
|
188
244
|
## Rule query
|
|
189
245
|
|
|
@@ -217,8 +273,9 @@ the analysis they need on their CheckMeta (`needs` — vocabulary:
|
|
|
217
273
|
and the report renders "narrowed" — a degraded run is visible, never
|
|
218
274
|
silent. `ctx.analysis.calls(file)` provides immediate call use, receiver identity,
|
|
219
275
|
inline callback registration, and linked call ranges. These are syntax facts;
|
|
220
|
-
stored, passed, or returned is not a promise-settlement verdict.
|
|
221
|
-
|
|
276
|
+
stored, passed, or returned is not a promise-settlement verdict. A parse or
|
|
277
|
+
adapter error narrows the affected file while analyzable neighboring files
|
|
278
|
+
continue; the partial scan receives no score or grade. References answer by position, so analysis queries compose with
|
|
222
279
|
rule queries: shapes from one engine, identities from the other.
|
|
223
280
|
|
|
224
281
|
## Engine
|
|
@@ -326,9 +383,10 @@ Where a doctor program lives: repo-local (`./doctors/`, committed with
|
|
|
326
383
|
the consuming repo), user-global (`~/.any-doctor/doctors/`, available
|
|
327
384
|
in every repo), or bundled (the first-party pack inside the package,
|
|
328
385
|
read-only — a starting point, not a dependency). Repo-local wins slug
|
|
329
|
-
collisions, then user-global, then bundled. Scanning
|
|
330
|
-
|
|
331
|
-
separate from doctor discovery and does
|
|
386
|
+
collisions, then user-global, then bundled. Scanning itself remains read-only.
|
|
387
|
+
Recording a review decision creates CLI-owned local state under the target's
|
|
388
|
+
`.any-doctor/` directory; that state is separate from doctor discovery and does
|
|
389
|
+
not grant doctors write capabilities.
|
|
332
390
|
|
|
333
391
|
## Skill
|
|
334
392
|
|
|
@@ -348,18 +406,19 @@ positive witness. Unspecified legacy units and unavailable analysis are
|
|
|
348
406
|
reported as not exercised, not counted as passing. Fixture expectations
|
|
349
407
|
establish tested coverage, not general correctness or independence of labels.
|
|
350
408
|
|
|
351
|
-
## Lifecycle vocabulary
|
|
409
|
+
## Lifecycle vocabulary
|
|
352
410
|
|
|
353
|
-
These terms describe
|
|
354
|
-
DoctorCtx. The [design](docs/plans/finding-lifecycle/design.md)
|
|
355
|
-
and applicability rules.
|
|
411
|
+
These terms describe shipped and planned lifecycle behavior. They are CLI-owned,
|
|
412
|
+
not DoctorCtx capabilities. The [design](docs/plans/finding-lifecycle/design.md)
|
|
413
|
+
owns their data and applicability rules.
|
|
356
414
|
|
|
357
|
-
- **Finding identity** *(landed in
|
|
415
|
+
- **Finding identity** *(landed in diff and local decisions, D30/D31)*: continuity of one
|
|
358
416
|
occurrence across comparable scans, distinct from its current source
|
|
359
|
-
coordinates. Host-derived today — check key, file, normalized
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
- **Observation
|
|
417
|
+
coordinates. Host-derived today — check key, file, normalized evidence digest,
|
|
418
|
+
indentation-relative column, and innermost enclosing function span. The CLI
|
|
419
|
+
computes it from source-bound capture; local decisions persist the resulting key.
|
|
420
|
+
- **Observation** *(planned, M4)*: evidence that a finding was detected in a
|
|
421
|
+
particular retained scan.
|
|
363
422
|
- **Decision** *(landed locally, D31)*: a reasoned accepted/not-applicable
|
|
364
423
|
disposition with a required reason, stored in the local decisions file,
|
|
365
424
|
reversible, attached to a Finding identity — it changes review state
|
|
@@ -371,7 +430,8 @@ and applicability rules.
|
|
|
371
430
|
matched by cardinality are flagged ambiguous.
|
|
372
431
|
- **No longer detected** *(landed in the diff path)*: absence established by
|
|
373
432
|
compatible, completed coverage.
|
|
374
|
-
- **Claimed fix
|
|
433
|
+
- **Claimed fix** *(planned, M4)*: a recorded explanation of remediation,
|
|
434
|
+
separate from rescan evidence.
|
|
375
435
|
- **Reassessment** *(landed locally)*: a decision requires review because
|
|
376
436
|
its evidence changed — the finding resurfaces with a warning; the
|
|
377
437
|
decision is never silently carried.
|
package/README.md
CHANGED
|
@@ -13,8 +13,9 @@ rescan after changes. Saved doctors run without model inference.
|
|
|
13
13
|
|
|
14
14
|
Analysis runs locally without an account, API key, or telemetry. Package/tool
|
|
15
15
|
installation can require downloads; the doctor runtime has no network access.
|
|
16
|
-
Remembered decisions and
|
|
17
|
-
|
|
16
|
+
Remembered local decisions persist between runs and resurface when their source
|
|
17
|
+
evidence or doctor meaning changes. Shared project decisions and finding history
|
|
18
|
+
remain [planned](docs/plans/finding-lifecycle/proposal.md).
|
|
18
19
|
|
|
19
20
|
[](https://www.npmjs.com/package/any-doctor)
|
|
20
21
|
[](#)
|
|
@@ -38,6 +39,13 @@ npx any-doctor@latest run slop # one doctor, straight to the report
|
|
|
38
39
|
Non-terminals and CI never see a prompt — output is stable and pipeable
|
|
39
40
|
(`--format json`, or `ANY_DOCTOR_HEADLESS=1`).
|
|
40
41
|
|
|
42
|
+
During review, press `a` to accept an applicable concern or `x` to mark it not
|
|
43
|
+
applicable; both require a reason. The decision is stored locally under
|
|
44
|
+
`.any-doctor/`, hides the matching occurrence from later active lists, and can
|
|
45
|
+
be inspected or reversed with `any-doctor decisions`. Changed evidence resurfaces
|
|
46
|
+
the finding for reassessment. Local decisions never suppress raw JSON findings or
|
|
47
|
+
change CI gates; keep `.any-doctor/decisions.local.json` untracked.
|
|
48
|
+
|
|
41
49
|
## The bundled pack
|
|
42
50
|
|
|
43
51
|
| Doctor | Discipline | Checks |
|
|
@@ -46,12 +54,13 @@ Non-terminals and CI never see a prompt — output is stable and pipeable
|
|
|
46
54
|
| **convex** | Convex discipline: indexed reads, bounded collects, validated args, awaited writes, honest runtime boundaries | 15 |
|
|
47
55
|
| **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
56
|
| **openrouter** | OpenRouter discipline: stream errors surfaced, keep-alives skipped, cancellations that stop billing | 5 |
|
|
57
|
+
| **deepgram** | Deepgram discipline: endpoint/model compatibility, streaming options, Read shapes, hosts, and proven browser key exposure | 5 |
|
|
49
58
|
| **async** | Async and concurrency: dropped promise results, uncleared timers, fetch hygiene | 3 |
|
|
50
59
|
|
|
51
60
|
Checks ship positive and innocent-lookalike fixtures. `verify` compares an
|
|
52
61
|
exact multiset of rule/file/line and optional column, then runs shared innocent
|
|
53
|
-
and sensitivity corpora.
|
|
54
|
-
|
|
62
|
+
and sensitivity corpora. Every bundled check declares its reporting unit, and
|
|
63
|
+
occurrence checks carry per-check location coverage. Passing fixtures show
|
|
55
64
|
agreement on those cases, not a general accuracy guarantee. See
|
|
56
65
|
[the reliability protocol](docs/doctor-reliability.md).
|
|
57
66
|
|
|
@@ -123,15 +132,18 @@ npx any-doctor@latest run --all --fail-on warning --base origin/main
|
|
|
123
132
|
|
|
124
133
|
## Docs
|
|
125
134
|
|
|
135
|
+
- [Doctor SDK semantic queries and recipes](docs/doctor-sdk.md) — confined,
|
|
136
|
+
evidence-bearing analysis and automatic recipe challenge profiles
|
|
137
|
+
|
|
126
138
|
| Doc | What it holds |
|
|
127
139
|
|---|---|
|
|
128
140
|
| [skill/any-doctor.skill.md](skill/any-doctor.skill.md) | The authoring contract — what your agent reads to write doctors |
|
|
129
141
|
| [CONTEXT.md](CONTEXT.md) | Domain glossary — canonical terms |
|
|
130
142
|
| [docs/vision.md](docs/vision.md) | Current goals and product direction |
|
|
131
143
|
| [docs/features.md](docs/features.md) | Available features versus planned work |
|
|
132
|
-
| [docs/plans/analysis-improvements.md](docs/plans/analysis-improvements.md) |
|
|
133
|
-
| [docs/plans/finding-lifecycle/proposal.md](docs/plans/finding-lifecycle/proposal.md) |
|
|
134
|
-
| [docs/plans/finding-lifecycle/design.md](docs/plans/finding-lifecycle/design.md) | State ownership,
|
|
144
|
+
| [docs/plans/analysis-improvements.md](docs/plans/analysis-improvements.md) | Delivered identity work and later analysis opportunities |
|
|
145
|
+
| [docs/plans/finding-lifecycle/proposal.md](docs/plans/finding-lifecycle/proposal.md) | Current local decisions plus planned history and team workflows |
|
|
146
|
+
| [docs/plans/finding-lifecycle/design.md](docs/plans/finding-lifecycle/design.md) | State ownership, Git convergence, future history storage, and open choices |
|
|
135
147
|
| [docs/plans/finding-lifecycle/milestones.md](docs/plans/finding-lifecycle/milestones.md) | Implementation slices and acceptance evidence |
|
|
136
148
|
| [docs/HANDOFF.md](docs/HANDOFF.md) | Current handoff and next bounded task |
|
|
137
149
|
| [docs/decisions.md](docs/decisions.md) | Historical choices and explicit supersessions |
|
|
@@ -141,7 +153,9 @@ npx any-doctor@latest run --all --fail-on warning --base origin/main
|
|
|
141
153
|
|
|
142
154
|
## Status
|
|
143
155
|
|
|
144
|
-
Pre-1.0.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
Pre-1.0. Finding identity and remembered local decisions are available. The next
|
|
157
|
+
product phase is real-world adoption and reliability feedback; the next planned
|
|
158
|
+
lifecycle feature is Git-shared project decisions, followed separately by bounded
|
|
159
|
+
local history. Scanning stays available through npx without mandatory
|
|
160
|
+
initialization. See the [feature map](docs/features.md) for current availability.
|
|
161
|
+
MIT.
|
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
|
+
}
|