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.
Files changed (157) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +116 -0
  3. package/dist/apply/jobs.js +429 -0
  4. package/dist/apply/open-terminal-child.mjs +98 -0
  5. package/dist/apply/open-terminal.js +221 -0
  6. package/dist/apply/settings-gen.js +35 -0
  7. package/dist/cli/agentwrangler.js +18 -0
  8. package/dist/daemon/config.js +51 -0
  9. package/dist/daemon/http.js +258 -0
  10. package/dist/daemon/index.js +372 -0
  11. package/dist/daemon/outcomes-pass.js +82 -0
  12. package/dist/daemon/readiness.js +15 -0
  13. package/dist/daemon/router.js +756 -0
  14. package/dist/daemon/static.js +146 -0
  15. package/dist/db/migrate.js +72 -0
  16. package/dist/db/migrations/001_observe.sql +196 -0
  17. package/dist/db/migrations/002_indexes.sql +6 -0
  18. package/dist/db/migrations/003_context_inventory_history.sql +20 -0
  19. package/dist/db/migrations/004_apply_jobs.sql +17 -0
  20. package/dist/db/migrations/005_tool_event_metadata.sql +17 -0
  21. package/dist/db/migrations/006_d7_query_indexes.sql +9 -0
  22. package/dist/db/migrations/007_work_item_branch_keys.sql +11 -0
  23. package/dist/db/migrations/008_thinking_tokens.sql +1 -0
  24. package/dist/db/migrations/009_user_turn_count.sql +1 -0
  25. package/dist/db/migrations/010_workspace_cwd.sql +1 -0
  26. package/dist/db/migrations/011_reports.sql +1 -0
  27. package/dist/db/migrations/012_reconcile_indexes.sql +2 -0
  28. package/dist/db/migrations/013_friction_fields.sql +5 -0
  29. package/dist/db/migrations/014_session_churn.sql +11 -0
  30. package/dist/db/migrations/015_gap_aggregates.sql +6 -0
  31. package/dist/db/open.js +30 -0
  32. package/dist/detector/benchmark-anchors.js +36 -0
  33. package/dist/detector/calibration.js +302 -0
  34. package/dist/detector/context-history-retention.js +312 -0
  35. package/dist/detector/context-probe.js +574 -0
  36. package/dist/detector/d1-source-identity.js +25 -0
  37. package/dist/detector/detectors/d10_catalog_footprint.js +146 -0
  38. package/dist/detector/detectors/d1_ctx_always_loaded.js +203 -0
  39. package/dist/detector/detectors/d2_session_long_full_context.js +119 -0
  40. package/dist/detector/detectors/d4_model_mismatch.js +258 -0
  41. package/dist/detector/detectors/d5_limit_burn_forecast.js +138 -0
  42. package/dist/detector/detectors/d6_tool_result_bloat.js +301 -0
  43. package/dist/detector/detectors/d7_loop_retry_waste.js +345 -0
  44. package/dist/detector/detectors/d8_cache_write_churn.js +201 -0
  45. package/dist/detector/detectors/d9_idle_background_session.js +101 -0
  46. package/dist/detector/engine.js +88 -0
  47. package/dist/detector/index.js +17 -0
  48. package/dist/detector/measurement.js +426 -0
  49. package/dist/detector/practice-registry.js +259 -0
  50. package/dist/detector/registry.js +32 -0
  51. package/dist/detector/savings.js +249 -0
  52. package/dist/detector/types.js +14 -0
  53. package/dist/evidence/common/approved-input.js +632 -0
  54. package/dist/evidence/common/boundary.js +84 -0
  55. package/dist/evidence/common/canonical.js +55 -0
  56. package/dist/evidence/common/redaction.js +321 -0
  57. package/dist/evidence/common/sqlite.js +25 -0
  58. package/dist/evidence/common/state.js +29 -0
  59. package/dist/evidence/cond1/cli.js +289 -0
  60. package/dist/evidence/cond1/packet.js +407 -0
  61. package/dist/evidence/cond1/prepare.js +295 -0
  62. package/dist/evidence/cond1/score.js +349 -0
  63. package/dist/evidence/cond1/types.js +1 -0
  64. package/dist/evidence/create-approval.js +365 -0
  65. package/dist/evidence/create-scratch.js +542 -0
  66. package/dist/evidence/d7/cli.js +113 -0
  67. package/dist/evidence/d7/measure.js +193 -0
  68. package/dist/evidence/d7/types.js +1 -0
  69. package/dist/evidence/discover-approval.js +492 -0
  70. package/dist/evidence/g2/adjudicate.js +20 -0
  71. package/dist/evidence/g2/cli.js +207 -0
  72. package/dist/evidence/g2/kappa.js +39 -0
  73. package/dist/evidence/g2/pipeline.js +92 -0
  74. package/dist/evidence/g2/store.js +14 -0
  75. package/dist/evidence/github/client.js +1 -0
  76. package/dist/evidence/github/gh-cli-client.js +301 -0
  77. package/dist/evidence/r3/cli.js +209 -0
  78. package/dist/evidence/r3/evaluate.js +417 -0
  79. package/dist/evidence/r3/packet.js +162 -0
  80. package/dist/evidence/r3/prepare.js +405 -0
  81. package/dist/evidence/r3/score.js +341 -0
  82. package/dist/evidence/r3/transcript.js +155 -0
  83. package/dist/evidence/r3/types.js +4 -0
  84. package/dist/hook/context-budget-hook.mjs +138 -0
  85. package/dist/hook/danger-guard-denylist.json +27 -0
  86. package/dist/hook/danger-guard-hook.mjs +167 -0
  87. package/dist/hook/install.js +0 -0
  88. package/dist/hook/limit-burn-hook.mjs +127 -0
  89. package/dist/hook/loop-guard-hook.mjs +104 -0
  90. package/dist/hook/precompact-checkpoint-hook.mjs +123 -0
  91. package/dist/ingest/churn-collector.js +122 -0
  92. package/dist/ingest/detector-hook.js +52 -0
  93. package/dist/ingest/discovery.js +207 -0
  94. package/dist/ingest/health.js +43 -0
  95. package/dist/ingest/index.js +28 -0
  96. package/dist/ingest/ingestor.js +509 -0
  97. package/dist/ingest/parser.js +344 -0
  98. package/dist/ingest/pricing.js +153 -0
  99. package/dist/ingest/reconcile.js +52 -0
  100. package/dist/ingest/tail.js +152 -0
  101. package/dist/ingest/types.js +24 -0
  102. package/dist/ingest/workspace-mapping.js +114 -0
  103. package/dist/oauth/anthropic-api-key.js +88 -0
  104. package/dist/oauth/count-tokens.js +86 -0
  105. package/dist/oauth/credentials.js +171 -0
  106. package/dist/oauth/judge-g2-client.js +154 -0
  107. package/dist/oauth/usage.js +167 -0
  108. package/dist/outcomes/branch-key.js +49 -0
  109. package/dist/outcomes/conclusions.js +45 -0
  110. package/dist/outcomes/derive.js +94 -0
  111. package/dist/outcomes/finding-extractors.js +131 -0
  112. package/dist/outcomes/findings.js +237 -0
  113. package/dist/outcomes/github/client.js +367 -0
  114. package/dist/outcomes/github/credential.js +195 -0
  115. package/dist/outcomes/github/gh-cli-client.js +340 -0
  116. package/dist/outcomes/linker.js +486 -0
  117. package/dist/outcomes/pool.js +24 -0
  118. package/dist/outcomes/sync.js +276 -0
  119. package/dist/query/api/agents-liveness.js +182 -0
  120. package/dist/query/api/burn-status.js +50 -0
  121. package/dist/query/api/context-budget.js +114 -0
  122. package/dist/query/api/context-composition.js +67 -0
  123. package/dist/query/api/cost-per-success.js +104 -0
  124. package/dist/query/api/delivery.js +92 -0
  125. package/dist/query/api/effectiveness.js +254 -0
  126. package/dist/query/api/efficiency-headroom.js +74 -0
  127. package/dist/query/api/headroom-trend.js +105 -0
  128. package/dist/query/api/hook-config.js +75 -0
  129. package/dist/query/api/hook-install.js +8 -0
  130. package/dist/query/api/hot-sessions.js +17 -0
  131. package/dist/query/api/idle-sessions.js +52 -0
  132. package/dist/query/api/index.js +40 -0
  133. package/dist/query/api/loop-guard.js +90 -0
  134. package/dist/query/api/offload-share.js +41 -0
  135. package/dist/query/api/outcomes.js +218 -0
  136. package/dist/query/api/overview.js +535 -0
  137. package/dist/query/api/rec-prompt.js +138 -0
  138. package/dist/query/api/recommendations-ledger.js +111 -0
  139. package/dist/query/api/recommendations.js +514 -0
  140. package/dist/query/api/reports.js +78 -0
  141. package/dist/query/api/self-churn.js +77 -0
  142. package/dist/query/api/self-percentiles.js +109 -0
  143. package/dist/query/api/session-drivers.js +153 -0
  144. package/dist/query/api/settings.js +85 -0
  145. package/dist/query/api/spend-flavor.js +234 -0
  146. package/dist/query/api/trends.js +155 -0
  147. package/dist/query/cap-weighted.js +119 -0
  148. package/dist/query/db-context.js +42 -0
  149. package/dist/query/envelope.js +71 -0
  150. package/dist/query/forecast.js +191 -0
  151. package/dist/query/settings-store.js +441 -0
  152. package/dist/query/spend.js +171 -0
  153. package/dist/query/trends.js +194 -0
  154. package/dist/ui/assets/index-DnRKgc21.css +1 -0
  155. package/dist/ui/assets/index-h1Q1wWq5.js +168 -0
  156. package/dist/ui/index.html +39 -0
  157. package/package.json +59 -0
