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