@jinn-network/jinn-layer 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 (131) hide show
  1. package/dist/adapters/contribution-adapter.d.ts +18 -0
  2. package/dist/adapters/contribution-adapter.js +134 -0
  3. package/dist/adapters/corpus-adapter.d.ts +12 -0
  4. package/dist/adapters/corpus-adapter.js +212 -0
  5. package/dist/adapters/index.d.ts +17 -0
  6. package/dist/adapters/index.js +17 -0
  7. package/dist/adapters/json-map-store.d.ts +4 -0
  8. package/dist/adapters/json-map-store.js +31 -0
  9. package/dist/adapters/local-learning-adapter.d.ts +25 -0
  10. package/dist/adapters/local-learning-adapter.js +49 -0
  11. package/dist/adapters/skills-adapter.d.ts +8 -0
  12. package/dist/adapters/skills-adapter.js +55 -0
  13. package/dist/admission-policy.d.ts +38 -0
  14. package/dist/admission-policy.js +17 -0
  15. package/dist/bin/jinn-distill-mcp.d.ts +2 -0
  16. package/dist/bin/jinn-distill-mcp.js +10 -0
  17. package/dist/bin/jinn-layer.d.ts +7 -0
  18. package/dist/bin/jinn-layer.js +15 -0
  19. package/dist/bridge-fetch-evidence.d.ts +102 -0
  20. package/dist/bridge-fetch-evidence.js +677 -0
  21. package/dist/bridge-verdict-source.d.ts +47 -0
  22. package/dist/bridge-verdict-source.js +216 -0
  23. package/dist/bridge.d.ts +247 -0
  24. package/dist/bridge.js +439 -0
  25. package/dist/capture.d.ts +98 -0
  26. package/dist/capture.js +617 -0
  27. package/dist/cli.d.ts +148 -0
  28. package/dist/cli.js +2221 -0
  29. package/dist/cluster-selection.d.ts +34 -0
  30. package/dist/cluster-selection.js +135 -0
  31. package/dist/cluster.d.ts +89 -0
  32. package/dist/cluster.js +142 -0
  33. package/dist/consume.d.ts +139 -0
  34. package/dist/consume.js +280 -0
  35. package/dist/corpus-probes.d.ts +61 -0
  36. package/dist/corpus-probes.js +120 -0
  37. package/dist/corpus-store.d.ts +16 -0
  38. package/dist/corpus-store.js +101 -0
  39. package/dist/distill-captures.d.ts +56 -0
  40. package/dist/distill-captures.js +283 -0
  41. package/dist/distill-feedback.d.ts +20 -0
  42. package/dist/distill-feedback.js +30 -0
  43. package/dist/distill-llm.d.ts +138 -0
  44. package/dist/distill-llm.js +438 -0
  45. package/dist/distill-mcp-server.d.ts +30 -0
  46. package/dist/distill-mcp-server.js +281 -0
  47. package/dist/distill-mode.d.ts +62 -0
  48. package/dist/distill-mode.js +107 -0
  49. package/dist/distill-progress.d.ts +78 -0
  50. package/dist/distill-progress.js +69 -0
  51. package/dist/distill-prompt.d.ts +34 -0
  52. package/dist/distill-prompt.js +85 -0
  53. package/dist/distill-render.d.ts +123 -0
  54. package/dist/distill-render.js +303 -0
  55. package/dist/distill-runs.d.ts +28 -0
  56. package/dist/distill-runs.js +39 -0
  57. package/dist/distill-traces.d.ts +67 -0
  58. package/dist/distill-traces.js +211 -0
  59. package/dist/distill.d.ts +180 -0
  60. package/dist/distill.js +337 -0
  61. package/dist/distiller.d.ts +118 -0
  62. package/dist/distiller.js +142 -0
  63. package/dist/envelope.d.ts +9 -0
  64. package/dist/envelope.js +9 -0
  65. package/dist/eval-prep.d.ts +129 -0
  66. package/dist/eval-prep.js +523 -0
  67. package/dist/execution-publish.d.ts +62 -0
  68. package/dist/execution-publish.js +72 -0
  69. package/dist/gate.d.ts +39 -0
  70. package/dist/gate.js +106 -0
  71. package/dist/index.d.ts +49 -0
  72. package/dist/index.js +50 -0
  73. package/dist/ipfs-cid.d.ts +18 -0
  74. package/dist/ipfs-cid.js +149 -0
  75. package/dist/layer-default.d.ts +3 -0
  76. package/dist/layer-default.js +10 -0
  77. package/dist/ledger.d.ts +62 -0
  78. package/dist/ledger.js +104 -0
  79. package/dist/measurement.d.ts +90 -0
  80. package/dist/measurement.js +134 -0
  81. package/dist/path-safety.d.ts +19 -0
  82. package/dist/path-safety.js +303 -0
  83. package/dist/pipeline.d.ts +94 -0
  84. package/dist/pipeline.js +124 -0
  85. package/dist/plugin-wiring.d.ts +4 -0
  86. package/dist/plugin-wiring.js +54 -0
  87. package/dist/preview.d.ts +31 -0
  88. package/dist/preview.js +28 -0
  89. package/dist/process-contract.d.ts +303 -0
  90. package/dist/process-contract.js +118 -0
  91. package/dist/publish-skill.d.ts +86 -0
  92. package/dist/publish-skill.js +116 -0
  93. package/dist/publish.d.ts +294 -0
  94. package/dist/publish.js +1090 -0
  95. package/dist/scripts/scan-skills-registry.d.ts +24 -0
  96. package/dist/scripts/scan-skills-registry.js +207 -0
  97. package/dist/seed-import/curated-batch.d.ts +36 -0
  98. package/dist/seed-import/curated-batch.js +103 -0
  99. package/dist/seed-import/episode-execute.d.ts +50 -0
  100. package/dist/seed-import/episode-execute.js +235 -0
  101. package/dist/seed-import/episode-fetch.d.ts +119 -0
  102. package/dist/seed-import/episode-fetch.js +137 -0
  103. package/dist/seed-import/episode-plan.d.ts +11 -0
  104. package/dist/seed-import/episode-plan.js +27 -0
  105. package/dist/seed-import/episode-report.d.ts +39 -0
  106. package/dist/seed-import/episode-report.js +36 -0
  107. package/dist/seed-import/execute.d.ts +61 -0
  108. package/dist/seed-import/execute.js +245 -0
  109. package/dist/seed-import/fetch.d.ts +62 -0
  110. package/dist/seed-import/fetch.js +82 -0
  111. package/dist/seed-import/licence.d.ts +17 -0
  112. package/dist/seed-import/licence.js +36 -0
  113. package/dist/seed-import/plan.d.ts +10 -0
  114. package/dist/seed-import/plan.js +21 -0
  115. package/dist/seed-import/report.d.ts +34 -0
  116. package/dist/seed-import/report.js +31 -0
  117. package/dist/seed-import/state.d.ts +72 -0
  118. package/dist/seed-import/state.js +120 -0
  119. package/dist/signal.d.ts +36 -0
  120. package/dist/signal.js +48 -0
  121. package/dist/signing.d.ts +8 -0
  122. package/dist/signing.js +21 -0
  123. package/dist/skill-package.d.ts +70 -0
  124. package/dist/skill-package.js +141 -0
  125. package/dist/skill.d.ts +21 -0
  126. package/dist/skill.js +77 -0
  127. package/dist/snapshot-transcript.d.ts +54 -0
  128. package/dist/snapshot-transcript.js +117 -0
  129. package/dist/three-arm-run.d.ts +27 -0
  130. package/dist/three-arm-run.js +41 -0
  131. package/package.json +60 -0
