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,519 @@
1
+ # Diagnostic codes
2
+
3
+ Living ledger of Ambit's diagnostic codes, appended as they are implemented.
4
+
5
+ An `id` and its meaning are part of the guaranteed surface (DESIGN.md §9.2):
6
+ a change to either is announced in `CHANGELOG.md` at the release that makes it.
7
+ While the major version is 0, such a change may land in a minor release
8
+ (§9.3) — ids may still be renumbered or reworded, but not silently. From 1.0,
9
+ DESIGN.md §5.2's stronger rule applies: an `id` is never deleted or reused, and
10
+ a meaning change requires an RFC (§9.1). That version qualifier is in §5.2
11
+ itself, not only here.
12
+
13
+ Message text is written in English; this file is written in English as well,
14
+ independent of the project's Japanese-language documentation policy — see
15
+ `AGENTS.md`.
16
+
17
+ ## AMB-E001
18
+
19
+ Declared effects exceeded.
20
+
21
+ **Severity:** error
22
+ **Category:** effects
23
+
24
+ The function's declared `@effects` set does not contain an effect that was
25
+ observed either directly in its body or propagated from a callee (DESIGN.md
26
+ §4.2, rule 1). `contract.via` lists the call path from the declaring function
27
+ to the function where the effect was found, when propagation crossed at
28
+ least one call.
29
+
30
+ Example: a function declared `@effects pure` calls another function that
31
+ performs a `fetch`.
32
+
33
+ **Fixes:** one `widen` candidate, replacing the `@effects` tag with the
34
+ observed set. It is always `consistentWithContract: false` — it loosens the
35
+ promise rather than keeping it — and `impact.pureCallersBroken` counts the
36
+ callers whose own declaration would no longer cover it. No contract-preserving
37
+ candidate is emitted: restoring the declaration means restructuring the code,
38
+ which Ambit cannot patch safely, and DESIGN.md §5.3 forbids inventing a
39
+ candidate for the sake of ranking.
40
+
41
+ ## AMB-W001
42
+
43
+ Declared effects reach unknown.
44
+
45
+ **Severity:** warning
46
+ **Category:** effects
47
+
48
+ A function with a declared `@effects` set (including `pure`) calls a
49
+ function whose effects could not be resolved (DESIGN.md §4.2, rule 3). This
50
+ does not by itself mean a violation occurred — `unknown` may resolve to a
51
+ set already covered by the declaration once the callee is annotated or a
52
+ stub is added — but the declaration is not yet backed by a verified
53
+ guarantee.
54
+
55
+ Example: a function declared `@effects pure` or `@effects network` calls
56
+ `eval(...)` or a function with an unresolved call graph.
57
+
58
+ Promoted to an error by `--strict`, and by a `strict` glob in
59
+ `ambit.config.ts` that matches the file it is reported in (DESIGN.md §4.3) —
60
+ the mechanism for tightening new code while leaving legacy directories at
61
+ warning level.
62
+
63
+ ## AMB-E002
64
+
65
+ Unknown effect name in `@effects`.
66
+
67
+ **Severity:** error
68
+ **Category:** effects
69
+
70
+ An `@effects` tag contains a token that is neither `pure` nor one of the
71
+ known effects (`network`, `db_read`, `db_write`, `fs_read`, `fs_write`,
72
+ `state_write`,
73
+ `llm`, `env`, `process`) — most often a typo. The declaration is rejected
74
+ rather than silently narrowed to whatever tokens did parse: the function is
75
+ treated as undeclared (not as `pure`) for propagation, so it never also
76
+ produces `AMB-E001` for the same tag.
77
+
78
+ Example: `@effects netwrok` (missing an `r`) instead of `@effects network`.
79
+
80
+ ## AMB-E003
81
+
82
+ Contract declared on a node that cannot carry one.
83
+
84
+ **Severity:** error
85
+ **Category:** effects
86
+
87
+ A contract tag (`@effects`, `@capabilities`, `@budget`, `@entrypoint`,
88
+ `@boundary`) is written on a function-like node the analysis does not extract, so it has no
89
+ symbol to attach the contract to. The declaration is inert: nothing propagates
90
+ it, nothing checks it, and it appears in no coverage figure. It is reported for
91
+ the same reason a misspelled effect name is (AMB-E002) — a declaration that
92
+ silently does nothing reads as a guarantee and is not one.
93
+
94
+ The message names why the node cannot carry a contract, using the same
95
+ classification `--coverage` counts under "skipped": a getter/setter, an
96
+ object-literal member with no stable declaration path, an anonymous default
97
+ export, a callback passed inline as an argument, a function declared inside
98
+ another function, or a declaration with no body.
99
+
100
+ Two of those *are* analyzed where the declaration path reaches them: a
101
+ `get`/`set` accessor on a class or on a module-scope `const` object literal,
102
+ and an anonymous `export default`, have stable paths (`Cls.get total`,
103
+ `default`), so their bodies propagate and `ambit.config.ts` can declare
104
+ contracts for them. An accessor in a literal that rule does not reach (a `let`
105
+ binding, a spread, a nested or inline literal) is still skipped, and neither
106
+ JSDoc nor config can name it — DESIGN.md §4.1 (a) keeps the config namespace a superset of the JSDoc
107
+ one. The comment on them is still inert, so this is still an error, and the
108
+ message ends with the config key that would work:
109
+ `declare it in ambit.config.ts under "src/cart.ts#Cart.get total" instead`.
110
+ DESIGN.md §12 records the asymmetry that leaves.
111
+
112
+ A declaration with no body — an overload signature, an `abstract` member, or a
113
+ `declare function` written in a `.ts` file — is reported for a reason worth
114
+ separating from the others. An overload set is one runtime function, and it is
115
+ the implementation: the signatures describe types, and only the implementation
116
+ runs. So the contract belongs on the implementation, and the message says so.
117
+ This is not a limitation waiting to be lifted. Adopting a signature's contract
118
+ would mean attributing it to a declaration Ambit does not model, and choosing
119
+ between two signatures that disagree; both are guesses, and DESIGN.md
120
+ §4.1 "Overloads and bodyless declarations" settles it the other way. A call
121
+ to an overload set with no implementation in the project is `unknown`
122
+ (`overload-without-body` in `--coverage`), not `pure`.
123
+
124
+ For a `declare function` with no implementation anywhere in the project, there
125
+ is nothing to move the contract *to*, and the message's advice does not apply.
126
+ What that code needs is a stub (`src/stubs/`); until it has one, calls to it
127
+ are honestly `unknown`.
128
+
129
+ One case is not a function-like node at all: a contract written on a `class`.
130
+ The class's construction *is* analyzed (indexed as `Class.constructor`), but a
131
+ class's own comment is never read as its implicit constructor's contract — a
132
+ comment about the class is not a verified statement about constructing it. The
133
+ message says the contract belongs on the constructor. This case is reported
134
+ but not counted under "skipped", which counts function-like nodes.
135
+
136
+ Example: `/** @effects fs_read */ get value() { … }`.
137
+
138
+ Note this is narrower than it was: an object-literal member *can* carry a
139
+ contract when the literal is a module-scope `const` and the member has an
140
+ identifier name. See `docs/limitations.md`.
141
+
142
+ ## AMB-E004
143
+
144
+ Malformed `@capabilities`.
145
+
146
+ **Severity:** error
147
+ **Category:** capabilities
148
+
149
+ A `@capabilities` tag is not a comma-separated list of
150
+ `<resource>:<action>:<target>` (DESIGN.md §4.4). All three segments must be
151
+ non-empty, and only `target` may contain a glob (`*`, `?`) — a `*` in the
152
+ resource or action segment reads as a restriction while meaning the opposite,
153
+ so it is rejected. The target may itself contain a colon
154
+ (`http:get:localhost:8080`).
155
+
156
+ The whole tag is rejected rather than partly honoured, and the function is
157
+ treated as granting nothing — so it never also produces AMB-E005 for the same
158
+ tag. Same rule as AMB-E002.
159
+
160
+ Example: `@capabilities db:read` (two segments), `@capabilities *:read:users`
161
+ (glob outside the target).
162
+
163
+ ## AMB-E005
164
+
165
+ Capability escalation.
166
+
167
+ **Severity:** error
168
+ **Category:** capabilities
169
+
170
+ A function declaring `@capabilities` reaches a callee that requires a
171
+ capability the declaration does not grant (DESIGN.md §4.4: capabilities may
172
+ only narrow from caller to callee). `contract.excess` lists the ungranted
173
+ capabilities and `contract.via` the call path to the one reported.
174
+
175
+ A capability is granted when the resource and action match exactly and the
176
+ grant's target, treated as a glob, matches the required target — so
177
+ `db:read:*` covers `db:read:users`.
178
+
179
+ The check crosses undeclared functions: `A` granting `db:read:users`, calling
180
+ an undeclared `B`, which calls a `C` declaring `db:write:users`, is a
181
+ violation in `A`. An undeclared hop does not launder an escalation.
182
+
183
+ Example: a function declaring `@capabilities db:read:users` that calls one
184
+ declaring `@capabilities db:write:users`.
185
+
186
+ ## AMB-E009
187
+
188
+ Literal target outside the granted capabilities.
189
+
190
+ **Severity:** error
191
+ **Category:** capabilities
192
+
193
+ A function declaring `@capabilities` performs an operation whose target the
194
+ source fixes — a literal URL, or a template literal whose static head already
195
+ ends the authority — and no grant covers it. This is the static half of
196
+ DESIGN.md §4.4's dual enforcement: "violations that can be decided
197
+ statically, such as literal URLs and known clients, are stopped by the
198
+ checker".
199
+
200
+ Reported at the call site, not at the declaration: that is the line to change.
201
+ Separate from AMB-E005 because the finding is different — nothing declared
202
+ this requirement, the body performs it directly, so there is no callee whose
203
+ contract is too wide.
204
+
205
+ What is matched: `http:<method>:<host>` for the bundled HTTP entry points
206
+ (`fetch`, `undici`'s `fetch`, `node:http`/`node:https`'s `get`/`request`). The
207
+ method comes from a literal `method` in an options object literal and defaults
208
+ to `get`; the host is taken as written, port included and userinfo dropped. A
209
+ URL the source does not fix — built at runtime, or a template literal whose
210
+ static head stops inside the authority — produces no requirement to compare
211
+ and is reported as `AMB-W003` instead, naming the runtime as the place it is
212
+ matched. A relative URL names no host and is treated the same way.
213
+
214
+ No `db:` capability is derived from a SQL statement. §4.4 is explicit that a
215
+ hook on a database client does not amount to deciding table-level permission
216
+ for arbitrary SQL, and reading a table name out of a literal statement would
217
+ be the same claim in a different place.
218
+
219
+ Example: an entrypoint granting `@capabilities http:get:api.example.com` whose
220
+ body calls `fetch("https://elsewhere.example/steal")`.
221
+
222
+ **Fixes:** none. Widening the grant and changing the URL are both plausible
223
+ and Ambit cannot tell which was meant; §5.3 forbids inventing a candidate for
224
+ the sake of ranking, and silently widening a capability is the expansion of
225
+ authority the tag exists to catch.
226
+
227
+ ## AMB-W003
228
+
229
+ Declared capabilities reach unknown.
230
+
231
+ **Severity:** warning
232
+ **Category:** capabilities
233
+
234
+ A function with declared `@capabilities` cannot have its requirement fully
235
+ determined. The capability analogue of AMB-W001, and promoted to an error by
236
+ `--strict` for the same reason — or by a `strict` glob in `ambit.config.ts`
237
+ that matches the file it is reported in (DESIGN.md §4.3). The two are a
238
+ union: a config listing fewer directories never narrows a `--strict` run.
239
+
240
+ The message names which of three causes applies, because they are fixed
241
+ differently: a callee that could not be resolved, a `@boundary` callee that
242
+ declared no `@capabilities`, or an operation whose target the source does not
243
+ fix (a URL built at runtime — see AMB-E009), which §4.4 assigns to the runtime
244
+ hook rather than to the checker.
245
+
246
+ ## AMB-E010
247
+
248
+ `withAmbit` or an adapter's `ambitHandler` / `ambitRoute` disagrees with the
249
+ handler's `@capabilities`.
250
+
251
+ **Severity:** error
252
+ **Category:** capabilities
253
+
254
+ DESIGN.md §4.4 chose explicit registration, so the capability set that reaches
255
+ the runtime is the one in the `withAmbit(spec, handler)`, `ambitHandler(spec,
256
+ handler, decode)` (the `ambit-ts/runtime/hono` adapter) or `ambitRoute(spec,
257
+ handler, decode)` (the `ambit-ts/runtime/next` adapter) beside the handler. A
258
+ literal one *is* the handler's `@capabilities`, so the tag need not repeat it.
259
+ Writing both is still allowed, and this reports the two disagreeing, as sets of
260
+ the text each one wrote. The message names the call the source actually wrote. Order does not matter; anything else does, including a glob on one
261
+ side only, since `db:read:*` and `db:read:users` are different grants.
262
+
263
+ Reported at the call. Neither side is privileged: whichever half an
264
+ agent edited, the pair stopped agreeing, and Ambit cannot tell which one the
265
+ author meant.
266
+
267
+ Compared only when all of this holds — otherwise `AMB-W004`:
268
+
269
+ - the spec is an object literal with no spread, and its `capabilities` is a
270
+ literal array of string literals (a missing `capabilities` key counts as an
271
+ empty grant, which can still disagree);
272
+ - the handler is an identifier naming a declaration in the same file that the
273
+ analysis extracted.
274
+
275
+ A handler that declares neither `@entrypoint` nor `@capabilities` still has a
276
+ declaration when the spec beside it is literal — that spec — so there is
277
+ something to compare and the two agree by construction. It is `AMB-W004` only
278
+ when the spec is not literal, where nothing was declared on either side.
279
+
280
+ A handler whose `@capabilities` failed to parse is skipped here: `AMB-E004`
281
+ already reports that tag, and comparing against a declaration Ambit rejected
282
+ would name the wrong problem.
283
+
284
+ The budget half of the same rule is `AMB-E011`, reported separately:
285
+ each half is fixed by the source on its own, so a spec may write one as a
286
+ literal and build the other at runtime.
287
+
288
+ **Fixes:** none. Aligning the two means choosing which one is right, which is
289
+ the decision being reported (§5.3).
290
+
291
+ ## AMB-E011
292
+
293
+ `withAmbit` or an adapter's `ambitHandler` / `ambitRoute` disagrees with the
294
+ handler's `@budget`.
295
+
296
+ **Severity:** error
297
+ **Category:** budget
298
+
299
+ The budget half of the same rule: `spec.budget` is what the runtime applies,
300
+ and a literal one is also what the source declares, so `@budget` on the handler
301
+ is optional beside it. Where both are written, this reports
302
+ the two disagreeing — a different `timeMs`, a different `onExceed`, or a limit
303
+ present on one side only.
304
+
305
+ A separate id rather than an extension of `AMB-E010` because that diagnostic's
306
+ `contract` field is capability text (`declared` / `required` / `excess`), which
307
+ a budget disagreement has nothing honest to put in, and because the category
308
+ that belongs on it is `budget`, not `capabilities`.
309
+
310
+ `onExceed` is compared **after** both sides are defaulted to `throw`.
311
+ `parseBudgetTag` writes the default into a `@budget` that omits it, and the
312
+ runtime defaults an omitted `spec.budget.onExceed` the same way, so the JSDoc
313
+ side has no absent state for an absent spec key to disagree with. The numeric
314
+ limits are not defaulted: `timeMs=500` against no `timeMs` is a real
315
+ disagreement, and is reported as one.
316
+
317
+ Compared only when all of this holds — otherwise `AMB-W004`:
318
+
319
+ - the spec is an object literal with no spread, and its `budget`, when
320
+ present, is an object literal whose keys are `timeMs` / `costUsd` /
321
+ `llmCalls` / `onExceed` and whose values are literals (a missing `budget`
322
+ key counts as no budget, which can still disagree with a declared one);
323
+ - the handler is an identifier naming a declaration in the same file that the
324
+ analysis extracted.
325
+
326
+ A handler whose `@budget` failed to parse is skipped here, for the reason
327
+ `AMB-E010` skips an unparsed `@capabilities`: `AMB-E008` already reports that
328
+ tag.
329
+
330
+ **Fixes:** none, for `AMB-E010`'s reason — which of the two is right is the
331
+ decision being reported.
332
+
333
+ ## AMB-W004
334
+
335
+ `withAmbit`, `ambitHandler` or `ambitRoute` was not compared with a declared
336
+ contract.
337
+
338
+ **Severity:** warning
339
+ **Category:** capabilities
340
+
341
+ A `withAmbit(spec, handler)` or an adapter's `ambitHandler(spec, handler,
342
+ decode)` / `ambitRoute(spec, handler, decode)` was found, but one of `AMB-E010`'s or `AMB-E011`'s conditions does not
343
+ hold: the capability list is built at runtime, the budget is not an object
344
+ literal of literal limits, or the handler is not a declaration in the same
345
+ file. The message names which.
346
+
347
+ A spec Ambit cannot read declares nothing. So this warning is also the place
348
+ where the single-source rule stops: everywhere else a literal spec supplies the
349
+ handler's `@capabilities` / `@budget`, and here it does not, which leaves the
350
+ handler's own JSDoc as the only declaration there is. Dropping the tag beside
351
+ one of these registrations does not make the contract implicit — it makes it
352
+ missing, and the entrypoint is then reported as capability-less (`AMB-W002`)
353
+ on top of this warning.
354
+
355
+ The two halves are reported independently, so one registration can produce a
356
+ compared capability set and an uncompared budget, or the reverse. Collapsing
357
+ the wrapper to a single warning the moment either half was dynamic would drop
358
+ a check the source does support.
359
+
360
+ Reported rather than skipped for the reason `AMB-E003` reports an inert
361
+ declaration: a wrapper that produced no diagnostic at all would read as
362
+ "checked and agreed".
363
+
364
+ Not an error, and not promoted by `--strict`. The comparison is on the source
365
+ only; matching a contract to the handler that actually runs — after a build
366
+ strips the comments, or a bundler moves it — is DESIGN.md §12's
367
+ "Mapping contracts to handlers" and is still open.
368
+
369
+ ## AMB-W005
370
+
371
+ JSDoc and `ambit.config.ts` declare the same tag differently.
372
+
373
+ **Severity:** warning
374
+ **Category:** effects
375
+
376
+ One symbol has both a JSDoc contract and a `contracts` entry, and for at least
377
+ one of the five tags the two do not say the same thing. DESIGN.md §4.1 settles
378
+ which wins — "if a symbol has both JSDoc and config, JSDoc wins and the
379
+ difference is warned about" — so the run proceeds with the JSDoc
380
+ declaration and this diagnostic reports what was ignored.
381
+
382
+ Compared tag by tag, on the parsed values rather than on the text: `@effects
383
+ db_read, network` and `effects: ["network", "db_read"]` are the same
384
+ declaration and are not reported. A tag only one side declares is not a
385
+ difference either — it is the other side filling a gap, which is the normal
386
+ way a config supplements code it cannot edit.
387
+
388
+ A warning rather than an error, because the specified behaviour is exactly
389
+ what happened. It is still reported for AMB-E003's reason: a config entry the
390
+ author believes is in force, and is not, is a declaration that does nothing.
391
+
392
+ Not promoted by `--strict`, in either its command-line or its per-directory
393
+ form. `--strict` means "an unverified path is not acceptable here" (§4.2 rule
394
+ 3); a disagreement between two declarations is a different thing.
395
+
396
+ No fix is offered. Deleting the config entry and rewriting the JSDoc are
397
+ opposite intentions, and §5.3 forbids inventing a candidate to fill the slot.
398
+
399
+ ## AMB-W006
400
+
401
+ A `contracts` key matches nothing.
402
+
403
+ **Severity:** warning
404
+ **Category:** effects
405
+
406
+ An exact `contracts` key — one whose file half contains no `*` — named no
407
+ declaration in what was analyzed. The contract it declares is not in force,
408
+ which is AMB-E003's failure in a different file: a declaration that silently
409
+ applies to nothing reads as a guarantee and is not one.
410
+
411
+ Only exact keys are reported. A glob is written to cover whatever is there,
412
+ and `ambit check src/domain` legitimately matches none of a
413
+ `src/legacy/**` pattern; reporting those would make the diagnostic noisiest
414
+ exactly when the run is narrowest.
415
+
416
+ The location is the key's own line in the config file, found textually — the
417
+ config is loaded by importing it, not by parsing it, so a key built by an
418
+ expression rather than written literally falls back to the file's first
419
+ character.
420
+
421
+ Common causes: a typo in the symbol half, a declaration path that is not what
422
+ the checker uses (`Cls.get total`, not `Cls.total` — DESIGN.md §4.1 (a)), or a
423
+ key naming a file outside the directory being checked.
424
+
425
+ ## AMB-W002
426
+
427
+ Entrypoint with no capabilities.
428
+
429
+ **Severity:** warning
430
+ **Category:** capabilities
431
+
432
+ A function marked `@entrypoint` declares no `@capabilities`. An entrypoint is
433
+ where `ambit-ts/runtime` would establish a capability context (DESIGN.md §4.4);
434
+ one with no declared set establishes nothing to check against. §4.4:
435
+ "leaving them unspecified is warned about as equivalent to unknown".
436
+
437
+ ## AMB-E006
438
+
439
+ `@boundary` with no reason.
440
+
441
+ **Severity:** error
442
+ **Category:** boundary
443
+
444
+ DESIGN.md §4.6 makes `reason` mandatory on `@boundary`
445
+ (`@boundary reason="legacy SDK, not annotated"`). A boundary is an explicit,
446
+ recorded decision to stop checking a body; without a reason it is an
447
+ unexplained hole, which is the thing the tag exists to make visible.
448
+
449
+ ## AMB-E007
450
+
451
+ `@boundary` with no contract to trust.
452
+
453
+ **Severity:** error
454
+ **Category:** boundary
455
+
456
+ A `@boundary` function declares no `@effects`. §4.6's bargain is "do not check
457
+ inside; trust what is declared to the outside" — with nothing declared, there
458
+ is nothing to trust, and the tag only removes checking. The function's effects
459
+ become `unknown`, so callers still see the hole, but the declaration itself is
460
+ reported: a tag that only subtracts a guarantee should not look like one.
461
+
462
+ ## AMB-E008
463
+
464
+ Malformed `@budget`.
465
+
466
+ **Severity:** error
467
+ **Category:** budget
468
+
469
+ A `@budget` tag is not a space-separated list of `key=value` pairs with keys
470
+ `timeMs`, `costUsd`, `llmCalls`, and an optional `onExceed` of `throw`
471
+ (default), `warn`, or `abort` (DESIGN.md §4.5). Limits must be finite and
472
+ non-negative, `llmCalls` an integer, no key repeated, and at least one limit
473
+ present — `@budget onExceed=warn` declares a policy with nothing to exceed.
474
+
475
+ Rejected whole rather than partly applied, for the reason AMB-E002 rejects a
476
+ misspelled effect name.
477
+
478
+ ## AMB-I001
479
+
480
+ Contract proposal.
481
+
482
+ **Severity:** info
483
+ **Category:** effects
484
+
485
+ Emitted by `ambit init`, never by `ambit check`. A function has no `@effects`
486
+ tag and its effects were fully resolved, so the tag can be written for it
487
+ (DESIGN.md §4.1). The fix carries a concrete patch adding the tag — into the
488
+ declaration's existing JSDoc block when it has exactly one, otherwise as a new
489
+ block above it — and is `consistentWithContract: true`: adding a declaration
490
+ where there was none cannot contradict one, and the set proposed is exactly
491
+ what was observed.
492
+
493
+ Three kinds of declaration have a stable symbol id and nowhere to write a
494
+ comment: a `get`/`set` accessor, an anonymous `export default` (DESIGN.md
495
+ §4.1 (a)), and a class that writes no constructor — its construction has real
496
+ effects (property initializers, the base constructor) but no declaration site
497
+ at all. For those, `ambit init` reports the inferred effects with **no** patch
498
+ and names the `ambit.config.ts` key that would carry them; `ambit init
499
+ --config` produces that patch, appending one `contracts` entry.
500
+
501
+ The config patch is an insertion at the end of the `contracts: {` line. It is
502
+ emitted only when a config file exists with such a line: the config is loaded
503
+ by importing it, not by parsing it, so there is no brace-matched block to
504
+ append to — and creating a whole config file, with a `defineConfig` specifier
505
+ that depends on how the consumer installed Ambit, is not a patch this command
506
+ can generate safely (§5.3). Without a config file, the proposal carries no fix
507
+ and says so.
508
+
509
+ Reporting these with no fix beats either proposing a patch that changes
510
+ nothing or staying silent about effects that are real.
511
+
512
+ No proposal is made when the function's effects reached `unknown`. Declaring
513
+ `@effects pure` for a function the analysis could not resolve would convert
514
+ "could not tell" into a guarantee, which is the thing `unknown` exists to
515
+ prevent (§4.3). Those functions stay undeclared and keep appearing in
516
+ `--coverage`.
517
+
518
+ `ambit init` exits 0 regardless of how many proposals it makes: contracts left
519
+ to write are not a failed check.