@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
|
@@ -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,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
|
+
});
|