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,542 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import Database from "better-sqlite3";
6
+ import { EvidenceBoundaryError, assertScratchBoundary, readFileIdentity, resolveProspectivePath, sameFileIdentity, } from "./common/boundary.js";
7
+ import { canonicalJson, sha256Bytes, sha256File } from "./common/canonical.js";
8
+ import { assertDatabaseIntegrity, openQueryOnlyDb } from "./common/sqlite.js";
9
+ import { writeApprovedStateJson } from "./common/state.js";
10
+ export const SCRATCH_APPROVAL_VERSION = "wave2-scratch-approval-v1";
11
+ export const SCRATCH_PREPARATION_VERSION = "wave2-scratch-preparation-v2";
12
+ const REPOSITORY_ROOT = path.resolve(fileURLToPath(new URL("../../", import.meta.url)));
13
+ const SHA256_RE = /^[0-9a-f]{64}$/u;
14
+ function isRecord(value) {
15
+ return value !== null && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+ function assertExactKeys(value, expected, code) {
18
+ const actual = Object.keys(value).sort();
19
+ const wanted = [...expected].sort();
20
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
21
+ throw new EvidenceBoundaryError(code);
22
+ }
23
+ }
24
+ function requiredNonEmptyString(value, code) {
25
+ if (typeof value !== "string" || value.length === 0)
26
+ throw new EvidenceBoundaryError(code);
27
+ return value;
28
+ }
29
+ function parseApprovalManifest(value) {
30
+ if (!isRecord(value))
31
+ throw new EvidenceBoundaryError("approval_manifest_invalid");
32
+ assertExactKeys(value, [
33
+ "version",
34
+ "sourceDbPath",
35
+ "scratchDbPath",
36
+ "scratchStatePath",
37
+ "repositories",
38
+ "privateArtifactParentAcknowledged",
39
+ ], "approval_manifest_unknown_or_missing_key");
40
+ if (value.version !== SCRATCH_APPROVAL_VERSION) {
41
+ throw new EvidenceBoundaryError("approval_manifest_version_mismatch");
42
+ }
43
+ if (value.privateArtifactParentAcknowledged !== true) {
44
+ throw new EvidenceBoundaryError("private_artifact_parent_not_acknowledged");
45
+ }
46
+ if (!Array.isArray(value.repositories)) {
47
+ throw new EvidenceBoundaryError("approval_repository_allowlist_invalid");
48
+ }
49
+ const workspaceIds = new Set();
50
+ const reportAliases = new Set();
51
+ const canonicalRepositories = new Set();
52
+ const repositories = value.repositories.map((entry) => {
53
+ if (!isRecord(entry))
54
+ throw new EvidenceBoundaryError("approval_repository_invalid");
55
+ assertExactKeys(entry, ["workspaceId", "owner", "repo", "reportAlias"], "approval_repository_unknown_or_missing_key");
56
+ const parsed = {
57
+ workspaceId: requiredNonEmptyString(entry.workspaceId, "approval_workspace_id_invalid"),
58
+ owner: requiredNonEmptyString(entry.owner, "approval_owner_invalid"),
59
+ repo: requiredNonEmptyString(entry.repo, "approval_repo_invalid"),
60
+ reportAlias: requiredNonEmptyString(entry.reportAlias, "approval_report_alias_invalid"),
61
+ };
62
+ if (workspaceIds.has(parsed.workspaceId)) {
63
+ throw new EvidenceBoundaryError("approval_workspace_duplicate");
64
+ }
65
+ if (reportAliases.has(parsed.reportAlias)) {
66
+ throw new EvidenceBoundaryError("approval_report_alias_duplicate");
67
+ }
68
+ const canonicalRepository = `${parsed.owner.toLowerCase()}/${parsed.repo.toLowerCase()}`;
69
+ if (canonicalRepositories.has(canonicalRepository)) {
70
+ throw new EvidenceBoundaryError("approval_repository_duplicate");
71
+ }
72
+ workspaceIds.add(parsed.workspaceId);
73
+ reportAliases.add(parsed.reportAlias);
74
+ canonicalRepositories.add(canonicalRepository);
75
+ return parsed;
76
+ });
77
+ return {
78
+ version: SCRATCH_APPROVAL_VERSION,
79
+ sourceDbPath: requiredNonEmptyString(value.sourceDbPath, "approval_source_path_invalid"),
80
+ scratchDbPath: requiredNonEmptyString(value.scratchDbPath, "approval_scratch_path_invalid"),
81
+ scratchStatePath: requiredNonEmptyString(value.scratchStatePath, "approval_state_path_invalid"),
82
+ repositories,
83
+ privateArtifactParentAcknowledged: true,
84
+ };
85
+ }
86
+ function comparisonPath(value) {
87
+ const normalized = path.normalize(value).replace(/[\\/]+$/u, "");
88
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
89
+ }
90
+ function isWithin(parent, child) {
91
+ const relative = path.relative(parent, child);
92
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
93
+ }
94
+ function exactFileIdentity(left, right) {
95
+ return left.device === right.device && left.inode === right.inode && left.size === right.size;
96
+ }
97
+ function readCanonicalSourceRepositoryRows(sourceDbPath) {
98
+ const db = openQueryOnlyDb(sourceDbPath);
99
+ try {
100
+ const rows = db
101
+ .prepare(`SELECT workspace_id, repo_owner, repo_name
102
+ FROM workspaces
103
+ ORDER BY workspace_id COLLATE BINARY`)
104
+ .all();
105
+ for (const row of rows) {
106
+ if (typeof row.workspace_id !== "string" ||
107
+ row.workspace_id.length === 0 ||
108
+ (row.repo_owner !== null && typeof row.repo_owner !== "string") ||
109
+ (row.repo_name !== null && typeof row.repo_name !== "string")) {
110
+ throw new EvidenceBoundaryError("source_repository_mapping_invalid");
111
+ }
112
+ }
113
+ return canonicalJson(rows);
114
+ }
115
+ finally {
116
+ db.close();
117
+ }
118
+ }
119
+ function readApproval(approvalManifestPath, approvalSha256, repositoryRoot) {
120
+ if (!SHA256_RE.test(approvalSha256))
121
+ throw new EvidenceBoundaryError("approval_sha256_invalid");
122
+ if (!fs.existsSync(approvalManifestPath)) {
123
+ throw new EvidenceBoundaryError("approval_manifest_missing");
124
+ }
125
+ const resolvedPath = fs.realpathSync.native(approvalManifestPath);
126
+ const stat = fs.statSync(resolvedPath);
127
+ if (!stat.isFile())
128
+ throw new EvidenceBoundaryError("approval_manifest_not_file");
129
+ if (isWithin(repositoryRoot, resolvedPath)) {
130
+ throw new EvidenceBoundaryError("approval_manifest_inside_repository");
131
+ }
132
+ const bytes = fs.readFileSync(resolvedPath);
133
+ if (sha256Bytes(bytes) !== approvalSha256) {
134
+ throw new EvidenceBoundaryError("approval_sha256_mismatch");
135
+ }
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(bytes.toString("utf8"));
139
+ }
140
+ catch {
141
+ throw new EvidenceBoundaryError("approval_manifest_invalid_json");
142
+ }
143
+ return parseApprovalManifest(parsed);
144
+ }
145
+ function assertApprovalPaths(approval, boundary) {
146
+ let approvedSource;
147
+ try {
148
+ approvedSource = fs.realpathSync.native(approval.sourceDbPath);
149
+ }
150
+ catch {
151
+ throw new EvidenceBoundaryError("approval_source_path_unresolvable");
152
+ }
153
+ const approvedScratch = resolveProspectivePath(approval.scratchDbPath);
154
+ const approvedState = resolveProspectivePath(approval.scratchStatePath);
155
+ if (comparisonPath(approvedSource) !== comparisonPath(boundary.sourceDbPath) ||
156
+ comparisonPath(approvedScratch) !== comparisonPath(boundary.scratchDbPath) ||
157
+ comparisonPath(approvedState) !== comparisonPath(boundary.scratchStatePath)) {
158
+ throw new EvidenceBoundaryError("approval_path_binding_mismatch");
159
+ }
160
+ }
161
+ function planApprovedRepoMap(db, approved, permitUnmappedAdditions) {
162
+ const rows = db
163
+ .prepare(`SELECT workspace_id, repo_owner, repo_name
164
+ FROM workspaces
165
+ ORDER BY workspace_id COLLATE BINARY`)
166
+ .all();
167
+ const live = new Map();
168
+ for (const row of rows) {
169
+ if (typeof row.workspace_id !== "string" || row.workspace_id.length === 0) {
170
+ throw new EvidenceBoundaryError("invalid_workspace_identity");
171
+ }
172
+ const ownerMissing = row.repo_owner === null;
173
+ const repoMissing = row.repo_name === null;
174
+ if (ownerMissing !== repoMissing)
175
+ throw new EvidenceBoundaryError("partial_repository_mapping");
176
+ if (live.has(row.workspace_id))
177
+ throw new EvidenceBoundaryError("duplicate_db_workspace");
178
+ if (ownerMissing) {
179
+ live.set(row.workspace_id, null);
180
+ continue;
181
+ }
182
+ if (typeof row.repo_owner !== "string" ||
183
+ row.repo_owner.length === 0 ||
184
+ typeof row.repo_name !== "string" ||
185
+ row.repo_name.length === 0) {
186
+ throw new EvidenceBoundaryError("invalid_repository_mapping");
187
+ }
188
+ live.set(row.workspace_id, { owner: row.repo_owner, repo: row.repo_name });
189
+ }
190
+ const result = [...approved].sort((left, right) => left.workspaceId < right.workspaceId ? -1 : left.workspaceId > right.workspaceId ? 1 : 0);
191
+ const approvedByWorkspace = new Map(result.map((entry) => [entry.workspaceId, entry]));
192
+ const additions = [];
193
+ for (const [workspaceId, dbMapping] of live) {
194
+ const entry = approvedByWorkspace.get(workspaceId);
195
+ if (dbMapping === null) {
196
+ if (entry !== undefined)
197
+ additions.push(entry);
198
+ continue;
199
+ }
200
+ if (entry === undefined || dbMapping.owner !== entry.owner || dbMapping.repo !== entry.repo) {
201
+ throw new EvidenceBoundaryError("repository_allowlist_not_exact");
202
+ }
203
+ }
204
+ for (const entry of result) {
205
+ if (!live.has(entry.workspaceId)) {
206
+ throw new EvidenceBoundaryError("repository_allowlist_unknown_workspace");
207
+ }
208
+ if (live.get(entry.workspaceId) === null && !permitUnmappedAdditions) {
209
+ throw new EvidenceBoundaryError("repository_overlay_verification_failed");
210
+ }
211
+ }
212
+ return { repoMap: result, additions };
213
+ }
214
+ function applyApprovedRepoMapOverlay(partialDbPath, approved, duringMappingOverlay) {
215
+ const db = new Database(partialDbPath, { fileMustExist: true });
216
+ try {
217
+ const plan = planApprovedRepoMap(db, approved, true);
218
+ const update = db.prepare(`UPDATE workspaces
219
+ SET repo_owner = ?, repo_name = ?
220
+ WHERE workspace_id = ? AND repo_owner IS NULL AND repo_name IS NULL`);
221
+ const apply = db.transaction(() => {
222
+ let updatedN = 0;
223
+ for (const entry of plan.additions) {
224
+ const result = update.run(entry.owner, entry.repo, entry.workspaceId);
225
+ if (result.changes !== 1)
226
+ throw new EvidenceBoundaryError("repository_overlay_race");
227
+ const verified = db
228
+ .prepare("SELECT repo_owner, repo_name FROM workspaces WHERE workspace_id = ?")
229
+ .get(entry.workspaceId);
230
+ if (verified?.repo_owner !== entry.owner || verified.repo_name !== entry.repo) {
231
+ throw new EvidenceBoundaryError("repository_overlay_readback_failed");
232
+ }
233
+ updatedN += 1;
234
+ duringMappingOverlay?.(updatedN);
235
+ }
236
+ });
237
+ apply();
238
+ if (db.pragma("journal_mode", { simple: true }) === "wal") {
239
+ const checkpoint = db.pragma("wal_checkpoint(TRUNCATE)");
240
+ if (checkpoint.length !== 1 || checkpoint[0]?.busy !== 0 || checkpoint[0]?.log !== 0) {
241
+ throw new EvidenceBoundaryError("repository_overlay_checkpoint_failed");
242
+ }
243
+ }
244
+ if (db.pragma("journal_mode = DELETE", { simple: true }) !== "delete") {
245
+ throw new EvidenceBoundaryError("repository_overlay_journal_mode_failed");
246
+ }
247
+ }
248
+ finally {
249
+ db.close();
250
+ }
251
+ }
252
+ function reservePartialPath(scratchDbPath) {
253
+ const parent = path.dirname(scratchDbPath);
254
+ for (let attempt = 0; attempt < 10; attempt++) {
255
+ const partialPath = path.join(parent, `.${path.basename(scratchDbPath)}.${process.pid}.${randomUUID()}.partial`);
256
+ try {
257
+ const descriptor = fs.openSync(partialPath, "wx", 0o600);
258
+ fs.closeSync(descriptor);
259
+ return partialPath;
260
+ }
261
+ catch (error) {
262
+ if (error.code !== "EEXIST")
263
+ throw error;
264
+ }
265
+ }
266
+ throw new EvidenceBoundaryError("partial_reservation_failed");
267
+ }
268
+ function retainedFailure(error, fallback) {
269
+ const base = error instanceof EvidenceBoundaryError ? error.code : fallback;
270
+ return new EvidenceBoundaryError(`${base}_partial_retained`);
271
+ }
272
+ function privateArtifactBoundary() {
273
+ return process.platform === "win32"
274
+ ? "WINDOWS_INHERITED_PARENT_ACL_ACKNOWLEDGED"
275
+ : "POSIX_OWNER_ONLY_MODES_REQUESTED";
276
+ }
277
+ export function parseCreateScratchArgs(args) {
278
+ const allowed = new Set([
279
+ "--source-db",
280
+ "--scratch-db",
281
+ "--scratch-state",
282
+ "--approval-manifest",
283
+ "--approval-sha256",
284
+ ]);
285
+ const values = new Map();
286
+ for (let index = 0; index < args.length; index += 2) {
287
+ const flag = args[index];
288
+ const value = args[index + 1];
289
+ if (flag === undefined || !allowed.has(flag)) {
290
+ throw new EvidenceBoundaryError("unknown_cli_argument");
291
+ }
292
+ if (values.has(flag))
293
+ throw new EvidenceBoundaryError("duplicate_cli_argument");
294
+ if (value === undefined || value.length === 0 || value.startsWith("--")) {
295
+ throw new EvidenceBoundaryError("missing_cli_value");
296
+ }
297
+ values.set(flag, value);
298
+ }
299
+ const sourceDbPath = values.get("--source-db");
300
+ const scratchDbPath = values.get("--scratch-db");
301
+ const scratchStatePath = values.get("--scratch-state");
302
+ const approvalManifestPath = values.get("--approval-manifest");
303
+ const approvalSha256 = values.get("--approval-sha256");
304
+ if (sourceDbPath === undefined ||
305
+ scratchDbPath === undefined ||
306
+ scratchStatePath === undefined ||
307
+ approvalManifestPath === undefined ||
308
+ approvalSha256 === undefined) {
309
+ throw new EvidenceBoundaryError("missing_required_cli_argument");
310
+ }
311
+ return {
312
+ sourceDbPath,
313
+ scratchDbPath,
314
+ scratchStatePath,
315
+ approvalManifestPath,
316
+ approvalSha256,
317
+ };
318
+ }
319
+ export async function createApprovedScratchCopy(input, options = {}) {
320
+ const now = options.now ?? new Date();
321
+ if (!Number.isFinite(now.getTime()))
322
+ throw new EvidenceBoundaryError("invalid_clock");
323
+ const repositoryRoot = fs.realpathSync.native(options.repositoryRoot ?? REPOSITORY_ROOT);
324
+ const approval = readApproval(input.approvalManifestPath, input.approvalSha256, repositoryRoot);
325
+ const boundary = assertScratchBoundary({
326
+ sourceDbPath: input.sourceDbPath,
327
+ scratchDbPath: input.scratchDbPath,
328
+ scratchStatePath: input.scratchStatePath,
329
+ repositoryRoot,
330
+ });
331
+ assertApprovalPaths(approval, boundary);
332
+ const sourceShaBefore = await sha256File(boundary.sourceDbPath);
333
+ const sourceRepositoryRowsBefore = readCanonicalSourceRepositoryRows(boundary.sourceDbPath);
334
+ fs.mkdirSync(path.dirname(boundary.scratchDbPath), { recursive: true });
335
+ const partialDbPath = reservePartialPath(boundary.scratchDbPath);
336
+ let backup;
337
+ let source;
338
+ try {
339
+ source = openQueryOnlyDb(boundary.sourceDbPath);
340
+ backup = await source.backup(partialDbPath);
341
+ }
342
+ catch (error) {
343
+ throw retainedFailure(error, "online_backup_failed");
344
+ }
345
+ finally {
346
+ source?.close();
347
+ }
348
+ await options.testHooks?.afterBackup?.(partialDbPath);
349
+ let verifiedSourceIdentity;
350
+ try {
351
+ verifiedSourceIdentity = readFileIdentity(boundary.sourceDbPath);
352
+ }
353
+ catch {
354
+ throw new EvidenceBoundaryError("source_identity_changed_partial_retained");
355
+ }
356
+ if (!exactFileIdentity(boundary.sourceIdentity, verifiedSourceIdentity)) {
357
+ throw new EvidenceBoundaryError("source_identity_changed_partial_retained");
358
+ }
359
+ if (backup.remainingPages !== 0) {
360
+ throw new EvidenceBoundaryError("online_backup_incomplete_partial_retained");
361
+ }
362
+ const reservedPartialIdentity = readFileIdentity(partialDbPath);
363
+ if (sameFileIdentity(verifiedSourceIdentity, reservedPartialIdentity)) {
364
+ throw new EvidenceBoundaryError("partial_aliases_source_partial_retained");
365
+ }
366
+ try {
367
+ applyApprovedRepoMapOverlay(partialDbPath, approval.repositories, options.testHooks?.duringMappingOverlay);
368
+ }
369
+ catch (error) {
370
+ throw retainedFailure(error, "repository_overlay_failed");
371
+ }
372
+ const partialIdentity = readFileIdentity(partialDbPath);
373
+ const firstSha = await sha256File(partialDbPath);
374
+ let repoMap;
375
+ const scratch = openQueryOnlyDb(partialDbPath);
376
+ try {
377
+ assertDatabaseIntegrity(scratch);
378
+ repoMap = planApprovedRepoMap(scratch, approval.repositories, false).repoMap;
379
+ }
380
+ catch (error) {
381
+ throw retainedFailure(error, "scratch_verification_failed");
382
+ }
383
+ finally {
384
+ scratch.close();
385
+ }
386
+ const verifiedSha = await sha256File(partialDbPath);
387
+ if (verifiedSha !== firstSha) {
388
+ throw new EvidenceBoundaryError("scratch_sha_changed_during_verify_partial_retained");
389
+ }
390
+ await options.testHooks?.beforePublish?.(partialDbPath);
391
+ let sourceShaAfter;
392
+ try {
393
+ sourceShaAfter = await sha256File(boundary.sourceDbPath);
394
+ }
395
+ catch {
396
+ throw new EvidenceBoundaryError("source_bytes_changed_partial_retained");
397
+ }
398
+ let finalSourceIdentity;
399
+ try {
400
+ finalSourceIdentity = readFileIdentity(boundary.sourceDbPath);
401
+ }
402
+ catch {
403
+ throw new EvidenceBoundaryError("source_bytes_changed_partial_retained");
404
+ }
405
+ if (sourceShaAfter !== sourceShaBefore ||
406
+ !exactFileIdentity(verifiedSourceIdentity, finalSourceIdentity)) {
407
+ throw new EvidenceBoundaryError("source_bytes_changed_partial_retained");
408
+ }
409
+ let sourceRepositoryRowsAfter;
410
+ try {
411
+ sourceRepositoryRowsAfter = readCanonicalSourceRepositoryRows(boundary.sourceDbPath);
412
+ }
413
+ catch {
414
+ throw new EvidenceBoundaryError("source_mapping_changed_partial_retained");
415
+ }
416
+ if (sourceRepositoryRowsAfter !== sourceRepositoryRowsBefore) {
417
+ throw new EvidenceBoundaryError("source_mapping_changed_partial_retained");
418
+ }
419
+ try {
420
+ fs.linkSync(partialDbPath, boundary.scratchDbPath);
421
+ }
422
+ catch (error) {
423
+ const code = error.code;
424
+ throw new EvidenceBoundaryError(code === "EEXIST"
425
+ ? "scratch_publish_target_exists_partial_retained"
426
+ : "scratch_publish_failed_partial_retained");
427
+ }
428
+ let publishedSha;
429
+ try {
430
+ publishedSha = await sha256File(boundary.scratchDbPath);
431
+ }
432
+ catch {
433
+ throw new EvidenceBoundaryError("published_scratch_hash_failed_partial_and_scratch_retained");
434
+ }
435
+ if (publishedSha !== verifiedSha) {
436
+ throw new EvidenceBoundaryError("published_scratch_sha_mismatch_partial_and_scratch_retained");
437
+ }
438
+ try {
439
+ fs.unlinkSync(partialDbPath);
440
+ }
441
+ catch {
442
+ throw new EvidenceBoundaryError("partial_unlink_failed_partial_and_scratch_retained");
443
+ }
444
+ let scratchIdentity;
445
+ try {
446
+ scratchIdentity = readFileIdentity(boundary.scratchDbPath);
447
+ }
448
+ catch {
449
+ throw new EvidenceBoundaryError("published_scratch_identity_failed_scratch_retained");
450
+ }
451
+ if (!exactFileIdentity(partialIdentity, scratchIdentity)) {
452
+ throw new EvidenceBoundaryError("published_scratch_identity_mismatch_scratch_retained");
453
+ }
454
+ try {
455
+ await options.testHooks?.beforeState?.();
456
+ fs.mkdirSync(path.dirname(boundary.scratchStatePath), { recursive: true });
457
+ fs.mkdirSync(boundary.scratchStatePath, { recursive: false, mode: 0o700 });
458
+ }
459
+ catch {
460
+ throw new EvidenceBoundaryError("scratch_state_create_failed_scratch_published_state_may_be_retained");
461
+ }
462
+ let repoMapWrite;
463
+ try {
464
+ repoMapWrite = writeApprovedStateJson(boundary.scratchStatePath, "repo-map.json", repoMap);
465
+ }
466
+ catch {
467
+ throw new EvidenceBoundaryError("repo_map_write_failed_scratch_published_state_retained");
468
+ }
469
+ const artifactBoundary = privateArtifactBoundary();
470
+ const manifest = {
471
+ version: SCRATCH_PREPARATION_VERSION,
472
+ createdAt: now.toISOString(),
473
+ approval: {
474
+ version: SCRATCH_APPROVAL_VERSION,
475
+ sha256: input.approvalSha256,
476
+ privateArtifactParentAcknowledged: true,
477
+ },
478
+ source: {
479
+ path: boundary.sourceDbPath,
480
+ identity: verifiedSourceIdentity,
481
+ access: "explicit-readonly-online-backup",
482
+ },
483
+ scratch: {
484
+ path: boundary.scratchDbPath,
485
+ identity: scratchIdentity,
486
+ sha256: verifiedSha,
487
+ integrity: "ok",
488
+ publication: "atomic-hard-link-no-replace",
489
+ },
490
+ state: {
491
+ path: boundary.scratchStatePath,
492
+ cleanup: "manual-only",
493
+ privateArtifactBoundary: artifactBoundary,
494
+ },
495
+ repositoryMap: {
496
+ file: "repo-map.json",
497
+ sha256: repoMapWrite.sha256,
498
+ mappedWorkspaceN: repoMap.length,
499
+ },
500
+ backup: { totalPages: backup.totalPages, remainingPages: 0 },
501
+ prohibitedOperations: [
502
+ "migrations",
503
+ "github-reads",
504
+ "transcript-reads",
505
+ "reset-replay-vacuum-prune-delete",
506
+ ],
507
+ };
508
+ let manifestWrite;
509
+ try {
510
+ await options.testHooks?.beforeManifestWrite?.();
511
+ manifestWrite = writeApprovedStateJson(boundary.scratchStatePath, "preparation-manifest.json", manifest);
512
+ }
513
+ catch {
514
+ throw new EvidenceBoundaryError("manifest_write_failed_scratch_published_state_retained");
515
+ }
516
+ return {
517
+ scratchDbSha256: verifiedSha,
518
+ repoMapSha256: repoMapWrite.sha256,
519
+ manifestSha256: manifestWrite.sha256,
520
+ approvalSha256: input.approvalSha256,
521
+ mappedWorkspaceN: repoMap.length,
522
+ totalPages: backup.totalPages,
523
+ privateArtifactBoundary: artifactBoundary,
524
+ };
525
+ }
526
+ async function main() {
527
+ try {
528
+ const result = await createApprovedScratchCopy(parseCreateScratchArgs(process.argv.slice(2)));
529
+ process.stdout.write(`${canonicalJson({ status: "CREATED", ...result })}\n`);
530
+ }
531
+ catch (error) {
532
+ const code = error instanceof EvidenceBoundaryError ? error.code : "unexpected_failure";
533
+ process.stderr.write(`${canonicalJson({ status: "REFUSED", failure: code })}\n`);
534
+ process.exitCode = 1;
535
+ }
536
+ }
537
+ const invokedPath = process.argv[1];
538
+ if (invokedPath !== undefined &&
539
+ pathToFileURL(path.resolve(invokedPath)).href ===
540
+ pathToFileURL(fileURLToPath(import.meta.url)).href) {
541
+ void main();
542
+ }
@@ -0,0 +1,113 @@
1
+ import * as path from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import { loadApprovedEvidenceInput, publishApprovedOutput, } from "../common/approved-input.js";
4
+ import { EvidenceBoundaryError } from "../common/boundary.js";
5
+ import { canonicalJson } from "../common/canonical.js";
6
+ import { assertD7AsOf, measureD7ForwardCoverage } from "./measure.js";
7
+ const FLAGS = [
8
+ "approval-manifest",
9
+ "approval-sha256",
10
+ "scratch-preparation-manifest",
11
+ "scratch-preparation-sha256",
12
+ "scratch-db",
13
+ "approved-scratch-db-sha256",
14
+ "repo-map",
15
+ "repo-map-sha256",
16
+ "scratch-state",
17
+ "scratch-verification",
18
+ "live-db",
19
+ "repository-root",
20
+ "source-commit",
21
+ "as-of",
22
+ "window-days",
23
+ "out",
24
+ ];
25
+ function fail(code) {
26
+ throw new Error(code);
27
+ }
28
+ function parse(argv) {
29
+ if (argv.length !== FLAGS.length * 2)
30
+ fail("d7_cli_argument_missing");
31
+ const values = new Map();
32
+ for (let i = 0; i < argv.length; i += 2) {
33
+ const flag = argv[i];
34
+ const value = argv[i + 1];
35
+ if (flag === undefined || value === undefined || !flag.startsWith("--") || value.length === 0)
36
+ fail("d7_cli_argument_invalid");
37
+ const key = flag.slice(2);
38
+ if (!FLAGS.includes(key) || values.has(key))
39
+ fail("d7_cli_argument_unknown_or_duplicate");
40
+ values.set(key, value);
41
+ }
42
+ if (values.size !== FLAGS.length)
43
+ fail("d7_cli_argument_missing");
44
+ return values;
45
+ }
46
+ function required(values, key) {
47
+ return values.get(key) ?? fail("d7_cli_argument_missing");
48
+ }
49
+ function approved(values) {
50
+ return {
51
+ approvalManifestPath: required(values, "approval-manifest"),
52
+ approvalManifestSha256: required(values, "approval-sha256"),
53
+ preparationManifestPath: required(values, "scratch-preparation-manifest"),
54
+ preparationManifestSha256: required(values, "scratch-preparation-sha256"),
55
+ scratchDbPath: required(values, "scratch-db"),
56
+ scratchDbSha256: required(values, "approved-scratch-db-sha256"),
57
+ repoMapPath: required(values, "repo-map"),
58
+ repoMapSha256: required(values, "repo-map-sha256"),
59
+ scratchStatePath: required(values, "scratch-state"),
60
+ scratchVerificationPath: required(values, "scratch-verification"),
61
+ liveDbPath: required(values, "live-db"),
62
+ repositoryRoot: required(values, "repository-root"),
63
+ };
64
+ }
65
+ export async function runD7CoverageCli(argv, dependencies = {}) {
66
+ const stdout = dependencies.stdout ?? ((line) => process.stdout.write(`${line}\n`));
67
+ const stderr = dependencies.stderr ?? ((line) => process.stderr.write(`${line}\n`));
68
+ try {
69
+ const values = parse(argv);
70
+ if (required(values, "window-days") !== "30")
71
+ fail("d7_window_days_invalid");
72
+ const sourceCommit = required(values, "source-commit");
73
+ if (!/^[0-9a-f]{40}$/u.test(sourceCommit))
74
+ fail("d7_source_commit_invalid");
75
+ assertD7AsOf(required(values, "as-of"));
76
+ const loaded = await (dependencies.loadApproved ?? loadApprovedEvidenceInput)(approved(values));
77
+ const db = loaded.openVerifiedScratchDb();
78
+ try {
79
+ const report = measureD7ForwardCoverage({
80
+ db,
81
+ repositories: loaded.repositories,
82
+ scratchDbSha256: loaded.scratchDbSha256,
83
+ repoMapSha256: loaded.repoMapSha256,
84
+ sourceCommit,
85
+ asOf: required(values, "as-of"),
86
+ windowDays: 30,
87
+ });
88
+ const publication = (dependencies.publish ?? publishApprovedOutput)(loaded, required(values, "out"), `${canonicalJson(report)}\n`);
89
+ stdout(canonicalJson({ status: report.status, reportSha256: publication.sha256 }));
90
+ }
91
+ finally {
92
+ db.close();
93
+ }
94
+ return 0;
95
+ }
96
+ catch (error) {
97
+ const message = error instanceof Error ? error.message : "unexpected_failure";
98
+ stderr(canonicalJson({
99
+ status: "REFUSED",
100
+ failure: error instanceof EvidenceBoundaryError || /^[a-z0-9_]+$/u.test(message)
101
+ ? message
102
+ : "unexpected_failure",
103
+ }));
104
+ return 1;
105
+ }
106
+ }
107
+ const invokedPath = process.argv[1];
108
+ if (invokedPath !== undefined &&
109
+ pathToFileURL(path.resolve(invokedPath)).href ===
110
+ pathToFileURL(fileURLToPath(import.meta.url)).href)
111
+ void runD7CoverageCli(process.argv.slice(2)).then((code) => {
112
+ process.exitCode = code;
113
+ });