package/dist/bridge.js ADDED
@@ -0,0 +1,439 @@
1
+ /**
2
+ * The bridge — SolverNet execution ledger → layer-1 evidence
3
+ * (spec/2026-07-06-distillation-v1.md §8, D5/D10).
4
+ *
5
+ * Turns swe-rebench attempts into canonical `jinn.episode.v1` evidence via
6
+ * the existing `capture()`→`publish()` pipe, scrubbed at the **layer-2
7
+ * (secret-only) altitude** (D6 — a verified swe-rebench solve is public-repo
8
+ * work, not raw private-machine activity). It sources **both polarities** (D10):
9
+ * verified passes → pattern-eligible evidence; evaluator-confirmed failures →
10
+ * lesson-eligible evidence. Held-out `cap-v0` slate instances are excluded by
11
+ * `instance_id` AND repo; each `(instance_id, polarity)` is retained up to
12
+ * `groupCap` attempts (default 1) so the per-instance attempt group survives for
13
+ * group-relative distillation (#1478).
14
+ *
15
+ * This module is the bridge **core + the publish adapter**. The ledger row
16
+ * source (indexer GraphQL over `verdictEnvelopeMeta`, both polarities) and the
17
+ * IPFS/corpus evidence fetch are injected ports (`BridgeDeps`) — the production
18
+ * adapters are thin and wired at run time; the logic here is what carries the
19
+ * tests.
20
+ */
21
+ import { capture } from './capture.js';
22
+ import { ManifestBatchAnchorError, ManifestBatchPreparationError, ManifestBatchRecordingError, ManifestBatchSetError, publish, publishManifestBatch as publishPendingManifestBatch, } from './publish.js';
23
+ import { buildLayer2ScrubPipeline, } from '@jinn-network/core/scrub';
24
+ /**
25
+ * Derive `owner/repo` from a SWE-bench `instance_id` (`owner__repo-<pr#>`),
26
+ * or null when the id does not match the convention. cap-v0 will carry repos
27
+ * explicitly; until then repo exclusion (§12) is derived here.
28
+ */
29
+ export function repoFromInstanceId(instanceId) {
30
+ const m = /^(.+)__(.+)-\d+$/.exec(instanceId);
31
+ return m ? `${m[1]}/${m[2]}` : null;
32
+ }
33
+ const NANO = (d) => `${d.getTime()}000000`;
34
+ function assertAuthenticatedBridgeEvidence(ref, ev) {
35
+ const verifier = ev.verifier;
36
+ const exactStringArray = (value) => Array.isArray(value)
37
+ && value.every((item) => typeof item === 'string' && item.length > 0);
38
+ if (!verifier
39
+ || !exactStringArray(verifier.failToPass)
40
+ || !exactStringArray(verifier.passToPass)
41
+ || typeof verifier.evalSemanticsVersion !== 'string'
42
+ || verifier.evalSemanticsVersion.length === 0) {
43
+ throw new Error(`bridge evidence for ${ref.instanceId} requires authenticated verifier facts before evaluator-verified publication`);
44
+ }
45
+ if (typeof ev.instanceId !== 'string'
46
+ || ev.instanceId.length === 0
47
+ || typeof ev.repo !== 'string'
48
+ || ev.repo.length === 0
49
+ || typeof ev.baseCommit !== 'string'
50
+ || ev.baseCommit.length === 0
51
+ || typeof ev.taskCreatedAt !== 'number'
52
+ || !Number.isInteger(ev.taskCreatedAt)
53
+ || ev.taskCreatedAt < 0) {
54
+ throw new Error(`bridge evidence for ${ref.instanceId} requires an authenticated task provenance tuple`);
55
+ }
56
+ }
57
+ /**
58
+ * Construct the layer-1 `CapturedTask` for one attempt (both polarities, D10).
59
+ *
60
+ * The full patch goes into the `apply_patch` step attribute. Note: `capture()`
61
+ * caps step attributes at `MAX_STEP_ATTRIBUTES_BYTES` (16 KiB) and truncates the
62
+ * largest value with a receipted `…[truncated]` suffix, so a patch over that
63
+ * bound is stored truncated (inherited from the frozen pipe, D5 — not
64
+ * re-implemented here). Real SWE-bench patches can exceed 16 KiB; the truncation
65
+ * is recorded in the envelope's step receipt, not silent.
66
+ *
67
+ * Evidence-richness (§8): canonical typed transcript spans are inserted
68
+ * between the patch and verdict steps. When no usable spans exist, a
69
+ * `patch-only` tag is appended to `distributionTags`.
70
+ */
71
+ export function toBridgeCapturedTask(ref, ev, now) {
72
+ assertAuthenticatedBridgeEvidence(ref, ev);
73
+ const nano = NANO(now);
74
+ const isPass = ref.polarity === 'pass';
75
+ const repo = ev.repo;
76
+ const instanceId = ev.instanceId;
77
+ const historySteps = (ev.trajectorySpans ?? []).flatMap((span, index) => {
78
+ const spanKind = span.attributes['jinn.span.kind'];
79
+ if (spanKind !== 'jinn.agent_turn' && spanKind !== 'jinn.tool_call') {
80
+ return [];
81
+ }
82
+ return [{
83
+ spanId: `history-${String(index + 1).padStart(6, '0')}`,
84
+ parentSpanId: null,
85
+ kind: spanKind,
86
+ name: span.name,
87
+ startTimeUnixNano: span.startTimeUnixNano,
88
+ endTimeUnixNano: span.endTimeUnixNano,
89
+ attributes: span.attributes,
90
+ redactedKeys: [],
91
+ ...(span.events.length > 0 ? { events: span.events } : {}),
92
+ ...(span.status ? { status: span.status } : {}),
93
+ }];
94
+ });
95
+ const enriched = historySteps.length > 0;
96
+ return {
97
+ session: {
98
+ sessionId: `bridge:${instanceId}:${ref.polarity}:${ref.requestId}`,
99
+ capturedAt: now.toISOString(),
100
+ },
101
+ task: {
102
+ summary: `swe-rebench ${instanceId}: ${ev.taskSummary}`.slice(0, 500),
103
+ distributionTags: [
104
+ 'coding',
105
+ 'swe-rebench',
106
+ `repo:${repo}`.slice(0, 64),
107
+ ...(enriched ? [] : ['patch-only']),
108
+ ],
109
+ repositorySlug: repo,
110
+ baseCommit: ev.baseCommit,
111
+ createdAt: ev.taskCreatedAt,
112
+ instanceId,
113
+ },
114
+ environment: {
115
+ harness: { name: 'jinn-execution-ledger-bridge', version: '0.1.0' },
116
+ model: ref.model || 'unknown',
117
+ tools: [],
118
+ ...(ev.generatorModel ? { generatorModel: ev.generatorModel } : {}),
119
+ ...(ev.distributionClass ? { distributionClass: ev.distributionClass } : {}),
120
+ verifier: {
121
+ type: 'f2p-p2p',
122
+ failToPass: ev.verifier.failToPass,
123
+ passToPass: ev.verifier.passToPass,
124
+ evalSemanticsVersion: ev.verifier.evalSemanticsVersion,
125
+ },
126
+ },
127
+ steps: [
128
+ {
129
+ spanId: 'patch',
130
+ parentSpanId: null,
131
+ kind: 'jinn.tool_call',
132
+ name: 'tool:apply_patch',
133
+ startTimeUnixNano: nano,
134
+ endTimeUnixNano: nano,
135
+ attributes: { patch: ev.patch },
136
+ redactedKeys: [],
137
+ },
138
+ ...historySteps,
139
+ {
140
+ spanId: 'verdict',
141
+ parentSpanId: null,
142
+ kind: 'jinn.tool_call',
143
+ name: 'evaluator:verdict',
144
+ startTimeUnixNano: nano,
145
+ endTimeUnixNano: nano,
146
+ attributes: { evaluatorVerdict: isPass ? 'PASS' : 'FAIL', actualPassed: isPass },
147
+ redactedKeys: [],
148
+ },
149
+ ],
150
+ outcome: isPass
151
+ ? { status: 'completed', verifiabilityTier: 'evaluator-verified' }
152
+ : {
153
+ status: 'failed',
154
+ verifiabilityTier: 'evaluator-verified',
155
+ summary: 'evaluator-confirmed failure (lesson-eligible)',
156
+ },
157
+ cost: { durationMs: 0 },
158
+ attemptGroup: {
159
+ groupId: instanceId,
160
+ attemptId: ref.requestId,
161
+ relatedAttemptRefs: [
162
+ ev.verdictEnvelopeCid,
163
+ ev.solutionEnvelopeCid,
164
+ ev.rawSnapshotRef,
165
+ ].filter((value) => value !== undefined),
166
+ },
167
+ ...(ev.solutionEnvelopeCid
168
+ ? { lineage: { episodeId: ev.solutionEnvelopeCid } }
169
+ : {}),
170
+ provenance: 'derived-from-history',
171
+ };
172
+ }
173
+ /**
174
+ * Bridge a batch of ledger verdict rows into layer-1 evidence. Permissionless
175
+ * enrichment projections are used only to discover authenticated evidence.
176
+ * Exclusion and dedup use the signed task identity returned by `fetchEvidence`.
177
+ */
178
+ export async function bridgeAttempts(refs, deps) {
179
+ const now = deps.now?.() ?? new Date();
180
+ // Derive the slate repos for axis-2 exclusion, and surface any slate id whose
181
+ // repo cannot be derived — repo-exclusion is incomplete for those (see
182
+ // BridgeResult.unresolvedSlateIds), rather than silently narrowing the guard.
183
+ const slateRepos = new Set();
184
+ const unresolvedSlateIds = [];
185
+ for (const id of deps.slateInstanceIds) {
186
+ const r = repoFromInstanceId(id);
187
+ if (r)
188
+ slateRepos.add(r);
189
+ else
190
+ unresolvedSlateIds.push(id);
191
+ }
192
+ const result = { bridged: [], excludedHeldOut: [], deduped: [], errors: [], unresolvedSlateIds };
193
+ // Retain up to `groupCap` attempts per (instance_id, polarity) — keep the whole
194
+ // attempt group, not one exemplar (#1478). Kept-K is deterministic (refs arrive
195
+ // in a stable order); rows beyond the cap are recorded in `deduped`.
196
+ const groupCap = Math.max(1, deps.groupCap ?? 1);
197
+ const kept = new Map();
198
+ const manifestCandidates = [];
199
+ for (const ref of refs) {
200
+ const candidates = ref.discoveryCandidates?.length
201
+ ? ref.discoveryCandidates
202
+ : [{
203
+ instanceId: ref.instanceId,
204
+ verdictManifestCid: ref.verdictManifestCid ?? '',
205
+ }];
206
+ let selected;
207
+ let lastError;
208
+ for (const candidate of candidates) {
209
+ const candidateRef = {
210
+ ...ref,
211
+ instanceId: candidate.instanceId,
212
+ verdictManifestCid: candidate.verdictManifestCid,
213
+ discoveryCandidates: undefined,
214
+ };
215
+ try {
216
+ const ev = await deps.fetchEvidence(candidateRef);
217
+ assertAuthenticatedBridgeEvidence(candidateRef, ev);
218
+ selected = { ref: candidateRef, evidence: ev };
219
+ break;
220
+ }
221
+ catch (err) {
222
+ lastError = err;
223
+ }
224
+ }
225
+ // Candidate-level misses are intentionally suppressed when a later
226
+ // projection reaches the authenticated task. Surface one error only when
227
+ // the entire bounded candidate set fails.
228
+ if (!selected) {
229
+ result.errors.push({
230
+ requestId: ref.requestId,
231
+ error: lastError instanceof Error ? lastError.message : String(lastError),
232
+ });
233
+ continue;
234
+ }
235
+ const ev = selected.evidence;
236
+ const instanceId = ev.instanceId;
237
+ const canonicalRef = {
238
+ ...selected.ref,
239
+ instanceId,
240
+ repo: ev.repo,
241
+ baseCommit: ev.baseCommit,
242
+ };
243
+ if (deps.weakSuiteInstanceIds?.has(instanceId)) {
244
+ result.excludedHeldOut.push({ instanceId, reason: 'instance_id' });
245
+ continue;
246
+ }
247
+ if (deps.lookupFlaggedInstanceIds?.has(instanceId)) {
248
+ result.deduped.push({ instanceId, polarity: ref.polarity });
249
+ continue;
250
+ }
251
+ if (deps.slateInstanceIds.has(instanceId)) {
252
+ result.excludedHeldOut.push({ instanceId, reason: 'instance_id' });
253
+ continue;
254
+ }
255
+ if (slateRepos.has(ev.repo)) {
256
+ result.excludedHeldOut.push({ instanceId, reason: 'repo' });
257
+ continue;
258
+ }
259
+ const key = `${instanceId}:${ref.polarity}`;
260
+ if ((kept.get(key) ?? 0) >= groupCap) {
261
+ result.deduped.push({ instanceId, polarity: ref.polarity });
262
+ continue;
263
+ }
264
+ try {
265
+ const task = toBridgeCapturedTask(canonicalRef, ev, now);
266
+ if (deps.anchorMode === 'manifest') {
267
+ kept.set(key, (kept.get(key) ?? 0) + 1);
268
+ manifestCandidates.push({ task, ref: canonicalRef });
269
+ continue;
270
+ }
271
+ const pub = await deps.publishEvidence(task, canonicalRef);
272
+ // Count only SUCCESSFUL bridges toward the cap. Publication failures are
273
+ // surfaced once and never retried through another discovery projection:
274
+ // a failed publish may already have external side effects.
275
+ kept.set(key, (kept.get(key) ?? 0) + 1);
276
+ result.bridged.push({
277
+ instanceId,
278
+ polarity: ref.polarity,
279
+ envelopeRef: pub.envelopeRef,
280
+ anchorTx: pub.anchorTx,
281
+ });
282
+ }
283
+ catch (err) {
284
+ result.errors.push({
285
+ requestId: ref.requestId,
286
+ error: err instanceof Error ? err.message : String(err),
287
+ });
288
+ }
289
+ }
290
+ if (deps.anchorMode === 'manifest' && manifestCandidates.length > 0) {
291
+ const observations = [];
292
+ const addCompleted = (batches, candidateOffset) => {
293
+ let cursor = candidateOffset;
294
+ for (const batch of batches) {
295
+ observations.push({ ...batch, confirmed: true });
296
+ for (const memberRef of batch.memberRefs) {
297
+ const candidate = manifestCandidates[cursor];
298
+ if (!candidate) {
299
+ throw new Error('manifest publisher returned more member refs than candidates');
300
+ }
301
+ result.bridged.push({
302
+ instanceId: candidate.ref.instanceId,
303
+ polarity: candidate.ref.polarity,
304
+ envelopeRef: memberRef,
305
+ anchorTx: batch.anchorTx,
306
+ });
307
+ cursor += 1;
308
+ }
309
+ }
310
+ return cursor;
311
+ };
312
+ const addFailedObservation = (error) => {
313
+ if (error instanceof ManifestBatchRecordingError) {
314
+ observations.push({ ...error.result, confirmed: true });
315
+ }
316
+ else if (error instanceof ManifestBatchAnchorError) {
317
+ observations.push({
318
+ manifestCid: error.manifestCid,
319
+ memberRefs: [...error.memberRefs],
320
+ root: error.root,
321
+ anchorTx: error.txHash,
322
+ gasUsed: null,
323
+ feeWei: null,
324
+ confirmed: false,
325
+ });
326
+ }
327
+ };
328
+ const finishManifestFacts = (memberRefs) => {
329
+ result.manifestMemberRefs = [...memberRefs];
330
+ if (observations.length > 0) {
331
+ result.manifestBatches = observations;
332
+ const batchKey = observations.find((batch) => batch.batchKey)?.batchKey;
333
+ if (batchKey)
334
+ result.manifestBatchKey = batchKey;
335
+ result.control = observations.find((batch) => batch.control)?.control;
336
+ }
337
+ if (observations.length === 1) {
338
+ const only = observations[0];
339
+ result.manifestCid = only.manifestCid;
340
+ result.anchorTx = only.anchorTx;
341
+ result.gasUsed = only.gasUsed;
342
+ result.feeWei = only.feeWei;
343
+ result.manifestConfirmed = only.confirmed;
344
+ }
345
+ };
346
+ try {
347
+ if (!deps.publishManifestBatch) {
348
+ throw new Error('manifest anchor mode requires publishManifestBatch');
349
+ }
350
+ const publication = await deps.publishManifestBatch(manifestCandidates);
351
+ const flattened = publication.batches.flatMap((batch) => batch.memberRefs);
352
+ if (publication.memberRefs.length !== manifestCandidates.length ||
353
+ flattened.length !== publication.memberRefs.length ||
354
+ flattened.some((ref, index) => ref !== publication.memberRefs[index])) {
355
+ throw new Error(`manifest publisher returned an inconsistent ${publication.memberRefs.length}-member partition set for ${manifestCandidates.length} candidates`);
356
+ }
357
+ addCompleted(publication.batches, 0);
358
+ finishManifestFacts(publication.memberRefs);
359
+ }
360
+ catch (error) {
361
+ let completedCount = 0;
362
+ let failed = error;
363
+ let memberRefs = [];
364
+ if (error instanceof ManifestBatchSetError) {
365
+ completedCount = addCompleted(error.completed, 0);
366
+ failed = error.failed;
367
+ memberRefs = [...error.memberRefs];
368
+ if (error.batchKey)
369
+ result.manifestBatchKey = error.batchKey;
370
+ addFailedObservation(failed);
371
+ }
372
+ else if (error instanceof ManifestBatchRecordingError) {
373
+ memberRefs = [...error.result.memberRefs];
374
+ if (error.batchKey)
375
+ result.manifestBatchKey = error.batchKey;
376
+ addFailedObservation(error);
377
+ }
378
+ else if (error instanceof ManifestBatchAnchorError) {
379
+ memberRefs = [...error.memberRefs];
380
+ if (error.batchKey)
381
+ result.manifestBatchKey = error.batchKey;
382
+ addFailedObservation(error);
383
+ }
384
+ else if (error instanceof ManifestBatchPreparationError) {
385
+ memberRefs = [...error.memberRefs];
386
+ if (error.batchKey)
387
+ result.manifestBatchKey = error.batchKey;
388
+ }
389
+ if (memberRefs.length > 0)
390
+ finishManifestFacts(memberRefs);
391
+ const message = failed instanceof Error ? failed.message : String(failed);
392
+ for (const { ref } of manifestCandidates.slice(completedCount)) {
393
+ result.errors.push({ requestId: ref.requestId, error: message });
394
+ }
395
+ }
396
+ }
397
+ return result;
398
+ }
399
+ /**
400
+ * The production `publishEvidence`: run the CapturedTask through `capture()`
401
+ * with the **layer-2 (secret-only) pipeline** injected (D6), then `publish()` —
402
+ * emitting a canonical `jinn.episode.v1` artifact. Reuses the scrubbed pipe; the only
403
+ * departure from a native capture is the scrub altitude.
404
+ */
405
+ export function buildBridgeEvidencePublisher(deps, opts = {}) {
406
+ const pipeline = opts.pipeline ?? buildLayer2ScrubPipeline();
407
+ return async (task) => {
408
+ const pending = await capture(task, { pipeline });
409
+ const published = await publish(pending, deps);
410
+ if (published.vetoed)
411
+ throw new Error('unexpected veto bridging evidence');
412
+ return { envelopeRef: published.envelopeRef, anchorTx: published.anchorTx };
413
+ };
414
+ }
415
+ /**
416
+ * Production manifest publisher: capture every bridge task with the shared
417
+ * layer-2 scrub pipeline, then hand all pending envelopes to the one-anchor
418
+ * batch publisher with their discovery hints.
419
+ */
420
+ export function buildBridgeManifestPublisher(deps, opts = {}) {
421
+ const pipeline = opts.pipeline ?? buildLayer2ScrubPipeline();
422
+ return async (candidates) => {
423
+ const members = [];
424
+ for (const { task, ref } of candidates) {
425
+ const pending = await capture(task, { pipeline });
426
+ opts.onCaptured?.(pending, { task, ref });
427
+ members.push({
428
+ pending,
429
+ polarity: ref.polarity,
430
+ instanceId: ref.instanceId,
431
+ sourceId: ref.requestId,
432
+ });
433
+ }
434
+ return publishPendingManifestBatch(members, deps, {
435
+ batchKind: 'bridge',
436
+ ...(opts.measurePerRecordControl ? { measurePerRecordControl: true } : {}),
437
+ });
438
+ };
439
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Harness-layer capture path — the privacy surface (spec §5).
3
+ *
4
+ * `capture(task, opts)` runs a captured task through the mandatory scrub
5
+ * pipeline and returns a `PendingEnvelope`: the captured-but-not-yet-consented
6
+ * state. Per the 2026-07-02 schema review, consent flags on `TraceEnvelopeV0`
7
+ * are literal `true`, so the pre-consent state cannot BE a `TraceEnvelopeV0` —
8
+ * the pending wrapper converts to one only at consent (preview renders the
9
+ * would-be envelope; the Task 4 publish path performs the real conversion).
10
+ *
11
+ * Fail-closed: any scrub-stage failure throws `CaptureScrubError` — no
12
+ * envelope, no partial output, never a silent pass.
13
+ *
14
+ * Scrub pipeline: reuses the seller-side pipeline from
15
+ * `@jinn-network/core/scrub` (key-policy → openredaction →
16
+ * plain-patterns → secretlint → optional ML PII) via `scrubCaptureSpans()`.
17
+ * The email/home-path gap-closers this file originally carried graduated
18
+ * into the shared plain-patterns stage (#1330), so the daemon publish path
19
+ * and this capture path share one implementation.
20
+ *
21
+ * Fitting rule (docs/envelope-v0.md): the conversion MUST produce envelopes
22
+ * inside the frozen caps. Over-long sessions are head/tail-sampled to
23
+ * `MAX_STEPS` (never dropped); oversized step attributes are truncated with
24
+ * the cut keys listed in the step's `truncatedKeys` receipt (never silent —
25
+ * both surface in the `ScrubReport`).
26
+ *
27
+ * Plan: docs/superpowers/plans/2026-07-02-jinn-harness-network-v0-plan.md
28
+ * Task 3 (issue #1310).
29
+ */
30
+ import { ScrubPipeline, type KeyPolicy, type PiiDetector } from '@jinn-network/core/scrub';
31
+ import { type TraceEnvelopeV0 } from './envelope.js';
32
+ export { CapturedTaskSchema, parseCapturedTask, type CapturedTask } from '@jinn-network/core';
33
+ import { type CapturedTask } from '@jinn-network/core';
34
+ /**
35
+ * One entry in the redaction diff. `before` is the original (sensitive)
36
+ * value: it exists so the operator can audit locally what was removed, and
37
+ * it must NEVER be persisted or published — display-only.
38
+ */
39
+ export interface ScrubRedaction {
40
+ /** Where in the envelope, e.g. `steps[2].attributes["llm.prompt"]`. */
41
+ field: string;
42
+ /** Scrub-pipeline stage (`key-policy`, `openredaction`, `secretlint`, …) or `fit`. */
43
+ stage: string;
44
+ /** Rule / entity detail when the stage reports one (e.g. `AWS_ACCESS_KEY`). */
45
+ detail?: string;
46
+ /** Original value — local display only, never persisted. */
47
+ before?: unknown;
48
+ /** Published value; `undefined` when the field was dropped entirely. */
49
+ after: unknown;
50
+ }
51
+ export declare const PENDING_ENVELOPE_KIND: "jinn.pending-envelope.v0";
52
+ /**
53
+ * The captured-but-not-yet-consented state. Not a `TraceEnvelopeV0` (the
54
+ * envelope's consent flags are literal `true`, so an unconsented envelope is
55
+ * not constructible); it becomes one only at consent — `preview()` renders
56
+ * the would-be envelope, the Task 4 publish path performs the conversion for
57
+ * real. Only `capture()` constructs this type; `redactions[].before` values
58
+ * live in memory for the local preview and must never be written anywhere.
59
+ */
60
+ export interface PendingEnvelope {
61
+ kind: typeof PENDING_ENVELOPE_KIND;
62
+ /** Every TraceEnvelopeV0 field except `consent`, scrubbed and fitted. */
63
+ draft: Omit<TraceEnvelopeV0, 'consent'>;
64
+ /** Canonical Episode-only facts that the frozen trace draft cannot carry. */
65
+ episodeFacts: {
66
+ session: Pick<CapturedTask['session'], 'kind' | 'parentSessionId'>;
67
+ task: Pick<CapturedTask['task'], 'repositorySlug' | 'baseCommit' | 'createdAt' | 'instanceId'>;
68
+ environment: Pick<CapturedTask['environment'], 'skillsLoadout' | 'generatorModel' | 'distributionClass' | 'verifier'>;
69
+ trajectoryKinds: Array<CapturedTask['steps'][number]['kind']>;
70
+ trajectoryObservations?: Array<Pick<CapturedTask['steps'][number], 'events' | 'status'>>;
71
+ attemptGroup?: CapturedTask['attemptGroup'];
72
+ lineage?: CapturedTask['lineage'];
73
+ };
74
+ /** The redaction diff for the local preview. NEVER persisted. */
75
+ redactions: ScrubRedaction[];
76
+ }
77
+ export interface CaptureOptions {
78
+ /** Key policy for the base pipeline. Default: DEFAULT_KEY_POLICY. */
79
+ policy?: KeyPolicy;
80
+ /** When provided, the base pipeline gains the ML PII (GLiNER) stage. */
81
+ piiDetector?: PiiDetector;
82
+ /**
83
+ * Injectable base pipeline. Production consumer: seed import passes
84
+ * buildSeedScrubPipeline() (#1409); also used by tests. Overrides
85
+ * policy/piiDetector.
86
+ */
87
+ pipeline?: ScrubPipeline;
88
+ }
89
+ /** A scrub-stage failure. capture() is fail-closed: no envelope was produced. */
90
+ export declare class CaptureScrubError extends Error {
91
+ constructor(cause: unknown);
92
+ }
93
+ /**
94
+ * Run the captured task through the scrub pipeline and the fitting rule and
95
+ * return the pending (pre-consent) envelope plus its redaction diff.
96
+ * Fail-closed: throws `CaptureScrubError` on any scrub-stage failure.
97
+ */
98
+ export declare function capture(task: CapturedTask, opts?: CaptureOptions): Promise<PendingEnvelope>;