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