@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.
- package/dist/adapters/contribution-adapter.d.ts +18 -0
- package/dist/adapters/contribution-adapter.js +134 -0
- package/dist/adapters/corpus-adapter.d.ts +12 -0
- package/dist/adapters/corpus-adapter.js +212 -0
- package/dist/adapters/index.d.ts +17 -0
- package/dist/adapters/index.js +17 -0
- package/dist/adapters/json-map-store.d.ts +4 -0
- package/dist/adapters/json-map-store.js +31 -0
- package/dist/adapters/local-learning-adapter.d.ts +25 -0
- package/dist/adapters/local-learning-adapter.js +49 -0
- package/dist/adapters/skills-adapter.d.ts +8 -0
- package/dist/adapters/skills-adapter.js +55 -0
- package/dist/admission-policy.d.ts +38 -0
- package/dist/admission-policy.js +17 -0
- package/dist/bin/jinn-distill-mcp.d.ts +2 -0
- package/dist/bin/jinn-distill-mcp.js +10 -0
- package/dist/bin/jinn-layer.d.ts +7 -0
- package/dist/bin/jinn-layer.js +15 -0
- package/dist/bridge-fetch-evidence.d.ts +102 -0
- package/dist/bridge-fetch-evidence.js +677 -0
- package/dist/bridge-verdict-source.d.ts +47 -0
- package/dist/bridge-verdict-source.js +216 -0
- package/dist/bridge.d.ts +247 -0
- package/dist/bridge.js +439 -0
- package/dist/capture.d.ts +98 -0
- package/dist/capture.js +617 -0
- package/dist/cli.d.ts +148 -0
- package/dist/cli.js +2221 -0
- package/dist/cluster-selection.d.ts +34 -0
- package/dist/cluster-selection.js +135 -0
- package/dist/cluster.d.ts +89 -0
- package/dist/cluster.js +142 -0
- package/dist/consume.d.ts +139 -0
- package/dist/consume.js +280 -0
- package/dist/corpus-probes.d.ts +61 -0
- package/dist/corpus-probes.js +120 -0
- package/dist/corpus-store.d.ts +16 -0
- package/dist/corpus-store.js +101 -0
- package/dist/distill-captures.d.ts +56 -0
- package/dist/distill-captures.js +283 -0
- package/dist/distill-feedback.d.ts +20 -0
- package/dist/distill-feedback.js +30 -0
- package/dist/distill-llm.d.ts +138 -0
- package/dist/distill-llm.js +438 -0
- package/dist/distill-mcp-server.d.ts +30 -0
- package/dist/distill-mcp-server.js +281 -0
- package/dist/distill-mode.d.ts +62 -0
- package/dist/distill-mode.js +107 -0
- package/dist/distill-progress.d.ts +78 -0
- package/dist/distill-progress.js +69 -0
- package/dist/distill-prompt.d.ts +34 -0
- package/dist/distill-prompt.js +85 -0
- package/dist/distill-render.d.ts +123 -0
- package/dist/distill-render.js +303 -0
- package/dist/distill-runs.d.ts +28 -0
- package/dist/distill-runs.js +39 -0
- package/dist/distill-traces.d.ts +67 -0
- package/dist/distill-traces.js +211 -0
- package/dist/distill.d.ts +180 -0
- package/dist/distill.js +337 -0
- package/dist/distiller.d.ts +118 -0
- package/dist/distiller.js +142 -0
- package/dist/envelope.d.ts +9 -0
- package/dist/envelope.js +9 -0
- package/dist/eval-prep.d.ts +129 -0
- package/dist/eval-prep.js +523 -0
- package/dist/execution-publish.d.ts +62 -0
- package/dist/execution-publish.js +72 -0
- package/dist/gate.d.ts +39 -0
- package/dist/gate.js +106 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +50 -0
- package/dist/ipfs-cid.d.ts +18 -0
- package/dist/ipfs-cid.js +149 -0
- package/dist/layer-default.d.ts +3 -0
- package/dist/layer-default.js +10 -0
- package/dist/ledger.d.ts +62 -0
- package/dist/ledger.js +104 -0
- package/dist/measurement.d.ts +90 -0
- package/dist/measurement.js +134 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +303 -0
- package/dist/pipeline.d.ts +94 -0
- package/dist/pipeline.js +124 -0
- package/dist/plugin-wiring.d.ts +4 -0
- package/dist/plugin-wiring.js +54 -0
- package/dist/preview.d.ts +31 -0
- package/dist/preview.js +28 -0
- package/dist/process-contract.d.ts +303 -0
- package/dist/process-contract.js +118 -0
- package/dist/publish-skill.d.ts +86 -0
- package/dist/publish-skill.js +116 -0
- package/dist/publish.d.ts +294 -0
- package/dist/publish.js +1090 -0
- package/dist/scripts/scan-skills-registry.d.ts +24 -0
- package/dist/scripts/scan-skills-registry.js +207 -0
- package/dist/seed-import/curated-batch.d.ts +36 -0
- package/dist/seed-import/curated-batch.js +103 -0
- package/dist/seed-import/episode-execute.d.ts +50 -0
- package/dist/seed-import/episode-execute.js +235 -0
- package/dist/seed-import/episode-fetch.d.ts +119 -0
- package/dist/seed-import/episode-fetch.js +137 -0
- package/dist/seed-import/episode-plan.d.ts +11 -0
- package/dist/seed-import/episode-plan.js +27 -0
- package/dist/seed-import/episode-report.d.ts +39 -0
- package/dist/seed-import/episode-report.js +36 -0
- package/dist/seed-import/execute.d.ts +61 -0
- package/dist/seed-import/execute.js +245 -0
- package/dist/seed-import/fetch.d.ts +62 -0
- package/dist/seed-import/fetch.js +82 -0
- package/dist/seed-import/licence.d.ts +17 -0
- package/dist/seed-import/licence.js +36 -0
- package/dist/seed-import/plan.d.ts +10 -0
- package/dist/seed-import/plan.js +21 -0
- package/dist/seed-import/report.d.ts +34 -0
- package/dist/seed-import/report.js +31 -0
- package/dist/seed-import/state.d.ts +72 -0
- package/dist/seed-import/state.js +120 -0
- package/dist/signal.d.ts +36 -0
- package/dist/signal.js +48 -0
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +21 -0
- package/dist/skill-package.d.ts +70 -0
- package/dist/skill-package.js +141 -0
- package/dist/skill.d.ts +21 -0
- package/dist/skill.js +77 -0
- package/dist/snapshot-transcript.d.ts +54 -0
- package/dist/snapshot-transcript.js +117 -0
- package/dist/three-arm-run.d.ts +27 -0
- package/dist/three-arm-run.js +41 -0
- 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>;
|