any-doctor 0.1.1 → 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.
Files changed (100) hide show
  1. package/CONTEXT.md +65 -7
  2. package/README.md +10 -6
  3. package/bin/analysis-host.d.ts +20 -1
  4. package/bin/analysis-host.js +148 -24
  5. package/bin/analysis.d.ts +13 -0
  6. package/bin/analysis.js +60 -6
  7. package/bin/authoring.d.ts +97 -0
  8. package/bin/authoring.js +269 -0
  9. package/bin/call-structure.d.ts +4 -0
  10. package/bin/call-structure.js +575 -0
  11. package/bin/certify.d.ts +5 -1
  12. package/bin/certify.js +70 -18
  13. package/bin/cli.js +337 -39
  14. package/bin/cohort.js +1 -1
  15. package/bin/contract.d.ts +268 -4
  16. package/bin/contract.js +12 -8
  17. package/bin/dashboard.d.ts +11 -1
  18. package/bin/dashboard.js +64 -23
  19. package/bin/doctor-sdk.d.ts +41 -0
  20. package/bin/doctor-sdk.js +823 -0
  21. package/bin/doctor-tree.js +2 -1
  22. package/bin/engine.js +1 -1
  23. package/bin/file-scope.d.ts +17 -0
  24. package/bin/file-scope.js +72 -0
  25. package/bin/function-structure.d.ts +11 -0
  26. package/bin/function-structure.js +89 -0
  27. package/bin/mask.js +1 -1
  28. package/bin/palette.d.ts +1 -0
  29. package/bin/palette.js +1 -1
  30. package/bin/project-consumers.d.ts +27 -0
  31. package/bin/project-consumers.js +1054 -0
  32. package/bin/recipe-definitions.d.ts +27 -0
  33. package/bin/recipe-definitions.js +47 -0
  34. package/bin/recipes/forbidden-call.d.ts +15 -0
  35. package/bin/recipes/forbidden-call.js +147 -0
  36. package/bin/recipes/required-option.d.ts +8 -0
  37. package/bin/recipes/required-option.js +116 -0
  38. package/bin/recipes/resource-without-release.d.ts +12 -0
  39. package/bin/recipes/resource-without-release.js +182 -0
  40. package/bin/recipes/support.d.ts +12 -0
  41. package/bin/recipes/support.js +54 -0
  42. package/bin/recipes/types.d.ts +61 -0
  43. package/bin/recipes/types.js +1 -0
  44. package/bin/recipes/unhandled-value.d.ts +12 -0
  45. package/bin/recipes/unhandled-value.js +161 -0
  46. package/bin/report.js +18 -11
  47. package/bin/runner.d.ts +6 -1
  48. package/bin/runner.js +14 -2
  49. package/bin/score.d.ts +3 -0
  50. package/bin/score.js +6 -2
  51. package/bin/sdk.d.ts +4 -1
  52. package/bin/sdk.js +246 -40
  53. package/bin/search-host.js +5 -2
  54. package/bin/semantic-query-parsers.d.ts +3 -0
  55. package/bin/semantic-query-parsers.js +18 -0
  56. package/bin/summary.d.ts +1 -0
  57. package/bin/summary.js +38 -2
  58. package/bin/tty.js +49 -9
  59. package/bin/value-flow.d.ts +111 -0
  60. package/bin/value-flow.js +261 -0
  61. package/docs/call-structure.md +78 -0
  62. package/docs/decisions.md +82 -0
  63. package/docs/doctor-reliability.md +61 -15
  64. package/docs/doctor-sdk.md +666 -0
  65. package/docs/openrouter-reliability.md +75 -0
  66. package/docs/project-consumer-analysis.md +190 -0
  67. package/docs/research-openrouter-patterns.md +35 -39
  68. package/doctors/AGENTS.md +506 -0
  69. package/doctors/{async-doctor.fixtures.mjs → async.fixtures.mjs} +106 -61
  70. package/doctors/async.mjs +58 -0
  71. package/doctors/{convex-doctor.fixtures.mjs → convex.fixtures.mjs} +81 -19
  72. package/doctors/convex.mjs +833 -0
  73. package/doctors/deepgram.fixtures.mjs +529 -0
  74. package/doctors/deepgram.mjs +637 -0
  75. package/doctors/{effect-v4-doctor.mjs → effect-v4-kitlangton.mjs} +2 -2
  76. package/doctors/{openrouter-doctor.fixtures.mjs → openrouter.fixtures.mjs} +67 -4
  77. package/doctors/openrouter.mjs +488 -0
  78. package/doctors/{slop-doctor.fixtures.mjs → slop.fixtures.mjs} +25 -12
  79. package/doctors/{slop-doctor.mjs → slop.mjs} +46 -164
  80. package/fixtures/doctor-sdk-recipe-only.fixtures.mjs +2 -0
  81. package/fixtures/doctor-sdk-recipe-only.mjs +12 -0
  82. package/fixtures/doctor-sdk-reference.fixtures.mjs +14 -0
  83. package/fixtures/doctor-sdk-reference.mjs +23 -0
  84. package/package.json +6 -5
  85. package/skill/any-doctor.skill.md +44 -4
  86. package/skill/author-workflow.md +90 -0
  87. package/docs/evidence/repair-after-0.0.7/convex-verify.txt +0 -103
  88. package/docs/evidence/repair-after-0.0.7/sift-convex-packed.json +0 -583
  89. package/docs/evidence/repair-after-0.0.7/sift-convex-published.json +0 -1023
  90. package/docs/evidence/repair-after-0.0.7/sift-slop-packed.json +0 -956
  91. package/docs/evidence/repair-after-0.0.7/sift-slop-published.json +0 -951
  92. package/docs/evidence/repair-after-0.0.7/tests.txt +0 -307
  93. package/docs/plans/analysis-improvements.md +0 -234
  94. package/docs/plans/finding-lifecycle/design.md +0 -170
  95. package/docs/plans/finding-lifecycle/milestones.md +0 -120
  96. package/docs/plans/finding-lifecycle/proposal.md +0 -104
  97. package/doctors/async-doctor.mjs +0 -478
  98. package/doctors/convex-doctor.mjs +0 -557
  99. package/doctors/openrouter-doctor.mjs +0 -181
  100. /package/doctors/{effect-v4-doctor.fixtures.mjs → effect-v4-kitlangton.fixtures.mjs} +0 -0
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; every read
178
- capability applies them. A Doctor run opts back in with
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 always sees everything
185
- its fixtures seed: the sandbox is the doctor's own world, and a seed
186
- named `*.test.ts` is deliberate test data.
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. Parse/adapter
221
- errors fail the doctor run explicitly. References answer by position, so analysis queries compose with
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
@@ -32,7 +32,7 @@ each with impact, why, fix, and the honest blind spots. `enter` copies a
32
32
  fix prompt for your agent.
