balladeer 0.0.4 → 1.0.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 (60) hide show
  1. package/LICENSE +200 -5
  2. package/README.md +154 -68
  3. package/dist/agent.d.ts +126 -0
  4. package/dist/agent.js +209 -0
  5. package/dist/cli.d.ts +34 -0
  6. package/dist/cli.js +392 -0
  7. package/dist/client.d.ts +44 -0
  8. package/dist/client.js +114 -0
  9. package/dist/commands/affected.d.ts +22 -0
  10. package/dist/commands/affected.js +122 -0
  11. package/dist/commands/check-seals.d.ts +37 -0
  12. package/dist/commands/check-seals.js +289 -0
  13. package/dist/commands/discover.d.ts +68 -0
  14. package/dist/commands/discover.js +395 -0
  15. package/dist/commands/explain.d.ts +35 -0
  16. package/dist/commands/explain.js +90 -0
  17. package/dist/commands/invite.d.ts +24 -0
  18. package/dist/commands/invite.js +197 -0
  19. package/dist/commands/mcp.d.ts +65 -0
  20. package/dist/commands/mcp.js +202 -0
  21. package/dist/commands/propose.d.ts +59 -0
  22. package/dist/commands/propose.js +262 -0
  23. package/dist/commands/repositories.d.ts +18 -0
  24. package/dist/commands/repositories.js +185 -0
  25. package/dist/commands/setup.d.ts +75 -0
  26. package/dist/commands/setup.js +1471 -0
  27. package/dist/commands/status.d.ts +35 -0
  28. package/dist/commands/status.js +482 -0
  29. package/dist/commands/touch-map.d.ts +42 -0
  30. package/dist/commands/touch-map.js +251 -0
  31. package/dist/commands/whoami.d.ts +8 -0
  32. package/dist/commands/whoami.js +79 -0
  33. package/dist/conventions.d.ts +69 -0
  34. package/dist/conventions.js +175 -0
  35. package/dist/copy.d.ts +148 -0
  36. package/dist/copy.js +459 -0
  37. package/dist/currency.d.ts +31 -0
  38. package/dist/currency.js +72 -0
  39. package/dist/gh.d.ts +80 -0
  40. package/dist/gh.js +188 -0
  41. package/dist/git.d.ts +76 -0
  42. package/dist/git.js +203 -0
  43. package/dist/markers.d.ts +76 -0
  44. package/dist/markers.js +125 -0
  45. package/dist/mcp-config.d.ts +99 -0
  46. package/dist/mcp-config.js +230 -0
  47. package/dist/release.d.ts +55 -0
  48. package/dist/release.js +67 -0
  49. package/dist/repository.d.ts +8 -0
  50. package/dist/repository.js +32 -0
  51. package/dist/seals.d.ts +48 -0
  52. package/dist/seals.js +112 -0
  53. package/dist/store.d.ts +98 -0
  54. package/dist/store.js +225 -0
  55. package/dist/touch-map.d.ts +241 -0
  56. package/dist/touch-map.js +487 -0
  57. package/dist/wire.d.ts +588 -0
  58. package/dist/wire.js +20 -0
  59. package/package.json +19 -10
  60. package/bin/balladeer.js +0 -136
