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.
- package/LICENSE +200 -5
- package/README.md +154 -68
- package/dist/agent.d.ts +126 -0
- package/dist/agent.js +209 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +392 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.js +114 -0
- package/dist/commands/affected.d.ts +22 -0
- package/dist/commands/affected.js +122 -0
- package/dist/commands/check-seals.d.ts +37 -0
- package/dist/commands/check-seals.js +289 -0
- package/dist/commands/discover.d.ts +68 -0
- package/dist/commands/discover.js +395 -0
- package/dist/commands/explain.d.ts +35 -0
- package/dist/commands/explain.js +90 -0
- package/dist/commands/invite.d.ts +24 -0
- package/dist/commands/invite.js +197 -0
- package/dist/commands/mcp.d.ts +65 -0
- package/dist/commands/mcp.js +202 -0
- package/dist/commands/propose.d.ts +59 -0
- package/dist/commands/propose.js +262 -0
- package/dist/commands/repositories.d.ts +18 -0
- package/dist/commands/repositories.js +185 -0
- package/dist/commands/setup.d.ts +75 -0
- package/dist/commands/setup.js +1471 -0
- package/dist/commands/status.d.ts +35 -0
- package/dist/commands/status.js +482 -0
- package/dist/commands/touch-map.d.ts +42 -0
- package/dist/commands/touch-map.js +251 -0
- package/dist/commands/whoami.d.ts +8 -0
- package/dist/commands/whoami.js +79 -0
- package/dist/conventions.d.ts +69 -0
- package/dist/conventions.js +175 -0
- package/dist/copy.d.ts +148 -0
- package/dist/copy.js +459 -0
- package/dist/currency.d.ts +31 -0
- package/dist/currency.js +72 -0
- package/dist/gh.d.ts +80 -0
- package/dist/gh.js +188 -0
- package/dist/git.d.ts +76 -0
- package/dist/git.js +203 -0
- package/dist/markers.d.ts +76 -0
- package/dist/markers.js +125 -0
- package/dist/mcp-config.d.ts +99 -0
- package/dist/mcp-config.js +230 -0
- package/dist/release.d.ts +55 -0
- package/dist/release.js +67 -0
- package/dist/repository.d.ts +8 -0
- package/dist/repository.js +32 -0
- package/dist/seals.d.ts +48 -0
- package/dist/seals.js +112 -0
- package/dist/store.d.ts +98 -0
- package/dist/store.js +225 -0
- package/dist/touch-map.d.ts +241 -0
- package/dist/touch-map.js +487 -0
- package/dist/wire.d.ts +588 -0
- package/dist/wire.js +20 -0
- package/package.json +19 -10
- package/bin/balladeer.js +0 -136
package/dist/store.d.ts
ADDED
|
@@ -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;
|