balladeer 1.0.14 → 1.0.16

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 (77) hide show
  1. package/README.md +2 -0
  2. package/dist/behavior-map-schema.d.ts +107 -0
  3. package/dist/behavior-map-schema.js +232 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +77 -2
  6. package/dist/commands/guidance.d.ts +1 -1
  7. package/dist/commands/guidance.js +6 -0
  8. package/dist/commands/judge.d.ts +271 -0
  9. package/dist/commands/judge.js +1170 -0
  10. package/dist/commands/map.d.ts +120 -0
  11. package/dist/commands/map.js +601 -0
  12. package/dist/commands/mcp.d.ts +6 -0
  13. package/dist/commands/mcp.js +53 -0
  14. package/dist/commands/offers.d.ts +265 -0
  15. package/dist/commands/offers.js +801 -0
  16. package/dist/commands/risk.d.ts +29 -0
  17. package/dist/commands/risk.js +133 -0
  18. package/dist/copy.d.ts +24 -1
  19. package/dist/copy.js +91 -0
  20. package/dist/guidance-hook.mjs +230 -74
  21. package/dist/guidance.d.ts +2 -0
  22. package/dist/guidance.js +7 -1
  23. package/dist/headless-agent.d.ts +166 -0
  24. package/dist/headless-agent.js +416 -0
  25. package/dist/hook-trust.d.ts +31 -0
  26. package/dist/hook-trust.js +63 -0
  27. package/dist/judge-brief.d.ts +36 -0
  28. package/dist/judge-brief.js +89 -0
  29. package/dist/judge-hook.d.ts +84 -0
  30. package/dist/judge-hook.js +420 -0
  31. package/dist/map-brief.d.ts +16 -0
  32. package/dist/map-brief.js +41 -0
  33. package/dist/offer-brief.d.ts +95 -0
  34. package/dist/offer-brief.js +217 -0
  35. package/dist/owned-process.d.ts +65 -0
  36. package/dist/owned-process.js +146 -0
  37. package/dist/promise-meaning.d.ts +126 -0
  38. package/dist/promise-meaning.js +292 -0
  39. package/dist/risk/contract.d.ts +145 -0
  40. package/dist/risk/contract.js +74 -0
  41. package/dist/risk/describe.d.ts +7 -0
  42. package/dist/risk/describe.js +45 -0
  43. package/dist/risk/diff.d.ts +26 -0
  44. package/dist/risk/diff.js +174 -0
  45. package/dist/risk/extract.d.ts +43 -0
  46. package/dist/risk/extract.js +336 -0
  47. package/dist/risk/git.d.ts +30 -0
  48. package/dist/risk/git.js +118 -0
  49. package/dist/risk/import-graph.d.ts +41 -0
  50. package/dist/risk/import-graph.js +487 -0
  51. package/dist/risk/index.d.ts +20 -0
  52. package/dist/risk/index.js +20 -0
  53. package/dist/risk/paths.d.ts +16 -0
  54. package/dist/risk/paths.js +73 -0
  55. package/dist/risk/pipeline.d.ts +47 -0
  56. package/dist/risk/pipeline.js +121 -0
  57. package/dist/risk/priors.d.ts +18 -0
  58. package/dist/risk/priors.js +86 -0
  59. package/dist/risk/resources.d.ts +56 -0
  60. package/dist/risk/resources.js +374 -0
  61. package/dist/risk/score.d.ts +85 -0
  62. package/dist/risk/score.js +543 -0
  63. package/dist/risk/symbols.d.ts +35 -0
  64. package/dist/risk/symbols.js +348 -0
  65. package/dist/risk/text.d.ts +43 -0
  66. package/dist/risk/text.js +277 -0
  67. package/dist/risk/validate.d.ts +19 -0
  68. package/dist/risk/validate.js +154 -0
  69. package/dist/scratch-worktree.d.ts +51 -0
  70. package/dist/scratch-worktree.js +153 -0
  71. package/dist/self-update.d.ts +29 -2
  72. package/dist/self-update.js +96 -11
  73. package/dist/user-scope.d.ts +2 -0
  74. package/dist/user-scope.js +26 -2
  75. package/dist/wire.d.ts +55 -2
  76. package/dist/wire.js +5 -1
  77. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ import { type BehaviorMapMeaning } from "./behavior-map-schema.js";
