@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,18 @@
1
+ import { type ContributionPort, type EvidencePort } from '@jinn-network/plugin';
2
+ import { ContributionStore } from '@jinn-network/core';
3
+ /** Compatibility name retained for existing composition roots. */
4
+ export type ContributionStatusStore = ContributionStore;
5
+ /**
6
+ * Compatibility factory retained for callers that previously supplied a
7
+ * sidecar status-file path. It now opens the canonical contribution store at
8
+ * exactly that path, so adapter writes and daemon reads share one file.
9
+ */
10
+ export declare function createContributionStatusStore(path: string): ContributionStatusStore;
11
+ export interface ContributionAdapterDeps {
12
+ /** `statusStore` is retained as a source-compatible dependency name. */
13
+ statusStore: ContributionStatusStore;
14
+ /** Canonical payload resolver; the status store contains references only. */
15
+ evidence: Pick<EvidencePort, 'get'>;
16
+ now?: () => string;
17
+ }
18
+ export declare function createContributionAdapter(deps: ContributionAdapterDeps): ContributionPort;
@@ -0,0 +1,134 @@
1
+ /** ContributionPort projection over the shared daemon/jinn-layer store. */
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { deriveContributionStatus, degraded, ok, unavailable, ContributionCandidateV1ProjectionSchema, ContributionCandidateV1Schema, } from '@jinn-network/plugin';
4
+ import { ContributionStore } from '@jinn-network/core';
5
+ /**
6
+ * Compatibility factory retained for callers that previously supplied a
7
+ * sidecar status-file path. It now opens the canonical contribution store at
8
+ * exactly that path, so adapter writes and daemon reads share one file.
9
+ */
10
+ export function createContributionStatusStore(path) {
11
+ return new ContributionStore({ filePath: path });
12
+ }
13
+ function snapshot(record) {
14
+ if (!record)
15
+ return null;
16
+ return {
17
+ localState: record.localState,
18
+ publicationState: record.publicationState,
19
+ ...(record.mintRef ? { mintRef: record.mintRef } : {}),
20
+ ...(record.publicationRef ? { publicationRef: record.publicationRef } : {}),
21
+ status: deriveContributionStatus(record),
22
+ };
23
+ }
24
+ export function createContributionAdapter(deps) {
25
+ const store = deps.statusStore;
26
+ const now = deps.now ?? (() => new Date().toISOString());
27
+ return {
28
+ async recordMineable(candidate, options) {
29
+ try {
30
+ const parsed = ContributionCandidateV1Schema.parse(candidate);
31
+ const evidence = await deps.evidence.get(parsed.sourceId);
32
+ const episode = evidence.status === 'unavailable' ? null : evidence.value;
33
+ const canonicalCandidate = episode?.contributionCandidate === undefined
34
+ ? undefined
35
+ : ContributionCandidateV1ProjectionSchema.safeParse(episode.contributionCandidate);
36
+ if (!episode) {
37
+ return unavailable(`canonical contribution episode is unavailable: ${parsed.sourceId}`);
38
+ }
39
+ if (!canonicalCandidate?.success
40
+ || canonicalCandidate.data.sourceId !== episode.episodeId
41
+ || !isDeepStrictEqual(canonicalCandidate.data, parsed)) {
42
+ return unavailable(`canonical contribution candidate mismatch: ${parsed.sourceId}`);
43
+ }
44
+ const record = await store.recordReference(parsed.sourceId, {
45
+ publishMinedTasksConsent: parsed.publishMinedTasksConsent,
46
+ ...options,
47
+ });
48
+ return ok({ recordId: record.recordId });
49
+ }
50
+ catch (error) {
51
+ return unavailable(`contribution store recordMineable failed: ${String(error)}`);
52
+ }
53
+ },
54
+ async ledger() {
55
+ try {
56
+ const reasons = [];
57
+ const entries = await Promise.all((await store.list()).map(async (record) => {
58
+ const evidence = await deps.evidence.get(record.recordId);
59
+ const episode = evidence.status === 'unavailable' ? null : evidence.value;
60
+ const parsedCandidate = episode?.contributionCandidate === undefined
61
+ ? undefined
62
+ : ContributionCandidateV1ProjectionSchema.safeParse(episode.contributionCandidate);
63
+ const candidate = parsedCandidate?.success && parsedCandidate.data.sourceId === record.recordId
64
+ ? parsedCandidate.data
65
+ : undefined;
66
+ if (!candidate)
67
+ reasons.push(`unresolved canonical episode: ${record.recordId}`);
68
+ return {
69
+ recordId: record.recordId,
70
+ sourceId: record.recordId,
71
+ ...(candidate ? {
72
+ createdAt: candidate.createdAt,
73
+ verifiabilityTier: candidate.testRuns.some((run) => run.exitCode === 0)
74
+ ? 'tests-passed'
75
+ : 'user-accepted',
76
+ repositorySlug: candidate.repositorySlug,
77
+ baseCommit: candidate.baseCommit,
78
+ } : {}),
79
+ localState: record.localState,
80
+ publicationState: record.publicationState,
81
+ ...(record.mintRef ? { mintRef: record.mintRef } : {}),
82
+ ...(record.publicationRef ? { publicationRef: record.publicationRef } : {}),
83
+ status: deriveContributionStatus(record),
84
+ };
85
+ }));
86
+ return reasons.length > 0
87
+ ? degraded([...new Set(reasons)].join('; '), entries)
88
+ : ok(entries);
89
+ }
90
+ catch (error) {
91
+ return unavailable(`contribution ledger failed: ${String(error)}`);
92
+ }
93
+ },
94
+ async mintStatus(recordId) {
95
+ try {
96
+ const value = snapshot(await store.get(recordId));
97
+ return value ? ok(value) : unavailable(`no such record: ${recordId}`);
98
+ }
99
+ catch (error) {
100
+ return unavailable(`contribution status failed: ${String(error)}`);
101
+ }
102
+ },
103
+ async authorize(recordId) {
104
+ try {
105
+ const record = await store.authorize(recordId, now());
106
+ const status = deriveContributionStatus(record);
107
+ if (record.publicationState !== 'queued' || status !== 'queued') {
108
+ return unavailable(`contribution ${recordId} did not enter the queued state`);
109
+ }
110
+ return ok({ recordId, publicationState: 'queued', status: 'queued' });
111
+ }
112
+ catch (error) {
113
+ return unavailable(`contribution authorization failed: ${String(error)}`);
114
+ }
115
+ },
116
+ async veto(recordId) {
117
+ try {
118
+ await store.veto(recordId);
119
+ return ok({ recordId, publicationState: 'vetoed', status: 'vetoed' });
120
+ }
121
+ catch (error) {
122
+ return unavailable(`contribution veto failed: ${String(error)}`);
123
+ }
124
+ },
125
+ async disableUnpublished() {
126
+ try {
127
+ return ok({ recordIds: await store.disableUnpublished() });
128
+ }
129
+ catch (error) {
130
+ return unavailable(`contribution disable failed: ${String(error)}`);
131
+ }
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,12 @@
1
+ import type { CorpusPort } from '@jinn-network/plugin';
2
+ import { type HarnessLayer, type HarnessLayerConfig } from '../consume.js';
3
+ export interface CorpusAdapterDeps {
4
+ /** A ready HarnessLayer (test seam: a fake `{ corpus: { search, get } }`). */
5
+ layer: HarnessLayer;
6
+ }
7
+ /**
8
+ * Build a CorpusPort over a HarnessLayer. Pass `{ layer }` (a real or fake
9
+ * layer) or a `HarnessLayerConfig` (a real layer is constructed via
10
+ * `createHarnessLayer`).
11
+ */
12
+ export declare function createCorpusAdapter(deps: CorpusAdapterDeps | HarnessLayerConfig): CorpusPort;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * CorpusPort adapter (#1660; content-bearing get() per Stage 1 rescope R2,
3
+ * #1772) — shims `HarnessLayer.corpus` onto the plugin's `CorpusPort`.
4
+ * `search` maps `CorpusSearchHit[]` → `KnowledgeHit[]`, carrying the
5
+ * evidence-first selection fields (tags/origin/publishedAt) alongside the
6
+ * pre-existing title/snippet. `get` decodes and sha256-verifies the
7
+ * `jinn.trace-envelope.v0` artifact the same way the Python
8
+ * `skills_install._extract_trace` path does, returning full content — not
9
+ * metadata — or a typed degraded reason on a corrupt/unreadable record.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ import { degraded, EpisodeV1Schema, ok, hasRetrievalMark, RETRIEVAL_VISIBLE_TAG, } from '@jinn-network/plugin';
13
+ import { createHarnessLayer, } from '../consume.js';
14
+ import { parseTraceEnvelopeV0 } from '../envelope.js';
15
+ import { EPISODE_ARTIFACT_TYPE, TRACE_ENVELOPE_ARTIFACT_TYPE, } from '../publish.js';
16
+ import { SKILL_ARTIFACT_TYPE } from '@jinn-network/core';
17
+ function isLayerDeps(deps) {
18
+ return 'layer' in deps;
19
+ }
20
+ /**
21
+ * `CorpusSearchHit` → `KnowledgeHit`. `summary` → `snippet`; `score` omitted.
22
+ * `origin` is the identity used by cross-record content dedup: prefer the
23
+ * on-chain-derived `agentId`, otherwise use the record ref so unattributed
24
+ * records cannot collide through a forged manifest `safeAddress`.
25
+ */
26
+ function toKnowledgeHit(hit) {
27
+ const origin = hit.operator.agentId || hit.ref;
28
+ const tags = hit.tags ?? [];
29
+ // Named indexed metadata is authoritative when present. The legacy mark is
30
+ // only a fallback for rows indexed before that field existed.
31
+ const retrievalVisible = hit.retrievalVisible ?? hasRetrievalMark(tags);
32
+ return {
33
+ ref: hit.ref,
34
+ kind: hit.kind === 'skill' ? 'skill' : 'trace',
35
+ ...(hit.title ? { title: hit.title } : {}),
36
+ ...(hit.summary ? { snippet: hit.summary } : {}),
37
+ tags: tags.filter((tag) => tag !== RETRIEVAL_VISIBLE_TAG),
38
+ ...(origin ? { origin } : {}),
39
+ publishedAt: hit.publishedAt,
40
+ retrievalVisible,
41
+ };
42
+ }
43
+ function seedStepSynthesis(steps) {
44
+ for (const step of steps) {
45
+ const value = step.attributes['seed.synthesis'];
46
+ if (typeof value === 'string' && value.trim().length > 0)
47
+ return value;
48
+ }
49
+ return undefined;
50
+ }
51
+ /**
52
+ * True when any step carries a string `skill.md` attribute — the legacy
53
+ * pre-#1394 seed shape (`seed-import/execute.ts` `toCapturedTask`). Checks
54
+ * every step, not only one named `seed:skill-md`: the wire shape this guard
55
+ * exists for already lied about `kind` once, so detection here keys on the
56
+ * attribute itself, not a step-naming convention layered on top of it.
57
+ */
58
+ function hasSkillMdAttribute(steps) {
59
+ return steps.some((step) => {
60
+ const value = step.attributes['skill.md'];
61
+ return typeof value === 'string' && value.length > 0;
62
+ });
63
+ }
64
+ /**
65
+ * Content-level skill-payload classification (mono #1782): true when the
66
+ * record is a skill package regardless of the search hit's wire `kind` —
67
+ * either a first-class `jinn.skill.v1` artifact (checked by presence only,
68
+ * not parsed/validated here — parsing a malformed skill artifact's body is
69
+ * `extractSkill()`'s job, and must not be able to fail *this* guard), or the
70
+ * legacy seeded shape `hasSkillMdAttribute` recognises.
71
+ */
72
+ function detectSkillPayload(record, steps) {
73
+ if (record.artifacts.some((a) => a.artifactType === SKILL_ARTIFACT_TYPE))
74
+ return true;
75
+ return hasSkillMdAttribute(steps);
76
+ }
77
+ function decodeEvidencePayload(artifactType, content) {
78
+ const body = JSON.parse(content.toString('utf-8'));
79
+ if (artifactType === EPISODE_ARTIFACT_TYPE) {
80
+ const declaresRetrievalVisible = body !== null
81
+ && typeof body === 'object'
82
+ && Object.prototype.hasOwnProperty.call(body, 'retrievalVisible');
83
+ const episode = EpisodeV1Schema.parse(body);
84
+ return {
85
+ task: episode.task,
86
+ ...(declaresRetrievalVisible
87
+ ? { retrievalVisible: episode.retrievalVisible }
88
+ : {}),
89
+ outcome: episode.outcome,
90
+ steps: episode.trajectory,
91
+ provenance: episode.provenance,
92
+ capturedAt: episode.session.capturedAt,
93
+ };
94
+ }
95
+ const trace = parseTraceEnvelopeV0(body);
96
+ return {
97
+ task: trace.task,
98
+ retrievalVisible: hasRetrievalMark(trace.task.distributionTags),
99
+ outcome: {
100
+ status: trace.outcome.status,
101
+ verificationStrength: trace.outcome.verifiabilityTier,
102
+ ...(trace.outcome.summary ? { summary: trace.outcome.summary } : {}),
103
+ },
104
+ steps: trace.steps,
105
+ provenance: trace.provenance,
106
+ capturedAt: trace.session.capturedAt,
107
+ };
108
+ }
109
+ /**
110
+ * Decode the record's canonical `jinn.episode.v1` artifact (or frozen
111
+ * `jinn.trace-envelope.v0` read-compat artifact) into the plugin's
112
+ * content-bearing `CorpusRecord`. Verifies the artifact bytes against the
113
+ * record's own sha256 before parsing — a mismatch refuses to serve
114
+ * unverified content (mirrors Python `_extract_trace`). Returns `null` when
115
+ * the record carries no trace-envelope artifact (e.g. a skill-only record;
116
+ * pickup selection never fetches these, but the port stays honest for any
117
+ * other caller).
118
+ */
119
+ function decodeRecord(record) {
120
+ const artifact = record.artifacts.find((a) => a.artifactType === EPISODE_ARTIFACT_TYPE)
121
+ ?? record.artifacts.find((a) => a.artifactType === TRACE_ENVELOPE_ARTIFACT_TYPE);
122
+ if (!artifact)
123
+ return null;
124
+ const actualSha256 = createHash('sha256').update(artifact.content).digest('hex');
125
+ if (actualSha256 !== artifact.sha256) {
126
+ throw new Error(`sha256 mismatch decoding ${record.ref} — expected ${artifact.sha256.slice(0, 12)}…, `
127
+ + `got ${actualSha256.slice(0, 12)}… — refusing to serve unverified content`);
128
+ }
129
+ const evidence = decodeEvidencePayload(artifact.artifactType, artifact.content);
130
+ // Packet attribution is display-only, so safeAddress remains an acceptable
131
+ // fallback here after trustworthy agentId; it is never used for hit dedup.
132
+ const origin = record.provenance.operator.agentId || record.provenance.operator.safeAddress || record.ref;
133
+ const synthesis = evidence.outcome.summary ?? seedStepSynthesis(evidence.steps);
134
+ // Fails open (mono #1782): a detection error never blocks the rest of the
135
+ // record's content from being served — it just leaves the fact
136
+ // undetermined, exactly as if this guard did not exist for that record.
137
+ let isSkillPayload = false;
138
+ try {
139
+ isSkillPayload = detectSkillPayload(record, evidence.steps);
140
+ }
141
+ catch {
142
+ isSkillPayload = false;
143
+ }
144
+ // Content remains the guard: a named canonical boolean is authoritative
145
+ // when the raw body declared it, and only legacy absence falls back to the
146
+ // compatibility mark.
147
+ const retrievalVisible = evidence.retrievalVisible ?? hasRetrievalMark(evidence.task.distributionTags);
148
+ return {
149
+ ref: record.ref,
150
+ task: {
151
+ summary: evidence.task.summary,
152
+ ...(evidence.task.repositorySlug
153
+ ? { repositorySlug: evidence.task.repositorySlug }
154
+ : {}),
155
+ },
156
+ outcome: {
157
+ status: evidence.outcome.status,
158
+ verifiabilityTier: evidence.outcome.verificationStrength,
159
+ },
160
+ ...(synthesis ? { synthesis } : {}),
161
+ steps: evidence.steps.map((step) => ({ name: step.name, attributes: step.attributes })),
162
+ // Deliberately un-stripped (unlike the search-hit path): CorpusRecord.tags
163
+ // is provenance-adjacent display data on the fetched packet, never fed to
164
+ // the scoring haystack — hiding the mark here would hide provenance.
165
+ tags: evidence.task.distributionTags,
166
+ retrievalVisible,
167
+ provenance: evidence.provenance,
168
+ origin,
169
+ capturedAt: evidence.capturedAt,
170
+ ...(isSkillPayload ? { isSkillPayload: true } : {}),
171
+ };
172
+ }
173
+ /**
174
+ * Build a CorpusPort over a HarnessLayer. Pass `{ layer }` (a real or fake
175
+ * layer) or a `HarnessLayerConfig` (a real layer is constructed via
176
+ * `createHarnessLayer`).
177
+ */
178
+ export function createCorpusAdapter(deps) {
179
+ const layer = isLayerDeps(deps) ? deps.layer : createHarnessLayer(deps);
180
+ return {
181
+ async search(query) {
182
+ try {
183
+ const hits = await layer.corpus.search(query);
184
+ return ok(hits.map(toKnowledgeHit));
185
+ }
186
+ catch (e) {
187
+ // Empty array keeps callers alive when discovery is unreachable.
188
+ return degraded(`corpus search failed: ${String(e)}`, []);
189
+ }
190
+ },
191
+ async get(ref) {
192
+ let record;
193
+ try {
194
+ record = await layer.corpus.get(ref);
195
+ }
196
+ catch {
197
+ // `corpus.get` throws on a missing manifest, and the contract requires
198
+ // `ok(null)` for that unknown-ref path (Stage 1). A genuine transport
199
+ // error is currently indistinguishable from not-found via the throw, so
200
+ // it also collapses to `ok(null)` here; distinguishing the two is a
201
+ // follow-up on `consume.ts`'s `corpus.get` surface, not this shim.
202
+ return ok(null);
203
+ }
204
+ try {
205
+ return ok(decodeRecord(record));
206
+ }
207
+ catch (e) {
208
+ return degraded(`corpus record ${ref} failed to decode: ${String(e)}`, null);
209
+ }
210
+ },
211
+ };
212
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Harness-layer → plugin-port adapter shims (#1660, Stage 1).
3
+ *
4
+ * Each `createXAdapter(deps)` factory returns an object that `implements` the
5
+ * matching `@jinn-network/plugin` port and passes that port's
6
+ * `describeXPortContract` from `@jinn-network/plugin/testing`. Every method
7
+ * converts internal throws into a typed `PortResult` (`ok`/`degraded`/
8
+ * `unavailable`) so an adapter never crashes across the port boundary.
9
+ *
10
+ * The dependency arrow is one-way: harness-layer imports the ports/schemas
11
+ * FROM `@jinn-network/plugin`; the plugin package never imports `client/**`.
12
+ */
13
+ export { createCorpusAdapter } from './corpus-adapter.js';
14
+ export { createEvidenceAdapter, capturedTaskToEpisode } from '@jinn-network/core';
15
+ export { createContributionAdapter, createContributionStatusStore, type ContributionStatusStore, } from './contribution-adapter.js';
16
+ export { createLocalLearningAdapter } from './local-learning-adapter.js';
17
+ export { createSkillsAdapter } from './skills-adapter.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Harness-layer → plugin-port adapter shims (#1660, Stage 1).
3
+ *
4
+ * Each `createXAdapter(deps)` factory returns an object that `implements` the
5
+ * matching `@jinn-network/plugin` port and passes that port's
6
+ * `describeXPortContract` from `@jinn-network/plugin/testing`. Every method
7
+ * converts internal throws into a typed `PortResult` (`ok`/`degraded`/
8
+ * `unavailable`) so an adapter never crashes across the port boundary.
9
+ *
10
+ * The dependency arrow is one-way: harness-layer imports the ports/schemas
11
+ * FROM `@jinn-network/plugin`; the plugin package never imports `client/**`.
12
+ */
13
+ export { createCorpusAdapter } from './corpus-adapter.js';
14
+ export { createEvidenceAdapter, capturedTaskToEpisode } from '@jinn-network/core';
15
+ export { createContributionAdapter, createContributionStatusStore, } from './contribution-adapter.js';
16
+ export { createLocalLearningAdapter } from './local-learning-adapter.js';
17
+ export { createSkillsAdapter } from './skills-adapter.js';
@@ -0,0 +1,4 @@
1
+ /** Read the JSON map at `path`. Missing / corrupt / non-object ⇒ `{}`. */
2
+ export declare function readJsonMap<T>(path: string): Record<string, T>;
3
+ /** Atomically write `map` to `path` (temp + rename), creating the dir. */
4
+ export declare function writeJsonMap<T>(path: string, map: Record<string, T>): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Tiny file-backed JSON-map primitive shared by the file-backed adapters
3
+ * (contribution status store, skills install index). Read-missing ⇒ `{}`;
4
+ * a corrupt or non-object file ⇒ `{}` (never throws on read). Writes are
5
+ * atomic (temp file + rename) and create the parent dir on demand.
6
+ *
7
+ * In-package on purpose: harness-layer must NOT reach into `client/src/**`
8
+ * (the one-way dependency arrow), so the mineable-store idiom is reproduced
9
+ * here rather than imported.
10
+ */
11
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
12
+ import { dirname } from 'node:path';
13
+ /** Read the JSON map at `path`. Missing / corrupt / non-object ⇒ `{}`. */
14
+ export function readJsonMap(path) {
15
+ if (!existsSync(path))
16
+ return {};
17
+ try {
18
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
19
+ return parsed && typeof parsed === 'object' ? parsed : {};
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ /** Atomically write `map` to `path` (temp + rename), creating the dir. */
26
+ export function writeJsonMap(path, map) {
27
+ mkdirSync(dirname(path), { recursive: true });
28
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
29
+ writeFileSync(tmp, JSON.stringify(map), 'utf-8');
30
+ renameSync(tmp, path);
31
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * LocalLearningPort adapter (#1660) — drives the rung-1 `Distiller` behind the
3
+ * plugin's `LocalLearningPort`. `run` mints a runId, kicks the distillation
4
+ * asynchronously, and returns `ok({ runId })` immediately; a failed
5
+ * distillation is a `'failed'` run STATE, not a port error.
6
+ *
7
+ * The returned object exposes `awaitRun(runId)` beyond the port surface so
8
+ * tests can observe the terminal state deterministically (no timers).
9
+ */
10
+ import type { LocalLearningPort, LocalLearningSkill } from '@jinn-network/plugin';
11
+ import type { CapturedTask } from '../capture.js';
12
+ import type { Distiller } from '../distiller.js';
13
+ export interface LocalLearningAdapterDeps {
14
+ distiller: Distiller;
15
+ /** Resolve episodeIds → the captures the distiller consumes. */
16
+ loadCaptures: (episodeIds: string[]) => Promise<CapturedTask[]>;
17
+ /** Read durable skill state; unlike process-local run state this survives CLI invocations. */
18
+ listSkills?: () => Promise<LocalLearningSkill[]>;
19
+ }
20
+ /** The port plus a test-only completion handle. */
21
+ export interface LocalLearningAdapter extends LocalLearningPort {
22
+ /** Resolve once the run reaches a terminal (`done`/`failed`) state. */
23
+ awaitRun(runId: string): Promise<void>;
24
+ }
25
+ export declare function createLocalLearningAdapter(deps: LocalLearningAdapterDeps): LocalLearningAdapter;
@@ -0,0 +1,49 @@
1
+ import { ok, unavailable } from '@jinn-network/plugin';
2
+ export function createLocalLearningAdapter(deps) {
3
+ const runs = new Map();
4
+ const completions = new Map();
5
+ let runCounter = 0;
6
+ return {
7
+ async run(input) {
8
+ runCounter += 1;
9
+ const runId = `run-${Date.now()}-${runCounter}`;
10
+ runs.set(runId, { runId, state: 'pending' });
11
+ const completion = (async () => {
12
+ runs.set(runId, { runId, state: 'running' });
13
+ try {
14
+ const captures = await deps.loadCaptures(input.episodeIds);
15
+ await deps.distiller.distill(captures);
16
+ runs.set(runId, { runId, state: 'done' });
17
+ }
18
+ catch {
19
+ // A failed distillation is a legitimate run state, not a port error.
20
+ runs.set(runId, { runId, state: 'failed' });
21
+ }
22
+ })();
23
+ completions.set(runId, completion);
24
+ return ok({ runId });
25
+ },
26
+ async status(runId) {
27
+ const run = runs.get(runId);
28
+ if (!run)
29
+ return unavailable(`no such run: ${runId}`);
30
+ return ok(run);
31
+ },
32
+ async list() {
33
+ return ok([...runs.values()]);
34
+ },
35
+ async skills() {
36
+ if (!deps.listSkills)
37
+ return unavailable('local skill provenance is unavailable');
38
+ try {
39
+ return ok(await deps.listSkills());
40
+ }
41
+ catch (error) {
42
+ return unavailable(`local skill provenance failed: ${String(error)}`);
43
+ }
44
+ },
45
+ async awaitRun(runId) {
46
+ await completions.get(runId);
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,8 @@
1
+ import type { SkillsPort } from '@jinn-network/plugin';
2
+ export interface SkillsAdapterDeps {
3
+ /** Skills-install dir. The registry index lives at `<installDir>/installed.json`. */
4
+ installDir: string;
5
+ /** Injectable clock (tests). */
6
+ now?: () => Date;
7
+ }
8
+ export declare function createSkillsAdapter(deps: SkillsAdapterDeps): SkillsPort;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * SkillsPort adapter (#1660) — a file-backed skills-install registry behind the
3
+ * plugin's `SkillsPort`. `install` records the ref (+ `installedAt`); `list`
4
+ * enumerates installed refs; `uninstall` removes a ref unconditionally
5
+ * (idempotent, matching the in-memory reference which `.delete()`s regardless).
6
+ *
7
+ * Stage 1 records/lists/removes refs; on-disk `<name>/SKILL.md` parsing
8
+ * (`parseSkillMarkdown`/`assertConformantName`) is a later stage's concern.
9
+ */
10
+ import { join } from 'node:path';
11
+ import { ok, unavailable } from '@jinn-network/plugin';
12
+ import { readJsonMap, writeJsonMap } from './json-map-store.js';
13
+ const INDEX_FILE = 'installed.json';
14
+ export function createSkillsAdapter(deps) {
15
+ const indexPath = join(deps.installDir, INDEX_FILE);
16
+ const now = deps.now ?? (() => new Date());
17
+ const readIndex = () => readJsonMap(indexPath);
18
+ const writeIndex = (index) => writeJsonMap(indexPath, index);
19
+ return {
20
+ async install(ref) {
21
+ try {
22
+ const index = readIndex();
23
+ const installedAt = now().toISOString();
24
+ index[ref] = { installedAt };
25
+ writeIndex(index);
26
+ return ok({ ref, installedAt });
27
+ }
28
+ catch (e) {
29
+ return unavailable(`skills install failed: ${String(e)}`);
30
+ }
31
+ },
32
+ async list() {
33
+ try {
34
+ const index = readIndex();
35
+ return ok(Object.entries(index).map(([ref, { installedAt }]) => ({ ref, installedAt })));
36
+ }
37
+ catch (e) {
38
+ return unavailable(`skills list failed: ${String(e)}`);
39
+ }
40
+ },
41
+ async uninstall(ref) {
42
+ try {
43
+ const index = readIndex();
44
+ if (ref in index) {
45
+ delete index[ref];
46
+ writeIndex(index);
47
+ }
48
+ return ok({ ref });
49
+ }
50
+ catch (e) {
51
+ return unavailable(`skills uninstall failed: ${String(e)}`);
52
+ }
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Policy-admission hook (issue #1824, corpus-supply-design §5, charter
3
+ * Decision 4 amendment): the intended publish-time chokepoint marking
4
+ * producers call before building distributionTags. Declared policies decide
5
+ * from record facts (verdict, evidence tier, tags, freshness, provenance);
6
+ * hand-marking (HAND_MARK_POLICY, 'hand.v0') is the day-one policy and
7
+ * admits nothing — the mark is set iff the curator explicitly requested it.
8
+ * Not yet wired into a producer: day-one hand-marking happens by the curated
9
+ * seed lane carrying RETRIEVAL_VISIBLE_TAG in the episode's own tags
10
+ * (seed-import/episode-execute.ts passes them through as distributionTags).
11
+ * Producers adopt this chokepoint when the first fact-based policy lands
12
+ * (B2+); enforcement (pickup ranking + post-fetch guard) needs no change.
13
+ */
14
+ export interface AdmissionFacts {
15
+ verdict?: 'passed' | 'failed' | 'unknown';
16
+ evidenceTier?: string;
17
+ tags?: readonly string[];
18
+ capturedAt?: string;
19
+ provenance?: 'imported' | 'contributed';
20
+ }
21
+ export interface AdmissionPolicy {
22
+ name: string;
23
+ admit(facts: AdmissionFacts): boolean;
24
+ }
25
+ /** Day-one policy (#1824): admits nothing automatically. Marking authority
26
+ * stays at publish time, by the curator's explicit request only. */
27
+ export declare const HAND_MARK_POLICY: AdmissionPolicy;
28
+ /**
29
+ * The chokepoint: `explicit` is the curator's hand-mark request; `policy`
30
+ * decides from `facts` when `explicit` is not itself true. Explicit wins
31
+ * outright — a policy can only ADD admission, never withdraw a curator's
32
+ * explicit request.
33
+ */
34
+ export declare function resolveRetrievalMark(args: {
35
+ explicit?: boolean;
36
+ facts: AdmissionFacts;
37
+ policy: AdmissionPolicy;
38
+ }): boolean;
@@ -0,0 +1,17 @@
1
+ /** Day-one policy (#1824): admits nothing automatically. Marking authority
2
+ * stays at publish time, by the curator's explicit request only. */
3
+ export const HAND_MARK_POLICY = {
4
+ name: 'hand.v0',
5
+ admit: () => false,
6
+ };
7
+ /**
8
+ * The chokepoint: `explicit` is the curator's hand-mark request; `policy`
9
+ * decides from `facts` when `explicit` is not itself true. Explicit wins
10
+ * outright — a policy can only ADD admission, never withdraw a curator's
11
+ * explicit request.
12
+ */
13
+ export function resolveRetrievalMark(args) {
14
+ if (args.explicit === true)
15
+ return true;
16
+ return args.policy.admit(args.facts);
17
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createDistillMcpServer } from '../distill-mcp-server.js';
4
+ const server = createDistillMcpServer();
5
+ const transport = new StdioServerTransport();
6
+ server.connect(transport).catch((err) => {
7
+ console.error('[jinn-distill-mcp] failed to start');
8
+ console.error(err);
9
+ process.exit(1);
10
+ });