agentwrangler 0.1.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 +191 -0
- package/README.md +116 -0
- package/dist/apply/jobs.js +429 -0
- package/dist/apply/open-terminal-child.mjs +98 -0
- package/dist/apply/open-terminal.js +221 -0
- package/dist/apply/settings-gen.js +35 -0
- package/dist/cli/agentwrangler.js +18 -0
- package/dist/daemon/config.js +51 -0
- package/dist/daemon/http.js +258 -0
- package/dist/daemon/index.js +372 -0
- package/dist/daemon/outcomes-pass.js +82 -0
- package/dist/daemon/readiness.js +15 -0
- package/dist/daemon/router.js +756 -0
- package/dist/daemon/static.js +146 -0
- package/dist/db/migrate.js +72 -0
- package/dist/db/migrations/001_observe.sql +196 -0
- package/dist/db/migrations/002_indexes.sql +6 -0
- package/dist/db/migrations/003_context_inventory_history.sql +20 -0
- package/dist/db/migrations/004_apply_jobs.sql +17 -0
- package/dist/db/migrations/005_tool_event_metadata.sql +17 -0
- package/dist/db/migrations/006_d7_query_indexes.sql +9 -0
- package/dist/db/migrations/007_work_item_branch_keys.sql +11 -0
- package/dist/db/migrations/008_thinking_tokens.sql +1 -0
- package/dist/db/migrations/009_user_turn_count.sql +1 -0
- package/dist/db/migrations/010_workspace_cwd.sql +1 -0
- package/dist/db/migrations/011_reports.sql +1 -0
- package/dist/db/migrations/012_reconcile_indexes.sql +2 -0
- package/dist/db/migrations/013_friction_fields.sql +5 -0
- package/dist/db/migrations/014_session_churn.sql +11 -0
- package/dist/db/migrations/015_gap_aggregates.sql +6 -0
- package/dist/db/open.js +30 -0
- package/dist/detector/benchmark-anchors.js +36 -0
- package/dist/detector/calibration.js +302 -0
- package/dist/detector/context-history-retention.js +312 -0
- package/dist/detector/context-probe.js +574 -0
- package/dist/detector/d1-source-identity.js +25 -0
- package/dist/detector/detectors/d10_catalog_footprint.js +146 -0
- package/dist/detector/detectors/d1_ctx_always_loaded.js +203 -0
- package/dist/detector/detectors/d2_session_long_full_context.js +119 -0
- package/dist/detector/detectors/d4_model_mismatch.js +258 -0
- package/dist/detector/detectors/d5_limit_burn_forecast.js +138 -0
- package/dist/detector/detectors/d6_tool_result_bloat.js +301 -0
- package/dist/detector/detectors/d7_loop_retry_waste.js +345 -0
- package/dist/detector/detectors/d8_cache_write_churn.js +201 -0
- package/dist/detector/detectors/d9_idle_background_session.js +101 -0
- package/dist/detector/engine.js +88 -0
- package/dist/detector/index.js +17 -0
- package/dist/detector/measurement.js +426 -0
- package/dist/detector/practice-registry.js +259 -0
- package/dist/detector/registry.js +32 -0
- package/dist/detector/savings.js +249 -0
- package/dist/detector/types.js +14 -0
- package/dist/evidence/common/approved-input.js +632 -0
- package/dist/evidence/common/boundary.js +84 -0
- package/dist/evidence/common/canonical.js +55 -0
- package/dist/evidence/common/redaction.js +321 -0
- package/dist/evidence/common/sqlite.js +25 -0
- package/dist/evidence/common/state.js +29 -0
- package/dist/evidence/cond1/cli.js +289 -0
- package/dist/evidence/cond1/packet.js +407 -0
- package/dist/evidence/cond1/prepare.js +295 -0
- package/dist/evidence/cond1/score.js +349 -0
- package/dist/evidence/cond1/types.js +1 -0
- package/dist/evidence/create-approval.js +365 -0
- package/dist/evidence/create-scratch.js +542 -0
- package/dist/evidence/d7/cli.js +113 -0
- package/dist/evidence/d7/measure.js +193 -0
- package/dist/evidence/d7/types.js +1 -0
- package/dist/evidence/discover-approval.js +492 -0
- package/dist/evidence/g2/adjudicate.js +20 -0
- package/dist/evidence/g2/cli.js +207 -0
- package/dist/evidence/g2/kappa.js +39 -0
- package/dist/evidence/g2/pipeline.js +92 -0
- package/dist/evidence/g2/store.js +14 -0
- package/dist/evidence/github/client.js +1 -0
- package/dist/evidence/github/gh-cli-client.js +301 -0
- package/dist/evidence/r3/cli.js +209 -0
- package/dist/evidence/r3/evaluate.js +417 -0
- package/dist/evidence/r3/packet.js +162 -0
- package/dist/evidence/r3/prepare.js +405 -0
- package/dist/evidence/r3/score.js +341 -0
- package/dist/evidence/r3/transcript.js +155 -0
- package/dist/evidence/r3/types.js +4 -0
- package/dist/hook/context-budget-hook.mjs +138 -0
- package/dist/hook/danger-guard-denylist.json +27 -0
- package/dist/hook/danger-guard-hook.mjs +167 -0
- package/dist/hook/install.js +0 -0
- package/dist/hook/limit-burn-hook.mjs +127 -0
- package/dist/hook/loop-guard-hook.mjs +104 -0
- package/dist/hook/precompact-checkpoint-hook.mjs +123 -0
- package/dist/ingest/churn-collector.js +122 -0
- package/dist/ingest/detector-hook.js +52 -0
- package/dist/ingest/discovery.js +207 -0
- package/dist/ingest/health.js +43 -0
- package/dist/ingest/index.js +28 -0
- package/dist/ingest/ingestor.js +509 -0
- package/dist/ingest/parser.js +344 -0
- package/dist/ingest/pricing.js +153 -0
- package/dist/ingest/reconcile.js +52 -0
- package/dist/ingest/tail.js +152 -0
- package/dist/ingest/types.js +24 -0
- package/dist/ingest/workspace-mapping.js +114 -0
- package/dist/oauth/anthropic-api-key.js +88 -0
- package/dist/oauth/count-tokens.js +86 -0
- package/dist/oauth/credentials.js +171 -0
- package/dist/oauth/judge-g2-client.js +154 -0
- package/dist/oauth/usage.js +167 -0
- package/dist/outcomes/branch-key.js +49 -0
- package/dist/outcomes/conclusions.js +45 -0
- package/dist/outcomes/derive.js +94 -0
- package/dist/outcomes/finding-extractors.js +131 -0
- package/dist/outcomes/findings.js +237 -0
- package/dist/outcomes/github/client.js +367 -0
- package/dist/outcomes/github/credential.js +195 -0
- package/dist/outcomes/github/gh-cli-client.js +340 -0
- package/dist/outcomes/linker.js +486 -0
- package/dist/outcomes/pool.js +24 -0
- package/dist/outcomes/sync.js +276 -0
- package/dist/query/api/agents-liveness.js +182 -0
- package/dist/query/api/burn-status.js +50 -0
- package/dist/query/api/context-budget.js +114 -0
- package/dist/query/api/context-composition.js +67 -0
- package/dist/query/api/cost-per-success.js +104 -0
- package/dist/query/api/delivery.js +92 -0
- package/dist/query/api/effectiveness.js +254 -0
- package/dist/query/api/efficiency-headroom.js +74 -0
- package/dist/query/api/headroom-trend.js +105 -0
- package/dist/query/api/hook-config.js +75 -0
- package/dist/query/api/hook-install.js +8 -0
- package/dist/query/api/hot-sessions.js +17 -0
- package/dist/query/api/idle-sessions.js +52 -0
- package/dist/query/api/index.js +40 -0
- package/dist/query/api/loop-guard.js +90 -0
- package/dist/query/api/offload-share.js +41 -0
- package/dist/query/api/outcomes.js +218 -0
- package/dist/query/api/overview.js +535 -0
- package/dist/query/api/rec-prompt.js +138 -0
- package/dist/query/api/recommendations-ledger.js +111 -0
- package/dist/query/api/recommendations.js +514 -0
- package/dist/query/api/reports.js +78 -0
- package/dist/query/api/self-churn.js +77 -0
- package/dist/query/api/self-percentiles.js +109 -0
- package/dist/query/api/session-drivers.js +153 -0
- package/dist/query/api/settings.js +85 -0
- package/dist/query/api/spend-flavor.js +234 -0
- package/dist/query/api/trends.js +155 -0
- package/dist/query/cap-weighted.js +119 -0
- package/dist/query/db-context.js +42 -0
- package/dist/query/envelope.js +71 -0
- package/dist/query/forecast.js +191 -0
- package/dist/query/settings-store.js +441 -0
- package/dist/query/spend.js +171 -0
- package/dist/query/trends.js +194 -0
- package/dist/ui/assets/index-DnRKgc21.css +1 -0
- package/dist/ui/assets/index-h1Q1wWq5.js +168 -0
- package/dist/ui/index.html +39 -0
- package/package.json +59 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { EXTRACTOR_VERSIONS, extractDeferralFindings, projectDiffMarkerCandidates, projectReviewThreadFindings, } from "../../outcomes/finding-extractors.js";
|
|
4
|
+
import { loadApprovedEvidenceInput, publishApprovedOutput } from "../common/approved-input.js";
|
|
5
|
+
import { EvidenceBoundaryError } from "../common/boundary.js";
|
|
6
|
+
import { canonicalJson, sha256Canonical } from "../common/canonical.js";
|
|
7
|
+
import { redactEvidenceExcerpt } from "../common/redaction.js";
|
|
8
|
+
import { COND1_RUNNER_VERSION, canonicalAnswerKeySha256 } from "./score.js";
|
|
9
|
+
function fail(code) {
|
|
10
|
+
throw new Error(code);
|
|
11
|
+
}
|
|
12
|
+
function binaryCompare(left, right) {
|
|
13
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
14
|
+
}
|
|
15
|
+
function validateIdentity(identity) {
|
|
16
|
+
const asOf = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,9})?Z$/u.exec(identity.asOf);
|
|
17
|
+
const asOfDate = asOf === null ? Number.NaN : Date.parse(`${asOf[1]}Z`);
|
|
18
|
+
if (Object.keys(identity).sort().join("|") !==
|
|
19
|
+
[
|
|
20
|
+
"sourceCommit",
|
|
21
|
+
"runnerVersion",
|
|
22
|
+
"findingsModuleSha256",
|
|
23
|
+
"extractorVersions",
|
|
24
|
+
"packetVersion",
|
|
25
|
+
"scorerVersion",
|
|
26
|
+
"asOf",
|
|
27
|
+
]
|
|
28
|
+
.sort()
|
|
29
|
+
.join("|") ||
|
|
30
|
+
Object.keys(identity.extractorVersions).sort().join("|") !== "E1|E2|E3" ||
|
|
31
|
+
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(identity.sourceCommit) ||
|
|
32
|
+
identity.runnerVersion !== COND1_RUNNER_VERSION ||
|
|
33
|
+
!/^[0-9a-f]{64}$/u.test(identity.findingsModuleSha256) ||
|
|
34
|
+
identity.packetVersion !== "cond1-blinded-v1" ||
|
|
35
|
+
identity.scorerVersion !== "cond1-precision-v1" ||
|
|
36
|
+
identity.extractorVersions.E1 !== EXTRACTOR_VERSIONS.E1 ||
|
|
37
|
+
identity.extractorVersions.E2 !== EXTRACTOR_VERSIONS.E2 ||
|
|
38
|
+
identity.extractorVersions.E3 !== EXTRACTOR_VERSIONS.E3 ||
|
|
39
|
+
!Number.isFinite(asOfDate) ||
|
|
40
|
+
new Date(asOfDate).toISOString().slice(0, 19) !== asOf?.[1]) {
|
|
41
|
+
fail("cond1_identity_invalid");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function privatePrKey(pr) {
|
|
45
|
+
return `${pr.reportAlias}#${pr.number}`;
|
|
46
|
+
}
|
|
47
|
+
function utcKey(value) {
|
|
48
|
+
const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?Z$/u.exec(value);
|
|
49
|
+
if (match?.[1] === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
const milliseconds = Date.parse(`${match[1]}Z`);
|
|
52
|
+
if (!Number.isFinite(milliseconds) ||
|
|
53
|
+
new Date(milliseconds).toISOString().slice(0, 19) !== match[1]) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return `${match[1]}.${(match[2] ?? "").padEnd(9, "0")}Z`;
|
|
57
|
+
}
|
|
58
|
+
function normalizeInventory(inventory, repositoryAliases, asOf) {
|
|
59
|
+
const asOfKey = utcKey(asOf) ?? fail("cond1_as_of_invalid");
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const rows = inventory.map((row) => {
|
|
62
|
+
const mergedAtKey = utcKey(row.mergedAt);
|
|
63
|
+
if (!repositoryAliases.has(row.reportAlias) ||
|
|
64
|
+
!Number.isSafeInteger(row.number) ||
|
|
65
|
+
row.number <= 0 ||
|
|
66
|
+
mergedAtKey === null ||
|
|
67
|
+
mergedAtKey > asOfKey) {
|
|
68
|
+
fail("cond1_inventory_row_invalid");
|
|
69
|
+
}
|
|
70
|
+
const key = privatePrKey(row);
|
|
71
|
+
if (seen.has(key))
|
|
72
|
+
fail("cond1_inventory_duplicate");
|
|
73
|
+
seen.add(key);
|
|
74
|
+
return { reportAlias: row.reportAlias, number: row.number, mergedAt: row.mergedAt };
|
|
75
|
+
});
|
|
76
|
+
return rows.sort((left, right) => binaryCompare(left.reportAlias, right.reportAlias) || left.number - right.number);
|
|
77
|
+
}
|
|
78
|
+
async function safeRead(read) {
|
|
79
|
+
try {
|
|
80
|
+
const result = await read();
|
|
81
|
+
return result.ok ? result.data : undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function acquireCorpus(github, repositories, prs) {
|
|
88
|
+
const byAlias = new Map(repositories.map((repo) => [repo.reportAlias, repo]));
|
|
89
|
+
const raw = [];
|
|
90
|
+
const succeeded = { E1: 0, E2: 0, E3: 0 };
|
|
91
|
+
let fullyReadPrN = 0;
|
|
92
|
+
for (const pr of prs) {
|
|
93
|
+
const repo = byAlias.get(pr.reportAlias);
|
|
94
|
+
if (repo === undefined)
|
|
95
|
+
fail("cond1_inventory_repository_unknown");
|
|
96
|
+
const [threads, body, diff] = await Promise.all([
|
|
97
|
+
safeRead(() => github.getReviewThreads(repo.owner, repo.repo, pr.number)),
|
|
98
|
+
safeRead(() => github.getPRBody(repo.owner, repo.repo, pr.number)),
|
|
99
|
+
safeRead(() => github.getPRDiff(repo.owner, repo.repo, pr.number)),
|
|
100
|
+
]);
|
|
101
|
+
if (threads !== undefined)
|
|
102
|
+
succeeded.E1++;
|
|
103
|
+
if (body !== undefined)
|
|
104
|
+
succeeded.E2++;
|
|
105
|
+
if (diff !== undefined)
|
|
106
|
+
succeeded.E3++;
|
|
107
|
+
if (threads !== undefined && body !== undefined && diff !== undefined)
|
|
108
|
+
fullyReadPrN++;
|
|
109
|
+
raw.push({
|
|
110
|
+
pr,
|
|
111
|
+
...(threads === undefined ? {} : { threads }),
|
|
112
|
+
...(body === undefined ? {} : { body: body ?? "" }),
|
|
113
|
+
...(diff === undefined ? {} : { diff }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const requiredN = prs.length;
|
|
117
|
+
return {
|
|
118
|
+
raw,
|
|
119
|
+
readCompletion: {
|
|
120
|
+
E1: { requiredN, succeededN: succeeded.E1, failedN: requiredN - succeeded.E1 },
|
|
121
|
+
E2: { requiredN, succeededN: succeeded.E2, failedN: requiredN - succeeded.E2 },
|
|
122
|
+
E3: { requiredN, succeededN: succeeded.E3, failedN: requiredN - succeeded.E3 },
|
|
123
|
+
},
|
|
124
|
+
fullyReadPrN,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function extractCorpus(raw) {
|
|
128
|
+
const findings = [];
|
|
129
|
+
for (const { pr, threads, body, diff } of raw) {
|
|
130
|
+
const corpusPrKey = privatePrKey(pr);
|
|
131
|
+
for (const projected of projectReviewThreadFindings(corpusPrKey, threads ?? [])) {
|
|
132
|
+
findings.push({
|
|
133
|
+
extractor: "E1",
|
|
134
|
+
extractorVersion: EXTRACTOR_VERSIONS.E1,
|
|
135
|
+
sourceFindingId: projected.sourceFindingId,
|
|
136
|
+
corpusPrKey,
|
|
137
|
+
repoAlias: pr.reportAlias,
|
|
138
|
+
evidenceKind: "REVIEW_THREAD_STATE",
|
|
139
|
+
evidence: projected.evidence,
|
|
140
|
+
evidenceSufficient: true,
|
|
141
|
+
projectionFailure: null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
for (const projected of extractDeferralFindings(body ?? "", corpusPrKey)) {
|
|
145
|
+
const excerpt = redactEvidenceExcerpt(projected.evidenceText);
|
|
146
|
+
findings.push({
|
|
147
|
+
extractor: "E2",
|
|
148
|
+
extractorVersion: EXTRACTOR_VERSIONS.E2,
|
|
149
|
+
sourceFindingId: projected.sourceFindingId,
|
|
150
|
+
corpusPrKey,
|
|
151
|
+
repoAlias: pr.reportAlias,
|
|
152
|
+
evidenceKind: "DEFERRAL_LIST_ITEM",
|
|
153
|
+
evidence: excerpt === null ? {} : { boundedExcerpt: excerpt },
|
|
154
|
+
evidenceSufficient: excerpt !== null,
|
|
155
|
+
projectionFailure: excerpt === null ? "REDACTION_FAILED" : null,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
for (const [index, projected] of projectDiffMarkerCandidates(diff ?? "").entries()) {
|
|
159
|
+
const excerpt = redactEvidenceExcerpt(projected.evidenceText);
|
|
160
|
+
findings.push({
|
|
161
|
+
extractor: "E3",
|
|
162
|
+
extractorVersion: EXTRACTOR_VERSIONS.E3,
|
|
163
|
+
sourceFindingId: `e3:${corpusPrKey}:${index}`,
|
|
164
|
+
corpusPrKey,
|
|
165
|
+
repoAlias: pr.reportAlias,
|
|
166
|
+
evidenceKind: "ADDED_DIFF_MARKER",
|
|
167
|
+
evidence: {
|
|
168
|
+
...(excerpt === null ? {} : { boundedExcerpt: excerpt }),
|
|
169
|
+
locationAlias: `loc-${sha256Canonical([projected.filePath, projected.lineNumber]).slice(0, 20)}`,
|
|
170
|
+
},
|
|
171
|
+
evidenceSufficient: excerpt !== null,
|
|
172
|
+
projectionFailure: excerpt === null ? "REDACTION_FAILED" : null,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return findings.sort((a, b) => binaryCompare(a.extractor, b.extractor) ||
|
|
177
|
+
binaryCompare(a.corpusPrKey, b.corpusPrKey) ||
|
|
178
|
+
binaryCompare(a.sourceFindingId, b.sourceFindingId));
|
|
179
|
+
}
|
|
180
|
+
function answerMaterial(findings) {
|
|
181
|
+
return findings.map((finding) => ({
|
|
182
|
+
findingAlias: "",
|
|
183
|
+
extractor: finding.extractor,
|
|
184
|
+
extractorVersion: finding.extractorVersion,
|
|
185
|
+
sourceFindingId: finding.sourceFindingId,
|
|
186
|
+
corpusPrKey: finding.corpusPrKey,
|
|
187
|
+
evidenceSufficient: finding.evidenceSufficient,
|
|
188
|
+
projectionFailure: finding.projectionFailure,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
function preflightOutputs(loaded, outputs) {
|
|
192
|
+
const state = path.resolve(loaded.scratchStatePath);
|
|
193
|
+
const resolved = outputs.map((output) => path.resolve(output));
|
|
194
|
+
if (new Set(resolved).size !== resolved.length)
|
|
195
|
+
fail("cond1_output_paths_duplicate");
|
|
196
|
+
for (const output of resolved) {
|
|
197
|
+
if (path.dirname(output) !== state)
|
|
198
|
+
fail("cond1_output_not_direct_state_child");
|
|
199
|
+
try {
|
|
200
|
+
fs.lstatSync(output);
|
|
201
|
+
fail("cond1_output_already_exists");
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error instanceof Error && error.message === "cond1_output_already_exists")
|
|
205
|
+
throw error;
|
|
206
|
+
if (error.code !== "ENOENT")
|
|
207
|
+
fail("cond1_output_preflight_failed");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export async function prepareCond1(input) {
|
|
212
|
+
validateIdentity(input.identity);
|
|
213
|
+
if (!input.github.enabled)
|
|
214
|
+
fail("cond1_github_disabled");
|
|
215
|
+
const loaded = await loadApprovedEvidenceInput(input.approvedInput);
|
|
216
|
+
preflightOutputs(loaded, [input.preparedOutPath, input.manifestOutPath]);
|
|
217
|
+
const scratch = loaded.openVerifiedScratchDb();
|
|
218
|
+
try {
|
|
219
|
+
scratch.prepare("SELECT 1").get();
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
scratch.close();
|
|
223
|
+
}
|
|
224
|
+
const repositories = loaded.repositories.map(({ owner, repo, reportAlias }) => ({
|
|
225
|
+
owner,
|
|
226
|
+
repo,
|
|
227
|
+
reportAlias,
|
|
228
|
+
}));
|
|
229
|
+
const inventory = await input.github.listMergedPRs(repositories, input.identity.asOf);
|
|
230
|
+
if (!inventory.ok)
|
|
231
|
+
fail("cond1_inventory_failed");
|
|
232
|
+
const orderedInventory = normalizeInventory(inventory.data, new Set(repositories.map(({ reportAlias }) => reportAlias)), input.identity.asOf);
|
|
233
|
+
// A second pure extraction pass detects nondeterministic projection before any artifact write.
|
|
234
|
+
const acquired = await acquireCorpus(input.github, repositories, orderedInventory);
|
|
235
|
+
const first = extractCorpus(acquired.raw);
|
|
236
|
+
const second = extractCorpus(acquired.raw);
|
|
237
|
+
if (sha256Canonical(first) !== sha256Canonical(second))
|
|
238
|
+
fail("cond1_extraction_nondeterministic");
|
|
239
|
+
const answers = answerMaterial(first);
|
|
240
|
+
const answerCanonicalSha256 = canonicalAnswerKeySha256(answers);
|
|
241
|
+
const campaignId = `cond1-${sha256Canonical({
|
|
242
|
+
identity: input.identity,
|
|
243
|
+
scratchDbSha256: loaded.scratchDbSha256,
|
|
244
|
+
repoMapSha256: loaded.repoMapSha256,
|
|
245
|
+
inventory: orderedInventory,
|
|
246
|
+
}).slice(0, 32)}`;
|
|
247
|
+
const prepared = {
|
|
248
|
+
version: "cond1-prepared-v1",
|
|
249
|
+
campaignId,
|
|
250
|
+
identity: input.identity,
|
|
251
|
+
scratchDbSha256: loaded.scratchDbSha256,
|
|
252
|
+
repoMapSha256: loaded.repoMapSha256,
|
|
253
|
+
eligiblePrN: orderedInventory.length,
|
|
254
|
+
findings: first,
|
|
255
|
+
};
|
|
256
|
+
const publish = input.publishOutput ?? publishApprovedOutput;
|
|
257
|
+
const preparedPublication = publish(loaded, input.preparedOutPath, `${canonicalJson(prepared)}\n`);
|
|
258
|
+
const counts = {
|
|
259
|
+
E1: first.filter((finding) => finding.extractor === "E1").length,
|
|
260
|
+
E2: first.filter((finding) => finding.extractor === "E2").length,
|
|
261
|
+
E3: first.filter((finding) => finding.extractor === "E3").length,
|
|
262
|
+
};
|
|
263
|
+
const manifest = {
|
|
264
|
+
version: "cond1-corpus-manifest-v1",
|
|
265
|
+
campaignId,
|
|
266
|
+
identity: input.identity,
|
|
267
|
+
scratchDbSha256: loaded.scratchDbSha256,
|
|
268
|
+
repoMapSha256: loaded.repoMapSha256,
|
|
269
|
+
eligiblePrN: orderedInventory.length,
|
|
270
|
+
readCompletion: acquired.readCompletion,
|
|
271
|
+
corpusReadSummary: {
|
|
272
|
+
fullyReadPrN: acquired.fullyReadPrN,
|
|
273
|
+
failedPrN: orderedInventory.length - acquired.fullyReadPrN,
|
|
274
|
+
},
|
|
275
|
+
emittedFindingN: counts,
|
|
276
|
+
preparedArtifactSha256: preparedPublication.sha256,
|
|
277
|
+
answerCanonicalSha256,
|
|
278
|
+
};
|
|
279
|
+
let manifestPublication;
|
|
280
|
+
try {
|
|
281
|
+
manifestPublication = publish(loaded, input.manifestOutPath, `${canonicalJson(manifest)}\n`);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
const code = error instanceof EvidenceBoundaryError ? error.code : "unexpected_failure";
|
|
285
|
+
fail(`cond1_manifest_publication_failed_prepared_output_retained_${code}`);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
campaignId,
|
|
289
|
+
eligiblePrN: orderedInventory.length,
|
|
290
|
+
emittedFindingN: counts,
|
|
291
|
+
preparedArtifactSha256: preparedPublication.sha256,
|
|
292
|
+
manifestSha256: manifestPublication.sha256,
|
|
293
|
+
answerCanonicalSha256,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { EXTRACTOR_VERSIONS } from "../../outcomes/finding-extractors.js";
|
|
3
|
+
const EXTRACTORS = ["E1", "E2", "E3"];
|
|
4
|
+
const VERDICTS = new Set(["TRUE_POSITIVE", "FALSE_POSITIVE", "UNCERTAIN"]);
|
|
5
|
+
const REASON_CODES = new Set([
|
|
6
|
+
"EVIDENCE_SUPPORTS",
|
|
7
|
+
"CONTEXT_NEGATES",
|
|
8
|
+
"NOT_A_DEFERRAL",
|
|
9
|
+
"NOT_AN_ADDED_MARKER",
|
|
10
|
+
"WRONG_THREAD_STATE",
|
|
11
|
+
"INSUFFICIENT_EVIDENCE",
|
|
12
|
+
]);
|
|
13
|
+
const MIN_INFORMATIVE_FINDINGS = 20;
|
|
14
|
+
const PRECISION_THRESHOLD = 0.8;
|
|
15
|
+
export const COND1_RUNNER_VERSION = "cond1-runner-v1";
|
|
16
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
17
|
+
const GIT_COMMIT_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
18
|
+
const UTC_RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/;
|
|
19
|
+
const IDENTITY_KEYS = [
|
|
20
|
+
"sourceCommit",
|
|
21
|
+
"runnerVersion",
|
|
22
|
+
"findingsModuleSha256",
|
|
23
|
+
"extractorVersions",
|
|
24
|
+
"packetVersion",
|
|
25
|
+
"scorerVersion",
|
|
26
|
+
"asOf",
|
|
27
|
+
];
|
|
28
|
+
function canonicalJson(value) {
|
|
29
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "number") {
|
|
33
|
+
if (!Number.isFinite(value))
|
|
34
|
+
throw new Error("canonical JSON rejects non-finite numbers");
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
39
|
+
if (typeof value === "object") {
|
|
40
|
+
const record = value;
|
|
41
|
+
return `{${Object.keys(record)
|
|
42
|
+
.sort()
|
|
43
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
44
|
+
.join(",")}}`;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`canonical JSON rejects ${typeof value}`);
|
|
47
|
+
}
|
|
48
|
+
export function canonicalSha256(value) {
|
|
49
|
+
return createHash("sha256").update(canonicalJson(value), "utf8").digest("hex");
|
|
50
|
+
}
|
|
51
|
+
function compareText(left, right) {
|
|
52
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
53
|
+
}
|
|
54
|
+
export function canonicalAnswerKeySha256(answers) {
|
|
55
|
+
const stableIdentities = answers
|
|
56
|
+
.map(({ extractor, extractorVersion, sourceFindingId, corpusPrKey, evidenceSufficient, projectionFailure, }) => ({
|
|
57
|
+
extractor,
|
|
58
|
+
extractorVersion,
|
|
59
|
+
sourceFindingId,
|
|
60
|
+
corpusPrKey,
|
|
61
|
+
evidenceSufficient,
|
|
62
|
+
projectionFailure,
|
|
63
|
+
}))
|
|
64
|
+
.sort((left, right) => {
|
|
65
|
+
return (compareText(left.extractor, right.extractor) ||
|
|
66
|
+
compareText(left.corpusPrKey, right.corpusPrKey) ||
|
|
67
|
+
compareText(left.sourceFindingId, right.sourceFindingId) ||
|
|
68
|
+
compareText(left.extractorVersion, right.extractorVersion));
|
|
69
|
+
});
|
|
70
|
+
return canonicalSha256(stableIdentities);
|
|
71
|
+
}
|
|
72
|
+
function hasExactKeys(value, keys) {
|
|
73
|
+
const actual = Object.keys(value).sort();
|
|
74
|
+
const expected = [...keys].sort();
|
|
75
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
76
|
+
}
|
|
77
|
+
function isStrictUtcRfc3339(value) {
|
|
78
|
+
const match = UTC_RFC3339_RE.exec(value);
|
|
79
|
+
if (match === null)
|
|
80
|
+
return false;
|
|
81
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match;
|
|
82
|
+
const parsed = new Date(value);
|
|
83
|
+
if (Number.isNaN(parsed.getTime()))
|
|
84
|
+
return false;
|
|
85
|
+
return (parsed.getUTCFullYear() === Number(yearText) &&
|
|
86
|
+
parsed.getUTCMonth() + 1 === Number(monthText) &&
|
|
87
|
+
parsed.getUTCDate() === Number(dayText) &&
|
|
88
|
+
parsed.getUTCHours() === Number(hourText) &&
|
|
89
|
+
parsed.getUTCMinutes() === Number(minuteText) &&
|
|
90
|
+
parsed.getUTCSeconds() === Number(secondText));
|
|
91
|
+
}
|
|
92
|
+
function frozenIdentity(identity) {
|
|
93
|
+
return {
|
|
94
|
+
sourceCommit: identity.sourceCommit,
|
|
95
|
+
runnerVersion: COND1_RUNNER_VERSION,
|
|
96
|
+
findingsModuleSha256: identity.findingsModuleSha256,
|
|
97
|
+
extractorVersions: {
|
|
98
|
+
E1: identity.extractorVersions.E1,
|
|
99
|
+
E2: identity.extractorVersions.E2,
|
|
100
|
+
E3: identity.extractorVersions.E3,
|
|
101
|
+
},
|
|
102
|
+
packetVersion: identity.packetVersion,
|
|
103
|
+
scorerVersion: identity.scorerVersion,
|
|
104
|
+
asOf: identity.asOf,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function isNonNegativeInteger(value) {
|
|
108
|
+
return Number.isInteger(value) && value >= 0;
|
|
109
|
+
}
|
|
110
|
+
function isVerdict(value) {
|
|
111
|
+
return typeof value === "string" && VERDICTS.has(value);
|
|
112
|
+
}
|
|
113
|
+
function isValidVerdictRow(value) {
|
|
114
|
+
return (typeof value.findingAlias === "string" &&
|
|
115
|
+
value.findingAlias.length > 0 &&
|
|
116
|
+
isVerdict(value.verdict) &&
|
|
117
|
+
typeof value.adjudicatorAlias === "string" &&
|
|
118
|
+
value.adjudicatorAlias.length > 0 &&
|
|
119
|
+
typeof value.reasonCode === "string" &&
|
|
120
|
+
REASON_CODES.has(value.reasonCode));
|
|
121
|
+
}
|
|
122
|
+
function scoreStatus(precision, limitation) {
|
|
123
|
+
if (limitation !== "NONE")
|
|
124
|
+
return "DATA_INSUFFICIENT";
|
|
125
|
+
return precision !== null && precision >= PRECISION_THRESHOLD ? "PASS" : "FAIL";
|
|
126
|
+
}
|
|
127
|
+
function overallStatus(scores) {
|
|
128
|
+
const statuses = EXTRACTORS.map((extractor) => scores[extractor].status);
|
|
129
|
+
if (statuses.includes("DATA_INSUFFICIENT"))
|
|
130
|
+
return "DATA_INSUFFICIENT";
|
|
131
|
+
if (statuses.includes("FAIL"))
|
|
132
|
+
return "FAIL";
|
|
133
|
+
return "PASS";
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Score a complete COND-1 answer key and verdict set. The function has no I/O
|
|
137
|
+
* and returns only the approved durable aggregate shape.
|
|
138
|
+
*/
|
|
139
|
+
export function scoreCond1(input) {
|
|
140
|
+
const { manifest, answers, verdicts } = input;
|
|
141
|
+
const { corpusReadSummary } = manifest;
|
|
142
|
+
const durableIdentity = frozenIdentity(manifest.identity);
|
|
143
|
+
const answerKeyCanonicalSha256 = canonicalAnswerKeySha256(answers);
|
|
144
|
+
let globalIdentityMismatch = !SHA256_RE.test(input.corpusManifestFileSha256) ||
|
|
145
|
+
manifest.version !== "cond1-corpus-manifest-v1" ||
|
|
146
|
+
answerKeyCanonicalSha256 !== manifest.answerCanonicalSha256 ||
|
|
147
|
+
manifest.identity.packetVersion !== "cond1-blinded-v1" ||
|
|
148
|
+
manifest.identity.scorerVersion !== "cond1-precision-v1" ||
|
|
149
|
+
manifest.campaignId.length === 0 ||
|
|
150
|
+
manifest.identity.runnerVersion !== COND1_RUNNER_VERSION ||
|
|
151
|
+
!GIT_COMMIT_RE.test(manifest.identity.sourceCommit) ||
|
|
152
|
+
!SHA256_RE.test(manifest.identity.findingsModuleSha256) ||
|
|
153
|
+
!SHA256_RE.test(manifest.scratchDbSha256) ||
|
|
154
|
+
!SHA256_RE.test(manifest.repoMapSha256) ||
|
|
155
|
+
!SHA256_RE.test(manifest.preparedArtifactSha256) ||
|
|
156
|
+
!SHA256_RE.test(manifest.answerCanonicalSha256) ||
|
|
157
|
+
!isStrictUtcRfc3339(manifest.identity.asOf) ||
|
|
158
|
+
!hasExactKeys(manifest.identity, IDENTITY_KEYS) ||
|
|
159
|
+
!hasExactKeys(manifest.identity.extractorVersions, EXTRACTORS) ||
|
|
160
|
+
EXTRACTORS.some((extractor) => manifest.identity.extractorVersions[extractor] !== EXTRACTOR_VERSIONS[extractor]);
|
|
161
|
+
const countsAreValid = isNonNegativeInteger(manifest.eligiblePrN) &&
|
|
162
|
+
isNonNegativeInteger(corpusReadSummary.fullyReadPrN) &&
|
|
163
|
+
isNonNegativeInteger(corpusReadSummary.failedPrN) &&
|
|
164
|
+
corpusReadSummary.fullyReadPrN + corpusReadSummary.failedPrN === manifest.eligiblePrN &&
|
|
165
|
+
corpusReadSummary.failedPrN <=
|
|
166
|
+
EXTRACTORS.reduce((sum, extractor) => sum + manifest.readCompletion[extractor].failedN, 0);
|
|
167
|
+
if (!countsAreValid)
|
|
168
|
+
globalIdentityMismatch = true;
|
|
169
|
+
const answersByAlias = new Map();
|
|
170
|
+
const answerAliasesByExtractor = {
|
|
171
|
+
E1: new Set(),
|
|
172
|
+
E2: new Set(),
|
|
173
|
+
E3: new Set(),
|
|
174
|
+
};
|
|
175
|
+
const duplicateAnswerExtractors = new Set();
|
|
176
|
+
const sourceIdentities = new Set();
|
|
177
|
+
for (const answer of answers) {
|
|
178
|
+
if (!EXTRACTORS.includes(answer.extractor) ||
|
|
179
|
+
answer.extractorVersion !== EXTRACTOR_VERSIONS[answer.extractor] ||
|
|
180
|
+
answer.findingAlias.length === 0 ||
|
|
181
|
+
answer.sourceFindingId.length === 0 ||
|
|
182
|
+
answer.corpusPrKey.length === 0 ||
|
|
183
|
+
typeof answer.evidenceSufficient !== "boolean" ||
|
|
184
|
+
!(answer.projectionFailure === null || answer.projectionFailure === "REDACTION_FAILED") ||
|
|
185
|
+
answer.evidenceSufficient === (answer.projectionFailure !== null)) {
|
|
186
|
+
globalIdentityMismatch = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (answersByAlias.has(answer.findingAlias)) {
|
|
190
|
+
duplicateAnswerExtractors.add(answer.extractor);
|
|
191
|
+
globalIdentityMismatch = true;
|
|
192
|
+
}
|
|
193
|
+
const sourceIdentity = canonicalSha256([
|
|
194
|
+
answer.extractor,
|
|
195
|
+
answer.corpusPrKey,
|
|
196
|
+
answer.sourceFindingId,
|
|
197
|
+
]);
|
|
198
|
+
if (sourceIdentities.has(sourceIdentity))
|
|
199
|
+
globalIdentityMismatch = true;
|
|
200
|
+
sourceIdentities.add(sourceIdentity);
|
|
201
|
+
answersByAlias.set(answer.findingAlias, answer);
|
|
202
|
+
answerAliasesByExtractor[answer.extractor].add(answer.findingAlias);
|
|
203
|
+
}
|
|
204
|
+
const verdictsByAlias = new Map();
|
|
205
|
+
const duplicateVerdictExtractors = new Set();
|
|
206
|
+
const adjudicatorAliases = new Set();
|
|
207
|
+
let invalidAdjudicatorAlias = false;
|
|
208
|
+
let unknownVerdictAlias = false;
|
|
209
|
+
for (const verdict of verdicts) {
|
|
210
|
+
if (typeof verdict.adjudicatorAlias !== "string" || verdict.adjudicatorAlias.length === 0) {
|
|
211
|
+
invalidAdjudicatorAlias = true;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
adjudicatorAliases.add(verdict.adjudicatorAlias);
|
|
215
|
+
}
|
|
216
|
+
const answer = answersByAlias.get(verdict.findingAlias);
|
|
217
|
+
if (answer === undefined) {
|
|
218
|
+
unknownVerdictAlias = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (verdictsByAlias.has(verdict.findingAlias))
|
|
222
|
+
duplicateVerdictExtractors.add(answer.extractor);
|
|
223
|
+
verdictsByAlias.set(verdict.findingAlias, verdict);
|
|
224
|
+
}
|
|
225
|
+
if (unknownVerdictAlias)
|
|
226
|
+
globalIdentityMismatch = true;
|
|
227
|
+
if (invalidAdjudicatorAlias || adjudicatorAliases.size !== 1)
|
|
228
|
+
globalIdentityMismatch = true;
|
|
229
|
+
const extractorScores = {};
|
|
230
|
+
for (const extractor of EXTRACTORS) {
|
|
231
|
+
const read = manifest.readCompletion[extractor];
|
|
232
|
+
const emittedN = manifest.emittedFindingN[extractor];
|
|
233
|
+
let extractorIdentityMismatch = globalIdentityMismatch ||
|
|
234
|
+
duplicateAnswerExtractors.has(extractor) ||
|
|
235
|
+
duplicateVerdictExtractors.has(extractor) ||
|
|
236
|
+
!isNonNegativeInteger(emittedN) ||
|
|
237
|
+
!isNonNegativeInteger(read.requiredN) ||
|
|
238
|
+
!isNonNegativeInteger(read.succeededN) ||
|
|
239
|
+
!isNonNegativeInteger(read.failedN) ||
|
|
240
|
+
read.requiredN !== manifest.eligiblePrN ||
|
|
241
|
+
read.succeededN + read.failedN !== read.requiredN ||
|
|
242
|
+
answerAliasesByExtractor[extractor].size !== emittedN;
|
|
243
|
+
if (countsAreValid &&
|
|
244
|
+
(read.succeededN < corpusReadSummary.fullyReadPrN ||
|
|
245
|
+
read.failedN > corpusReadSummary.failedPrN)) {
|
|
246
|
+
extractorIdentityMismatch = true;
|
|
247
|
+
}
|
|
248
|
+
const counts = { truePositiveN: 0, falsePositiveN: 0, uncertainN: 0 };
|
|
249
|
+
let adjudicatedN = 0;
|
|
250
|
+
let invalidVerdict = false;
|
|
251
|
+
let unsupportedProjectionVerdict = false;
|
|
252
|
+
for (const alias of answerAliasesByExtractor[extractor]) {
|
|
253
|
+
const verdict = verdictsByAlias.get(alias);
|
|
254
|
+
if (verdict === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
if (!isValidVerdictRow(verdict)) {
|
|
257
|
+
invalidVerdict = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const answer = answersByAlias.get(alias);
|
|
261
|
+
if (answer !== undefined &&
|
|
262
|
+
!answer.evidenceSufficient &&
|
|
263
|
+
(verdict.verdict !== "UNCERTAIN" || verdict.reasonCode !== "INSUFFICIENT_EVIDENCE")) {
|
|
264
|
+
unsupportedProjectionVerdict = true;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
adjudicatedN++;
|
|
268
|
+
if (verdict.verdict === "TRUE_POSITIVE")
|
|
269
|
+
counts.truePositiveN++;
|
|
270
|
+
else if (verdict.verdict === "FALSE_POSITIVE")
|
|
271
|
+
counts.falsePositiveN++;
|
|
272
|
+
else
|
|
273
|
+
counts.uncertainN++;
|
|
274
|
+
}
|
|
275
|
+
const unadjudicatedN = Math.max(emittedN - adjudicatedN, 0);
|
|
276
|
+
const precisionDenominator = counts.truePositiveN + counts.falsePositiveN;
|
|
277
|
+
const completeReads = read.failedN === 0 && read.succeededN === read.requiredN;
|
|
278
|
+
const completeAdjudication = !invalidVerdict &&
|
|
279
|
+
!unsupportedProjectionVerdict &&
|
|
280
|
+
adjudicatedN === emittedN &&
|
|
281
|
+
unadjudicatedN === 0 &&
|
|
282
|
+
!duplicateVerdictExtractors.has(extractor);
|
|
283
|
+
const precision = extractor !== "E1" &&
|
|
284
|
+
emittedN > 0 &&
|
|
285
|
+
completeReads &&
|
|
286
|
+
completeAdjudication &&
|
|
287
|
+
counts.uncertainN === 0
|
|
288
|
+
? counts.truePositiveN / precisionDenominator
|
|
289
|
+
: null;
|
|
290
|
+
const sparse = emittedN < MIN_INFORMATIVE_FINDINGS;
|
|
291
|
+
let limitation = "NONE";
|
|
292
|
+
if (extractorIdentityMismatch)
|
|
293
|
+
limitation = "IDENTITY_MISMATCH";
|
|
294
|
+
else if (!completeReads)
|
|
295
|
+
limitation = "INCOMPLETE_READS";
|
|
296
|
+
else if (emittedN === 0)
|
|
297
|
+
limitation = "ZERO_EMISSIONS";
|
|
298
|
+
else if (!completeAdjudication)
|
|
299
|
+
limitation = "INCOMPLETE_ADJUDICATION";
|
|
300
|
+
else if (counts.uncertainN > 0)
|
|
301
|
+
limitation = "UNCERTAIN_VERDICTS";
|
|
302
|
+
else if (extractor === "E1")
|
|
303
|
+
limitation = "CURRENT_STATE_ONLY";
|
|
304
|
+
else if (sparse)
|
|
305
|
+
limitation = "SPARSE_LT_20";
|
|
306
|
+
extractorScores[extractor] = {
|
|
307
|
+
extractor,
|
|
308
|
+
corpusPrN: manifest.eligiblePrN,
|
|
309
|
+
emittedN,
|
|
310
|
+
requiredAdjudicationN: emittedN,
|
|
311
|
+
adjudicatedN,
|
|
312
|
+
truePositiveN: counts.truePositiveN,
|
|
313
|
+
falsePositiveN: counts.falsePositiveN,
|
|
314
|
+
uncertainN: counts.uncertainN,
|
|
315
|
+
unadjudicatedN,
|
|
316
|
+
precisionNumerator: counts.truePositiveN,
|
|
317
|
+
precisionDenominator,
|
|
318
|
+
precision,
|
|
319
|
+
threshold: PRECISION_THRESHOLD,
|
|
320
|
+
sparse,
|
|
321
|
+
status: scoreStatus(precision, limitation),
|
|
322
|
+
limitation,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
campaign: "COND1_FINDINGS_PRECISION",
|
|
327
|
+
identity: {
|
|
328
|
+
sourceCommit: durableIdentity.sourceCommit,
|
|
329
|
+
runnerVersion: durableIdentity.runnerVersion,
|
|
330
|
+
findingsModuleSha256: durableIdentity.findingsModuleSha256,
|
|
331
|
+
extractorVersions: durableIdentity.extractorVersions,
|
|
332
|
+
packetVersion: durableIdentity.packetVersion,
|
|
333
|
+
scorerVersion: durableIdentity.scorerVersion,
|
|
334
|
+
asOf: durableIdentity.asOf,
|
|
335
|
+
scratchDbSha256: manifest.scratchDbSha256,
|
|
336
|
+
repoMapSha256: manifest.repoMapSha256,
|
|
337
|
+
corpusManifestSha256: input.corpusManifestFileSha256,
|
|
338
|
+
answerKeyCanonicalSha256,
|
|
339
|
+
},
|
|
340
|
+
corpus: {
|
|
341
|
+
eligiblePrN: manifest.eligiblePrN,
|
|
342
|
+
fullyReadPrN: corpusReadSummary.fullyReadPrN,
|
|
343
|
+
failedPrN: corpusReadSummary.failedPrN,
|
|
344
|
+
},
|
|
345
|
+
extractors: extractorScores,
|
|
346
|
+
overallStatus: overallStatus(extractorScores),
|
|
347
|
+
durablePrivacy: { rawEvidenceN: 0, evidenceRefN: 0, pathN: 0, tokenN: 0 },
|
|
348
|
+
};
|
|
349
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|