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,117 @@
|
|
|
1
|
+
export function emptyCapabilitySet() {
|
|
2
|
+
return { capabilities: [], unknown: false };
|
|
3
|
+
}
|
|
4
|
+
export function unknownCapabilitySet() {
|
|
5
|
+
return { capabilities: [], unknown: true };
|
|
6
|
+
}
|
|
7
|
+
export function formatCapability(capability) {
|
|
8
|
+
return `${capability.resource}:${capability.action}:${capability.target}`;
|
|
9
|
+
}
|
|
10
|
+
const SEGMENT = /^[A-Za-z0-9_.*?[\]{}@/-]+$/;
|
|
11
|
+
/**
|
|
12
|
+
* `target` names a host, a filesystem path, or a command, so it is defined by
|
|
13
|
+
* exclusion rather than by an allowlist (DESIGN.md §4.4 (b)): anything but a
|
|
14
|
+
* comma — the `@capabilities` list separator — and control characters. That
|
|
15
|
+
* admits the colon of `http:get:localhost:8080` and the spaces, `+`, `~` and
|
|
16
|
+
* `%` that real paths contain.
|
|
17
|
+
*
|
|
18
|
+
* Widening only fails closed: a mistyped tag becomes a target that matches
|
|
19
|
+
* nothing, which surfaces as a denial, never as a broader grant.
|
|
20
|
+
*/
|
|
21
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: excluding control characters is the point.
|
|
22
|
+
const TARGET_SEGMENT = /^[^,\u0000-\u001f\u007f]+$/;
|
|
23
|
+
/**
|
|
24
|
+
* Parse one `<resource>:<action>:<target>` token. `undefined` when the token
|
|
25
|
+
* is not three non-empty segments — a malformed capability is rejected rather
|
|
26
|
+
* than partly honoured, on the same principle as a misspelled effect name
|
|
27
|
+
* (AMB-E002): a declaration that does not mean what it says must not be read
|
|
28
|
+
* as a narrower guarantee than the author intended.
|
|
29
|
+
*
|
|
30
|
+
* `target` may itself contain `:` (a host:port, say), so the split takes the
|
|
31
|
+
* first two separators only.
|
|
32
|
+
*/
|
|
33
|
+
export function parseCapability(token) {
|
|
34
|
+
const trimmed = token.trim();
|
|
35
|
+
const firstColon = trimmed.indexOf(":");
|
|
36
|
+
if (firstColon <= 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const secondColon = trimmed.indexOf(":", firstColon + 1);
|
|
39
|
+
if (secondColon <= firstColon + 1)
|
|
40
|
+
return undefined;
|
|
41
|
+
const resource = trimmed.slice(0, firstColon);
|
|
42
|
+
const action = trimmed.slice(firstColon + 1, secondColon);
|
|
43
|
+
const target = trimmed.slice(secondColon + 1);
|
|
44
|
+
if (target.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!SEGMENT.test(resource) || !SEGMENT.test(action) || !TARGET_SEGMENT.test(target)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
// A glob belongs to `target` only; a `*` elsewhere would read as a
|
|
50
|
+
// restriction while meaning the opposite.
|
|
51
|
+
if (resource.includes("*") || action.includes("*"))
|
|
52
|
+
return undefined;
|
|
53
|
+
return { resource, action, target };
|
|
54
|
+
}
|
|
55
|
+
/** Parse a `@capabilities` tag's comma-separated list. `undefined` if any token is malformed. */
|
|
56
|
+
export function parseCapabilitiesTag(text) {
|
|
57
|
+
const trimmed = text.trim();
|
|
58
|
+
if (trimmed.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
const capabilities = [];
|
|
61
|
+
for (const token of trimmed.split(",")) {
|
|
62
|
+
const capability = parseCapability(token);
|
|
63
|
+
if (!capability)
|
|
64
|
+
return undefined;
|
|
65
|
+
capabilities.push(capability);
|
|
66
|
+
}
|
|
67
|
+
return { capabilities, unknown: false };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether `granted` permits `required` (DESIGN.md §4.4: capabilities may only
|
|
71
|
+
* narrow from caller to callee). `resource` and `action` must match exactly;
|
|
72
|
+
* `granted.target` is matched as a glob against `required.target`.
|
|
73
|
+
*/
|
|
74
|
+
export function capabilityCovers(granted, required) {
|
|
75
|
+
if (granted.resource !== required.resource)
|
|
76
|
+
return false;
|
|
77
|
+
if (granted.action !== required.action)
|
|
78
|
+
return false;
|
|
79
|
+
return globMatches(granted.target, required.target);
|
|
80
|
+
}
|
|
81
|
+
/** Capabilities in `required` that no capability in `granted` permits. */
|
|
82
|
+
export function excessCapabilities(granted, required) {
|
|
83
|
+
return required.filter((capability) => !granted.some((grant) => capabilityCovers(grant, capability)));
|
|
84
|
+
}
|
|
85
|
+
export function unionCapabilitySets(a, b) {
|
|
86
|
+
const merged = [...a.capabilities];
|
|
87
|
+
for (const capability of b.capabilities) {
|
|
88
|
+
if (!merged.some((existing) => capabilitiesEqual(existing, capability)))
|
|
89
|
+
merged.push(capability);
|
|
90
|
+
}
|
|
91
|
+
return { capabilities: merged, unknown: a.unknown || b.unknown };
|
|
92
|
+
}
|
|
93
|
+
export function capabilitiesEqual(a, b) {
|
|
94
|
+
return a.resource === b.resource && a.action === b.action && a.target === b.target;
|
|
95
|
+
}
|
|
96
|
+
export function capabilitySetsEqual(a, b) {
|
|
97
|
+
if (a.unknown !== b.unknown)
|
|
98
|
+
return false;
|
|
99
|
+
if (a.capabilities.length !== b.capabilities.length)
|
|
100
|
+
return false;
|
|
101
|
+
return a.capabilities.every((capability) => b.capabilities.some((other) => capabilitiesEqual(capability, other)));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `*` matches any run of characters, `?` exactly one — the shell-glob subset
|
|
105
|
+
* §4.4's `http:get:*.example.com` example needs. Everything else is literal,
|
|
106
|
+
* so a target containing regex metacharacters (`api.example.com`'s dots) is
|
|
107
|
+
* matched as written rather than as a pattern.
|
|
108
|
+
*/
|
|
109
|
+
function globMatches(pattern, value) {
|
|
110
|
+
if (pattern === value)
|
|
111
|
+
return true;
|
|
112
|
+
if (!pattern.includes("*") && !pattern.includes("?"))
|
|
113
|
+
return false;
|
|
114
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
115
|
+
const source = `^${escaped.replaceAll("*", "[\\s\\S]*").replaceAll("?", "[\\s\\S]")}$`;
|
|
116
|
+
return new RegExp(source).test(value);
|
|
117
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of `ambit.config.ts` (DESIGN.md §4.1, "Out-of-code declarations").
|
|
3
|
+
*
|
|
4
|
+
* Types only — no loader, no filesystem, no compiler. This module is what
|
|
5
|
+
* `ambit-ts/config` exports, so a consumer's config file gets the same type
|
|
6
|
+
* checking a JSDoc contract gets from the checker, without pulling the
|
|
7
|
+
* checker (or `typescript`) into their project's type graph.
|
|
8
|
+
*/
|
|
9
|
+
import type { BudgetInput } from "./budget.ts";
|
|
10
|
+
/**
|
|
11
|
+
* One symbol's contract, written where the code cannot be touched.
|
|
12
|
+
*
|
|
13
|
+
* The five keys are the five JSDoc contract tags (§4.1), and they mean the
|
|
14
|
+
* same thing here as there — a config declaration is not a weaker kind of
|
|
15
|
+
* declaration, it is the same declaration written somewhere else.
|
|
16
|
+
*
|
|
17
|
+
* `effects` accepts a user-defined name from {@link AmbitConfig.effects}
|
|
18
|
+
* alongside the standard ones; the loader expands it before anything else
|
|
19
|
+
* sees it (§4.1 (d)).
|
|
20
|
+
*/
|
|
21
|
+
export interface ConfigContract {
|
|
22
|
+
readonly effects?: readonly string[];
|
|
23
|
+
readonly capabilities?: readonly string[];
|
|
24
|
+
/** The `@budget` tag's fields, as an object rather than as tag text. */
|
|
25
|
+
readonly budget?: BudgetInput;
|
|
26
|
+
readonly entrypoint?: boolean;
|
|
27
|
+
/** `@boundary reason="…"`'s reason. §4.6 makes it mandatory, so there is no bare `true`. */
|
|
28
|
+
readonly boundary?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface AmbitConfig {
|
|
31
|
+
/**
|
|
32
|
+
* User-defined effect names, each a combination of standard effects
|
|
33
|
+
* (DESIGN.md §4.2: "User-defined effects can be declared in
|
|
34
|
+
* `ambit.config.ts` as combinations of standard effects"). Usable from both
|
|
35
|
+
* `@effects` and {@link ConfigContract.effects}. Values are standard effect names
|
|
36
|
+
* only: a definition never expands into another definition.
|
|
37
|
+
*/
|
|
38
|
+
readonly effects?: Readonly<Record<string, readonly string[]>>;
|
|
39
|
+
/**
|
|
40
|
+
* Contracts keyed by `"<file>#<symbol>"`. `<file>` is relative to this
|
|
41
|
+
* config file's directory and may use `*` and `**`; `<symbol>` is the
|
|
42
|
+
* declaration path and may not (§4.1 (a), (b)).
|
|
43
|
+
*/
|
|
44
|
+
readonly contracts?: Readonly<Record<string, ConfigContract>>;
|
|
45
|
+
/**
|
|
46
|
+
* File globs — same syntax as a key's `<file>` half — whose diagnostics get
|
|
47
|
+
* the promotion `--strict` applies, and only theirs (§4.3: "`strict` can be
|
|
48
|
+
* set per directory in `ambit.config.ts`").
|
|
49
|
+
*/
|
|
50
|
+
readonly strict?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Identity. It exists for the type checking and completion a consumer gets
|
|
54
|
+
* from writing `defineConfig({ … })`, and for nothing else — so a config that
|
|
55
|
+
* exports a bare object literal is exactly as valid, which is what the
|
|
56
|
+
* in-repo fixtures rely on (they cannot resolve `ambit-ts/config` from a scratch
|
|
57
|
+
* directory).
|
|
58
|
+
*/
|
|
59
|
+
export declare function defineConfig(config: AmbitConfig): AmbitConfig;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity. It exists for the type checking and completion a consumer gets
|
|
3
|
+
* from writing `defineConfig({ … })`, and for nothing else — so a config that
|
|
4
|
+
* exports a bare object literal is exactly as valid, which is what the
|
|
5
|
+
* in-repo fixtures rely on (they cannot resolve `ambit-ts/config` from a scratch
|
|
6
|
+
* directory).
|
|
7
|
+
*/
|
|
8
|
+
export function defineConfig(config) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { KnownEffect } from "./effects.ts";
|
|
2
|
+
import type { SourceLocation } from "./location.ts";
|
|
3
|
+
import type { SymbolId } from "./symbol-id.ts";
|
|
4
|
+
export type Severity = "error" | "warning" | "info";
|
|
5
|
+
export type DiagnosticCategory = "effects" | "capabilities" | "budget" | "boundary" | "types";
|
|
6
|
+
/** One step of the call path from a declaring function to where an effect was found. */
|
|
7
|
+
export interface ContractViaEntry {
|
|
8
|
+
readonly symbol: SymbolId;
|
|
9
|
+
readonly file: string;
|
|
10
|
+
readonly line: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Where an effect is actually performed: the operation's own call site, inside
|
|
14
|
+
* the last function of `via` (or inside the reported function itself, when
|
|
15
|
+
* `via` is empty).
|
|
16
|
+
*
|
|
17
|
+
* `via` names functions, and a function's location is its declaration — not
|
|
18
|
+
* the `fetch(...)` or `readFileSync(...)` line inside it. This field carries
|
|
19
|
+
* that line, so a reader of a diagnostic never has to open the file to find
|
|
20
|
+
* the operation the contract was broken by.
|
|
21
|
+
*
|
|
22
|
+
* `qualifiedName` is the operation as the stub tables name it (`fetch`,
|
|
23
|
+
* `node:fs.readFileSync`), in `StubCall.qualifiedName`'s module-specifier
|
|
24
|
+
* namespace.
|
|
25
|
+
*/
|
|
26
|
+
export interface ContractOperation {
|
|
27
|
+
readonly qualifiedName: string;
|
|
28
|
+
readonly file: string;
|
|
29
|
+
readonly line: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* DESIGN.md §5.1 `contract` field, for the `effects` category.
|
|
33
|
+
*
|
|
34
|
+
* `declared` uses `"pure"` as a literal spelling for the declared empty set
|
|
35
|
+
* (matching the §5.1 example: `"declared": ["pure"]`), rather than `[]` —
|
|
36
|
+
* `EffectSet`'s internal representation collapses `pure` to an empty
|
|
37
|
+
* `effects` set (DESIGN.md §4.2 rule 2), but that internal choice should not
|
|
38
|
+
* leak into what a human or an agent reads back from the diagnostic.
|
|
39
|
+
*/
|
|
40
|
+
export interface EffectsContract {
|
|
41
|
+
readonly declared: readonly (KnownEffect | "pure")[];
|
|
42
|
+
readonly observed: readonly KnownEffect[];
|
|
43
|
+
readonly via: readonly ContractViaEntry[];
|
|
44
|
+
/**
|
|
45
|
+
* Absent when the operation site is not known: the effect came from a
|
|
46
|
+
* callee's `@effects` declaration with no matching operation in its body, or
|
|
47
|
+
* from a mutation, which has no operation to name. Never synthesized — an
|
|
48
|
+
* unknown site stays absent rather than falling back to the function's
|
|
49
|
+
* declaration line (DESIGN.md §5.3).
|
|
50
|
+
*/
|
|
51
|
+
readonly operation?: ContractOperation;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* DESIGN.md §5.1 `contract` field for the `capabilities` category. Same
|
|
55
|
+
* declared/observed/via shape as {@link EffectsContract}; `required` is the
|
|
56
|
+
* capability set the body actually needs, and `excess` narrows it to the ones
|
|
57
|
+
* the declaration does not grant (§4.4: capabilities may only narrow from
|
|
58
|
+
* caller to callee).
|
|
59
|
+
*/
|
|
60
|
+
export interface CapabilitiesContract {
|
|
61
|
+
readonly declared: readonly string[];
|
|
62
|
+
readonly required: readonly string[];
|
|
63
|
+
readonly excess: readonly string[];
|
|
64
|
+
readonly via: readonly ContractViaEntry[];
|
|
65
|
+
}
|
|
66
|
+
export type DiagnosticContract = EffectsContract | CapabilitiesContract;
|
|
67
|
+
/**
|
|
68
|
+
* Narrow a diagnostic's `contract` to the effects shape. The union carries no
|
|
69
|
+
* discriminant field on purpose: DESIGN.md §5.1 fixes the wire shape of a
|
|
70
|
+
* `contract`, and an extra key invented for TypeScript's convenience would be
|
|
71
|
+
* a schema change nobody asked for. `category` already tells a consumer which
|
|
72
|
+
* shape to expect; this is the in-process equivalent.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isEffectsContract(contract: DiagnosticContract): contract is EffectsContract;
|
|
75
|
+
export declare function isCapabilitiesContract(contract: DiagnosticContract): contract is CapabilitiesContract;
|
|
76
|
+
export interface FixEdit {
|
|
77
|
+
readonly file: string;
|
|
78
|
+
readonly range: readonly [readonly [number, number], readonly [number, number]];
|
|
79
|
+
readonly replacement: string;
|
|
80
|
+
}
|
|
81
|
+
export interface FixImpact {
|
|
82
|
+
readonly callersAffected?: readonly SymbolId[];
|
|
83
|
+
readonly pureCallersBroken?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A fix candidate. `edits` must be a concrete, applicable patch — never a
|
|
87
|
+
* summary-only or elided candidate (DESIGN.md §5.3). `consistentWithContract`
|
|
88
|
+
* separates a fix that preserves the declared contract from one that widens
|
|
89
|
+
* it (§5.2).
|
|
90
|
+
*/
|
|
91
|
+
export interface DiagnosticFix {
|
|
92
|
+
readonly rank: number;
|
|
93
|
+
readonly kind: "widen" | "narrow";
|
|
94
|
+
readonly summary: string;
|
|
95
|
+
readonly confidence: number;
|
|
96
|
+
readonly consistentWithContract: boolean;
|
|
97
|
+
readonly edits: readonly FixEdit[];
|
|
98
|
+
readonly impact?: FixImpact;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The analysis backend that produced a diagnostic (DESIGN.md §3.4: "Give
|
|
102
|
+
* diagnostics, coverage, and performance records information that identifies
|
|
103
|
+
* the analysis engine and its version").
|
|
104
|
+
* Only the engine identity is captured here; the schema version and Ambit's
|
|
105
|
+
* own version that §5.2 groups alongside it are not yet defined.
|
|
106
|
+
*/
|
|
107
|
+
export interface DiagnosticEngine {
|
|
108
|
+
readonly name: string;
|
|
109
|
+
readonly version: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* One diagnostic, matching the shape in DESIGN.md §5.1. `ambit check
|
|
113
|
+
* --format json` emits one of these per line (NDJSON).
|
|
114
|
+
*/
|
|
115
|
+
export interface Diagnostic {
|
|
116
|
+
readonly id: string;
|
|
117
|
+
readonly severity: Severity;
|
|
118
|
+
readonly category: DiagnosticCategory;
|
|
119
|
+
readonly message: string;
|
|
120
|
+
readonly location: SourceLocation;
|
|
121
|
+
readonly contract?: DiagnosticContract;
|
|
122
|
+
/** Empty when no concrete, applicable patch could be generated (§5.3). */
|
|
123
|
+
readonly fixes: readonly DiagnosticFix[];
|
|
124
|
+
readonly docs?: string;
|
|
125
|
+
readonly engine: DiagnosticEngine;
|
|
126
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow a diagnostic's `contract` to the effects shape. The union carries no
|
|
3
|
+
* discriminant field on purpose: DESIGN.md §5.1 fixes the wire shape of a
|
|
4
|
+
* `contract`, and an extra key invented for TypeScript's convenience would be
|
|
5
|
+
* a schema change nobody asked for. `category` already tells a consumer which
|
|
6
|
+
* shape to expect; this is the in-process equivalent.
|
|
7
|
+
*/
|
|
8
|
+
export function isEffectsContract(contract) {
|
|
9
|
+
return "observed" in contract;
|
|
10
|
+
}
|
|
11
|
+
export function isCapabilitiesContract(contract) {
|
|
12
|
+
return "required" in contract;
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effects a function can have on the outside world (DESIGN.md §4.2).
|
|
3
|
+
*
|
|
4
|
+
* `pure` is not a member of this list — it is the empty {@link EffectSet}
|
|
5
|
+
* (§4.2 rule 2: "`pure` is another name for the empty set"). `unknown` is
|
|
6
|
+
* likewise not a member; it is
|
|
7
|
+
* a separate flag on {@link EffectSet}, because "this function might have
|
|
8
|
+
* more effects than listed" and "this function definitely has these
|
|
9
|
+
* effects" are different guarantees and must not collapse into one value.
|
|
10
|
+
*/
|
|
11
|
+
export declare const KNOWN_EFFECTS: readonly ["network", "db_read", "db_write", "fs_read", "fs_write", "state_write", "llm", "env", "process"];
|
|
12
|
+
export type KnownEffect = (typeof KNOWN_EFFECTS)[number];
|
|
13
|
+
export declare function isKnownEffect(value: string): value is KnownEffect;
|
|
14
|
+
/**
|
|
15
|
+
* The effects a function has, plus whether the set might be incomplete.
|
|
16
|
+
*
|
|
17
|
+
* `unknown: true` means propagation reached a call that could not be
|
|
18
|
+
* resolved (DESIGN.md §4.2 rule 6) — the function's true effect set could
|
|
19
|
+
* include anything. It is tracked separately from `effects` so a diagnostic
|
|
20
|
+
* can distinguish "declares pure but definitely does X" (§AMB-E001) from
|
|
21
|
+
* "declares pure but calls something unanalyzable" (§AMB-W001).
|
|
22
|
+
*/
|
|
23
|
+
export interface EffectSet {
|
|
24
|
+
readonly effects: ReadonlySet<KnownEffect>;
|
|
25
|
+
readonly unknown: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function emptyEffectSet(): EffectSet;
|
|
28
|
+
export declare function unknownEffectSet(): EffectSet;
|
|
29
|
+
export declare function effectSetOf(...effects: readonly KnownEffect[]): EffectSet;
|
|
30
|
+
/**
|
|
31
|
+
* `llm` implies `network` (DESIGN.md §4.2: "`llm` ... Implies `network`"):
|
|
32
|
+
* expand a raw effect set so the containment relation always holds, no
|
|
33
|
+
* matter where the set was built.
|
|
34
|
+
*/
|
|
35
|
+
export declare function withImpliedEffects(set: EffectSet): EffectSet;
|
|
36
|
+
export declare function unionEffectSets(a: EffectSet, b: EffectSet): EffectSet;
|
|
37
|
+
export declare function effectSetsEqual(a: EffectSet, b: EffectSet): boolean;
|
|
38
|
+
/** Effects present in `observed` but not covered by `declared` (§4.2 rule 1). */
|
|
39
|
+
export declare function excessEffects(declared: EffectSet, observed: EffectSet): ReadonlySet<KnownEffect>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effects a function can have on the outside world (DESIGN.md §4.2).
|
|
3
|
+
*
|
|
4
|
+
* `pure` is not a member of this list — it is the empty {@link EffectSet}
|
|
5
|
+
* (§4.2 rule 2: "`pure` is another name for the empty set"). `unknown` is
|
|
6
|
+
* likewise not a member; it is
|
|
7
|
+
* a separate flag on {@link EffectSet}, because "this function might have
|
|
8
|
+
* more effects than listed" and "this function definitely has these
|
|
9
|
+
* effects" are different guarantees and must not collapse into one value.
|
|
10
|
+
*/
|
|
11
|
+
export const KNOWN_EFFECTS = [
|
|
12
|
+
"network",
|
|
13
|
+
"db_read",
|
|
14
|
+
"db_write",
|
|
15
|
+
"fs_read",
|
|
16
|
+
"fs_write",
|
|
17
|
+
// Mutation of a value reachable from outside the function (DESIGN.md
|
|
18
|
+
// §4.2, "Local mutation and `pure`"). Mutating a value the function itself
|
|
19
|
+
// allocated is not this effect — it is not observable to a caller.
|
|
20
|
+
"state_write",
|
|
21
|
+
"llm",
|
|
22
|
+
"env",
|
|
23
|
+
"process",
|
|
24
|
+
];
|
|
25
|
+
export function isKnownEffect(value) {
|
|
26
|
+
return KNOWN_EFFECTS.includes(value);
|
|
27
|
+
}
|
|
28
|
+
export function emptyEffectSet() {
|
|
29
|
+
return { effects: new Set(), unknown: false };
|
|
30
|
+
}
|
|
31
|
+
export function unknownEffectSet() {
|
|
32
|
+
return { effects: new Set(), unknown: true };
|
|
33
|
+
}
|
|
34
|
+
export function effectSetOf(...effects) {
|
|
35
|
+
return withImpliedEffects({ effects: new Set(effects), unknown: false });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `llm` implies `network` (DESIGN.md §4.2: "`llm` ... Implies `network`"):
|
|
39
|
+
* expand a raw effect set so the containment relation always holds, no
|
|
40
|
+
* matter where the set was built.
|
|
41
|
+
*/
|
|
42
|
+
export function withImpliedEffects(set) {
|
|
43
|
+
if (!set.effects.has("llm") || set.effects.has("network"))
|
|
44
|
+
return set;
|
|
45
|
+
return { effects: new Set([...set.effects, "network"]), unknown: set.unknown };
|
|
46
|
+
}
|
|
47
|
+
export function unionEffectSets(a, b) {
|
|
48
|
+
return withImpliedEffects({
|
|
49
|
+
effects: new Set([...a.effects, ...b.effects]),
|
|
50
|
+
unknown: a.unknown || b.unknown,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function effectSetsEqual(a, b) {
|
|
54
|
+
if (a.unknown !== b.unknown)
|
|
55
|
+
return false;
|
|
56
|
+
if (a.effects.size !== b.effects.size)
|
|
57
|
+
return false;
|
|
58
|
+
for (const effect of a.effects) {
|
|
59
|
+
if (!b.effects.has(effect))
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/** Effects present in `observed` but not covered by `declared` (§4.2 rule 1). */
|
|
65
|
+
export function excessEffects(declared, observed) {
|
|
66
|
+
const excess = new Set();
|
|
67
|
+
for (const effect of observed.effects) {
|
|
68
|
+
if (!declared.effects.has(effect))
|
|
69
|
+
excess.add(effect);
|
|
70
|
+
}
|
|
71
|
+
return excess;
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./approvals.ts";
|
|
2
|
+
export * from "./authority.ts";
|
|
3
|
+
export * from "./authority-diff.ts";
|
|
4
|
+
export * from "./backend.ts";
|
|
5
|
+
export * from "./budget.ts";
|
|
6
|
+
export * from "./capability.ts";
|
|
7
|
+
export * from "./config.ts";
|
|
8
|
+
export * from "./diagnostic.ts";
|
|
9
|
+
export * from "./effects.ts";
|
|
10
|
+
export * from "./location.ts";
|
|
11
|
+
export * from "./sql.ts";
|
|
12
|
+
export * from "./summary.ts";
|
|
13
|
+
export * from "./symbol-id.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./approvals.js";
|
|
2
|
+
export * from "./authority.js";
|
|
3
|
+
export * from "./authority-diff.js";
|
|
4
|
+
export * from "./backend.js";
|
|
5
|
+
export * from "./budget.js";
|
|
6
|
+
export * from "./capability.js";
|
|
7
|
+
export * from "./config.js";
|
|
8
|
+
export * from "./diagnostic.js";
|
|
9
|
+
export * from "./effects.js";
|
|
10
|
+
export * from "./location.js";
|
|
11
|
+
export * from "./sql.js";
|
|
12
|
+
export * from "./summary.js";
|
|
13
|
+
export * from "./symbol-id.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A source position, always relative to the project root that was passed to
|
|
3
|
+
* `ambit check`. Never a compiler-internal position or snapshot offset
|
|
4
|
+
* (DESIGN.md §5.3).
|
|
5
|
+
*
|
|
6
|
+
* `line`/`col` are 1-based (human-facing position); `endLine`/`endCol` mark
|
|
7
|
+
* the end of the range, exclusive, also 1-based to match `line`/`col`.
|
|
8
|
+
*/
|
|
9
|
+
export interface SourceLocation {
|
|
10
|
+
readonly file: string;
|
|
11
|
+
readonly line: number;
|
|
12
|
+
readonly col: number;
|
|
13
|
+
readonly endLine: number;
|
|
14
|
+
readonly endCol: number;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The direction of one SQL statement, read from its leading keyword.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `src/core/` because DESIGN.md §4.4 requires the static side
|
|
5
|
+
* (`src/stubs/data-clients.ts`, which turns the direction into `db_read` /
|
|
6
|
+
* `db_write` effects) and the runtime `pg` hook (which turns it into a
|
|
7
|
+
* `db:read:` / `db:write:` capability) to use *the same rule*. Two copies
|
|
8
|
+
* would let the checker and the running process give different answers about
|
|
9
|
+
* the same statement, which is the worst outcome available here.
|
|
10
|
+
*/
|
|
11
|
+
export type SqlDirection = "read" | "write" | "both";
|
|
12
|
+
/**
|
|
13
|
+
* `"both"` — not a guess at the likelier direction — whenever the leading
|
|
14
|
+
* keyword is not present or not recognised. Picking `read` would let a
|
|
15
|
+
* generated `UPDATE` pass a read-only contract, which is the failure this
|
|
16
|
+
* layer exists to prevent (DESIGN.md §3.4).
|
|
17
|
+
*
|
|
18
|
+
* A template literal's static head is enough for the static side: the keyword
|
|
19
|
+
* that decides the direction is written before any substitution, or it is not
|
|
20
|
+
* statically present at all — and then the answer is `"both"`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sqlStatementDirection(text: string | undefined): SqlDirection;
|
package/dist/core/sql.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Leading SQL keywords that only read. `WITH` is deliberately absent: a CTE can wrap an `INSERT`. */
|
|
2
|
+
const READ_KEYWORDS = new Set(["select", "show", "explain", "describe"]);
|
|
3
|
+
const WRITE_KEYWORDS = new Set([
|
|
4
|
+
"insert",
|
|
5
|
+
"update",
|
|
6
|
+
"delete",
|
|
7
|
+
"replace",
|
|
8
|
+
"merge",
|
|
9
|
+
"upsert",
|
|
10
|
+
"create",
|
|
11
|
+
"drop",
|
|
12
|
+
"alter",
|
|
13
|
+
"truncate",
|
|
14
|
+
"grant",
|
|
15
|
+
"revoke",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* `"both"` — not a guess at the likelier direction — whenever the leading
|
|
19
|
+
* keyword is not present or not recognised. Picking `read` would let a
|
|
20
|
+
* generated `UPDATE` pass a read-only contract, which is the failure this
|
|
21
|
+
* layer exists to prevent (DESIGN.md §3.4).
|
|
22
|
+
*
|
|
23
|
+
* A template literal's static head is enough for the static side: the keyword
|
|
24
|
+
* that decides the direction is written before any substitution, or it is not
|
|
25
|
+
* statically present at all — and then the answer is `"both"`.
|
|
26
|
+
*/
|
|
27
|
+
export function sqlStatementDirection(text) {
|
|
28
|
+
if (text === undefined)
|
|
29
|
+
return "both";
|
|
30
|
+
const keyword = /^[\s(]*([a-z]+)/i.exec(text)?.[1]?.toLowerCase();
|
|
31
|
+
if (keyword === undefined)
|
|
32
|
+
return "both";
|
|
33
|
+
if (READ_KEYWORDS.has(keyword))
|
|
34
|
+
return "read";
|
|
35
|
+
if (WRITE_KEYWORDS.has(keyword))
|
|
36
|
+
return "write";
|
|
37
|
+
return "both";
|
|
38
|
+
}
|