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,162 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { publishApprovedOutput } from "../common/approved-input.js";
|
|
5
|
+
import { canonicalJson, sha256Bytes, sha256Canonical } from "../common/canonical.js";
|
|
6
|
+
import { redactEvidenceExcerpt } from "../common/redaction.js";
|
|
7
|
+
import { evaluateR3Private } from "./evaluate.js";
|
|
8
|
+
import { R3_PACKET_VERSION, R3_SCORER_VERSION, } from "./types.js";
|
|
9
|
+
const SHA256_RE = /^[0-9a-f]{64}$/u;
|
|
10
|
+
function refuse(code) {
|
|
11
|
+
throw new Error(code);
|
|
12
|
+
}
|
|
13
|
+
function retainedFailure(error, artifacts) {
|
|
14
|
+
const message = error instanceof Error && /^[a-z0-9_]+$/u.test(error.message)
|
|
15
|
+
? error.message
|
|
16
|
+
: "r3_packet_failure";
|
|
17
|
+
return new Error(`${message}_${artifacts.join("_and_")}_retained`);
|
|
18
|
+
}
|
|
19
|
+
function directFile(state, value) {
|
|
20
|
+
const statePath = fs.realpathSync.native(state);
|
|
21
|
+
const filePath = fs.realpathSync.native(value);
|
|
22
|
+
if (path.dirname(filePath) !== statePath)
|
|
23
|
+
refuse("r3_aggregate_path_invalid");
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
function rank(seed, domain, value) {
|
|
27
|
+
return createHmac("sha256", seed).update(domain).update("\0").update(value).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
function stableIdentity(candidate) {
|
|
30
|
+
return `${candidate.repoAlias}\0${candidate.sessionId}\0${candidate.candidateWorkItemId ?? ""}`;
|
|
31
|
+
}
|
|
32
|
+
function backtestSample(candidates, seed) {
|
|
33
|
+
const byAlias = new Map();
|
|
34
|
+
for (const candidate of candidates) {
|
|
35
|
+
const rows = byAlias.get(candidate.repoAlias) ?? [];
|
|
36
|
+
rows.push(candidate);
|
|
37
|
+
byAlias.set(candidate.repoAlias, rows);
|
|
38
|
+
}
|
|
39
|
+
for (const rows of byAlias.values()) {
|
|
40
|
+
rows.sort((a, b) => rank(seed, "backtest", stableIdentity(a)).localeCompare(rank(seed, "backtest", stableIdentity(b))));
|
|
41
|
+
}
|
|
42
|
+
const result = [];
|
|
43
|
+
const aliases = [...byAlias.keys()].sort();
|
|
44
|
+
for (let index = 0; result.length < 100; index += 1) {
|
|
45
|
+
let added = false;
|
|
46
|
+
for (const alias of aliases) {
|
|
47
|
+
const row = byAlias.get(alias)?.[index];
|
|
48
|
+
if (row !== undefined && result.length < 100) {
|
|
49
|
+
result.push(row);
|
|
50
|
+
added = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!added)
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function day(value) {
|
|
59
|
+
return value !== null && Number.isFinite(Date.parse(value))
|
|
60
|
+
? new Date(value).toISOString().slice(0, 10)
|
|
61
|
+
: "UNKNOWN";
|
|
62
|
+
}
|
|
63
|
+
/** Build a mixed, blinded packet and a separately published private join key. */
|
|
64
|
+
export async function packetR3(request, dependencies = {}) {
|
|
65
|
+
if (!SHA256_RE.test(request.aggregateSha256))
|
|
66
|
+
refuse("r3_aggregate_digest_invalid");
|
|
67
|
+
const evaluated = await evaluateR3Private({ ...request, out: request.packetOut });
|
|
68
|
+
const retainedArtifacts = ["scratch_verification"];
|
|
69
|
+
try {
|
|
70
|
+
const aggregateBytes = fs.readFileSync(directFile(evaluated.approved.scratchStatePath, request.aggregatePath));
|
|
71
|
+
if (sha256Bytes(aggregateBytes) !== request.aggregateSha256)
|
|
72
|
+
refuse("r3_aggregate_digest_mismatch");
|
|
73
|
+
let aggregate;
|
|
74
|
+
try {
|
|
75
|
+
aggregate = JSON.parse(aggregateBytes.toString("utf8"));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
refuse("r3_aggregate_json_invalid");
|
|
79
|
+
}
|
|
80
|
+
if (canonicalJson(aggregate) !== canonicalJson(evaluated.report))
|
|
81
|
+
refuse("r3_aggregate_content_mismatch");
|
|
82
|
+
const seed = dependencies.randomSeed?.() ?? randomBytes(32);
|
|
83
|
+
if (!Buffer.isBuffer(seed) || seed.length < 32)
|
|
84
|
+
refuse("r3_random_seed_invalid");
|
|
85
|
+
const commitment = sha256Bytes(seed);
|
|
86
|
+
const campaignId = sha256Canonical({
|
|
87
|
+
identity: evaluated.report.identity,
|
|
88
|
+
aggregate: request.aggregateSha256,
|
|
89
|
+
}).slice(0, 32);
|
|
90
|
+
const uplift = evaluated.candidates.filter((candidate) => candidate.candidateReason === "UNIQUE_CANDIDATE" &&
|
|
91
|
+
candidate.disposition !== "HIGHER_PRECEDENCE" &&
|
|
92
|
+
candidate.candidateWorkItemId !== null);
|
|
93
|
+
const backtest = backtestSample(evaluated.candidates.filter((candidate) => candidate.candidateReason === "UNIQUE_CANDIDATE" &&
|
|
94
|
+
candidate.disposition === "HIGHER_PRECEDENCE" &&
|
|
95
|
+
candidate.candidateWorkItemId !== null), seed);
|
|
96
|
+
const selected = [
|
|
97
|
+
...uplift.map((candidate) => ({ candidate, cohort: "UPLIFT" })),
|
|
98
|
+
...backtest.map((candidate) => ({ candidate, cohort: "BACKTEST" })),
|
|
99
|
+
];
|
|
100
|
+
selected.sort((a, b) => rank(seed, "order", stableIdentity(a.candidate)).localeCompare(rank(seed, "order", stableIdentity(b.candidate))));
|
|
101
|
+
const keyEntries = [];
|
|
102
|
+
const entries = selected.map(({ candidate, cohort }) => {
|
|
103
|
+
const stable = stableIdentity(candidate);
|
|
104
|
+
const candidateId = rank(seed, "candidate", stable).slice(0, 32);
|
|
105
|
+
const proposedPrAlias = `pr-${rank(seed, "pr", candidate.candidateWorkItemId).slice(0, 16)}`;
|
|
106
|
+
const rawExcerpt = dependencies.intentExcerpt?.(candidate);
|
|
107
|
+
const redacted = rawExcerpt === undefined ? undefined : redactEvidenceExcerpt(rawExcerpt);
|
|
108
|
+
const redactionFailed = rawExcerpt !== undefined && redacted === null;
|
|
109
|
+
keyEntries.push({
|
|
110
|
+
candidateId,
|
|
111
|
+
sessionId: candidate.sessionId,
|
|
112
|
+
candidateWorkItemId: candidate.candidateWorkItemId,
|
|
113
|
+
repoAlias: candidate.repoAlias,
|
|
114
|
+
cohort,
|
|
115
|
+
backtestCorrect: candidate.backtestCorrect,
|
|
116
|
+
redactionFailed,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
candidateId,
|
|
120
|
+
repoAlias: candidate.repoAlias,
|
|
121
|
+
sessionWindow: {
|
|
122
|
+
startedBucket: day(candidate.firstTurnAt),
|
|
123
|
+
endedBucket: day(candidate.lastTurnAt),
|
|
124
|
+
},
|
|
125
|
+
proposedPrAlias,
|
|
126
|
+
structuredEvidence: {
|
|
127
|
+
sessionCommitSignalN: candidate.commitSignalN,
|
|
128
|
+
prLifecycleOverlap: candidate.lifecycleOverlap,
|
|
129
|
+
acceptedHigherPrecedenceSignal: candidate.excludedBy === null ? "ABSENT" : "PRESENT",
|
|
130
|
+
...(redacted === undefined || redacted === null
|
|
131
|
+
? {}
|
|
132
|
+
: { redactedIntentExcerpt: redacted }),
|
|
133
|
+
},
|
|
134
|
+
question: "SAME_WORK_ITEM",
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const packet = {
|
|
138
|
+
campaignId,
|
|
139
|
+
packetVersion: R3_PACKET_VERSION,
|
|
140
|
+
randomizationSeedCommitment: commitment,
|
|
141
|
+
aggregateSha256: request.aggregateSha256,
|
|
142
|
+
entries,
|
|
143
|
+
};
|
|
144
|
+
const key = {
|
|
145
|
+
campaignId,
|
|
146
|
+
packetVersion: R3_PACKET_VERSION,
|
|
147
|
+
scorerVersion: R3_SCORER_VERSION,
|
|
148
|
+
randomizationSeed: seed.toString("hex"),
|
|
149
|
+
randomizationSeedCommitment: commitment,
|
|
150
|
+
aggregateSha256: request.aggregateSha256,
|
|
151
|
+
entries: keyEntries,
|
|
152
|
+
};
|
|
153
|
+
publishApprovedOutput(evaluated.approved, request.packetOut, canonicalJson(packet));
|
|
154
|
+
retainedArtifacts.push("packet");
|
|
155
|
+
publishApprovedOutput(evaluated.approved, request.keyOut, canonicalJson(key));
|
|
156
|
+
retainedArtifacts.push("sealed_key");
|
|
157
|
+
return { packet, key };
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
throw retainedFailure(error, retainedArtifacts);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { runMigrations } from "../../db/migrate.js";
|
|
7
|
+
import { snapshotSessionWorkLinks } from "../../outcomes/linker.js";
|
|
8
|
+
import { backfillMissingWorkItemBranchKeys } from "../../outcomes/sync.js";
|
|
9
|
+
import { loadApprovedEvidenceInput, publishApprovedOutput, } from "../common/approved-input.js";
|
|
10
|
+
import { canonicalJson, sha256Canonical, sha256File } from "../common/canonical.js";
|
|
11
|
+
import { assertDatabaseIntegrity } from "../common/sqlite.js";
|
|
12
|
+
import { R3_REASON_VOCABULARY_VERSION, R3_RUNNER_VERSION, } from "./types.js";
|
|
13
|
+
const REQUIRED_SCHEMA_6 = [
|
|
14
|
+
"001_observe",
|
|
15
|
+
"002_indexes",
|
|
16
|
+
"003_context_inventory_history",
|
|
17
|
+
"004_apply_jobs",
|
|
18
|
+
"005_tool_event_metadata",
|
|
19
|
+
"006_d7_query_indexes",
|
|
20
|
+
];
|
|
21
|
+
const REQUIRED_SCHEMA_7 = [...REQUIRED_SCHEMA_6, "007_work_item_branch_keys"];
|
|
22
|
+
const SHA256_RE = /^[0-9a-f]{64}$/u;
|
|
23
|
+
const COMMIT_RE = /^[0-9a-f]{40}$/u;
|
|
24
|
+
const STRICT_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
|
|
25
|
+
const EVALUATOR_MODULE_PATH = fileURLToPath(new URL("../../outcomes/linker.ts", import.meta.url));
|
|
26
|
+
function refuse(code) {
|
|
27
|
+
throw new Error(code);
|
|
28
|
+
}
|
|
29
|
+
function retainedFailure(error, artifacts) {
|
|
30
|
+
const message = error instanceof Error && /^[a-z0-9_]+$/u.test(error.message)
|
|
31
|
+
? error.message
|
|
32
|
+
: "r3_prepare_failure";
|
|
33
|
+
return new Error(`${message}_${artifacts.join("_and_")}_retained`);
|
|
34
|
+
}
|
|
35
|
+
function validateRequest(request) {
|
|
36
|
+
if (!COMMIT_RE.test(request.evaluatorCommit))
|
|
37
|
+
refuse("r3_evaluator_commit_invalid");
|
|
38
|
+
if (!SHA256_RE.test(request.evaluatorModuleSha256))
|
|
39
|
+
refuse("r3_evaluator_digest_invalid");
|
|
40
|
+
if (!STRICT_UTC.test(request.asOf) || !Number.isFinite(Date.parse(request.asOf))) {
|
|
41
|
+
refuse("r3_as_of_invalid");
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isSafeInteger(request.backfillPageSize) ||
|
|
44
|
+
request.backfillPageSize < 1 ||
|
|
45
|
+
request.backfillPageSize > 1_000) {
|
|
46
|
+
refuse("r3_backfill_page_size_invalid");
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isSafeInteger(request.githubConcurrency) ||
|
|
49
|
+
request.githubConcurrency < 1 ||
|
|
50
|
+
request.githubConcurrency > 32) {
|
|
51
|
+
refuse("r3_github_concurrency_invalid");
|
|
52
|
+
}
|
|
53
|
+
if (request.resume !== (request.workingDbSha256 !== null)) {
|
|
54
|
+
refuse("r3_working_db_resume_binding_invalid");
|
|
55
|
+
}
|
|
56
|
+
if (request.workingDbSha256 !== null && !SHA256_RE.test(request.workingDbSha256)) {
|
|
57
|
+
refuse("r3_working_db_digest_invalid");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function directWorkingPath(approved, candidate) {
|
|
61
|
+
const parent = fs.realpathSync.native(path.dirname(path.resolve(candidate)));
|
|
62
|
+
if (parent !== approved.scratchStatePath)
|
|
63
|
+
refuse("r3_working_db_not_direct_state_child");
|
|
64
|
+
return path.join(parent, path.basename(candidate));
|
|
65
|
+
}
|
|
66
|
+
function initializeCheckpoint(db, request, approved) {
|
|
67
|
+
db.exec(`
|
|
68
|
+
CREATE TABLE r3_prepare_checkpoint (
|
|
69
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton=1),
|
|
70
|
+
approved_input_db_sha256 TEXT NOT NULL,
|
|
71
|
+
evaluator_commit TEXT NOT NULL,
|
|
72
|
+
evaluator_module_sha256 TEXT NOT NULL,
|
|
73
|
+
repo_map_sha256 TEXT NOT NULL,
|
|
74
|
+
as_of TEXT NOT NULL,
|
|
75
|
+
link_snapshot_sha256 TEXT NOT NULL,
|
|
76
|
+
ineligible_n INTEGER NOT NULL,
|
|
77
|
+
failed_n INTEGER NOT NULL,
|
|
78
|
+
failure_reason_json TEXT NOT NULL,
|
|
79
|
+
completed INTEGER NOT NULL
|
|
80
|
+
);
|
|
81
|
+
CREATE TABLE r3_prepare_initial_keys (
|
|
82
|
+
work_item_id TEXT PRIMARY KEY
|
|
83
|
+
);
|
|
84
|
+
INSERT INTO r3_prepare_initial_keys(work_item_id)
|
|
85
|
+
SELECT work_item_id FROM work_item_branch_keys;
|
|
86
|
+
`);
|
|
87
|
+
db.prepare(`INSERT INTO r3_prepare_checkpoint
|
|
88
|
+
(singleton,approved_input_db_sha256,evaluator_commit,evaluator_module_sha256,
|
|
89
|
+
repo_map_sha256,as_of,link_snapshot_sha256,ineligible_n,failed_n,failure_reason_json,completed)
|
|
90
|
+
VALUES (1,?,?,?,?,?,?,0,0,'{}',0)`).run(approved.scratchDbSha256, request.evaluatorCommit, request.evaluatorModuleSha256, approved.repoMapSha256, request.asOf, sha256Canonical(JSON.parse(snapshotSessionWorkLinks(db))));
|
|
91
|
+
}
|
|
92
|
+
function verifyAndResetCheckpoint(db, request, approved) {
|
|
93
|
+
if (!exactStrings(schemaVersions(db), REQUIRED_SCHEMA_7)) {
|
|
94
|
+
refuse("r3_working_db_schema_invalid");
|
|
95
|
+
}
|
|
96
|
+
assertDatabaseIntegrity(db);
|
|
97
|
+
const row = db.prepare("SELECT * FROM r3_prepare_checkpoint WHERE singleton=1").get();
|
|
98
|
+
if (row === undefined ||
|
|
99
|
+
row.approved_input_db_sha256 !== approved.scratchDbSha256 ||
|
|
100
|
+
row.evaluator_commit !== request.evaluatorCommit ||
|
|
101
|
+
row.evaluator_module_sha256 !== request.evaluatorModuleSha256 ||
|
|
102
|
+
row.repo_map_sha256 !== approved.repoMapSha256 ||
|
|
103
|
+
row.as_of !== request.asOf ||
|
|
104
|
+
row.link_snapshot_sha256 !==
|
|
105
|
+
sha256Canonical(JSON.parse(snapshotSessionWorkLinks(db))) ||
|
|
106
|
+
(row.completed !== 0 && row.completed !== 1))
|
|
107
|
+
refuse("r3_working_db_checkpoint_binding_mismatch");
|
|
108
|
+
db.prepare("UPDATE r3_prepare_checkpoint SET ineligible_n=0, failed_n=0, failure_reason_json='{}', completed=0 WHERE singleton=1").run();
|
|
109
|
+
}
|
|
110
|
+
function schemaVersions(db) {
|
|
111
|
+
const table = db
|
|
112
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'")
|
|
113
|
+
.get();
|
|
114
|
+
if (table === undefined)
|
|
115
|
+
refuse("r3_schema_missing");
|
|
116
|
+
return db
|
|
117
|
+
.prepare("SELECT version FROM schema_migrations ORDER BY version COLLATE BINARY")
|
|
118
|
+
.all().map((row) => {
|
|
119
|
+
if (typeof row.version !== "string")
|
|
120
|
+
refuse("r3_schema_invalid");
|
|
121
|
+
return row.version;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function exactStrings(actual, expected) {
|
|
125
|
+
return (actual.length === expected.length && actual.every((value, index) => value === expected[index]));
|
|
126
|
+
}
|
|
127
|
+
function migrateExactlyToSeven(db) {
|
|
128
|
+
const before = schemaVersions(db);
|
|
129
|
+
if (exactStrings(before, REQUIRED_SCHEMA_6)) {
|
|
130
|
+
// R3 is an exact historical schema-6-to-7 transition. Newer application
|
|
131
|
+
// migrations (for example the reports artifact) must not enter this gate.
|
|
132
|
+
const applied = runMigrations(db, "007_work_item_branch_keys");
|
|
133
|
+
if (!exactStrings(applied, ["007_work_item_branch_keys"])) {
|
|
134
|
+
refuse("r3_schema_transition_invalid");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (!exactStrings(before, REQUIRED_SCHEMA_7)) {
|
|
138
|
+
refuse("r3_schema_version_refused");
|
|
139
|
+
}
|
|
140
|
+
if (!exactStrings(schemaVersions(db), REQUIRED_SCHEMA_7))
|
|
141
|
+
refuse("r3_schema_seven_invalid");
|
|
142
|
+
}
|
|
143
|
+
function assertCorpusWorkspaceScope(db, approvedWorkspaceIds) {
|
|
144
|
+
const rows = db
|
|
145
|
+
.prepare(`SELECT DISTINCT s.workspace_id
|
|
146
|
+
FROM sessions s
|
|
147
|
+
WHERE s.state = 'RECONCILED'
|
|
148
|
+
AND EXISTS (SELECT 1 FROM tool_events te
|
|
149
|
+
WHERE te.session_id = s.session_id AND te.tool_name = 'Bash')
|
|
150
|
+
-- R3 corpus: evidence-bearing workspaces only (see plan §5G). Keep this
|
|
151
|
+
-- predicate byte-identical across all six corpus queries.
|
|
152
|
+
AND EXISTS (SELECT 1 FROM work_items wi WHERE wi.workspace_id = s.workspace_id)
|
|
153
|
+
ORDER BY s.workspace_id COLLATE BINARY`)
|
|
154
|
+
.all();
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
if (typeof row.workspace_id !== "string" || !approvedWorkspaceIds.has(row.workspace_id)) {
|
|
157
|
+
refuse("r3_corpus_workspace_not_allowlisted");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function freezeCorpus(db, approvedWorkspaceIds) {
|
|
162
|
+
assertCorpusWorkspaceScope(db, approvedWorkspaceIds);
|
|
163
|
+
const rows = db
|
|
164
|
+
.prepare(`SELECT s.session_id, s.file_path
|
|
165
|
+
FROM sessions s
|
|
166
|
+
WHERE s.state = 'RECONCILED'
|
|
167
|
+
AND EXISTS (SELECT 1 FROM tool_events te
|
|
168
|
+
WHERE te.session_id = s.session_id AND te.tool_name = 'Bash')
|
|
169
|
+
-- R3 corpus: evidence-bearing workspaces only (see plan §5G). Keep this
|
|
170
|
+
-- predicate byte-identical across all six corpus queries.
|
|
171
|
+
AND EXISTS (SELECT 1 FROM work_items wi WHERE wi.workspace_id = s.workspace_id)
|
|
172
|
+
ORDER BY s.session_id COLLATE BINARY`)
|
|
173
|
+
.all();
|
|
174
|
+
const entries = [];
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
if (typeof row.session_id !== "string" ||
|
|
177
|
+
row.session_id.length === 0 ||
|
|
178
|
+
typeof row.file_path !== "string" ||
|
|
179
|
+
row.file_path.length === 0) {
|
|
180
|
+
refuse("r3_corpus_row_invalid");
|
|
181
|
+
}
|
|
182
|
+
const resolvedPath = fs.realpathSync.native(row.file_path);
|
|
183
|
+
const handle = await fs.promises.open(resolvedPath, "r");
|
|
184
|
+
try {
|
|
185
|
+
const before = await handle.stat({ bigint: true });
|
|
186
|
+
const pathnameBefore = await fs.promises.stat(resolvedPath, { bigint: true });
|
|
187
|
+
const identity = {
|
|
188
|
+
device: before.dev.toString(),
|
|
189
|
+
inode: before.ino.toString(),
|
|
190
|
+
size: before.size.toString(),
|
|
191
|
+
};
|
|
192
|
+
const sameIdentity = (stat) => stat.isFile() &&
|
|
193
|
+
stat.dev.toString() === identity.device &&
|
|
194
|
+
stat.ino.toString() === identity.inode &&
|
|
195
|
+
stat.size.toString() === identity.size &&
|
|
196
|
+
identity.inode !== "0";
|
|
197
|
+
if (!sameIdentity(pathnameBefore))
|
|
198
|
+
refuse("r3_corpus_path_replaced_before_hash");
|
|
199
|
+
const digest = createHash("sha256");
|
|
200
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
201
|
+
const size = Number(before.size);
|
|
202
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
203
|
+
refuse("r3_corpus_size_invalid");
|
|
204
|
+
let position = 0;
|
|
205
|
+
while (position < size) {
|
|
206
|
+
const remaining = size - position;
|
|
207
|
+
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, remaining), position);
|
|
208
|
+
if (bytesRead === 0)
|
|
209
|
+
refuse("r3_corpus_short_read");
|
|
210
|
+
digest.update(buffer.subarray(0, bytesRead));
|
|
211
|
+
position += bytesRead;
|
|
212
|
+
}
|
|
213
|
+
const after = await handle.stat({ bigint: true });
|
|
214
|
+
const pathnameAfter = await fs.promises.stat(resolvedPath, { bigint: true });
|
|
215
|
+
if (!sameIdentity(after) ||
|
|
216
|
+
!sameIdentity(pathnameAfter) ||
|
|
217
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
218
|
+
after.ctimeNs !== before.ctimeNs)
|
|
219
|
+
refuse("r3_corpus_changed_during_hash");
|
|
220
|
+
entries.push({
|
|
221
|
+
sessionId: row.session_id,
|
|
222
|
+
path: resolvedPath,
|
|
223
|
+
identity,
|
|
224
|
+
sha256: digest.digest("hex"),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await handle.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { version: "r3-u4-private-corpus-v1", entries };
|
|
232
|
+
}
|
|
233
|
+
function identity(request) {
|
|
234
|
+
return {
|
|
235
|
+
sourceCommit: request.evaluatorCommit,
|
|
236
|
+
runnerVersion: R3_RUNNER_VERSION,
|
|
237
|
+
evaluatorModuleSha256: request.evaluatorModuleSha256,
|
|
238
|
+
normalizationVersion: "branch-v1",
|
|
239
|
+
schemaVersion: 7,
|
|
240
|
+
reasonVocabularyVersion: R3_REASON_VOCABULARY_VERSION,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** Prepare a sealed, hydrated clone. The attested Stage-0 database is never reopened or mutated. */
|
|
244
|
+
export async function prepareR3(request, dependencies) {
|
|
245
|
+
validateRequest(request);
|
|
246
|
+
if ((await sha256File(EVALUATOR_MODULE_PATH)) !== request.evaluatorModuleSha256) {
|
|
247
|
+
refuse("r3_evaluator_module_digest_mismatch");
|
|
248
|
+
}
|
|
249
|
+
if (!dependencies.github.enabled)
|
|
250
|
+
refuse("r3_github_disabled");
|
|
251
|
+
const approved = await (dependencies.loadApproved ?? loadApprovedEvidenceInput)(request.approvedInput);
|
|
252
|
+
const retainedArtifacts = ["scratch_verification"];
|
|
253
|
+
let source;
|
|
254
|
+
let working;
|
|
255
|
+
try {
|
|
256
|
+
const workingPath = directWorkingPath(approved, request.workingDbPath);
|
|
257
|
+
if (request.resume) {
|
|
258
|
+
let existing;
|
|
259
|
+
try {
|
|
260
|
+
existing = fs.realpathSync.native(workingPath);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
refuse("r3_working_db_missing");
|
|
264
|
+
}
|
|
265
|
+
retainedArtifacts.push("working_db");
|
|
266
|
+
if ((await sha256File(existing)) !== request.workingDbSha256)
|
|
267
|
+
refuse("r3_working_db_digest_mismatch");
|
|
268
|
+
working = new Database(existing, { fileMustExist: true });
|
|
269
|
+
if ((await sha256File(existing)) !== request.workingDbSha256)
|
|
270
|
+
refuse("r3_working_db_changed_during_open");
|
|
271
|
+
verifyAndResetCheckpoint(working, request, approved);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
source = approved.openVerifiedScratchDb();
|
|
275
|
+
const initial = new Database(source.serialize());
|
|
276
|
+
source.close();
|
|
277
|
+
source = undefined;
|
|
278
|
+
try {
|
|
279
|
+
migrateExactlyToSeven(initial);
|
|
280
|
+
initializeCheckpoint(initial, request, approved);
|
|
281
|
+
const publication = publishApprovedOutput(approved, workingPath, initial.serialize());
|
|
282
|
+
retainedArtifacts.push("working_db");
|
|
283
|
+
fs.chmodSync(publication.path, 0o600);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
initial.close();
|
|
287
|
+
}
|
|
288
|
+
working = new Database(workingPath, { fileMustExist: true });
|
|
289
|
+
}
|
|
290
|
+
const beforeLinks = snapshotSessionWorkLinks(working);
|
|
291
|
+
const totals = {
|
|
292
|
+
completed: false,
|
|
293
|
+
selectedN: 0,
|
|
294
|
+
keyedN: 0,
|
|
295
|
+
ineligibleN: 0,
|
|
296
|
+
failedN: 0,
|
|
297
|
+
missingN: 0,
|
|
298
|
+
failureReasonN: {},
|
|
299
|
+
};
|
|
300
|
+
let cursor = "";
|
|
301
|
+
do {
|
|
302
|
+
const page = await backfillMissingWorkItemBranchKeys(working, dependencies.github, {
|
|
303
|
+
cursor,
|
|
304
|
+
limit: request.backfillPageSize,
|
|
305
|
+
concurrency: request.githubConcurrency,
|
|
306
|
+
evidence: {
|
|
307
|
+
repositories: approved.repositories.map(({ workspaceId, owner, repo }) => ({
|
|
308
|
+
workspaceId,
|
|
309
|
+
owner,
|
|
310
|
+
repo,
|
|
311
|
+
})),
|
|
312
|
+
asOf: request.asOf,
|
|
313
|
+
syncedAt: (dependencies.now?.() ?? new Date(request.asOf)).toISOString(),
|
|
314
|
+
...(dependencies.onBackfillCheckpoint === undefined
|
|
315
|
+
? {}
|
|
316
|
+
: { onPageCheckpoint: dependencies.onBackfillCheckpoint }),
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
totals.selectedN += page.selected;
|
|
320
|
+
totals.keyedN += page.keyed;
|
|
321
|
+
totals.ineligibleN += page.ineligible;
|
|
322
|
+
totals.failedN += page.failed;
|
|
323
|
+
totals.missingN += page.missing;
|
|
324
|
+
for (const [reason, count] of Object.entries(page.failureReasonCounts ?? {})) {
|
|
325
|
+
totals.failureReasonN[reason] = (totals.failureReasonN[reason] ?? 0) + count;
|
|
326
|
+
}
|
|
327
|
+
const checkpointRow = working
|
|
328
|
+
.prepare("SELECT ineligible_n,failed_n,failure_reason_json FROM r3_prepare_checkpoint WHERE singleton=1")
|
|
329
|
+
.get();
|
|
330
|
+
const checkpointReasons = JSON.parse(checkpointRow.failure_reason_json);
|
|
331
|
+
for (const [reason, count] of Object.entries(page.failureReasonCounts ?? {})) {
|
|
332
|
+
checkpointReasons[reason] = (checkpointReasons[reason] ?? 0) + count;
|
|
333
|
+
}
|
|
334
|
+
working
|
|
335
|
+
.prepare(`UPDATE r3_prepare_checkpoint
|
|
336
|
+
SET ineligible_n=?, failed_n=?, failure_reason_json=?
|
|
337
|
+
WHERE singleton=1`)
|
|
338
|
+
.run(checkpointRow.ineligible_n + page.ineligible, checkpointRow.failed_n + page.failed, canonicalJson(checkpointReasons));
|
|
339
|
+
cursor = page.nextCursor;
|
|
340
|
+
} while (cursor !== null);
|
|
341
|
+
totals.completed = true;
|
|
342
|
+
const checkpoint = working
|
|
343
|
+
.prepare(`SELECT ineligible_n,failed_n,failure_reason_json,
|
|
344
|
+
(SELECT COUNT(*) FROM work_item_branch_keys bk
|
|
345
|
+
WHERE NOT EXISTS (SELECT 1 FROM r3_prepare_initial_keys i WHERE i.work_item_id=bk.work_item_id)) keyed_n
|
|
346
|
+
FROM r3_prepare_checkpoint WHERE singleton=1`)
|
|
347
|
+
.get();
|
|
348
|
+
totals.keyedN = checkpoint.keyed_n;
|
|
349
|
+
totals.ineligibleN = checkpoint.ineligible_n;
|
|
350
|
+
totals.failedN = checkpoint.failed_n;
|
|
351
|
+
totals.missingN = checkpoint.ineligible_n + checkpoint.failed_n;
|
|
352
|
+
totals.selectedN = totals.keyedN + totals.missingN;
|
|
353
|
+
totals.failureReasonN = JSON.parse(checkpoint.failure_reason_json);
|
|
354
|
+
working.prepare("UPDATE r3_prepare_checkpoint SET completed=1 WHERE singleton=1").run();
|
|
355
|
+
if (snapshotSessionWorkLinks(working) !== beforeLinks)
|
|
356
|
+
refuse("r3_link_snapshot_changed_prepare");
|
|
357
|
+
const corpus = await freezeCorpus(working, new Set(approved.repositories.map((repository) => repository.workspaceId)));
|
|
358
|
+
assertDatabaseIntegrity(working);
|
|
359
|
+
working.pragma("wal_checkpoint(TRUNCATE)");
|
|
360
|
+
const sealedBytes = working.serialize();
|
|
361
|
+
working.close();
|
|
362
|
+
working = undefined;
|
|
363
|
+
const sealedPublication = publishApprovedOutput(approved, request.sealedDbOut, sealedBytes);
|
|
364
|
+
retainedArtifacts.push("sealed_db");
|
|
365
|
+
const corpusPublication = publishApprovedOutput(approved, request.privateCorpusOut, canonicalJson(corpus));
|
|
366
|
+
retainedArtifacts.push("private_corpus");
|
|
367
|
+
try {
|
|
368
|
+
fs.chmodSync(sealedPublication.path, 0o444);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
refuse("r3_sealed_db_readonly_failed_sealed_and_corpus_retained");
|
|
372
|
+
}
|
|
373
|
+
const manifest = {
|
|
374
|
+
campaign: "R3_U4_BRANCH_LINKAGE",
|
|
375
|
+
status: totals.completed && totals.failedN === 0 && totals.ineligibleN === 0
|
|
376
|
+
? "PREPARED"
|
|
377
|
+
: "DATA_INSUFFICIENT",
|
|
378
|
+
identity: {
|
|
379
|
+
...identity(request),
|
|
380
|
+
approvedInputDbSha256: approved.scratchDbSha256,
|
|
381
|
+
sealedHydratedDbSha256: sealedPublication.sha256,
|
|
382
|
+
repoMapSha256: approved.repoMapSha256,
|
|
383
|
+
privateCorpusSha256: corpusPublication.sha256,
|
|
384
|
+
linkSnapshotSha256: sha256Canonical(JSON.parse(beforeLinks)),
|
|
385
|
+
asOf: request.asOf,
|
|
386
|
+
},
|
|
387
|
+
backfill: totals,
|
|
388
|
+
corpus: { outcomeBearingSessionsN: corpus.entries.length },
|
|
389
|
+
privacy: { rawRefN: 0, transcriptPathN: 0, tokenN: 0 },
|
|
390
|
+
};
|
|
391
|
+
publishApprovedOutput(approved, request.manifestOut, canonicalJson(manifest));
|
|
392
|
+
retainedArtifacts.push("manifest");
|
|
393
|
+
return manifest;
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
throw retainedFailure(error, retainedArtifacts);
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
source?.close();
|
|
400
|
+
working?.close();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
export function approvedState(approved) {
|
|
404
|
+
return { scratchStatePath: approved.scratchStatePath };
|
|
405
|
+
}
|