33
33
 
34
34
  ```bash
35
- npx any-doctor@latest run slop-doctor # one doctor, straight to the report
35
+ npx any-doctor@latest run slop # one doctor, straight to the report
36
36
  ```
37
37
 
38
38
  Non-terminals and CI never see a prompt — output is stable and pipeable
@@ -42,11 +42,12 @@ Non-terminals and CI never see a prompt — output is stable and pipeable
42
42
 
43
43
  | Doctor | Discipline | Checks |
44
44
  |---|---|---:|
45
- | **slop-doctor** | The recurring failures of LLM-written code: identical helpers copied across modules, dead exports, unread bindings, hostname-sniffed environments, careless substring matching, collapsed boolean states | 8 |
46
- | **convex-doctor** | Convex discipline: indexed reads, bounded collects, validated args, awaited writes, honest runtime boundaries | 15 |
47
- | **effect-v4-doctor** | Effect v4 discipline — the mechanical rules of the [kitlangton Effect skill](https://www.ui-skills.com/skills/kitlangton/effect), enforced | 10 |
48
- | **openrouter-doctor** | OpenRouter discipline: stream errors surfaced, keep-alives skipped, cancellations that stop billing | 5 |
49
- | **async-doctor** | Async and concurrency: dropped promise results, uncleared timers, fetch hygiene | 3 |
45
+ | **slop** | The recurring failures of LLM-written code: identical helpers copied across modules, dead exports, unread bindings, hostname-sniffed environments, careless substring matching, collapsed boolean states | 8 |
46
+ | **convex** | Convex discipline: indexed reads, bounded collects, validated args, awaited writes, honest runtime boundaries | 15 |
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
+ | **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 |
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
52
53
  exact multiset of rule/file/line and optional column, then runs shared innocent
@@ -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 |
@@ -1,4 +1,6 @@
1
- import { AnalysisFile, AnalysisSpans, AnalysisCalls, Mode } from "./contract.js";
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
  };
@@ -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 mtime+size for the process
12
- // lifetime; tests bust it between cases. Invisible to slop-doctor's
13
- // default run (test-file consumers are the documented narrowing).
17
+ // Test seam: the model cache is keyed by content digest for the process
18
+ // lifetime; tests bust it between cases. Invisible to slop's
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
- return s.available ? { available: true } : { available: false, reason: s.reason };
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
- return cachedModel(abs, root, modelCache, analyzer, req.file);
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
- if (req.kind === "calls")
41
- return cachedModel(abs, root, callsCache, callsAnalyzer, req.file);
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: stat (cache hit on mtime+size),
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 mtimeMs;
52
- let size;
170
+ let digest;
53
171
  try {
54
- const stat = fs.statSync(abs);
55
- mtimeMs = stat.mtimeMs;
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, { mtimeMs, size, file: r.file });
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 parsed = stack.parseSync(file, source, { sourceType: "module" });
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 !== "Identifier")
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
- return { ok: true, file: { file, calls, functions, differences } };
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;