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.
Files changed (117) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +22 -14
  4. package/SECURITY.md +17 -1
  5. package/dist/adapters/ask-files.js +11 -2
  6. package/dist/adapters/ask-proof.js +63 -7
  7. package/dist/adapters/command.js +82 -29
  8. package/dist/adapters/docs.js +30 -10
  9. package/dist/adapters/evidence-context.js +119 -0
  10. package/dist/adapters/files.js +141 -16
  11. package/dist/adapters/find.js +34 -6
  12. package/dist/adapters/git-base.js +7 -1
  13. package/dist/adapters/git.js +51 -7
  14. package/dist/adapters/locate-file.js +47 -9
  15. package/dist/adapters/private-storage.js +14 -6
  16. package/dist/adapters/risk-callers.js +3 -0
  17. package/dist/adapters/shell.js +23 -7
  18. package/dist/adapters/test-inventory.js +10 -2
  19. package/dist/configuration.js +17 -7
  20. package/dist/constants.js +26 -5
  21. package/dist/core/ask-references.js +193 -109
  22. package/dist/core/asks.js +78 -7
  23. package/dist/core/locate.js +8 -8
  24. package/dist/core/output.js +17 -0
  25. package/dist/core/result-report.js +302 -0
  26. package/dist/core/secret-path.js +34 -0
  27. package/dist/core/state.js +8 -1
  28. package/dist/core/units.js +1 -1
  29. package/dist/jev/client.js +34 -12
  30. package/dist/mcp/protocol.js +50 -27
  31. package/dist/mcp/tools.js +20 -7
  32. package/dist/render.js +72 -0
  33. package/dist/report-schema.js +1356 -0
  34. package/dist/result-types.js +1 -0
  35. package/dist/texts/ask-files.js +3 -1
  36. package/dist/texts/ask.js +3 -1
  37. package/dist/texts/check-diff.js +7 -4
  38. package/dist/texts/find.js +7 -2
  39. package/dist/texts/guide.js +3 -16
  40. package/dist/texts/instructions.js +72 -0
  41. package/dist/texts/locate.js +7 -2
  42. package/dist/texts/select-tests.js +3 -1
  43. package/dist/tools/ask-files.js +248 -15
  44. package/dist/tools/ask.js +523 -62
  45. package/dist/tools/check-diff.js +222 -30
  46. package/dist/tools/docs-check.js +122 -13
  47. package/dist/tools/find.js +320 -27
  48. package/dist/tools/locate.js +317 -18
  49. package/dist/tools/review-report.js +230 -0
  50. package/dist/tools/select-tests.js +273 -19
  51. package/dist/tools/spec-check.js +119 -22
  52. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
  53. package/docs/agent-instructions.md +59 -30
  54. package/docs/design.md +13 -1
  55. package/docs/mcp.md +8 -6
  56. package/docs/tools/jev_ask.md +8 -5
  57. package/docs/tools/jev_ask_files.md +2 -1
  58. package/docs/tools/jev_check_diff.md +4 -1
  59. package/docs/tools/jev_find_files.md +2 -1
  60. package/docs/tools/jev_locate_in_file.md +5 -0
  61. package/docs/tools/jev_select_tests.md +4 -1
  62. package/package.json +1 -1
  63. package/rules/jev-ask.md +22 -1
  64. package/server.json +2 -2
  65. package/src/adapters/ask-files.ts +11 -3
  66. package/src/adapters/ask-proof.ts +69 -11
  67. package/src/adapters/command.ts +96 -33
  68. package/src/adapters/docs.ts +33 -14
  69. package/src/adapters/evidence-context.ts +169 -0
  70. package/src/adapters/files.ts +146 -16
  71. package/src/adapters/find.ts +37 -7
  72. package/src/adapters/git-base.ts +7 -1
  73. package/src/adapters/git.ts +61 -8
  74. package/src/adapters/locate-file.ts +51 -9
  75. package/src/adapters/private-storage.ts +17 -5
  76. package/src/adapters/risk-callers.ts +3 -0
  77. package/src/adapters/shell.ts +23 -7
  78. package/src/adapters/test-inventory.ts +12 -4
  79. package/src/configuration.ts +16 -2
  80. package/src/constants.ts +26 -5
  81. package/src/core/ask-references.ts +262 -146
  82. package/src/core/asks.ts +79 -7
  83. package/src/core/import-boundaries.ts +8 -3
  84. package/src/core/locate.ts +8 -5
  85. package/src/core/output.ts +34 -0
  86. package/src/core/result-report.ts +410 -0
  87. package/src/core/secret-path.ts +37 -0
  88. package/src/core/state.ts +8 -1
  89. package/src/core/units.ts +3 -2
  90. package/src/index.ts +3 -0
  91. package/src/jev/client.ts +54 -16
  92. package/src/jev/types.ts +18 -3
  93. package/src/mcp/protocol.ts +91 -41
  94. package/src/mcp/tools.ts +26 -13
  95. package/src/render.ts +109 -0
  96. package/src/report-schema.ts +1380 -0
  97. package/src/result-types.ts +234 -0
  98. package/src/result.ts +4 -1
  99. package/src/runtime.ts +6 -0
  100. package/src/texts/ask-files.ts +4 -1
  101. package/src/texts/ask.ts +8 -1
  102. package/src/texts/check-diff.ts +7 -4
  103. package/src/texts/find.ts +8 -2
  104. package/src/texts/guide.ts +8 -16
  105. package/src/texts/instructions.ts +98 -0
  106. package/src/texts/locate.ts +8 -2
  107. package/src/texts/run-end.ts +2 -2
  108. package/src/texts/select-tests.ts +4 -1
  109. package/src/tools/ask-files.ts +309 -14
  110. package/src/tools/ask.ts +700 -77
  111. package/src/tools/check-diff.ts +331 -28
  112. package/src/tools/docs-check.ts +241 -39
  113. package/src/tools/find.ts +386 -29
  114. package/src/tools/locate.ts +384 -19
  115. package/src/tools/review-report.ts +308 -0
  116. package/src/tools/select-tests.ts +479 -21
  117. 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
