@tea-agent/loop-agent 0.16.0 → 0.16.1-beta.1

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 (72) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +7 -51
  3. package/README.md +1 -1
  4. package/dist/application/evaluation/candidate.js +6 -23
  5. package/dist/application/evaluation/types.js +0 -522
  6. package/dist/cli/command-definitions.js +3 -19
  7. package/dist/commands/eval.js +13 -1176
  8. package/dist/commands/init.js +1 -4
  9. package/dist/executors/dag-pi-executor.js +44 -4
  10. package/dist/worker/delivery/verification-bundle.js +2 -13
  11. package/dist/worker/feature/review.js +2 -3
  12. package/dist/worker/outcomes/adapters.js +1 -4
  13. package/dist/worker/outcomes/gate.js +11 -10
  14. package/dist/worker/outcomes/projector.js +1 -10
  15. package/dist/worker/outcomes/types.js +0 -3
  16. package/dist/worker/run-task/run-task.js +4 -23
  17. package/dist/worker/task-graph/ready-planner.js +8 -14
  18. package/dist/worker/task-graph/task-graph-schema.js +3 -5
  19. package/dist/workflows/dag/frontend-implementation-contract.js +77 -0
  20. package/dist/workflows/dag/init-hybrid.js +47 -3
  21. package/dist/workflows/dag/knowledge-curator.js +0 -3
  22. package/dist/workflows/dag/node-execution.js +4 -11
  23. package/dist/workflows/dag/prompt.js +1 -1
  24. package/dist/workflows/dag/runner.js +16 -43
  25. package/dist/workflows/dag/skill-snapshot.js +7 -11
  26. package/dist/workflows/dag/types.js +1 -18
  27. package/docs/init-surface.manifest.json +0 -3
  28. package/docs/templates/agent-dag.schema.json +5 -0
  29. package/docs/templates/product-line/AGENTS.md +0 -1
  30. package/docs/templates/product-line/README.md +0 -17
  31. package/docs/templates/product-line/acceptance.yaml +0 -9
  32. package/docs/templates/product-line/task-graph.yaml +0 -8
  33. package/docs/templates/product-line/task.yaml +0 -4
  34. package/package.json +1 -1
  35. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  36. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  37. package/dist/application/evaluation/alias.js +0 -184
  38. package/dist/application/evaluation/budget.js +0 -192
  39. package/dist/application/evaluation/campaign-hash.js +0 -47
  40. package/dist/application/evaluation/campaign-matrix.js +0 -372
  41. package/dist/application/evaluation/campaign-scorecard.js +0 -135
  42. package/dist/application/evaluation/campaign.js +0 -370
  43. package/dist/application/evaluation/corpus-hash.js +0 -38
  44. package/dist/application/evaluation/corpus.js +0 -56
  45. package/dist/application/evaluation/experiment.js +0 -294
  46. package/dist/application/evaluation/ignition.js +0 -198
  47. package/dist/application/evaluation/integrity-audit.js +0 -162
  48. package/dist/application/evaluation/outer-loop.js +0 -132
  49. package/dist/application/evaluation/pi-cell-executor.js +0 -39
  50. package/dist/application/evaluation/private-verifier.js +0 -46
  51. package/dist/application/evaluation/promotion-policy.js +0 -151
  52. package/dist/application/evaluation/proposer.js +0 -98
  53. package/dist/infrastructure/evaluation/alias-store.js +0 -199
  54. package/dist/infrastructure/evaluation/campaign-store.js +0 -154
  55. package/dist/infrastructure/evaluation/corpus-store.js +0 -181
  56. package/dist/infrastructure/evaluation/experiment-store.js +0 -124
  57. package/dist/infrastructure/evaluation/ignition-store.js +0 -82
  58. package/dist/infrastructure/evaluation/private-verifier-store.js +0 -145
  59. package/dist/infrastructure/evaluation/proposer-store.js +0 -78
  60. package/dist/worker/outcomes/evidence-tokens.js +0 -29
  61. package/dist/workflows/dag/budget-enforcement.js +0 -67
  62. package/dist/workflows/dag/context-policy.js +0 -137
  63. package/docs/templates/evaluation/campaign-budget-v1.json +0 -12
  64. package/docs/templates/evaluation/campaign-dogfood-v0.json +0 -24
  65. package/docs/templates/evaluation/campaign-evidence-v1.json +0 -44
  66. package/docs/templates/evaluation/context-policy-baseline-v1.json +0 -17
  67. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +0 -28
  68. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +0 -118
  69. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +0 -21
  70. package/docs/templates/evaluation/matrix-fixture-v1.json +0 -10
  71. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +0 -16
  72. package/docs/templates/product-line/feature.yaml +0 -11
