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,70 @@
1
+ const HTTP_CAPABILITY_RULES = new Map([
2
+ ["fetch", { urlArgument: 0, optionsArgument: 1, defaultAction: "get" }],
3
+ ["globalThis.fetch", { urlArgument: 0, optionsArgument: 1, defaultAction: "get" }],
4
+ ["undici.fetch", { urlArgument: 0, optionsArgument: 1, defaultAction: "get" }],
5
+ ["node:http.get", { urlArgument: 0, defaultAction: "get" }],
6
+ ["node:https.get", { urlArgument: 0, defaultAction: "get" }],
7
+ ["node:http.request", { urlArgument: 0, optionsArgument: 1, defaultAction: "get" }],
8
+ ["node:https.request", { urlArgument: 0, optionsArgument: 1, defaultAction: "get" }],
9
+ ]);
10
+ /**
11
+ * The capability requirement of one call, or `undefined` when this table knows
12
+ * of no target for it — which is not the same as "requires nothing": every
13
+ * other operation simply has no rule here yet (`node:fs`, DB clients, LLM
14
+ * SDKs), and §4.4's caveat forbids inferring one for a DB client from a SQL
15
+ * string.
16
+ */
17
+ export function lookupHttpCapability(qualifiedName, literalArguments) {
18
+ const rule = HTTP_CAPABILITY_RULES.get(qualifiedName);
19
+ if (!rule)
20
+ return undefined;
21
+ const host = hostOf(literalArguments?.[rule.urlArgument]);
22
+ if (host === undefined)
23
+ return { targetUnknown: true };
24
+ const options = rule.optionsArgument === undefined ? undefined : literalArguments?.[rule.optionsArgument];
25
+ const method = options?.properties?.get("method");
26
+ return {
27
+ capability: {
28
+ resource: "http",
29
+ action: (method ?? rule.defaultAction).toLowerCase(),
30
+ target: host,
31
+ },
32
+ };
33
+ }
34
+ /**
35
+ * The host a URL argument fixes, or `undefined` when the source does not fix
36
+ * one.
37
+ *
38
+ * A template literal's static head is enough *only if the authority ends
39
+ * inside it*: `` `https://api.example.com/rates/${c}` `` fixes
40
+ * `api.example.com`, while `` `https://api.${env}.example.com/` `` fixes
41
+ * nothing and must not be read as `api.` — a prefix match would be a target
42
+ * the author never granted.
43
+ *
44
+ * The host is taken as written (minus userinfo), port included: §4.4's own
45
+ * example spells a target `http:get:localhost:8080`, and inventing or dropping
46
+ * a default port would make a grant and a requirement disagree over text
47
+ * neither one wrote.
48
+ */
49
+ function hostOf(argument) {
50
+ const text = argument?.text;
51
+ if (text === undefined)
52
+ return undefined;
53
+ const schemeEnd = text.indexOf("://");
54
+ // A relative URL ("/v1/rates") names no host; the host comes from wherever
55
+ // the request is sent, which is not in this call.
56
+ if (schemeEnd < 0)
57
+ return undefined;
58
+ const rest = text.slice(schemeEnd + 3);
59
+ const terminator = rest.search(/[/?#]/);
60
+ if (terminator < 0) {
61
+ // The authority runs to the end of the text we have. Only a complete
62
+ // literal proves nothing more follows.
63
+ return argument?.complete === true ? normalizeAuthority(rest) : undefined;
64
+ }
65
+ return normalizeAuthority(rest.slice(0, terminator));
66
+ }
67
+ function normalizeAuthority(authority) {
68
+ const afterUserInfo = authority.slice(authority.lastIndexOf("@") + 1);
69
+ return afterUserInfo.length === 0 ? undefined : afterUserInfo.toLowerCase();
70
+ }
@@ -0,0 +1 @@
1
+ export declare function isMutatingBuiltin(qualifiedName: string): boolean;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Allowlist of default-lib (builtin) methods that change their receiver in
3
+ * place. Keyed the same way as `src/stubs/pure-builtins.ts` — `checker.
4
+ * getFullyQualifiedName()`'s format (e.g. `"Array.push"`, `"Map.set"`) — and
5
+ * kept in a separate table from it because the two answer different
6
+ * questions: that one says "no `KnownEffect` at all", this one says "a
7
+ * `state_write` if the receiver is reachable from outside the function"
8
+ * (DESIGN.md §4.2, "Local mutation and `pure`").
9
+ *
10
+ * Whether a listed call actually produces `state_write` is decided per call
11
+ * site by the receiver's locality, not by this table: mutating a value the
12
+ * function itself allocated is not observable outside it, so it carries no
13
+ * effect. The locality rule lives in `src/checker/backend/legacy-ts.ts`
14
+ * (`isLocallyOwnedMutationTarget`).
15
+ *
16
+ * Admission rule, same as `pure-builtins.ts`: names that `ambit check
17
+ * --coverage`'s `top-unresolved-names` actually surfaced, plus the in-place
18
+ * siblings on the same builtin type — a table that listed `push` but not
19
+ * `pop` would mislead the next reader. When unsure, leave a name out; it
20
+ * falls back to `unknown`, which over-approximates rather than under-.
21
+ */
22
+ const MUTATING_BUILTINS = new Set([
23
+ // Surfaced by `check src --coverage` on 2026-09-09: Array.push=50,
24
+ // Map.set=13, Set.add=6 — together 69 of the 131 `builtin-method`
25
+ // unresolved calls, which is what made this table worth having.
26
+ "Array.push",
27
+ "Array.pop",
28
+ "Array.shift",
29
+ "Array.unshift",
30
+ "Array.splice",
31
+ "Array.sort",
32
+ "Array.reverse",
33
+ "Array.fill",
34
+ "Array.copyWithin",
35
+ "Map.set",
36
+ "Map.delete",
37
+ "Map.clear",
38
+ "Set.add",
39
+ "Set.delete",
40
+ "Set.clear",
41
+ "WeakMap.set",
42
+ "WeakMap.delete",
43
+ "WeakSet.add",
44
+ "WeakSet.delete",
45
+ ]);
46
+ export function isMutatingBuiltin(qualifiedName) {
47
+ return MUTATING_BUILTINS.has(qualifiedName);
48
+ }
@@ -0,0 +1,2 @@
1
+ import type { KnownEffect } from "../core/index.ts";
2
+ export declare function lookupStubEffect(qualifiedName: string): KnownEffect | undefined;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Minimal effect table for common globals, Node.js builtins, and a handful
3
+ * of widely-used third-party packages (DESIGN.md §4.2, "Evidence for effect
4
+ * detection", lists both as legitimate sources — the same table, not a
5
+ * separate one).
6
+ * Matching is purely textual against the connector layer's best-effort
7
+ * `calleeQualifiedName` (see `src/core/backend.ts`) — a call written as
8
+ * `import * as f from "node:fs"; f.readFileSync(...)`, `import f from
9
+ * "node:fs"; f.readFileSync(...)`, or `import { readFileSync } from
10
+ * "node:fs"; readFileSync(...)` are all recognized: the connector layer
11
+ * reports the qualified name from the *module specifier text* (`"node:fs"`)
12
+ * plus the imported property/export name, not the local binding name, so a
13
+ * local `as` alias doesn't affect matching. This is still a known
14
+ * simplification for this slice: a destructured or re-exported binding
15
+ * several hops away (e.g. through a barrel file) is not resolved to its
16
+ * originating module specifier.
17
+ *
18
+ * `docs/diagnostics/` and DESIGN.md §8 track stub trust levels; this table
19
+ * is bundled with Ambit itself, the highest trust level.
20
+ */
21
+ const STUB_EFFECTS = new Map([
22
+ ["fetch", "network"],
23
+ ["globalThis.fetch", "network"],
24
+ ["undici.fetch", "network"],
25
+ ["node:http.request", "network"],
26
+ ["node:http.get", "network"],
27
+ ["node:https.request", "network"],
28
+ ["node:https.get", "network"],
29
+ ["node:net.connect", "network"],
30
+ ["node:net.createConnection", "network"],
31
+ ["node:fs.readFile", "fs_read"],
32
+ ["node:fs.readdir", "fs_read"],
33
+ ["node:fs.readdirSync", "fs_read"],
34
+ ["node:fs.realpath", "fs_read"],
35
+ ["node:fs.realpathSync", "fs_read"],
36
+ ["node:fs.stat", "fs_read"],
37
+ ["node:fs.lstat", "fs_read"],
38
+ ["node:fs.lstatSync", "fs_read"],
39
+ ["node:fs.access", "fs_read"],
40
+ ["node:fs.accessSync", "fs_read"],
41
+ ["node:fs.readFileSync", "fs_read"],
42
+ ["node:fs.existsSync", "fs_read"],
43
+ ["node:fs.statSync", "fs_read"],
44
+ ["node:fs.writeFile", "fs_write"],
45
+ ["node:fs.writeFileSync", "fs_write"],
46
+ ["node:fs.appendFile", "fs_write"],
47
+ ["node:fs.appendFileSync", "fs_write"],
48
+ ["node:fs.mkdir", "fs_write"],
49
+ ["node:fs.mkdirSync", "fs_write"],
50
+ ["node:fs.mkdtemp", "fs_write"],
51
+ ["node:fs.mkdtempSync", "fs_write"],
52
+ ["node:fs.rm", "fs_write"],
53
+ ["node:fs.rmSync", "fs_write"],
54
+ ["node:fs.unlink", "fs_write"],
55
+ ["node:fs.unlinkSync", "fs_write"],
56
+ ["node:fs/promises.readFile", "fs_read"],
57
+ ["node:fs/promises.readdir", "fs_read"],
58
+ ["node:fs/promises.realpath", "fs_read"],
59
+ ["node:fs/promises.stat", "fs_read"],
60
+ ["node:fs/promises.lstat", "fs_read"],
61
+ ["node:fs/promises.access", "fs_read"],
62
+ ["node:fs/promises.writeFile", "fs_write"],
63
+ ["node:fs/promises.appendFile", "fs_write"],
64
+ ["node:fs/promises.mkdir", "fs_write"],
65
+ ["node:fs/promises.mkdtemp", "fs_write"],
66
+ ["node:fs/promises.rm", "fs_write"],
67
+ ["node:fs/promises.unlink", "fs_write"],
68
+ ["node:child_process.exec", "process"],
69
+ ["node:child_process.execSync", "process"],
70
+ ["node:child_process.execFile", "process"],
71
+ ["node:child_process.spawn", "process"],
72
+ ["node:child_process.spawnSync", "process"],
73
+ ["node:child_process.fork", "process"],
74
+ ]);
75
+ export function lookupStubEffect(qualifiedName) {
76
+ return STUB_EFFECTS.get(qualifiedName);
77
+ }
@@ -0,0 +1 @@
1
+ export declare function isKnownPureBuiltin(qualifiedName: string): boolean;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Allowlist of default-lib (builtin) methods known to perform none of
3
+ * Ambit's `KnownEffect`s — no network, filesystem, process, database, LLM,
4
+ * or env access — regardless of arguments. Keyed by `checker.
5
+ * getFullyQualifiedName()`'s format (e.g. `"Set.has"`, `"Array.map"`), a
6
+ * different namespace from `src/stubs/node-builtins.ts`'s module-specifier
7
+ * keys (e.g. `"node:fs.readFileSync"`) — the two tables are never merged or
8
+ * compared (`src/core/backend.ts`'s `CallSite.pureBuiltinName` doc comment).
9
+ * In-place mutation is a separate table, `src/stubs/mutating-builtins.ts`
10
+ * (DESIGN.md §4.2, "Local mutation and `pure`"): a mutator's effect depends on
11
+ * its receiver, so it cannot be answered by a name alone the way this table
12
+ * answers. Nondeterminism beyond `env` is still outside the model; this
13
+ * allowlist claims only "no `KnownEffect`", not "pure" in a stricter sense.
14
+ *
15
+ * This exists because `qualifiedNameOf` (`src/checker/backend/legacy-ts.ts`)
16
+ * cannot produce a textual name for a builtin method reached through a local
17
+ * value (`set.has(...)`, `arr.map(...)`) — there is no import binding to read
18
+ * a module specifier from. `checker.getFullyQualifiedName()` still resolves
19
+ * a stable name from the symbol itself, so those calls are named, but a
20
+ * *name* is not a *pure* verdict: only entries listed here are trusted.
21
+ *
22
+ * A method that can take a callback (`map`, `forEach`, `filter`, `some`,
23
+ * ...) may still list a symbol reached by reference (`arr.forEach(handler)`)
24
+ * — the connector layer marks that call `callbackByReference` and
25
+ * `summarize.ts`'s `toCall` refuses to treat it as pure even if the method
26
+ * name is listed here, because an opaque callback might do anything
27
+ * (DESIGN.md §4.2 rule 4: a higher-order call's callback effects must be
28
+ * inferred from the actual argument, never treated as complete from the
29
+ * type signature alone).
30
+ *
31
+ * Populated from what `ambit check --coverage`'s `top-unresolved-names`
32
+ * actually surfaces on real code, not written ahead of evidence. When
33
+ * unsure, leave a name out — it just falls back to `unknown`, which is safe.
34
+ */
35
+ const PURE_BUILTINS = new Set([
36
+ // Confirmed against `ambit check --coverage`'s top-unresolved-names on
37
+ // Ambit's own source (2026-09-08). Mutating names — `Array.push`,
38
+ // `Array.sort` (in-place), `Map.set`, `Set.add` — belong to
39
+ // `mutating-builtins.ts` instead; a "pure" table that listed mutators
40
+ // would mislead the next reader.
41
+ "Set.has",
42
+ "Map.get",
43
+ "Map.has",
44
+ "Map.entries",
45
+ "ReadonlyMap.get",
46
+ "ReadonlyMap.has",
47
+ "ReadonlyMap.values",
48
+ "ReadonlyMap.entries",
49
+ "ReadonlySet.has",
50
+ "Array.map",
51
+ "Array.filter",
52
+ "Array.join",
53
+ "Array.slice",
54
+ "Array.reduce",
55
+ "ReadonlyArray.map",
56
+ "ReadonlyArray.filter",
57
+ "ReadonlyArray.some",
58
+ "ReadonlyArray.includes",
59
+ "ReadonlyArray.join",
60
+ "ReadonlyArray.find",
61
+ "String.trim",
62
+ "String.split",
63
+ // Surfaced at 8 occurrences by `check src --coverage` on 2026-09-09, which is
64
+ // this table's own admission rule. Non-mutating, and the exact twin of
65
+ // `Array.slice` above; listing one without the other was an accident of what
66
+ // an earlier measurement happened to show.
67
+ "String.slice",
68
+ "String.includes",
69
+ "String.startsWith",
70
+ "String.localeCompare",
71
+ "ObjectConstructor.fromEntries",
72
+ "JSON.stringify",
73
+ "Math.round",
74
+ "Number.toFixed",
75
+ // Surfaced by `check test/fixtures/realistic-api --coverage` on 2026-09-09,
76
+ // the same admission rule. Each is the exact twin of an entry already here —
77
+ // `ReadonlyArray.filter` of `Array.filter`, `ReadonlyArray.every` of
78
+ // `ReadonlyArray.some`, `Math.max` of `Math.round` — or a non-mutating
79
+ // `String` method beside `String.slice`. Listing one of a pair without the
80
+ // other was an accident of what an earlier measurement happened to show.
81
+ "ReadonlyArray.filter",
82
+ "ReadonlyArray.every",
83
+ "Math.max",
84
+ "String.toLowerCase",
85
+ "String.toUpperCase",
86
+ ]);
87
+ export function isKnownPureBuiltin(qualifiedName) {
88
+ return PURE_BUILTINS.has(qualifiedName);
89
+ }