@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/consume.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-layer consume path — the embeddable corpus surface.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over core's `createCorpus()` and the configured corpus
|
|
5
|
+
* discovery port. No new query logic lives
|
|
6
|
+
* here: `search` delegates ref discovery to the discovery port and manifest
|
|
7
|
+
* retrieval to the corpus, then applies a client-side substring match
|
|
8
|
+
* (solverType lives in the IPFS manifest body, not in the on-chain envelope
|
|
9
|
+
* payload, so the indexer cannot filter on it — see
|
|
10
|
+
* core's HTTP discovery `queryEnvelopes`). `get` is
|
|
11
|
+
* fetchManifest + acquire.
|
|
12
|
+
*
|
|
13
|
+
* Plan: docs/superpowers/plans/2026-07-02-jinn-harness-network-v0-plan.md
|
|
14
|
+
* Task 1 (issue #1308).
|
|
15
|
+
*/
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { createCorpus, createHttpCorpusDiscovery, queryCaptureMeta as queryCaptureMetaCore, } from '@jinn-network/core/corpus-read';
|
|
19
|
+
import { SignedEnvelopeSchema, SKILL_ARTIFACT_TYPE, } from '@jinn-network/core';
|
|
20
|
+
import { SqliteCorpusStore } from './corpus-store.js';
|
|
21
|
+
import { extractSkill } from './skill.js';
|
|
22
|
+
export const DEFAULT_TESTNET_DISCOVERY_URL = 'https://jinn-indexer-production.up.railway.app';
|
|
23
|
+
/**
|
|
24
|
+
* Default public IPFS gateway. Mirrors the `ipfsGatewayUrl` zod default in
|
|
25
|
+
* client/src/config.ts (inline schema default; not exported as a constant).
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_IPFS_GATEWAY_URL = 'https://gateway.autonolas.tech';
|
|
28
|
+
const DEFAULT_SEARCH_LIMIT = 50;
|
|
29
|
+
function matchesQuery(hit, needle) {
|
|
30
|
+
if (needle === '')
|
|
31
|
+
return true;
|
|
32
|
+
const haystack = [
|
|
33
|
+
hit.solverType,
|
|
34
|
+
hit.role,
|
|
35
|
+
hit.ref,
|
|
36
|
+
hit.task?.cid ?? '',
|
|
37
|
+
hit.operator.safeAddress,
|
|
38
|
+
...hit.artifactTypes,
|
|
39
|
+
].join('\n').toLowerCase();
|
|
40
|
+
return haystack.includes(needle.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
function toSearchHit(ref, envelope) {
|
|
43
|
+
return {
|
|
44
|
+
title: `${envelope.solverType} / ${envelope.role}`,
|
|
45
|
+
ref: ref.manifestCid,
|
|
46
|
+
solverType: envelope.solverType,
|
|
47
|
+
role: envelope.role,
|
|
48
|
+
artifactTypes: envelope.artifacts.map((a) => a.artifactType),
|
|
49
|
+
kind: envelope.artifacts.some((a) => a.artifactType === SKILL_ARTIFACT_TYPE) ? 'skill' : 'trace',
|
|
50
|
+
evidenceTier: ref.evidenceTier,
|
|
51
|
+
generatedAt: envelope.generatedAt,
|
|
52
|
+
publishedAt: ref.publishedAt,
|
|
53
|
+
operator: {
|
|
54
|
+
agentId: ref.operator.agentId,
|
|
55
|
+
// The indexer does not store safeAddress; enrich from the manifest.
|
|
56
|
+
safeAddress: ref.operator.safeAddress || envelope.participant.safeAddress,
|
|
57
|
+
},
|
|
58
|
+
task: envelope.task
|
|
59
|
+
? { cid: envelope.task.cid, requestId: envelope.task.requestId }
|
|
60
|
+
: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Synthesize an EnvelopeRef for a bare manifest CID (get-by-ref path). */
|
|
64
|
+
function refForCid(manifestCid) {
|
|
65
|
+
return {
|
|
66
|
+
manifestCid,
|
|
67
|
+
manifestHash: '',
|
|
68
|
+
operator: { agentId: '', safeAddress: '' },
|
|
69
|
+
evidenceTier: 'unknown',
|
|
70
|
+
publishedAt: 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function createHarnessLayer(config = {}) {
|
|
74
|
+
const discoveryUrl = config.discoveryUrl ?? DEFAULT_TESTNET_DISCOVERY_URL;
|
|
75
|
+
const resolved = {
|
|
76
|
+
discoveryUrl,
|
|
77
|
+
ipfsGatewayUrl: config.ipfsGatewayUrl ?? DEFAULT_IPFS_GATEWAY_URL,
|
|
78
|
+
dbPath: config.store?.path
|
|
79
|
+
?? config.dbPath
|
|
80
|
+
?? join(homedir(), '.jinn-client', 'harness-layer', 'corpus-cache.db'),
|
|
81
|
+
captureMetaUrl: config.captureMetaUrl
|
|
82
|
+
?? `${discoveryUrl.replace(/\/graphql\/?$/, '').replace(/\/$/, '')}/capture-meta`,
|
|
83
|
+
};
|
|
84
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
85
|
+
const store = config.store ?? new SqliteCorpusStore(resolved.dbPath);
|
|
86
|
+
const discovery = config.discovery ?? createHttpCorpusDiscovery({
|
|
87
|
+
url: resolved.discoveryUrl,
|
|
88
|
+
fetchImpl: globalThis.fetch,
|
|
89
|
+
});
|
|
90
|
+
// `signer.privateKey` and `selfSafeAddress` are required by CorpusOptions
|
|
91
|
+
// but unused by the read paths this layer exposes (query / fetchManifest /
|
|
92
|
+
// ipfs-and-origin acquire). Placeholders, same as the read-only corpus in
|
|
93
|
+
// client/src/mcp/server.ts.
|
|
94
|
+
const corpus = createCorpus({
|
|
95
|
+
discovery,
|
|
96
|
+
ipfsGatewayUrl: resolved.ipfsGatewayUrl,
|
|
97
|
+
store,
|
|
98
|
+
signer: { privateKey: '0x0' },
|
|
99
|
+
selfSafeAddress: '0x0000000000000000000000000000000000000000',
|
|
100
|
+
parseEnvelope(input) {
|
|
101
|
+
return SignedEnvelopeSchema.parse(input);
|
|
102
|
+
},
|
|
103
|
+
}, {
|
|
104
|
+
...(config.fetchFromIpfs ? { fetchFromIpfs: config.fetchFromIpfs } : {}),
|
|
105
|
+
...(config.acquireFn ? { acquireFn: config.acquireFn } : {}),
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* Content-aware fast path (#1344): substring search over the indexer's
|
|
109
|
+
* enriched capture metadata (tags + task summary) — finds by content
|
|
110
|
+
* without any artifact fetch. Degrades to [] on any failure (older
|
|
111
|
+
* indexer, onchain mode, network error) so the manifest scan below is
|
|
112
|
+
* always the floor.
|
|
113
|
+
*/
|
|
114
|
+
async function queryCaptureMeta(query, limit) {
|
|
115
|
+
return queryCaptureMetaCore({
|
|
116
|
+
url: resolved.captureMetaUrl,
|
|
117
|
+
query,
|
|
118
|
+
limit,
|
|
119
|
+
fetchImpl,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Collapse a page of hits to lineage heads (#1462). For every `kind:'skill'`
|
|
124
|
+
* hit, fetch its body and read `provenance.supersedes` / `provenance.deprecates`:
|
|
125
|
+
* - a `deprecates: true` record hides itself, and
|
|
126
|
+
* - a `supersedes` pointer hides its target when — and only when — the
|
|
127
|
+
* target's operator matches this record's operator.
|
|
128
|
+
*
|
|
129
|
+
* Operator identity is the on-chain-derived `operator.agentId` the DiscoveryAPI
|
|
130
|
+
* supplies on each hit (IdentityRegistry event → indexer `row.agentId`), NOT the
|
|
131
|
+
* envelope `participant.safeAddress`. participant.safeAddress is free-form IPFS
|
|
132
|
+
* manifest content the publisher writes at will; keying the same-operator check
|
|
133
|
+
* off it would let a forged address grief default discovery. agentId is not
|
|
134
|
+
* forgeable in this way — a record is only indexed under its publisher's real
|
|
135
|
+
* agentId — so a supersede fires only against the same on-chain operator. The
|
|
136
|
+
* match is fail-safe: an absent agentId never matches, so an unattributed record
|
|
137
|
+
* collapses nothing. Residual (narrower): the supersede intent itself is not yet
|
|
138
|
+
* signature-verified end-to-end; tracked in spec/2026-07-06-distillation-v1.md §16.
|
|
139
|
+
*
|
|
140
|
+
* O(page) window (DECISION #3, no indexer change): a supersede is honored
|
|
141
|
+
* only when BOTH successor and target land in the same fetched page (spec
|
|
142
|
+
* §5/§16). Head-resolution is fail-safe: an unfetchable body hides nothing
|
|
143
|
+
* (mirrors the warn+continue on the manifest-scan path above).
|
|
144
|
+
*/
|
|
145
|
+
async function resolveHeads(hits) {
|
|
146
|
+
const lineage = new Map();
|
|
147
|
+
for (const h of hits) {
|
|
148
|
+
if (h.kind !== 'skill')
|
|
149
|
+
continue;
|
|
150
|
+
try {
|
|
151
|
+
const record = await get(h.ref);
|
|
152
|
+
const prov = extractSkill(record)?.skill.provenance;
|
|
153
|
+
lineage.set(h.ref, {
|
|
154
|
+
// Operator identity from the on-chain-derived `operator.agentId` the
|
|
155
|
+
// DiscoveryAPI supplies on the hit (IdentityRegistry event → indexer
|
|
156
|
+
// `row.agentId`, carried through `corpus.fetchManifest` untouched) —
|
|
157
|
+
// NOT the envelope `participant.safeAddress`, which is free-form IPFS
|
|
158
|
+
// manifest content the publisher writes at will (a forged value there
|
|
159
|
+
// could grief default discovery). A record can only be indexed under
|
|
160
|
+
// its publisher's real `agentId`, so a supersede pointer only fires
|
|
161
|
+
// against the SAME on-chain operator.
|
|
162
|
+
op: h.operator.agentId,
|
|
163
|
+
supersedes: prov?.supersedes,
|
|
164
|
+
deprecates: prov?.deprecates,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.warn(`[harness-layer] head-resolution skipping ${h.ref}: ${err instanceof Error ? err.message : String(err)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const excluded = new Set();
|
|
172
|
+
for (const [selfRef, info] of lineage) {
|
|
173
|
+
if (info.deprecates === true)
|
|
174
|
+
excluded.add(selfRef);
|
|
175
|
+
if (info.supersedes) {
|
|
176
|
+
const target = lineage.get(info.supersedes);
|
|
177
|
+
// Same-operator only, fail-safe when identity is unknown: require both
|
|
178
|
+
// agentIds non-empty and equal. An empty agentId (a backend that does
|
|
179
|
+
// not attribute the hit) never matches, so an unattributable record
|
|
180
|
+
// hides nothing rather than griefing.
|
|
181
|
+
if (target && info.op && target.op && target.op === info.op) {
|
|
182
|
+
excluded.add(info.supersedes);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return hits.filter((h) => !(h.kind === 'skill' && excluded.has(h.ref)));
|
|
187
|
+
}
|
|
188
|
+
async function search(query, opts = {}) {
|
|
189
|
+
const limit = opts.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
190
|
+
const hits = [];
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
// Fast path: content matches from indexed capture meta. Manifest fetch
|
|
193
|
+
// per hit only (never artifact bodies).
|
|
194
|
+
for (const metaHit of await queryCaptureMeta(query, limit)) {
|
|
195
|
+
if (hits.length >= limit)
|
|
196
|
+
break;
|
|
197
|
+
try {
|
|
198
|
+
const preview = await corpus.fetchManifest(refForCid(metaHit.manifestCid));
|
|
199
|
+
const hit = toSearchHit(preview.ref, preview.envelope);
|
|
200
|
+
hits.push({
|
|
201
|
+
...hit,
|
|
202
|
+
tags: Array.isArray(metaHit.tags) ? metaHit.tags : [],
|
|
203
|
+
summary: metaHit.taskSummary,
|
|
204
|
+
...(typeof metaHit.repositorySlug === 'string'
|
|
205
|
+
? { repositorySlug: metaHit.repositorySlug }
|
|
206
|
+
: {}),
|
|
207
|
+
...(typeof metaHit.synthesis === 'string'
|
|
208
|
+
? { synthesis: metaHit.synthesis }
|
|
209
|
+
: {}),
|
|
210
|
+
...(typeof metaHit.retrievalVisible === 'boolean'
|
|
211
|
+
? { retrievalVisible: metaHit.retrievalVisible }
|
|
212
|
+
: {}),
|
|
213
|
+
});
|
|
214
|
+
seen.add(metaHit.manifestCid);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
console.warn(`[harness-layer] skipping capture-meta hit ${metaHit.manifestCid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const refs = await corpus.query({ limit });
|
|
221
|
+
for (const ref of refs) {
|
|
222
|
+
if (seen.has(ref.manifestCid))
|
|
223
|
+
continue;
|
|
224
|
+
let preview;
|
|
225
|
+
try {
|
|
226
|
+
preview = await corpus.fetchManifest(ref);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
// A single unfetchable manifest must not sink the whole search.
|
|
230
|
+
console.warn(`[harness-layer] skipping manifest ${ref.manifestCid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const hit = toSearchHit(preview.ref, preview.envelope);
|
|
234
|
+
if (matchesQuery(hit, query))
|
|
235
|
+
hits.push(hit);
|
|
236
|
+
if (hits.length >= limit)
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
// Collapse to lineage heads (#1462) unless the caller opts out. Runs before
|
|
240
|
+
// the kind filter so a superseded skill is dropped regardless of the filter.
|
|
241
|
+
const heads = opts.includeSuperseded ? hits : await resolveHeads(hits);
|
|
242
|
+
// Client-side kind filter (spec §5: the indexer has no artifactType column,
|
|
243
|
+
// so this is a best-effort filter over the fetched page — a kind-filtered
|
|
244
|
+
// search may return fewer than `limit` if other kinds consumed the budget).
|
|
245
|
+
return opts.kind ? heads.filter((h) => h.kind === opts.kind) : heads;
|
|
246
|
+
}
|
|
247
|
+
async function get(ref) {
|
|
248
|
+
const preview = await corpus.fetchManifest(refForCid(ref));
|
|
249
|
+
const envelope = await corpus.acquire(preview);
|
|
250
|
+
const artifacts = envelope.envelope.artifacts.map((a) => {
|
|
251
|
+
const content = envelope.artifactContents.get(a.sha256);
|
|
252
|
+
if (!content)
|
|
253
|
+
throw new Error(`artifact ${a.sha256} missing from acquire result`);
|
|
254
|
+
return {
|
|
255
|
+
sha256: a.sha256,
|
|
256
|
+
artifactType: a.artifactType,
|
|
257
|
+
content: content.bytes,
|
|
258
|
+
source: content.source,
|
|
259
|
+
sizeBytes: content.bytes.length,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
return {
|
|
263
|
+
ref: envelope.ref.manifestCid,
|
|
264
|
+
envelope: envelope.envelope,
|
|
265
|
+
provenance: {
|
|
266
|
+
operator: {
|
|
267
|
+
agentId: envelope.ref.operator.agentId,
|
|
268
|
+
safeAddress: envelope.ref.operator.safeAddress || envelope.envelope.participant.safeAddress,
|
|
269
|
+
},
|
|
270
|
+
evidenceTier: envelope.ref.evidenceTier,
|
|
271
|
+
publishedAt: envelope.ref.publishedAt,
|
|
272
|
+
},
|
|
273
|
+
artifacts,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
config: resolved,
|
|
278
|
+
corpus: { search, get },
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer-side corpus doctor probes — the single shared implementation of the
|
|
3
|
+
* two "does the operator have corpus" checks the doctor renders.
|
|
4
|
+
*
|
|
5
|
+
* Two checks come out of one search against the corpus consume path:
|
|
6
|
+
* - `corpus-reachable` — is the corpus read path answering at all?
|
|
7
|
+
* - `corpus-content` — did the repo-slug query find enough evidence that
|
|
8
|
+
* interactive pickup can actually serve?
|
|
9
|
+
*
|
|
10
|
+
* Both are shared so the Python doctor (apps/jinn-agent/plugins/jinn/doctor.py)
|
|
11
|
+
* and the layer CLI (`jinn-layer corpus probe`) render identical semantics.
|
|
12
|
+
*
|
|
13
|
+
* The content check deliberately mirrors pickup's two-layer, fail-closed
|
|
14
|
+
* retrieval allowlist (#1824): the search hit must carry the canonical mark,
|
|
15
|
+
* then the fetched trace content must carry it too. Unmarked substrate and
|
|
16
|
+
* skills never satisfy onboarding, even though they remain valid corpus data.
|
|
17
|
+
*/
|
|
18
|
+
import type { HarnessLayer, CorpusSearchHit } from './consume.js';
|
|
19
|
+
/**
|
|
20
|
+
* How many matching corpus records a repo needs before Jinn considers itself
|
|
21
|
+
* "onboarded" for that repo. B's "enough corpus" guarantee — the sole source of
|
|
22
|
+
* truth for the K threshold, consumed by both `enoughCorpusForRepo` and the
|
|
23
|
+
* `corpus-content.ok` field below.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CORPUS_ONBOARDING_K = 3;
|
|
26
|
+
/**
|
|
27
|
+
* One doctor check result. Mirrors the Python doctor contract in
|
|
28
|
+
* apps/jinn-agent/plugins/jinn/doctor.py: a plain `{name, ok, detail}` dict with
|
|
29
|
+
* `remedy` present exactly when `ok` is false — EXCEPT informational checks,
|
|
30
|
+
* which never carry a remedy even when `ok` is false (there is nothing for the
|
|
31
|
+
* operator to fix, so there is no copy-paste command to offer). `corpus-content`
|
|
32
|
+
* is such an informational check.
|
|
33
|
+
*/
|
|
34
|
+
export type DoctorCheck = {
|
|
35
|
+
name: string;
|
|
36
|
+
ok: boolean;
|
|
37
|
+
detail: string;
|
|
38
|
+
remedy?: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* B's "enough corpus" guarantee: a repo has enough corpus when at least `k`
|
|
42
|
+
* retrieval-visible matching records survived both of pickup's visibility
|
|
43
|
+
* guards. The single source of truth for the K threshold —
|
|
44
|
+
* `corpusProbes`' `corpus-content.ok` MUST call this over the same admitted hits
|
|
45
|
+
* so the threshold cannot drift between the two.
|
|
46
|
+
*/
|
|
47
|
+
export declare function enoughCorpusForRepo(hits: CorpusSearchHit[], k?: number): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Run the two corpus doctor probes off one corpus search plus bounded fetches
|
|
50
|
+
* for search-marked candidates.
|
|
51
|
+
*
|
|
52
|
+
* Search failures are never re-thrown: they are reported as a
|
|
53
|
+
* failing `corpus-reachable` check plus an informational (remedy-free)
|
|
54
|
+
* `corpus-content` that says it was not checked. Individual candidate fetch or
|
|
55
|
+
* decode failures simply fail closed for content counting.
|
|
56
|
+
*/
|
|
57
|
+
export declare function corpusProbes({ layer, repoSlug, k, }: {
|
|
58
|
+
layer: HarnessLayer;
|
|
59
|
+
repoSlug: string;
|
|
60
|
+
k?: number;
|
|
61
|
+
}): Promise<DoctorCheck[]>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer-side corpus doctor probes — the single shared implementation of the
|
|
3
|
+
* two "does the operator have corpus" checks the doctor renders.
|
|
4
|
+
*
|
|
5
|
+
* Two checks come out of one search against the corpus consume path:
|
|
6
|
+
* - `corpus-reachable` — is the corpus read path answering at all?
|
|
7
|
+
* - `corpus-content` — did the repo-slug query find enough evidence that
|
|
8
|
+
* interactive pickup can actually serve?
|
|
9
|
+
*
|
|
10
|
+
* Both are shared so the Python doctor (apps/jinn-agent/plugins/jinn/doctor.py)
|
|
11
|
+
* and the layer CLI (`jinn-layer corpus probe`) render identical semantics.
|
|
12
|
+
*
|
|
13
|
+
* The content check deliberately mirrors pickup's two-layer, fail-closed
|
|
14
|
+
* retrieval allowlist (#1824): the search hit must carry the canonical mark,
|
|
15
|
+
* then the fetched trace content must carry it too. Unmarked substrate and
|
|
16
|
+
* skills never satisfy onboarding, even though they remain valid corpus data.
|
|
17
|
+
*/
|
|
18
|
+
import { deriveRepositorySearchTerms, hasRetrievalMark } from '@jinn-network/plugin';
|
|
19
|
+
import { createCorpusAdapter } from './adapters/corpus-adapter.js';
|
|
20
|
+
/**
|
|
21
|
+
* How many matching corpus records a repo needs before Jinn considers itself
|
|
22
|
+
* "onboarded" for that repo. B's "enough corpus" guarantee — the sole source of
|
|
23
|
+
* truth for the K threshold, consumed by both `enoughCorpusForRepo` and the
|
|
24
|
+
* `corpus-content.ok` field below.
|
|
25
|
+
*/
|
|
26
|
+
export const CORPUS_ONBOARDING_K = 3;
|
|
27
|
+
/**
|
|
28
|
+
* The consume path has no pagination cursor. Ask for a page comfortably wider
|
|
29
|
+
* than K so unmarked substrate at the front cannot hide the curated records
|
|
30
|
+
* behind it. The doctor is a full/manual check, not a session-start hot path.
|
|
31
|
+
*/
|
|
32
|
+
const CORPUS_PROBE_CANDIDATE_LIMIT = 50;
|
|
33
|
+
/**
|
|
34
|
+
* B's "enough corpus" guarantee: a repo has enough corpus when at least `k`
|
|
35
|
+
* retrieval-visible matching records survived both of pickup's visibility
|
|
36
|
+
* guards. The single source of truth for the K threshold —
|
|
37
|
+
* `corpusProbes`' `corpus-content.ok` MUST call this over the same admitted hits
|
|
38
|
+
* so the threshold cannot drift between the two.
|
|
39
|
+
*/
|
|
40
|
+
export function enoughCorpusForRepo(hits, k = CORPUS_ONBOARDING_K) {
|
|
41
|
+
return hits.length >= k;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run the two corpus doctor probes off one corpus search plus bounded fetches
|
|
45
|
+
* for search-marked candidates.
|
|
46
|
+
*
|
|
47
|
+
* Search failures are never re-thrown: they are reported as a
|
|
48
|
+
* failing `corpus-reachable` check plus an informational (remedy-free)
|
|
49
|
+
* `corpus-content` that says it was not checked. Individual candidate fetch or
|
|
50
|
+
* decode failures simply fail closed for content counting.
|
|
51
|
+
*/
|
|
52
|
+
export async function corpusProbes({ layer, repoSlug, k = CORPUS_ONBOARDING_K, }) {
|
|
53
|
+
const repoTerms = deriveRepositorySearchTerms(repoSlug);
|
|
54
|
+
// An empty term set means pickup has no repository vocabulary (for example,
|
|
55
|
+
// a two-character repo name). Probe the empty query only for reachability;
|
|
56
|
+
// it must not make arbitrary corpus records count as repo content.
|
|
57
|
+
const searchTerms = repoTerms.length > 0 ? repoTerms : [''];
|
|
58
|
+
let hits;
|
|
59
|
+
try {
|
|
60
|
+
const pages = await Promise.all(searchTerms.map((term) => layer.corpus.search(term, {
|
|
61
|
+
limit: Math.max(CORPUS_PROBE_CANDIDATE_LIMIT, k),
|
|
62
|
+
})));
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
hits = pages.flat().filter((hit) => {
|
|
65
|
+
if (seen.has(hit.ref))
|
|
66
|
+
return false;
|
|
67
|
+
seen.add(hit.ref);
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
name: 'corpus-reachable',
|
|
76
|
+
ok: false,
|
|
77
|
+
detail: `unreachable — ${reason}`,
|
|
78
|
+
remedy: 'check network / discovery config (JINN_DISCOVERY_URL) and re-run',
|
|
79
|
+
},
|
|
80
|
+
// Informational: never carries a remedy — there is nothing to fix here, the
|
|
81
|
+
// reachability failure above owns the remedy.
|
|
82
|
+
{ name: 'corpus-content', ok: false, detail: 'not checked — corpus unreachable' },
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
// Ranking-side visibility gate: this is the same canonical mark helper the
|
|
86
|
+
// corpus adapter uses to set KnowledgeHit.retrievalVisible. Skills are not
|
|
87
|
+
// retrieval evidence, even if a malformed producer tagged one.
|
|
88
|
+
const markedCandidates = (repoTerms.length > 0 ? hits : []).filter((hit) => hit.kind !== 'skill' && hasRetrievalMark(hit.tags ?? []));
|
|
89
|
+
// Content-side visibility gate: createCorpusAdapter is the exact decoder the
|
|
90
|
+
// plugin pickup wiring uses. It verifies the trace artifact and computes
|
|
91
|
+
// retrievalVisible from the canonical envelope distributionTags, so the
|
|
92
|
+
// doctor cannot drift into a parallel admission policy.
|
|
93
|
+
const corpus = createCorpusAdapter({ layer });
|
|
94
|
+
const admittedHits = [];
|
|
95
|
+
for (const hit of markedCandidates) {
|
|
96
|
+
if (admittedHits.length >= k)
|
|
97
|
+
break;
|
|
98
|
+
const result = await corpus.get(hit.ref);
|
|
99
|
+
if (result.status !== 'ok' || result.value === null)
|
|
100
|
+
continue;
|
|
101
|
+
if (result.value.isSkillPayload === true || result.value.retrievalVisible !== true)
|
|
102
|
+
continue;
|
|
103
|
+
admittedHits.push(hit);
|
|
104
|
+
}
|
|
105
|
+
const contentOk = enoughCorpusForRepo(admittedHits, k);
|
|
106
|
+
return [
|
|
107
|
+
// Reachable is about the read path answering; 0 records is still reachable
|
|
108
|
+
// (empty is non-blocking nothing-found, not an error).
|
|
109
|
+
{ name: 'corpus-reachable', ok: true, detail: `reachable — ${hits.length} record(s)` },
|
|
110
|
+
// Informational: no remedy field EVER — a bare corpus is expected while the
|
|
111
|
+
// repo has no records yet; Jinn stays quiet until content exists.
|
|
112
|
+
{
|
|
113
|
+
name: 'corpus-content',
|
|
114
|
+
ok: contentOk,
|
|
115
|
+
detail: contentOk
|
|
116
|
+
? `${admittedHits.length} retrieval-visible matching record(s)`
|
|
117
|
+
: 'no matching content yet; Jinn stays quiet until it exists',
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CorpusStorePort, NetworkArtifactRow, SaveNetworkArtifactInput, ServedArtifactRow } from '@jinn-network/core/corpus-read';
|
|
2
|
+
/**
|
|
3
|
+
* Standalone corpus cache implementing core's read-side store port. The table
|
|
4
|
+
* names and columns intentionally match the client database so an existing
|
|
5
|
+
* `JINN_LAYER_DB_PATH` remains readable after extraction.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SqliteCorpusStore implements CorpusStorePort {
|
|
8
|
+
#private;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
constructor(path: string);
|
|
11
|
+
close(): void;
|
|
12
|
+
getNetworkArtifact(sha256: string): NetworkArtifactRow | null;
|
|
13
|
+
touchNetworkArtifactUsage(sha256: string, usedAt: string): void;
|
|
14
|
+
saveNetworkArtifact(input: SaveNetworkArtifactInput): void;
|
|
15
|
+
getServedArtifact(sha256: string): ServedArtifactRow | null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Standalone corpus cache implementing core's read-side store port. The table
|
|
6
|
+
* names and columns intentionally match the client database so an existing
|
|
7
|
+
* `JINN_LAYER_DB_PATH` remains readable after extraction.
|
|
8
|
+
*/
|
|
9
|
+
export class SqliteCorpusStore {
|
|
10
|
+
path;
|
|
11
|
+
#db;
|
|
12
|
+
constructor(path) {
|
|
13
|
+
this.path = path;
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
+
this.#db = new Database(path);
|
|
16
|
+
this.#db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS served_artifacts (
|
|
18
|
+
sha256 TEXT PRIMARY KEY,
|
|
19
|
+
artifact_type TEXT NOT NULL,
|
|
20
|
+
request_id TEXT,
|
|
21
|
+
envelope_cid TEXT,
|
|
22
|
+
content BLOB NOT NULL,
|
|
23
|
+
content_size INTEGER NOT NULL,
|
|
24
|
+
price_usdc TEXT NOT NULL,
|
|
25
|
+
created_at TEXT NOT NULL
|
|
26
|
+
);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS network_artifacts (
|
|
28
|
+
sha256 TEXT PRIMARY KEY,
|
|
29
|
+
artifact_type TEXT NOT NULL,
|
|
30
|
+
envelope_cid TEXT,
|
|
31
|
+
content BLOB NOT NULL,
|
|
32
|
+
content_size INTEGER NOT NULL,
|
|
33
|
+
source TEXT NOT NULL CHECK (
|
|
34
|
+
source IN ('origin', 'route-resolver', 'self-store-mirror')
|
|
35
|
+
),
|
|
36
|
+
source_operator TEXT,
|
|
37
|
+
source_endpoint TEXT,
|
|
38
|
+
paid_amount_usdc TEXT NOT NULL,
|
|
39
|
+
fetched_at TEXT NOT NULL,
|
|
40
|
+
last_used_at TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
this.#db.close();
|
|
46
|
+
}
|
|
47
|
+
getNetworkArtifact(sha256) {
|
|
48
|
+
const row = this.#db
|
|
49
|
+
.prepare(`SELECT sha256, artifact_type, envelope_cid, content, source,
|
|
50
|
+
source_operator, source_endpoint, paid_amount_usdc, fetched_at
|
|
51
|
+
FROM network_artifacts
|
|
52
|
+
WHERE sha256 = ?`)
|
|
53
|
+
.get(sha256);
|
|
54
|
+
return row
|
|
55
|
+
? {
|
|
56
|
+
sha256: row.sha256,
|
|
57
|
+
artifactType: row.artifact_type,
|
|
58
|
+
content: row.content,
|
|
59
|
+
sourceOperator: row.source_operator,
|
|
60
|
+
fetchedAt: row.fetched_at,
|
|
61
|
+
}
|
|
62
|
+
: null;
|
|
63
|
+
}
|
|
64
|
+
touchNetworkArtifactUsage(sha256, usedAt) {
|
|
65
|
+
this.#db
|
|
66
|
+
.prepare('UPDATE network_artifacts SET last_used_at = ? WHERE sha256 = ?')
|
|
67
|
+
.run(usedAt, sha256);
|
|
68
|
+
}
|
|
69
|
+
saveNetworkArtifact(input) {
|
|
70
|
+
this.#db
|
|
71
|
+
.prepare(`INSERT OR REPLACE INTO network_artifacts
|
|
72
|
+
(sha256, artifact_type, envelope_cid, content, content_size, source,
|
|
73
|
+
source_operator, source_endpoint, paid_amount_usdc, fetched_at,
|
|
74
|
+
last_used_at)
|
|
75
|
+
VALUES
|
|
76
|
+
(@sha256, @artifactType, @envelopeCid, @content, @contentSize,
|
|
77
|
+
@source, @sourceOperator, @sourceEndpoint, @paidAmountUsdc,
|
|
78
|
+
@fetchedAt, @fetchedAt)`)
|
|
79
|
+
.run({
|
|
80
|
+
...input,
|
|
81
|
+
envelopeCid: input.envelopeCid ?? null,
|
|
82
|
+
contentSize: input.content.length,
|
|
83
|
+
sourceOperator: input.sourceOperator ?? null,
|
|
84
|
+
sourceEndpoint: input.sourceEndpoint ?? null,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
getServedArtifact(sha256) {
|
|
88
|
+
const row = this.#db
|
|
89
|
+
.prepare(`SELECT artifact_type, envelope_cid, content
|
|
90
|
+
FROM served_artifacts
|
|
91
|
+
WHERE sha256 = ?`)
|
|
92
|
+
.get(sha256);
|
|
93
|
+
return row
|
|
94
|
+
? {
|
|
95
|
+
artifactType: row.artifact_type,
|
|
96
|
+
envelopeCid: row.envelope_cid,
|
|
97
|
+
content: row.content,
|
|
98
|
+
}
|
|
99
|
+
: null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { EpisodeV1, LocalLearningSkill } from '@jinn-network/plugin';
|
|
2
|
+
import { type CapturedTask } from './capture.js';
|
|
3
|
+
/** Deprecated CapturedTask directory retained as a read-only fallback. */
|
|
4
|
+
export declare const DEFAULT_CAPTURES_DIR: string;
|
|
5
|
+
/** Canonical complete EpisodeV1 records, distinct from legacy distill captures. */
|
|
6
|
+
export declare const DEFAULT_EPISODES_DIR: string;
|
|
7
|
+
/** Local skills library `distill` installs into by default. */
|
|
8
|
+
export declare const DEFAULT_SKILLS_INSTALL_DIR: string;
|
|
9
|
+
/** How many recent own captures `distill` considers when --limit is unset. */
|
|
10
|
+
export declare const DEFAULT_DISTILL_CAPTURE_LIMIT = 50;
|
|
11
|
+
/**
|
|
12
|
+
* Load the operator's most recent own captures from `dir`. Every `*.json` file
|
|
13
|
+
* must be a `CapturedTask`; malformed files are skipped with a stderr warning
|
|
14
|
+
* so machine-readable stdout stays parseable.
|
|
15
|
+
*/
|
|
16
|
+
export declare function loadRecentCaptures(dir: string, limit: number): CapturedTask[];
|
|
17
|
+
/**
|
|
18
|
+
* Project the canonical local evidence record into the input shape consumed by
|
|
19
|
+
* the existing rung-1 distillation engine. The projection is intentionally
|
|
20
|
+
* lossless for trace facts the distiller understands; episode-only state stays
|
|
21
|
+
* in the canonical record and is never copied to another store.
|
|
22
|
+
*/
|
|
23
|
+
export declare function episodeToCapturedTask(episode: EpisodeV1): CapturedTask;
|
|
24
|
+
export interface DistillSourceOptions {
|
|
25
|
+
/** Canonical EpisodeV1 store. */
|
|
26
|
+
episodesDir: string;
|
|
27
|
+
/** Deprecated CapturedTask store, retained for read compatibility only. */
|
|
28
|
+
legacyCapturesDir?: string;
|
|
29
|
+
limit: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load recent local-learning inputs from the canonical episode store, then
|
|
33
|
+
* merge historical CapturedTask files during the deprecation window. A
|
|
34
|
+
* canonical episode always wins for a duplicate session id, even when the old
|
|
35
|
+
* file has a later timestamp, and the limit is applied only after global sort.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadRecentDistillSources(options: DistillSourceOptions): Promise<CapturedTask[]>;
|
|
38
|
+
/** Staging directory beside the active generated-skill directory. */
|
|
39
|
+
export declare function stagingDirFor(activeDir: string): string;
|
|
40
|
+
/** Derive local skill history from the canonical active/staged SKILL.md files. */
|
|
41
|
+
export declare function localSkillProvenance(activeDir: string, stagedDir?: string): LocalLearningSkill[];
|
|
42
|
+
/**
|
|
43
|
+
* The session ids already covered by a generated skill under any of `dirs`.
|
|
44
|
+
* Both active and staged skill dirs count so `--resume` never re-spends a
|
|
45
|
+
* capture that already produced a skill.
|
|
46
|
+
*/
|
|
47
|
+
export declare function coveredSessionIds(dirs: string[]): Set<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Map local provenance refs to human labels for run panels. Unknown refs remain
|
|
50
|
+
* unchanged so non-local provenance stays auditable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function provenanceLabels(pkg: {
|
|
53
|
+
jinn: {
|
|
54
|
+
provenance: string[];
|
|
55
|
+
};
|
|
56
|
+
}, summaryBySession: Map<string, string>): string[];
|