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,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,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
|
+
}
|