@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
@@ -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[];