jev-agent-tools 0.2.0 → 0.3.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/CHANGELOG.md +37 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +22 -14
- package/SECURITY.md +17 -1
- package/dist/adapters/ask-files.js +11 -2
- package/dist/adapters/ask-proof.js +63 -7
- package/dist/adapters/command.js +82 -29
- package/dist/adapters/docs.js +30 -10
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/files.js +141 -16
- package/dist/adapters/find.js +34 -6
- package/dist/adapters/git-base.js +7 -1
- package/dist/adapters/git.js +51 -7
- package/dist/adapters/locate-file.js +47 -9
- package/dist/adapters/private-storage.js +14 -6
- package/dist/adapters/risk-callers.js +3 -0
- package/dist/adapters/shell.js +23 -7
- package/dist/adapters/test-inventory.js +10 -2
- package/dist/configuration.js +17 -7
- package/dist/constants.js +26 -5
- package/dist/core/ask-references.js +193 -109
- package/dist/core/asks.js +78 -7
- package/dist/core/locate.js +8 -8
- package/dist/core/output.js +17 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/state.js +8 -1
- package/dist/core/units.js +1 -1
- package/dist/jev/client.js +34 -12
- package/dist/mcp/protocol.js +50 -27
- package/dist/mcp/tools.js +20 -7
- package/dist/render.js +72 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/texts/ask-files.js +3 -1
- package/dist/texts/ask.js +3 -1
- package/dist/texts/check-diff.js +7 -4
- package/dist/texts/find.js +7 -2
- package/dist/texts/guide.js +3 -16
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +7 -2
- package/dist/texts/select-tests.js +3 -1
- package/dist/tools/ask-files.js +248 -15
- package/dist/tools/ask.js +523 -62
- package/dist/tools/check-diff.js +222 -30
- package/dist/tools/docs-check.js +122 -13
- package/dist/tools/find.js +320 -27
- package/dist/tools/locate.js +317 -18
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +273 -19
- package/dist/tools/spec-check.js +119 -22
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
- package/docs/agent-instructions.md +59 -30
- package/docs/design.md +13 -1
- package/docs/mcp.md +8 -6
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +1 -1
- package/rules/jev-ask.md +22 -1
- package/server.json +2 -2
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/command.ts +96 -33
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +17 -5
- package/src/adapters/risk-callers.ts +3 -0
- package/src/adapters/shell.ts +23 -7
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +16 -2
- package/src/constants.ts +26 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/index.ts +3 -0
- package/src/jev/client.ts +54 -16
- package/src/jev/types.ts +18 -3
- package/src/mcp/protocol.ts +91 -41
- package/src/mcp/tools.ts +26 -13
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +309 -14
- package/src/tools/ask.ts +700 -77
- package/src/tools/check-diff.ts +331 -28
- package/src/tools/docs-check.ts +241 -39
- package/src/tools/find.ts +386 -29
- package/src/tools/locate.ts +384 -19
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +479 -21
- package/src/tools/spec-check.ts +193 -19
|
@@ -1,107 +1,122 @@
|
|
|
1
1
|
import { ASK_MAX_FILES } from "../constants.ts";
|
|
2
2
|
import type { Question, State } from "../jev/types.ts";
|
|
3
|
+
import type { Cause } from "../result-types.ts";
|
|
3
4
|
import type { CompiledAsks } from "./asks.ts";
|
|
4
5
|
|
|
5
6
|
export interface UnresolvedAskReference {
|
|
6
7
|
reference: string;
|
|
7
8
|
reason: string;
|
|
9
|
+
/** Admission cause when the reference was refused, e.g. `secret_pattern`. */
|
|
10
|
+
cause?: Cause;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
origin?: "question" | "note";
|
|
13
|
+
scope?: string;
|
|
14
|
+
side?: "files" | "files_before";
|
|
15
|
+
}
|
|
16
|
+
export interface EvidenceIndex {
|
|
17
|
+
files: Readonly<Record<string, string>>;
|
|
18
|
+
files_before?: Readonly<Record<string, string | null>>;
|
|
19
|
+
beforeInventory?: readonly string[];
|
|
20
|
+
aliases?: Readonly<Record<string, string | readonly string[]>>;
|
|
21
|
+
}
|
|
22
|
+
export interface ReferenceResolution {
|
|
23
|
+
additions: readonly {
|
|
24
|
+
reference: string;
|
|
25
|
+
path: string;
|
|
26
|
+
required: boolean;
|
|
27
|
+
origin: "question" | "note";
|
|
28
|
+
scope: string;
|
|
29
|
+
side: "files" | "files_before";
|
|
30
|
+
}[];
|
|
31
|
+
unresolved: readonly UnresolvedAskReference[];
|
|
32
|
+
aliases: Readonly<Record<string, string>>;
|
|
33
|
+
invalid: readonly UnresolvedAskReference[];
|
|
8
34
|
}
|
|
9
|
-
|
|
10
35
|
const fileSelector =
|
|
11
36
|
/\b(files|files_before)\s*\[\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\]/g;
|
|
12
37
|
const selector =
|
|
13
38
|
/\b[A-Za-z_$][\w$]*(?:(?:\s*\.\s*[A-Za-z_$][\w$]*)|(?:\s*\[\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\d+)\s*\]))*/g;
|
|
14
|
-
|
|
15
39
|
function unquote(value: string): string {
|
|
16
40
|
if (value.startsWith('"')) {
|
|
17
41
|
try {
|
|
18
42
|
const parsed: unknown = JSON.parse(value);
|
|
19
43
|
if (typeof parsed === "string") return parsed;
|
|
20
44
|
} catch {
|
|
21
|
-
|
|
45
|
+
/* retain the literal selector */
|
|
22
46
|
}
|
|
23
47
|
}
|
|
24
48
|
return value.slice(1, -1).replace(/\\(['\\])/g, "$1");
|
|
25
49
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
!/[A-Za-z_]/.test(value) ||
|
|
31
|
-
/^(?:state|docs?|files|files_before)\./.test(value)
|
|
32
|
-
)
|
|
33
|
-
return false;
|
|
34
|
-
if (/^(?:e\.g|i\.e|Node\.js|Next\.js|Vue\.js)$/i.test(value)) return false;
|
|
50
|
+
function normalized(path: string): string {
|
|
51
|
+
return path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
|
|
52
|
+
}
|
|
53
|
+
function forbidden(path: string): boolean {
|
|
35
54
|
return (
|
|
36
|
-
/^(
|
|
37
|
-
|
|
38
|
-
value,
|
|
39
|
-
) ||
|
|
40
|
-
/^\.[A-Za-z][\w.-]*$/.test(value) ||
|
|
41
|
-
/^(?:README|LICENSE|LICENCE|Makefile|Dockerfile|CHANGELOG|AGENTS|CLAUDE)(?:\.[\w-]+)?$/.test(
|
|
42
|
-
value,
|
|
43
|
-
)
|
|
55
|
+
/^(?:\/|[A-Za-z]:|[a-z][a-z\d+.-]*:\/\/)/i.test(path) ||
|
|
56
|
+
path.split(/[\\/]/).some((part) => part === ".." || part === ".git")
|
|
44
57
|
);
|
|
45
58
|
}
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
function explicitPaths(
|
|
60
|
+
text: string,
|
|
61
|
+
): { reference: string; side: "files" | "files_before" }[] {
|
|
62
|
+
const references = new Map<
|
|
63
|
+
string,
|
|
64
|
+
{ reference: string; side: "files" | "files_before" }
|
|
65
|
+
>();
|
|
66
|
+
for (const match of text.matchAll(fileSelector)) {
|
|
67
|
+
const reference = unquote(match[2] ?? "");
|
|
68
|
+
const side = match[1] as "files" | "files_before";
|
|
69
|
+
references.set(JSON.stringify([side, reference]), { reference, side });
|
|
70
|
+
}
|
|
71
|
+
return [...references.values()];
|
|
72
|
+
}
|
|
73
|
+
/** Lexical discovery hints, never requirements. Ordinary member expressions are excluded. */
|
|
48
74
|
export function citedPaths(text: string): string[] {
|
|
49
|
-
const found = new Set
|
|
50
|
-
const
|
|
51
|
-
/\b(?:[a-z][a-z\d+.-]*:\/\/|www\.)[^\s<>"'`]+/gi,
|
|
52
|
-
" "
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
61
|
-
const withoutFields = withoutSelectors.replace(selector, (value) =>
|
|
62
|
-
value.includes("[") || /^(?:state|docs?|files|files_before)\./.test(value)
|
|
63
|
-
? " "
|
|
64
|
-
: value,
|
|
65
|
-
);
|
|
66
|
-
const unquoted = withoutFields.replace(
|
|
75
|
+
const found = new Set(explicitPaths(text).map(({ reference }) => reference));
|
|
76
|
+
const cleaned = text
|
|
77
|
+
.replace(/\b(?:[a-z][a-z\d+.-]*:\/\/|www\.)[^\s<>"'`]+/gi, " ")
|
|
78
|
+
.replace(fileSelector, " ")
|
|
79
|
+
.replace(selector, (value) =>
|
|
80
|
+
value.includes("[") ||
|
|
81
|
+
/^(?:state|output|docs?|files|files_before)\./.test(value)
|
|
82
|
+
? " "
|
|
83
|
+
: value,
|
|
84
|
+
);
|
|
85
|
+
for (const match of cleaned.matchAll(
|
|
67
86
|
/`([^`\n]+)`|"([^"\n]+)"|'([^'\n]+)'/g,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
);
|
|
82
|
-
for (const match of unquoted.matchAll(/[\w.@+-]+(?:\/[\w.@+-]+)*/g)) {
|
|
83
|
-
const value = match[0].replace(/[.,;:!?]+$/, "");
|
|
87
|
+
)) {
|
|
88
|
+
const value = match[1] ?? match[2] ?? match[3] ?? "";
|
|
89
|
+
if (/^(?:[\w@+-]+\/)+[\w.@+-]+$/.test(value)) found.add(value);
|
|
90
|
+
}
|
|
91
|
+
for (const match of cleaned.matchAll(
|
|
92
|
+
/\b(?:README|LICENSE|LICENCE|Makefile|Dockerfile|CHANGELOG|AGENTS|CLAUDE)(?:\.[\w-]+)?\b/g,
|
|
93
|
+
))
|
|
94
|
+
found.add(match[0]);
|
|
95
|
+
for (const match of cleaned.matchAll(
|
|
96
|
+
/(?:\.?\.?\/)?[\w@+-]+(?:\/[\w.@+-]+)*\.(?:[cm]?[jt]sx?|py|jsonc?|ya?ml|toml|ini|cfg|conf|mdx?|txt|rst|html?|css|scss|sql|sh|bash|rs|go|java|rb|php|cpp|hpp|vue|svelte|xml|csv|lock)\b/g,
|
|
97
|
+
)) {
|
|
98
|
+
const start = match.index ?? 0;
|
|
84
99
|
if (
|
|
85
|
-
|
|
86
|
-
(
|
|
87
|
-
looksLikePath(value.slice(value.lastIndexOf("/") + 1)))
|
|
100
|
+
(start && /[\w.]/.test(cleaned[start - 1] ?? "")) ||
|
|
101
|
+
/^(?:Node|Next|Vue)\.js$/i.test(match[0])
|
|
88
102
|
)
|
|
89
|
-
|
|
103
|
+
continue;
|
|
104
|
+
found.add(match[0]);
|
|
90
105
|
}
|
|
91
106
|
return [...found];
|
|
92
107
|
}
|
|
93
|
-
|
|
94
108
|
function questionText(question: Question): string {
|
|
95
|
-
const criteria = question.criteria;
|
|
96
109
|
return [
|
|
97
110
|
question.instructions,
|
|
98
|
-
...
|
|
111
|
+
...Object.values(question.criteria ?? {}),
|
|
99
112
|
].join("\n");
|
|
100
113
|
}
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
function readingTexts(
|
|
115
|
+
plan: CompiledAsks,
|
|
116
|
+
): { id: string; text: string; about?: string }[] {
|
|
103
117
|
return plan.readings.map((reading) => ({
|
|
104
118
|
id: reading.id,
|
|
119
|
+
about: plan.asks[reading.askIndex]?.about,
|
|
105
120
|
text: [reading.id, reading.twin, ...Object.values(reading.controls ?? {})]
|
|
106
121
|
.flatMap((id) =>
|
|
107
122
|
id && plan.questions[id] ? [questionText(plan.questions[id])] : [],
|
|
@@ -109,62 +124,141 @@ function readingTexts(plan: CompiledAsks): { id: string; text: string }[] {
|
|
|
109
124
|
.join("\n"),
|
|
110
125
|
}));
|
|
111
126
|
}
|
|
112
|
-
|
|
113
|
-
function normalized(path: string): string {
|
|
114
|
-
return path.replace(/^(?:\.\/)+/, "");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
127
|
export function resolveAskReferences(
|
|
118
128
|
plan: CompiledAsks,
|
|
119
129
|
note: string | undefined,
|
|
120
|
-
|
|
130
|
+
evidence: EvidenceIndex | Readonly<Record<string, string>>,
|
|
121
131
|
inventory: readonly string[],
|
|
122
|
-
): {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
132
|
+
): ReferenceResolution {
|
|
133
|
+
const index: EvidenceIndex =
|
|
134
|
+
evidence.files !== null && typeof evidence.files === "object"
|
|
135
|
+
? (evidence as EvidenceIndex)
|
|
136
|
+
: { files: evidence as Readonly<Record<string, string>> };
|
|
137
|
+
const supplied = [
|
|
138
|
+
...new Set([
|
|
139
|
+
...Object.keys(index.files),
|
|
140
|
+
...Object.keys(index.files_before ?? {}),
|
|
141
|
+
]),
|
|
142
|
+
].sort();
|
|
143
|
+
const currentKnown = [...new Set(inventory)].sort();
|
|
144
|
+
const beforeKnown = [
|
|
145
|
+
...new Set([...inventory, ...(index.beforeInventory ?? [])]),
|
|
146
|
+
].sort();
|
|
147
|
+
const selected = new Set(supplied.map(normalized));
|
|
148
|
+
const versions = new Set([
|
|
149
|
+
...Object.keys(index.files).map((path) => `files:${normalized(path)}`),
|
|
150
|
+
...Object.keys(index.files_before ?? {}).map(
|
|
151
|
+
(path) => `files_before:${normalized(path)}`,
|
|
152
|
+
),
|
|
153
|
+
]);
|
|
154
|
+
const additions: ReferenceResolution["additions"][number][] = [];
|
|
134
155
|
const unresolved: UnresolvedAskReference[] = [];
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
const invalid: UnresolvedAskReference[] = [];
|
|
157
|
+
const aliases: Record<string, string> = Object.create(null) as Record<
|
|
158
|
+
string,
|
|
159
|
+
string
|
|
160
|
+
>;
|
|
161
|
+
const sources = [
|
|
162
|
+
...readingTexts(plan).map(({ id, text, about }) => ({
|
|
163
|
+
text: `${about ?? ""}\n${text}`,
|
|
164
|
+
scope: id,
|
|
165
|
+
origin: "question" as const,
|
|
166
|
+
output: about === "output",
|
|
167
|
+
})),
|
|
168
|
+
{
|
|
169
|
+
text: note ?? "",
|
|
170
|
+
scope: "global",
|
|
171
|
+
origin: "note" as const,
|
|
172
|
+
output: false,
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
for (const source of sources) {
|
|
176
|
+
const explicit = explicitPaths(source.text);
|
|
177
|
+
const explicitNames = new Set(explicit.map(({ reference }) => reference));
|
|
178
|
+
const references = [
|
|
179
|
+
...explicit.map((ref) => ({ ...ref, required: true })),
|
|
180
|
+
...(source.output
|
|
181
|
+
? []
|
|
182
|
+
: citedPaths(source.text)
|
|
183
|
+
.filter((reference) => !explicitNames.has(reference))
|
|
184
|
+
.map((reference) => ({
|
|
185
|
+
reference,
|
|
186
|
+
side: "files" as const,
|
|
187
|
+
required: false,
|
|
188
|
+
}))),
|
|
189
|
+
];
|
|
190
|
+
for (const ref of references) {
|
|
191
|
+
const { reference, required, side } = ref;
|
|
192
|
+
const known = side === "files_before" ? beforeKnown : currentKnown;
|
|
193
|
+
const facts = {
|
|
153
194
|
reference,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
required,
|
|
196
|
+
side,
|
|
197
|
+
origin: source.origin,
|
|
198
|
+
scope: source.scope,
|
|
199
|
+
};
|
|
200
|
+
if (forbidden(reference)) {
|
|
201
|
+
if (required)
|
|
202
|
+
invalid.push({
|
|
203
|
+
...facts,
|
|
204
|
+
reason: `Path not permitted: ${reference}`,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const key = normalized(reference);
|
|
209
|
+
// Directory-bearing explicit paths are exact, never suffix fallbacks.
|
|
210
|
+
const exact = supplied.find((path) => normalized(path) === key);
|
|
211
|
+
const abbreviated = !key.includes("/");
|
|
212
|
+
let matches: string[] = exact ? [exact] : [];
|
|
213
|
+
if (!matches.length && abbreviated) {
|
|
214
|
+
const alias =
|
|
215
|
+
index.aliases && Object.hasOwn(index.aliases, reference)
|
|
216
|
+
? index.aliases[reference]
|
|
217
|
+
: undefined;
|
|
218
|
+
if (alias !== undefined)
|
|
219
|
+
matches = (typeof alias === "string" ? [alias] : [...alias]).filter(
|
|
220
|
+
(path) => supplied.includes(path),
|
|
221
|
+
);
|
|
222
|
+
if (!matches.length)
|
|
223
|
+
matches = supplied.filter((path) =>
|
|
224
|
+
normalized(path).endsWith(`/${key}`),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (!matches.length)
|
|
228
|
+
matches = known.filter((path) => normalized(path) === key);
|
|
229
|
+
if (!matches.length && abbreviated)
|
|
230
|
+
matches = known.filter((path) => normalized(path).endsWith(`/${key}`));
|
|
231
|
+
if (matches.length !== 1) {
|
|
232
|
+
if (required)
|
|
233
|
+
unresolved.push({
|
|
234
|
+
...facts,
|
|
235
|
+
reason: `${reference} is required but missing from state (${matches.length ? "ambiguous path" : "not found"})`,
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const path = matches[0];
|
|
240
|
+
if (path === undefined) continue;
|
|
241
|
+
aliases[reference] = path;
|
|
242
|
+
const identity = normalized(path);
|
|
243
|
+
const version = `${side}:${identity}`;
|
|
244
|
+
if (versions.has(version)) continue;
|
|
245
|
+
if (!selected.has(identity) && selected.size >= ASK_MAX_FILES) {
|
|
246
|
+
unresolved.push({
|
|
247
|
+
...facts,
|
|
248
|
+
reason: `${reference} omitted (ASK_MAX_FILES limit: ${ASK_MAX_FILES})`,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
selected.add(identity);
|
|
253
|
+
versions.add(version);
|
|
254
|
+
additions.push({ ...facts, path });
|
|
157
255
|
}
|
|
158
|
-
selected.add(normalized(path));
|
|
159
|
-
additions.push({ reference, path });
|
|
160
256
|
}
|
|
161
|
-
return { additions, unresolved };
|
|
257
|
+
return { additions, unresolved, aliases, invalid };
|
|
162
258
|
}
|
|
163
|
-
|
|
164
259
|
function object(value: unknown): value is Record<string, unknown> {
|
|
165
260
|
return value !== null && typeof value === "object";
|
|
166
261
|
}
|
|
167
|
-
|
|
168
262
|
function unpack(value: unknown): unknown {
|
|
169
263
|
if (typeof value !== "string" || !/^[\s]*[[{]/.test(value)) return value;
|
|
170
264
|
try {
|
|
@@ -173,24 +267,17 @@ function unpack(value: unknown): unknown {
|
|
|
173
267
|
return value;
|
|
174
268
|
}
|
|
175
269
|
}
|
|
176
|
-
|
|
177
270
|
function empty(value: unknown): boolean {
|
|
178
271
|
return (
|
|
272
|
+
value === undefined ||
|
|
179
273
|
value === null ||
|
|
180
274
|
(typeof value === "string" && value.trim() === "") ||
|
|
181
275
|
(object(value) && Object.keys(value).length === 0)
|
|
182
276
|
);
|
|
183
277
|
}
|
|
184
|
-
|
|
185
278
|
function emptyReference(text: string, state: State): string | undefined {
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
if (object(files)) {
|
|
189
|
-
for (const reference of citedPaths(text)) {
|
|
190
|
-
if (Object.hasOwn(files, reference) && empty(files[reference]))
|
|
191
|
-
return reference;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
279
|
+
const metadata = object(state.evidence) ? state.evidence : {};
|
|
280
|
+
const aliases = object(metadata.aliases) ? metadata.aliases : {};
|
|
194
281
|
for (const match of text.matchAll(selector)) {
|
|
195
282
|
const parts = [
|
|
196
283
|
...match[0].matchAll(
|
|
@@ -198,32 +285,51 @@ function emptyReference(text: string, state: State): string | undefined {
|
|
|
198
285
|
),
|
|
199
286
|
].map(([part]) => (/^["']/.test(part) ? unquote(part) : part));
|
|
200
287
|
const root = parts[0];
|
|
201
|
-
if (root
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
288
|
+
if (!root) continue;
|
|
289
|
+
if (
|
|
290
|
+
!["state", "output", "files", "files_before"].includes(root) &&
|
|
291
|
+
!Object.hasOwn(state, root)
|
|
292
|
+
)
|
|
293
|
+
continue;
|
|
294
|
+
if (!/[.[]/.test(match[0]) && text.trim() !== match[0].trim()) continue;
|
|
295
|
+
let value: unknown =
|
|
296
|
+
root === "state"
|
|
297
|
+
? Object.hasOwn(state, "state")
|
|
298
|
+
? state.state
|
|
299
|
+
: state
|
|
300
|
+
: state[root];
|
|
301
|
+
const tail = parts.slice(1);
|
|
302
|
+
if (
|
|
303
|
+
(root === "files" || root === "files_before") &&
|
|
304
|
+
tail[0] &&
|
|
305
|
+
object(value) &&
|
|
306
|
+
!Object.hasOwn(value, tail[0])
|
|
307
|
+
) {
|
|
308
|
+
const alias = Object.hasOwn(aliases, tail[0])
|
|
309
|
+
? aliases[tail[0]]
|
|
310
|
+
: undefined;
|
|
311
|
+
if (typeof alias === "string") tail[0] = alias;
|
|
312
|
+
}
|
|
313
|
+
for (const part of tail) {
|
|
213
314
|
value = unpack(value);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
value = value[part];
|
|
315
|
+
value =
|
|
316
|
+
object(value) && Object.hasOwn(value, part) ? value[part] : undefined;
|
|
219
317
|
}
|
|
220
|
-
//
|
|
221
|
-
if (
|
|
222
|
-
|
|
318
|
+
// null before is admitted only by the base reader for a tracked-tree absence.
|
|
319
|
+
if (
|
|
320
|
+
root === "files_before" &&
|
|
321
|
+
value === null &&
|
|
322
|
+
typeof state.base_sha === "string" &&
|
|
323
|
+
state.base_sha.length > 0
|
|
324
|
+
)
|
|
325
|
+
continue;
|
|
326
|
+
if (
|
|
327
|
+
empty(root === "files" || root === "files_before" ? value : unpack(value))
|
|
328
|
+
)
|
|
329
|
+
return match[0].trim();
|
|
223
330
|
}
|
|
224
331
|
return undefined;
|
|
225
332
|
}
|
|
226
|
-
|
|
227
333
|
export function blockedAskReadings(
|
|
228
334
|
plan: CompiledAsks,
|
|
229
335
|
state: State,
|
|
@@ -231,19 +337,29 @@ export function blockedAskReadings(
|
|
|
231
337
|
note?: string,
|
|
232
338
|
): ReadonlyMap<string, string> {
|
|
233
339
|
const blocked = new Map<string, string>();
|
|
234
|
-
const notePaths = new Set(citedPaths(note ?? ""));
|
|
235
340
|
const noteEmpty = emptyReference(note ?? "", state);
|
|
236
|
-
for (const { id, text } of readingTexts(plan)) {
|
|
237
|
-
const
|
|
341
|
+
for (const { id, text, about } of readingTexts(plan)) {
|
|
342
|
+
const explicit = new Set(
|
|
343
|
+
explicitPaths(`${about ?? ""}\n${text}`).map(
|
|
344
|
+
({ reference }) => reference,
|
|
345
|
+
),
|
|
346
|
+
);
|
|
238
347
|
const missing = unresolved.find(
|
|
239
|
-
(
|
|
348
|
+
(ref) =>
|
|
349
|
+
ref.required !== false &&
|
|
350
|
+
(ref.scope === "global" ||
|
|
351
|
+
ref.scope === id ||
|
|
352
|
+
(ref.scope === undefined && explicit.has(ref.reference))),
|
|
240
353
|
);
|
|
241
354
|
if (missing) {
|
|
242
355
|
blocked.set(id, missing.reason);
|
|
243
356
|
continue;
|
|
244
357
|
}
|
|
245
|
-
const reference =
|
|
246
|
-
|
|
358
|
+
const reference =
|
|
359
|
+
noteEmpty ??
|
|
360
|
+
(about ? emptyReference(about, state) : undefined) ??
|
|
361
|
+
emptyReference(text, state);
|
|
362
|
+
if (reference) blocked.set(id, `${reference} is missing or empty`);
|
|
247
363
|
}
|
|
248
364
|
return blocked;
|
|
249
365
|
}
|
package/src/core/asks.ts
CHANGED
|
@@ -505,8 +505,7 @@ export function readAsks(
|
|
|
505
505
|
.join("; ");
|
|
506
506
|
result.push({
|
|
507
507
|
label: r.label,
|
|
508
|
-
|
|
509
|
-
band: "unsure",
|
|
508
|
+
unjudged: true,
|
|
510
509
|
reason: `${reason}; ${r.missing}`,
|
|
511
510
|
uncalibrated: r.uncalibrated,
|
|
512
511
|
});
|
|
@@ -520,7 +519,23 @@ export function readAsks(
|
|
|
520
519
|
: undefined;
|
|
521
520
|
result.push({
|
|
522
521
|
label: level ? `${r.label} ${JSON.stringify(level)}` : r.label,
|
|
522
|
+
source: answer.source,
|
|
523
|
+
...(answer.type === "bool" ? { publicResult: answer.p >= 0.5 } : {}),
|
|
523
524
|
answer,
|
|
525
|
+
rawValues:
|
|
526
|
+
answer.type === "bool"
|
|
527
|
+
? [
|
|
528
|
+
{
|
|
529
|
+
label: "bool",
|
|
530
|
+
value: answer.p >= 0.5,
|
|
531
|
+
probability: { status: "known", value: answer.p },
|
|
532
|
+
},
|
|
533
|
+
]
|
|
534
|
+
: Object.entries(answer.probabilities).map(([label, value]) => ({
|
|
535
|
+
label,
|
|
536
|
+
value: label,
|
|
537
|
+
probability: { status: "known" as const, value },
|
|
538
|
+
})),
|
|
524
539
|
uncalibrated: r.uncalibrated,
|
|
525
540
|
});
|
|
526
541
|
continue;
|
|
@@ -583,13 +598,22 @@ export function readAsks(
|
|
|
583
598
|
if (
|
|
584
599
|
Object.values(r.controls ?? {}).some((id) => answers[id]?.type !== "bool")
|
|
585
600
|
) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
601
|
+
result.push({
|
|
602
|
+
label: r.label,
|
|
603
|
+
unjudged: true,
|
|
604
|
+
reason: "required coherence control has no valid bool response",
|
|
605
|
+
missing: r.missing,
|
|
606
|
+
});
|
|
607
|
+
continue;
|
|
589
608
|
}
|
|
590
609
|
if (!r.twin && Object.hasOwn(reversed, r.id) && !second) {
|
|
591
|
-
|
|
592
|
-
|
|
610
|
+
result.push({
|
|
611
|
+
label: r.label,
|
|
612
|
+
unjudged: true,
|
|
613
|
+
reason: "required reverse-order control unjudged",
|
|
614
|
+
missing: r.missing,
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
593
617
|
}
|
|
594
618
|
const firstMerged = merged(original, r.kind === "verify");
|
|
595
619
|
const secondMerged = second
|
|
@@ -601,6 +625,54 @@ export function readAsks(
|
|
|
601
625
|
? `${r.label} ${JSON.stringify(r.options[head])}`
|
|
602
626
|
: r.label,
|
|
603
627
|
answer: { ...answer, probabilities: p, choice: head },
|
|
628
|
+
source: answer.source,
|
|
629
|
+
rawValues: Object.entries(original).map(([label, value]) => ({
|
|
630
|
+
label,
|
|
631
|
+
value: label,
|
|
632
|
+
probability: { status: "known" as const, value },
|
|
633
|
+
})),
|
|
634
|
+
reportControls: [
|
|
635
|
+
...members.slice(1).map(([id, label]) => ({
|
|
636
|
+
id,
|
|
637
|
+
label,
|
|
638
|
+
answer: answers[id],
|
|
639
|
+
kind: "cross_check" as const,
|
|
640
|
+
})),
|
|
641
|
+
...(!r.twin && reversed[r.id]
|
|
642
|
+
? [
|
|
643
|
+
{
|
|
644
|
+
id: `${r.id}:reverse`,
|
|
645
|
+
label: "reverse order",
|
|
646
|
+
answer: reversed[r.id],
|
|
647
|
+
kind: "order" as const,
|
|
648
|
+
},
|
|
649
|
+
]
|
|
650
|
+
: []),
|
|
651
|
+
].flatMap(({ id, label, answer: a, kind }) => {
|
|
652
|
+
if (!a || a.type === "unjudged" || !a.source) return [];
|
|
653
|
+
return [
|
|
654
|
+
{
|
|
655
|
+
id,
|
|
656
|
+
kind,
|
|
657
|
+
source: a.source,
|
|
658
|
+
outcome: label,
|
|
659
|
+
rawValues:
|
|
660
|
+
a.type === "bool"
|
|
661
|
+
? [
|
|
662
|
+
{
|
|
663
|
+
label,
|
|
664
|
+
value: a.p >= 0.5,
|
|
665
|
+
probability: { status: "known" as const, value: a.p },
|
|
666
|
+
},
|
|
667
|
+
]
|
|
668
|
+
: Object.entries(a.probabilities).map(([key, value]) => ({
|
|
669
|
+
label: key,
|
|
670
|
+
value: key,
|
|
671
|
+
probability: { status: "known" as const, value },
|
|
672
|
+
})),
|
|
673
|
+
},
|
|
674
|
+
];
|
|
675
|
+
}),
|
|
604
676
|
value: { head, p: mass },
|
|
605
677
|
band,
|
|
606
678
|
reason,
|
|
@@ -58,9 +58,14 @@ export function checkImportBoundaries(
|
|
|
58
58
|
detail: `cannot resolve ${edge.specifier} inside src/`,
|
|
59
59
|
});
|
|
60
60
|
} else if (
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
[
|
|
62
|
+
"core",
|
|
63
|
+
"presets",
|
|
64
|
+
"texts",
|
|
65
|
+
"constants.ts",
|
|
66
|
+
"result.ts",
|
|
67
|
+
"result-types.ts",
|
|
68
|
+
].includes(layer) &&
|
|
64
69
|
!(layer === "core" && ["node:path", "path"].includes(edge.specifier)) &&
|
|
65
70
|
!(
|
|
66
71
|
layer === "core" &&
|