@@ -1,135 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { readCampaignEvidence, readCampaignManifest, readCampaignPlan, readCampaignScorecard, writeCampaignScorecard, } from "../../infrastructure/evaluation/campaign-store.js";
4
- import { evaluatePromotionPolicy } from "./promotion-policy.js";
5
- import { campaignEvidenceSchema, } from "./types.js";
6
- export async function loadCampaignEvidence(input) {
7
- const absolute = path.resolve(input.repoRoot, input.evidencePath);
8
- const raw = JSON.parse(await readFile(absolute, "utf-8"));
9
- return campaignEvidenceSchema.parse(raw);
10
- }
11
- export async function buildCampaignScorecard(input) {
12
- const manifest = await readCampaignManifest(input.repoRoot, input.campaignId);
13
- const plan = await readCampaignPlan(input.repoRoot, input.campaignId);
14
- if (!plan) {
15
- throw new Error(`campaign ${input.campaignId} has no plan; run eval campaign plan first`);
16
- }
17
- if (input.evidence.campaignId !== input.campaignId) {
18
- throw new Error(`evidence campaignId mismatch: expected ${input.campaignId}, got ${input.evidence.campaignId}`);
19
- }
20
- const policy = evaluatePromotionPolicy({
21
- campaignId: input.campaignId,
22
- incumbentCandidateId: manifest.incumbentCandidateId,
23
- evidence: input.evidence,
24
- acknowledgeReview: input.acknowledgeReview,
25
- });
26
- const scorecard = {
27
- schemaVersion: 1,
28
- campaignId: input.campaignId,
29
- planHash: plan.planHash,
30
- createdAt: new Date().toISOString(),
31
- evidenceCellCount: input.evidence.cells.length,
32
- policy,
33
- };
34
- if (input.persist !== false) {
35
- await writeCampaignScorecard({
36
- repoRoot: input.repoRoot,
37
- scorecard,
38
- evidence: input.evidence,
39
- });
40
- }
41
- return scorecard;
42
- }
43
- export async function auditCampaignEvidence(input) {
44
- const scorecard = await buildCampaignScorecard({
45
- ...input,
46
- persist: false,
47
- });
48
- return {
49
- findings: scorecard.policy.findings,
50
- policy: scorecard.policy,
51
- };
52
- }
53
- export async function requireCampaignPromotionGate(input) {
54
- let evidence;
55
- if (input.evidencePath) {
56
- evidence = await loadCampaignEvidence({
57
- repoRoot: input.repoRoot,
58
- evidencePath: input.evidencePath,
59
- });
60
- }
61
- else {
62
- evidence = await readCampaignEvidence(input.repoRoot, input.campaignId);
63
- }
64
- let scorecard;
65
- if (evidence) {
66
- scorecard = await buildCampaignScorecard({
67
- repoRoot: input.repoRoot,
68
- campaignId: input.campaignId,
69
- evidence,
70
- acknowledgeReview: input.acknowledgeReview,
71
- persist: true,
72
- });
73
- }
74
- else {
75
- const existing = await readCampaignScorecard(input.repoRoot, input.campaignId);
76
- if (!existing) {
77
- throw new Error(`campaign ${input.campaignId} has no scorecard; run eval campaign scorecard --evidence <path> first`);
78
- }
79
- if (input.acknowledgeReview &&
80
- existing.policy.gates.safetyOk === "review" &&
81
- !existing.policy.promotionEligible) {
82
- throw new Error(`campaign ${input.campaignId} scorecard needs re-evaluation with evidence; provide --evidence or re-run scorecard`);
83
- }
84
- scorecard = existing;
85
- }
86
- if (scorecard.policy.challengerCandidateId !== input.toCandidateId) {
87
- throw new Error(`promotion target mismatch: scorecard challenger=${scorecard.policy.challengerCandidateId}, --to-candidate=${input.toCandidateId}`);
88
- }
89
- if (!scorecard.policy.promotionEligible) {
90
- throw new Error(`campaign promotion policy rejected: ${scorecard.policy.reasons.join("; ")}`);
91
- }
92
- return scorecard;
93
- }
94
- export function formatCampaignScorecardMarkdown(scorecard) {
95
- const { policy } = scorecard;
96
- const lines = [
97
- `# Campaign Scorecard: ${scorecard.campaignId}`,
98
- "",
99
- `- planHash: \`${scorecard.planHash}\``,
100
- `- createdAt: \`${scorecard.createdAt}\``,
101
- `- evidenceCells: ${scorecard.evidenceCellCount}`,
102
- `- policy: \`${policy.policyVersion}\``,
103
- `- promotionEligible: **${policy.promotionEligible}**`,
104
- "",
105
- "## Gates",
106
- "",
107
- `- privateImprovement: \`${policy.gates.privateImprovement}\``,
108
- `- heldOutNonRegression: \`${policy.gates.heldOutNonRegression}\``,
109
- `- budgetOk: \`${policy.gates.budgetOk}\``,
110
- `- safetyOk: \`${policy.gates.safetyOk}\``,
111
- `- humanRequired: \`${policy.gates.humanRequired}\``,
112
- "",
113
- "## Paired tallies",
114
- "",
115
- `- private: challengerWins=${policy.paired.private.challengerWins} incumbentWins=${policy.paired.private.incumbentWins} ties=${policy.paired.private.ties}`,
116
- `- held_out: challengerWins=${policy.paired.held_out.challengerWins} incumbentWins=${policy.paired.held_out.incumbentWins} ties=${policy.paired.held_out.ties}`,
117
- `- public: challengerWins=${policy.paired.public.challengerWins} incumbentWins=${policy.paired.public.incumbentWins} ties=${policy.paired.public.ties}`,
118
- "",
119
- ];
120
- if (policy.findings.length > 0) {
121
- lines.push("## Integrity findings", "");
122
- for (const finding of policy.findings) {
123
- lines.push(`- [${finding.severity}] \`${finding.code}\`${finding.path ? ` @ ${finding.path}` : ""}: ${finding.message}`);
124
- }
125
- lines.push("");
126
- }
127
- if (policy.reasons.length > 0) {
128
- lines.push("## Reasons", "");
129
- for (const reason of policy.reasons) {
130
- lines.push(`- ${reason}`);
131
- }
132
- lines.push("");
133
- }
134
- return `${lines.join("\n")}\n`;
135
- }
@@ -1,370 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { campaignBudgetSchema } from "./budget.js";
5
- import { computeBudgetHash, computeCampaignPlanHash, computeScoringRulesHash, } from "./campaign-hash.js";
6
- import { showCandidate } from "./candidate.js";
7
- import { showCorpus } from "./corpus.js";
8
- import { listCampaignIds, readCampaignManifest, readCampaignPlan, registerCampaignManifest, writeCampaignPlan, } from "../../infrastructure/evaluation/campaign-store.js";
9
- import { readPrivateVerifierManifest } from "../../infrastructure/evaluation/private-verifier-store.js";
10
- import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
11
- import { campaignManifestInputSchema, campaignManifestSchema, } from "./types.js";
12
- const EXECUTABLE_LIFECYCLES = new Set([
13
- "eligible",
14
- "experimenting",
15
- "accepted",
16
- ]);
17
- function cellId(parts) {
18
- const safeTask = parts.taskRef.replace(/[^A-Za-z0-9._-]+/g, "_");
19
- return `${parts.candidateId}__${parts.split}__${safeTask}__s${parts.seed}`;
20
- }
21
- export function collectPrivateVerifierSecrets(manifest) {
22
- const secrets = new Set();
23
- secrets.add(manifest.privateVerifierId);
24
- secrets.add(manifest.manifestHash);
25
- for (const check of manifest.checks) {
26
- secrets.add(check.checkId);
27
- for (const token of check.command) {
28
- if (token.trim().length >= 3)
29
- secrets.add(token);
30
- }
31
- }
32
- return [...secrets];
33
- }
34
- export async function assertCandidateDoesNotLeakPrivateVerifier(input) {
35
- const record = await showCandidate({
36
- repoRoot: input.repoRoot,
37
- candidateId: input.candidateId,
38
- });
39
- const secrets = collectPrivateVerifierSecrets(input.privateVerifier);
40
- for (const ref of record.manifest.contentRefs) {
41
- const absolute = path.resolve(input.repoRoot, ref.path);
42
- const bytes = await readFile(absolute, "utf-8");
43
- for (const secret of secrets) {
44
- if (bytes.includes(secret)) {
45
- throw new Error(`private verifier leak: candidate ${input.candidateId} contentRef ${ref.path} contains private token`);
46
- }
47
- }
48
- }
49
- }
50
- export async function verifyCampaignIntegrity(repoRoot, manifest) {
51
- const corpus = await showCorpus({
52
- repoRoot,
53
- corpusId: manifest.corpusId,
54
- });
55
- if (corpus.manifest.corpusHash !== manifest.corpusHash) {
56
- throw new Error(`campaign corpusHash drifted: declared ${manifest.corpusHash}, registry ${corpus.manifest.corpusHash}`);
57
- }
58
- const privateVerifier = await readPrivateVerifierManifest(repoRoot, manifest.privateVerifierId);
59
- if (privateVerifier.manifestHash !== manifest.privateVerifierManifestHash) {
60
- throw new Error(`campaign privateVerifierManifestHash drifted: declared ${manifest.privateVerifierManifestHash}, registry ${privateVerifier.manifestHash}`);
61
- }
62
- const budgetHash = computeBudgetHash(manifest.budget);
63
- if (budgetHash !== manifest.evaluatorIntegrity.budgetHash) {
64
- throw new Error(`campaign budgetHash drifted: declared ${manifest.evaluatorIntegrity.budgetHash}, recomputed ${budgetHash}`);
65
- }
66
- if (corpus.manifest.corpusHash !== manifest.evaluatorIntegrity.corpusHash) {
67
- throw new Error("campaign evaluatorIntegrity.corpusHash mismatch");
68
- }
69
- if (privateVerifier.manifestHash !==
70
- manifest.evaluatorIntegrity.privateVerifierManifestHash) {
71
- throw new Error("campaign evaluatorIntegrity.privateVerifierManifestHash mismatch");
72
- }
73
- const scoringRulesHash = computeScoringRulesHash(manifest.policyVersion);
74
- if (scoringRulesHash !== manifest.evaluatorIntegrity.scoringRulesHash) {
75
- throw new Error(`campaign scoringRulesHash drifted for policyVersion ${manifest.policyVersion}`);
76
- }
77
- }
78
- export async function buildCampaignPlan(input) {
79
- await verifyCampaignIntegrity(input.repoRoot, input.manifest);
80
- const corpus = await showCorpus({
81
- repoRoot: input.repoRoot,
82
- corpusId: input.manifest.corpusId,
83
- });
84
- const privateVerifier = await readPrivateVerifierManifest(input.repoRoot, input.manifest.privateVerifierId);
85
- const candidateIds = [
86
- input.manifest.incumbentCandidateId,
87
- ...input.manifest.challengerCandidateIds,
88
- ];
89
- const cells = [];
90
- for (const candidateId of candidateIds) {
91
- await assertCandidateDoesNotLeakPrivateVerifier({
92
- repoRoot: input.repoRoot,
93
- candidateId,
94
- privateVerifier,
95
- });
96
- const record = await showCandidate({
97
- repoRoot: input.repoRoot,
98
- candidateId,
99
- });
100
- if (!EXECUTABLE_LIFECYCLES.has(record.status)) {
101
- throw new Error(`campaign candidate ${candidateId} lifecycle ${record.status} is not executable`);
102
- }
103
- for (const task of corpus.manifest.tasks) {
104
- const seeds = input.manifest.seeds && input.manifest.seeds.length > 0
105
- ? input.manifest.seeds.filter((seed) => task.seeds.includes(seed))
106
- : task.seeds;
107
- if (seeds.length === 0) {
108
- throw new Error(`campaign seeds do not intersect task ${task.taskRef} seeds`);
109
- }
110
- for (const seed of seeds) {
111
- const split = task.split;
112
- const executionRole = split === "public"
113
- ? "candidate_run"
114
- : split === "private"
115
- ? "private_verify"
116
- : "promotion_gate_only";
117
- cells.push({
118
- cellId: cellId({
119
- candidateId,
120
- split,
121
- taskRef: task.taskRef,
122
- seed,
123
- }),
124
- candidateId,
125
- bundleHash: record.manifest.bundleHash,
126
- taskRef: task.taskRef,
127
- seed,
128
- split,
129
- executionRole,
130
- privateVerifierAttached: executionRole === "private_verify",
131
- });
132
- }
133
- }
134
- }
135
- cells.sort((a, b) => (a.cellId < b.cellId ? -1 : a.cellId > b.cellId ? 1 : 0));
136
- const planHash = computeCampaignPlanHash(input.manifest.campaignId, cells);
137
- return {
138
- schemaVersion: 1,
139
- campaignId: input.manifest.campaignId,
140
- planHash,
141
- createdAt: new Date().toISOString(),
142
- cells,
143
- };
144
- }
145
- export async function materializeCampaignManifest(input) {
146
- const parsed = campaignManifestInputSchema.parse(input.raw);
147
- const budget = campaignBudgetSchema.parse(parsed.budget);
148
- if (budget.mode !== "hard") {
149
- throw new Error("campaign budget.mode must be hard");
150
- }
151
- const corpus = await showCorpus({
152
- repoRoot: input.repoRoot,
153
- corpusId: parsed.corpusId,
154
- });
155
- if (corpus.manifest.corpusHash !== parsed.corpusHash) {
156
- throw new Error(`corpusHash mismatch: declared ${parsed.corpusHash}, registry ${corpus.manifest.corpusHash}`);
157
- }
158
- const privateVerifier = await readPrivateVerifierManifest(input.repoRoot, parsed.privateVerifierId);
159
- if (privateVerifier.manifestHash !== parsed.privateVerifierManifestHash) {
160
- throw new Error(`privateVerifierManifestHash mismatch: declared ${parsed.privateVerifierManifestHash}, registry ${privateVerifier.manifestHash}`);
161
- }
162
- const candidateIds = [
163
- parsed.incumbentCandidateId,
164
- ...parsed.challengerCandidateIds,
165
- ];
166
- for (const candidateId of candidateIds) {
167
- const record = await showCandidate({
168
- repoRoot: input.repoRoot,
169
- candidateId,
170
- });
171
- if (!EXECUTABLE_LIFECYCLES.has(record.status)) {
172
- throw new Error(`campaign candidate ${candidateId} lifecycle ${record.status} is not executable`);
173
- }
174
- await assertCandidateDoesNotLeakPrivateVerifier({
175
- repoRoot: input.repoRoot,
176
- candidateId,
177
- privateVerifier,
178
- });
179
- }
180
- const identity = resolveRunningControllerIdentity();
181
- const controllerFingerprint = parsed.controllerFingerprint ??
182
- (identity
183
- ? `${identity.packageName}@${identity.packageVersion}+${identity.packageFingerprint.value}`
184
- : "unresolved-controller");
185
- const evaluatorIntegrity = {
186
- corpusHash: corpus.manifest.corpusHash,
187
- privateVerifierManifestHash: privateVerifier.manifestHash,
188
- budgetHash: computeBudgetHash(budget),
189
- scoringRulesHash: computeScoringRulesHash(parsed.policyVersion),
190
- };
191
- if (parsed.evaluatorIntegrity) {
192
- if (JSON.stringify(parsed.evaluatorIntegrity) !==
193
- JSON.stringify(evaluatorIntegrity)) {
194
- throw new Error("evaluatorIntegrity mismatch with recomputed corpus/privateVerifier/budget/scoring hashes");
195
- }
196
- }
197
- return campaignManifestSchema.parse({
198
- ...parsed,
199
- budget,
200
- controllerFingerprint,
201
- evaluatorIntegrity,
202
- });
203
- }
204
- export async function createCampaign(input) {
205
- const resolved = path.isAbsolute(input.manifestPath)
206
- ? input.manifestPath
207
- : path.resolve(input.repoRoot, input.manifestPath);
208
- const raw = JSON.parse(await readFile(resolved, "utf-8"));
209
- const manifest = await materializeCampaignManifest({
210
- repoRoot: input.repoRoot,
211
- raw,
212
- });
213
- const registered = await registerCampaignManifest({
214
- repoRoot: input.repoRoot,
215
- manifest,
216
- });
217
- const plan = await buildCampaignPlan({
218
- repoRoot: input.repoRoot,
219
- manifest: registered.manifest,
220
- });
221
- const planPath = await writeCampaignPlan({
222
- repoRoot: input.repoRoot,
223
- plan,
224
- });
225
- return {
226
- manifest: registered.manifest,
227
- plan,
228
- idempotent: registered.idempotent,
229
- manifestPath: registered.manifestPath,
230
- planPath,
231
- };
232
- }
233
- export async function validateCampaign(input) {
234
- const resolved = path.isAbsolute(input.manifestPath)
235
- ? input.manifestPath
236
- : path.resolve(input.repoRoot, input.manifestPath);
237
- const raw = JSON.parse(await readFile(resolved, "utf-8"));
238
- const manifest = await materializeCampaignManifest({
239
- repoRoot: input.repoRoot,
240
- raw,
241
- });
242
- const plan = await buildCampaignPlan({
243
- repoRoot: input.repoRoot,
244
- manifest,
245
- });
246
- return { manifest, plan };
247
- }
248
- export async function showCampaign(input) {
249
- const manifest = await readCampaignManifest(input.repoRoot, input.campaignId);
250
- await verifyCampaignIntegrity(input.repoRoot, manifest);
251
- const plan = await readCampaignPlan(input.repoRoot, input.campaignId);
252
- return {
253
- manifest,
254
- plan,
255
- summary: summarizeCampaign(manifest, plan),
256
- };
257
- }
258
- export async function listCampaigns(input) {
259
- const ids = await listCampaignIds(input.repoRoot);
260
- const rows = [];
261
- for (const campaignId of ids) {
262
- const manifest = await readCampaignManifest(input.repoRoot, campaignId);
263
- const plan = await readCampaignPlan(input.repoRoot, campaignId);
264
- rows.push(summarizeCampaign(manifest, plan));
265
- }
266
- return rows;
267
- }
268
- export async function planCampaign(input) {
269
- const manifest = await readCampaignManifest(input.repoRoot, input.campaignId);
270
- const plan = await buildCampaignPlan({
271
- repoRoot: input.repoRoot,
272
- manifest,
273
- });
274
- const existing = await readCampaignPlan(input.repoRoot, input.campaignId);
275
- if (existing && existing.planHash !== plan.planHash) {
276
- throw new Error(`campaign planHash drifted for ${input.campaignId}: stored ${existing.planHash}, recomputed ${plan.planHash}`);
277
- }
278
- const planPath = await writeCampaignPlan({
279
- repoRoot: input.repoRoot,
280
- plan: existing ?? plan,
281
- });
282
- return { manifest, plan: existing ?? plan, planPath };
283
- }
284
- /**
285
- * M4 campaign run: re-verify integrity, ensure plan repeatability, and execute
286
- * controller-only private verifier checks. Does not run Pi candidate DAGs and
287
- * never auto-promotes.
288
- */
289
- export async function runCampaignPrivateChecks(input) {
290
- const planned = await planCampaign(input);
291
- const privateVerifier = await readPrivateVerifierManifest(input.repoRoot, planned.manifest.privateVerifierId);
292
- const results = [];
293
- for (const cell of planned.plan.cells) {
294
- if (cell.executionRole !== "private_verify")
295
- continue;
296
- if (!cell.privateVerifierAttached) {
297
- throw new Error(`private_verify cell ${cell.cellId} missing privateVerifierAttached`);
298
- }
299
- for (const check of privateVerifier.checks) {
300
- const cwd = path.resolve(input.repoRoot, check.cwd);
301
- const [bin, ...args] = check.command;
302
- const spawned = spawnSync(bin, args, {
303
- cwd,
304
- encoding: "utf-8",
305
- timeout: check.timeoutMs,
306
- env: process.env,
307
- });
308
- const exitCode = typeof spawned.status === "number" ? spawned.status : null;
309
- results.push({
310
- cellId: cell.cellId,
311
- checkId: check.checkId,
312
- ok: exitCode === check.expectedExitCode,
313
- exitCode,
314
- stderr: (spawned.stderr ?? "").slice(0, 2_000),
315
- });
316
- }
317
- }
318
- return {
319
- manifest: planned.manifest,
320
- plan: planned.plan,
321
- results,
322
- };
323
- }
324
- function summarizeCampaign(manifest, plan) {
325
- return {
326
- campaignId: manifest.campaignId,
327
- status: manifest.status,
328
- corpusId: manifest.corpusId,
329
- incumbentCandidateId: manifest.incumbentCandidateId,
330
- challengerCount: manifest.challengerCandidateIds.length,
331
- ...(plan
332
- ? { cellCount: plan.cells.length, planHash: plan.planHash }
333
- : {}),
334
- };
335
- }
336
- export function formatCampaignMarkdown(input) {
337
- const { manifest, plan } = input;
338
- const lines = [
339
- `# Campaign: ${manifest.campaignId}`,
340
- "",
341
- `- status: \`${manifest.status}\``,
342
- `- corpus: \`${manifest.corpusId}\` (\`${manifest.corpusHash}\`)`,
343
- `- incumbent: \`${manifest.incumbentCandidateId}\``,
344
- `- challengers: ${manifest.challengerCandidateIds.map((id) => `\`${id}\``).join(", ")}`,
345
- `- privateVerifier: \`${manifest.privateVerifierId}\``,
346
- `- budget.mode: \`${manifest.budget.mode}\``,
347
- `- policyVersion: \`${manifest.policyVersion}\``,
348
- "",
349
- "## Evaluator integrity",
350
- "",
351
- `- corpusHash: \`${manifest.evaluatorIntegrity.corpusHash}\``,
352
- `- privateVerifierManifestHash: \`${manifest.evaluatorIntegrity.privateVerifierManifestHash}\``,
353
- `- budgetHash: \`${manifest.evaluatorIntegrity.budgetHash}\``,
354
- `- scoringRulesHash: \`${manifest.evaluatorIntegrity.scoringRulesHash}\``,
355
- "",
356
- "> M4: private verifier is controller-only; held_out cells are promotion_gate_only; promotion remains human-gated.",
357
- "",
358
- ];
359
- if (plan) {
360
- const counts = {
361
- candidate_run: 0,
362
- private_verify: 0,
363
- promotion_gate_only: 0,
364
- };
365
- for (const cell of plan.cells)
366
- counts[cell.executionRole] += 1;
367
- lines.push(`## Plan`, "", `- planHash: \`${plan.planHash}\``, `- cells: ${plan.cells.length} (candidate_run=${counts.candidate_run}, private_verify=${counts.private_verify}, promotion_gate_only=${counts.promotion_gate_only})`, "");
368
- }
369
- return `${lines.join("\n")}\n`;
370
- }
@@ -1,38 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { formatContentSha, normalizeContentSha, stableStringify, } from "./candidate-hash.js";
3
- function normalizeTask(task) {
4
- return {
5
- taskRef: task.taskRef.replace(/\\/g, "/"),
6
- class: task.class,
7
- split: task.split,
8
- seeds: [...task.seeds].sort((a, b) => a - b),
9
- featureId: task.featureId ?? null,
10
- };
11
- }
12
- /**
13
- * Canonical payload for corpusHash.
14
- * Excludes corpusId, createdAt, description, corpusHash, and free-text notes.
15
- */
16
- export function buildCanonicalCorpusPayload(manifest) {
17
- const tasks = manifest.tasks
18
- .map((task) => normalizeTask(task))
19
- .sort((a, b) => (a.taskRef < b.taskRef ? -1 : a.taskRef > b.taskRef ? 1 : 0));
20
- return {
21
- schemaVersion: 1,
22
- tasks,
23
- };
24
- }
25
- export function computeCorpusHash(manifest) {
26
- const payload = buildCanonicalCorpusPayload(manifest);
27
- const digest = createHash("sha256")
28
- .update(stableStringify(payload))
29
- .digest("hex");
30
- return formatContentSha(digest);
31
- }
32
- export function assertCorpusHashMatches(manifest) {
33
- const expected = computeCorpusHash(manifest);
34
- const actual = formatContentSha(normalizeContentSha(manifest.corpusHash));
35
- if (expected !== actual) {
36
- throw new Error(`corpusHash mismatch: expected ${expected}, got ${actual}`);
37
- }
38
- }
@@ -1,56 +0,0 @@
1
- import { loadCorpusManifestFromPath, listCorpusIds, materializeCorpusManifest, readCorpusManifest, registerCorpusManifest, summarizeCorpus, } from "../../infrastructure/evaluation/corpus-store.js";
2
- export async function validateCorpus(input) {
3
- const raw = await loadCorpusManifestFromPath(input.repoRoot, input.manifestPath);
4
- const manifest = await materializeCorpusManifest(raw);
5
- return { manifest, summary: summarizeCorpus(manifest) };
6
- }
7
- export async function registerCorpus(input) {
8
- const validated = await validateCorpus(input);
9
- const result = await registerCorpusManifest({
10
- repoRoot: input.repoRoot,
11
- manifest: validated.manifest,
12
- });
13
- return {
14
- manifest: result.manifest,
15
- summary: summarizeCorpus(result.manifest),
16
- idempotent: result.idempotent,
17
- manifestPath: result.manifestPath,
18
- };
19
- }
20
- export async function showCorpus(input) {
21
- const manifest = await readCorpusManifest(input.repoRoot, input.corpusId);
22
- return { manifest, summary: summarizeCorpus(manifest) };
23
- }
24
- export async function listCorpora(input) {
25
- const ids = await listCorpusIds(input.repoRoot);
26
- const rows = [];
27
- for (const corpusId of ids) {
28
- const manifest = await readCorpusManifest(input.repoRoot, corpusId);
29
- rows.push(summarizeCorpus(manifest));
30
- }
31
- return rows;
32
- }
33
- export function formatCorpusMarkdown(input) {
34
- const { manifest, summary } = input;
35
- const lines = [
36
- `# Corpus: ${manifest.corpusId}`,
37
- "",
38
- `- corpusHash: \`${manifest.corpusHash}\``,
39
- `- taskCount: \`${summary.taskCount}\``,
40
- `- splits: public=${summary.splits.public} private=${summary.splits.private} held_out=${summary.splits.held_out}`,
41
- `- classes: ${summary.classes.map((value) => `\`${value}\``).join(", ")}`,
42
- `- createdAt: \`${manifest.createdAt}\``,
43
- "",
44
- "> Contract-only corpus. Registration does not execute live campaigns or authorize promotion.",
45
- "",
46
- "## Tasks",
47
- "",
48
- "| taskRef | class | split | seeds | featureId |",
49
- "|---|---|---|---|---|",
50
- ];
51
- for (const task of manifest.tasks) {
52
- lines.push(`| \`${task.taskRef}\` | ${task.class} | ${task.split} | ${task.seeds.join(",")} | ${task.featureId ?? "-"} |`);
53
- }
54
- lines.push("");
55
- return `${lines.join("\n")}\n`;
56
- }