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,348 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { extensionOf, languageOf } from "./paths.js";
|
|
4
|
+
const loadedTypeScript = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* The TypeScript compiler of the repository being read, resolved from its root
|
|
7
|
+
* the way its own tools resolve it. The published command ships no compiler and
|
|
8
|
+
* never imports one: a TypeScript repository nearly always has its own, and one
|
|
9
|
+
* without it has its script files read by the declaration pattern instead.
|
|
10
|
+
* Looked up once per repository.
|
|
11
|
+
*/
|
|
12
|
+
export function loadTypeScript(root) {
|
|
13
|
+
if (root === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (loadedTypeScript.has(root))
|
|
16
|
+
return loadedTypeScript.get(root) ?? undefined;
|
|
17
|
+
let found = null;
|
|
18
|
+
try {
|
|
19
|
+
const loaded = createRequire(join(root, "package.json"))("typescript");
|
|
20
|
+
if (typeof loaded.createSourceFile === "function")
|
|
21
|
+
found = loaded;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// No compiler in reach of this repository.
|
|
25
|
+
}
|
|
26
|
+
loadedTypeScript.set(root, found);
|
|
27
|
+
return found ?? undefined;
|
|
28
|
+
}
|
|
29
|
+
function scriptKind(typescript, path) {
|
|
30
|
+
switch (extensionOf(path)) {
|
|
31
|
+
case "tsx":
|
|
32
|
+
return typescript.ScriptKind.TSX;
|
|
33
|
+
case "jsx":
|
|
34
|
+
return typescript.ScriptKind.JSX;
|
|
35
|
+
case "js":
|
|
36
|
+
case "mjs":
|
|
37
|
+
case "cjs":
|
|
38
|
+
return typescript.ScriptKind.JS;
|
|
39
|
+
default:
|
|
40
|
+
return typescript.ScriptKind.TS;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function scriptSymbols(typescript, path, text) {
|
|
44
|
+
const source = typescript.createSourceFile(path, text, typescript.ScriptTarget.Latest, true, scriptKind(typescript, path));
|
|
45
|
+
const found = [];
|
|
46
|
+
const lineOf = (position) => source.getLineAndCharacterOfPosition(position).line + 1;
|
|
47
|
+
const push = (name, kind, node) => {
|
|
48
|
+
found.push({ name, kind, startLine: lineOf(node.getStart(source)), endLine: lineOf(node.end) });
|
|
49
|
+
};
|
|
50
|
+
const isExported = (node) => (typescript.getCombinedModifierFlags(node) &
|
|
51
|
+
typescript.ModifierFlags.Export) !==
|
|
52
|
+
0;
|
|
53
|
+
const isFunctionValue = (node) => node !== undefined &&
|
|
54
|
+
(typescript.isArrowFunction(node) ||
|
|
55
|
+
typescript.isFunctionExpression(node) ||
|
|
56
|
+
typescript.isClassExpression(node) ||
|
|
57
|
+
((typescript.isAsExpression(node) ||
|
|
58
|
+
typescript.isSatisfiesExpression(node) ||
|
|
59
|
+
typescript.isParenthesizedExpression(node)) &&
|
|
60
|
+
isFunctionValue(node.expression)));
|
|
61
|
+
const visitClass = (node, className) => {
|
|
62
|
+
push(className, "class", node);
|
|
63
|
+
for (const member of node.members) {
|
|
64
|
+
const memberName = member.name !== undefined && typescript.isIdentifier(member.name)
|
|
65
|
+
? member.name.text
|
|
66
|
+
: member.name !== undefined && typescript.isPrivateIdentifier(member.name)
|
|
67
|
+
? member.name.text
|
|
68
|
+
: typescript.isConstructorDeclaration(member)
|
|
69
|
+
? "constructor"
|
|
70
|
+
: undefined;
|
|
71
|
+
if (memberName === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
if (typescript.isMethodDeclaration(member) ||
|
|
74
|
+
typescript.isConstructorDeclaration(member) ||
|
|
75
|
+
typescript.isGetAccessorDeclaration(member) ||
|
|
76
|
+
typescript.isSetAccessorDeclaration(member) ||
|
|
77
|
+
(typescript.isPropertyDeclaration(member) && isFunctionValue(member.initializer)))
|
|
78
|
+
push(`${className}.${memberName}`, "method", member);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
for (const statement of source.statements) {
|
|
82
|
+
if (typescript.isFunctionDeclaration(statement)) {
|
|
83
|
+
push(statement.name?.text ?? "default", "function", statement);
|
|
84
|
+
}
|
|
85
|
+
else if (typescript.isClassDeclaration(statement)) {
|
|
86
|
+
visitClass(statement, statement.name?.text ?? "default");
|
|
87
|
+
}
|
|
88
|
+
else if (typescript.isInterfaceDeclaration(statement) ||
|
|
89
|
+
typescript.isTypeAliasDeclaration(statement) ||
|
|
90
|
+
typescript.isEnumDeclaration(statement)) {
|
|
91
|
+
push(statement.name.text, "type", statement);
|
|
92
|
+
}
|
|
93
|
+
else if (typescript.isModuleDeclaration(statement) &&
|
|
94
|
+
typescript.isIdentifier(statement.name)) {
|
|
95
|
+
push(statement.name.text, "module", statement);
|
|
96
|
+
}
|
|
97
|
+
else if (typescript.isVariableStatement(statement)) {
|
|
98
|
+
const exported = isExported(statement);
|
|
99
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
100
|
+
if (!typescript.isIdentifier(declaration.name))
|
|
101
|
+
continue;
|
|
102
|
+
const initializer = declaration.initializer;
|
|
103
|
+
if (initializer !== undefined && typescript.isClassExpression(initializer)) {
|
|
104
|
+
visitClass(initializer, declaration.name.text);
|
|
105
|
+
}
|
|
106
|
+
else if (isFunctionValue(initializer)) {
|
|
107
|
+
push(declaration.name.text, "function", statements(declaration, statement));
|
|
108
|
+
}
|
|
109
|
+
else if (exported) {
|
|
110
|
+
push(declaration.name.text, "const", statements(declaration, statement));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (typescript.isExportAssignment(statement) && isFunctionValue(statement.expression)) {
|
|
115
|
+
push("default", "function", statement);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
/** A lone declarator spans its whole statement, so the `export const` line counts as its own. */
|
|
121
|
+
function statements(declaration, statement) {
|
|
122
|
+
return statement.declarationList.declarations.length === 1 ? statement : declaration;
|
|
123
|
+
}
|
|
124
|
+
function indentation(line) {
|
|
125
|
+
const match = /^[\t ]*/.exec(line);
|
|
126
|
+
return (match?.[0] ?? "").replace(/\t/g, " ").length;
|
|
127
|
+
}
|
|
128
|
+
function pythonSymbols(text) {
|
|
129
|
+
const lines = text.split("\n");
|
|
130
|
+
const found = [];
|
|
131
|
+
const open = [];
|
|
132
|
+
const close = (upTo, atIndent) => {
|
|
133
|
+
while (open.length > 0 && (open[open.length - 1]?.indent ?? -1) >= atIndent) {
|
|
134
|
+
const symbol = open.pop();
|
|
135
|
+
if (symbol === undefined)
|
|
136
|
+
break;
|
|
137
|
+
found.push({ name: symbol.name, kind: symbol.kind, startLine: symbol.start, endLine: upTo });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
let lastCode = 0;
|
|
141
|
+
lines.forEach((line, index) => {
|
|
142
|
+
const lineNumber = index + 1;
|
|
143
|
+
if (line.trim().length === 0 || /^\s*#/.test(line))
|
|
144
|
+
return;
|
|
145
|
+
const indent = indentation(line);
|
|
146
|
+
close(lastCode, indent);
|
|
147
|
+
lastCode = lineNumber;
|
|
148
|
+
const declaration = /^\s*(?:async\s+)?(def|class)\s+([A-Za-z_]\w*)/.exec(line);
|
|
149
|
+
if (declaration) {
|
|
150
|
+
const enclosingClass = [...open].reverse().find((symbol) => symbol.kind === "class");
|
|
151
|
+
const isMethod = declaration[1] === "def" && enclosingClass !== undefined;
|
|
152
|
+
const name = isMethod
|
|
153
|
+
? `${enclosingClass?.name}.${declaration[2]}`
|
|
154
|
+
: declaration[2];
|
|
155
|
+
const kind = declaration[1] === "class" ? "class" : isMethod ? "method" : "function";
|
|
156
|
+
// Decorators directly above belong to the declaration.
|
|
157
|
+
let start = lineNumber;
|
|
158
|
+
while (start > 1 && /^\s*@/.test(lines[start - 2] ?? ""))
|
|
159
|
+
start -= 1;
|
|
160
|
+
open.push({ name, kind, indent, start });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
close(lastCode, -1);
|
|
164
|
+
return found.sort((a, b) => a.startLine - b.startLine);
|
|
165
|
+
}
|
|
166
|
+
const GENERIC_DECLARATIONS = [
|
|
167
|
+
// Go: func Name( and func (r *T) Name(
|
|
168
|
+
/^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*[[(]/,
|
|
169
|
+
// Rust: fn name
|
|
170
|
+
/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+([A-Za-z_]\w*)/,
|
|
171
|
+
// Kotlin: fun name
|
|
172
|
+
/^\s*(?:(?:public|private|internal|protected|override|suspend|inline|open)\s+)*fun\s+(?:<[^>]*>\s*)?(?:[\w.]+\.)?([A-Za-z_]\w*)\s*\(/,
|
|
173
|
+
// Ruby: def name
|
|
174
|
+
/^\s*def\s+(?:self\.)?([A-Za-z_]\w*[?!]?)/,
|
|
175
|
+
// Classes, structs, interfaces, traits, modules
|
|
176
|
+
/^\s*(?:(?:public|private|protected|internal|abstract|final|sealed|open|data|static|export|pub)\s+)*(?:class|struct|interface|trait|module|object|enum|impl)\s+([A-Za-z_]\w*)/,
|
|
177
|
+
// Java/C#-style methods: modifiers, a return type, a name and an open parenthesis.
|
|
178
|
+
/^\s*(?:(?:public|private|protected|static|final|synchronized|abstract|override|virtual|async)\s+)+[\w<>[\],.? ]+\s+([A-Za-z_]\w*)\s*\([^;]*$/,
|
|
179
|
+
// JavaScript-like: function name(
|
|
180
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/,
|
|
181
|
+
];
|
|
182
|
+
function genericSymbols(text) {
|
|
183
|
+
const lines = text.split("\n");
|
|
184
|
+
const starts = [];
|
|
185
|
+
lines.forEach((line, index) => {
|
|
186
|
+
for (const pattern of GENERIC_DECLARATIONS) {
|
|
187
|
+
const match = pattern.exec(line);
|
|
188
|
+
if (match?.[1] === undefined)
|
|
189
|
+
continue;
|
|
190
|
+
const kind = /\b(?:class|struct|interface|trait|module|object|enum|impl)\b/.test(line) &&
|
|
191
|
+
!/\b(?:func|fn|fun|def|function)\b/.test(line)
|
|
192
|
+
? "class"
|
|
193
|
+
: "function";
|
|
194
|
+
starts.push({ name: match[1], kind, line: index + 1 });
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return starts.map((start, index) => ({
|
|
199
|
+
name: start.name,
|
|
200
|
+
kind: start.kind,
|
|
201
|
+
startLine: start.line,
|
|
202
|
+
endLine: (starts[index + 1]?.line ?? lines.length + 1) - 1,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Where a declaration starting on `start` (1-based) ends, by counting braces
|
|
207
|
+
* outside strings and comments. A declaration with no brace (an arrow function
|
|
208
|
+
* returning an expression) ends at its first line that closes the statement.
|
|
209
|
+
*/
|
|
210
|
+
function braceEnd(lines, start) {
|
|
211
|
+
let depth = 0;
|
|
212
|
+
let opened = false;
|
|
213
|
+
let inBlockComment = false;
|
|
214
|
+
for (let index = start - 1; index < lines.length && index < start - 1 + 2000; index += 1) {
|
|
215
|
+
const line = lines[index] ?? "";
|
|
216
|
+
let quote;
|
|
217
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
218
|
+
const char = line[at];
|
|
219
|
+
const next = line[at + 1];
|
|
220
|
+
if (inBlockComment) {
|
|
221
|
+
if (char === "*" && next === "/") {
|
|
222
|
+
inBlockComment = false;
|
|
223
|
+
at += 1;
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (quote !== undefined) {
|
|
228
|
+
if (char === "\\")
|
|
229
|
+
at += 1;
|
|
230
|
+
else if (char === quote)
|
|
231
|
+
quote = undefined;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (char === "/" && next === "/")
|
|
235
|
+
break;
|
|
236
|
+
if (char === "/" && next === "*") {
|
|
237
|
+
inBlockComment = true;
|
|
238
|
+
at += 1;
|
|
239
|
+
}
|
|
240
|
+
else if (char === '"' || char === "'" || char === "`")
|
|
241
|
+
quote = char;
|
|
242
|
+
else if (char === "{") {
|
|
243
|
+
depth += 1;
|
|
244
|
+
opened = true;
|
|
245
|
+
}
|
|
246
|
+
else if (char === "}")
|
|
247
|
+
depth -= 1;
|
|
248
|
+
}
|
|
249
|
+
if (opened && depth <= 0)
|
|
250
|
+
return index + 1;
|
|
251
|
+
if (!opened && /[;,]\s*$/.test(line) && index - (start - 1) < 3)
|
|
252
|
+
return index + 1;
|
|
253
|
+
}
|
|
254
|
+
return Math.min(lines.length, start + 1999);
|
|
255
|
+
}
|
|
256
|
+
const SCRIPT_KEYWORDS = new Set([
|
|
257
|
+
"if",
|
|
258
|
+
"for",
|
|
259
|
+
"while",
|
|
260
|
+
"switch",
|
|
261
|
+
"catch",
|
|
262
|
+
"function",
|
|
263
|
+
"return",
|
|
264
|
+
"with",
|
|
265
|
+
"else",
|
|
266
|
+
"do",
|
|
267
|
+
"try",
|
|
268
|
+
"new",
|
|
269
|
+
"typeof",
|
|
270
|
+
"await",
|
|
271
|
+
]);
|
|
272
|
+
/**
|
|
273
|
+
* TypeScript and JavaScript without a compiler to hand: functions, arrow
|
|
274
|
+
* functions assigned to constants, classes and their methods, exported
|
|
275
|
+
* constants and types, each spanning its braces.
|
|
276
|
+
*/
|
|
277
|
+
function scriptPatternSymbols(text) {
|
|
278
|
+
const lines = text.split("\n");
|
|
279
|
+
const found = [];
|
|
280
|
+
const classes = [];
|
|
281
|
+
lines.forEach((line, index) => {
|
|
282
|
+
const lineNumber = index + 1;
|
|
283
|
+
const indent = indentation(line);
|
|
284
|
+
while (classes.length > 0 && (classes[classes.length - 1]?.end ?? 0) < lineNumber)
|
|
285
|
+
classes.pop();
|
|
286
|
+
const push = (name, kind) => found.push({ name, kind, startLine: lineNumber, endLine: braceEnd(lines, lineNumber) });
|
|
287
|
+
let match = /^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/.exec(line);
|
|
288
|
+
if (match?.[1] !== undefined)
|
|
289
|
+
return push(match[1], "function");
|
|
290
|
+
match =
|
|
291
|
+
/^\s*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/.exec(line);
|
|
292
|
+
if (match?.[1] !== undefined) {
|
|
293
|
+
push(match[1], "class");
|
|
294
|
+
classes.push({ name: match[1], indent, end: braceEnd(lines, lineNumber) });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
match = /^\s*(?:export\s+)?(?:declare\s+)?(?:interface|type|enum)\s+([A-Za-z_$][\w$]*)/.exec(line);
|
|
298
|
+
if (match?.[1] !== undefined)
|
|
299
|
+
return push(match[1], "type");
|
|
300
|
+
match =
|
|
301
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s+)?(?:function\b|\([^)]*\)?\s*(?::[^=]+)?=>|\(\s*$|[A-Za-z_$][\w$]*\s*=>|<)/.exec(line);
|
|
302
|
+
if (match?.[1] !== undefined)
|
|
303
|
+
return push(match[1], "function");
|
|
304
|
+
match = /^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/.exec(line);
|
|
305
|
+
if (match?.[1] !== undefined)
|
|
306
|
+
return push(match[1], "const");
|
|
307
|
+
const owner = classes[classes.length - 1];
|
|
308
|
+
if (owner !== undefined && indent > owner.indent) {
|
|
309
|
+
match =
|
|
310
|
+
/^\s+(?:(?:public|private|protected|static|async|readonly|override|abstract|get|set)\s+)*(#?[A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\(.*\)?\s*(?::\s*[^{;=]+)?\{?\s*$/.exec(line);
|
|
311
|
+
const name = match?.[1];
|
|
312
|
+
if (name !== undefined && !SCRIPT_KEYWORDS.has(name))
|
|
313
|
+
push(`${owner.name}.${name}`, "method");
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
return found;
|
|
317
|
+
}
|
|
318
|
+
/** Every symbol declared in a file, with its line range, and which reader found them. */
|
|
319
|
+
export function declaredSymbols(path, text, root) {
|
|
320
|
+
const language = languageOf(path);
|
|
321
|
+
if (language === "ts" || language === "tsx" || language === "js") {
|
|
322
|
+
const typescript = loadTypeScript(root);
|
|
323
|
+
if (typescript !== undefined) {
|
|
324
|
+
try {
|
|
325
|
+
return { symbols: scriptSymbols(typescript, path, text), extractor: "typescript" };
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// A file the parser cannot read falls through to the pattern.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { symbols: scriptPatternSymbols(text), extractor: "regex" };
|
|
332
|
+
}
|
|
333
|
+
if (language === "py")
|
|
334
|
+
return { symbols: pythonSymbols(text), extractor: "regex" };
|
|
335
|
+
return { symbols: genericSymbols(text), extractor: "regex" };
|
|
336
|
+
}
|
|
337
|
+
/** The symbols whose range contains any of the given lines. */
|
|
338
|
+
export function symbolsAtLines(symbols, lines) {
|
|
339
|
+
if (lines.size === 0)
|
|
340
|
+
return [];
|
|
341
|
+
return symbols.filter((symbol) => {
|
|
342
|
+
for (const line of lines) {
|
|
343
|
+
if (line >= symbol.startLine && line <= symbol.endLine)
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens and lexical similarity.
|
|
3
|
+
*
|
|
4
|
+
* Two different readings of text live here. Identifiers are what the change
|
|
5
|
+
* extraction records: whole tokens of four or more characters from changed
|
|
6
|
+
* lines, minus one shared stop list. Words are what similarity compares:
|
|
7
|
+
* identifiers split at case and underscore boundaries, lowercased, lightly
|
|
8
|
+
* stemmed, so `idempotencyKey` in a diff can meet "idempotency key" in a failing
|
|
9
|
+
* case.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The shared stop list: language keywords and words so common in code that
|
|
13
|
+
* they say nothing about which behavior a change touches.
|
|
14
|
+
*/
|
|
15
|
+
export declare const STOP_WORDS: ReadonlySet<string>;
|
|
16
|
+
/** Identifier tokens of four or more characters, minus the stop list, in first-seen order. */
|
|
17
|
+
export declare function identifierTokens(text: string): string[];
|
|
18
|
+
/** `idempotencyKey` → idempotency, key; `HTTPServer` → http, server; `retry_count` → retry, count. */
|
|
19
|
+
export declare function splitIdentifier(token: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* A light suffix stemmer: enough that charge, charges, charged and charging
|
|
22
|
+
* meet, and nothing that needs a dictionary.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stem(word: string): string;
|
|
25
|
+
/** The comparable words of a text: split, lowercased, stop words out, stemmed, deduplicated. */
|
|
26
|
+
export declare function words(text: string): Set<string>;
|
|
27
|
+
/** Inverse document frequency over a small corpus, smoothed so an unseen word counts most. */
|
|
28
|
+
export type Idf = Readonly<{
|
|
29
|
+
weight: (word: string) => number;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function buildIdf(documents: readonly ReadonlySet<string>[]): Idf;
|
|
32
|
+
/**
|
|
33
|
+
* Cosine similarity of two word sets under idf weights, in [0, 1]. Binary term
|
|
34
|
+
* frequency, so a long source text is penalized for its length: a fifty-file
|
|
35
|
+
* diff's identifiers do not resemble every failing case by covering them.
|
|
36
|
+
*/
|
|
37
|
+
export declare function cosine(source: ReadonlySet<string>, target: ReadonlySet<string>, idf: Idf): number;
|
|
38
|
+
/**
|
|
39
|
+
* Wording that says a change takes something away or goes around it. When the
|
|
40
|
+
* stated intent carries one of these and resembles the promise's own outcome,
|
|
41
|
+
* the change is saying out loud that it undoes what was agreed.
|
|
42
|
+
*/
|
|
43
|
+
export declare const REVERSAL_CUE: RegExp;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens and lexical similarity.
|
|
3
|
+
*
|
|
4
|
+
* Two different readings of text live here. Identifiers are what the change
|
|
5
|
+
* extraction records: whole tokens of four or more characters from changed
|
|
6
|
+
* lines, minus one shared stop list. Words are what similarity compares:
|
|
7
|
+
* identifiers split at case and underscore boundaries, lowercased, lightly
|
|
8
|
+
* stemmed, so `idempotencyKey` in a diff can meet "idempotency key" in a failing
|
|
9
|
+
* case.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The shared stop list: language keywords and words so common in code that
|
|
13
|
+
* they say nothing about which behavior a change touches.
|
|
14
|
+
*/
|
|
15
|
+
export const STOP_WORDS = new Set([
|
|
16
|
+
// keywords across the languages the extraction reads
|
|
17
|
+
"abstract",
|
|
18
|
+
"async",
|
|
19
|
+
"await",
|
|
20
|
+
"break",
|
|
21
|
+
"case",
|
|
22
|
+
"catch",
|
|
23
|
+
"class",
|
|
24
|
+
"const",
|
|
25
|
+
"continue",
|
|
26
|
+
"declare",
|
|
27
|
+
"default",
|
|
28
|
+
"defer",
|
|
29
|
+
"delete",
|
|
30
|
+
"elif",
|
|
31
|
+
"else",
|
|
32
|
+
"enum",
|
|
33
|
+
"except",
|
|
34
|
+
"export",
|
|
35
|
+
"extends",
|
|
36
|
+
"false",
|
|
37
|
+
"final",
|
|
38
|
+
"finally",
|
|
39
|
+
"func",
|
|
40
|
+
"function",
|
|
41
|
+
"global",
|
|
42
|
+
"implements",
|
|
43
|
+
"import",
|
|
44
|
+
"interface",
|
|
45
|
+
"lambda",
|
|
46
|
+
"module",
|
|
47
|
+
"none",
|
|
48
|
+
"null",
|
|
49
|
+
"package",
|
|
50
|
+
"pass",
|
|
51
|
+
"private",
|
|
52
|
+
"protected",
|
|
53
|
+
"public",
|
|
54
|
+
"raise",
|
|
55
|
+
"readonly",
|
|
56
|
+
"require",
|
|
57
|
+
"return",
|
|
58
|
+
"self",
|
|
59
|
+
"static",
|
|
60
|
+
"string",
|
|
61
|
+
"struct",
|
|
62
|
+
"super",
|
|
63
|
+
"switch",
|
|
64
|
+
"this",
|
|
65
|
+
"throw",
|
|
66
|
+
"throws",
|
|
67
|
+
"true",
|
|
68
|
+
"type",
|
|
69
|
+
"typeof",
|
|
70
|
+
"undefined",
|
|
71
|
+
"unless",
|
|
72
|
+
"void",
|
|
73
|
+
"while",
|
|
74
|
+
"with",
|
|
75
|
+
"yield",
|
|
76
|
+
"number",
|
|
77
|
+
"boolean",
|
|
78
|
+
"object",
|
|
79
|
+
"unknown",
|
|
80
|
+
"never",
|
|
81
|
+
"keyof",
|
|
82
|
+
"instanceof",
|
|
83
|
+
"impl",
|
|
84
|
+
"trait",
|
|
85
|
+
"then",
|
|
86
|
+
"from",
|
|
87
|
+
"nonlocal",
|
|
88
|
+
"assert",
|
|
89
|
+
// words that carry no behavior
|
|
90
|
+
"that",
|
|
91
|
+
"these",
|
|
92
|
+
"those",
|
|
93
|
+
"there",
|
|
94
|
+
"their",
|
|
95
|
+
"they",
|
|
96
|
+
"what",
|
|
97
|
+
"when",
|
|
98
|
+
"where",
|
|
99
|
+
"which",
|
|
100
|
+
"will",
|
|
101
|
+
"would",
|
|
102
|
+
"should",
|
|
103
|
+
"could",
|
|
104
|
+
"must",
|
|
105
|
+
"does",
|
|
106
|
+
"done",
|
|
107
|
+
"have",
|
|
108
|
+
"has",
|
|
109
|
+
"been",
|
|
110
|
+
"being",
|
|
111
|
+
"into",
|
|
112
|
+
"onto",
|
|
113
|
+
"only",
|
|
114
|
+
"also",
|
|
115
|
+
"every",
|
|
116
|
+
"each",
|
|
117
|
+
"some",
|
|
118
|
+
"such",
|
|
119
|
+
"than",
|
|
120
|
+
"them",
|
|
121
|
+
"just",
|
|
122
|
+
"more",
|
|
123
|
+
"most",
|
|
124
|
+
"other",
|
|
125
|
+
"over",
|
|
126
|
+
"same",
|
|
127
|
+
"very",
|
|
128
|
+
"your",
|
|
129
|
+
"yours",
|
|
130
|
+
"about",
|
|
131
|
+
"after",
|
|
132
|
+
"before",
|
|
133
|
+
"again",
|
|
134
|
+
"always",
|
|
135
|
+
"without",
|
|
136
|
+
"within",
|
|
137
|
+
"because",
|
|
138
|
+
"value",
|
|
139
|
+
"values",
|
|
140
|
+
"data",
|
|
141
|
+
"item",
|
|
142
|
+
"items",
|
|
143
|
+
"result",
|
|
144
|
+
"results",
|
|
145
|
+
"todo",
|
|
146
|
+
"fixme",
|
|
147
|
+
"http",
|
|
148
|
+
"https",
|
|
149
|
+
"the",
|
|
150
|
+
"and",
|
|
151
|
+
"for",
|
|
152
|
+
"not",
|
|
153
|
+
"are",
|
|
154
|
+
"but",
|
|
155
|
+
"can",
|
|
156
|
+
"any",
|
|
157
|
+
"all",
|
|
158
|
+
"its",
|
|
159
|
+
"was",
|
|
160
|
+
"one",
|
|
161
|
+
"two",
|
|
162
|
+
"new",
|
|
163
|
+
"get",
|
|
164
|
+
"set",
|
|
165
|
+
"let",
|
|
166
|
+
"var",
|
|
167
|
+
"def",
|
|
168
|
+
"use",
|
|
169
|
+
"via",
|
|
170
|
+
"per",
|
|
171
|
+
"out",
|
|
172
|
+
"now",
|
|
173
|
+
"yet",
|
|
174
|
+
]);
|
|
175
|
+
const IDENTIFIER = /[A-Za-z_$][A-Za-z0-9_$]*/g;
|
|
176
|
+
/** Identifier tokens of four or more characters, minus the stop list, in first-seen order. */
|
|
177
|
+
export function identifierTokens(text) {
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
for (const match of text.matchAll(IDENTIFIER)) {
|
|
180
|
+
const token = match[0];
|
|
181
|
+
if (token.length < 4)
|
|
182
|
+
continue;
|
|
183
|
+
if (STOP_WORDS.has(token.toLowerCase()))
|
|
184
|
+
continue;
|
|
185
|
+
seen.add(token);
|
|
186
|
+
}
|
|
187
|
+
return [...seen];
|
|
188
|
+
}
|
|
189
|
+
/** `idempotencyKey` → idempotency, key; `HTTPServer` → http, server; `retry_count` → retry, count. */
|
|
190
|
+
export function splitIdentifier(token) {
|
|
191
|
+
return token
|
|
192
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
193
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
194
|
+
.split(/[^A-Za-z0-9]+/)
|
|
195
|
+
.map((part) => part.toLowerCase())
|
|
196
|
+
.filter((part) => part.length > 0);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A light suffix stemmer: enough that charge, charges, charged and charging
|
|
200
|
+
* meet, and nothing that needs a dictionary.
|
|
201
|
+
*/
|
|
202
|
+
export function stem(word) {
|
|
203
|
+
let w = word;
|
|
204
|
+
if (w.length > 4 && w.endsWith("ies"))
|
|
205
|
+
w = `${w.slice(0, -3)}y`;
|
|
206
|
+
else if (w.endsWith("sses"))
|
|
207
|
+
w = w.slice(0, -2);
|
|
208
|
+
else if (w.length > 4 && /(?:[sxz]|ch|sh)es$/.test(w))
|
|
209
|
+
w = w.slice(0, -2);
|
|
210
|
+
else if (w.length > 3 && w.endsWith("s") && !/(?:ss|us|is)$/.test(w))
|
|
211
|
+
w = w.slice(0, -1);
|
|
212
|
+
if (w.length > 5 && w.endsWith("ing"))
|
|
213
|
+
w = w.slice(0, -3);
|
|
214
|
+
else if (w.length > 4 && w.endsWith("ed"))
|
|
215
|
+
w = w.slice(0, -2);
|
|
216
|
+
if (w.length > 4 && w.endsWith("e"))
|
|
217
|
+
w = w.slice(0, -1);
|
|
218
|
+
if (w.length > 4 && /([b-df-hj-np-tv-z])\1$/.test(w))
|
|
219
|
+
w = w.slice(0, -1);
|
|
220
|
+
return w;
|
|
221
|
+
}
|
|
222
|
+
/** The comparable words of a text: split, lowercased, stop words out, stemmed, deduplicated. */
|
|
223
|
+
export function words(text) {
|
|
224
|
+
const found = new Set();
|
|
225
|
+
for (const match of text.matchAll(IDENTIFIER)) {
|
|
226
|
+
for (const part of splitIdentifier(match[0])) {
|
|
227
|
+
if (part.length < 3 || /^\d+$/.test(part))
|
|
228
|
+
continue;
|
|
229
|
+
if (STOP_WORDS.has(part))
|
|
230
|
+
continue;
|
|
231
|
+
found.add(stem(part));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return found;
|
|
235
|
+
}
|
|
236
|
+
export function buildIdf(documents) {
|
|
237
|
+
const frequency = new Map();
|
|
238
|
+
for (const document of documents) {
|
|
239
|
+
for (const word of document)
|
|
240
|
+
frequency.set(word, (frequency.get(word) ?? 0) + 1);
|
|
241
|
+
}
|
|
242
|
+
const total = documents.length;
|
|
243
|
+
return {
|
|
244
|
+
weight: (word) => Math.log(1 + (total + 1) / ((frequency.get(word) ?? 0) + 1)),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Cosine similarity of two word sets under idf weights, in [0, 1]. Binary term
|
|
249
|
+
* frequency, so a long source text is penalized for its length: a fifty-file
|
|
250
|
+
* diff's identifiers do not resemble every failing case by covering them.
|
|
251
|
+
*/
|
|
252
|
+
export function cosine(source, target, idf) {
|
|
253
|
+
if (source.size === 0 || target.size === 0)
|
|
254
|
+
return 0;
|
|
255
|
+
let shared = 0;
|
|
256
|
+
let sourceNorm = 0;
|
|
257
|
+
let targetNorm = 0;
|
|
258
|
+
for (const word of source) {
|
|
259
|
+
const w = idf.weight(word);
|
|
260
|
+
sourceNorm += w * w;
|
|
261
|
+
if (target.has(word))
|
|
262
|
+
shared += w * w;
|
|
263
|
+
}
|
|
264
|
+
for (const word of target) {
|
|
265
|
+
const w = idf.weight(word);
|
|
266
|
+
targetNorm += w * w;
|
|
267
|
+
}
|
|
268
|
+
if (shared === 0)
|
|
269
|
+
return 0;
|
|
270
|
+
return Math.min(1, shared / (Math.sqrt(sourceNorm) * Math.sqrt(targetNorm)));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Wording that says a change takes something away or goes around it. When the
|
|
274
|
+
* stated intent carries one of these and resembles the promise's own outcome,
|
|
275
|
+
* the change is saying out loud that it undoes what was agreed.
|
|
276
|
+
*/
|
|
277
|
+
export const REVERSAL_CUE = /\b(?:no longer|stop(?:s|ped|ping)?|remov(?:e|es|ed|ing)|drop(?:s|ped|ping)?|skip(?:s|ped|ping)?|bypass(?:es|ed|ing)?|disabl(?:e|es|ed|ing)|revert(?:s|ed|ing)?|ignor(?:e|es|ed|ing)|instead of|rather than|allow(?:s|ed|ing)? (?:\w+ ){0,3}(?:again|twice|more than once|without)|without)\b/i;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ChangeExtraction, type RiskReport } from "./contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* The change extraction and the risk report, checked by hand against the
|
|
4
|
+
* contract's field names. A shape arriving from outside this process (a model's
|
|
5
|
+
* deltas, a report the evaluation harness reads back) is checked before it is
|
|
6
|
+
* trusted; unknown fields are refused, as the behavior map's checker refuses
|
|
7
|
+
* them.
|
|
8
|
+
*/
|
|
9
|
+
export type ParseResult<T> = {
|
|
10
|
+
ok: true;
|
|
11
|
+
value: T;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
errors: string[];
|
|
15
|
+
};
|
|
16
|
+
/** A change extraction, checked against the contract. */
|
|
17
|
+
export declare function parseChangeExtraction(value: unknown): ParseResult<ChangeExtraction>;
|
|
18
|
+
/** A risk report, checked against the contract. */
|
|
19
|
+
export declare function parseRiskReport(value: unknown): ParseResult<RiskReport>;
|