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.
- package/README.md +2 -0
- package/dist/behavior-map-schema.d.ts +107 -0
- package/dist/behavior-map-schema.js +232 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +77 -2
- package/dist/commands/guidance.d.ts +1 -1
- package/dist/commands/guidance.js +6 -0
- package/dist/commands/judge.d.ts +271 -0
- package/dist/commands/judge.js +1170 -0
- package/dist/commands/map.d.ts +120 -0
- package/dist/commands/map.js +601 -0
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +53 -0
- package/dist/commands/offers.d.ts +265 -0
- package/dist/commands/offers.js +801 -0
- package/dist/commands/risk.d.ts +29 -0
- package/dist/commands/risk.js +133 -0
- package/dist/copy.d.ts +24 -1
- package/dist/copy.js +91 -0
- package/dist/guidance-hook.mjs +230 -74
- package/dist/guidance.d.ts +2 -0
- package/dist/guidance.js +7 -1
- package/dist/headless-agent.d.ts +166 -0
- package/dist/headless-agent.js +416 -0
- package/dist/hook-trust.d.ts +31 -0
- package/dist/hook-trust.js +63 -0
- package/dist/judge-brief.d.ts +36 -0
- package/dist/judge-brief.js +89 -0
- package/dist/judge-hook.d.ts +84 -0
- package/dist/judge-hook.js +420 -0
- package/dist/map-brief.d.ts +16 -0
- package/dist/map-brief.js +41 -0
- package/dist/offer-brief.d.ts +95 -0
- package/dist/offer-brief.js +217 -0
- package/dist/owned-process.d.ts +65 -0
- package/dist/owned-process.js +146 -0
- package/dist/promise-meaning.d.ts +126 -0
- package/dist/promise-meaning.js +292 -0
- package/dist/risk/contract.d.ts +145 -0
- package/dist/risk/contract.js +74 -0
- package/dist/risk/describe.d.ts +7 -0
- package/dist/risk/describe.js +45 -0
- package/dist/risk/diff.d.ts +26 -0
- package/dist/risk/diff.js +174 -0
- package/dist/risk/extract.d.ts +43 -0
- package/dist/risk/extract.js +336 -0
- package/dist/risk/git.d.ts +30 -0
- package/dist/risk/git.js +118 -0
- package/dist/risk/import-graph.d.ts +41 -0
- package/dist/risk/import-graph.js +487 -0
- package/dist/risk/index.d.ts +20 -0
- package/dist/risk/index.js +20 -0
- package/dist/risk/paths.d.ts +16 -0
- package/dist/risk/paths.js +73 -0
- package/dist/risk/pipeline.d.ts +47 -0
- package/dist/risk/pipeline.js +121 -0
- package/dist/risk/priors.d.ts +18 -0
- package/dist/risk/priors.js +86 -0
- package/dist/risk/resources.d.ts +56 -0
- package/dist/risk/resources.js +374 -0
- package/dist/risk/score.d.ts +85 -0
- package/dist/risk/score.js +543 -0
- package/dist/risk/symbols.d.ts +35 -0
- package/dist/risk/symbols.js +348 -0
- package/dist/risk/text.d.ts +43 -0
- package/dist/risk/text.js +277 -0
- package/dist/risk/validate.d.ts +19 -0
- package/dist/risk/validate.js +154 -0
- package/dist/scratch-worktree.d.ts +51 -0
- package/dist/scratch-worktree.js +153 -0
- package/dist/self-update.d.ts +29 -2
- package/dist/self-update.js +96 -11
- package/dist/user-scope.d.ts +2 -0
- package/dist/user-scope.js +26 -2
- package/dist/wire.d.ts +55 -2
- package/dist/wire.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The risk ranking of agentic testing: which agreed promises a change could put
|
|
3
|
+
* at risk, and why, worked out on this machine with no model and no network.
|
|
4
|
+
* `balladeer risk` is the command over it; see `README.md` beside this file.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./contract.js";
|
|
7
|
+
export { BEHAVIOR_MAP_DIRECTORY, BEHAVIOR_MAP_SCHEMA_VERSION, mapFiles, parseBehaviorMap, } from "../behavior-map-schema.js";
|
|
8
|
+
export { describeReason } from "./describe.js";
|
|
9
|
+
export { parseUnifiedDiff, unquotePath, } from "./diff.js";
|
|
10
|
+
export { analyzeFileChange, extractChange, readCommitIntent, sqlLineContexts, } from "./extract.js";
|
|
11
|
+
export { commitMessages, GitReadError, listTree, mergeBase, readBlobs, resolveCommit, tryGit, } from "./git.js";
|
|
12
|
+
export { buildImportGraph, forwardClosure, parseLooseJson, reverseClosure, workingTreeReader, } from "./import-graph.js";
|
|
13
|
+
export { isTestPath, languageOf } from "./paths.js";
|
|
14
|
+
export { assessChange, buildGraphAt, readBehaviorMaps, watchedResources, } from "./pipeline.js";
|
|
15
|
+
export { churnPriors, priorBreaksFromJudgeLog, readPriors } from "./priors.js";
|
|
16
|
+
export { mentionsResource, normalizeRoute, parseResourceRef, resourceKey, resourcesInLine, resourcesMatch, routeFromPath, } from "./resources.js";
|
|
17
|
+
export { BAND_THRESHOLDS, bandFor, byRisk, directness, forwardDecay, mapSeeds, reverseDecay, scoreChange, seededShuffle, WEIGHTS, } from "./score.js";
|
|
18
|
+
export { declaredSymbols, loadTypeScript } from "./symbols.js";
|
|
19
|
+
export { buildIdf, cosine, identifierTokens, STOP_WORDS, words } from "./text.js";
|
|
20
|
+
export { parseChangeExtraction, parseRiskReport } from "./validate.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Language } from "./contract.js";
|
|
2
|
+
export declare function extensionOf(path: string): string;
|
|
3
|
+
export declare function basenameOf(path: string): string;
|
|
4
|
+
export declare function dirnameOf(path: string): string;
|
|
5
|
+
/** The contract's language for a path, by extension. */
|
|
6
|
+
export declare function languageOf(path: string): Language;
|
|
7
|
+
/** The TypeScript compiler reads these: the ts, tsx and js languages. */
|
|
8
|
+
export declare function isScriptPath(path: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a path is a test: under a test directory, or named the way the common
|
|
11
|
+
* runners find tests (`*.test.*`, `*.spec.*`, `*_test.py`, `test_*.py`,
|
|
12
|
+
* `*_test.go`, `*_spec.rb`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isTestPath(path: string): boolean;
|
|
15
|
+
/** Joins and normalizes repository-relative segments; undefined when it escapes the root. */
|
|
16
|
+
export declare function joinRepositoryPath(directory: string, relative: string): string | undefined;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const LANGUAGE_BY_EXTENSION = {
|
|
2
|
+
ts: "ts",
|
|
3
|
+
mts: "ts",
|
|
4
|
+
cts: "ts",
|
|
5
|
+
tsx: "tsx",
|
|
6
|
+
js: "js",
|
|
7
|
+
mjs: "js",
|
|
8
|
+
cjs: "js",
|
|
9
|
+
jsx: "js",
|
|
10
|
+
py: "py",
|
|
11
|
+
go: "go",
|
|
12
|
+
rb: "rb",
|
|
13
|
+
java: "java",
|
|
14
|
+
kt: "kt",
|
|
15
|
+
kts: "kt",
|
|
16
|
+
rs: "rs",
|
|
17
|
+
};
|
|
18
|
+
export function extensionOf(path) {
|
|
19
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
20
|
+
const dot = base.lastIndexOf(".");
|
|
21
|
+
return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
export function basenameOf(path) {
|
|
24
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
25
|
+
}
|
|
26
|
+
export function dirnameOf(path) {
|
|
27
|
+
const slash = path.lastIndexOf("/");
|
|
28
|
+
return slash < 0 ? "" : path.slice(0, slash);
|
|
29
|
+
}
|
|
30
|
+
/** The contract's language for a path, by extension. */
|
|
31
|
+
export function languageOf(path) {
|
|
32
|
+
return LANGUAGE_BY_EXTENSION[extensionOf(path)] ?? "other";
|
|
33
|
+
}
|
|
34
|
+
/** The TypeScript compiler reads these: the ts, tsx and js languages. */
|
|
35
|
+
export function isScriptPath(path) {
|
|
36
|
+
const language = languageOf(path);
|
|
37
|
+
return language === "ts" || language === "tsx" || language === "js";
|
|
38
|
+
}
|
|
39
|
+
const TEST_DIRECTORIES = new Set(["test", "tests", "__tests__", "spec", "specs"]);
|
|
40
|
+
/**
|
|
41
|
+
* Whether a path is a test: under a test directory, or named the way the common
|
|
42
|
+
* runners find tests (`*.test.*`, `*.spec.*`, `*_test.py`, `test_*.py`,
|
|
43
|
+
* `*_test.go`, `*_spec.rb`).
|
|
44
|
+
*/
|
|
45
|
+
export function isTestPath(path) {
|
|
46
|
+
const segments = path.split("/");
|
|
47
|
+
const base = segments[segments.length - 1] ?? "";
|
|
48
|
+
if (segments.slice(0, -1).some((segment) => TEST_DIRECTORIES.has(segment)))
|
|
49
|
+
return true;
|
|
50
|
+
if (/\.(?:test|spec)\.[^.]+$/.test(base))
|
|
51
|
+
return true;
|
|
52
|
+
if (/^test_.+\.py$/.test(base) || /_test\.py$/.test(base))
|
|
53
|
+
return true;
|
|
54
|
+
if (/_test\.go$/.test(base) || /_spec\.rb$/.test(base))
|
|
55
|
+
return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/** Joins and normalizes repository-relative segments; undefined when it escapes the root. */
|
|
59
|
+
export function joinRepositoryPath(directory, relative) {
|
|
60
|
+
const parts = directory === "" ? [] : directory.split("/");
|
|
61
|
+
for (const segment of relative.split("/")) {
|
|
62
|
+
if (segment === "" || segment === ".")
|
|
63
|
+
continue;
|
|
64
|
+
if (segment === "..") {
|
|
65
|
+
if (parts.length === 0)
|
|
66
|
+
return undefined;
|
|
67
|
+
parts.pop();
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
parts.push(segment);
|
|
71
|
+
}
|
|
72
|
+
return parts.join("/");
|
|
73
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ImportGraph } from "./import-graph.js";
|
|
2
|
+
import { type ResourceRef } from "./resources.js";
|
|
3
|
+
import type { BehaviorMap, ChangeExtraction, Delta, Intent, RiskReport } from "./contract.js";
|
|
4
|
+
import { type Prior, type Weights } from "./score.js";
|
|
5
|
+
export type MapProblem = Readonly<{
|
|
6
|
+
file: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Every `*.json` in a directory read as a behavior map. A file that does not
|
|
11
|
+
* parse is reported, not thrown: one bad map must not hide the others.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readBehaviorMaps(directory: string): {
|
|
14
|
+
maps: BehaviorMap[];
|
|
15
|
+
problems: MapProblem[];
|
|
16
|
+
};
|
|
17
|
+
/** Every resource the maps name, including sink resources, for the extraction to look for directly. */
|
|
18
|
+
export declare function watchedResources(maps: readonly BehaviorMap[]): ResourceRef[];
|
|
19
|
+
/** The import graph of the tree at a commit, read from git rather than the working tree. */
|
|
20
|
+
export declare function buildGraphAt(root: string, rev: string): ImportGraph;
|
|
21
|
+
export type AssessOptions = Readonly<{
|
|
22
|
+
root: string;
|
|
23
|
+
base: string;
|
|
24
|
+
head: string;
|
|
25
|
+
maps: readonly BehaviorMap[];
|
|
26
|
+
/** Promises that should be listed even without a map. */
|
|
27
|
+
promiseIds?: readonly string[];
|
|
28
|
+
weights?: Weights;
|
|
29
|
+
/** Read churn and judge-log priors from the repository. On unless false. */
|
|
30
|
+
priors?: boolean | Readonly<Record<string, Prior>>;
|
|
31
|
+
/** Behavioral deltas a model wrote for this change, when one ran. */
|
|
32
|
+
deltas?: readonly Delta[];
|
|
33
|
+
/** The change's intent from its description or a model; else it is read from the commits. */
|
|
34
|
+
intent?: Intent;
|
|
35
|
+
}>;
|
|
36
|
+
export type Assessment = Readonly<{
|
|
37
|
+
report: RiskReport;
|
|
38
|
+
extraction: ChangeExtraction;
|
|
39
|
+
graph: ImportGraph;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* The whole pipeline for one change: extract it (looking for every resource
|
|
43
|
+
* the maps name), take its intent from the commit messages unless a description
|
|
44
|
+
* or a model supplied one, add a model's deltas when there are any, build the
|
|
45
|
+
* import graph at head, read the priors, score. Nothing here calls a model.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assessChange(options: AssessOptions): Promise<Assessment>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { extractChange, readCommitIntent } from "./extract.js";
|
|
4
|
+
import { listTree, readBlobs, resolveCommit } from "./git.js";
|
|
5
|
+
import { buildImportGraph } from "./import-graph.js";
|
|
6
|
+
import { extensionOf } from "./paths.js";
|
|
7
|
+
import { readPriors } from "./priors.js";
|
|
8
|
+
import { parseResourceRef, resourceKey } from "./resources.js";
|
|
9
|
+
import { parseBehaviorMap } from "../behavior-map-schema.js";
|
|
10
|
+
import { scoreChange } from "./score.js";
|
|
11
|
+
/**
|
|
12
|
+
* Every `*.json` in a directory read as a behavior map. A file that does not
|
|
13
|
+
* parse is reported, not thrown: one bad map must not hide the others.
|
|
14
|
+
*/
|
|
15
|
+
export function readBehaviorMaps(directory) {
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = readdirSync(directory)
|
|
19
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
20
|
+
.sort();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { maps: [], problems: [] };
|
|
24
|
+
}
|
|
25
|
+
const maps = [];
|
|
26
|
+
const problems = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = JSON.parse(readFileSync(join(directory, entry), "utf8"));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
problems.push({
|
|
34
|
+
file: entry,
|
|
35
|
+
message: error instanceof Error ? error.message : String(error),
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const parsed = parseBehaviorMap(raw);
|
|
40
|
+
if (!parsed.ok) {
|
|
41
|
+
problems.push({ file: entry, message: parsed.errors[0] ?? "not a behavior map" });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
maps.push(parsed.value);
|
|
45
|
+
}
|
|
46
|
+
return { maps, problems };
|
|
47
|
+
}
|
|
48
|
+
/** Every resource the maps name, including sink resources, for the extraction to look for directly. */
|
|
49
|
+
export function watchedResources(maps) {
|
|
50
|
+
const refs = new Map();
|
|
51
|
+
for (const map of maps) {
|
|
52
|
+
for (const resource of map.resources) {
|
|
53
|
+
const ref = { kind: resource.kind, name: resource.name };
|
|
54
|
+
refs.set(resourceKey(ref), ref);
|
|
55
|
+
}
|
|
56
|
+
for (const sink of map.sinks) {
|
|
57
|
+
for (const text of sink.resources) {
|
|
58
|
+
const ref = parseResourceRef(text);
|
|
59
|
+
if (ref !== undefined)
|
|
60
|
+
refs.set(resourceKey(ref), ref);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...refs.values()];
|
|
65
|
+
}
|
|
66
|
+
const GRAPH_EXTENSIONS = new Set(["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs", "py"]);
|
|
67
|
+
const MAX_GRAPH_FILE_BYTES = 1_000_000;
|
|
68
|
+
/** The import graph of the tree at a commit, read from git rather than the working tree. */
|
|
69
|
+
export function buildGraphAt(root, rev) {
|
|
70
|
+
const entries = listTree(root, rev).filter((entry) => {
|
|
71
|
+
if (entry.size > MAX_GRAPH_FILE_BYTES)
|
|
72
|
+
return false;
|
|
73
|
+
if (/(?:^|\/)(?:node_modules|vendor|dist|build|\.next|coverage)\//.test(entry.path))
|
|
74
|
+
return false;
|
|
75
|
+
const base = entry.path.slice(entry.path.lastIndexOf("/") + 1);
|
|
76
|
+
return (GRAPH_EXTENSIONS.has(extensionOf(entry.path)) ||
|
|
77
|
+
base === "package.json" ||
|
|
78
|
+
base === "tsconfig.json" ||
|
|
79
|
+
base === "jsconfig.json" ||
|
|
80
|
+
/^tsconfig\..*json$/.test(base));
|
|
81
|
+
});
|
|
82
|
+
const paths = entries.map((entry) => entry.path);
|
|
83
|
+
const blobs = readBlobs(root, rev, paths);
|
|
84
|
+
return buildImportGraph(root, paths, (path) => blobs.get(path));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The whole pipeline for one change: extract it (looking for every resource
|
|
88
|
+
* the maps name), take its intent from the commit messages unless a description
|
|
89
|
+
* or a model supplied one, add a model's deltas when there are any, build the
|
|
90
|
+
* import graph at head, read the priors, score. Nothing here calls a model.
|
|
91
|
+
*/
|
|
92
|
+
export async function assessChange(options) {
|
|
93
|
+
const head = resolveCommit(options.root, options.head);
|
|
94
|
+
const extraction = await extractChange({
|
|
95
|
+
root: options.root,
|
|
96
|
+
base: options.base,
|
|
97
|
+
head,
|
|
98
|
+
watch: watchedResources(options.maps),
|
|
99
|
+
});
|
|
100
|
+
const intent = options.intent ?? readCommitIntent({ root: options.root, base: extraction.base, head });
|
|
101
|
+
const withIntent = {
|
|
102
|
+
...extraction,
|
|
103
|
+
...(options.deltas === undefined ? {} : { deltas: [...options.deltas] }),
|
|
104
|
+
...(intent === undefined ? {} : { intent }),
|
|
105
|
+
};
|
|
106
|
+
const graph = buildGraphAt(options.root, head);
|
|
107
|
+
const priors = options.priors === false
|
|
108
|
+
? undefined
|
|
109
|
+
: typeof options.priors === "object"
|
|
110
|
+
? options.priors
|
|
111
|
+
: readPriors(options.root, head, options.maps);
|
|
112
|
+
const report = scoreChange({
|
|
113
|
+
extraction: withIntent,
|
|
114
|
+
maps: options.maps,
|
|
115
|
+
graph,
|
|
116
|
+
...(priors === undefined ? {} : { priors }),
|
|
117
|
+
...(options.promiseIds === undefined ? {} : { promiseIds: options.promiseIds }),
|
|
118
|
+
...(options.weights === undefined ? {} : { weights: options.weights }),
|
|
119
|
+
});
|
|
120
|
+
return { report, extraction: withIntent, graph };
|
|
121
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Prior } from "./score.js";
|
|
2
|
+
import type { BehaviorMap } from "./contract.js";
|
|
3
|
+
/**
|
|
4
|
+
* The two priors, read from what the repository already holds: how often the
|
|
5
|
+
* promise's mapped files change (git history), and how often a judge has
|
|
6
|
+
* recorded a verified break of it (the local judge log). Both are small
|
|
7
|
+
* additions to a promise that already has some signal, never a signal alone.
|
|
8
|
+
*/
|
|
9
|
+
/** Share of the last `window` commits before head that touched any file a map names. */
|
|
10
|
+
export declare function churnPriors(root: string, head: string, maps: readonly BehaviorMap[], window?: number): Record<string, number>;
|
|
11
|
+
/**
|
|
12
|
+
* Verified breaks per promise from `.continuity/judge-log/*.jsonl`: verdicts of
|
|
13
|
+
* `broken` whose reproduction the runner did not reject. A missing or
|
|
14
|
+
* unreadable log is no history, not an error.
|
|
15
|
+
*/
|
|
16
|
+
export declare function priorBreaksFromJudgeLog(root: string): Record<string, number>;
|
|
17
|
+
/** Both priors, keyed by promise. */
|
|
18
|
+
export declare function readPriors(root: string, head: string, maps: readonly BehaviorMap[]): Record<string, Prior>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tryGit } from "./git.js";
|
|
4
|
+
import { mapFiles } from "../behavior-map-schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* The two priors, read from what the repository already holds: how often the
|
|
7
|
+
* promise's mapped files change (git history), and how often a judge has
|
|
8
|
+
* recorded a verified break of it (the local judge log). Both are small
|
|
9
|
+
* additions to a promise that already has some signal, never a signal alone.
|
|
10
|
+
*/
|
|
11
|
+
/** Share of the last `window` commits before head that touched any file a map names. */
|
|
12
|
+
export function churnPriors(root, head, maps, window = 200) {
|
|
13
|
+
const out = tryGit(root, ["log", "-n", String(window), "--format=%x00", "--name-only", head]);
|
|
14
|
+
if (out === undefined)
|
|
15
|
+
return {};
|
|
16
|
+
const commits = out
|
|
17
|
+
.split("\0")
|
|
18
|
+
.map((block) => new Set(block
|
|
19
|
+
.split("\n")
|
|
20
|
+
.map((line) => line.trim())
|
|
21
|
+
.filter(Boolean)))
|
|
22
|
+
.filter((files) => files.size > 0);
|
|
23
|
+
if (commits.length === 0)
|
|
24
|
+
return {};
|
|
25
|
+
const churn = {};
|
|
26
|
+
for (const map of maps) {
|
|
27
|
+
const files = mapFiles(map);
|
|
28
|
+
const touching = commits.filter((changed) => files.some((file) => changed.has(file))).length;
|
|
29
|
+
churn[map.promiseId] = touching / commits.length;
|
|
30
|
+
}
|
|
31
|
+
return churn;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Verified breaks per promise from `.continuity/judge-log/*.jsonl`: verdicts of
|
|
35
|
+
* `broken` whose reproduction the runner did not reject. A missing or
|
|
36
|
+
* unreadable log is no history, not an error.
|
|
37
|
+
*/
|
|
38
|
+
export function priorBreaksFromJudgeLog(root) {
|
|
39
|
+
const directory = join(root, ".continuity", "judge-log");
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = readdirSync(directory).filter((entry) => entry.endsWith(".jsonl"));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
const breaks = {};
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
let text;
|
|
50
|
+
try {
|
|
51
|
+
text = readFileSync(join(directory, entry), "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const line of text.split("\n")) {
|
|
57
|
+
if (line.trim().length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const verdict = JSON.parse(line);
|
|
61
|
+
if (verdict.verdict !== "broken" || verdict.reproductionRejected)
|
|
62
|
+
continue;
|
|
63
|
+
if (typeof verdict.promiseId !== "string")
|
|
64
|
+
continue;
|
|
65
|
+
breaks[verdict.promiseId] = (breaks[verdict.promiseId] ?? 0) + 1;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// A torn line is skipped, not fatal.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return breaks;
|
|
73
|
+
}
|
|
74
|
+
/** Both priors, keyed by promise. */
|
|
75
|
+
export function readPriors(root, head, maps) {
|
|
76
|
+
const churn = churnPriors(root, head, maps);
|
|
77
|
+
const breaks = priorBreaksFromJudgeLog(root);
|
|
78
|
+
const priors = {};
|
|
79
|
+
for (const map of maps) {
|
|
80
|
+
priors[map.promiseId] = {
|
|
81
|
+
churn: churn[map.promiseId] ?? 0,
|
|
82
|
+
priorBreak: breaks[map.promiseId] ?? 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return priors;
|
|
86
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ResourceKind } from "./contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resources a line of code names, found by regular expression across languages:
|
|
4
|
+
* tables and columns in SQL, route literals, environment keys, feature flags,
|
|
5
|
+
* queue and event names, outbound hosts, file paths and configuration keys.
|
|
6
|
+
*
|
|
7
|
+
* This reads text, not programs. It will name a table in a comment and miss one
|
|
8
|
+
* assembled at run time. What it buys is the couplings an import graph cannot
|
|
9
|
+
* see: two files that never import each other and meet at a table name, a
|
|
10
|
+
* route string or a file on disk.
|
|
11
|
+
*/
|
|
12
|
+
export type ResourceRef = Readonly<{
|
|
13
|
+
kind: ResourceKind;
|
|
14
|
+
name: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function resourceKey(ref: ResourceRef): string;
|
|
17
|
+
/** `outbound:stripe.charges.create` → { kind: outbound, name: stripe.charges.create }. */
|
|
18
|
+
export declare function parseResourceRef(text: string): ResourceRef | undefined;
|
|
19
|
+
/** Whether the file is SQL, where keywords are read in any case. */
|
|
20
|
+
export declare function isSqlPath(path: string): boolean;
|
|
21
|
+
/** Normalizes a route so `/orders/:id`, `/orders/{id}` and `/orders/[id]` meet. */
|
|
22
|
+
export declare function normalizeRoute(route: string): string;
|
|
23
|
+
export type LineContext = Readonly<{
|
|
24
|
+
path: string;
|
|
25
|
+
/** For SQL files: the table the statement around this line alters or creates. */
|
|
26
|
+
currentTable?: string | undefined;
|
|
27
|
+
/** Whether this line sits inside a CREATE TABLE body, so a leading identifier is a column. */
|
|
28
|
+
inCreateTable?: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
/** Every resource this one line names. */
|
|
31
|
+
export declare function resourcesInLine(line: string, context: LineContext): ResourceRef[];
|
|
32
|
+
/** Next.js and similar file-system routers: the route a file serves, from its path. */
|
|
33
|
+
export declare function routeFromPath(path: string): string | undefined;
|
|
34
|
+
/** The contents of every string literal on a line. */
|
|
35
|
+
export declare function stringLiterals(line: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Whether one line names a resource a behavior map lists, read directly.
|
|
38
|
+
*
|
|
39
|
+
* The extraction runs this for every resource the maps list, so a resource the
|
|
40
|
+
* expressions above cannot recognize (a call chain such as
|
|
41
|
+
* `stripe.charges.create`, a configuration key) is still found where it is
|
|
42
|
+
* written. Tables, columns and files are looked for inside string literals
|
|
43
|
+
* only, outside SQL files, because their names are ordinary words in code.
|
|
44
|
+
* Routes are left to the route literals above, which normalize parameters.
|
|
45
|
+
* Names shorter than four characters are too common to look for.
|
|
46
|
+
*/
|
|
47
|
+
export declare function mentionsResource(line: string, ref: ResourceRef, context: LineContext): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a resource the change names is the resource a map names.
|
|
50
|
+
*
|
|
51
|
+
* Names meet case-insensitively. Tables meet on their last segment as well
|
|
52
|
+
* (`public.orders` and `orders`), columns with or without their table, routes
|
|
53
|
+
* by prefix after parameters are normalized, files by base name, hosts by
|
|
54
|
+
* suffix, and configuration keys by their final segments.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resourcesMatch(mapped: ResourceRef, changed: ResourceRef): boolean;
|