@@ -0,0 +1,209 @@
1
+ import * as path from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import { readGithubToken } from "../../outcomes/github/credential.js";
4
+ import { canonicalJson, sha256Canonical } from "../common/canonical.js";
5
+ import { EvidenceGhCliClient } from "../github/gh-cli-client.js";
6
+ import { evaluateR3 } from "./evaluate.js";
7
+ import { packetR3 } from "./packet.js";
8
+ import { prepareR3 } from "./prepare.js";
9
+ import { scoreR3 } from "./score.js";
10
+ const BASE_FLAGS = [
11
+ "--approval-manifest",
12
+ "--approval-sha256",
13
+ "--scratch-preparation-manifest",
14
+ "--scratch-preparation-sha256",
15
+ "--scratch-db",
16
+ "--approved-scratch-db-sha256",
17
+ "--repo-map",
18
+ "--repo-map-sha256",
19
+ "--scratch-state",
20
+ "--scratch-verification",
21
+ "--live-db",
22
+ "--repository-root",
23
+ ];
24
+ function refuse(code) {
25
+ throw new Error(code);
26
+ }
27
+ function parseValues(args, commandFlags) {
28
+ const allowed = new Set([...BASE_FLAGS, ...commandFlags]);
29
+ const values = new Map();
30
+ if (args.length % 2 !== 0)
31
+ refuse("r3_cli_missing_value");
32
+ for (let index = 0; index < args.length; index += 2) {
33
+ const flag = args[index];
34
+ const value = args[index + 1];
35
+ if (flag === undefined || !allowed.has(flag))
36
+ refuse("r3_cli_unknown_argument");
37
+ if (values.has(flag))
38
+ refuse("r3_cli_duplicate_argument");
39
+ if (value === undefined || value.length === 0 || value.startsWith("--"))
40
+ refuse("r3_cli_missing_value");
41
+ values.set(flag, value);
42
+ }
43
+ for (const flag of allowed)
44
+ if (!values.has(flag))
45
+ refuse("r3_cli_missing_required_argument");
46
+ return values;
47
+ }
48
+ function get(values, flag) {
49
+ const value = values.get(flag);
50
+ if (value === undefined)
51
+ refuse("r3_cli_missing_required_argument");
52
+ return value;
53
+ }
54
+ function integer(values, flag) {
55
+ const value = get(values, flag);
56
+ if (!/^\d+$/u.test(value))
57
+ refuse("r3_cli_integer_invalid");
58
+ return Number(value);
59
+ }
60
+ function approved(values) {
61
+ return {
62
+ approvalManifestPath: get(values, "--approval-manifest"),
63
+ approvalManifestSha256: get(values, "--approval-sha256"),
64
+ preparationManifestPath: get(values, "--scratch-preparation-manifest"),
65
+ preparationManifestSha256: get(values, "--scratch-preparation-sha256"),
66
+ scratchDbPath: get(values, "--scratch-db"),
67
+ scratchDbSha256: get(values, "--approved-scratch-db-sha256"),
68
+ repoMapPath: get(values, "--repo-map"),
69
+ repoMapSha256: get(values, "--repo-map-sha256"),
70
+ scratchStatePath: get(values, "--scratch-state"),
71
+ scratchVerificationPath: get(values, "--scratch-verification"),
72
+ liveDbPath: get(values, "--live-db"),
73
+ repositoryRoot: get(values, "--repository-root"),
74
+ };
75
+ }
76
+ function evaluation(values) {
77
+ return {
78
+ approvedInput: approved(values),
79
+ sealedDbPath: get(values, "--sealed-scratch-db"),
80
+ sealedDbSha256: get(values, "--sealed-scratch-db-sha256"),
81
+ manifestPath: get(values, "--manifest"),
82
+ manifestSha256: get(values, "--manifest-sha256"),
83
+ privateCorpusPath: get(values, "--private-corpus"),
84
+ privateCorpusSha256: get(values, "--private-corpus-sha256"),
85
+ };
86
+ }
87
+ const EVALUATION_FLAGS = [
88
+ "--sealed-scratch-db",
89
+ "--sealed-scratch-db-sha256",
90
+ "--manifest",
91
+ "--manifest-sha256",
92
+ "--private-corpus",
93
+ "--private-corpus-sha256",
94
+ ];
95
+ export async function runR3Cli(args, dependencies = {}) {
96
+ const [command, ...rest] = args;
97
+ if (command === "prepare") {
98
+ const values = parseValues(rest, [
99
+ "--sealed-db-out",
100
+ "--manifest-out",
101
+ "--private-corpus-out",
102
+ "--working-db",
103
+ "--working-db-sha256",
104
+ "--resume",
105
+ "--evaluator-commit",
106
+ "--evaluator-module-sha256",
107
+ "--as-of",
108
+ "--backfill-page-size",
109
+ "--github-concurrency",
110
+ ]);
111
+ const envToken = process.env.GH_TOKEN;
112
+ const token = envToken !== undefined && envToken.length > 0
113
+ ? { ok: true, data: envToken }
114
+ : await (dependencies.token?.() ?? readGithubToken());
115
+ const resumeValue = get(values, "--resume");
116
+ if (resumeValue !== "true" && resumeValue !== "false")
117
+ refuse("r3_cli_resume_invalid");
118
+ const workingDigest = get(values, "--working-db-sha256");
119
+ const manifest = await prepareR3({
120
+ approvedInput: approved(values),
121
+ sealedDbOut: get(values, "--sealed-db-out"),
122
+ manifestOut: get(values, "--manifest-out"),
123
+ privateCorpusOut: get(values, "--private-corpus-out"),
124
+ workingDbPath: get(values, "--working-db"),
125
+ workingDbSha256: workingDigest === "NEW" ? null : workingDigest,
126
+ resume: resumeValue === "true",
127
+ evaluatorCommit: get(values, "--evaluator-commit"),
128
+ evaluatorModuleSha256: get(values, "--evaluator-module-sha256"),
129
+ asOf: get(values, "--as-of"),
130
+ backfillPageSize: integer(values, "--backfill-page-size"),
131
+ githubConcurrency: integer(values, "--github-concurrency"),
132
+ }, { github: new EvidenceGhCliClient(token) });
133
+ return {
134
+ status: manifest.status,
135
+ backfill: manifest.backfill,
136
+ corpus: manifest.corpus,
137
+ identity: manifest.identity,
138
+ privacy: manifest.privacy,
139
+ };
140
+ }
141
+ if (command === "evaluate") {
142
+ const values = parseValues(rest, [...EVALUATION_FLAGS, "--out"]);
143
+ return evaluateR3({ ...evaluation(values), out: get(values, "--out") });
144
+ }
145
+ if (command === "packet") {
146
+ const values = parseValues(rest, [
147
+ ...EVALUATION_FLAGS,
148
+ "--aggregate",
149
+ "--aggregate-sha256",
150
+ "--packet-out",
151
+ "--key-out",
152
+ ]);
153
+ const result = await packetR3({
154
+ ...evaluation(values),
155
+ aggregatePath: get(values, "--aggregate"),
156
+ aggregateSha256: get(values, "--aggregate-sha256"),
157
+ packetOut: get(values, "--packet-out"),
158
+ keyOut: get(values, "--key-out"),
159
+ });
160
+ return {
161
+ status: "PACKET_CREATED",
162
+ campaignId: result.packet.campaignId,
163
+ entryN: result.packet.entries.length,
164
+ aggregateSha256: result.packet.aggregateSha256,
165
+ packetCanonicalSha256: sha256Canonical(result.packet),
166
+ keyCanonicalSha256: sha256Canonical(result.key),
167
+ privacy: { rawRefN: 0, transcriptPathN: 0, tokenN: 0 },
168
+ };
169
+ }
170
+ if (command === "score") {
171
+ const values = parseValues(rest, [
172
+ "--aggregate",
173
+ "--aggregate-sha256",
174
+ "--sealed-key",
175
+ "--sealed-key-sha256",
176
+ "--verdicts",
177
+ "--verdicts-sha256",
178
+ "--out",
179
+ ]);
180
+ return scoreR3({
181
+ approvedInput: approved(values),
182
+ aggregatePath: get(values, "--aggregate"),
183
+ aggregateSha256: get(values, "--aggregate-sha256"),
184
+ sealedKeyPath: get(values, "--sealed-key"),
185
+ sealedKeySha256: get(values, "--sealed-key-sha256"),
186
+ verdictsPath: get(values, "--verdicts"),
187
+ verdictsSha256: get(values, "--verdicts-sha256"),
188
+ out: get(values, "--out"),
189
+ });
190
+ }
191
+ refuse("r3_cli_command_invalid");
192
+ }
193
+ async function main() {
194
+ try {
195
+ const result = await runR3Cli(process.argv.slice(2));
196
+ process.stdout.write(`${canonicalJson(result)}\n`);
197
+ }
198
+ catch (error) {
199
+ const message = error instanceof Error ? error.message : "r3_unexpected_failure";
200
+ const failure = /^[a-z0-9_]+$/u.test(message) ? message : "r3_unexpected_failure";
201
+ process.stderr.write(`${canonicalJson({ status: "REFUSED", failure })}\n`);
202
+ process.exitCode = 1;
203
+ }
204
+ }
205
+ const invokedPath = process.argv[1];
206
+ if (invokedPath !== undefined &&
207
+ pathToFileURL(path.resolve(invokedPath)).href ===
208
+ pathToFileURL(fileURLToPath(import.meta.url)).href)
209
+ void main();
@@ -0,0 +1,417 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import Database from "better-sqlite3";
5
+ import { evaluateBranchLinksShadow, snapshotSessionWorkLinks, } from "../../outcomes/linker.js";
6
+ import { loadApprovedEvidenceInput, publishApprovedOutput, } from "../common/approved-input.js";
7
+ import { canonicalJson, sha256Bytes, sha256Canonical } from "../common/canonical.js";
8
+ import { assertDatabaseIntegrity } from "../common/sqlite.js";
9
+ import { harvestFrozenTranscript } from "./transcript.js";
10
+ const SHA256_RE = /^[0-9a-f]{64}$/u;
11
+ const EVALUATOR_MODULE_PATH = fileURLToPath(new URL("../../outcomes/linker.ts", import.meta.url));
12
+ const CANDIDATE_REASONS = [
13
+ "NO_VALID_SESSION_KEY",
14
+ "MULTIPLE_SESSION_KEYS",
15
+ "NO_MATCHING_WORK_ITEM_KEY",
16
+ "DUPLICATE_WORKSPACE_MATCH",
17
+ "UNIQUE_CANDIDATE",
18
+ ];
19
+ const DISPOSITION_REASONS = [...CANDIDATE_REASONS, "HIGHER_PRECEDENCE"];
20
+ const TRANSCRIPT_FAILURES = [
21
+ "MISSING",
22
+ "UNREADABLE",
23
+ "REPLACED",
24
+ "CHANGED",
25
+ "LIMIT_EXCEEDED",
26
+ "CORPUS_MISMATCH",
27
+ ];
28
+ function refuse(code) {
29
+ throw new Error(code);
30
+ }
31
+ function retainedFailure(error, artifacts) {
32
+ const message = error instanceof Error && /^[a-z0-9_]+$/u.test(error.message)
33
+ ? error.message
34
+ : "r3_evaluate_failure";
35
+ return new Error(`${message}_${artifacts.join("_and_")}_retained`);
36
+ }
37
+ function exactKeys(value, keys, code) {
38
+ const actual = Object.keys(value).sort();
39
+ const expected = [...keys].sort();
40
+ if (actual.length !== expected.length || actual.some((key, i) => key !== expected[i])) {
41
+ refuse(code);
42
+ }
43
+ }
44
+ function record(value, code) {
45
+ if (value === null || typeof value !== "object" || Array.isArray(value))
46
+ refuse(code);
47
+ return value;
48
+ }
49
+ function directStateFile(state, file, code) {
50
+ const stateReal = fs.realpathSync.native(state);
51
+ const fileReal = fs.realpathSync.native(file);
52
+ if (path.dirname(fileReal) !== stateReal)
53
+ refuse(code);
54
+ return fileReal;
55
+ }
56
+ function readBoundJson(state, file, digest, code) {
57
+ if (!SHA256_RE.test(digest))
58
+ refuse(`${code}_digest_invalid`);
59
+ const bytes = fs.readFileSync(directStateFile(state, file, `${code}_path_invalid`));
60
+ if (sha256Bytes(bytes) !== digest)
61
+ refuse(`${code}_digest_mismatch`);
62
+ try {
63
+ return JSON.parse(bytes.toString("utf8"));
64
+ }
65
+ catch {
66
+ refuse(`${code}_json_invalid`);
67
+ }
68
+ }
69
+ function parseManifest(value) {
70
+ const parsed = record(value, "r3_manifest_invalid");
71
+ exactKeys(parsed, ["campaign", "status", "identity", "backfill", "corpus", "privacy"], "r3_manifest_keys_invalid");
72
+ if (parsed.campaign !== "R3_U4_BRANCH_LINKAGE")
73
+ refuse("r3_manifest_campaign_invalid");
74
+ const identity = record(parsed.identity, "r3_manifest_identity_invalid");
75
+ exactKeys(identity, [
76
+ "sourceCommit",
77
+ "runnerVersion",
78
+ "evaluatorModuleSha256",
79
+ "normalizationVersion",
80
+ "schemaVersion",
81
+ "reasonVocabularyVersion",
82
+ "approvedInputDbSha256",
83
+ "sealedHydratedDbSha256",
84
+ "repoMapSha256",
85
+ "privateCorpusSha256",
86
+ "linkSnapshotSha256",
87
+ "asOf",
88
+ ], "r3_manifest_identity_keys_invalid");
89
+ for (const key of [
90
+ "evaluatorModuleSha256",
91
+ "approvedInputDbSha256",
92
+ "sealedHydratedDbSha256",
93
+ "repoMapSha256",
94
+ "privateCorpusSha256",
95
+ "linkSnapshotSha256",
96
+ ]) {
97
+ if (typeof identity[key] !== "string" || !SHA256_RE.test(identity[key]))
98
+ refuse("r3_manifest_identity_digest_invalid");
99
+ }
100
+ if (typeof identity.sourceCommit !== "string" ||
101
+ !/^[0-9a-f]{40}$/u.test(identity.sourceCommit) ||
102
+ identity.runnerVersion !== "r3-u4-runner-v1" ||
103
+ identity.normalizationVersion !== "branch-v1" ||
104
+ identity.schemaVersion !== 7 ||
105
+ identity.reasonVocabularyVersion !== "r3-shadow-reasons-v1" ||
106
+ typeof identity.asOf !== "string" ||
107
+ !Number.isFinite(Date.parse(identity.asOf)))
108
+ refuse("r3_manifest_identity_version_invalid");
109
+ if (parsed.status !== "PREPARED" && parsed.status !== "DATA_INSUFFICIENT") {
110
+ refuse("r3_manifest_status_invalid");
111
+ }
112
+ const backfill = record(parsed.backfill, "r3_manifest_backfill_invalid");
113
+ exactKeys(backfill, ["completed", "selectedN", "keyedN", "ineligibleN", "failedN", "missingN", "failureReasonN"], "r3_manifest_backfill_keys_invalid");
114
+ if (typeof backfill.completed !== "boolean")
115
+ refuse("r3_manifest_backfill_value_invalid");
116
+ for (const key of ["selectedN", "keyedN", "ineligibleN", "failedN", "missingN"]) {
117
+ if (typeof backfill[key] !== "number" ||
118
+ !Number.isSafeInteger(backfill[key]) ||
119
+ backfill[key] < 0)
120
+ refuse("r3_manifest_backfill_value_invalid");
121
+ }
122
+ const reasons = record(backfill.failureReasonN, "r3_manifest_backfill_reasons_invalid");
123
+ for (const count of Object.values(reasons)) {
124
+ if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)
125
+ refuse("r3_manifest_backfill_reasons_invalid");
126
+ }
127
+ const corpus = record(parsed.corpus, "r3_manifest_corpus_invalid");
128
+ exactKeys(corpus, ["outcomeBearingSessionsN"], "r3_manifest_corpus_keys_invalid");
129
+ if (typeof corpus.outcomeBearingSessionsN !== "number" ||
130
+ !Number.isSafeInteger(corpus.outcomeBearingSessionsN) ||
131
+ corpus.outcomeBearingSessionsN < 0)
132
+ refuse("r3_manifest_corpus_value_invalid");
133
+ const privacy = record(parsed.privacy, "r3_manifest_privacy_invalid");
134
+ exactKeys(privacy, ["rawRefN", "transcriptPathN", "tokenN"], "r3_manifest_privacy_keys_invalid");
135
+ if (privacy.rawRefN !== 0 || privacy.transcriptPathN !== 0 || privacy.tokenN !== 0)
136
+ refuse("r3_manifest_privacy_value_invalid");
137
+ return value;
138
+ }
139
+ function parseCorpus(value) {
140
+ const parsed = record(value, "r3_corpus_invalid");
141
+ exactKeys(parsed, ["version", "entries"], "r3_corpus_keys_invalid");
142
+ if (parsed.version !== "r3-u4-private-corpus-v1" || !Array.isArray(parsed.entries)) {
143
+ refuse("r3_corpus_version_invalid");
144
+ }
145
+ const sessions = new Set();
146
+ for (const raw of parsed.entries) {
147
+ const entry = record(raw, "r3_corpus_entry_invalid");
148
+ exactKeys(entry, ["sessionId", "path", "identity", "sha256"], "r3_corpus_entry_keys_invalid");
149
+ const identity = record(entry.identity, "r3_corpus_identity_invalid");
150
+ exactKeys(identity, ["device", "inode", "size"], "r3_corpus_identity_keys_invalid");
151
+ if (typeof entry.sessionId !== "string" ||
152
+ entry.sessionId.length === 0 ||
153
+ sessions.has(entry.sessionId) ||
154
+ typeof entry.path !== "string" ||
155
+ entry.path.length === 0 ||
156
+ typeof entry.sha256 !== "string" ||
157
+ !SHA256_RE.test(entry.sha256) ||
158
+ typeof identity.device !== "string" ||
159
+ typeof identity.inode !== "string" ||
160
+ typeof identity.size !== "string")
161
+ refuse("r3_corpus_entry_value_invalid");
162
+ sessions.add(entry.sessionId);
163
+ }
164
+ return value;
165
+ }
166
+ function openBoundDb(state, file, digest) {
167
+ if (!SHA256_RE.test(digest))
168
+ refuse("r3_sealed_db_digest_invalid");
169
+ const bytes = fs.readFileSync(directStateFile(state, file, "r3_sealed_db_path_invalid"));
170
+ if (sha256Bytes(bytes) !== digest)
171
+ refuse("r3_sealed_db_digest_mismatch");
172
+ const db = new Database(bytes);
173
+ try {
174
+ db.pragma("query_only = ON");
175
+ assertDatabaseIntegrity(db);
176
+ return db;
177
+ }
178
+ catch (error) {
179
+ db.close();
180
+ throw error;
181
+ }
182
+ }
183
+ function zero(values) {
184
+ return Object.fromEntries(values.map((value) => [value, 0]));
185
+ }
186
+ function emptyCohort(repoAlias) {
187
+ return {
188
+ repoAlias,
189
+ outcomeBearingSessionsN: 0,
190
+ baselineLinkedOutcomeBearingN: 0,
191
+ candidateReasonN: zero(CANDIDATE_REASONS),
192
+ dispositionReasonN: zero(DISPOSITION_REASONS),
193
+ requiredAdjudicationN: 0,
194
+ adjudicatedN: 0,
195
+ correctN: 0,
196
+ incorrectN: 0,
197
+ uncertainN: 0,
198
+ };
199
+ }
200
+ function assertEvaluationWorkspaceScope(db, repositories) {
201
+ const approved = new Set(repositories.map((repository) => repository.workspaceId));
202
+ const rows = db
203
+ .prepare(`SELECT DISTINCT s.workspace_id
204
+ FROM sessions s
205
+ WHERE s.state='RECONCILED'
206
+ AND EXISTS (SELECT 1 FROM tool_events te
207
+ WHERE te.session_id=s.session_id AND te.tool_name='Bash')
208
+ -- R3 corpus: evidence-bearing workspaces only (see plan §5G). The evaluation
209
+ -- workspace-scope check must mirror the corpus predicate, else evidence-free
210
+ -- (unmappable) workspaces trip the allowlist. Keep byte-identical across all six sites.
211
+ AND EXISTS (SELECT 1 FROM work_items wi WHERE wi.workspace_id = s.workspace_id)
212
+ ORDER BY s.workspace_id COLLATE BINARY`)
213
+ .all();
214
+ for (const row of rows) {
215
+ if (typeof row.workspace_id !== "string" || !approved.has(row.workspace_id)) {
216
+ refuse("r3_evaluation_workspace_not_allowlisted");
217
+ }
218
+ }
219
+ }
220
+ function utcDay(value) {
221
+ if (value === null || !Number.isFinite(Date.parse(value)))
222
+ return null;
223
+ return new Date(value).toISOString().slice(0, 10);
224
+ }
225
+ async function runOnce(db, corpus, repositories) {
226
+ assertEvaluationWorkspaceScope(db, repositories);
227
+ const before = snapshotSessionWorkLinks(db);
228
+ const bySession = new Map(corpus.entries.map((entry) => [entry.sessionId, entry]));
229
+ const shadow = await evaluateBranchLinksShadow(db, "shadow", {
230
+ transcriptEvidence: {
231
+ entryForSession: (sessionId) => bySession.get(sessionId),
232
+ harvest: harvestFrozenTranscript,
233
+ },
234
+ });
235
+ const after = snapshotSessionWorkLinks(db);
236
+ const linkTableUnchanged = before === after;
237
+ const aliases = new Map(repositories.map((repo) => [repo.workspaceId, repo.reportAlias]));
238
+ const sessionRows = db
239
+ .prepare(`SELECT s.session_id, s.workspace_id, s.first_turn_at, s.last_turn_at,
240
+ (SELECT COUNT(*) FROM tool_events te WHERE te.session_id=s.session_id AND te.commit_sha IS NOT NULL) commit_n
241
+ FROM sessions s WHERE s.state='RECONCILED'
242
+ AND EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id=s.session_id AND te.tool_name='Bash')
243
+ -- R3 corpus: evidence-bearing workspaces only (see plan §5G). Keep byte-aligned with prepare.ts.
244
+ AND EXISTS (SELECT 1 FROM work_items wi WHERE wi.workspace_id = s.workspace_id)`)
245
+ .all();
246
+ const sessions = new Map(sessionRows.map((row) => [row.session_id, row]));
247
+ const accepted = db.prepare("SELECT work_item_id FROM session_work_links WHERE session_id=? AND method IN ('MANUAL','PR_LINK','SHA_OVERLAP')");
248
+ const lifecycle = db.prepare("SELECT opened_at, COALESCE(merged_at, closed_at) ended_at FROM work_items WHERE work_item_id=?");
249
+ const candidates = shadow.evaluations.map((evaluation) => {
250
+ const session = sessions.get(evaluation.sessionId);
251
+ if (session === undefined)
252
+ refuse("r3_shadow_unknown_session");
253
+ const repoAlias = aliases.get(session.workspace_id);
254
+ if (repoAlias === undefined)
255
+ refuse("r3_shadow_workspace_not_allowlisted");
256
+ const acceptedIds = accepted.all(evaluation.sessionId).map((row) => row.work_item_id);
257
+ const dates = evaluation.candidateWorkItemId === null
258
+ ? undefined
259
+ : lifecycle.get(evaluation.candidateWorkItemId);
260
+ const startDay = utcDay(session.first_turn_at);
261
+ const endDay = utcDay(session.last_turn_at);
262
+ const openedDay = utcDay(dates?.opened_at ?? null);
263
+ const closedDay = utcDay(dates?.ended_at ?? null);
264
+ const lifecycleOverlap = startDay === null || endDay === null || openedDay === null
265
+ ? "UNKNOWN"
266
+ : endDay < openedDay || (closedDay !== null && startDay > closedDay)
267
+ ? "NO"
268
+ : "YES";
269
+ return {
270
+ ...evaluation,
271
+ repoAlias,
272
+ firstTurnAt: session.first_turn_at,
273
+ lastTurnAt: session.last_turn_at,
274
+ commitSignalN: session.commit_n,
275
+ lifecycleOverlap,
276
+ backtestCorrect: evaluation.excludedBy === null || evaluation.candidateWorkItemId === null
277
+ ? null
278
+ : acceptedIds.includes(evaluation.candidateWorkItemId),
279
+ };
280
+ });
281
+ const cohortMap = new Map(repositories.map((repo) => [repo.reportAlias, emptyCohort(repo.reportAlias)]));
282
+ const baselineLinked = new Set(db
283
+ .prepare(`SELECT DISTINCT s.session_id FROM sessions s JOIN session_work_links l ON l.session_id=s.session_id
284
+ WHERE s.state='RECONCILED' AND EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id=s.session_id AND te.tool_name='Bash')
285
+ -- Keep the numerator aligned with the section 5G narrowed corpus: the numerator must be a subset of the denominator.
286
+ AND EXISTS (SELECT 1 FROM work_items wi WHERE wi.workspace_id = s.workspace_id)`)
287
+ .all().map((row) => row.session_id));
288
+ for (const candidate of candidates) {
289
+ const cohort = cohortMap.get(candidate.repoAlias);
290
+ if (cohort === undefined)
291
+ refuse("r3_cohort_missing");
292
+ cohort.outcomeBearingSessionsN += 1;
293
+ if (baselineLinked.has(candidate.sessionId))
294
+ cohort.baselineLinkedOutcomeBearingN += 1;
295
+ cohort.candidateReasonN[candidate.candidateReason] += 1;
296
+ cohort.dispositionReasonN[candidate.disposition] += 1;
297
+ if (candidate.candidateReason === "UNIQUE_CANDIDATE" &&
298
+ candidate.disposition !== "HIGHER_PRECEDENCE")
299
+ cohort.requiredAdjudicationN += 1;
300
+ }
301
+ const uniqueUplift = candidates.filter((item) => item.candidateReason === "UNIQUE_CANDIDATE" && item.disposition !== "HIGHER_PRECEDENCE");
302
+ const backtest = candidates.filter((item) => item.candidateReason === "UNIQUE_CANDIDATE" && item.disposition === "HIGHER_PRECEDENCE");
303
+ const allSessionsN = db.prepare("SELECT COUNT(*) n FROM sessions").get().n;
304
+ const reconciledN = db.prepare("SELECT COUNT(*) n FROM sessions WHERE state='RECONCILED'").get().n;
305
+ // reconciled + Bash, BEFORE the evidence-bearing (work_items) filter — lets us report the
306
+ // no-Bash and no-work_items exclusions separately instead of lumping them (see plan §5G).
307
+ const reconciledBashN = db
308
+ .prepare(`SELECT COUNT(*) n FROM sessions s
309
+ WHERE s.state='RECONCILED'
310
+ AND EXISTS (SELECT 1 FROM tool_events te
311
+ WHERE te.session_id=s.session_id AND te.tool_name='Bash')`)
312
+ .get().n;
313
+ const aggregate = {
314
+ denominator: {
315
+ reconciledSessionsN: reconciledN,
316
+ outcomeBearingSessionsN: candidates.length,
317
+ baselineLinkedOutcomeBearingN: baselineLinked.size,
318
+ excludedNotReconciledN: allSessionsN - reconciledN,
319
+ excludedNoBashN: reconciledN - reconciledBashN,
320
+ excludedNoWorkItemsN: reconciledBashN - candidates.length,
321
+ },
322
+ shadow: {
323
+ candidateReasonN: shadow.candidateReasonCounts,
324
+ dispositionReasonN: shadow.dispositionCounts,
325
+ uniqueUpliftCandidatesN: uniqueUplift.length,
326
+ higherPrecedenceCandidatesN: backtest.length,
327
+ backtestEligibleN: backtest.length,
328
+ backtestAgreementN: backtest.filter((item) => item.backtestCorrect === true).length,
329
+ backtestDisagreementN: backtest.filter((item) => item.backtestCorrect === false).length,
330
+ transcriptSucceededN: shadow.strictTranscript?.succeeded ?? 0,
331
+ transcriptFailureReasonN: shadow.strictTranscript?.failureReasonCounts ?? zero(TRANSCRIPT_FAILURES),
332
+ },
333
+ adjudication: {
334
+ requiredN: uniqueUplift.length,
335
+ adjudicatedN: 0,
336
+ correctN: 0,
337
+ incorrectN: 0,
338
+ uncertainN: 0,
339
+ precisionNumerator: 0,
340
+ precisionDenominator: 0,
341
+ },
342
+ projected: {
343
+ correctUniqueUpliftN: 0,
344
+ linkedNumerator: baselineLinked.size,
345
+ outcomeBearingDenominator: candidates.length,
346
+ coverage: null,
347
+ precision: null,
348
+ },
349
+ byRepoAlias: [...cohortMap.values()].sort((a, b) => a.repoAlias < b.repoAlias ? -1 : a.repoAlias > b.repoAlias ? 1 : 0),
350
+ privacy: {
351
+ linkTableUnchanged,
352
+ rawRefN: 0,
353
+ transcriptPathN: 0,
354
+ tokenN: 0,
355
+ },
356
+ };
357
+ return { digest: sha256Canonical(aggregate), candidates, aggregate };
358
+ }
359
+ export async function evaluateR3Private(request) {
360
+ const approved = await loadApprovedEvidenceInput(request.approvedInput);
361
+ try {
362
+ const manifest = parseManifest(readBoundJson(approved.scratchStatePath, request.manifestPath, request.manifestSha256, "r3_manifest"));
363
+ const corpus = parseCorpus(readBoundJson(approved.scratchStatePath, request.privateCorpusPath, request.privateCorpusSha256, "r3_corpus"));
364
+ if (manifest.identity.approvedInputDbSha256 !== approved.scratchDbSha256 ||
365
+ manifest.identity.repoMapSha256 !== approved.repoMapSha256 ||
366
+ manifest.identity.sealedHydratedDbSha256 !== request.sealedDbSha256 ||
367
+ manifest.identity.privateCorpusSha256 !== request.privateCorpusSha256 ||
368
+ manifest.corpus.outcomeBearingSessionsN !== corpus.entries.length)
369
+ refuse("r3_evaluation_binding_mismatch");
370
+ if (sha256Bytes(fs.readFileSync(EVALUATOR_MODULE_PATH)) !==
371
+ manifest.identity.evaluatorModuleSha256)
372
+ refuse("r3_evaluator_module_digest_changed");
373
+ const db = openBoundDb(approved.scratchStatePath, request.sealedDbPath, request.sealedDbSha256);
374
+ try {
375
+ const first = await runOnce(db, corpus, approved.repositories);
376
+ const second = await runOnce(db, corpus, approved.repositories);
377
+ const transcriptFailedN = Object.values(first.aggregate.shadow.transcriptFailureReasonN).reduce((a, b) => a + b, 0);
378
+ const equal = first.digest === second.digest;
379
+ const sufficient = manifest.status === "PREPARED" &&
380
+ manifest.backfill.completed &&
381
+ manifest.backfill.failedN === 0 &&
382
+ transcriptFailedN === 0 &&
383
+ first.aggregate.privacy.linkTableUnchanged &&
384
+ second.aggregate.privacy.linkTableUnchanged &&
385
+ equal;
386
+ const report = {
387
+ campaign: "R3_U4_BRANCH_LINKAGE",
388
+ status: sufficient ? "PREPARED" : "DATA_INSUFFICIENT",
389
+ identity: { ...manifest.identity, prepareManifestSha256: request.manifestSha256 },
390
+ backfill: manifest.backfill,
391
+ ...first.aggregate,
392
+ deterministicRerun: {
393
+ firstCanonicalSha256: first.digest,
394
+ secondCanonicalSha256: second.digest,
395
+ equal,
396
+ },
397
+ };
398
+ return { report, candidates: first.candidates, approved };
399
+ }
400
+ finally {
401
+ db.close();
402
+ }
403
+ }
404
+ catch (error) {
405
+ throw retainedFailure(error, ["scratch_verification"]);
406
+ }
407
+ }
408
+ export async function evaluateR3(request) {
409
+ const result = await evaluateR3Private(request);
410
+ try {
411
+ publishApprovedOutput(result.approved, request.out, canonicalJson(result.report));
412
+ }
413
+ catch (error) {
414
+ throw retainedFailure(error, ["scratch_verification"]);
415
+ }
416
+ return result.report;
417
+ }