@@ -0,0 +1,98 @@
1
+ import type { DelegatedScope } from "./wire.js";
2
+ export declare class StoreError extends Error {
3
+ readonly code: string;
4
+ constructor(code: string, message: string);
5
+ }
6
+ export type PendingPairing = Readonly<{
7
+ controlPlane: string;
8
+ pairingId: string;
9
+ userCode: string;
10
+ verifier: string;
11
+ verificationUri: string;
12
+ expiresAt: string;
13
+ startedAt: string;
14
+ }>;
15
+ export type StoredSession = Readonly<{
16
+ controlPlane: string;
17
+ sessionId: string;
18
+ token: string;
19
+ workspaceId: string;
20
+ workspaceName: string;
21
+ workspaceSlug: string;
22
+ /**
23
+ * The membership that approved this pairing. It is what lets `discover` name
24
+ * an owner on every promise it files without asking a person to look an id up.
25
+ * Optional because a store written by a copy of this command that predates the
26
+ * field has none, and an entry without one makes `discover` say so rather than
27
+ * guess at an owner.
28
+ */
29
+ membershipId?: string;
30
+ membershipDisplayName: string;
31
+ scopes: readonly DelegatedScope[];
32
+ expiresAt: string;
33
+ }>;
34
+ /**
35
+ * One repository-bound agent credential, written when setup issues it.
36
+ *
37
+ * `balladeer mcp` refuses an entry whose `mcpUrl` origin differs from its
38
+ * `controlPlane` origin, so a server response can never redirect this bearer to
39
+ * a host the person never paired with.
40
+ */
41
+ export type StoredAgent = Readonly<{
42
+ controlPlane: string;
43
+ workspaceId: string;
44
+ /**
45
+ * The repository this credential reaches, by id and by the owner/name a person
46
+ * reads. The name is what lets `balladeer mcp` pick the credential belonging
47
+ * to the directory it was started in rather than whichever one happens to be
48
+ * stored first. It is absent only in a store written by a copy of this command
49
+ * that predates the field, and an entry without it simply never matches a
50
+ * directory, which is a refusal rather than a wrong answer.
51
+ */
52
+ repositoryId: string;
53
+ repository?: string;
54
+ connectionId: string;
55
+ mcpUrl: string;
56
+ token: string;
57
+ }>;
58
+ export type Credentials = {
59
+ version: 1;
60
+ pendingPairings: PendingPairing[];
61
+ sessions: StoredSession[];
62
+ agents: StoredAgent[];
63
+ };
64
+ export declare function configHome(environment?: NodeJS.ProcessEnv): string;
65
+ export declare function credentialsPath(environment?: NodeJS.ProcessEnv): string;
66
+ /**
67
+ * The credential must not land somewhere a commit would pick it up.
68
+ *
69
+ * An explicitly chosen location, or one under the current working directory, is
70
+ * refused outright the moment it sits in a work tree. The default
71
+ * `~/.config/balladeer` is only refused when git says the file would actually be
72
+ * tracked, because a great many developers keep their dotfiles in a repository
73
+ * and refusing all of them would brick setup for no gain.
74
+ */
75
+ export declare function assertSafeStoreLocation(directory: string, environment?: NodeJS.ProcessEnv, cwd?: string): void;
76
+ export declare function ensureStoreDirectory(environment?: NodeJS.ProcessEnv): string;
77
+ export declare function readCredentials(environment?: NodeJS.ProcessEnv): Credentials;
78
+ /**
79
+ * Atomic, and never through a path an attacker could have pre-planted: the
80
+ * temporary file is opened with `wx` (fail if it exists) under a random name,
81
+ * its mode is set on the open descriptor, and it is flushed before the rename.
82
+ */
83
+ export declare function writeCredentials(credentials: Credentials, environment?: NodeJS.ProcessEnv): void;
84
+ /** Origins, not URL strings, so `https://host/../` cannot alias a stored entry. */
85
+ export declare function normalizeControlPlane(value: string): string;
86
+ /** At most one pending pairing per control plane, newest wins. */
87
+ export declare function putPendingPairing(credentials: Credentials, pending: PendingPairing): Credentials;
88
+ export declare function findPendingPairing(credentials: Credentials, controlPlane: string): PendingPairing | undefined;
89
+ export declare function dropPendingPairing(credentials: Credentials, controlPlane: string): Credentials;
90
+ export declare function putSession(credentials: Credentials, session: StoredSession): Credentials;
91
+ export declare function findSession(credentials: Credentials, controlPlane: string): StoredSession | undefined;
92
+ export declare function dropSession(credentials: Credentials, controlPlane: string): Credentials;
93
+ /**
94
+ * One agent credential per repository per control plane, newest wins. Keyed on
95
+ * both, because a workspace holds many repositories and each one's connection
96
+ * reaches exactly that repository.
97
+ */
98
+ export declare function putAgent(credentials: Credentials, agent: StoredAgent): Credentials;
package/dist/store.js ADDED
@@ -0,0 +1,225 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { closeSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
3
+ import { randomBytes } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, parse, resolve } from "node:path";
6
+ export class StoreError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "StoreError";
11
+ this.code = code;
12
+ }
13
+ }
14
+ const EMPTY = { version: 1, pendingPairings: [], sessions: [], agents: [] };
15
+ export function configHome(environment = process.env) {
16
+ const explicit = environment.BALLADEER_CONFIG_HOME?.trim();
17
+ if (explicit)
18
+ return resolve(explicit);
19
+ const xdg = environment.XDG_CONFIG_HOME?.trim();
20
+ if (xdg)
21
+ return resolve(join(xdg, "balladeer"));
22
+ return resolve(join(homedir(), ".config", "balladeer"));
23
+ }
24
+ export function credentialsPath(environment = process.env) {
25
+ return join(configHome(environment), "credentials.json");
26
+ }
27
+ function isTrackedByGit(directory, file) {
28
+ try {
29
+ execFileSync("git", ["-C", directory, "check-ignore", "--quiet", file], {
30
+ stdio: "ignore",
31
+ });
32
+ // Exit 0 means the path is ignored, so writing there would not be committed.
33
+ return false;
34
+ }
35
+ catch (error) {
36
+ const status = error.status;
37
+ // Exit 1 means "not ignored", which for a path inside a work tree means a
38
+ // commit would pick the credential up. Anything else (128, git absent) means
39
+ // we could not tell, and refusing on a guess would brick setup.
40
+ return status === 1;
41
+ }
42
+ }
43
+ function nearestGitDirectory(from) {
44
+ let current = resolve(from);
45
+ const { root } = parse(current);
46
+ for (;;) {
47
+ try {
48
+ lstatSync(join(current, ".git"));
49
+ return current;
50
+ }
51
+ catch {
52
+ // Keep walking.
53
+ }
54
+ if (current === root)
55
+ return undefined;
56
+ current = dirname(current);
57
+ }
58
+ }
59
+ /**
60
+ * The credential must not land somewhere a commit would pick it up.
61
+ *
62
+ * An explicitly chosen location, or one under the current working directory, is
63
+ * refused outright the moment it sits in a work tree. The default
64
+ * `~/.config/balladeer` is only refused when git says the file would actually be
65
+ * tracked, because a great many developers keep their dotfiles in a repository
66
+ * and refusing all of them would brick setup for no gain.
67
+ */
68
+ export function assertSafeStoreLocation(directory, environment = process.env, cwd = process.cwd()) {
69
+ const worktree = nearestGitDirectory(directory);
70
+ if (!worktree)
71
+ return;
72
+ const explicit = Boolean(environment.BALLADEER_CONFIG_HOME?.trim());
73
+ const underCwd = resolve(directory).startsWith(`${resolve(cwd)}/`) || resolve(directory) === resolve(cwd);
74
+ if (explicit || underCwd || isTrackedByGit(worktree, join(directory, "credentials.json"))) {
75
+ throw new StoreError("credential_store_inside_git_worktree", `Balladeer will not write a credential inside a git work tree (${worktree}). Set BALLADEER_CONFIG_HOME to a directory outside every repository.`);
76
+ }
77
+ }
78
+ function assertPrivate(path, kind) {
79
+ const stats = lstatSync(path);
80
+ if (stats.isSymbolicLink()) {
81
+ throw new StoreError("credential_store_unsafe", `${path} is a symbolic link. Balladeer will not follow one to write a credential.`);
82
+ }
83
+ // Checked on every read and write, not only at creation: a store made private
84
+ // once but widened later is exactly as readable as one never protected.
85
+ if ((stats.mode & 0o077) !== 0) {
86
+ throw new StoreError("credential_store_unsafe", `${path} is readable by other users. Run: chmod ${kind === "directory" ? "700" : "600"} ${path}`);
87
+ }
88
+ }
89
+ export function ensureStoreDirectory(environment = process.env) {
90
+ const directory = configHome(environment);
91
+ assertSafeStoreLocation(directory, environment);
92
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
93
+ assertPrivate(directory, "directory");
94
+ return directory;
95
+ }
96
+ export function readCredentials(environment = process.env) {
97
+ const path = credentialsPath(environment);
98
+ let raw;
99
+ try {
100
+ assertPrivate(path, "file");
101
+ raw = readFileSync(path, "utf8");
102
+ }
103
+ catch (error) {
104
+ if (error instanceof StoreError)
105
+ throw error;
106
+ return { ...EMPTY, pendingPairings: [], sessions: [], agents: [] };
107
+ }
108
+ try {
109
+ const parsed = JSON.parse(raw);
110
+ return {
111
+ version: 1,
112
+ pendingPairings: Array.isArray(parsed.pendingPairings) ? parsed.pendingPairings : [],
113
+ sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
114
+ agents: Array.isArray(parsed.agents) ? parsed.agents : [],
115
+ };
116
+ }
117
+ catch {
118
+ throw new StoreError("credential_store_unreadable", `${path} is not valid JSON. Balladeer will not overwrite a file it cannot read.`);
119
+ }
120
+ }
121
+ /**
122
+ * Atomic, and never through a path an attacker could have pre-planted: the
123
+ * temporary file is opened with `wx` (fail if it exists) under a random name,
124
+ * its mode is set on the open descriptor, and it is flushed before the rename.
125
+ */
126
+ export function writeCredentials(credentials, environment = process.env) {
127
+ const directory = ensureStoreDirectory(environment);
128
+ const path = join(directory, "credentials.json");
129
+ const temporary = join(directory, `.credentials.${randomBytes(8).toString("hex")}.tmp`);
130
+ let descriptor;
131
+ try {
132
+ descriptor = openSync(temporary, "wx", 0o600);
133
+ fchmodSync(descriptor, 0o600);
134
+ writeSync(descriptor, `${JSON.stringify(credentials, null, 2)}\n`);
135
+ fsyncSync(descriptor);
136
+ closeSync(descriptor);
137
+ descriptor = undefined;
138
+ renameSync(temporary, path);
139
+ }
140
+ catch (error) {
141
+ if (descriptor !== undefined) {
142
+ try {
143
+ closeSync(descriptor);
144
+ }
145
+ catch {
146
+ // Nothing further to do; the unlink below is the cleanup that matters.
147
+ }
148
+ }
149
+ try {
150
+ unlinkSync(temporary);
151
+ }
152
+ catch {
153
+ // The temporary file may never have been created.
154
+ }
155
+ if (error instanceof StoreError)
156
+ throw error;
157
+ throw new StoreError("credential_store_unwritable", `Balladeer could not write ${path}: ${error instanceof Error ? error.message : "unknown reason"}`);
158
+ }
159
+ }
160
+ /** Origins, not URL strings, so `https://host/../` cannot alias a stored entry. */
161
+ export function normalizeControlPlane(value) {
162
+ let url;
163
+ try {
164
+ url = new URL(value);
165
+ }
166
+ catch {
167
+ throw new StoreError("invalid_control_plane", `${value} is not a URL.`);
168
+ }
169
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1";
170
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
171
+ throw new StoreError("invalid_control_plane", `${value} is not https. Balladeer will not send a pairing secret over http.`);
172
+ }
173
+ return url.origin;
174
+ }
175
+ /** At most one pending pairing per control plane, newest wins. */
176
+ export function putPendingPairing(credentials, pending) {
177
+ return {
178
+ ...credentials,
179
+ pendingPairings: [
180
+ ...credentials.pendingPairings.filter((entry) => entry.controlPlane !== pending.controlPlane),
181
+ pending,
182
+ ],
183
+ };
184
+ }
185
+ export function findPendingPairing(credentials, controlPlane) {
186
+ return credentials.pendingPairings.find((entry) => entry.controlPlane === controlPlane);
187
+ }
188
+ export function dropPendingPairing(credentials, controlPlane) {
189
+ return {
190
+ ...credentials,
191
+ pendingPairings: credentials.pendingPairings.filter((entry) => entry.controlPlane !== controlPlane),
192
+ };
193
+ }
194
+ export function putSession(credentials, session) {
195
+ return {
196
+ ...credentials,
197
+ sessions: [
198
+ ...credentials.sessions.filter((entry) => entry.controlPlane !== session.controlPlane),
199
+ session,
200
+ ],
201
+ };
202
+ }
203
+ export function findSession(credentials, controlPlane) {
204
+ return credentials.sessions.find((entry) => entry.controlPlane === controlPlane);
205
+ }
206
+ export function dropSession(credentials, controlPlane) {
207
+ return {
208
+ ...credentials,
209
+ sessions: credentials.sessions.filter((entry) => entry.controlPlane !== controlPlane),
210
+ };
211
+ }
212
+ /**
213
+ * One agent credential per repository per control plane, newest wins. Keyed on
214
+ * both, because a workspace holds many repositories and each one's connection
215
+ * reaches exactly that repository.
216
+ */
217
+ export function putAgent(credentials, agent) {
218
+ return {
219
+ ...credentials,
220
+ agents: [
221
+ ...credentials.agents.filter((entry) => entry.controlPlane !== agent.controlPlane || entry.repositoryId !== agent.repositoryId),
222
+ agent,
223
+ ],
224
+ };
225
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * The touch map: which files each promise's verifier actually executed.
3
+ *
4
+ * A promise's scope surfaces are what somebody wrote down when they agreed it.
5
+ * They are a guess about which files matter, they age, and nobody rewrites them
6
+ * when a module moves. What a verifier executed is not a guess: it is the file
7
+ * list V8 recorded while the check ran. So an agent asking "which promises does
8
+ * this change touch" gets an answer measured from the repository in front of it
9
+ * rather than one somebody typed months ago.
10
+ *
11
+ * Two rules hold the whole feature up, and both are structural rather than
12
+ * remembered. The map is written under `.continuity/` and nothing in this
13
+ * command sends it anywhere: it names a customer's file tree, which is the one
14
+ * thing Balladeer promises never to receive. And every entry carries the digest
15
+ * of the verifier that produced it, so a map computed against a verifier that
16
+ * has since changed reads as stale rather than as fact.
17
+ */
18
+ /** The directory a repository keeps its promises, packages and map in. */
19
+ export declare const CONTINUITY_DIRECTORY = ".continuity";
20
+ /** Where a promise's sealed files live. Everything under it is the verifier itself. */
21
+ export declare const PROMISE_TREE_ROOT = ".continuity/promises";
22
+ /** Where the sealed packages live, one JSON file per promise. */
23
+ export declare const PACKAGE_DIRECTORY = ".continuity/packages";
24
+ /** Where the scaffold leaves the readable draft each package was sealed from. */
25
+ export declare const DRAFT_DIRECTORY = ".continuity/drafts";
26
+ /** The map itself, beside the packages it was computed from. */
27
+ export declare const TOUCH_MAP_FILE = ".continuity/touch-map.json";
28
+ /** The document's own version, so a reader can refuse one it does not understand. */
29
+ export declare const TOUCH_MAP_SCHEMA = "balladeer-touch-map/v1";
30
+ /**
31
+ * What one map may hold.
32
+ *
33
+ * A repository with a thousand promises and a hundred thousand files would
34
+ * otherwise write a document nothing can open. The caps are stated in the
35
+ * document when they bite, because a map that quietly stopped early would be
36
+ * read as "nothing else is touched", which is the one thing it must never say
37
+ * by accident.
38
+ */
39
+ export declare const TOUCH_MAP_LIMITS: {
40
+ /** Files one map names. */
41
+ readonly files: 5000;
42
+ /** Promises listed under one file. */
43
+ readonly promisesPerFile: 50;
44
+ /** Characters of one repository-relative path. */
45
+ readonly path: 400;
46
+ /** Characters of one promise's claim, as `affected` prints it. */
47
+ readonly claim: 200;
48
+ };
49
+ /** How a verifier is invoked, as the sealed package declares it. */
50
+ export type VerifierTarget = Readonly<{
51
+ executable: string;
52
+ args: readonly string[];
53
+ cwd?: string;
54
+ timeoutMs?: number;
55
+ }>;
56
+ /** One promise, as this checkout's sealed package file carries it. */
57
+ export type LocalPackage = Readonly<{
58
+ promiseId: string;
59
+ /** The sealed package file, repository-relative. */
60
+ packageFile: string;
61
+ target: VerifierTarget;
62
+ /** Every file the package digest-locks, repository-relative. */
63
+ materialPaths: readonly string[];
64
+ title?: string;
65
+ claim?: string;
66
+ }>;
67
+ /** Why a promise's verifier produced no coverage, in a word a caller can branch on. */
68
+ export type UnmappedReason = "no_verifier_target" | "unsupported_executable" | "cwd_outside_repository" | "did_not_run" | "timed_out" | "no_coverage";
69
+ /** One promise this run could not map, and why. */
70
+ export type UnmappedPromise = Readonly<{
71
+ promiseId: string;
72
+ reason: UnmappedReason;
73
+ /** One sentence a person can act on. Never a page of the verifier's output. */
74
+ detail: string;
75
+ }>;
76
+ /** One promise's claim on one file: it executed it, under this verifier. */
77
+ export type TouchEntry = Readonly<{
78
+ promiseId: string;
79
+ verifierDigest: string;
80
+ }>;
81
+ export type TouchMapFile = Readonly<{
82
+ path: string;
83
+ promises: readonly TouchEntry[];
84
+ }>;
85
+ export type TouchMapPromise = Readonly<{
86
+ promiseId: string;
87
+ verifierDigest: string;
88
+ /** How many files this promise's verifier executed, before any cap. */
89
+ files: number;
90
+ }>;
91
+ export type TouchMap = Readonly<{
92
+ schemaVersion: typeof TOUCH_MAP_SCHEMA;
93
+ generatedAt: string;
94
+ /** True when a cap cut something, so nothing reads a short map as a complete one. */
95
+ truncated: boolean;
96
+ promises: readonly TouchMapPromise[];
97
+ files: readonly TouchMapFile[];
98
+ unmapped: readonly UnmappedPromise[];
99
+ }>;
100
+ /**
101
+ * The first sentence of a stored outcome, never crossing a paragraph break.
102
+ *
103
+ * The same rule the promise index uses, written out rather than imported: this
104
+ * package is published with no dependency of its own, and `affected` prints a
105
+ * claim from a file on the customer's disk with no server in the conversation.
106
+ */
107
+ export declare function firstSentence(value: string): string;
108
+ /**
109
+ * Every promise this checkout has a sealed package for, in id order.
110
+ *
111
+ * It reads the package files and nothing else, so it works with no network, no
112
+ * credential, and no runner checkout. A file that is not a readable sealed
113
+ * package is skipped rather than reported: a half-written draft in that
114
+ * directory is not a promise, and refusing to map the other promises because of
115
+ * it would be worse than leaving it out.
116
+ *
117
+ * The claim is read from the draft the scaffold leaves beside the package when
118
+ * there is one, because that is the readable copy a person edits, and from the
119
+ * package itself when there is not.
120
+ */
121
+ export declare function readLocalPackages(root: string): LocalPackage[];
122
+ /** JSON with its object keys in sorted order, so the same input digests the same. */
123
+ export declare function canonicalJson(value: unknown): string;
124
+ /**
125
+ * The digest a map entry was computed under.
126
+ *
127
+ * It covers exactly what decides which files a verifier executes: how the
128
+ * verifier is invoked, and the bytes of every file the package locks. It is
129
+ * taken from the files on disk rather than from the digests the package
130
+ * declares, so both ways a verifier can change move it: a reseal, and an edit
131
+ * nobody has sealed yet.
132
+ *
133
+ * It is deliberately not the package digest. That moves when somebody fixes a
134
+ * typo in the promise's title, and a map that read stale after a wording change
135
+ * would be rebuilt so often that nobody would trust the word.
136
+ */
137
+ export declare function verifierDigest(root: string, pkg: LocalPackage): string;
138
+ /** A path in the map's own spelling: repository-relative, forward slashes. */
139
+ export declare function repositoryPath(root: string, absolute: string): string | undefined;
140
+ /**
141
+ * Whether a file this verifier executed belongs in the map.
142
+ *
143
+ * The promise tree is out, because those files are the verifier: a map that
144
+ * said "this promise touches its own verifier" would answer every question with
145
+ * itself. Installed dependencies are out too. They are executed by everything,
146
+ * they are nobody's change, and a map that carried them would be mostly
147
+ * `node_modules` and would blow the size cap before it reached the first file
148
+ * anybody edits.
149
+ */
150
+ export declare function mappablePath(path: string): boolean;
151
+ /**
152
+ * The repository files one coverage document says were executed, before any
153
+ * exclusion.
154
+ *
155
+ * V8 records every script the process loaded, including Node's own internals
156
+ * and anything outside this repository. A script counts as executed when any of
157
+ * its ranges ran at least once, which is true of a module the verifier merely
158
+ * imported: importing it runs its top level, and a verifier that imports a file
159
+ * has read the behavior in it.
160
+ *
161
+ * The promise's own tree is still in this answer, because it is what proves the
162
+ * verifier ran at all. `mappablePath` is what takes it back out before anything
163
+ * is written down.
164
+ */
165
+ export declare function executedPaths(root: string, document: unknown): string[];
166
+ /** Every coverage document V8 left in one directory, unparsed ones skipped. */
167
+ export declare function readCoverageDirectory(directory: string): unknown[];
168
+ /** One promise's measured result: the files it executed, under one digest. */
169
+ export type PromiseCoverage = Readonly<{
170
+ promiseId: string;
171
+ verifierDigest: string;
172
+ paths: readonly string[];
173
+ }>;
174
+ /**
175
+ * The map document, assembled from what each verifier executed.
176
+ *
177
+ * Ordering is total and by name at every level, so two runs over an unchanged
178
+ * repository produce the same bytes and a diff shows a behavior change rather
179
+ * than a shuffle.
180
+ */
181
+ export declare function buildTouchMap(input: {
182
+ generatedAt: string;
183
+ covered: readonly PromiseCoverage[];
184
+ unmapped: readonly UnmappedPromise[];
185
+ }): TouchMap;
186
+ /** The document as it is written: two-space JSON with a trailing newline. */
187
+ export declare function serializeTouchMap(map: TouchMap): string;
188
+ /** The same document with the timestamp removed, which is what "unchanged" means. */
189
+ export declare function touchMapBody(map: TouchMap): string;
190
+ export type TouchMapReading = Readonly<{
191
+ kind: "absent";
192
+ }> | Readonly<{
193
+ kind: "unreadable";
194
+ }> | Readonly<{
195
+ kind: "map";
196
+ map: TouchMap;
197
+ }>;
198
+ /** The map this checkout holds, or why there is nothing to read. */
199
+ export declare function readTouchMap(root: string): TouchMapReading;
200
+ /** One promise a path is mapped to, and whether the map still describes it. */
201
+ export type AffectedPromise = Readonly<{
202
+ promiseId: string;
203
+ /** True when the verifier has changed since the map was built. */
204
+ stale: boolean;
205
+ title?: string;
206
+ claim?: string;
207
+ }>;
208
+ export type AffectedPath = Readonly<{
209
+ path: string;
210
+ promises: readonly AffectedPromise[];
211
+ }>;
212
+ /**
213
+ * Which promises a set of paths touches, and which of those answers have aged.
214
+ *
215
+ * Staleness is per entry rather than per map. A repository where one verifier
216
+ * was repaired this morning still has a true answer for every other promise in
217
+ * it, and marking the whole map stale would throw those away.
218
+ */
219
+ export declare function affectedPaths(map: TouchMap, paths: readonly string[], currentDigests: ReadonlyMap<string, string>, details: ReadonlyMap<string, LocalPackage>): AffectedPath[];
220
+ /**
221
+ * The environment a verifier is measured in.
222
+ *
223
+ * The same closed allow-list the pinned runner gives a verifier, plus the two
224
+ * variables this measurement needs: where V8 writes coverage, and where a
225
+ * native-protocol verifier writes its verdict. It is an allow-list rather than
226
+ * a subtraction so a control-plane token, a cloud credential, or anything else
227
+ * this terminal happens to hold cannot reach a customer's verifier because
228
+ * somebody forgot to name it.
229
+ */
230
+ export declare function touchMapEnvironment(environment: Record<string, string | undefined>, coverageDirectory: string, resultPath?: string): Record<string, string>;
231
+ /**
232
+ * Whether this release can measure a verifier invoked that way.
233
+ *
234
+ * Node only, for now. Coverage is collected by the runtime executing the
235
+ * verifier, so every language needs its own collector, and a command that
236
+ * guessed would map a Python verifier to nothing and call it an empty answer.
237
+ * The others are named as unmapped instead, which is a thing a person can read.
238
+ */
239
+ export declare function measurableWithNode(target: VerifierTarget): boolean;
240
+ /** The working directory a verifier runs in, or nothing when it leaves the repository. */
241
+ export declare function verifierCwd(root: string, target: VerifierTarget): string | undefined;