@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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed-import idempotency state (issue #1771, folding the idempotency AC
|
|
3
|
+
* originally scoped to #1772/R2 — R4 has no dependency on R1 so it ships
|
|
4
|
+
* here; see the #1771 PR body for the rationale).
|
|
5
|
+
*
|
|
6
|
+
* Both seed lanes (skill seeds in `execute.ts`, evidence-episode seeds in
|
|
7
|
+
* `episode-execute.ts`) publish through this shared store: a small
|
|
8
|
+
* identity -> {contentHash, envelopeRef} map that each `execute*()` consults
|
|
9
|
+
* before publishing. Re-running an import over unchanged content is a no-op
|
|
10
|
+
* (a `skipped` row; no new envelope); re-running over CHANGED content
|
|
11
|
+
* republishes and points the new record's provenance at the prior
|
|
12
|
+
* `envelopeRef` via `supersedes` — skills carry it in
|
|
13
|
+
* `SkillArtifactV1.provenance.supersedes` (client/src/types/skill-artifact.ts,
|
|
14
|
+
* already schema'd for this, issue #1462); episodes carry it in the
|
|
15
|
+
* `seed.attribution` step attribute (episode-fetch.ts's step convention),
|
|
16
|
+
* since the frozen `jinn.trace-envelope.v0` has no top-level field for it.
|
|
17
|
+
*
|
|
18
|
+
* File-backed by default (`~/.jinn-client/harness-layer/seed-import-state.json`,
|
|
19
|
+
* mirroring `ledger.ts`'s convention) so real CLI runs persist across
|
|
20
|
+
* invocations; tests inject `createMemorySeedImportState()`.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately NOT a corpus query: idempotency here answers "did *I* already
|
|
23
|
+
* publish this exact seed identity, unchanged?" — a question the operator's
|
|
24
|
+
* own local state answers without a network round trip. It does not detect a
|
|
25
|
+
* differently-identified duplicate already sitting in the shared corpus
|
|
26
|
+
* (two seeds, two identities, same content) — that is the consumer-side
|
|
27
|
+
* content-key dedup (rescope plan §3.3) and the testnet hygiene sweep
|
|
28
|
+
* (#1776), not a publish-time concern.
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
import { basename, dirname, join } from 'node:path';
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import { canonicalJson } from '@jinn-network/core';
|
|
36
|
+
import { sha256Hex } from '../execution-publish.js';
|
|
37
|
+
export const DEFAULT_SEED_IMPORT_STATE_PATH = join(homedir(), '.jinn-client', 'harness-layer', 'seed-import-state.json');
|
|
38
|
+
export const SeedPublicationRecordSchema = z.strictObject({
|
|
39
|
+
/** sha256 over the canonical JSON of the seed's meaningful content. */
|
|
40
|
+
contentHash: z.string().min(1),
|
|
41
|
+
/** The published wrapper envelope CID (the corpus ref) — the corpus's own identity for the record. */
|
|
42
|
+
envelopeRef: z.string().min(1),
|
|
43
|
+
publishedAt: z.iso.datetime(),
|
|
44
|
+
});
|
|
45
|
+
const SeedImportStateFileSchema = z.record(z.string(), SeedPublicationRecordSchema);
|
|
46
|
+
/** In-memory store — tests, and any embedder that does not want on-disk state. */
|
|
47
|
+
export function createMemorySeedImportState(initial = {}) {
|
|
48
|
+
const state = new Map(Object.entries(initial));
|
|
49
|
+
return {
|
|
50
|
+
get: (identity) => state.get(identity),
|
|
51
|
+
set: (identity, record) => {
|
|
52
|
+
state.set(identity, SeedPublicationRecordSchema.parse(record));
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* File-backed store (JSON map, `~/.jinn-client/harness-layer/seed-import-state.json`
|
|
58
|
+
* by default) — read-modify-write on every `set()`. Fine at seed-import's
|
|
59
|
+
* scale (dozens to low hundreds of curated, human-approved entries via the
|
|
60
|
+
* plan/execute gate), not a database.
|
|
61
|
+
*
|
|
62
|
+
* A corrupt/foreign state file is FAIL-CLOSED: reads and writes throw rather
|
|
63
|
+
* than treating it as empty and destroying supersedes lineage. Persistence
|
|
64
|
+
* writes a complete same-directory temp file and atomically renames it over
|
|
65
|
+
* the destination, so interruption cannot leave a partial JSON document.
|
|
66
|
+
*/
|
|
67
|
+
export function createFileSeedImportState(path = DEFAULT_SEED_IMPORT_STATE_PATH, opts = {}) {
|
|
68
|
+
const warn = opts.onWarning;
|
|
69
|
+
let warned = false;
|
|
70
|
+
const read = () => {
|
|
71
|
+
if (!existsSync(path))
|
|
72
|
+
return {};
|
|
73
|
+
try {
|
|
74
|
+
return SeedImportStateFileSchema.parse(JSON.parse(readFileSync(path, 'utf-8')));
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
const message = `[harness-layer] seed-import state at ${path} is unreadable; refusing to continue so publication lineage is preserved`;
|
|
78
|
+
if (!warned) {
|
|
79
|
+
warned = true;
|
|
80
|
+
warn?.(message);
|
|
81
|
+
}
|
|
82
|
+
throw new Error(message, { cause });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
get(identity) {
|
|
87
|
+
return read()[identity];
|
|
88
|
+
},
|
|
89
|
+
set(identity, record) {
|
|
90
|
+
const all = read();
|
|
91
|
+
all[identity] = SeedPublicationRecordSchema.parse(record);
|
|
92
|
+
const dir = dirname(path);
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
94
|
+
const tempPath = join(dir, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(tempPath, JSON.stringify(all, null, 2) + '\n', 'utf-8');
|
|
97
|
+
renameSync(tempPath, path);
|
|
98
|
+
}
|
|
99
|
+
catch (cause) {
|
|
100
|
+
try {
|
|
101
|
+
rmSync(tempPath, { force: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Preserve the persistence failure that triggered cleanup.
|
|
105
|
+
}
|
|
106
|
+
throw cause;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Deterministic content fingerprint for idempotency: sha256 over the
|
|
113
|
+
* RFC 8785 canonical JSON of the seed's meaningful fields (the caller
|
|
114
|
+
* decides which fields are "meaningful" — e.g. excludes capture-time-only
|
|
115
|
+
* fields like `capturedAt`). Two calls over the same logical content always
|
|
116
|
+
* agree, regardless of key order.
|
|
117
|
+
*/
|
|
118
|
+
export function hashSeedContent(value) {
|
|
119
|
+
return sha256Hex(canonicalJson(value));
|
|
120
|
+
}
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distribution signal — where is real usage concentrating (plan Task 7,
|
|
3
|
+
* issue #1314; spec §7).
|
|
4
|
+
*
|
|
5
|
+
* v0 clustering is a **tag rollup**: an envelope's primary (first)
|
|
6
|
+
* distribution tag is its cluster; the cluster's `topTags` are the other
|
|
7
|
+
* tags' frequencies inside it. Deliberately crude — spec §8 says crude
|
|
8
|
+
* counts are enough for v0 — and replaceable by an upstream clustering
|
|
9
|
+
* endpoint without changing the output shape.
|
|
10
|
+
*
|
|
11
|
+
* Seeds are excluded from every number by default (`provenance:
|
|
12
|
+
* 'imported'`): seeds provide day-one usefulness but are not demand. The
|
|
13
|
+
* `includeSeeds` option exists for the demonstrate-it-live toggle in the
|
|
14
|
+
* explorer view, never as a default.
|
|
15
|
+
*/
|
|
16
|
+
export interface SignalInput {
|
|
17
|
+
/** distributionTags from the trace envelope (first tag = primary). */
|
|
18
|
+
tags: string[];
|
|
19
|
+
provenance: 'contributed' | 'imported' | 'derived-from-history';
|
|
20
|
+
/** Contributor identity (operator Safe address). */
|
|
21
|
+
contributor: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SignalRow {
|
|
24
|
+
cluster: string;
|
|
25
|
+
envelopeCount: number;
|
|
26
|
+
contributorCount: number;
|
|
27
|
+
/** Co-occurring tags in the cluster, most frequent first (primary excluded). */
|
|
28
|
+
topTags: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface SignalOptions {
|
|
31
|
+
/** Fold `provenance: 'imported'` entries back into the counts. Default false. */
|
|
32
|
+
includeSeeds?: boolean;
|
|
33
|
+
/** Cap on topTags per cluster. Default 5. */
|
|
34
|
+
topTagsLimit?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function computeSignal(entries: SignalInput[], opts?: SignalOptions): SignalRow[];
|
package/dist/signal.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distribution signal — where is real usage concentrating (plan Task 7,
|
|
3
|
+
* issue #1314; spec §7).
|
|
4
|
+
*
|
|
5
|
+
* v0 clustering is a **tag rollup**: an envelope's primary (first)
|
|
6
|
+
* distribution tag is its cluster; the cluster's `topTags` are the other
|
|
7
|
+
* tags' frequencies inside it. Deliberately crude — spec §8 says crude
|
|
8
|
+
* counts are enough for v0 — and replaceable by an upstream clustering
|
|
9
|
+
* endpoint without changing the output shape.
|
|
10
|
+
*
|
|
11
|
+
* Seeds are excluded from every number by default (`provenance:
|
|
12
|
+
* 'imported'`): seeds provide day-one usefulness but are not demand. The
|
|
13
|
+
* `includeSeeds` option exists for the demonstrate-it-live toggle in the
|
|
14
|
+
* explorer view, never as a default.
|
|
15
|
+
*/
|
|
16
|
+
export function computeSignal(entries, opts = {}) {
|
|
17
|
+
const topTagsLimit = opts.topTagsLimit ?? 5;
|
|
18
|
+
const clusters = new Map();
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.provenance === 'imported' && !opts.includeSeeds)
|
|
21
|
+
continue;
|
|
22
|
+
const primary = entry.tags[0];
|
|
23
|
+
if (!primary)
|
|
24
|
+
continue;
|
|
25
|
+
const cluster = clusters.get(primary) ?? {
|
|
26
|
+
envelopes: 0,
|
|
27
|
+
contributors: new Set(),
|
|
28
|
+
tagCounts: new Map(),
|
|
29
|
+
};
|
|
30
|
+
cluster.envelopes += 1;
|
|
31
|
+
cluster.contributors.add(entry.contributor);
|
|
32
|
+
for (const tag of entry.tags.slice(1)) {
|
|
33
|
+
cluster.tagCounts.set(tag, (cluster.tagCounts.get(tag) ?? 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
clusters.set(primary, cluster);
|
|
36
|
+
}
|
|
37
|
+
return [...clusters.entries()]
|
|
38
|
+
.map(([cluster, agg]) => ({
|
|
39
|
+
cluster,
|
|
40
|
+
envelopeCount: agg.envelopes,
|
|
41
|
+
contributorCount: agg.contributors.size,
|
|
42
|
+
topTags: [...agg.tagCounts.entries()]
|
|
43
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
44
|
+
.slice(0, topTagsLimit)
|
|
45
|
+
.map(([tag]) => tag),
|
|
46
|
+
}))
|
|
47
|
+
.sort((a, b) => b.envelopeCount - a.envelopeCount || a.cluster.localeCompare(b.cluster));
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SignedCanonical {
|
|
2
|
+
canonicalJson: string;
|
|
3
|
+
hash: `0x${string}`;
|
|
4
|
+
sig: `0x${string}`;
|
|
5
|
+
signer: `0x${string}`;
|
|
6
|
+
}
|
|
7
|
+
/** Sign canonical JSON with raw secp256k1 ECDSA (no EIP-191 prefix). */
|
|
8
|
+
export declare function signCanonical(value: unknown, privateKey: `0x${string}`, signerAddress: `0x${string}`): Promise<SignedCanonical>;
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { hexToBytes, keccak256, toHex } from 'viem';
|
|
2
|
+
import { sign } from 'viem/accounts';
|
|
3
|
+
import { canonicalJson } from '@jinn-network/core';
|
|
4
|
+
/** Sign canonical JSON with raw secp256k1 ECDSA (no EIP-191 prefix). */
|
|
5
|
+
export async function signCanonical(value, privateKey, signerAddress) {
|
|
6
|
+
const canonical = canonicalJson(value);
|
|
7
|
+
const hash = keccak256(new TextEncoder().encode(canonical));
|
|
8
|
+
const signature = await sign({ hash, privateKey });
|
|
9
|
+
const recoveryByte = signature.yParity ?? (signature.v === 28n ? 1 : 0);
|
|
10
|
+
const sig = toHex(new Uint8Array([
|
|
11
|
+
...hexToBytes(signature.r),
|
|
12
|
+
...hexToBytes(signature.s),
|
|
13
|
+
recoveryByte,
|
|
14
|
+
]));
|
|
15
|
+
return {
|
|
16
|
+
canonicalJson: canonical,
|
|
17
|
+
hash,
|
|
18
|
+
sig,
|
|
19
|
+
signer: signerAddress,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SKILL_ARTIFACT_TYPE } from '@jinn-network/core';
|
|
3
|
+
export { SKILL_ARTIFACT_TYPE };
|
|
4
|
+
/**
|
|
5
|
+
* The distiller-side provenance meta carried on a `SkillPackage` (and embedded
|
|
6
|
+
* as `metadata.jinn` frontmatter in EXPORT renders). The canonical stored form
|
|
7
|
+
* is `SkillProvenanceSchema` in `client/src/types/skill-artifact.ts`;
|
|
8
|
+
* `publishSkill()` maps this onto it (`provenance` → `sourceEnvelopeCids`,
|
|
9
|
+
* `distilledFrom` is derived as its length there and not stored twice).
|
|
10
|
+
*/
|
|
11
|
+
export declare const SkillPackageMetaSchema: z.ZodObject<{
|
|
12
|
+
schema: z.ZodLiteral<"jinn.skill.v1">;
|
|
13
|
+
distribution: z.ZodString;
|
|
14
|
+
verifiabilityTier: z.ZodString;
|
|
15
|
+
distilledFrom: z.ZodNumber;
|
|
16
|
+
provenance: z.ZodArray<z.ZodString>;
|
|
17
|
+
distillPromptSha256: z.ZodOptional<z.ZodString>;
|
|
18
|
+
distilledAt: z.ZodOptional<z.ZodString>;
|
|
19
|
+
seedSource: z.ZodOptional<z.ZodString>;
|
|
20
|
+
skillKind: z.ZodOptional<z.ZodEnum<{
|
|
21
|
+
"strategic-pattern": "strategic-pattern";
|
|
22
|
+
"failure-lesson": "failure-lesson";
|
|
23
|
+
contrastive: "contrastive";
|
|
24
|
+
"cross-instance": "cross-instance";
|
|
25
|
+
}>>;
|
|
26
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
experimental: "experimental";
|
|
28
|
+
stable: "stable";
|
|
29
|
+
deprecated: "deprecated";
|
|
30
|
+
}>>;
|
|
31
|
+
evidenceTier: z.ZodOptional<z.ZodEnum<{
|
|
32
|
+
contrastive: "contrastive";
|
|
33
|
+
stable: "stable";
|
|
34
|
+
deprecated: "deprecated";
|
|
35
|
+
"single-example": "single-example";
|
|
36
|
+
"recurring-pattern": "recurring-pattern";
|
|
37
|
+
"user-confirmed": "user-confirmed";
|
|
38
|
+
}>>;
|
|
39
|
+
sourceTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
40
|
+
targetTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
41
|
+
uses: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
positiveFeedback: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
negativeFeedback: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
distillModel: z.ZodOptional<z.ZodString>;
|
|
45
|
+
evidenceTokens: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
skillTokens: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
export type SkillPackageMeta = z.infer<typeof SkillPackageMetaSchema>;
|
|
49
|
+
export interface SkillPackage {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
license: string | null;
|
|
53
|
+
jinn: SkillPackageMeta;
|
|
54
|
+
body: string;
|
|
55
|
+
}
|
|
56
|
+
export declare function assertConformantName(name: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Emit a conformant SKILL.md: YAML frontmatter then the markdown body verbatim.
|
|
59
|
+
*
|
|
60
|
+
* Default (EXPORT mode) embeds the `metadata.jinn` provenance block so a
|
|
61
|
+
* standalone SKILL.md written to disk carries its provenance. Pass
|
|
62
|
+
* `{ embedProvenance: false }` when rendering the `skill.skillMd` stored inside
|
|
63
|
+
* a `SkillArtifactV1` — there the structured provenance object is canonical and
|
|
64
|
+
* embedding a second copy would create drift.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildSkillMarkdown(pkg: SkillPackage, opts?: {
|
|
67
|
+
embedProvenance?: boolean;
|
|
68
|
+
}): string;
|
|
69
|
+
/** Inverse of buildSkillMarkdown (EXPORT mode); throws on a non-conformant package. */
|
|
70
|
+
export declare function parseSkillMarkdown(md: string): SkillPackage;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layer-2 consumable package builder: renders/parses a `jinn.skill.v1`
|
|
3
|
+
* conformant Agent-Skill package (spec/2026-07-06-distillation-v1.md §5,
|
|
4
|
+
* reconciled 2026-07-07 with the shipped #1394 substrate).
|
|
5
|
+
*
|
|
6
|
+
* A skill is a `<name>/SKILL.md` package — standard skills.sh / Vercel shape —
|
|
7
|
+
* so it is drop-in installable by the wider `skills` ecosystem.
|
|
8
|
+
*
|
|
9
|
+
* Division of labour after the #1394 reconciliation:
|
|
10
|
+
* - `client/src/types/skill-artifact.ts` (#1394, shipped) owns the CANONICAL
|
|
11
|
+
* stored form: `SkillArtifactV1` with structured `SkillProvenanceSchema`.
|
|
12
|
+
* - `./skill.ts` (shipped) owns consume-side recognition (`extractSkill`).
|
|
13
|
+
* - THIS module is the distiller-side package builder/renderer: the
|
|
14
|
+
* `SkillPackage` shape the distiller produces, and SKILL.md
|
|
15
|
+
* rendering/parsing. `buildSkillMarkdown` embeds the `metadata.jinn`
|
|
16
|
+
* provenance block only in EXPORT mode (default) — when a package is
|
|
17
|
+
* written to disk as a standalone SKILL.md. The stored `skill.skillMd`
|
|
18
|
+
* inside a `SkillArtifactV1` is rendered WITHOUT the provenance block
|
|
19
|
+
* (`embedProvenance: false`) so provenance lives in exactly one canonical
|
|
20
|
+
* place (the structured object) and cannot drift.
|
|
21
|
+
*/
|
|
22
|
+
import { parse as parseYaml, Scalar, stringify as stringifyYaml } from 'yaml';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { SKILL_ARTIFACT_TYPE } from '@jinn-network/core';
|
|
25
|
+
export { SKILL_ARTIFACT_TYPE };
|
|
26
|
+
/** skills.sh rule: name is lowercase letters/digits/hyphens and equals the dir. */
|
|
27
|
+
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
28
|
+
/**
|
|
29
|
+
* The distiller-side provenance meta carried on a `SkillPackage` (and embedded
|
|
30
|
+
* as `metadata.jinn` frontmatter in EXPORT renders). The canonical stored form
|
|
31
|
+
* is `SkillProvenanceSchema` in `client/src/types/skill-artifact.ts`;
|
|
32
|
+
* `publishSkill()` maps this onto it (`provenance` → `sourceEnvelopeCids`,
|
|
33
|
+
* `distilledFrom` is derived as its length there and not stored twice).
|
|
34
|
+
*/
|
|
35
|
+
export const SkillPackageMetaSchema = z
|
|
36
|
+
.strictObject({
|
|
37
|
+
schema: z.literal(SKILL_ARTIFACT_TYPE),
|
|
38
|
+
distribution: z.string().min(1),
|
|
39
|
+
verifiabilityTier: z.string().min(1),
|
|
40
|
+
distilledFrom: z.number().int().nonnegative(),
|
|
41
|
+
provenance: z.array(z.string().min(1)),
|
|
42
|
+
distillPromptSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
43
|
+
distilledAt: z.string().optional(),
|
|
44
|
+
seedSource: z.string().optional(),
|
|
45
|
+
skillKind: z.enum(['strategic-pattern', 'failure-lesson', 'contrastive', 'cross-instance']).optional(),
|
|
46
|
+
status: z.enum(['experimental', 'stable', 'deprecated']).optional(),
|
|
47
|
+
evidenceTier: z
|
|
48
|
+
.enum(['single-example', 'recurring-pattern', 'contrastive', 'user-confirmed', 'stable', 'deprecated'])
|
|
49
|
+
.optional(),
|
|
50
|
+
sourceTools: z.array(z.string().min(1)).optional(),
|
|
51
|
+
targetTools: z.array(z.string().min(1)).optional(),
|
|
52
|
+
uses: z.number().int().nonnegative().optional(),
|
|
53
|
+
positiveFeedback: z.number().int().nonnegative().optional(),
|
|
54
|
+
negativeFeedback: z.number().int().nonnegative().optional(),
|
|
55
|
+
// Auditability (spec §5, v0.5): the distilling model + a deterministic
|
|
56
|
+
// ceil(chars/4) token estimate of input vs body (the compression ratio).
|
|
57
|
+
distillModel: z.string().min(1).optional(),
|
|
58
|
+
evidenceTokens: z.number().int().nonnegative().optional(),
|
|
59
|
+
skillTokens: z.number().int().nonnegative().optional(),
|
|
60
|
+
})
|
|
61
|
+
// Corroboration vs audit: `distilledFrom` is the DISTINCT-INSTANCE
|
|
62
|
+
// corroboration count (§6 — retries at one problem are one unit, not N),
|
|
63
|
+
// while `provenance` anchors EVERY audited source trace. Under group
|
|
64
|
+
// retention (#1478) one instance can contribute many traces, so
|
|
65
|
+
// `distilledFrom <= provenance.length` (a skill cannot corroborate across
|
|
66
|
+
// more distinct instances than it has traces), and is ≥1 whenever any trace
|
|
67
|
+
// is anchored. This keeps the ecosystem-facing "distilled from N" claim
|
|
68
|
+
// honest as a breadth signal instead of inflating with retry count.
|
|
69
|
+
.refine((m) => m.distilledFrom <= m.provenance.length && (m.provenance.length === 0 || m.distilledFrom >= 1), {
|
|
70
|
+
message: 'distilledFrom must be between 1 and provenance.length (distinct-instance corroboration ≤ audited traces)',
|
|
71
|
+
});
|
|
72
|
+
export function assertConformantName(name) {
|
|
73
|
+
if (!NAME_RE.test(name)) {
|
|
74
|
+
throw new Error(`skill name "${name}" is not conformant (lowercase letters/digits/hyphens only)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function quotedYamlString(value) {
|
|
78
|
+
const scalar = new Scalar(value);
|
|
79
|
+
scalar.type = Scalar.QUOTE_DOUBLE;
|
|
80
|
+
return scalar;
|
|
81
|
+
}
|
|
82
|
+
function renderableJinnMeta(jinn) {
|
|
83
|
+
return {
|
|
84
|
+
...jinn,
|
|
85
|
+
...(jinn.distilledAt ? { distilledAt: quotedYamlString(jinn.distilledAt) } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function normalizeParsedJinnMeta(value) {
|
|
89
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
90
|
+
return value;
|
|
91
|
+
const record = { ...value };
|
|
92
|
+
if (record.distilledAt instanceof Date) {
|
|
93
|
+
record.distilledAt = record.distilledAt.toISOString();
|
|
94
|
+
}
|
|
95
|
+
return record;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Emit a conformant SKILL.md: YAML frontmatter then the markdown body verbatim.
|
|
99
|
+
*
|
|
100
|
+
* Default (EXPORT mode) embeds the `metadata.jinn` provenance block so a
|
|
101
|
+
* standalone SKILL.md written to disk carries its provenance. Pass
|
|
102
|
+
* `{ embedProvenance: false }` when rendering the `skill.skillMd` stored inside
|
|
103
|
+
* a `SkillArtifactV1` — there the structured provenance object is canonical and
|
|
104
|
+
* embedding a second copy would create drift.
|
|
105
|
+
*/
|
|
106
|
+
export function buildSkillMarkdown(pkg, opts = {}) {
|
|
107
|
+
assertConformantName(pkg.name);
|
|
108
|
+
const jinn = SkillPackageMetaSchema.parse(pkg.jinn);
|
|
109
|
+
const embed = opts.embedProvenance ?? true;
|
|
110
|
+
const frontmatter = stringifyYaml({
|
|
111
|
+
name: pkg.name,
|
|
112
|
+
description: pkg.description,
|
|
113
|
+
license: pkg.license,
|
|
114
|
+
...(embed ? { metadata: { jinn: renderableJinnMeta(jinn) } } : {}),
|
|
115
|
+
});
|
|
116
|
+
return `---\n${frontmatter}---\n${pkg.body}`;
|
|
117
|
+
}
|
|
118
|
+
/** Inverse of buildSkillMarkdown (EXPORT mode); throws on a non-conformant package. */
|
|
119
|
+
export function parseSkillMarkdown(md) {
|
|
120
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(md);
|
|
121
|
+
if (!m)
|
|
122
|
+
throw new Error('skill markdown is missing a YAML frontmatter block');
|
|
123
|
+
const doc = parseYaml(m[1]);
|
|
124
|
+
if (doc === null || typeof doc !== 'object') {
|
|
125
|
+
throw new Error('skill frontmatter did not parse to an object');
|
|
126
|
+
}
|
|
127
|
+
const name = String(doc.name ?? '');
|
|
128
|
+
assertConformantName(name);
|
|
129
|
+
if (typeof doc.description !== 'string' || doc.description.length === 0) {
|
|
130
|
+
throw new Error('skill frontmatter is missing a description');
|
|
131
|
+
}
|
|
132
|
+
const metadata = doc.metadata;
|
|
133
|
+
const jinn = SkillPackageMetaSchema.parse(normalizeParsedJinnMeta(metadata?.jinn));
|
|
134
|
+
return {
|
|
135
|
+
name,
|
|
136
|
+
description: doc.description,
|
|
137
|
+
license: doc.license == null ? null : String(doc.license),
|
|
138
|
+
jinn,
|
|
139
|
+
body: m[2] ?? '',
|
|
140
|
+
};
|
|
141
|
+
}
|
package/dist/skill.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill recognition (#1394): the shared install-path helper.
|
|
3
|
+
*
|
|
4
|
+
* `extractSkill(record)` prefers a first-class `jinn.skill.v1` artifact and
|
|
5
|
+
* falls back to the seeded shape (episode/legacy trace artifact ->
|
|
6
|
+
* `seed:skill-md` step -> `skill.md` attribute + `seed.attribution`),
|
|
7
|
+
* synthesising an equivalent provenance block from the embedded fields.
|
|
8
|
+
*/
|
|
9
|
+
import { type SkillArtifactV1 } from '@jinn-network/core';
|
|
10
|
+
import type { CorpusRecord } from './consume.js';
|
|
11
|
+
export interface ExtractedSkill {
|
|
12
|
+
skill: SkillArtifactV1;
|
|
13
|
+
/** Which carrier the skill came from. */
|
|
14
|
+
shape: 'jinn.skill.v1' | 'seeded-episode' | 'seeded-trace';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract the skill carried by a corpus record, or null when the record
|
|
18
|
+
* carries none. Throws when a `jinn.skill.v1` artifact is present but
|
|
19
|
+
* malformed — a corrupt first-class skill is an error, not a fall-through.
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractSkill(record: CorpusRecord): ExtractedSkill | null;
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill recognition (#1394): the shared install-path helper.
|
|
3
|
+
*
|
|
4
|
+
* `extractSkill(record)` prefers a first-class `jinn.skill.v1` artifact and
|
|
5
|
+
* falls back to the seeded shape (episode/legacy trace artifact ->
|
|
6
|
+
* `seed:skill-md` step -> `skill.md` attribute + `seed.attribution`),
|
|
7
|
+
* synthesising an equivalent provenance block from the embedded fields.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod/v3';
|
|
10
|
+
import { SKILL_ARTIFACT_TYPE, SkillArtifactV1Schema, } from '@jinn-network/core';
|
|
11
|
+
import { EpisodeV1Schema } from '@jinn-network/plugin';
|
|
12
|
+
import { EPISODE_ARTIFACT_TYPE, TRACE_ENVELOPE_ARTIFACT_TYPE, } from './publish.js';
|
|
13
|
+
import { parseTraceEnvelopeV0 } from './envelope.js';
|
|
14
|
+
import { frontmatterName, skillSlug } from './seed-import/execute.js';
|
|
15
|
+
const SeedAttributionSchema = z.object({
|
|
16
|
+
skill: z.string().min(1),
|
|
17
|
+
source: z.string().min(1),
|
|
18
|
+
licence: z.string().nullable(),
|
|
19
|
+
});
|
|
20
|
+
function parseJson(content) {
|
|
21
|
+
return JSON.parse(content.toString('utf-8'));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract the skill carried by a corpus record, or null when the record
|
|
25
|
+
* carries none. Throws when a `jinn.skill.v1` artifact is present but
|
|
26
|
+
* malformed — a corrupt first-class skill is an error, not a fall-through.
|
|
27
|
+
*/
|
|
28
|
+
export function extractSkill(record) {
|
|
29
|
+
const skillArtifact = record.artifacts.find((a) => a.artifactType === SKILL_ARTIFACT_TYPE);
|
|
30
|
+
if (skillArtifact) {
|
|
31
|
+
return {
|
|
32
|
+
skill: SkillArtifactV1Schema.parse(parseJson(skillArtifact.content)),
|
|
33
|
+
shape: 'jinn.skill.v1',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const episodeArtifact = record.artifacts.find((a) => a.artifactType === EPISODE_ARTIFACT_TYPE);
|
|
37
|
+
const traceArtifact = episodeArtifact
|
|
38
|
+
? undefined
|
|
39
|
+
: record.artifacts.find((a) => a.artifactType === TRACE_ENVELOPE_ARTIFACT_TYPE);
|
|
40
|
+
const carrier = episodeArtifact ?? traceArtifact;
|
|
41
|
+
if (!carrier)
|
|
42
|
+
return null;
|
|
43
|
+
let steps;
|
|
44
|
+
try {
|
|
45
|
+
steps = episodeArtifact
|
|
46
|
+
? EpisodeV1Schema.parse(parseJson(carrier.content)).trajectory
|
|
47
|
+
: parseTraceEnvelopeV0(parseJson(carrier.content)).steps;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null; // malformed evidence carrier — do not silently use a second shape
|
|
51
|
+
}
|
|
52
|
+
const step = steps.find((candidate) => candidate.name === 'seed:skill-md');
|
|
53
|
+
const skillMd = step?.attributes['skill.md'];
|
|
54
|
+
if (typeof skillMd !== 'string' || skillMd.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const attribution = SeedAttributionSchema.safeParse(step.attributes['seed.attribution']);
|
|
57
|
+
const seed = attribution.success ? attribution.data : undefined;
|
|
58
|
+
return {
|
|
59
|
+
skill: {
|
|
60
|
+
schemaVersion: SKILL_ARTIFACT_TYPE,
|
|
61
|
+
skill: {
|
|
62
|
+
name: frontmatterName(skillMd) ?? (seed ? skillSlug(seed.skill) : 'skill'),
|
|
63
|
+
skillMd,
|
|
64
|
+
},
|
|
65
|
+
files: [],
|
|
66
|
+
provenance: {
|
|
67
|
+
kind: 'imported',
|
|
68
|
+
// [] for seed imports (the schema's documented convention, matching
|
|
69
|
+
// first-class seed artifacts) — callers already hold record.ref.
|
|
70
|
+
sourceEnvelopeCids: [],
|
|
71
|
+
operator: { safeAddress: record.envelope.participant.safeAddress },
|
|
72
|
+
...(seed ? { seed } : {}),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
shape: episodeArtifact ? 'seeded-episode' : 'seeded-trace',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* system_snapshot extraction — donation unwrap + ustar read + transcript
|
|
3
|
+
* lookup (issue #1472, spec §8).
|
|
4
|
+
*
|
|
5
|
+
* The engine tars each solve's working dir into a `system_snapshot` artifact
|
|
6
|
+
* (hand-rolled POSIX ustar + gzip, `client/src/harnesses/engine/packaging.ts`
|
|
7
|
+
* `createWorkdirTarball`) and publishes it donation-wrapped
|
|
8
|
+
* (`jinn.artifact.donation.v1`: JSON with base64 `data`). Inside is the
|
|
9
|
+
* harness's raw stdout transcript (`.claude-code/stdout.jsonl`,
|
|
10
|
+
* `.codex-code/stdout.jsonl`) — the solve's actual reasoning, which
|
|
11
|
+
* `jinn.trajectory.v1` does NOT carry (2 artifact.emit spans only; #1473
|
|
12
|
+
* tracks fixing that at solve time).
|
|
13
|
+
*
|
|
14
|
+
* `unwrapDonation` mirrors the module-private `decodeDonationArtifact`
|
|
15
|
+
* (`@jinn-network/core/corpus-read`) and additionally verifies the decoded
|
|
16
|
+
* bytes hash to the declared sha256 (tamper check). The ustar reader mirrors
|
|
17
|
+
* the writer: regular files only, names ≤100 chars, no PAX/GNU extensions —
|
|
18
|
+
* a malformed header ends iteration rather than throwing. Decompression is
|
|
19
|
+
* bomb-guarded via zlib's `maxOutputLength`.
|
|
20
|
+
*/
|
|
21
|
+
import { type TranscriptSpanInput } from '@jinn-network/core/trajectory';
|
|
22
|
+
/** Gzip-bomb guard: snapshots observed live are ≤ ~300 KB compressed. */
|
|
23
|
+
export declare const MAX_SNAPSHOT_DECOMPRESSED_BYTES: number;
|
|
24
|
+
export interface TarEntry {
|
|
25
|
+
name: string;
|
|
26
|
+
bytes: Buffer;
|
|
27
|
+
}
|
|
28
|
+
export interface SolveTranscript {
|
|
29
|
+
harness: 'claude-code' | 'codex';
|
|
30
|
+
jsonl: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Decode a `jinn.artifact.donation.v1` wrapper to its raw bytes. Throws on a
|
|
34
|
+
* non-donation shape, a declared-sha mismatch against `expectedSha256`, or
|
|
35
|
+
* decoded bytes that do not hash to the declared sha (tamper check — parity
|
|
36
|
+
* plus-one with corpus/acquire.ts `decodeDonationArtifact`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function unwrapDonation(raw: unknown, expectedSha256: string): Buffer;
|
|
39
|
+
/**
|
|
40
|
+
* Gunzip (bomb-guarded) + read a POSIX ustar archive into entries. Regular
|
|
41
|
+
* files only; a malformed/truncated header or size field ends iteration
|
|
42
|
+
* (best-effort read of what is intact, never an infinite loop).
|
|
43
|
+
*/
|
|
44
|
+
export declare function untarGz(tarGz: Buffer): TarEntry[];
|
|
45
|
+
/** Locate the solve transcript among snapshot entries, or null if absent. */
|
|
46
|
+
export declare function findSolveTranscript(entries: TarEntry[]): SolveTranscript | null;
|
|
47
|
+
/**
|
|
48
|
+
* End-to-end: donation wrapper JSON → the solve transcript, or null when the
|
|
49
|
+
* snapshot carries none (hermes, or transcript-less). Throws on unwrap /
|
|
50
|
+
* decompression failures — the caller treats any throw as "no enrichment".
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractSnapshotTranscript(wrapper: unknown, expectedSha256: string): SolveTranscript | null;
|
|
53
|
+
/** Parse snapshot stdout with the same canonical typed-span parser as the live path. */
|
|
54
|
+
export declare function parseSolveTranscript(transcript: SolveTranscript): TranscriptSpanInput[];
|