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,66 @@
|
|
|
1
|
+
import type { ApprovalReview, AuthorityDiff, MalformedApprovalLine } from "../core/index.ts";
|
|
2
|
+
/** What `ambit diff` compared, and what came out. */
|
|
3
|
+
export interface DiffResult {
|
|
4
|
+
readonly diff: AuthorityDiff;
|
|
5
|
+
/**
|
|
6
|
+
* Which increases the approval ledger lets through, which it does not, and
|
|
7
|
+
* which lines granted nothing (DESIGN.md §6.3).
|
|
8
|
+
*/
|
|
9
|
+
readonly review: ApprovalReview;
|
|
10
|
+
/** `-` lines in the head side's ledger that did not parse. Reported; they grant nothing. */
|
|
11
|
+
readonly malformedApprovals: readonly MalformedApprovalLine[];
|
|
12
|
+
/** The ledger that governed the head side, relative to the checked directory's repository. */
|
|
13
|
+
readonly approvalsFile?: string;
|
|
14
|
+
/** The ref as given, and the commit it resolved to — a branch name moves, a sha does not. */
|
|
15
|
+
readonly ref: string;
|
|
16
|
+
readonly baseCommit: string;
|
|
17
|
+
/** The directory that was compared, relative to the repository root. */
|
|
18
|
+
readonly subdir: string;
|
|
19
|
+
/** That same directory as the caller gave it — what a `location.file` is relative to. */
|
|
20
|
+
readonly dir: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compare the working tree's authority against `ref`.
|
|
24
|
+
*
|
|
25
|
+
* The base side is a `git worktree` checkout under the OS temporary
|
|
26
|
+
* directory; both sides then run the same {@link analyze} over the same
|
|
27
|
+
* subdirectory, and the two dumps go to `diffAuthority`, which is pure. Git
|
|
28
|
+
* appears here and nowhere in the comparison.
|
|
29
|
+
*
|
|
30
|
+
* The worktree is removed in a `finally`, so a failed analysis leaves nothing
|
|
31
|
+
* behind either.
|
|
32
|
+
*
|
|
33
|
+
* @effects process, fs_read, fs_write
|
|
34
|
+
*/
|
|
35
|
+
export declare function runDiff(ref: string, dir: string): Promise<DiffResult>;
|
|
36
|
+
/** Whether the comparison fails: an increase with no approval in force (DESIGN.md §6.3). */
|
|
37
|
+
export declare function hasUnapprovedIncrease(result: DiffResult): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The comparison, for a human reading a pull request.
|
|
40
|
+
*
|
|
41
|
+
* What increased comes first and is the only part rendered in full: each
|
|
42
|
+
* added authority gets the call path that brought it, so the reader sees the
|
|
43
|
+
* hop that introduced it without opening anything. What decreased, what was
|
|
44
|
+
* deleted and what stayed the same follow, named or counted but never
|
|
45
|
+
* expanded — the point of the command is that nobody has to read every
|
|
46
|
+
* contract to find the one that changed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatDiffText(result: DiffResult): string;
|
|
49
|
+
/**
|
|
50
|
+
* One GitHub Actions annotation per authority gained — per symbol *and*
|
|
51
|
+
* authority, since that is what a reviewer acts on: `priceOrder` gaining
|
|
52
|
+
* `network` and `fs_write` are two decisions, not one.
|
|
53
|
+
*
|
|
54
|
+
* Annotated at the symbol's own declaration, with the call path folded into
|
|
55
|
+
* the body, so the annotation lands on the function whose contract changed
|
|
56
|
+
* and carries the reason without the reader opening the job log — the same
|
|
57
|
+
* shape `check --format github` uses (DESIGN.md §5.1 / §6).
|
|
58
|
+
*
|
|
59
|
+
* An unapproved increase is an `error`, and carries the ledger line to add.
|
|
60
|
+
* An **approved** one is a `notice`, carrying the reason that was given: it
|
|
61
|
+
* does not fail the build, and it must still be visible on the diff, because
|
|
62
|
+
* an increase nobody sees is the thing the ledger exists to prevent (§6.3).
|
|
63
|
+
* A decrease and a deletion are reported by the text output and do not fail a
|
|
64
|
+
* build (§6), so an annotation on them would be noise on a diff.
|
|
65
|
+
*/
|
|
66
|
+
export declare function formatDiffGithub(result: DiffResult): string;
|
package/dist/cli/diff.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { authorityDecreases, deletedSymbols, diffAuthority, displayName, formatApprovalLine, movedSymbols, pathFor, reviewIncreases, unchangedSymbols, unknownGained, } from "../core/index.js";
|
|
3
|
+
import { analyze } from "./analyze.js";
|
|
4
|
+
import { loadApprovals } from "./approvals.js";
|
|
5
|
+
import { githubAnnotation, workspacePath } from "./github.js";
|
|
6
|
+
import { addWorktree, git, removeWorktree, renamedFiles, repositoryRoot } from "./worktree.js";
|
|
7
|
+
/**
|
|
8
|
+
* Compare the working tree's authority against `ref`.
|
|
9
|
+
*
|
|
10
|
+
* The base side is a `git worktree` checkout under the OS temporary
|
|
11
|
+
* directory; both sides then run the same {@link analyze} over the same
|
|
12
|
+
* subdirectory, and the two dumps go to `diffAuthority`, which is pure. Git
|
|
13
|
+
* appears here and nowhere in the comparison.
|
|
14
|
+
*
|
|
15
|
+
* The worktree is removed in a `finally`, so a failed analysis leaves nothing
|
|
16
|
+
* behind either.
|
|
17
|
+
*
|
|
18
|
+
* @effects process, fs_read, fs_write
|
|
19
|
+
*/
|
|
20
|
+
export async function runDiff(ref, dir) {
|
|
21
|
+
const repoRoot = await repositoryRoot(dir);
|
|
22
|
+
// Resolved before the checkout so a bad ref fails with git's own message
|
|
23
|
+
// rather than as a half-made worktree.
|
|
24
|
+
const baseCommit = await git(repoRoot, "rev-parse", ref);
|
|
25
|
+
const subdir = path.relative(repoRoot, path.resolve(dir));
|
|
26
|
+
// Asked of the working tree, before the base checkout exists: `git diff`
|
|
27
|
+
// compares the index and the working tree against `baseCommit`, and the
|
|
28
|
+
// temporary worktree is neither.
|
|
29
|
+
const renames = await renamedFiles(repoRoot, baseCommit, subdir);
|
|
30
|
+
const worktree = await addWorktree(repoRoot, baseCommit);
|
|
31
|
+
try {
|
|
32
|
+
// The same subpath on both sides: a symbol id is relative to the
|
|
33
|
+
// directory that was checked (DESIGN.md §5.3), so comparing `src` against
|
|
34
|
+
// a whole repository would make every id look new.
|
|
35
|
+
const baseDir = path.join(worktree.root, subdir);
|
|
36
|
+
const headDir = path.resolve(dir);
|
|
37
|
+
const base = await analyze(baseDir);
|
|
38
|
+
const head = await analyze(headDir);
|
|
39
|
+
// The ledger is read on both sides, because an approval counts only in
|
|
40
|
+
// the comparison that adds it (DESIGN.md §6.3).
|
|
41
|
+
const baseApprovals = loadApprovals(baseDir);
|
|
42
|
+
const headApprovals = loadApprovals(headDir);
|
|
43
|
+
const diff = diffAuthority(base.authority, head.authority, renames);
|
|
44
|
+
return {
|
|
45
|
+
diff,
|
|
46
|
+
review: reviewIncreases(diff, baseApprovals.parsed.approvals, headApprovals.parsed.approvals),
|
|
47
|
+
malformedApprovals: headApprovals.parsed.malformed,
|
|
48
|
+
...(headApprovals.filePath
|
|
49
|
+
? { approvalsFile: path.relative(repoRoot, headApprovals.filePath) }
|
|
50
|
+
: {}),
|
|
51
|
+
ref,
|
|
52
|
+
baseCommit,
|
|
53
|
+
subdir,
|
|
54
|
+
dir: headDir,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await removeWorktree(worktree);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Whether the comparison fails: an increase with no approval in force (DESIGN.md §6.3). */
|
|
62
|
+
export function hasUnapprovedIncrease(result) {
|
|
63
|
+
return result.review.unapproved.length > 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The comparison, for a human reading a pull request.
|
|
67
|
+
*
|
|
68
|
+
* What increased comes first and is the only part rendered in full: each
|
|
69
|
+
* added authority gets the call path that brought it, so the reader sees the
|
|
70
|
+
* hop that introduced it without opening anything. What decreased, what was
|
|
71
|
+
* deleted and what stayed the same follow, named or counted but never
|
|
72
|
+
* expanded — the point of the command is that nobody has to read every
|
|
73
|
+
* contract to find the one that changed.
|
|
74
|
+
*/
|
|
75
|
+
export function formatDiffText(result) {
|
|
76
|
+
const { diff, review } = result;
|
|
77
|
+
const decreases = authorityDecreases(diff);
|
|
78
|
+
const deleted = deletedSymbols(diff);
|
|
79
|
+
const moved = movedSymbols(diff);
|
|
80
|
+
const unknown = unknownGained(diff);
|
|
81
|
+
const where = result.subdir === "" ? "the repository root" : result.subdir;
|
|
82
|
+
const lines = [
|
|
83
|
+
`base ${result.ref} (${result.baseCommit.slice(0, 7)}) vs the working tree, over ${where}`,
|
|
84
|
+
"",
|
|
85
|
+
];
|
|
86
|
+
if (review.unapproved.length === 0 && review.approved.length === 0) {
|
|
87
|
+
lines.push("No authority increased.", "");
|
|
88
|
+
}
|
|
89
|
+
if (review.unapproved.length > 0) {
|
|
90
|
+
lines.push(`${count(review.unapproved.length, "authority")} increased without approval:`, "", ...review.unapproved.flatMap((item) => renderIncrease(item)), `Add each line above to ${result.approvalsFile ?? APPROVALS_HINT}, with the reason, and`, "commit it in the same change (DESIGN.md §6.3). An approval already in the base", "grants nothing.", "");
|
|
91
|
+
}
|
|
92
|
+
// Approved increases are still printed in full. The point of the ledger is
|
|
93
|
+
// that an increase is visible to whoever reads the pull request, and a
|
|
94
|
+
// section that collapses to a count would undo exactly that.
|
|
95
|
+
if (review.approved.length > 0) {
|
|
96
|
+
lines.push(`${count(review.approved.length, "authority")} increased, approved in this change:`, "", ...review.approved.flatMap((item) => [
|
|
97
|
+
...renderIncrease(item, { suggestApproval: false }),
|
|
98
|
+
` approved: ${item.approval.reason} (${result.approvalsFile ?? APPROVALS_HINT}:${item.approval.line})`,
|
|
99
|
+
"",
|
|
100
|
+
]));
|
|
101
|
+
}
|
|
102
|
+
if (review.unused.length > 0) {
|
|
103
|
+
lines.push(`${count(review.unused.length, "approval")} added here matched no increase and granted nothing:`, ...review.unused.map((approval) => ` ${result.approvalsFile ?? APPROVALS_HINT}:${approval.line} ${approval.symbol} ${approval.authority.kind}:${approval.authority.name}`), "");
|
|
104
|
+
}
|
|
105
|
+
if (result.malformedApprovals.length > 0) {
|
|
106
|
+
lines.push(`${count(result.malformedApprovals.length, "line")} in ${result.approvalsFile ?? APPROVALS_HINT} did not parse as an approval and granted nothing:`, ...result.malformedApprovals.map((entry) => ` line ${entry.line}: ${entry.text}`), "");
|
|
107
|
+
}
|
|
108
|
+
if (moved.length > 0) {
|
|
109
|
+
lines.push(`${count(moved.length, "symbol")} moved with a renamed file and was compared against its old path:`, ...moved.map((entry) => ` ${entry.movedFrom} -> ${entry.symbol}`), "");
|
|
110
|
+
}
|
|
111
|
+
// Not an increase — `unknown` is not authority (DESIGN.md §4.3) — but never
|
|
112
|
+
// silent either: a range that stopped being analyzable is exactly what must
|
|
113
|
+
// not be reported as "nothing increased here".
|
|
114
|
+
if (unknown.length > 0) {
|
|
115
|
+
lines.push(`Analysis reached something it could not resolve in ${count(unknown.length, "symbol")} where it previously did not.`, "This is not authority and is not counted as an increase; those symbols' effects may be incomplete:", ...unknown.map((entry) => ` ${entry.symbol}`), "");
|
|
116
|
+
}
|
|
117
|
+
if (decreases.length > 0) {
|
|
118
|
+
lines.push(`Authority decreased in ${count(decreases.length, "symbol")}:`);
|
|
119
|
+
for (const entry of decreases) {
|
|
120
|
+
lines.push(` ${entry.symbol}`, ...entry.removed.map((ref) => ` - ${refText(ref)}`));
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
}
|
|
124
|
+
if (deleted.length > 0) {
|
|
125
|
+
lines.push(`${count(deleted.length, "symbol")} no longer present:`, ...deleted.map((entry) => ` ${entry.symbol}`), "");
|
|
126
|
+
}
|
|
127
|
+
lines.push(`${count(unchangedSymbols(diff).length, "symbol")} unchanged, out of ${count(diff.symbols.length, "symbol")} compared.`, "");
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
/** Where to write an approval when the repository has no ledger yet (DESIGN.md §6.3). */
|
|
131
|
+
const APPROVALS_HINT = "ambit.approvals.md";
|
|
132
|
+
/**
|
|
133
|
+
* One authority one symbol gained: the symbol at its own position, the
|
|
134
|
+
* authority, and the path that carries it.
|
|
135
|
+
*
|
|
136
|
+
* A path is shown only where the record has one. An authority a function
|
|
137
|
+
* declares but does not reach has no path, and none is invented
|
|
138
|
+
* (DESIGN.md §5.3).
|
|
139
|
+
*
|
|
140
|
+
* For an increase with nothing approving it, the ledger line to add follows,
|
|
141
|
+
* ready to copy — the grammar of §6.3 is a thing to paste, not to recall.
|
|
142
|
+
*/
|
|
143
|
+
function renderIncrease(item, options = {}) {
|
|
144
|
+
const { entry, ref } = item;
|
|
145
|
+
const at = entry.head ? ` (${entry.head.location.file}:${entry.head.location.line})` : "";
|
|
146
|
+
const what = entry.status === "new" ? " [new symbol]" : entry.status === "moved" ? " [moved]" : "";
|
|
147
|
+
const lines = [` ${entry.symbol}${at}${what}`, ` + ${refText(ref)}`];
|
|
148
|
+
const path = pathFor(entry.head, ref);
|
|
149
|
+
for (const hop of path?.via ?? []) {
|
|
150
|
+
lines.push(` -> ${displayName(hop.symbol)} (${hop.file}:${hop.line})`);
|
|
151
|
+
}
|
|
152
|
+
if (path?.operation) {
|
|
153
|
+
lines.push(` operation: ${path.operation.qualifiedName} (${path.operation.file}:${path.operation.line})`);
|
|
154
|
+
}
|
|
155
|
+
if (options.suggestApproval !== false) {
|
|
156
|
+
lines.push(` ${formatApprovalLine(entry.symbol, ref)}`, "");
|
|
157
|
+
}
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* An authority as a reader recognizes it: an effect by its bare name, a
|
|
162
|
+
* capability prefixed so the two lattices never read as one namespace.
|
|
163
|
+
*/
|
|
164
|
+
function refText(ref) {
|
|
165
|
+
return ref.kind === "effect" ? ref.name : `capability ${ref.name}`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* `1 symbol` / `2 symbols`, with the plural spelled out where appending an
|
|
169
|
+
* `s` gets it wrong — "2 authoritys" is the sort of thing a reader stops on.
|
|
170
|
+
*/
|
|
171
|
+
const PLURALS = { authority: "authorities" };
|
|
172
|
+
function count(n, noun) {
|
|
173
|
+
if (n === 1)
|
|
174
|
+
return `${n} ${noun}`;
|
|
175
|
+
return `${n} ${PLURALS[noun] ?? `${noun}s`}`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* One GitHub Actions annotation per authority gained — per symbol *and*
|
|
179
|
+
* authority, since that is what a reviewer acts on: `priceOrder` gaining
|
|
180
|
+
* `network` and `fs_write` are two decisions, not one.
|
|
181
|
+
*
|
|
182
|
+
* Annotated at the symbol's own declaration, with the call path folded into
|
|
183
|
+
* the body, so the annotation lands on the function whose contract changed
|
|
184
|
+
* and carries the reason without the reader opening the job log — the same
|
|
185
|
+
* shape `check --format github` uses (DESIGN.md §5.1 / §6).
|
|
186
|
+
*
|
|
187
|
+
* An unapproved increase is an `error`, and carries the ledger line to add.
|
|
188
|
+
* An **approved** one is a `notice`, carrying the reason that was given: it
|
|
189
|
+
* does not fail the build, and it must still be visible on the diff, because
|
|
190
|
+
* an increase nobody sees is the thing the ledger exists to prevent (§6.3).
|
|
191
|
+
* A decrease and a deletion are reported by the text output and do not fail a
|
|
192
|
+
* build (§6), so an annotation on them would be noise on a diff.
|
|
193
|
+
*/
|
|
194
|
+
export function formatDiffGithub(result) {
|
|
195
|
+
let out = "";
|
|
196
|
+
for (const item of result.review.unapproved) {
|
|
197
|
+
out += increaseAnnotation(result, item, "error", [
|
|
198
|
+
`add to ${result.approvalsFile ?? APPROVALS_HINT}: ${formatApprovalLine(item.entry.symbol, item.ref)}`,
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
for (const item of result.review.approved) {
|
|
202
|
+
out += increaseAnnotation(result, item, "notice", [
|
|
203
|
+
`approved in this change: ${item.approval.reason}`,
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function increaseAnnotation(result, item, severity, trailer) {
|
|
209
|
+
const head = item.entry.head;
|
|
210
|
+
if (!head)
|
|
211
|
+
return "";
|
|
212
|
+
const path = pathFor(head, item.ref);
|
|
213
|
+
const what = item.entry.status === "new"
|
|
214
|
+
? "new symbol"
|
|
215
|
+
: item.entry.status === "moved"
|
|
216
|
+
? "moved, and widened"
|
|
217
|
+
: "authority increased";
|
|
218
|
+
return githubAnnotation({
|
|
219
|
+
severity,
|
|
220
|
+
file: workspacePath(result.dir, head.location.file),
|
|
221
|
+
line: head.location.line,
|
|
222
|
+
col: head.location.col,
|
|
223
|
+
title: "ambit diff",
|
|
224
|
+
body: [
|
|
225
|
+
`${displayName(head.symbol)} gained ${refText(item.ref)} since ${result.ref} (${what})`,
|
|
226
|
+
...(path?.via ?? []).map((hop) => `-> ${displayName(hop.symbol)} (${hop.file}:${hop.line})`),
|
|
227
|
+
...(path?.operation
|
|
228
|
+
? [
|
|
229
|
+
`operation: ${path.operation.qualifiedName} (${path.operation.file}:${path.operation.line})`,
|
|
230
|
+
]
|
|
231
|
+
: []),
|
|
232
|
+
...trailer,
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions workflow-command escaping and path handling, shared by every
|
|
3
|
+
* command that emits annotations.
|
|
4
|
+
*
|
|
5
|
+
* One implementation, because `check` and `diff` annotate the same files in
|
|
6
|
+
* the same run: two escapings that drifted apart would show up as one command
|
|
7
|
+
* annotating a line the other could not.
|
|
8
|
+
*/
|
|
9
|
+
/** Workflow-command escaping for the message body. */
|
|
10
|
+
export declare function githubData(value: string): string;
|
|
11
|
+
/** Workflow-command escaping for a property value, where `:` and `,` also terminate. */
|
|
12
|
+
export declare function githubProperty(value: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* A `location.file` — relative to the directory that was checked — re-expressed
|
|
15
|
+
* relative to the working directory, which is what an annotation is resolved
|
|
16
|
+
* against.
|
|
17
|
+
*/
|
|
18
|
+
export declare function workspacePath(rootDir: string, file: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* One workflow command: `::<severity> file=...,line=...,col=...,title=...::<body>`.
|
|
21
|
+
*
|
|
22
|
+
* The body is folded onto one line with `%0A`, so an annotation carrying a
|
|
23
|
+
* call path stays self-sufficient — a reader on the diff sees every hop
|
|
24
|
+
* without opening the job log (DESIGN.md §5.1).
|
|
25
|
+
*/
|
|
26
|
+
export declare function githubAnnotation(annotation: {
|
|
27
|
+
readonly severity: string;
|
|
28
|
+
readonly file: string;
|
|
29
|
+
readonly line: number;
|
|
30
|
+
readonly col: number;
|
|
31
|
+
readonly title: string;
|
|
32
|
+
readonly body: readonly string[];
|
|
33
|
+
}): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Actions workflow-command escaping and path handling, shared by every
|
|
4
|
+
* command that emits annotations.
|
|
5
|
+
*
|
|
6
|
+
* One implementation, because `check` and `diff` annotate the same files in
|
|
7
|
+
* the same run: two escapings that drifted apart would show up as one command
|
|
8
|
+
* annotating a line the other could not.
|
|
9
|
+
*/
|
|
10
|
+
/** Workflow-command escaping for the message body. */
|
|
11
|
+
export function githubData(value) {
|
|
12
|
+
return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
13
|
+
}
|
|
14
|
+
/** Workflow-command escaping for a property value, where `:` and `,` also terminate. */
|
|
15
|
+
export function githubProperty(value) {
|
|
16
|
+
return githubData(value).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A `location.file` — relative to the directory that was checked — re-expressed
|
|
20
|
+
* relative to the working directory, which is what an annotation is resolved
|
|
21
|
+
* against.
|
|
22
|
+
*/
|
|
23
|
+
export function workspacePath(rootDir, file) {
|
|
24
|
+
return path.relative(process.cwd(), path.resolve(rootDir, file)).split(path.sep).join("/");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One workflow command: `::<severity> file=...,line=...,col=...,title=...::<body>`.
|
|
28
|
+
*
|
|
29
|
+
* The body is folded onto one line with `%0A`, so an annotation carrying a
|
|
30
|
+
* call path stays self-sufficient — a reader on the diff sees every hop
|
|
31
|
+
* without opening the job log (DESIGN.md §5.1).
|
|
32
|
+
*/
|
|
33
|
+
export function githubAnnotation(annotation) {
|
|
34
|
+
const properties = [
|
|
35
|
+
`file=${githubProperty(annotation.file)}`,
|
|
36
|
+
`line=${annotation.line}`,
|
|
37
|
+
`col=${annotation.col}`,
|
|
38
|
+
`title=${githubProperty(annotation.title)}`,
|
|
39
|
+
].join(",");
|
|
40
|
+
return `::${annotation.severity} ${properties}::${githubData(annotation.body.join("\n"))}\n`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `diff` shells out to git and writes a worktree, so the entry point's own
|
|
4
|
+
* contract is wider than `check`'s alone (DESIGN.md §6).
|
|
5
|
+
*
|
|
6
|
+
* @effects fs_read, fs_write, process
|
|
7
|
+
*/
|
|
8
|
+
export declare function main(argv: readonly string[]): Promise<number>;
|