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.
Files changed (96) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/README.md +403 -0
  4. package/dist/checker/authority.d.ts +13 -0
  5. package/dist/checker/authority.js +87 -0
  6. package/dist/checker/backend/legacy-ts.d.ts +26 -0
  7. package/dist/checker/backend/legacy-ts.js +1936 -0
  8. package/dist/checker/config.d.ts +84 -0
  9. package/dist/checker/config.js +391 -0
  10. package/dist/checker/coverage.d.ts +78 -0
  11. package/dist/checker/coverage.js +84 -0
  12. package/dist/checker/diagnose.d.ts +89 -0
  13. package/dist/checker/diagnose.js +734 -0
  14. package/dist/checker/index.d.ts +8 -0
  15. package/dist/checker/index.js +8 -0
  16. package/dist/checker/init.d.ts +38 -0
  17. package/dist/checker/init.js +205 -0
  18. package/dist/checker/propagate.d.ts +69 -0
  19. package/dist/checker/propagate.js +259 -0
  20. package/dist/checker/summarize.d.ts +27 -0
  21. package/dist/checker/summarize.js +411 -0
  22. package/dist/cli/analyze.d.ts +33 -0
  23. package/dist/cli/analyze.js +98 -0
  24. package/dist/cli/approvals.d.ts +28 -0
  25. package/dist/cli/approvals.js +55 -0
  26. package/dist/cli/diff.d.ts +66 -0
  27. package/dist/cli/diff.js +235 -0
  28. package/dist/cli/github.d.ts +33 -0
  29. package/dist/cli/github.js +41 -0
  30. package/dist/cli/main.d.ts +8 -0
  31. package/dist/cli/main.js +385 -0
  32. package/dist/cli/worktree.d.ts +75 -0
  33. package/dist/cli/worktree.js +154 -0
  34. package/dist/config.d.ts +12 -0
  35. package/dist/config.js +10 -0
  36. package/dist/core/approvals.d.ts +82 -0
  37. package/dist/core/approvals.js +0 -0
  38. package/dist/core/authority-diff.d.ts +98 -0
  39. package/dist/core/authority-diff.js +209 -0
  40. package/dist/core/authority.d.ts +109 -0
  41. package/dist/core/authority.js +50 -0
  42. package/dist/core/backend.d.ts +355 -0
  43. package/dist/core/backend.js +1 -0
  44. package/dist/core/budget.d.ts +61 -0
  45. package/dist/core/budget.js +95 -0
  46. package/dist/core/capability.d.ts +53 -0
  47. package/dist/core/capability.js +117 -0
  48. package/dist/core/config.d.ts +59 -0
  49. package/dist/core/config.js +10 -0
  50. package/dist/core/diagnostic.d.ts +126 -0
  51. package/dist/core/diagnostic.js +13 -0
  52. package/dist/core/effects.d.ts +39 -0
  53. package/dist/core/effects.js +72 -0
  54. package/dist/core/index.d.ts +13 -0
  55. package/dist/core/index.js +13 -0
  56. package/dist/core/location.d.ts +15 -0
  57. package/dist/core/location.js +1 -0
  58. package/dist/core/sql.d.ts +22 -0
  59. package/dist/core/sql.js +38 -0
  60. package/dist/core/summary.d.ts +240 -0
  61. package/dist/core/summary.js +8 -0
  62. package/dist/core/symbol-id.d.ts +25 -0
  63. package/dist/core/symbol-id.js +23 -0
  64. package/dist/index.d.ts +14 -0
  65. package/dist/index.js +14 -0
  66. package/dist/runtime/child-process.d.ts +29 -0
  67. package/dist/runtime/child-process.js +124 -0
  68. package/dist/runtime/context.d.ts +37 -0
  69. package/dist/runtime/context.js +8 -0
  70. package/dist/runtime/enforce.d.ts +52 -0
  71. package/dist/runtime/enforce.js +95 -0
  72. package/dist/runtime/fs.d.ts +46 -0
  73. package/dist/runtime/fs.js +188 -0
  74. package/dist/runtime/hono.d.ts +55 -0
  75. package/dist/runtime/hono.js +68 -0
  76. package/dist/runtime/index.d.ts +71 -0
  77. package/dist/runtime/index.js +126 -0
  78. package/dist/runtime/next.d.ts +95 -0
  79. package/dist/runtime/next.js +60 -0
  80. package/dist/runtime/pg.d.ts +48 -0
  81. package/dist/runtime/pg.js +122 -0
  82. package/dist/stubs/constructors.d.ts +34 -0
  83. package/dist/stubs/constructors.js +111 -0
  84. package/dist/stubs/data-clients.d.ts +9 -0
  85. package/dist/stubs/data-clients.js +109 -0
  86. package/dist/stubs/http-capabilities.d.ts +15 -0
  87. package/dist/stubs/http-capabilities.js +70 -0
  88. package/dist/stubs/mutating-builtins.d.ts +1 -0
  89. package/dist/stubs/mutating-builtins.js +48 -0
  90. package/dist/stubs/node-builtins.d.ts +2 -0
  91. package/dist/stubs/node-builtins.js +77 -0
  92. package/dist/stubs/pure-builtins.d.ts +1 -0
  93. package/dist/stubs/pure-builtins.js +89 -0
  94. package/docs/diagnostics/README.md +519 -0
  95. package/docs/limitations.md +712 -0
  96. 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;