ambit-ts 0.1.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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +403 -0
- package/dist/checker/authority.d.ts +13 -0
- package/dist/checker/authority.js +87 -0
- package/dist/checker/backend/legacy-ts.d.ts +26 -0
- package/dist/checker/backend/legacy-ts.js +1936 -0
- package/dist/checker/config.d.ts +84 -0
- package/dist/checker/config.js +391 -0
- package/dist/checker/coverage.d.ts +78 -0
- package/dist/checker/coverage.js +84 -0
- package/dist/checker/diagnose.d.ts +89 -0
- package/dist/checker/diagnose.js +734 -0
- package/dist/checker/index.d.ts +8 -0
- package/dist/checker/index.js +8 -0
- package/dist/checker/init.d.ts +38 -0
- package/dist/checker/init.js +205 -0
- package/dist/checker/propagate.d.ts +69 -0
- package/dist/checker/propagate.js +259 -0
- package/dist/checker/summarize.d.ts +27 -0
- package/dist/checker/summarize.js +411 -0
- package/dist/cli/analyze.d.ts +33 -0
- package/dist/cli/analyze.js +98 -0
- package/dist/cli/approvals.d.ts +28 -0
- package/dist/cli/approvals.js +55 -0
- package/dist/cli/diff.d.ts +66 -0
- package/dist/cli/diff.js +235 -0
- package/dist/cli/github.d.ts +33 -0
- package/dist/cli/github.js +41 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +385 -0
- package/dist/cli/worktree.d.ts +75 -0
- package/dist/cli/worktree.js +154 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +10 -0
- package/dist/core/approvals.d.ts +82 -0
- package/dist/core/approvals.js +0 -0
- package/dist/core/authority-diff.d.ts +98 -0
- package/dist/core/authority-diff.js +209 -0
- package/dist/core/authority.d.ts +109 -0
- package/dist/core/authority.js +50 -0
- package/dist/core/backend.d.ts +355 -0
- package/dist/core/backend.js +1 -0
- package/dist/core/budget.d.ts +61 -0
- package/dist/core/budget.js +95 -0
- package/dist/core/capability.d.ts +53 -0
- package/dist/core/capability.js +117 -0
- package/dist/core/config.d.ts +59 -0
- package/dist/core/config.js +10 -0
- package/dist/core/diagnostic.d.ts +126 -0
- package/dist/core/diagnostic.js +13 -0
- package/dist/core/effects.d.ts +39 -0
- package/dist/core/effects.js +72 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/location.d.ts +15 -0
- package/dist/core/location.js +1 -0
- package/dist/core/sql.d.ts +22 -0
- package/dist/core/sql.js +38 -0
- package/dist/core/summary.d.ts +240 -0
- package/dist/core/summary.js +8 -0
- package/dist/core/symbol-id.d.ts +25 -0
- package/dist/core/symbol-id.js +23 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/runtime/child-process.d.ts +29 -0
- package/dist/runtime/child-process.js +124 -0
- package/dist/runtime/context.d.ts +37 -0
- package/dist/runtime/context.js +8 -0
- package/dist/runtime/enforce.d.ts +52 -0
- package/dist/runtime/enforce.js +95 -0
- package/dist/runtime/fs.d.ts +46 -0
- package/dist/runtime/fs.js +188 -0
- package/dist/runtime/hono.d.ts +55 -0
- package/dist/runtime/hono.js +68 -0
- package/dist/runtime/index.d.ts +71 -0
- package/dist/runtime/index.js +126 -0
- package/dist/runtime/next.d.ts +95 -0
- package/dist/runtime/next.js +60 -0
- package/dist/runtime/pg.d.ts +48 -0
- package/dist/runtime/pg.js +122 -0
- package/dist/stubs/constructors.d.ts +34 -0
- package/dist/stubs/constructors.js +111 -0
- package/dist/stubs/data-clients.d.ts +9 -0
- package/dist/stubs/data-clients.js +109 -0
- package/dist/stubs/http-capabilities.d.ts +15 -0
- package/dist/stubs/http-capabilities.js +70 -0
- package/dist/stubs/mutating-builtins.d.ts +1 -0
- package/dist/stubs/mutating-builtins.js +48 -0
- package/dist/stubs/node-builtins.d.ts +2 -0
- package/dist/stubs/node-builtins.js +77 -0
- package/dist/stubs/pure-builtins.d.ts +1 -0
- package/dist/stubs/pure-builtins.js +89 -0
- package/docs/diagnostics/README.md +519 -0
- package/docs/limitations.md +712 -0
- package/package.json +89 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type { BudgetInput } from "./budget.ts";
|
|
2
|
+
import type { SourceLocation } from "./location.ts";
|
|
3
|
+
import type { SymbolId } from "./symbol-id.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Why a call's target — or its effects — could not be resolved
|
|
6
|
+
* (DESIGN.md §4.2 rule 6, and rule 4 for `callback-parameter`).
|
|
7
|
+
*
|
|
8
|
+
* `import-binding`, `builtin-method`, and `external-module` sub-classify
|
|
9
|
+
* what was previously reported as a bare `unresolved-symbol`, so `ambit
|
|
10
|
+
* check --coverage` can show *why* a call is unresolved instead of a single
|
|
11
|
+
* undifferentiated count:
|
|
12
|
+
* - `import-binding`: the callee is a call to an imported identifier whose
|
|
13
|
+
* alias could not be followed to any declaration at all — the module
|
|
14
|
+
* specifier doesn't resolve, or the named export doesn't exist
|
|
15
|
+
* (`checker.getAliasedSymbol()` returns TypeScript's `unknownSymbol`).
|
|
16
|
+
* Distinct from `unresolved-symbol` in that the shape is known (an import
|
|
17
|
+
* binding) even though the target is not; unlike `unresolved-symbol`, a
|
|
18
|
+
* stub match may still apply if the qualified name happens to be
|
|
19
|
+
* recognized (see `classifyCall`).
|
|
20
|
+
* - `builtin-method`: the callee resolved to an ambient declaration from
|
|
21
|
+
* TypeScript's default lib (e.g. `Array.prototype.map`, `Set.prototype.has`)
|
|
22
|
+
* whose call site (a method on a local value) `qualifiedNameOf` cannot
|
|
23
|
+
* turn into a name the stub table can key on.
|
|
24
|
+
* - `external-module`: same shape, but the ambient declaration lives in a
|
|
25
|
+
* third-party package's `.d.ts` under `node_modules` (e.g. an ORM client
|
|
26
|
+
* installed as a dependency) rather than the default lib.
|
|
27
|
+
* - `ambient-declaration`: the callee resolved to a declaration in a `.d.ts`
|
|
28
|
+
* that is part of the project itself — a hand-written `declare module "pg"`
|
|
29
|
+
* or a `declare function`, neither shipped with the compiler nor installed
|
|
30
|
+
* as a package. Distinct from `external-module` because the two are fixed
|
|
31
|
+
* differently: the first is the project's own file to annotate, the second
|
|
32
|
+
* needs a bundled or package-provided stub.
|
|
33
|
+
* `unresolved-symbol` remains the residual case: either no declaration could
|
|
34
|
+
* be found at all, or one was found but the call still cannot be followed to
|
|
35
|
+
* an extracted function — a nested function declaration, or a receiver with no
|
|
36
|
+
* single object literal certainly behind it (a parameter, a `let` binding, a
|
|
37
|
+
* literal carrying a spread). In the latter shapes the call target may be
|
|
38
|
+
* fully known to the compiler and may even declare its own `@effects`; the
|
|
39
|
+
* call site simply has no one `SymbolId` it can honestly propagate from.
|
|
40
|
+
*/
|
|
41
|
+
export type UnresolvedReason = "dynamic-import" | "eval" | "new-function" | "any-typed" | "callback-parameter" | "overload-without-body" | "import-binding" | "builtin-method" | "external-module" | "ambient-declaration" | "unresolved-symbol";
|
|
42
|
+
/**
|
|
43
|
+
* What kind of function-like node was seen but not extracted (DESIGN.md
|
|
44
|
+
* §4.3's coverage concern, and the connector layer's documented slice
|
|
45
|
+
* boundary — see `collectFunctionLikeDeclarations` in
|
|
46
|
+
* `src/checker/backend/legacy-ts.ts`). Purely descriptive; carries no
|
|
47
|
+
* compiler-specific node, so it can cross the `TsBackend` boundary freely.
|
|
48
|
+
*
|
|
49
|
+
* `class-declaration` is not a function-like node at all; it appears only as
|
|
50
|
+
* the reason a contract written on a `class` cannot be carried — the contract
|
|
51
|
+
* belongs on the class's constructor. It is reported through
|
|
52
|
+
* `uncarriedContracts` and deliberately not counted in `skippedFunctions`,
|
|
53
|
+
* which counts function-like nodes.
|
|
54
|
+
*
|
|
55
|
+
* `object-literal-method` is narrower than its name: a member of a module-scope
|
|
56
|
+
* `const` literal with an identifier name *is* extracted. What remains are the
|
|
57
|
+
* members that have no stable declaration path (a computed, string, or numeric
|
|
58
|
+
* key) or no path at all (a nested literal, one declared inside a function
|
|
59
|
+
* body, one passed inline as an argument, or one bound by `let`).
|
|
60
|
+
*
|
|
61
|
+
* `bodyless-declaration` is a function-like node that declares a signature and
|
|
62
|
+
* no code: an overload signature, an `abstract` member, or an ambient
|
|
63
|
+
* `declare` written in a `.ts` file. It is skipped because it is not a
|
|
64
|
+
* function — the implementation is (DESIGN.md §4.1, "Overloads and bodyless
|
|
65
|
+
* declarations"). Indexing one would give two declarations the same declaration
|
|
66
|
+
* path, and the first of them has no body to infer effects from, so every
|
|
67
|
+
* caller would read as `pure` whatever the implementation does.
|
|
68
|
+
*/
|
|
69
|
+
export type SkippedFunctionKind = "class-declaration" | "getter-setter" | "object-literal-method" | "anonymous-default-export" | "callback-argument" | "nested-function" | "bodyless-declaration" | "other";
|
|
70
|
+
/**
|
|
71
|
+
* What the connector layer could read statically from one call argument.
|
|
72
|
+
*
|
|
73
|
+
* Carries no compiler object, so it crosses the `TsBackend` boundary freely
|
|
74
|
+
* (DESIGN.md §3.4). Two consumers need it, and both need the same distinction
|
|
75
|
+
* between "this is the whole value" and "this is only how the value starts":
|
|
76
|
+
* `src/stubs/data-clients.ts` reads a SQL statement's leading keyword, and
|
|
77
|
+
* `src/stubs/http-capabilities.ts` reads a URL's host. A template literal's
|
|
78
|
+
* static head is enough for both, and is enough for neither to claim it saw
|
|
79
|
+
* the whole string — hence {@link complete}.
|
|
80
|
+
*/
|
|
81
|
+
export interface LiteralArgument {
|
|
82
|
+
/**
|
|
83
|
+
* A string literal's value, or a template literal's static leading text.
|
|
84
|
+
* Absent when the argument is not a string at all (an object literal, say —
|
|
85
|
+
* see {@link properties}).
|
|
86
|
+
*/
|
|
87
|
+
readonly text?: string;
|
|
88
|
+
/**
|
|
89
|
+
* False when {@link text} is only a prefix (a template literal with a
|
|
90
|
+
* substitution). Absent when there is no `text`.
|
|
91
|
+
*/
|
|
92
|
+
readonly complete?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* An object literal's string-literal properties, for an options argument
|
|
95
|
+
* like `fetch(url, { method: "POST" })`. Only identifier-named properties
|
|
96
|
+
* whose value is a string literal appear; anything else is left out rather
|
|
97
|
+
* than guessed at.
|
|
98
|
+
*/
|
|
99
|
+
readonly properties?: ReadonlyMap<string, string>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One call expression found while walking a function's body.
|
|
103
|
+
*
|
|
104
|
+
* `resolvedCallee` is set only for a project-local call. `calleeQualifiedName`
|
|
105
|
+
* is a best-effort textual name (e.g. `"fetch"`, `"fs.readFileSync"`) used by
|
|
106
|
+
* `src/stubs/node-builtins.ts` to recognize known standard-library/library
|
|
107
|
+
* calls; it carries no meaning on its own and is never treated as a resolved
|
|
108
|
+
* call target. `pureBuiltinName` is a separate, checker-derived name
|
|
109
|
+
* (`checker.getFullyQualifiedName()` form, e.g. `"Set.has"`) set only when
|
|
110
|
+
* `calleeQualifiedName` could not be produced (a builtin method reached
|
|
111
|
+
* through a local value, e.g. `set.has(...)`) — it is checked against
|
|
112
|
+
* `src/stubs/pure-builtins.ts`'s allowlist, a different namespace from
|
|
113
|
+
* `calleeQualifiedName`'s module-specifier keys; the two must never be
|
|
114
|
+
* merged or compared. `unresolvedReason` may accompany either name as the
|
|
115
|
+
* reason to fall back to if no match is found (`src/checker/summarize.ts`'s
|
|
116
|
+
* `toCall`) — the connector layer already knows, from the callee's own
|
|
117
|
+
* declaration, whether a miss would mean "unresolved-symbol" or something
|
|
118
|
+
* more specific (e.g. `builtin-method`). `callbackByReference` is set when
|
|
119
|
+
* one of the call's arguments is a callable passed by reference rather than
|
|
120
|
+
* written inline (`arr.forEach(handler)`, not `arr.forEach(x => ...)`): an
|
|
121
|
+
* inline callback's body is walked by `collectCalls` and its effects
|
|
122
|
+
* attributed to the enclosing function, but a callback passed by reference
|
|
123
|
+
* is never visited, so `pureBuiltinName` must not be trusted as pure when
|
|
124
|
+
* this is set (DESIGN.md §4.2 rule 4) — regardless of what
|
|
125
|
+
* `src/stubs/pure-builtins.ts` says about the method name itself.
|
|
126
|
+
*/
|
|
127
|
+
export interface CallSite {
|
|
128
|
+
readonly location: SourceLocation;
|
|
129
|
+
readonly resolvedCallee?: SymbolId;
|
|
130
|
+
readonly calleeQualifiedName?: string;
|
|
131
|
+
readonly pureBuiltinName?: string;
|
|
132
|
+
readonly callbackByReference?: true;
|
|
133
|
+
/**
|
|
134
|
+
* Set when this site mutates a value in place (DESIGN.md §4.2, "Local
|
|
135
|
+
* mutation and `pure`") — a mutating builtin method, or an assignment / `++` /
|
|
136
|
+
* `delete` on a property. An assignment is not a call, but it propagates
|
|
137
|
+
* exactly like one, so it rides in the same array rather than in a parallel
|
|
138
|
+
* channel every consumer would have to remember to read. `escaping` is
|
|
139
|
+
* false only when the mutated value was allocated inside the function.
|
|
140
|
+
*/
|
|
141
|
+
readonly mutation?: {
|
|
142
|
+
readonly escaping: boolean;
|
|
143
|
+
readonly qualifiedName?: string;
|
|
144
|
+
readonly unknownCallback?: true;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* What each argument was, statically, indexed by position — `undefined`
|
|
148
|
+
* where nothing could be read. Present only when `calleeQualifiedName` is:
|
|
149
|
+
* the arguments matter to a stub table keyed on that name, and to nothing
|
|
150
|
+
* else.
|
|
151
|
+
*/
|
|
152
|
+
readonly literalArguments?: readonly (LiteralArgument | undefined)[];
|
|
153
|
+
/**
|
|
154
|
+
* Set on a construction (`new X(...)`, `super(...)`, a derived class's
|
|
155
|
+
* implicit base call) that passes no arguments. `src/stubs/constructors.ts`
|
|
156
|
+
* needs it to tell `new Date()` (reads the clock — `env`) from
|
|
157
|
+
* `new Date(2020, 0, 1)` (a pure conversion of its arguments). Absent on a
|
|
158
|
+
* plain call.
|
|
159
|
+
*/
|
|
160
|
+
readonly constructedWithoutArguments?: true;
|
|
161
|
+
readonly unresolvedReason?: UnresolvedReason;
|
|
162
|
+
}
|
|
163
|
+
/** Raw JSDoc tag text for one function declaration, before contract parsing. */
|
|
164
|
+
export interface RawJsDoc {
|
|
165
|
+
readonly tags: ReadonlyMap<string, string>;
|
|
166
|
+
/**
|
|
167
|
+
* Where each tag was written, so a fix can replace the tag itself rather
|
|
168
|
+
* than guess at a line (DESIGN.md §5.3: `fixes[].edits` must be a concrete,
|
|
169
|
+
* applicable patch). Same 1-based, end-exclusive convention as every other
|
|
170
|
+
* `SourceLocation`; `diagnose.ts` converts to the 0-based edit range §5.3
|
|
171
|
+
* specifies. Character offsets are UTF-16 units, which is what the
|
|
172
|
+
* compiler already reports.
|
|
173
|
+
*/
|
|
174
|
+
readonly tagLocations: ReadonlyMap<string, SourceLocation>;
|
|
175
|
+
}
|
|
176
|
+
/** Everything the connector layer can extract about one function/method declaration. */
|
|
177
|
+
export interface ExtractedFunction {
|
|
178
|
+
readonly id: SymbolId;
|
|
179
|
+
readonly location: SourceLocation;
|
|
180
|
+
/**
|
|
181
|
+
* Where the declaration itself begins (`export async function …`, not the
|
|
182
|
+
* name), so `ambit init` can insert a JSDoc block above it at the right
|
|
183
|
+
* indentation. `location` points at the name, which is the right place for
|
|
184
|
+
* a diagnostic and the wrong place for an edit.
|
|
185
|
+
*/
|
|
186
|
+
readonly declarationStart: SourceLocation;
|
|
187
|
+
/**
|
|
188
|
+
* The single JSDoc block attached to this declaration, when there is
|
|
189
|
+
* exactly one — so a fix can add a tag to the comment that is already there
|
|
190
|
+
* instead of stacking a second block above it. Independent of whether that
|
|
191
|
+
* block contains any tags: a purely descriptive comment is the common case
|
|
192
|
+
* `ambit init` has to add to.
|
|
193
|
+
*/
|
|
194
|
+
readonly jsDocRange?: SourceLocation;
|
|
195
|
+
/**
|
|
196
|
+
* Set on a class's construction entry when the class writes no constructor.
|
|
197
|
+
* The entry is real — property initializers and the base constructor still
|
|
198
|
+
* run, and their effects still propagate — but it has no declaration site,
|
|
199
|
+
* so no contract can be attached to it. A proposal to declare one would be
|
|
200
|
+
* a patch that changes nothing, which is worse than no proposal.
|
|
201
|
+
*/
|
|
202
|
+
readonly implicitConstructor?: true;
|
|
203
|
+
/**
|
|
204
|
+
* Set on a declaration only `ambit.config.ts` can name (DESIGN.md §4.1
|
|
205
|
+
* (a)): a `get`/`set` accessor, or an anonymous `export default`.
|
|
206
|
+
*
|
|
207
|
+
* The declaration is extracted and propagates like any other — its body's
|
|
208
|
+
* effects are real — but {@link ExtractedFunction.jsDoc} is left undefined
|
|
209
|
+
* for it on purpose: §4.1 (a) keeps the config namespace a superset of the
|
|
210
|
+
* JSDoc one, so a contract comment here is inert and is reported as
|
|
211
|
+
* `AMB-E003` instead (with the config key that would work).
|
|
212
|
+
*/
|
|
213
|
+
readonly configOnly?: true;
|
|
214
|
+
readonly jsDoc: RawJsDoc | undefined;
|
|
215
|
+
readonly calls: readonly CallSite[];
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* A call that establishes an entrypoint's context, as the source shows it: a
|
|
219
|
+
* hand-written `withAmbit(spec, handler)` from `ambit-ts/runtime`, or a framework
|
|
220
|
+
* adapter's registration (`ambitHandler(spec, handler, decode)` from
|
|
221
|
+
* `ambit-ts/runtime/hono`) — DESIGN.md §4.4, "Mapping contracts to handlers".
|
|
222
|
+
*
|
|
223
|
+
* Ambit reads it to check one thing only: that the capability list the runtime
|
|
224
|
+
* would establish is the one the handler's JSDoc declares. Written twice, the
|
|
225
|
+
* two drift, and nothing noticed before this existed.
|
|
226
|
+
*
|
|
227
|
+
* The check is **on the source alone**. §12's "Mapping contracts to handlers" —
|
|
228
|
+
* a build that strips comments, a bundle that moves the handler — is not
|
|
229
|
+
* solved here, and `unmatchedReason` exists so a wrapper this comparison
|
|
230
|
+
* cannot reach is reported rather than passed over.
|
|
231
|
+
*/
|
|
232
|
+
export interface RuntimeWrapper {
|
|
233
|
+
/** The call itself: where a mismatch is reported. */
|
|
234
|
+
readonly location: SourceLocation;
|
|
235
|
+
/**
|
|
236
|
+
* The exported name that was called (`withAmbit`, `ambitHandler`), so a
|
|
237
|
+
* diagnostic names what the source actually wrote. Not the module specifier:
|
|
238
|
+
* the reader is looking at the call, not the import.
|
|
239
|
+
*/
|
|
240
|
+
readonly wrapper: string;
|
|
241
|
+
/**
|
|
242
|
+
* The capability strings in the spec's literal `capabilities` array. An
|
|
243
|
+
* empty array is a real grant of nothing, not a missing one — that case is
|
|
244
|
+
* `unmatchedReason` instead.
|
|
245
|
+
*/
|
|
246
|
+
readonly capabilities?: readonly string[];
|
|
247
|
+
/**
|
|
248
|
+
* The spec's `budget` as the source fixes it, or absent when the source does
|
|
249
|
+
* not fix it. Independent of {@link RuntimeWrapper.capabilities}: a spec can
|
|
250
|
+
* write one half as a literal and build the other at runtime, and each half
|
|
251
|
+
* is compared — or reported as uncompared — on its own.
|
|
252
|
+
*/
|
|
253
|
+
readonly budget?: WrapperBudget;
|
|
254
|
+
/** The wrapped handler, when it is an identifier naming a declaration extracted from the same file. */
|
|
255
|
+
readonly handler?: SymbolId;
|
|
256
|
+
/** Why this wrapper could not be compared, when it could not. */
|
|
257
|
+
readonly unmatchedReason?: "handler-not-in-this-file";
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* A `spec.budget` the source fixes, in the shape the source wrote it
|
|
261
|
+
* ({@link BudgetInput}).
|
|
262
|
+
*
|
|
263
|
+
* `absent` is a spec that writes no budget at all, which the handler's JSDoc
|
|
264
|
+
* can agree or disagree with; it is not the same as the field being missing,
|
|
265
|
+
* which means the source did not fix the budget and nothing can be compared.
|
|
266
|
+
*
|
|
267
|
+
* Both sides are defaulted to `throw` before they are compared —
|
|
268
|
+
* `parseBudgetTag` already writes the default into a parsed `@budget`, so the
|
|
269
|
+
* JSDoc side has no absent state to compare an absent spec key against.
|
|
270
|
+
*/
|
|
271
|
+
export type WrapperBudget = {
|
|
272
|
+
readonly kind: "absent";
|
|
273
|
+
} | ({
|
|
274
|
+
readonly kind: "literal";
|
|
275
|
+
} & BudgetInput);
|
|
276
|
+
export interface ExtractedFile {
|
|
277
|
+
readonly filePath: string;
|
|
278
|
+
/**
|
|
279
|
+
* Every entry's {@link ExtractedFunction.id} is distinct. This is a
|
|
280
|
+
* requirement on the backend, not an observation about one: `propagate`
|
|
281
|
+
* argues its termination from each `SymbolId` naming exactly one summary
|
|
282
|
+
* (its state map is keyed by id, and its worklist iterates the summaries).
|
|
283
|
+
* Two entries sharing an id overwrite each other's state on every pass, so
|
|
284
|
+
* the fixed-point loop never settles and `ambit check` does not return.
|
|
285
|
+
*
|
|
286
|
+
* The shape that breaks it is an overload set — several declarations, one
|
|
287
|
+
* declaration path — which is why only the implementation is extracted
|
|
288
|
+
* (see {@link SkippedFunctionKind}'s `bodyless-declaration`).
|
|
289
|
+
* `test/backend.conformance.test.ts` asserts it for every fixture root.
|
|
290
|
+
*/
|
|
291
|
+
readonly functions: readonly ExtractedFunction[];
|
|
292
|
+
/**
|
|
293
|
+
* Required, not optional, for the reason {@link ExtractedProject}'s counts
|
|
294
|
+
* are: a backend that omitted them would report "no wrappers" and "wrappers
|
|
295
|
+
* not looked for" identically.
|
|
296
|
+
*/
|
|
297
|
+
readonly runtimeWrappers: readonly RuntimeWrapper[];
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* A contract written on a function-like node the backend did not extract, and
|
|
301
|
+
* which therefore cannot carry one. Reported as `AMB-E003` rather than
|
|
302
|
+
* dropped: a declaration that silently does nothing is the opposite of what
|
|
303
|
+
* Ambit is for (DESIGN.md §3.4 — do not convert an analysis failure into "no
|
|
304
|
+
* violations").
|
|
305
|
+
*
|
|
306
|
+
* `kind` is the same classification `skippedFunctions` counts, so the message
|
|
307
|
+
* can say *why* the node cannot carry the contract.
|
|
308
|
+
*/
|
|
309
|
+
export interface UncarriedContract {
|
|
310
|
+
readonly location: SourceLocation;
|
|
311
|
+
readonly kind: SkippedFunctionKind;
|
|
312
|
+
readonly tag: string;
|
|
313
|
+
readonly raw: string;
|
|
314
|
+
/**
|
|
315
|
+
* The `ambit.config.ts` key that *would* carry this contract, when one
|
|
316
|
+
* exists (DESIGN.md §4.1 (a) — an accessor or an anonymous default export).
|
|
317
|
+
* Absent for a node config cannot name either, where the only honest advice
|
|
318
|
+
* is to restructure the code.
|
|
319
|
+
*/
|
|
320
|
+
readonly configKey?: string;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Everything `extractProject` produces for one run: the extracted files, how
|
|
324
|
+
* many function-like nodes it saw but did not extract (by kind), and any
|
|
325
|
+
* contract written on one of those nodes.
|
|
326
|
+
*
|
|
327
|
+
* The count exists so "no violations" and "nothing was analyzed" stay
|
|
328
|
+
* distinguishable (DESIGN.md §3.4) — a file made entirely of, say, callback
|
|
329
|
+
* arguments would otherwise vanish from `files` with no trace.
|
|
330
|
+
*
|
|
331
|
+
* Both fields are required, not optional: a backend that omitted them would
|
|
332
|
+
* silently under-report what it could not analyze, which is the failure mode
|
|
333
|
+
* they exist to prevent.
|
|
334
|
+
*/
|
|
335
|
+
export interface ExtractedProject {
|
|
336
|
+
readonly files: readonly ExtractedFile[];
|
|
337
|
+
readonly skippedFunctions: ReadonlyMap<SkippedFunctionKind, number>;
|
|
338
|
+
readonly uncarriedContracts: readonly UncarriedContract[];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The connector layer's contract with the rest of Ambit (DESIGN.md §3.4,
|
|
342
|
+
* §3.5). No TypeScript-specific object (`ts.Node`, `ts.Symbol`, `ts.Type`,
|
|
343
|
+
* a compiler-internal id, ...) may cross this boundary in either direction.
|
|
344
|
+
*
|
|
345
|
+
* Implementations live under `src/checker/backend/`. The only one is
|
|
346
|
+
* `legacy-ts.ts`, adopted as the default by DESIGN.md §3.5
|
|
347
|
+
* and ADR-0001, and it must stay the only file that imports
|
|
348
|
+
* `typescript`. `test/backend.conformance.test.ts` states what any
|
|
349
|
+
* implementation of this interface has to satisfy.
|
|
350
|
+
*/
|
|
351
|
+
export interface TsBackend {
|
|
352
|
+
readonly name: string;
|
|
353
|
+
readonly version: string;
|
|
354
|
+
extractProject(rootDir: string): Promise<ExtractedProject>;
|
|
355
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A budget as an author writes it: a `withAmbit` / `ambitHandler` spec, or the
|
|
3
|
+
* `spec.budget` literal a backend extracts from one.
|
|
4
|
+
*
|
|
5
|
+
* Distinct from {@link Budget}, which is a budget after the default has been
|
|
6
|
+
* applied. Keeping the two apart is what lets `{ timeMs: 500 }` be a valid
|
|
7
|
+
* spec while every consumer of a parsed `@budget` can still read `onExceed`
|
|
8
|
+
* without re-defaulting it.
|
|
9
|
+
*/
|
|
10
|
+
export interface BudgetInput {
|
|
11
|
+
readonly timeMs?: number;
|
|
12
|
+
readonly costUsd?: number;
|
|
13
|
+
readonly llmCalls?: number;
|
|
14
|
+
/** Omittable; an omitted policy is {@link DEFAULT_ON_EXCEED} (`throw`). */
|
|
15
|
+
readonly onExceed?: OnExceed;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A per-invocation budget (DESIGN.md §4.5). A budget is "a declaration, a
|
|
19
|
+
* measurement, and a block", never a static guarantee: the checker's job here
|
|
20
|
+
* is to make sure the
|
|
21
|
+
* declaration itself is well-formed and to carry it to the runtime, not to
|
|
22
|
+
* prove the limit holds.
|
|
23
|
+
*/
|
|
24
|
+
export interface Budget extends BudgetInput {
|
|
25
|
+
/**
|
|
26
|
+
* Resolved, never absent: {@link parseBudgetTag} writes the default into a
|
|
27
|
+
* tag that omits it, and `withAmbit` does the same to a spec that omits it.
|
|
28
|
+
*/
|
|
29
|
+
readonly onExceed: OnExceed;
|
|
30
|
+
}
|
|
31
|
+
/** §4.5: `throw` (default) / `warn` / `abort`. */
|
|
32
|
+
export type OnExceed = "throw" | "warn" | "abort";
|
|
33
|
+
export declare const DEFAULT_ON_EXCEED: OnExceed;
|
|
34
|
+
/** Whether a string is one of §4.5's three `onExceed` policies. */
|
|
35
|
+
export declare function isOnExceed(value: string): value is OnExceed;
|
|
36
|
+
/**
|
|
37
|
+
* A {@link BudgetInput} with its omitted policy resolved, so a spec's budget
|
|
38
|
+
* and a parsed `@budget` are the same shape.
|
|
39
|
+
*
|
|
40
|
+
* `parseBudgetTag` already writes `throw` into a tag that omits it, so the
|
|
41
|
+
* JSDoc side has no absent state; leaving a spec's key absent would make
|
|
42
|
+
* `@budget timeMs=500` and `{ timeMs: 500 }` disagree over a policy both sides
|
|
43
|
+
* apply identically. The numeric limits are *not* defaulted: there `timeMs=500`
|
|
44
|
+
* against no `timeMs` is a real disagreement.
|
|
45
|
+
*
|
|
46
|
+
* Field by field rather than a spread: the input may carry a discriminant
|
|
47
|
+
* (`WrapperBudget`'s `kind`) that has no business in a {@link Budget}.
|
|
48
|
+
*/
|
|
49
|
+
export declare function budgetFrom(input: BudgetInput): Budget;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a `@budget timeMs=500 costUsd=0.01 llmCalls=2 onExceed=throw` tag.
|
|
52
|
+
*
|
|
53
|
+
* `undefined` when any part is malformed — an unknown key, a non-numeric
|
|
54
|
+
* limit, a negative limit, an `onExceed` outside the three §4.5 names, a
|
|
55
|
+
* repeated key, or no limit at all. A budget that does not parse is rejected
|
|
56
|
+
* rather than partly applied, for the reason AMB-E002 rejects a misspelled
|
|
57
|
+
* effect: a declaration read as narrower than written is a manufactured
|
|
58
|
+
* guarantee.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseBudgetTag(text: string): Budget | undefined;
|
|
61
|
+
export declare function formatBudget(budget: Budget): string;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const ON_EXCEED = ["throw", "warn", "abort"];
|
|
2
|
+
export const DEFAULT_ON_EXCEED = "throw";
|
|
3
|
+
/** Whether a string is one of §4.5's three `onExceed` policies. */
|
|
4
|
+
export function isOnExceed(value) {
|
|
5
|
+
return ON_EXCEED.includes(value);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A {@link BudgetInput} with its omitted policy resolved, so a spec's budget
|
|
9
|
+
* and a parsed `@budget` are the same shape.
|
|
10
|
+
*
|
|
11
|
+
* `parseBudgetTag` already writes `throw` into a tag that omits it, so the
|
|
12
|
+
* JSDoc side has no absent state; leaving a spec's key absent would make
|
|
13
|
+
* `@budget timeMs=500` and `{ timeMs: 500 }` disagree over a policy both sides
|
|
14
|
+
* apply identically. The numeric limits are *not* defaulted: there `timeMs=500`
|
|
15
|
+
* against no `timeMs` is a real disagreement.
|
|
16
|
+
*
|
|
17
|
+
* Field by field rather than a spread: the input may carry a discriminant
|
|
18
|
+
* (`WrapperBudget`'s `kind`) that has no business in a {@link Budget}.
|
|
19
|
+
*/
|
|
20
|
+
export function budgetFrom(input) {
|
|
21
|
+
return {
|
|
22
|
+
...(input.timeMs === undefined ? {} : { timeMs: input.timeMs }),
|
|
23
|
+
...(input.costUsd === undefined ? {} : { costUsd: input.costUsd }),
|
|
24
|
+
...(input.llmCalls === undefined ? {} : { llmCalls: input.llmCalls }),
|
|
25
|
+
onExceed: input.onExceed ?? DEFAULT_ON_EXCEED,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const NUMERIC_KEYS = ["timeMs", "costUsd", "llmCalls"];
|
|
29
|
+
function isNumericKey(key) {
|
|
30
|
+
return NUMERIC_KEYS.includes(key);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse a `@budget timeMs=500 costUsd=0.01 llmCalls=2 onExceed=throw` tag.
|
|
34
|
+
*
|
|
35
|
+
* `undefined` when any part is malformed — an unknown key, a non-numeric
|
|
36
|
+
* limit, a negative limit, an `onExceed` outside the three §4.5 names, a
|
|
37
|
+
* repeated key, or no limit at all. A budget that does not parse is rejected
|
|
38
|
+
* rather than partly applied, for the reason AMB-E002 rejects a misspelled
|
|
39
|
+
* effect: a declaration read as narrower than written is a manufactured
|
|
40
|
+
* guarantee.
|
|
41
|
+
*/
|
|
42
|
+
export function parseBudgetTag(text) {
|
|
43
|
+
const tokens = text.trim().split(/\s+/).filter(Boolean);
|
|
44
|
+
if (tokens.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
const limits = new Map();
|
|
47
|
+
let onExceed;
|
|
48
|
+
for (const token of tokens) {
|
|
49
|
+
const separator = token.indexOf("=");
|
|
50
|
+
if (separator <= 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const key = token.slice(0, separator);
|
|
53
|
+
const raw = token.slice(separator + 1);
|
|
54
|
+
if (raw.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (key === "onExceed") {
|
|
57
|
+
if (onExceed !== undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (!ON_EXCEED.includes(raw))
|
|
60
|
+
return undefined;
|
|
61
|
+
onExceed = raw;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isNumericKey(key))
|
|
65
|
+
return undefined;
|
|
66
|
+
if (limits.has(key))
|
|
67
|
+
return undefined;
|
|
68
|
+
const value = Number(raw);
|
|
69
|
+
if (!Number.isFinite(value) || value < 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (key === "llmCalls" && !Number.isInteger(value))
|
|
72
|
+
return undefined;
|
|
73
|
+
limits.set(key, value);
|
|
74
|
+
}
|
|
75
|
+
// `@budget onExceed=warn` alone declares a policy with nothing to exceed.
|
|
76
|
+
if (limits.size === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
return {
|
|
79
|
+
...(limits.has("timeMs") ? { timeMs: limits.get("timeMs") } : {}),
|
|
80
|
+
...(limits.has("costUsd") ? { costUsd: limits.get("costUsd") } : {}),
|
|
81
|
+
...(limits.has("llmCalls") ? { llmCalls: limits.get("llmCalls") } : {}),
|
|
82
|
+
onExceed: onExceed ?? DEFAULT_ON_EXCEED,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function formatBudget(budget) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
if (budget.timeMs !== undefined)
|
|
88
|
+
parts.push(`timeMs=${budget.timeMs}`);
|
|
89
|
+
if (budget.costUsd !== undefined)
|
|
90
|
+
parts.push(`costUsd=${budget.costUsd}`);
|
|
91
|
+
if (budget.llmCalls !== undefined)
|
|
92
|
+
parts.push(`llmCalls=${budget.llmCalls}`);
|
|
93
|
+
parts.push(`onExceed=${budget.onExceed}`);
|
|
94
|
+
return parts.join(" ");
|
|
95
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A capability: what a function is allowed to act on, as opposed to what it
|
|
3
|
+
* does (DESIGN.md §4.4). Written `<resource>:<action>:<target>` —
|
|
4
|
+
* `db:read:users`, `http:get:api.example.com`.
|
|
5
|
+
*
|
|
6
|
+
* Only `target` may be a glob (`http:get:*.example.com`); `resource` and
|
|
7
|
+
* `action` match literally. §4.4 says exactly that ("`target` may be globbed")
|
|
8
|
+
* and nothing more, and widening the glob to the other segments would let
|
|
9
|
+
* `*:*:*` be written as a contract that reads like a restriction.
|
|
10
|
+
*/
|
|
11
|
+
export interface Capability {
|
|
12
|
+
readonly resource: string;
|
|
13
|
+
readonly action: string;
|
|
14
|
+
readonly target: string;
|
|
15
|
+
}
|
|
16
|
+
/** Parsed `@capabilities`, plus whether the set might be incomplete. */
|
|
17
|
+
export interface CapabilitySet {
|
|
18
|
+
readonly capabilities: readonly Capability[];
|
|
19
|
+
/**
|
|
20
|
+
* Set when a call could not be resolved (DESIGN.md §4.2 rule 6), so the
|
|
21
|
+
* true requirement could include anything. Tracked separately for the same
|
|
22
|
+
* reason `EffectSet.unknown` is: "requires nothing more" and "we could not
|
|
23
|
+
* tell" are different claims.
|
|
24
|
+
*/
|
|
25
|
+
readonly unknown: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function emptyCapabilitySet(): CapabilitySet;
|
|
28
|
+
export declare function unknownCapabilitySet(): CapabilitySet;
|
|
29
|
+
export declare function formatCapability(capability: Capability): string;
|
|
30
|
+
/**
|
|
31
|
+
* Parse one `<resource>:<action>:<target>` token. `undefined` when the token
|
|
32
|
+
* is not three non-empty segments — a malformed capability is rejected rather
|
|
33
|
+
* than partly honoured, on the same principle as a misspelled effect name
|
|
34
|
+
* (AMB-E002): a declaration that does not mean what it says must not be read
|
|
35
|
+
* as a narrower guarantee than the author intended.
|
|
36
|
+
*
|
|
37
|
+
* `target` may itself contain `:` (a host:port, say), so the split takes the
|
|
38
|
+
* first two separators only.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseCapability(token: string): Capability | undefined;
|
|
41
|
+
/** Parse a `@capabilities` tag's comma-separated list. `undefined` if any token is malformed. */
|
|
42
|
+
export declare function parseCapabilitiesTag(text: string): CapabilitySet | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Whether `granted` permits `required` (DESIGN.md §4.4: capabilities may only
|
|
45
|
+
* narrow from caller to callee). `resource` and `action` must match exactly;
|
|
46
|
+
* `granted.target` is matched as a glob against `required.target`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function capabilityCovers(granted: Capability, required: Capability): boolean;
|
|
49
|
+
/** Capabilities in `required` that no capability in `granted` permits. */
|
|
50
|
+
export declare function excessCapabilities(granted: readonly Capability[], required: readonly Capability[]): readonly Capability[];
|
|
51
|
+
export declare function unionCapabilitySets(a: CapabilitySet, b: CapabilitySet): CapabilitySet;
|
|
52
|
+
export declare function capabilitiesEqual(a: Capability, b: Capability): boolean;
|
|
53
|
+
export declare function capabilitySetsEqual(a: CapabilitySet, b: CapabilitySet): boolean;
|