2
+ /**
3
+ * One promise's agreed meaning, the way the map builder and the judge read it.
4
+ *
5
+ * It comes from one of two places. With this repository's agent connection it
6
+ * is `get_promise`'s own answer. Without one, it is a file holding that same
7
+ * shape, which is how the evaluation harness hands over promises it wrote for
8
+ * public repositories that have never heard of Balladeer. A sealed package's
9
+ * `promise` object is accepted too, because it carries the same meaning under
10
+ * slightly different keys and this repository's own promises live that way.
11
+ */
12
+ export type PromiseExample = Readonly<{
13
+ label: string;
14
+ setup: string;
15
+ expectedOutcome: string;
16
+ saidWords?: string;
17
+ }>;
18
+ export type PromiseMeaning = Readonly<{
19
+ title: string;
20
+ observableOutcome: string;
21
+ nonGoals: readonly string[];
22
+ passingExamples: readonly PromiseExample[];
23
+ failingExamples: readonly PromiseExample[];
24
+ refactorExamples?: readonly PromiseExample[];
25
+ scope?: Readonly<{
26
+ surfaces?: readonly string[];
27
+ labels?: readonly string[];
28
+ }>;
29
+ [field: string]: unknown;
30
+ }>;
31
+ export type PromiseDocument = Readonly<{
32
+ promiseId: string;
33
+ title: string;
34
+ semanticDigest: string;
35
+ meaning: PromiseMeaning;
36
+ }>;
37
+ /** Deterministic JSON with sorted keys, the form Balladeer digests a meaning in. */
38
+ export declare function canonicalJson(value: unknown): string;
39
+ export declare function sha256Digest(value: unknown): string;
40
+ /**
41
+ * The promise a file or a tool answer describes, or the reason it describes
42
+ * none. The meaning is kept whole, every field as it came, because the judge is
43
+ * handed it verbatim; only the fields this command reads are checked.
44
+ */
45
+ export declare function normalizePromise(value: unknown): {
46
+ ok: true;
47
+ promise: PromiseDocument;
48
+ } | {
49
+ ok: false;
50
+ reason: string;
51
+ };
52
+ export declare function readPromiseFile(path: string): {
53
+ ok: true;
54
+ promise: PromiseDocument;
55
+ } | {
56
+ ok: false;
57
+ reason: string;
58
+ };
59
+ /** One failing case as a sentence, which is what the scorer compares a change against. */
60
+ export declare function exampleText(example: PromiseExample): string;
61
+ /** The excerpt a map carries so the scorer never needs the server. */
62
+ export declare function meaningExcerpt(promise: PromiseDocument): BehaviorMapMeaning;
63
+ /**
64
+ * A promise rebuilt from a map's excerpt, for a judge that has no connection
65
+ * and no file. It carries no passing cases, and the judge brief says so.
66
+ */
67
+ export declare function promiseFromExcerpt(promiseId: string, semanticDigest: string, excerpt: BehaviorMapMeaning): PromiseDocument;
68
+ /** Case ids in the order the meaning lists them. */
69
+ export declare function caseIds(promise: PromiseDocument): {
70
+ passing: string[];
71
+ failing: string[];
72
+ refactor: string[];
73
+ };
74
+ export type PromiseReader = Readonly<{
75
+ get(promiseId: string): Promise<{
76
+ ok: true;
77
+ promise: PromiseDocument;
78
+ } | {
79
+ ok: false;
80
+ reason: string;
81
+ }>;
82
+ /**
83
+ * Every agreed promise's id, page after page. With `max`, at most that many,
84
+ * and `more` says whether the index held further ids beyond them.
85
+ */
86
+ list(options?: Readonly<{
87
+ max?: number;
88
+ }>): Promise<{
89
+ ok: true;
90
+ ids: string[];
91
+ more?: boolean;
92
+ } | {
93
+ ok: false;
94
+ reason: string;
95
+ }>;
96
+ }>;
97
+ /**
98
+ * What to tell a person when the server refuses the unattended read because it
99
+ * is older than this command, or nothing when the refusal is about anything
100
+ * else.
101
+ *
102
+ * The command reaches people before the server that understands it can: a
103
+ * server that has never heard of `unattended` refuses the whole read, and its
104
+ * own words for that are about a key nobody typed. The read is never retried
105
+ * without the field, because a read that counts as the person having seen the
106
+ * promise would silence what they are still to be told about it. So the person
107
+ * is told what happened and what works meanwhile.
108
+ */
109
+ export declare function olderServerRefusal(text: string): string | undefined;
110
+ /**
111
+ * This repository's agent connection as a reader of promises, or the reason
112
+ * there is none. Reading is all it does: `get_promise` and `list_promises`,
113
+ * the same two reads an agent in this repository makes.
114
+ */
115
+ export declare function connectionReader(input: Readonly<{
116
+ environment: NodeJS.ProcessEnv;
117
+ controlPlane: string;
118
+ cwd: string;
119
+ repository?: string;
120
+ }>): {
121
+ ok: true;
122
+ reader: PromiseReader;
123
+ } | {
124
+ ok: false;
125
+ reason: string;
126
+ };
@@ -0,0 +1,292 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { callAgentTool, selectAgent } from "./agent.js";
4
+ import { PROMISE_ID } from "./behavior-map-schema.js";
5
+ import { repositoryHints } from "./repository.js";
6
+ import { readCredentials } from "./store.js";
7
+ /** Deterministic JSON with sorted keys, the form Balladeer digests a meaning in. */
8
+ export function canonicalJson(value) {
9
+ const canonical = (item) => {
10
+ if (Array.isArray(item))
11
+ return item.map(canonical);
12
+ if (item !== null && typeof item === "object") {
13
+ const out = {};
14
+ for (const key of Object.keys(item).sort()) {
15
+ const child = item[key];
16
+ if (child !== undefined)
17
+ out[key] = canonical(child);
18
+ }
19
+ return out;
20
+ }
21
+ return item;
22
+ };
23
+ return JSON.stringify(canonical(value));
24
+ }
25
+ export function sha256Digest(value) {
26
+ return `sha256:${createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
27
+ }
28
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
29
+ function examples(value) {
30
+ if (!Array.isArray(value))
31
+ return undefined;
32
+ const out = [];
33
+ for (const entry of value) {
34
+ if (entry === null || typeof entry !== "object")
35
+ return undefined;
36
+ const row = entry;
37
+ if (typeof row.setup !== "string" || typeof row.expectedOutcome !== "string")
38
+ return undefined;
39
+ out.push({
40
+ label: typeof row.label === "string" ? row.label : "",
41
+ setup: row.setup,
42
+ expectedOutcome: row.expectedOutcome,
43
+ ...(typeof row.saidWords === "string" ? { saidWords: row.saidWords } : {}),
44
+ });
45
+ }
46
+ return out;
47
+ }
48
+ /**
49
+ * The promise a file or a tool answer describes, or the reason it describes
50
+ * none. The meaning is kept whole, every field as it came, because the judge is
51
+ * handed it verbatim; only the fields this command reads are checked.
52
+ */
53
+ export function normalizePromise(value) {
54
+ if (value === null || typeof value !== "object" || Array.isArray(value))
55
+ return { ok: false, reason: "it is not a JSON object" };
56
+ const input = value;
57
+ if (input.found === false)
58
+ return {
59
+ ok: false,
60
+ reason: typeof input.reason === "string" ? input.reason : "Balladeer has no such promise",
61
+ };
62
+ let promiseId;
63
+ let digest;
64
+ let meaning;
65
+ const packaged = input.promise;
66
+ if (packaged !== null &&
67
+ typeof packaged === "object" &&
68
+ !Array.isArray(packaged) &&
69
+ input.meaning === undefined) {
70
+ // A sealed package: the meaning's fields sit beside the id, owner and digest.
71
+ const rest = { ...packaged };
72
+ promiseId = rest.id;
73
+ digest = rest.semanticDigest;
74
+ delete rest.id;
75
+ delete rest.ownerId;
76
+ delete rest.semanticDigest;
77
+ meaning = rest;
78
+ }
79
+ else {
80
+ if (input.meaning === null || typeof input.meaning !== "object" || Array.isArray(input.meaning))
81
+ return { ok: false, reason: "it carries no meaning object" };
82
+ promiseId = input.promiseId ?? input.id;
83
+ digest = input.semanticDigest;
84
+ meaning = input.meaning;
85
+ }
86
+ if (typeof promiseId !== "string" || !PROMISE_ID.test(promiseId))
87
+ return { ok: false, reason: "it names no usable promise id" };
88
+ const passing = examples(meaning.passingExamples);
89
+ const failing = examples(meaning.failingExamples);
90
+ if (typeof meaning.title !== "string" || meaning.title.trim() === "")
91
+ return { ok: false, reason: "its meaning has no title" };
92
+ if (typeof meaning.observableOutcome !== "string")
93
+ return { ok: false, reason: "its meaning has no observable outcome" };
94
+ if (passing === undefined || failing === undefined)
95
+ return { ok: false, reason: "its meaning's passing and failing cases are unreadable" };
96
+ const nonGoals = Array.isArray(meaning.nonGoals)
97
+ ? meaning.nonGoals.filter((goal) => typeof goal === "string")
98
+ : [];
99
+ const semanticDigest = typeof digest === "string" && DIGEST.test(digest) ? digest : sha256Digest(meaning);
100
+ const title = typeof input.title === "string" && input.title.trim() !== "" ? input.title : meaning.title;
101
+ const refactor = examples(meaning.refactorExamples);
102
+ return {
103
+ ok: true,
104
+ promise: {
105
+ promiseId,
106
+ title,
107
+ semanticDigest,
108
+ meaning: {
109
+ ...meaning,
110
+ title: meaning.title,
111
+ observableOutcome: meaning.observableOutcome,
112
+ nonGoals,
113
+ passingExamples: passing,
114
+ failingExamples: failing,
115
+ ...(refactor === undefined ? {} : { refactorExamples: refactor }),
116
+ },
117
+ },
118
+ };
119
+ }
120
+ export function readPromiseFile(path) {
121
+ let raw;
122
+ try {
123
+ raw = readFileSync(path, "utf8");
124
+ }
125
+ catch (error) {
126
+ return {
127
+ ok: false,
128
+ reason: `${path} could not be read: ${error instanceof Error ? error.message : String(error)}`,
129
+ };
130
+ }
131
+ let value;
132
+ try {
133
+ value = JSON.parse(raw);
134
+ }
135
+ catch {
136
+ return { ok: false, reason: `${path} is not JSON` };
137
+ }
138
+ const normalized = normalizePromise(value);
139
+ return normalized.ok
140
+ ? normalized
141
+ : { ok: false, reason: `${path} does not hold a promise: ${normalized.reason}` };
142
+ }
143
+ /** One failing case as a sentence, which is what the scorer compares a change against. */
144
+ export function exampleText(example) {
145
+ const label = example.label.trim();
146
+ return `${label ? `${label}: ` : ""}${example.setup.trim()} Then: ${example.expectedOutcome.trim()}`;
147
+ }
148
+ /** The excerpt a map carries so the scorer never needs the server. */
149
+ export function meaningExcerpt(promise) {
150
+ const surfaces = promise.meaning.scope?.surfaces?.filter((surface) => typeof surface === "string" && surface.trim() !== "");
151
+ return {
152
+ title: promise.meaning.title,
153
+ observableOutcome: promise.meaning.observableOutcome,
154
+ failingExamples: promise.meaning.failingExamples.map((example, index) => ({
155
+ id: `failing-${index + 1}`,
156
+ text: exampleText(example),
157
+ })),
158
+ nonGoals: [...promise.meaning.nonGoals],
159
+ ...(surfaces !== undefined && surfaces.length > 0 ? { surfaces } : {}),
160
+ };
161
+ }
162
+ /**
163
+ * A promise rebuilt from a map's excerpt, for a judge that has no connection
164
+ * and no file. It carries no passing cases, and the judge brief says so.
165
+ */
166
+ export function promiseFromExcerpt(promiseId, semanticDigest, excerpt) {
167
+ return {
168
+ promiseId,
169
+ title: excerpt.title,
170
+ semanticDigest,
171
+ meaning: {
172
+ title: excerpt.title,
173
+ observableOutcome: excerpt.observableOutcome,
174
+ nonGoals: excerpt.nonGoals,
175
+ passingExamples: [],
176
+ failingExamples: excerpt.failingExamples.map((example) => ({
177
+ label: example.id,
178
+ setup: example.text,
179
+ expectedOutcome: "",
180
+ })),
181
+ },
182
+ };
183
+ }
184
+ /** Case ids in the order the meaning lists them. */
185
+ export function caseIds(promise) {
186
+ return {
187
+ passing: promise.meaning.passingExamples.map((_, index) => `passing-${index + 1}`),
188
+ failing: promise.meaning.failingExamples.map((_, index) => `failing-${index + 1}`),
189
+ refactor: (promise.meaning.refactorExamples ?? []).map((_, index) => `refactor-${index + 1}`),
190
+ };
191
+ }
192
+ /** How many index pages one listing reads before it stops and says there was more. */
193
+ const LIST_PAGES = 20;
194
+ /**
195
+ * What to tell a person when the server refuses the unattended read because it
196
+ * is older than this command, or nothing when the refusal is about anything
197
+ * else.
198
+ *
199
+ * The command reaches people before the server that understands it can: a
200
+ * server that has never heard of `unattended` refuses the whole read, and its
201
+ * own words for that are about a key nobody typed. The read is never retried
202
+ * without the field, because a read that counts as the person having seen the
203
+ * promise would silence what they are still to be told about it. So the person
204
+ * is told what happened and what works meanwhile.
205
+ */
206
+ export function olderServerRefusal(text) {
207
+ if (!/\bunattended\b/.test(text) || !/unrecognized|unknown|unexpected|invalid/i.test(text))
208
+ return undefined;
209
+ return ("This Balladeer server is older than this command. It cannot yet read a promise for a " +
210
+ "program without counting that as you having seen it, so nothing was read. This works once " +
211
+ "the server is updated; until then, hand the promise over as a file with --from-file.");
212
+ }
213
+ /**
214
+ * This repository's agent connection as a reader of promises, or the reason
215
+ * there is none. Reading is all it does: `get_promise` and `list_promises`,
216
+ * the same two reads an agent in this repository makes.
217
+ */
218
+ export function connectionReader(input) {
219
+ let agent;
220
+ try {
221
+ const credentials = readCredentials(input.environment);
222
+ const selection = selectAgent(credentials.agents, input.controlPlane, input.repository, repositoryHints(input.cwd));
223
+ if (selection.kind === "refused")
224
+ return { ok: false, reason: selection.reason };
225
+ agent = selection.agent;
226
+ }
227
+ catch (error) {
228
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
229
+ }
230
+ const refusal = (kind) => kind === "unreachable"
231
+ ? `Balladeer could not be reached at ${input.controlPlane}`
232
+ : kind === "unauthorized"
233
+ ? "Balladeer refused this repository's agent connection"
234
+ : kind === "client_too_old"
235
+ ? "this copy of the Balladeer command is too old for the server"
236
+ : "Balladeer could not answer";
237
+ return {
238
+ ok: true,
239
+ reader: {
240
+ async get(promiseId) {
241
+ // Unattended: a program reading on the agent's behalf. The server
242
+ // then leaves the person's "seen" bookkeeping untouched.
243
+ const call = await callAgentTool(agent, "get_promise", { promiseId, unattended: true });
244
+ if (call.kind === "tool_refusal")
245
+ return { ok: false, reason: olderServerRefusal(call.text) ?? call.text };
246
+ if (call.kind !== "result")
247
+ return { ok: false, reason: refusal(call.kind) };
248
+ return normalizePromise(call.structured);
249
+ },
250
+ async list(options) {
251
+ const max = options?.max ?? Number.POSITIVE_INFINITY;
252
+ const ids = [];
253
+ const seen = new Set();
254
+ let cursor;
255
+ let more = false;
256
+ for (let page = 0;; page += 1) {
257
+ if (page >= LIST_PAGES || ids.length >= max) {
258
+ // Stopped with the index still offering a next page.
259
+ more = cursor !== undefined;
260
+ break;
261
+ }
262
+ const call = await callAgentTool(agent, "list_promises", {
263
+ limit: 100,
264
+ ...(cursor === undefined ? {} : { cursor }),
265
+ });
266
+ if (call.kind === "tool_refusal")
267
+ return { ok: false, reason: call.text };
268
+ if (call.kind !== "result")
269
+ return { ok: false, reason: refusal(call.kind) };
270
+ const structured = call.structured;
271
+ for (const row of Array.isArray(structured.promises) ? structured.promises : []) {
272
+ const id = row?.promiseId;
273
+ if (typeof id === "string" && PROMISE_ID.test(id) && !seen.has(id)) {
274
+ seen.add(id);
275
+ ids.push(id);
276
+ }
277
+ }
278
+ const next = structured.nextCursor;
279
+ // A cursor handed back unchanged would page the same rows forever.
280
+ if (typeof next !== "string" || next === "" || next === cursor) {
281
+ cursor = undefined;
282
+ break;
283
+ }
284
+ cursor = next;
285
+ }
286
+ if (ids.length > max)
287
+ return { ok: true, ids: ids.slice(0, max), more: true };
288
+ return { ok: true, ids, more };
289
+ },
290
+ },
291
+ };
292
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The change extraction (`balladeer-change-extraction/v1`) and the risk report
3
+ * (`balladeer-risk/v1`), as `docs/agentic-testing/contracts.md` spells them,
4
+ * with the additions recorded under "Contract notes" on that page.
5
+ *
6
+ * Plain types rather than a schema library: the published command carries one
7
+ * reviewed runtime dependency and no other. `validate.ts` checks both shapes by
8
+ * hand, and the behavior map's own shape and checker are in
9
+ * `../behavior-map-schema.ts`, shared with `balladeer map`.
10
+ */
11
+ export type { BehaviorMap, BehaviorMapEntryPoint, BehaviorMapLinkedTest, BehaviorMapMeaning, BehaviorMapResource, BehaviorMapSink, BehaviorMapSymbol, } from "../behavior-map-schema.js";
12
+ export declare const CHANGE_EXTRACTION_SCHEMA_VERSION = "balladeer-change-extraction/v1";
13
+ export declare const RISK_REPORT_SCHEMA_VERSION = "balladeer-risk/v1";
14
+ export declare const RESOURCE_KINDS: readonly ["table", "column", "route", "flag", "env", "queue", "event", "outbound", "file", "config"];
15
+ export type ResourceKind = (typeof RESOURCE_KINDS)[number];
16
+ export declare const SYMBOL_KINDS: readonly ["function", "class", "method", "const", "type", "module"];
17
+ export type SymbolKind = (typeof SYMBOL_KINDS)[number];
18
+ export declare const LANGUAGES: readonly ["ts", "tsx", "js", "py", "go", "rb", "java", "kt", "rs", "other"];
19
+ export type Language = (typeof LANGUAGES)[number];
20
+ export type Hunk = {
21
+ oldStart: number;
22
+ oldLines: number;
23
+ newStart: number;
24
+ newLines: number;
25
+ };
26
+ export declare const SYMBOL_CHANGES: readonly ["added", "modified", "deleted"];
27
+ export type ChangedSymbol = {
28
+ name: string;
29
+ kind: SymbolKind;
30
+ change: (typeof SYMBOL_CHANGES)[number];
31
+ };
32
+ /**
33
+ * `added`: the file names the resource at head and nowhere at base (new reach).
34
+ * `removed`: it named it at base and nowhere at head. `touched`: a changed
35
+ * line, or the body of a changed function, names a resource the file already
36
+ * named.
37
+ */
38
+ export declare const RESOURCE_CHANGES: readonly ["added", "removed", "touched"];
39
+ export type ChangedResource = {
40
+ kind: ResourceKind;
41
+ name: string;
42
+ change: (typeof RESOURCE_CHANGES)[number];
43
+ };
44
+ /** Which reader found the symbols: the repository's own TypeScript compiler, or a pattern. */
45
+ export declare const SYMBOL_EXTRACTORS: readonly ["typescript", "regex"];
46
+ export type SymbolExtractor = (typeof SYMBOL_EXTRACTORS)[number];
47
+ export declare const FILE_STATUSES: readonly ["added", "modified", "deleted", "renamed"];
48
+ export type ChangedFile = {
49
+ path: string;
50
+ status: (typeof FILE_STATUSES)[number];
51
+ oldPath: string | null;
52
+ language: Language;
53
+ isTest: boolean;
54
+ hunks: Hunk[];
55
+ symbolsChanged: ChangedSymbol[];
56
+ identifiersAdded: string[];
57
+ identifiersRemoved: string[];
58
+ resources: ChangedResource[];
59
+ guardDeletions: {
60
+ line: number;
61
+ text: string;
62
+ }[];
63
+ constantChanges: {
64
+ name: string;
65
+ from: string;
66
+ to: string;
67
+ }[];
68
+ /** Absent when no symbols were read, for a binary or an oversized file. */
69
+ symbolExtractor?: SymbolExtractor;
70
+ };
71
+ export declare const DELTA_KINDS: readonly ["behavior", "refactor", "removal", "addition", "config", "schema", "test"];
72
+ export type Delta = {
73
+ text: string;
74
+ files: string[];
75
+ kind: (typeof DELTA_KINDS)[number];
76
+ };
77
+ export declare const INTENT_SOURCES: readonly ["description", "commits", "model"];
78
+ export type Intent = {
79
+ summary: string;
80
+ source: (typeof INTENT_SOURCES)[number];
81
+ };
82
+ export type ChangeExtraction = {
83
+ schemaVersion: typeof CHANGE_EXTRACTION_SCHEMA_VERSION;
84
+ base: string;
85
+ head: string;
86
+ files: ChangedFile[];
87
+ deltas?: Delta[];
88
+ intent?: Intent;
89
+ };
90
+ export declare const SIGNALS: readonly ["symbol_hit", "graph_forward", "graph_reverse", "resource", "linked_test_changed", "sink_reach", "delta_similarity", "intent_clash", "prior_break", "churn"];
91
+ export type Signal = (typeof SIGNALS)[number];
92
+ export declare const BANDS: readonly ["high", "medium", "low", "none", "unmapped"];
93
+ export type Band = (typeof BANDS)[number];
94
+ /**
95
+ * The four fields the contract names are always present, empty when they do
96
+ * not apply. `caseId` and `detail` are additions: the case a sink or a
97
+ * similarity points at, and a short qualifier such as `deleted`,
98
+ * `assertion_removed` or `distance:2`.
99
+ */
100
+ export type Evidence = {
101
+ file: string;
102
+ symbol: string;
103
+ resource: string;
104
+ delta: string;
105
+ caseId?: string;
106
+ detail?: string;
107
+ };
108
+ export type Reason = {
109
+ signal: Signal;
110
+ weight: number;
111
+ evidence: Evidence;
112
+ };
113
+ export type Features = {
114
+ symbol_hit: number;
115
+ graph_forward_distance: number | null;
116
+ graph_reverse_distance: number | null;
117
+ resource_overlap: number;
118
+ linked_test_changed: number;
119
+ sink_reach: number;
120
+ delta_similarity: number;
121
+ intent_clash: number;
122
+ prior_break: number;
123
+ churn: number;
124
+ };
125
+ export type PromiseRisk = {
126
+ promiseId: string;
127
+ title?: string;
128
+ score: number;
129
+ band: Band;
130
+ reasons: Reason[];
131
+ features: Features;
132
+ };
133
+ export type Baselines = {
134
+ scope_only: string[];
135
+ import_closure: string[];
136
+ all: string[];
137
+ random: string[];
138
+ };
139
+ export type RiskReport = {
140
+ schemaVersion: typeof RISK_REPORT_SCHEMA_VERSION;
141
+ base: string;
142
+ head: string;
143
+ promises: PromiseRisk[];
144
+ baselines: Baselines;
145
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The change extraction (`balladeer-change-extraction/v1`) and the risk report
3
+ * (`balladeer-risk/v1`), as `docs/agentic-testing/contracts.md` spells them,
4
+ * with the additions recorded under "Contract notes" on that page.
5
+ *
6
+ * Plain types rather than a schema library: the published command carries one
7
+ * reviewed runtime dependency and no other. `validate.ts` checks both shapes by
8
+ * hand, and the behavior map's own shape and checker are in
9
+ * `../behavior-map-schema.ts`, shared with `balladeer map`.
10
+ */
11
+ export const CHANGE_EXTRACTION_SCHEMA_VERSION = "balladeer-change-extraction/v1";
12
+ export const RISK_REPORT_SCHEMA_VERSION = "balladeer-risk/v1";
13
+ export const RESOURCE_KINDS = [
14
+ "table",
15
+ "column",
16
+ "route",
17
+ "flag",
18
+ "env",
19
+ "queue",
20
+ "event",
21
+ "outbound",
22
+ "file",
23
+ "config",
24
+ ];
25
+ export const SYMBOL_KINDS = ["function", "class", "method", "const", "type", "module"];
26
+ export const LANGUAGES = [
27
+ "ts",
28
+ "tsx",
29
+ "js",
30
+ "py",
31
+ "go",
32
+ "rb",
33
+ "java",
34
+ "kt",
35
+ "rs",
36
+ "other",
37
+ ];
38
+ export const SYMBOL_CHANGES = ["added", "modified", "deleted"];
39
+ /**
40
+ * `added`: the file names the resource at head and nowhere at base (new reach).
41
+ * `removed`: it named it at base and nowhere at head. `touched`: a changed
42
+ * line, or the body of a changed function, names a resource the file already
43
+ * named.
44
+ */
45
+ export const RESOURCE_CHANGES = ["added", "removed", "touched"];
46
+ /** Which reader found the symbols: the repository's own TypeScript compiler, or a pattern. */
47
+ export const SYMBOL_EXTRACTORS = ["typescript", "regex"];
48
+ export const FILE_STATUSES = ["added", "modified", "deleted", "renamed"];
49
+ export const DELTA_KINDS = [
50
+ "behavior",
51
+ "refactor",
52
+ "removal",
53
+ "addition",
54
+ "config",
55
+ "schema",
56
+ "test",
57
+ ];
58
+ export const INTENT_SOURCES = ["description", "commits", "model"];
59
+ // ---------------------------------------------------------------------------
60
+ // Risk report (`balladeer-risk/v1`)
61
+ // ---------------------------------------------------------------------------
62
+ export const SIGNALS = [
63
+ "symbol_hit",
64
+ "graph_forward",
65
+ "graph_reverse",
66
+ "resource",
67
+ "linked_test_changed",
68
+ "sink_reach",
69
+ "delta_similarity",
70
+ "intent_clash",
71
+ "prior_break",
72
+ "churn",
73
+ ];
74
+ export const BANDS = ["high", "medium", "low", "none", "unmapped"];
@@ -0,0 +1,7 @@
1
+ import type { Reason } from "./contract.js";
2
+ /**
3
+ * One reason as a person reads it: what in the change, and why it matters to
4
+ * the promise. Evidence only, never a verdict: a reason says where to look, and
5
+ * a judge says whether the promise still holds.
6
+ */
7
+ export declare function describeReason(reason: Reason): string;