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,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval ledger: reading `ambit.approvals.md` and deciding which
|
|
3
|
+
* authority increases it lets through (DESIGN.md §6.3).
|
|
4
|
+
*
|
|
5
|
+
* Pure, like `authority-diff.ts` beside it. Text in, decisions out — nothing
|
|
6
|
+
* here opens a file or runs git, so the whole rule that governs whether a
|
|
7
|
+
* pull request passes can be unit tested without a repository.
|
|
8
|
+
*/
|
|
9
|
+
import type { AuthorityRef } from "./authority.ts";
|
|
10
|
+
import type { AuthorityDiff, SymbolAuthorityDiff } from "./authority-diff.ts";
|
|
11
|
+
import type { SymbolId } from "./symbol-id.ts";
|
|
12
|
+
/** One parsed approval line. */
|
|
13
|
+
export interface Approval {
|
|
14
|
+
readonly symbol: SymbolId;
|
|
15
|
+
readonly authority: AuthorityRef;
|
|
16
|
+
/** Why the increase was accepted. Free text, required — an approval with no reason records nothing. */
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
/** 1-based line in the ledger, so a report can point at it. */
|
|
19
|
+
readonly line: number;
|
|
20
|
+
}
|
|
21
|
+
/** A `-` line that is not an approval. Reported, and grants nothing. */
|
|
22
|
+
export interface MalformedApprovalLine {
|
|
23
|
+
readonly line: number;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ParsedApprovals {
|
|
27
|
+
readonly approvals: readonly Approval[];
|
|
28
|
+
readonly malformed: readonly MalformedApprovalLine[];
|
|
29
|
+
}
|
|
30
|
+
/** The file name looked for, beside `ambit.config.ts` and found the same way (§6.3). */
|
|
31
|
+
export declare const APPROVALS_FILENAME = "ambit.approvals.md";
|
|
32
|
+
/**
|
|
33
|
+
* Read a ledger.
|
|
34
|
+
*
|
|
35
|
+
* Approvals are the lines whose first non-space character is `-`, below the
|
|
36
|
+
* `Approvals` heading; everything else is prose, so the file explains itself
|
|
37
|
+
* to the person reading the pull request — and can use a bullet list to do it,
|
|
38
|
+
* which is why the heading is needed at all. A file with no such heading is
|
|
39
|
+
* read as approvals throughout, so a ledger that is nothing but lines works.
|
|
40
|
+
*
|
|
41
|
+
* A `-` line inside the region that does not parse is *not* silently demoted
|
|
42
|
+
* to prose — that would turn a typo in an approval into an approval that does
|
|
43
|
+
* nothing and says nothing (DESIGN.md §3.4) — it is returned as malformed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseApprovals(text: string): ParsedApprovals;
|
|
46
|
+
/** The line to add to the ledger for one increase — what `ambit diff` prints to be copied. */
|
|
47
|
+
export declare function formatApprovalLine(symbol: SymbolId, ref: AuthorityRef, reason?: string): string;
|
|
48
|
+
/** One authority one symbol gained — the unit an approval line is written for. */
|
|
49
|
+
export interface IncreaseItem {
|
|
50
|
+
readonly entry: SymbolAuthorityDiff;
|
|
51
|
+
readonly ref: AuthorityRef;
|
|
52
|
+
}
|
|
53
|
+
export interface ApprovedIncrease extends IncreaseItem {
|
|
54
|
+
readonly approval: Approval;
|
|
55
|
+
}
|
|
56
|
+
export interface ApprovalReview {
|
|
57
|
+
/** Increases carrying an approval added in this comparison. Reported; not a failure. */
|
|
58
|
+
readonly approved: readonly ApprovedIncrease[];
|
|
59
|
+
/** Increases with nothing to approve them. This is what fails a check. */
|
|
60
|
+
readonly unapproved: readonly IncreaseItem[];
|
|
61
|
+
/**
|
|
62
|
+
* Approvals added in this comparison that matched no increase — a mistyped
|
|
63
|
+
* symbol id or authority, most often. Reported, never a failure on its own:
|
|
64
|
+
* whatever the line was meant to approve is still unapproved, and that is
|
|
65
|
+
* what fails.
|
|
66
|
+
*/
|
|
67
|
+
readonly unused: readonly Approval[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Decide, for every increase in `diff`, whether the ledger approves it.
|
|
71
|
+
*
|
|
72
|
+
* An approval is in force only in the comparison that adds it: the count of
|
|
73
|
+
* approvals for a `(symbol, authority)` pair is the head side's count minus
|
|
74
|
+
* the base side's, and the *last* that many head-side lines are the ones that
|
|
75
|
+
* count. So a line already merged grants nothing, re-approving a pair later is
|
|
76
|
+
* appending a second identical line, and deleting a line can only ever lower
|
|
77
|
+
* what is approved (DESIGN.md §6.3).
|
|
78
|
+
*
|
|
79
|
+
* Both sides' lines are read from the same ledger file, one in the working
|
|
80
|
+
* tree and one in the base checkout.
|
|
81
|
+
*/
|
|
82
|
+
export declare function reviewIncreases(diff: AuthorityDiff, base: readonly Approval[], head: readonly Approval[]): ApprovalReview;
|
|
Binary file
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AuthorityPath, AuthorityRecord, AuthorityRef } from "./authority.ts";
|
|
2
|
+
import type { SymbolId } from "./symbol-id.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a symbol exists on both sides of the comparison, only on the new
|
|
5
|
+
* side, only on the old one, or on both under different paths.
|
|
6
|
+
*
|
|
7
|
+
* A symbol id contains the file path (DESIGN.md §5.3), so a function whose
|
|
8
|
+
* file moved would be a `"deleted"` and a `"new"` symbol. `"moved"` is that
|
|
9
|
+
* pair recombined, and only ever on evidence git supplied: the caller passes
|
|
10
|
+
* the renames git reported, and nothing here guesses at identity beyond them.
|
|
11
|
+
* A guess would put a fabricated "unchanged" in front of a reader whose
|
|
12
|
+
* function may in fact have gained authority on the way (DESIGN.md §6.3).
|
|
13
|
+
*/
|
|
14
|
+
export type SymbolStatus = "present" | "new" | "deleted" | "moved";
|
|
15
|
+
/** What changed about one symbol's authority between two dumps. */
|
|
16
|
+
export interface SymbolAuthorityDiff {
|
|
17
|
+
readonly symbol: SymbolId;
|
|
18
|
+
readonly status: SymbolStatus;
|
|
19
|
+
/** For a `"moved"` symbol, the id it had on the base side. Absent otherwise. */
|
|
20
|
+
readonly movedFrom?: SymbolId;
|
|
21
|
+
/** Authority the new side has that the old side did not grant. */
|
|
22
|
+
readonly added: readonly AuthorityRef[];
|
|
23
|
+
/** Authority the old side had that the new side does not. */
|
|
24
|
+
readonly removed: readonly AuthorityRef[];
|
|
25
|
+
readonly unchanged: readonly AuthorityRef[];
|
|
26
|
+
/**
|
|
27
|
+
* The analysis reached something it could not resolve on the new side but
|
|
28
|
+
* not on the old one — the opposite for `unknownLost`.
|
|
29
|
+
*
|
|
30
|
+
* Reported, never counted as an increase: `unknown` is not authority
|
|
31
|
+
* (DESIGN.md §4.3). Reported all the same, because a range that stopped
|
|
32
|
+
* being analyzable must not come out as "nothing increased here".
|
|
33
|
+
*/
|
|
34
|
+
readonly unknownGained: boolean;
|
|
35
|
+
readonly unknownLost: boolean;
|
|
36
|
+
/** The new side's record, for rendering the path of an increase. Absent for a deleted symbol. */
|
|
37
|
+
readonly head?: AuthorityRecord;
|
|
38
|
+
/** The old side's record. Absent for a new symbol. */
|
|
39
|
+
readonly base?: AuthorityRecord;
|
|
40
|
+
}
|
|
41
|
+
export interface AuthorityDiff {
|
|
42
|
+
/** Every symbol either side knows about, sorted by symbol id. */
|
|
43
|
+
readonly symbols: readonly SymbolAuthorityDiff[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Files git reported as renamed between the two sides, base path to head
|
|
47
|
+
* path, each relative to the checked directory and separated by `"/"`.
|
|
48
|
+
*
|
|
49
|
+
* Supplied by the caller because this module runs no git (see
|
|
50
|
+
* {@link diffAuthority}); `ambit diff` reads it from
|
|
51
|
+
* `git diff --find-renames`.
|
|
52
|
+
*/
|
|
53
|
+
export type RenamedFiles = ReadonlyMap<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* Compare two authority dumps (DESIGN.md §5.1's `kind: "authority"` records).
|
|
56
|
+
*
|
|
57
|
+
* Pure: two arrays in, one result out. It never reads a file, runs git, or
|
|
58
|
+
* asks a backend anything, which is what lets the whole comparison be unit
|
|
59
|
+
* tested without a repository — `ambit diff` is only the part that produces
|
|
60
|
+
* the two arrays.
|
|
61
|
+
*
|
|
62
|
+
* What is compared is *effective* authority (see `effectiveEffects`): what a
|
|
63
|
+
* function declares, or what its body was inferred to do when it declares
|
|
64
|
+
* nothing. Widening a tag therefore reads as an increase even though the body
|
|
65
|
+
* did not move, which is the case this comparison exists for.
|
|
66
|
+
*/
|
|
67
|
+
export declare function diffAuthority(base: readonly AuthorityRecord[], head: readonly AuthorityRecord[], renames?: RenamedFiles): AuthorityDiff;
|
|
68
|
+
/**
|
|
69
|
+
* Symbols whose authority grew, and new symbols that hold any — the increases
|
|
70
|
+
* an approval is written for (DESIGN.md §6.3).
|
|
71
|
+
*
|
|
72
|
+
* A symbol that only moved is not here: it is compared against its own base
|
|
73
|
+
* record, so `added` is empty unless the move also widened something, and then
|
|
74
|
+
* only the widening is reported.
|
|
75
|
+
*/
|
|
76
|
+
export declare function authorityIncreases(diff: AuthorityDiff): readonly SymbolAuthorityDiff[];
|
|
77
|
+
/** Symbols that lost authority without disappearing. Reported; never a failure. */
|
|
78
|
+
export declare function authorityDecreases(diff: AuthorityDiff): readonly SymbolAuthorityDiff[];
|
|
79
|
+
/** Symbols carried across a file git reported as renamed. Reported; never a failure on its own. */
|
|
80
|
+
export declare function movedSymbols(diff: AuthorityDiff): readonly SymbolAuthorityDiff[];
|
|
81
|
+
/** Symbols the new side no longer has. Reported; never a failure. */
|
|
82
|
+
export declare function deletedSymbols(diff: AuthorityDiff): readonly SymbolAuthorityDiff[];
|
|
83
|
+
/** Symbols present on both sides whose authority is exactly the same. */
|
|
84
|
+
export declare function unchangedSymbols(diff: AuthorityDiff): readonly SymbolAuthorityDiff[];
|
|
85
|
+
/**
|
|
86
|
+
* Symbols whose analysis went from resolved to unresolved. Not an increase —
|
|
87
|
+
* `unknown` is not authority — but never silence either: a range that stopped
|
|
88
|
+
* being analyzable is exactly what must not be reported as "not increased".
|
|
89
|
+
*/
|
|
90
|
+
export declare function unknownGained(diff: AuthorityDiff): readonly SymbolAuthorityDiff[];
|
|
91
|
+
export declare function hasAuthorityIncrease(diff: AuthorityDiff): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The call path a record carries for one authority, if it carries one.
|
|
94
|
+
*
|
|
95
|
+
* Absent when the function declares an authority its body does not reach:
|
|
96
|
+
* there is no path to show and none is invented (DESIGN.md §5.3).
|
|
97
|
+
*/
|
|
98
|
+
export declare function pathFor(record: AuthorityRecord | undefined, ref: AuthorityRef): AuthorityPath | undefined;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { capabilityRef, effectiveCapabilities, effectiveEffects, effectRef, formatAuthorityRef, holdsAuthority, } from "./authority.js";
|
|
2
|
+
import { capabilityCovers, parseCapability } from "./capability.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compare two authority dumps (DESIGN.md §5.1's `kind: "authority"` records).
|
|
5
|
+
*
|
|
6
|
+
* Pure: two arrays in, one result out. It never reads a file, runs git, or
|
|
7
|
+
* asks a backend anything, which is what lets the whole comparison be unit
|
|
8
|
+
* tested without a repository — `ambit diff` is only the part that produces
|
|
9
|
+
* the two arrays.
|
|
10
|
+
*
|
|
11
|
+
* What is compared is *effective* authority (see `effectiveEffects`): what a
|
|
12
|
+
* function declares, or what its body was inferred to do when it declares
|
|
13
|
+
* nothing. Widening a tag therefore reads as an increase even though the body
|
|
14
|
+
* did not move, which is the case this comparison exists for.
|
|
15
|
+
*/
|
|
16
|
+
export function diffAuthority(base, head, renames = new Map()) {
|
|
17
|
+
const rebased = rebaseRenamed(base, renames);
|
|
18
|
+
const baseById = indexBySymbol(rebased.records, "base");
|
|
19
|
+
const headById = indexBySymbol(head, "head");
|
|
20
|
+
const symbols = [...new Set([...baseById.keys(), ...headById.keys()])]
|
|
21
|
+
.toSorted()
|
|
22
|
+
.map((symbol) => compareSymbol(symbol, baseById.get(symbol), headById.get(symbol), rebased.movedFrom.get(symbol)));
|
|
23
|
+
return { symbols };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Re-express the base side's symbol ids under the head side's paths, for the
|
|
27
|
+
* files git reported as renamed.
|
|
28
|
+
*
|
|
29
|
+
* This is the whole of move handling: once `old.ts#f` is called `new.ts#f`,
|
|
30
|
+
* the ordinary comparison does the rest, and a move that also widened a
|
|
31
|
+
* contract still reports exactly the widening. A remap that would collide
|
|
32
|
+
* with an id the base side already has is dropped rather than resolved —
|
|
33
|
+
* git cannot report a rename onto a path that existed on the base side, so
|
|
34
|
+
* this is unreachable, and guessing which of the two records wins is the one
|
|
35
|
+
* answer that could hide an increase.
|
|
36
|
+
*/
|
|
37
|
+
function rebaseRenamed(base, renames) {
|
|
38
|
+
if (renames.size === 0)
|
|
39
|
+
return { records: base, movedFrom: new Map() };
|
|
40
|
+
const existing = new Set(base.map((record) => record.symbol));
|
|
41
|
+
const movedFrom = new Map();
|
|
42
|
+
const records = base.map((record) => {
|
|
43
|
+
const hash = record.symbol.indexOf("#");
|
|
44
|
+
if (hash < 0)
|
|
45
|
+
return record;
|
|
46
|
+
const renamed = renames.get(record.symbol.slice(0, hash));
|
|
47
|
+
if (renamed === undefined)
|
|
48
|
+
return record;
|
|
49
|
+
const moved = `${renamed}${record.symbol.slice(hash)}`;
|
|
50
|
+
if (existing.has(moved) || movedFrom.has(moved))
|
|
51
|
+
return record;
|
|
52
|
+
movedFrom.set(moved, record.symbol);
|
|
53
|
+
return { ...record, symbol: moved };
|
|
54
|
+
});
|
|
55
|
+
return { records, movedFrom };
|
|
56
|
+
}
|
|
57
|
+
function indexBySymbol(records, side) {
|
|
58
|
+
const byId = new Map();
|
|
59
|
+
for (const record of records) {
|
|
60
|
+
// Two records for one id would make the comparison silently depend on
|
|
61
|
+
// input order. The backend contract already forbids it
|
|
62
|
+
// (test/backend.conformance.test.ts), so this is a broken input, not a
|
|
63
|
+
// case to paper over with last-write-wins.
|
|
64
|
+
if (byId.has(record.symbol)) {
|
|
65
|
+
throw new Error(`duplicate authority record for ${record.symbol} on the ${side} side`);
|
|
66
|
+
}
|
|
67
|
+
byId.set(record.symbol, record);
|
|
68
|
+
}
|
|
69
|
+
return byId;
|
|
70
|
+
}
|
|
71
|
+
function compareSymbol(symbol, base, head, movedFrom) {
|
|
72
|
+
if (head === undefined) {
|
|
73
|
+
if (base === undefined)
|
|
74
|
+
throw new Error(`no record for ${symbol} on either side`);
|
|
75
|
+
return {
|
|
76
|
+
symbol,
|
|
77
|
+
status: "deleted",
|
|
78
|
+
added: [],
|
|
79
|
+
removed: authorityOf(base),
|
|
80
|
+
unchanged: [],
|
|
81
|
+
unknownGained: false,
|
|
82
|
+
unknownLost: isUnknown(base),
|
|
83
|
+
base,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (base === undefined) {
|
|
87
|
+
return {
|
|
88
|
+
symbol,
|
|
89
|
+
status: "new",
|
|
90
|
+
added: authorityOf(head),
|
|
91
|
+
removed: [],
|
|
92
|
+
unchanged: [],
|
|
93
|
+
unknownGained: isUnknown(head),
|
|
94
|
+
unknownLost: false,
|
|
95
|
+
head,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const addedEffects = missingEffects(effectiveEffects(base), effectiveEffects(head));
|
|
99
|
+
const removedEffects = missingEffects(effectiveEffects(head), effectiveEffects(base));
|
|
100
|
+
const addedCapabilities = uncoveredCapabilities(effectiveCapabilities(base), effectiveCapabilities(head));
|
|
101
|
+
const removedCapabilities = uncoveredCapabilities(effectiveCapabilities(head), effectiveCapabilities(base));
|
|
102
|
+
const added = [...addedEffects.map(effectRef), ...addedCapabilities.map(capabilityRef)];
|
|
103
|
+
const removed = [...removedEffects.map(effectRef), ...removedCapabilities.map(capabilityRef)];
|
|
104
|
+
const addedNames = new Set(added.map(formatAuthorityRef));
|
|
105
|
+
const unchanged = authorityOf(head).filter((ref) => !addedNames.has(formatAuthorityRef(ref)));
|
|
106
|
+
return {
|
|
107
|
+
symbol,
|
|
108
|
+
status: movedFrom === undefined ? "present" : "moved",
|
|
109
|
+
...(movedFrom === undefined ? {} : { movedFrom }),
|
|
110
|
+
added,
|
|
111
|
+
removed,
|
|
112
|
+
unchanged,
|
|
113
|
+
unknownGained: isUnknown(head) && !isUnknown(base),
|
|
114
|
+
unknownLost: isUnknown(base) && !isUnknown(head),
|
|
115
|
+
head,
|
|
116
|
+
base,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function authorityOf(record) {
|
|
120
|
+
return [
|
|
121
|
+
...effectiveEffects(record).map(effectRef),
|
|
122
|
+
...effectiveCapabilities(record).map((capability) => capabilityRef(capability)),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function isUnknown(record) {
|
|
126
|
+
return record.effects.unknown || record.capabilities.unknown;
|
|
127
|
+
}
|
|
128
|
+
/** Effects in `candidate` that `reference` does not contain. Plain set difference. */
|
|
129
|
+
function missingEffects(reference, candidate) {
|
|
130
|
+
const have = new Set(reference);
|
|
131
|
+
return candidate.filter((effect) => !have.has(effect));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Capabilities in `candidate` that nothing in `reference` permits.
|
|
135
|
+
*
|
|
136
|
+
* Containment, not string equality, because `<resource>:<action>:<target>`
|
|
137
|
+
* has a glob in `target` (DESIGN.md §4.4) and the same rule that governs
|
|
138
|
+
* caller-to-callee narrowing governs this comparison. Narrowing
|
|
139
|
+
* `http:get:*` to `http:get:api.example.com` is therefore not an increase,
|
|
140
|
+
* while widening it back is: nothing in `[api.example.com]` covers `*`.
|
|
141
|
+
*
|
|
142
|
+
* A token neither side can parse falls back to exact text. It came from a
|
|
143
|
+
* dump this codebase wrote, so this is unreachable in practice; treating an
|
|
144
|
+
* unparseable token as covered would be the one wrong answer, since it would
|
|
145
|
+
* hide an increase.
|
|
146
|
+
*/
|
|
147
|
+
function uncoveredCapabilities(reference, candidate) {
|
|
148
|
+
const grants = reference.map((text) => ({ text, parsed: parseCapability(text) }));
|
|
149
|
+
return candidate.filter((text) => {
|
|
150
|
+
const required = parseCapability(text);
|
|
151
|
+
return !grants.some((grant) => grant.parsed && required ? capabilityCovers(grant.parsed, required) : grant.text === text);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Present under the same id on both sides, or under a renamed path — the two
|
|
156
|
+
* statuses whose `added` / `removed` are a real comparison rather than the
|
|
157
|
+
* whole of one side's authority.
|
|
158
|
+
*/
|
|
159
|
+
function comparable(entry) {
|
|
160
|
+
return entry.status === "present" || entry.status === "moved";
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Symbols whose authority grew, and new symbols that hold any — the increases
|
|
164
|
+
* an approval is written for (DESIGN.md §6.3).
|
|
165
|
+
*
|
|
166
|
+
* A symbol that only moved is not here: it is compared against its own base
|
|
167
|
+
* record, so `added` is empty unless the move also widened something, and then
|
|
168
|
+
* only the widening is reported.
|
|
169
|
+
*/
|
|
170
|
+
export function authorityIncreases(diff) {
|
|
171
|
+
return diff.symbols.filter((entry) => (comparable(entry) && entry.added.length > 0) ||
|
|
172
|
+
(entry.status === "new" && entry.head !== undefined && holdsAuthority(entry.head)));
|
|
173
|
+
}
|
|
174
|
+
/** Symbols that lost authority without disappearing. Reported; never a failure. */
|
|
175
|
+
export function authorityDecreases(diff) {
|
|
176
|
+
return diff.symbols.filter((entry) => comparable(entry) && entry.removed.length > 0);
|
|
177
|
+
}
|
|
178
|
+
/** Symbols carried across a file git reported as renamed. Reported; never a failure on its own. */
|
|
179
|
+
export function movedSymbols(diff) {
|
|
180
|
+
return diff.symbols.filter((entry) => entry.status === "moved");
|
|
181
|
+
}
|
|
182
|
+
/** Symbols the new side no longer has. Reported; never a failure. */
|
|
183
|
+
export function deletedSymbols(diff) {
|
|
184
|
+
return diff.symbols.filter((entry) => entry.status === "deleted");
|
|
185
|
+
}
|
|
186
|
+
/** Symbols present on both sides whose authority is exactly the same. */
|
|
187
|
+
export function unchangedSymbols(diff) {
|
|
188
|
+
return diff.symbols.filter((entry) => comparable(entry) && entry.added.length === 0 && entry.removed.length === 0);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Symbols whose analysis went from resolved to unresolved. Not an increase —
|
|
192
|
+
* `unknown` is not authority — but never silence either: a range that stopped
|
|
193
|
+
* being analyzable is exactly what must not be reported as "not increased".
|
|
194
|
+
*/
|
|
195
|
+
export function unknownGained(diff) {
|
|
196
|
+
return diff.symbols.filter((entry) => entry.unknownGained);
|
|
197
|
+
}
|
|
198
|
+
export function hasAuthorityIncrease(diff) {
|
|
199
|
+
return authorityIncreases(diff).length > 0;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The call path a record carries for one authority, if it carries one.
|
|
203
|
+
*
|
|
204
|
+
* Absent when the function declares an authority its body does not reach:
|
|
205
|
+
* there is no path to show and none is invented (DESIGN.md §5.3).
|
|
206
|
+
*/
|
|
207
|
+
export function pathFor(record, ref) {
|
|
208
|
+
return record?.paths.find((path) => path.kind === ref.kind && path.authority === ref.name);
|
|
209
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type Capability } from "./capability.ts";
|
|
2
|
+
import type { ContractOperation, ContractViaEntry } from "./diagnostic.ts";
|
|
3
|
+
import type { KnownEffect } from "./effects.ts";
|
|
4
|
+
import type { SourceLocation } from "./location.ts";
|
|
5
|
+
import type { SymbolId } from "./symbol-id.ts";
|
|
6
|
+
/**
|
|
7
|
+
* One function's authority, as `ambit check --format json` emits it
|
|
8
|
+
* (DESIGN.md §5.1's `kind: "authority"` record) and as `ambit diff` compares
|
|
9
|
+
* it.
|
|
10
|
+
*
|
|
11
|
+
* "Authority" here is what a function may act with, not what a diagnostic
|
|
12
|
+
* says about it: a record is emitted for every analyzed function, including
|
|
13
|
+
* the ones nothing is wrong with. It is a checker-side artifact — nothing at
|
|
14
|
+
* runtime reads it, so ADR-0005's objection to shipping contract data to the
|
|
15
|
+
* runtime does not apply.
|
|
16
|
+
*
|
|
17
|
+
* Both halves of each contract are carried, and neither is derived from the
|
|
18
|
+
* other, because they answer different questions: `declared` is what the
|
|
19
|
+
* source claims, `observed` / `required` is what the body was inferred to
|
|
20
|
+
* do. {@link effectiveEffects} states which one a comparison uses.
|
|
21
|
+
*/
|
|
22
|
+
export interface AuthorityRecord {
|
|
23
|
+
readonly kind: "authority";
|
|
24
|
+
readonly symbol: SymbolId;
|
|
25
|
+
readonly location: SourceLocation;
|
|
26
|
+
/** `@entrypoint` (DESIGN.md §4.1): where the runtime establishes a context. */
|
|
27
|
+
readonly entrypoint: boolean;
|
|
28
|
+
readonly effects: AuthorityEffects;
|
|
29
|
+
readonly capabilities: AuthorityCapabilities;
|
|
30
|
+
/**
|
|
31
|
+
* For an authority this function actually reaches, the call path that
|
|
32
|
+
* carries it — the same hops `--format github` folds into an annotation
|
|
33
|
+
* (§5.1). `ambit diff` renders the path of an *increase* from here, so the
|
|
34
|
+
* reader sees what introduced it without re-running the check.
|
|
35
|
+
*
|
|
36
|
+
* Only authority with a witness appears. An effect a function declares but
|
|
37
|
+
* whose body does not reach has no path, and none is synthesized (§5.3).
|
|
38
|
+
*/
|
|
39
|
+
readonly paths: readonly AuthorityPath[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `declared` is `null` when no `@effects` tag was written, and also when one
|
|
43
|
+
* was written but did not parse (`AMB-E002`): a declaration that does not
|
|
44
|
+
* mean what it says must not read as a narrower one than the author intended,
|
|
45
|
+
* which is how every other consumer treats `{ kind: "invalid" }`.
|
|
46
|
+
*
|
|
47
|
+
* `pure` never appears here; it is the empty list (DESIGN.md §4.2). The two
|
|
48
|
+
* are distinguishable because `declared: []` is a declared empty set while
|
|
49
|
+
* `declared: null` is no declaration at all.
|
|
50
|
+
*/
|
|
51
|
+
export interface AuthorityEffects {
|
|
52
|
+
readonly declared: readonly KnownEffect[] | null;
|
|
53
|
+
readonly observed: readonly KnownEffect[];
|
|
54
|
+
/** Propagation reached a call it could not resolve, so `observed` may be incomplete. */
|
|
55
|
+
readonly unknown: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** The capability half, in the same shape. Capabilities are `<resource>:<action>:<target>` text. */
|
|
58
|
+
export interface AuthorityCapabilities {
|
|
59
|
+
readonly declared: readonly string[] | null;
|
|
60
|
+
readonly required: readonly string[];
|
|
61
|
+
readonly unknown: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Which lattice an authority token belongs to — the two do not share a namespace. */
|
|
64
|
+
export type AuthorityKind = "effect" | "capability";
|
|
65
|
+
/** The call path that brings one authority into one function. */
|
|
66
|
+
export interface AuthorityPath {
|
|
67
|
+
readonly authority: string;
|
|
68
|
+
readonly kind: AuthorityKind;
|
|
69
|
+
readonly via: readonly ContractViaEntry[];
|
|
70
|
+
/** The operation's own call site, when one is known. Absent, never guessed (§5.3). */
|
|
71
|
+
readonly operation?: ContractOperation;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One authority, named so that an effect and a capability can never collide
|
|
75
|
+
* in the same set: `effect:network`, `capability:http:get:api.example.com`.
|
|
76
|
+
*/
|
|
77
|
+
export interface AuthorityRef {
|
|
78
|
+
readonly kind: AuthorityKind;
|
|
79
|
+
readonly name: string;
|
|
80
|
+
}
|
|
81
|
+
export declare function effectRef(effect: KnownEffect): AuthorityRef;
|
|
82
|
+
export declare function capabilityRef(capability: Capability | string): AuthorityRef;
|
|
83
|
+
export declare function formatAuthorityRef(ref: AuthorityRef): string;
|
|
84
|
+
/**
|
|
85
|
+
* The effects a record's function is trusted with: what it declared, or —
|
|
86
|
+
* when it declared nothing — what its body was inferred to do.
|
|
87
|
+
*
|
|
88
|
+
* This is the rule `propagate` already applies to a callee (a declaration is
|
|
89
|
+
* believed; an undeclared function contributes its own inferred effects), so
|
|
90
|
+
* a change here is exactly a change in what the rest of the codebase is
|
|
91
|
+
* entitled to assume. It is also what makes widening a tag visible: declaring
|
|
92
|
+
* `network` where `pure` was declared raises the effective set even though
|
|
93
|
+
* the body did not move.
|
|
94
|
+
*/
|
|
95
|
+
export declare function effectiveEffects(record: AuthorityRecord): readonly KnownEffect[];
|
|
96
|
+
/** The capability half of {@link effectiveEffects}, on the same rule. */
|
|
97
|
+
export declare function effectiveCapabilities(record: AuthorityRecord): readonly string[];
|
|
98
|
+
/**
|
|
99
|
+
* Every authority a record's function is trusted with, as refs.
|
|
100
|
+
*
|
|
101
|
+
* `unknown` is deliberately absent: it is not an authority but a statement
|
|
102
|
+
* that the analysis is incomplete (DESIGN.md §4.3), and counting it as one
|
|
103
|
+
* would report an unanalyzable call as a permission. `ambit diff` reports
|
|
104
|
+
* `unknown` transitions separately, so an unanalyzable range is still never
|
|
105
|
+
* silently reported as unchanged.
|
|
106
|
+
*/
|
|
107
|
+
export declare function effectiveAuthority(record: AuthorityRecord): readonly AuthorityRef[];
|
|
108
|
+
/** Whether the function holds any authority at all — what makes a *new* symbol worth failing on. */
|
|
109
|
+
export declare function holdsAuthority(record: AuthorityRecord): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { formatCapability } from "./capability.js";
|
|
2
|
+
export function effectRef(effect) {
|
|
3
|
+
return { kind: "effect", name: effect };
|
|
4
|
+
}
|
|
5
|
+
export function capabilityRef(capability) {
|
|
6
|
+
return {
|
|
7
|
+
kind: "capability",
|
|
8
|
+
name: typeof capability === "string" ? capability : formatCapability(capability),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function formatAuthorityRef(ref) {
|
|
12
|
+
return `${ref.kind}:${ref.name}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The effects a record's function is trusted with: what it declared, or —
|
|
16
|
+
* when it declared nothing — what its body was inferred to do.
|
|
17
|
+
*
|
|
18
|
+
* This is the rule `propagate` already applies to a callee (a declaration is
|
|
19
|
+
* believed; an undeclared function contributes its own inferred effects), so
|
|
20
|
+
* a change here is exactly a change in what the rest of the codebase is
|
|
21
|
+
* entitled to assume. It is also what makes widening a tag visible: declaring
|
|
22
|
+
* `network` where `pure` was declared raises the effective set even though
|
|
23
|
+
* the body did not move.
|
|
24
|
+
*/
|
|
25
|
+
export function effectiveEffects(record) {
|
|
26
|
+
return record.effects.declared ?? record.effects.observed;
|
|
27
|
+
}
|
|
28
|
+
/** The capability half of {@link effectiveEffects}, on the same rule. */
|
|
29
|
+
export function effectiveCapabilities(record) {
|
|
30
|
+
return record.capabilities.declared ?? record.capabilities.required;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every authority a record's function is trusted with, as refs.
|
|
34
|
+
*
|
|
35
|
+
* `unknown` is deliberately absent: it is not an authority but a statement
|
|
36
|
+
* that the analysis is incomplete (DESIGN.md §4.3), and counting it as one
|
|
37
|
+
* would report an unanalyzable call as a permission. `ambit diff` reports
|
|
38
|
+
* `unknown` transitions separately, so an unanalyzable range is still never
|
|
39
|
+
* silently reported as unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export function effectiveAuthority(record) {
|
|
42
|
+
return [
|
|
43
|
+
...effectiveEffects(record).map(effectRef),
|
|
44
|
+
...effectiveCapabilities(record).map((capability) => capabilityRef(capability)),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
/** Whether the function holds any authority at all — what makes a *new* symbol worth failing on. */
|
|
48
|
+
export function holdsAuthority(record) {
|
|
49
|
+
return effectiveAuthority(record).length > 0;
|
|
50
|
+
}
|