@withtruing/companion 0.0.0-stage → 0.13.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/LICENSE +21 -0
- package/README.md +329 -3
- package/dist/api-key-caller.js +388 -0
- package/dist/claude-binary.js +122 -0
- package/dist/claude-cli-caller.js +344 -0
- package/dist/cli-arguments.js +11 -0
- package/dist/cli.js +1486 -0
- package/dist/codex-cli-caller.js +536 -0
- package/dist/config.js +119 -0
- package/dist/design-context.js +54 -0
- package/dist/git-range.js +291 -0
- package/dist/http-client.js +214 -0
- package/dist/import-resolver.js +644 -0
- package/dist/investigation-reader.js +1303 -0
- package/dist/local-scan.js +243 -0
- package/dist/managed-repo.js +408 -0
- package/dist/observed-sources.js +85 -0
- package/dist/reader-result-contract.js +467 -0
- package/dist/sandbox.js +215 -0
- package/dist/schema-entities.js +755 -0
- package/dist/service.js +710 -0
- package/dist/shipment.js +115 -0
- package/dist/strict-schema.js +187 -0
- package/dist/surface-mapper.js +357 -0
- package/dist/text-model.js +121 -0
- package/dist/types.js +11 -0
- package/dist/usage-limit.js +130 -0
- package/package.json +50 -6
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createSandbox } from './sandbox.js';
|
|
2
|
+
/** Resolve every immutable identity before exposing any worktree for reading. This prevents an
|
|
3
|
+
* earlier source from being inspected before a later required source proves unavailable. */
|
|
4
|
+
export async function obtainRequestedSources(request, connections) {
|
|
5
|
+
const sourceEntryIds = new Set();
|
|
6
|
+
for (const source of request.requestedSources) {
|
|
7
|
+
if (sourceEntryIds.has(source.sourceEntryId)) {
|
|
8
|
+
throw new Error(`Investigation repeats sourceEntryId ${source.sourceEntryId}`);
|
|
9
|
+
}
|
|
10
|
+
sourceEntryIds.add(source.sourceEntryId);
|
|
11
|
+
}
|
|
12
|
+
const observedSources = [];
|
|
13
|
+
const sandboxes = new Map();
|
|
14
|
+
const cleanup = async () => {
|
|
15
|
+
const pending = [...sandboxes.values()];
|
|
16
|
+
sandboxes.clear();
|
|
17
|
+
await Promise.all(pending.map((sandbox) => sandbox.cleanup()));
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
for (const source of request.requestedSources) {
|
|
21
|
+
const connection = connections.find((candidate) => candidate.connectionId === source.connectionId);
|
|
22
|
+
if (!connection) {
|
|
23
|
+
observedSources.push({
|
|
24
|
+
sourceEntryId: source.sourceEntryId,
|
|
25
|
+
role: source.role,
|
|
26
|
+
connectionId: source.connectionId,
|
|
27
|
+
sourceIdentity: `unavailable:no configured checkout for connection ${source.connectionId}`,
|
|
28
|
+
obtainedBy: 'unavailable',
|
|
29
|
+
});
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const sandbox = await createSandbox(connection.repoPath, source.commit);
|
|
33
|
+
if (sandbox.obtainedBy === 'unavailable') {
|
|
34
|
+
observedSources.push({
|
|
35
|
+
sourceEntryId: source.sourceEntryId,
|
|
36
|
+
role: source.role,
|
|
37
|
+
connectionId: source.connectionId,
|
|
38
|
+
sourceIdentity: sandbox.sourceIdentity,
|
|
39
|
+
obtainedBy: sandbox.obtainedBy,
|
|
40
|
+
...(sandbox.fetchOk === undefined ? {} : { fetchOk: sandbox.fetchOk }),
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
sandboxes.set(source.sourceEntryId, sandbox);
|
|
45
|
+
observedSources.push({
|
|
46
|
+
sourceEntryId: source.sourceEntryId,
|
|
47
|
+
role: source.role,
|
|
48
|
+
connectionId: source.connectionId,
|
|
49
|
+
sourceIdentity: sandbox.sourceIdentity,
|
|
50
|
+
obtainedBy: sandbox.obtainedBy,
|
|
51
|
+
...(sandbox.fetchOk === undefined ? {} : { fetchOk: sandbox.fetchOk }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
await cleanup();
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
observedSources,
|
|
61
|
+
sandboxes,
|
|
62
|
+
unavailable: observedSources.some((source) => source.obtainedBy === 'unavailable'),
|
|
63
|
+
cleanup,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Obtain each requested immutable source, optionally inspect it while its worktree is alive, and
|
|
67
|
+
* return the observed-source half of a reader result. Every created worktree is cleaned in a
|
|
68
|
+
* finally block, including when the inspection callback crashes. */
|
|
69
|
+
export async function buildObservedSources(request, connections, inspectSource = async () => { }) {
|
|
70
|
+
const obtained = await obtainRequestedSources(request, connections);
|
|
71
|
+
try {
|
|
72
|
+
if (!obtained.unavailable) {
|
|
73
|
+
for (const source of request.requestedSources) {
|
|
74
|
+
const sandbox = obtained.sandboxes.get(source.sourceEntryId);
|
|
75
|
+
if (!sandbox)
|
|
76
|
+
throw new Error(`No sandbox obtained for ${source.sourceEntryId}`);
|
|
77
|
+
await inspectSource(source, sandbox);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return obtained.observedSources;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await obtained.cleanup();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
const contributes = (assessment) => assessment.outcome === 'supported' || assessment.outcome === 'contradicted';
|
|
2
|
+
const breadth = { element: 0, named_paths: 1, module_surface: 2 };
|
|
3
|
+
export function scopeCovers(covering, declared) {
|
|
4
|
+
const coveringHasSurface = covering.surfaceId !== undefined || covering.surfaceHash !== undefined;
|
|
5
|
+
const declaredHasSurface = declared.surfaceId !== undefined || declared.surfaceHash !== undefined;
|
|
6
|
+
if (coveringHasSurface || declaredHasSurface) {
|
|
7
|
+
if (covering.surfaceId !== declared.surfaceId || covering.surfaceHash !== declared.surfaceHash)
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (breadth[covering.breadth] < breadth[declared.breadth])
|
|
11
|
+
return false;
|
|
12
|
+
const coveredOperations = new Set(covering.operations ?? []);
|
|
13
|
+
if ((declared.operations ?? []).some((operation) => !coveredOperations.has(operation))) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (declared.breadth !== 'named_paths' || covering.breadth !== 'named_paths')
|
|
17
|
+
return true;
|
|
18
|
+
const covered = new Set(covering.paths || []);
|
|
19
|
+
return (declared.paths || []).every((path) => covered.has(path));
|
|
20
|
+
}
|
|
21
|
+
const inventoryAccess = {
|
|
22
|
+
read: ['read_ok'],
|
|
23
|
+
missing: ['read_failed', 'not_found'],
|
|
24
|
+
truncated: ['truncated'],
|
|
25
|
+
skipped: ['skipped_size'],
|
|
26
|
+
};
|
|
27
|
+
function evidenceRepositoryPath(evidence, sourceEntries) {
|
|
28
|
+
const source = sourceEntries.find((entry) => entry.sourceEntryId === evidence.sourceEntryId);
|
|
29
|
+
if (!source)
|
|
30
|
+
return null;
|
|
31
|
+
const prefix = `repo:${source.connectionId}@${source.commit}:`;
|
|
32
|
+
return evidence.locator.startsWith(prefix) ? evidence.locator.slice(prefix.length) : null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Checks only factual access metadata. It deliberately does not judge whether the delivered
|
|
36
|
+
* evidence is sufficient for the assessment's declared semantic scope.
|
|
37
|
+
*/
|
|
38
|
+
export function factualInventoryError(scope, evidence, sourceEntries, allowedSourceEntryIds) {
|
|
39
|
+
if (!scope.investigated)
|
|
40
|
+
return null;
|
|
41
|
+
const allowed = new Set(allowedSourceEntryIds);
|
|
42
|
+
const eligible = evidence
|
|
43
|
+
.filter((row) => allowed.has(row.sourceEntryId))
|
|
44
|
+
.map((row) => ({ row, path: evidenceRepositoryPath(row, sourceEntries) }))
|
|
45
|
+
.filter((item) => item.path !== null);
|
|
46
|
+
for (const bucket of Object.keys(inventoryAccess)) {
|
|
47
|
+
for (const claimedPath of scope.investigated[bucket]) {
|
|
48
|
+
if (!eligible.some(({ row, path }) => path === claimedPath && inventoryAccess[bucket].includes(row.access))) {
|
|
49
|
+
return `${bucket} inventory claims '${claimedPath}' without matching ${inventoryAccess[bucket].join(' or ')} evidence from an allowed repository snapshot`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const claimedSearch of scope.investigated.searched) {
|
|
54
|
+
if (!eligible.some(({ row }) => row.kind === 'search_result'
|
|
55
|
+
&& row.delivery === 'search_result'
|
|
56
|
+
&& (row.access === 'read_ok' || row.access === 'truncated')
|
|
57
|
+
&& row.query === claimedSearch)) {
|
|
58
|
+
return `searched inventory claims '${claimedSearch}' without matching delivered search evidence from an allowed repository snapshot`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function parseRange(value) {
|
|
64
|
+
const match = /^L(\d+)-L(\d+)$/.exec(String(value || ''));
|
|
65
|
+
if (!match)
|
|
66
|
+
return null;
|
|
67
|
+
const start = Number(match[1]);
|
|
68
|
+
const end = Number(match[2]);
|
|
69
|
+
return start <= end ? [start, end] : null;
|
|
70
|
+
}
|
|
71
|
+
export const result_binds_request = (result, context) => {
|
|
72
|
+
if (result.investigationId !== context.request.investigationId)
|
|
73
|
+
return `result claims ${result.investigationId}; the request is ${context.request.investigationId}`;
|
|
74
|
+
if (result.attempt !== context.request.attempt)
|
|
75
|
+
return `result claims attempt ${result.attempt}; the request issued attempt ${context.request.attempt}`;
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
export const identifiers_unique = (result, context) => {
|
|
79
|
+
const duplicate = (values) => values.find((value, index) => values.indexOf(value) !== index);
|
|
80
|
+
let found = duplicate(result.evidence.map((evidence) => evidence.evidenceId));
|
|
81
|
+
if (found)
|
|
82
|
+
return `evidence id ${found} appears twice`;
|
|
83
|
+
found = duplicate(result.assessments.map((assessment) => assessment.obligationId));
|
|
84
|
+
if (found)
|
|
85
|
+
return `two assessments answer ${found}`;
|
|
86
|
+
found = duplicate(result.observedSources.map((source) => source.sourceEntryId));
|
|
87
|
+
if (found)
|
|
88
|
+
return `source entry ${found} observed twice`;
|
|
89
|
+
found = duplicate(context.request.requestedSources.map((source) => source.sourceEntryId));
|
|
90
|
+
if (found)
|
|
91
|
+
return `request lists source entry ${found} twice`;
|
|
92
|
+
for (const assessment of result.assessments) {
|
|
93
|
+
const citations = new Set();
|
|
94
|
+
for (const citation of assessment.citations) {
|
|
95
|
+
const key = JSON.stringify([citation.evidenceId, citation.use]);
|
|
96
|
+
if (citations.has(key)) {
|
|
97
|
+
return `${assessment.obligationId} cites evidence ${citation.evidenceId} as ${citation.use} twice`;
|
|
98
|
+
}
|
|
99
|
+
citations.add(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
export const policy4_surface_complete = (result, context) => {
|
|
105
|
+
if (context.request.policyVersion !== 'policy-4')
|
|
106
|
+
return null;
|
|
107
|
+
const surface = context.request.surface;
|
|
108
|
+
if (!surface)
|
|
109
|
+
return 'policy-4 request has no sealed surface';
|
|
110
|
+
if (!result.execution)
|
|
111
|
+
return 'policy-4 result has no execution metadata';
|
|
112
|
+
const requested = context.sourceEntry(surface.sourceEntryId);
|
|
113
|
+
if (!requested
|
|
114
|
+
|| requested.role !== 'current'
|
|
115
|
+
|| requested.commit !== surface.commit) {
|
|
116
|
+
return 'sealed surface source does not match the requested current source';
|
|
117
|
+
}
|
|
118
|
+
for (const obligation of context.request.obligations) {
|
|
119
|
+
if (obligation.scope.surfaceId !== surface.surfaceId
|
|
120
|
+
|| obligation.scope.surfaceHash !== surface.surfaceHash) {
|
|
121
|
+
return `${obligation.obligationId} does not carry the sealed surface identity`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const expectedLocator = (path) => `repo:${requested.connectionId}@${surface.commit}:${path}`;
|
|
125
|
+
for (const assessment of result.assessments) {
|
|
126
|
+
if (assessment.scope.surfaceId !== surface.surfaceId
|
|
127
|
+
|| assessment.scope.surfaceHash !== surface.surfaceHash) {
|
|
128
|
+
return `${assessment.obligationId} does not report the sealed surface identity`;
|
|
129
|
+
}
|
|
130
|
+
if (!contributes(assessment))
|
|
131
|
+
continue;
|
|
132
|
+
if (assessment.scope.breadth !== 'module_surface'
|
|
133
|
+
|| assessment.scope.surfaceId !== surface.surfaceId
|
|
134
|
+
|| assessment.scope.surfaceHash !== surface.surfaceHash) {
|
|
135
|
+
return `${assessment.obligationId} is positive without the complete sealed surface scope`;
|
|
136
|
+
}
|
|
137
|
+
for (const path of surface.entries) {
|
|
138
|
+
const rows = result.evidence.filter((row) => row.sourceEntryId === surface.sourceEntryId && row.locator === expectedLocator(path));
|
|
139
|
+
if (rows.length !== 1
|
|
140
|
+
|| rows[0].kind !== 'source_file'
|
|
141
|
+
|| rows[0].access !== 'read_ok'
|
|
142
|
+
|| rows[0].delivery !== 'full_file'
|
|
143
|
+
|| rows[0].complete !== 1) {
|
|
144
|
+
return `${assessment.obligationId} is positive without complete delivery of ${path}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
export const evidence_source_entry_requested = (result, context) => {
|
|
151
|
+
const entries = new Set(context.request.requestedSources.map((source) => source.sourceEntryId));
|
|
152
|
+
for (const evidence of result.evidence) {
|
|
153
|
+
if (!entries.has(evidence.sourceEntryId))
|
|
154
|
+
return `evidence ${evidence.evidenceId} cites source entry ${evidence.sourceEntryId}, which was never requested`;
|
|
155
|
+
}
|
|
156
|
+
for (const source of result.observedSources) {
|
|
157
|
+
if (!entries.has(source.sourceEntryId))
|
|
158
|
+
return `observed source ${source.sourceEntryId} was never requested`;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
};
|
|
162
|
+
export const assessment_obligation_issued = (result, context) => {
|
|
163
|
+
const issued = new Set(context.request.obligations.map((obligation) => obligation.obligationId));
|
|
164
|
+
for (const assessment of result.assessments) {
|
|
165
|
+
if (!issued.has(assessment.obligationId))
|
|
166
|
+
return `assessment answers ${assessment.obligationId}, which this investigation never issued`;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
export const assessment_question_matches = (result, context) => {
|
|
171
|
+
for (const assessment of result.assessments) {
|
|
172
|
+
if (!contributes(assessment))
|
|
173
|
+
continue;
|
|
174
|
+
const obligation = context.obligation(assessment.obligationId);
|
|
175
|
+
if (!obligation)
|
|
176
|
+
continue;
|
|
177
|
+
if (assessment.question !== obligation.question)
|
|
178
|
+
return `${assessment.obligationId} asks '${obligation.question}'; the assessment answers '${assessment.question}'`;
|
|
179
|
+
if (assessment.finding && assessment.finding.question !== assessment.question)
|
|
180
|
+
return `${assessment.obligationId}: finding answers '${assessment.finding.question}', assessment claims '${assessment.question}'`;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
};
|
|
184
|
+
export const assessment_scope_covers = (result, context) => {
|
|
185
|
+
const evidenceById = new Map(result.evidence.map((row) => [row.evidenceId, row]));
|
|
186
|
+
for (const assessment of result.assessments) {
|
|
187
|
+
const obligation = context.obligation(assessment.obligationId);
|
|
188
|
+
if (!obligation)
|
|
189
|
+
continue;
|
|
190
|
+
const hasUnusableSupport = assessment.citations.some((citation) => {
|
|
191
|
+
if (citation.use !== 'support')
|
|
192
|
+
return false;
|
|
193
|
+
const row = evidenceById.get(citation.evidenceId);
|
|
194
|
+
return row && row.access !== 'read_ok' && row.access !== 'truncated';
|
|
195
|
+
});
|
|
196
|
+
if (!hasUnusableSupport) {
|
|
197
|
+
const inventoryError = factualInventoryError(assessment.scope, result.evidence, context.request.requestedSources, obligation.requiredSourceEntryIds);
|
|
198
|
+
if (inventoryError)
|
|
199
|
+
return `${assessment.obligationId}: ${inventoryError}`;
|
|
200
|
+
}
|
|
201
|
+
if (!contributes(assessment))
|
|
202
|
+
continue;
|
|
203
|
+
if (!scopeCovers(assessment.scope, obligation.scope)) {
|
|
204
|
+
if (breadth[assessment.scope.breadth] < breadth[obligation.scope.breadth])
|
|
205
|
+
return `${assessment.obligationId} declares '${obligation.scope.breadth}'; the assessment investigated only '${assessment.scope.breadth}'`;
|
|
206
|
+
const coveredOperations = new Set(assessment.scope.operations ?? []);
|
|
207
|
+
const missingOperations = (obligation.scope.operations ?? [])
|
|
208
|
+
.filter((operation) => !coveredOperations.has(operation));
|
|
209
|
+
if (missingOperations.length) {
|
|
210
|
+
return `${assessment.obligationId}: declared operations not covered — ${missingOperations.join(', ')}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (obligation.scope.breadth === 'named_paths' && assessment.scope.breadth === 'named_paths') {
|
|
214
|
+
const covered = new Set(assessment.scope.paths || []);
|
|
215
|
+
const missing = (obligation.scope.paths || []).filter((path) => !covered.has(path));
|
|
216
|
+
if (missing.length)
|
|
217
|
+
return `${assessment.obligationId}: declared paths not covered — ${missing.join(', ')}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
};
|
|
222
|
+
export const observed_sources_satisfy_obligation = (result, context) => {
|
|
223
|
+
const observed = new Map(result.observedSources.map((source) => [source.sourceEntryId, source]));
|
|
224
|
+
for (const assessment of result.assessments) {
|
|
225
|
+
if (!contributes(assessment))
|
|
226
|
+
continue;
|
|
227
|
+
const obligation = context.obligation(assessment.obligationId);
|
|
228
|
+
if (!obligation)
|
|
229
|
+
continue;
|
|
230
|
+
for (const entryId of obligation.requiredSourceEntryIds) {
|
|
231
|
+
const expected = context.sourceEntry(entryId);
|
|
232
|
+
const actual = observed.get(entryId);
|
|
233
|
+
if (!actual)
|
|
234
|
+
return `${assessment.obligationId} requires ${entryId}, which was never observed`;
|
|
235
|
+
if (!expected)
|
|
236
|
+
continue;
|
|
237
|
+
if (actual.role !== expected.role)
|
|
238
|
+
return `${entryId}: requested role '${expected.role}', observed '${actual.role}'`;
|
|
239
|
+
if (actual.connectionId !== expected.connectionId)
|
|
240
|
+
return `${entryId}: requested repository '${expected.connectionId}', observed '${actual.connectionId}'`;
|
|
241
|
+
if (actual.sourceIdentity !== `commit:${expected.commit}`)
|
|
242
|
+
return `${entryId}: requested commit:${expected.commit}, observed '${actual.sourceIdentity}'`;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
};
|
|
247
|
+
export const citation_source_allowed_by_obligation = (result, context) => {
|
|
248
|
+
const evidenceById = new Map(result.evidence.map((evidence) => [evidence.evidenceId, evidence]));
|
|
249
|
+
for (const assessment of result.assessments) {
|
|
250
|
+
const obligation = context.obligation(assessment.obligationId);
|
|
251
|
+
if (!obligation)
|
|
252
|
+
continue;
|
|
253
|
+
const allowed = new Set(obligation.requiredSourceEntryIds);
|
|
254
|
+
for (const citation of assessment.citations) {
|
|
255
|
+
const evidence = evidenceById.get(citation.evidenceId);
|
|
256
|
+
if (!evidence)
|
|
257
|
+
continue;
|
|
258
|
+
if (!allowed.has(evidence.sourceEntryId))
|
|
259
|
+
return `${assessment.obligationId} cites ${citation.evidenceId} from ${evidence.sourceEntryId}, which is not one of its required sources`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
};
|
|
264
|
+
export const citation_evidence_delivered = (result) => {
|
|
265
|
+
const delivered = new Set(result.evidence.map((evidence) => evidence.evidenceId));
|
|
266
|
+
for (const assessment of result.assessments) {
|
|
267
|
+
for (const citation of assessment.citations) {
|
|
268
|
+
if (!delivered.has(citation.evidenceId))
|
|
269
|
+
return `citation of ${citation.evidenceId}, not delivered in this result`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
};
|
|
274
|
+
export const support_citation_usable = (result) => {
|
|
275
|
+
const evidenceById = new Map(result.evidence.map((evidence) => [evidence.evidenceId, evidence]));
|
|
276
|
+
for (const assessment of result.assessments) {
|
|
277
|
+
for (const citation of assessment.citations) {
|
|
278
|
+
if (citation.use !== 'support')
|
|
279
|
+
continue;
|
|
280
|
+
const evidence = evidenceById.get(citation.evidenceId);
|
|
281
|
+
if (!evidence)
|
|
282
|
+
continue;
|
|
283
|
+
if (evidence.access !== 'read_ok' && evidence.access !== 'truncated')
|
|
284
|
+
return `${citation.evidenceId} has access '${evidence.access}' and may be cited only as a limitation`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
};
|
|
289
|
+
export const changed_since_two_roles = (result, context) => {
|
|
290
|
+
const evidenceById = new Map(result.evidence.map((evidence) => [evidence.evidenceId, evidence]));
|
|
291
|
+
for (const assessment of result.assessments) {
|
|
292
|
+
const obligation = context.obligation(assessment.obligationId);
|
|
293
|
+
if (!obligation || obligation.question !== 'changed_since')
|
|
294
|
+
continue;
|
|
295
|
+
if (!contributes(assessment))
|
|
296
|
+
continue;
|
|
297
|
+
const roles = obligation.requiredSourceEntryIds.map((id) => context.sourceEntry(id)?.role);
|
|
298
|
+
if (!roles.includes('base') || !roles.includes('current'))
|
|
299
|
+
return `${assessment.obligationId}: changed_since needs a base AND a current entry; got ${roles.join(', ')}`;
|
|
300
|
+
const cited = new Set(assessment.citations
|
|
301
|
+
.filter((citation) => citation.use === 'support')
|
|
302
|
+
.map((citation) => evidenceById.get(citation.evidenceId))
|
|
303
|
+
.filter((evidence) => Boolean(evidence))
|
|
304
|
+
.map((evidence) => context.sourceEntry(evidence.sourceEntryId)?.role));
|
|
305
|
+
if (!cited.has('base') || !cited.has('current'))
|
|
306
|
+
return `${assessment.obligationId}: a resolved historical conclusion cites support from [${[...cited].join(', ') || 'nothing'}]; both base and current are required`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
};
|
|
310
|
+
export const cited_range_within_returned = (result) => {
|
|
311
|
+
const evidenceById = new Map(result.evidence.map((evidence) => [evidence.evidenceId, evidence]));
|
|
312
|
+
for (const assessment of result.assessments) {
|
|
313
|
+
if (!contributes(assessment))
|
|
314
|
+
continue;
|
|
315
|
+
for (const citation of assessment.citations) {
|
|
316
|
+
const evidence = evidenceById.get(citation.evidenceId);
|
|
317
|
+
if (!evidence)
|
|
318
|
+
continue;
|
|
319
|
+
if (evidence.access !== 'truncated') {
|
|
320
|
+
if (citation.citedRange !== null
|
|
321
|
+
&& citation.citedRange !== undefined
|
|
322
|
+
&& !parseRange(citation.citedRange)) {
|
|
323
|
+
return `${citation.evidenceId} has an unparsable citedRange ('${citation.citedRange}')`;
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (citation.use !== 'support'
|
|
328
|
+
&& (citation.citedRange === null || citation.citedRange === undefined))
|
|
329
|
+
continue;
|
|
330
|
+
const returned = parseRange(evidence.rangeReturned);
|
|
331
|
+
if (!returned)
|
|
332
|
+
return `${citation.evidenceId} was truncated but reports no usable rangeReturned ('${evidence.rangeReturned}')`;
|
|
333
|
+
const cited = parseRange(citation.citedRange);
|
|
334
|
+
if (!cited)
|
|
335
|
+
return `${citation.evidenceId} is truncated support with a missing or unparsable citedRange ('${citation.citedRange}')`;
|
|
336
|
+
if (cited[0] < returned[0] || cited[1] > returned[1])
|
|
337
|
+
return `cited ${citation.citedRange} lies outside delivered ${evidence.rangeReturned}`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
};
|
|
342
|
+
const AUTOMATIC_SURFACE_REASONS = new Set([
|
|
343
|
+
'depends_on_unmapped_absence',
|
|
344
|
+
'absent_from_automatic_surface',
|
|
345
|
+
]);
|
|
346
|
+
const automaticSurface = (context) => context.request.surface?.surfaceOrigin === 'automatic' ? context.request.surface : null;
|
|
347
|
+
/** Automatic-surface rule 1: the assessor states the shape of every claim it judged. */
|
|
348
|
+
export const automatic_claim_shape_required = (result, context) => {
|
|
349
|
+
if (!automaticSurface(context))
|
|
350
|
+
return null;
|
|
351
|
+
for (const assessment of result.assessments) {
|
|
352
|
+
if (assessment.claimShape === undefined)
|
|
353
|
+
return `${assessment.obligationId} has no claimShape on an automatic surface`;
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* Automatic-surface rule 2: a negative or universal claim depends on code the map might have
|
|
359
|
+
* missed, so it may only abstain with depends_on_unmapped_absence.
|
|
360
|
+
*/
|
|
361
|
+
export const automatic_claim_shape_abstains = (result, context) => {
|
|
362
|
+
if (!automaticSurface(context))
|
|
363
|
+
return null;
|
|
364
|
+
for (const assessment of result.assessments) {
|
|
365
|
+
if (assessment.claimShape === undefined || assessment.claimShape === 'positive')
|
|
366
|
+
continue;
|
|
367
|
+
if (assessment.outcome !== 'not_established'
|
|
368
|
+
|| assessment.outcomeReason !== 'depends_on_unmapped_absence') {
|
|
369
|
+
return `${assessment.obligationId} is a ${assessment.claimShape} claim on an automatic surface; only not_established / depends_on_unmapped_absence is legal`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
};
|
|
374
|
+
/** Automatic-surface rule 4: the automatic-surface reasons explain an abstention, nothing else. */
|
|
375
|
+
export const automatic_reason_requires_not_established = (result, context) => {
|
|
376
|
+
if (!automaticSurface(context))
|
|
377
|
+
return null;
|
|
378
|
+
for (const assessment of result.assessments) {
|
|
379
|
+
if (AUTOMATIC_SURFACE_REASONS.has(assessment.outcomeReason)
|
|
380
|
+
&& assessment.outcome !== 'not_established') {
|
|
381
|
+
return `${assessment.obligationId} carries ${assessment.outcomeReason} with outcome '${assessment.outcome}'; only not_established may`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Automatic-surface rule 3: supported or contradicted needs at least one support citation of a
|
|
388
|
+
* path inside the surface. A citation outside the map does not count.
|
|
389
|
+
*/
|
|
390
|
+
export const automatic_verdict_cites_surface = (result, context) => {
|
|
391
|
+
const surface = automaticSurface(context);
|
|
392
|
+
if (!surface)
|
|
393
|
+
return null;
|
|
394
|
+
const entries = new Set(surface.entries);
|
|
395
|
+
const evidenceById = new Map(result.evidence.map((evidence) => [evidence.evidenceId, evidence]));
|
|
396
|
+
for (const assessment of result.assessments) {
|
|
397
|
+
if (!contributes(assessment))
|
|
398
|
+
continue;
|
|
399
|
+
const citesSurface = assessment.citations.some((citation) => {
|
|
400
|
+
if (citation.use !== 'support')
|
|
401
|
+
return false;
|
|
402
|
+
const evidence = evidenceById.get(citation.evidenceId);
|
|
403
|
+
if (!evidence || evidence.sourceEntryId !== surface.sourceEntryId)
|
|
404
|
+
return false;
|
|
405
|
+
const path = evidenceRepositoryPath(evidence, context.request.requestedSources);
|
|
406
|
+
return path !== null && entries.has(path);
|
|
407
|
+
});
|
|
408
|
+
if (!citesSurface)
|
|
409
|
+
return `${assessment.obligationId} is ${assessment.outcome} on an automatic surface without a support citation inside the surface`;
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
};
|
|
413
|
+
export const resultCheckNames = [
|
|
414
|
+
'result_binds_request',
|
|
415
|
+
'identifiers_unique',
|
|
416
|
+
'evidence_source_entry_requested',
|
|
417
|
+
'assessment_obligation_issued',
|
|
418
|
+
'assessment_question_matches',
|
|
419
|
+
'assessment_scope_covers',
|
|
420
|
+
'policy4_surface_complete',
|
|
421
|
+
'observed_sources_satisfy_obligation',
|
|
422
|
+
'citation_source_allowed_by_obligation',
|
|
423
|
+
'citation_evidence_delivered',
|
|
424
|
+
'support_citation_usable',
|
|
425
|
+
'changed_since_two_roles',
|
|
426
|
+
'cited_range_within_returned',
|
|
427
|
+
'automatic_claim_shape_required',
|
|
428
|
+
'automatic_claim_shape_abstains',
|
|
429
|
+
'automatic_reason_requires_not_established',
|
|
430
|
+
'automatic_verdict_cites_surface',
|
|
431
|
+
];
|
|
432
|
+
export const resultChecks = {
|
|
433
|
+
result_binds_request,
|
|
434
|
+
identifiers_unique,
|
|
435
|
+
evidence_source_entry_requested,
|
|
436
|
+
assessment_obligation_issued,
|
|
437
|
+
assessment_question_matches,
|
|
438
|
+
assessment_scope_covers,
|
|
439
|
+
policy4_surface_complete,
|
|
440
|
+
observed_sources_satisfy_obligation,
|
|
441
|
+
citation_source_allowed_by_obligation,
|
|
442
|
+
citation_evidence_delivered,
|
|
443
|
+
support_citation_usable,
|
|
444
|
+
changed_since_two_roles,
|
|
445
|
+
cited_range_within_returned,
|
|
446
|
+
automatic_claim_shape_required,
|
|
447
|
+
automatic_claim_shape_abstains,
|
|
448
|
+
automatic_reason_requires_not_established,
|
|
449
|
+
automatic_verdict_cites_surface,
|
|
450
|
+
};
|
|
451
|
+
export function createWireContext(request, result) {
|
|
452
|
+
return {
|
|
453
|
+
request,
|
|
454
|
+
result,
|
|
455
|
+
obligation: (id) => request.obligations.find((obligation) => obligation.obligationId === id),
|
|
456
|
+
sourceEntry: (id) => request.requestedSources.find((source) => source.sourceEntryId === id),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
export function validateReaderResult(request, result) {
|
|
460
|
+
const context = createWireContext(request, result);
|
|
461
|
+
for (const check of resultCheckNames) {
|
|
462
|
+
const message = resultChecks[check](result, context);
|
|
463
|
+
if (message)
|
|
464
|
+
return { ok: false, check, message };
|
|
465
|
+
}
|
|
466
|
+
return { ok: true };
|
|
467
|
+
}
|