- return value.slice(1, -1);
45
+ /* retain the literal selector */
22
46
  }
23
47
  }
24
48
  return value.slice(1, -1).replace(/\\(['\\])/g, "$1");
25
49
  }
26
-
27
- function looksLikePath(value: string): boolean {
28
- if (/\s/.test(value) || /^(?:[a-z]+:|www\.)/i.test(value)) return false;
29
- if (
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
- /^(?:\.?\.?\/)?[\w.@+-]+(?:\/[\w.@+-]+)+$/.test(value) ||
37
- /^[\w@+-][\w.@+-]*\.(?:[cm]?[jt]sx?|py|json|jsonc|ya?ml|toml|ini|cfg|conf|md|mdx|txt|rst|html?|css|scss|sql|sh|bash|rs|go|java|rb|php|c|h|cpp|hpp|vue|svelte|xml|csv|env|lock)$/i.test(
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
- /** Explicit paths only: numbers, dates and URLs are not evidence references. */
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<string>();
50
- const withoutUrls = text.replace(
51
- /\b(?:[a-z][a-z\d+.-]*:\/\/|www\.)[^\s<>"'`]+/gi,
52
- " ",
53
- );
54
- const withoutSelectors = withoutUrls.replace(
55
- fileSelector,
56
- (_whole, _root: string, quoted: string) => {
57
- found.add(unquote(quoted));
58
- return " ";
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
- _whole,
70
- backtick: string | undefined,
71
- double: string | undefined,
72
- single: string | undefined,
73
- ) => {
74
- const value = backtick ?? double ?? single ?? "";
75
- if (looksLikePath(value)) {
76
- found.add(value);
77
- return " ";
78
- }
79
- return value;
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
- looksLikePath(value) &&
86
- (!value.includes("/") ||
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
- found.add(value);
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
- ...(criteria ? Object.values(criteria) : []),
111
+ ...Object.values(question.criteria ?? {}),
99
112
  ].join("\n");
100
113
  }
101
-
102
- function readingTexts(plan: CompiledAsks): { id: string; text: string }[] {
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
- files: Readonly<Record<string, string>>,
130
+ evidence: EvidenceIndex | Readonly<Record<string, string>>,
121
131
  inventory: readonly string[],
122
- ): {
123
- additions: readonly { reference: string; path: string }[];
124
- unresolved: readonly UnresolvedAskReference[];
125
- } {
126
- const references = new Set(
127
- readingTexts(plan).flatMap(({ text }) => citedPaths(text)),
128
- );
129
- for (const reference of citedPaths(note ?? "")) references.add(reference);
130
- const paths = [...new Set([...inventory, ...Object.keys(files)])];
131
- const exact = new Map(paths.map((path) => [normalized(path), path]));
132
- const selected = new Set(Object.keys(files).map(normalized));
133
- const additions: { reference: string; path: string }[] = [];
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
- for (const reference of references) {
136
- if (Object.hasOwn(files, reference)) continue;
137
- const key = normalized(reference);
138
- const direct = exact.get(key);
139
- const matches = direct
140
- ? [direct]
141
- : paths.filter((path) => normalized(path).endsWith(`/${key}`));
142
- if (matches.length !== 1) {
143
- unresolved.push({
144
- reference,
145
- reason: `${reference} is cited but missing from state (${matches.length ? "ambiguous path" : "not found"})`,
146
- });
147
- continue;
148
- }
149
- const path = matches[0];
150
- if (path === undefined) continue;
151
- if (!selected.has(normalized(path)) && selected.size >= ASK_MAX_FILES) {
152
- unresolved.push({
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
- reason: `${reference} is cited but missing from state (ASK_MAX_FILES limit: ${ASK_MAX_FILES})`,
155
- });
156
- continue;
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
- // File references can be plain paths, not just selectors.
187
- const files = state.files;
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 === undefined) continue;
202
- let value: unknown;
203
- let offset: number;
204
- if (root === "state") {
205
- value = state;
206
- offset = 1;
207
- } else if (Object.hasOwn(state, root)) {
208
- value = state[root];
209
- offset = 1;
210
- } else continue;
211
- let present = true;
212
- for (const part of parts.slice(offset)) {
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
- if (!object(value) || !Object.hasOwn(value, part)) {
215
- present = false;
216
- break;
217
- }
218
- value = value[part];
315
+ value =
316
+ object(value) && Object.hasOwn(value, part) ? value[part] : undefined;
219
317
  }
220
- // A file absent from the base proves creation; it is not empty evidence.
221
- if (root === "files_before" && value === null) continue;
222
- if (present && empty(unpack(value))) return match[0].trim();
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 paths = new Set(citedPaths(text));
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
- ({ reference }) => paths.has(reference) || notePaths.has(reference),
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 = noteEmpty ?? emptyReference(text, state);
246
- if (reference) blocked.set(id, `${reference} is empty`);
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
- value: { head: "unjudged", p: 0 },
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
- band = "unsure";
587
- reason =
588
- "coherence control unjudged; no verdict: rerun with decisive evidence";
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
- band = "unsure";
592
- reason = `reverse-order control unjudged; no verdict: ${r.missing}`;
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
- ["core", "presets", "texts", "constants.ts", "result.ts"].includes(
62
- layer,
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" &&