@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,24 @@
1
+ /**
2
+ * Full skills.sh registry scan (issue #1313 follow-up).
3
+ *
4
+ * The registry publishes its complete catalogue via sitemaps
5
+ * (sitemap-skills-*.xml — ~20k skill pages across ~2.5k source repos).
6
+ * This script enumerates it, licence-checks every SOURCE repo against the
7
+ * seed importer's disclosed allowlist, resolves each skill's SKILL.md path
8
+ * from the repo's git tree, and emits:
9
+ *
10
+ * - candidates.txt — seed-list lines (owner/repo#path) for every skill
11
+ * whose repo licence passes the import gate
12
+ * - scan-report.json — full per-repo results (licence, verdict, skills)
13
+ * - a terminal summary (counts by licence / verdict)
14
+ *
15
+ * Read-only against GitHub (REST, token from GITHUB_TOKEN or `gh auth
16
+ * token`). Checkpointed: repo results append to the report file as they
17
+ * land, and a rerun skips repos already scanned — safe to interrupt.
18
+ * Publishes nothing; the output is INPUT to `seed plan`, and the human
19
+ * approval gate on `seed execute` is unchanged.
20
+ *
21
+ * Run: cd packages/layer && yarn build && GITHUB_TOKEN=$(gh auth token) \
22
+ * node dist/scripts/scan-skills-registry.js [--out <dir>]
23
+ */
24
+ export {};
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Full skills.sh registry scan (issue #1313 follow-up).
3
+ *
4
+ * The registry publishes its complete catalogue via sitemaps
5
+ * (sitemap-skills-*.xml — ~20k skill pages across ~2.5k source repos).
6
+ * This script enumerates it, licence-checks every SOURCE repo against the
7
+ * seed importer's disclosed allowlist, resolves each skill's SKILL.md path
8
+ * from the repo's git tree, and emits:
9
+ *
10
+ * - candidates.txt — seed-list lines (owner/repo#path) for every skill
11
+ * whose repo licence passes the import gate
12
+ * - scan-report.json — full per-repo results (licence, verdict, skills)
13
+ * - a terminal summary (counts by licence / verdict)
14
+ *
15
+ * Read-only against GitHub (REST, token from GITHUB_TOKEN or `gh auth
16
+ * token`). Checkpointed: repo results append to the report file as they
17
+ * land, and a rerun skips repos already scanned — safe to interrupt.
18
+ * Publishes nothing; the output is INPUT to `seed plan`, and the human
19
+ * approval gate on `seed execute` is unchanged.
20
+ *
21
+ * Run: cd packages/layer && yarn build && GITHUB_TOKEN=$(gh auth token) \
22
+ * node dist/scripts/scan-skills-registry.js [--out <dir>]
23
+ */
24
+ import { execFileSync } from 'node:child_process';
25
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { parseArgs } from 'node:util';
28
+ import { checkLicence } from '../seed-import/licence.js';
29
+ const SITEMAP_INDEX = 'https://www.skills.sh/sitemap.xml';
30
+ const GITHUB_API = 'https://api.github.com';
31
+ const CONCURRENCY = 8;
32
+ function token() {
33
+ const env = (process.env['GITHUB_TOKEN'] || '').trim();
34
+ if (env)
35
+ return env;
36
+ try {
37
+ return execFileSync('gh', ['auth', 'token'], { encoding: 'utf-8' }).trim();
38
+ }
39
+ catch {
40
+ throw new Error('set GITHUB_TOKEN or log in with gh');
41
+ }
42
+ }
43
+ async function fetchText(url) {
44
+ const res = await fetch(url, { redirect: 'follow' });
45
+ if (!res.ok)
46
+ throw new Error(`${res.status} for ${url}`);
47
+ return res.text();
48
+ }
49
+ async function github(path, tok) {
50
+ const res = await fetch(`${GITHUB_API}${path}`, {
51
+ headers: {
52
+ accept: 'application/vnd.github+json',
53
+ 'user-agent': 'jinn-harness-layer-registry-scan',
54
+ authorization: `Bearer ${tok}`,
55
+ },
56
+ });
57
+ if (res.status === 403 || res.status === 429) {
58
+ const reset = Number(res.headers.get('x-ratelimit-reset') || 0) * 1000;
59
+ const waitMs = Math.max(5_000, reset - Date.now() + 1_000);
60
+ console.warn(`[scan] rate limited — waiting ${Math.ceil(waitMs / 1000)}s`);
61
+ await new Promise((r) => setTimeout(r, waitMs));
62
+ return github(path, tok);
63
+ }
64
+ if (res.status === 404)
65
+ return null;
66
+ if (!res.ok)
67
+ throw new Error(`GitHub ${res.status} for ${path}`);
68
+ return res.json();
69
+ }
70
+ async function enumerateRegistry() {
71
+ const index = await fetchText(SITEMAP_INDEX);
72
+ const sitemaps = [...index.matchAll(/<loc>([^<]+sitemap-skills-[^<]+)<\/loc>/g)].map((m) => m[1]);
73
+ const byRepo = new Map();
74
+ for (const sm of sitemaps) {
75
+ const body = await fetchText(sm);
76
+ for (const m of body.matchAll(/<loc>https?:\/\/(?:www\.)?skills\.sh\/([^/<]+)\/([^/<]+)\/([^/<]+)<\/loc>/g)) {
77
+ const [, owner, repo, skill] = m;
78
+ // Registry lists some non-GitHub sources (e.g. open.feishu.cn slugs) —
79
+ // owner/repo that are not valid GitHub names get filtered by the 404 path.
80
+ const key = `${owner}/${repo}`;
81
+ (byRepo.get(key) ?? byRepo.set(key, new Set()).get(key)).add(decodeURIComponent(skill));
82
+ }
83
+ }
84
+ return byRepo;
85
+ }
86
+ async function scanRepo(repo, skills, tok) {
87
+ const meta = (await github(`/repos/${repo}`, tok));
88
+ if (meta === null) {
89
+ return { repo, licence: null, verdict: 'skip', reason: 'not found on GitHub', resolved: {}, unresolved: [...skills] };
90
+ }
91
+ const spdx = meta.license?.spdx_id && meta.license.spdx_id !== 'NOASSERTION' ? meta.license.spdx_id : null;
92
+ const { verdict, reason } = checkLicence(spdx);
93
+ const scan = { repo, licence: spdx, verdict, reason, resolved: {}, unresolved: [] };
94
+ if (verdict !== 'import') {
95
+ scan.unresolved = [...skills];
96
+ return scan;
97
+ }
98
+ // One tree call resolves every skill's SKILL.md directory.
99
+ const tree = (await github(`/repos/${repo}/git/trees/${meta.default_branch ?? 'HEAD'}?recursive=1`, tok));
100
+ const dirs = new Map(); // dir basename → dir path (first wins, shallowest first)
101
+ const entries = (tree?.tree ?? [])
102
+ .filter((e) => e.type === 'blob' && e.path.endsWith('/SKILL.md'))
103
+ .sort((a, b) => a.path.split('/').length - b.path.split('/').length);
104
+ for (const e of entries) {
105
+ const dir = e.path.slice(0, -'/SKILL.md'.length);
106
+ const base = dir.split('/').pop();
107
+ if (!dirs.has(base))
108
+ dirs.set(base, dir);
109
+ }
110
+ // Root SKILL.md: usable when the registry slug equals the repo name.
111
+ const hasRootSkillMd = (tree?.tree ?? []).some((e) => e.type === 'blob' && e.path === 'SKILL.md');
112
+ for (const skill of skills) {
113
+ const dir = dirs.get(skill);
114
+ if (dir !== undefined)
115
+ scan.resolved[skill] = dir;
116
+ else if (hasRootSkillMd && skill === repo.split('/')[1])
117
+ scan.resolved[skill] = '';
118
+ else
119
+ scan.unresolved.push(skill);
120
+ }
121
+ if (tree?.truncated)
122
+ scan.error = 'git tree truncated — unresolved list may overcount';
123
+ return scan;
124
+ }
125
+ async function main() {
126
+ const { values } = parseArgs({
127
+ options: { out: { type: 'string', default: 'registry-scan' } },
128
+ });
129
+ const outDir = values.out;
130
+ mkdirSync(outDir, { recursive: true });
131
+ const reportPath = join(outDir, 'scan-report.json');
132
+ const candidatesPath = join(outDir, 'candidates.txt');
133
+ const tok = token();
134
+ console.log('[scan] enumerating registry sitemaps…');
135
+ const byRepo = await enumerateRegistry();
136
+ const totalSkills = [...byRepo.values()].reduce((n, s) => n + s.size, 0);
137
+ console.log(`[scan] ${totalSkills} skills across ${byRepo.size} repos`);
138
+ // Resume support: skip repos already in the report.
139
+ const done = new Map();
140
+ if (existsSync(reportPath)) {
141
+ for (const row of JSON.parse(readFileSync(reportPath, 'utf-8')))
142
+ done.set(row.repo, row);
143
+ console.log(`[scan] resuming — ${done.size} repos already scanned`);
144
+ }
145
+ const queue = [...byRepo.entries()].filter(([repo]) => !done.has(repo));
146
+ const results = [...done.values()];
147
+ let inFlight = 0;
148
+ let cursor = 0;
149
+ let scanned = 0;
150
+ await new Promise((resolve) => {
151
+ const pump = () => {
152
+ if (cursor >= queue.length && inFlight === 0)
153
+ return resolve();
154
+ while (inFlight < CONCURRENCY && cursor < queue.length) {
155
+ const [repo, skills] = queue[cursor];
156
+ cursor += 1;
157
+ inFlight += 1;
158
+ scanRepo(repo, skills, tok)
159
+ .catch((err) => ({
160
+ repo,
161
+ licence: null,
162
+ verdict: 'skip',
163
+ reason: `scan error: ${err instanceof Error ? err.message : String(err)}`,
164
+ resolved: {},
165
+ unresolved: [...skills],
166
+ error: String(err),
167
+ }))
168
+ .then((scan) => {
169
+ results.push(scan);
170
+ scanned += 1;
171
+ if (scanned % 100 === 0) {
172
+ console.log(`[scan] ${scanned}/${queue.length} repos`);
173
+ writeFileSync(reportPath, JSON.stringify(results, null, 1));
174
+ }
175
+ inFlight -= 1;
176
+ pump();
177
+ });
178
+ }
179
+ };
180
+ pump();
181
+ });
182
+ writeFileSync(reportPath, JSON.stringify(results, null, 1));
183
+ const lines = [];
184
+ for (const scan of results.sort((a, b) => a.repo.localeCompare(b.repo))) {
185
+ for (const [skill, dir] of Object.entries(scan.resolved).sort()) {
186
+ lines.push(dir === '' ? scan.repo : `${scan.repo}#${dir}`);
187
+ void skill;
188
+ }
189
+ }
190
+ writeFileSync(candidatesPath, [...new Set(lines)].join('\n') + '\n');
191
+ const importRepos = results.filter((r) => r.verdict === 'import');
192
+ const licences = {};
193
+ for (const r of importRepos)
194
+ licences[r.licence ?? '?'] = (licences[r.licence ?? '?'] ?? 0) + 1;
195
+ const resolvedCount = importRepos.reduce((n, r) => n + Object.keys(r.resolved).length, 0);
196
+ const unresolvedLicensed = importRepos.reduce((n, r) => n + r.unresolved.length, 0);
197
+ console.log('[scan] done');
198
+ console.log(` repos: ${results.length} scanned · ${importRepos.length} licence-pass`);
199
+ console.log(` licences (passing): ${JSON.stringify(licences)}`);
200
+ console.log(` skills: ${resolvedCount} resolved SKILL.md paths → ${candidatesPath}`);
201
+ console.log(` ${unresolvedLicensed} skills in licensed repos had no resolvable SKILL.md path`);
202
+ console.log(` full report: ${reportPath}`);
203
+ }
204
+ main().catch((err) => {
205
+ console.error(err);
206
+ process.exit(1);
207
+ });
@@ -0,0 +1,36 @@
1
+ import type { SeedEpisode } from './episode-fetch.js';
2
+ export declare const CURATED_SEED_AUDIT_SCHEMA_VERSION: "jinn.curated-seed-audit.v1";
3
+ export interface CuratedSeedRecordAudit {
4
+ id: string;
5
+ automatedStatus: 'pass' | 'fail';
6
+ errors: string[];
7
+ }
8
+ export interface CuratedSeedBatchAudit {
9
+ schemaVersion: typeof CURATED_SEED_AUDIT_SCHEMA_VERSION;
10
+ repoSlug: string;
11
+ probeTerms: string[];
12
+ requiredRecords: number;
13
+ recordCount: number;
14
+ eligibleRecordCount: number;
15
+ automatedStatus: 'pass' | 'fail';
16
+ errors: string[];
17
+ records: CuratedSeedRecordAudit[];
18
+ humanCurationRequired: true;
19
+ publishAuthorized: false;
20
+ liveProbe: {
21
+ status: 'not-run';
22
+ command: string;
23
+ };
24
+ }
25
+ export interface AuditCuratedSeedBatchOptions {
26
+ repoSlug: string;
27
+ episodes: SeedEpisode[];
28
+ }
29
+ /**
30
+ * Audit the automatable portion of a candidate curated seed batch.
31
+ *
32
+ * A passing report means only that the records meet the mechanical evidence,
33
+ * provenance, retrieval, and scrub gates. It deliberately cannot approve the
34
+ * curation judgment, publish records, or claim that a live corpus probe passed.
35
+ */
36
+ export declare function auditCuratedSeedBatch({ repoSlug, episodes, }: AuditCuratedSeedBatchOptions): Promise<CuratedSeedBatchAudit>;
@@ -0,0 +1,103 @@
1
+ import { deriveRepositorySearchTerms, hasRetrievalMark } from '@jinn-network/plugin';
2
+ import { CORPUS_ONBOARDING_K } from '../corpus-probes.js';
3
+ import { publishedEpisodeTags, seedEpisodePrivacyRedactionCount, } from './episode-execute.js';
4
+ export const CURATED_SEED_AUDIT_SCHEMA_VERSION = 'jinn.curated-seed-audit.v1';
5
+ const REQUIRED_STEP_LABELS = ['failure', 'fix', 'command'];
6
+ const ACCEPTED_VERIFIABILITY_TIERS = new Set(['tests-passed', 'evaluator-verified']);
7
+ /**
8
+ * Audit the automatable portion of a candidate curated seed batch.
9
+ *
10
+ * A passing report means only that the records meet the mechanical evidence,
11
+ * provenance, retrieval, and scrub gates. It deliberately cannot approve the
12
+ * curation judgment, publish records, or claim that a live corpus probe passed.
13
+ */
14
+ export async function auditCuratedSeedBatch({ repoSlug, episodes, }) {
15
+ const probeTerms = deriveRepositorySearchTerms(repoSlug);
16
+ const seenIds = new Set();
17
+ const seenSourceUrls = new Set();
18
+ const commitUrlPrefix = `https://github.com/${repoSlug}/commit/`;
19
+ const records = [];
20
+ for (const episode of episodes) {
21
+ const errors = [];
22
+ const publishedTags = publishedEpisodeTags(episode);
23
+ if (episode.repo !== repoSlug) {
24
+ errors.push(`repo must be ${repoSlug}`);
25
+ }
26
+ if (!episode.baseCommit) {
27
+ errors.push('baseCommit must name a full commit');
28
+ }
29
+ if (!hasRetrievalMark(publishedTags)) {
30
+ errors.push('published tags missing retrieval visibility mark');
31
+ }
32
+ if (!probeTerms.some((term) => publishedTags.includes(term))) {
33
+ errors.push(`published tags must include a shared probe term: ${probeTerms.join(', ')}`);
34
+ }
35
+ if (episode.outcome.status !== 'completed' ||
36
+ !ACCEPTED_VERIFIABILITY_TIERS.has(episode.outcome.verifiabilityTier)) {
37
+ errors.push('outcome must be completed and tests-passed or evaluator-verified');
38
+ }
39
+ const stepLabels = new Set(episode.steps.map((step) => step.label));
40
+ if (!REQUIRED_STEP_LABELS.every((label) => stepLabels.has(label))) {
41
+ errors.push('steps must include failure, fix, and command evidence');
42
+ }
43
+ if (episode.attribution.origin !== 'operator-recorded-session') {
44
+ errors.push('attribution origin must be operator-recorded-session');
45
+ }
46
+ const sourceUrl = episode.attribution.sourceUrl;
47
+ const sourceCommit = sourceUrl?.startsWith(commitUrlPrefix) === true
48
+ ? sourceUrl.slice(commitUrlPrefix.length)
49
+ : '';
50
+ if (!/^[0-9a-f]{40}$/.test(sourceCommit)) {
51
+ errors.push(`sourceUrl must name a full ${repoSlug} commit`);
52
+ }
53
+ if (seenIds.has(episode.id)) {
54
+ errors.push(`duplicate episode id: ${episode.id}`);
55
+ }
56
+ else {
57
+ seenIds.add(episode.id);
58
+ }
59
+ if (sourceUrl) {
60
+ if (seenSourceUrls.has(sourceUrl)) {
61
+ errors.push(`duplicate sourceUrl: ${sourceUrl}`);
62
+ }
63
+ else {
64
+ seenSourceUrls.add(sourceUrl);
65
+ }
66
+ }
67
+ const redactionCount = await seedEpisodePrivacyRedactionCount(episode);
68
+ if (redactionCount > 0) {
69
+ errors.push(`seed scrub rejected content: ${redactionCount} redaction(s) would be required`);
70
+ }
71
+ records.push({
72
+ id: episode.id,
73
+ automatedStatus: errors.length === 0 ? 'pass' : 'fail',
74
+ errors,
75
+ });
76
+ }
77
+ const eligibleRecordCount = records.filter((record) => record.automatedStatus === 'pass').length;
78
+ const errors = [];
79
+ const failedRecordCount = records.length - eligibleRecordCount;
80
+ if (failedRecordCount > 0) {
81
+ errors.push(`${failedRecordCount} record${failedRecordCount === 1 ? '' : 's'} failed automated checks`);
82
+ }
83
+ if (eligibleRecordCount < CORPUS_ONBOARDING_K) {
84
+ errors.push(`need at least ${CORPUS_ONBOARDING_K} mechanically eligible records; found ${eligibleRecordCount}`);
85
+ }
86
+ return {
87
+ schemaVersion: CURATED_SEED_AUDIT_SCHEMA_VERSION,
88
+ repoSlug,
89
+ probeTerms,
90
+ requiredRecords: CORPUS_ONBOARDING_K,
91
+ recordCount: episodes.length,
92
+ eligibleRecordCount,
93
+ automatedStatus: errors.length === 0 ? 'pass' : 'fail',
94
+ errors,
95
+ records,
96
+ humanCurationRequired: true,
97
+ publishAuthorized: false,
98
+ liveProbe: {
99
+ status: 'not-run',
100
+ command: `jinn-layer corpus probe "${repoSlug}" --json`,
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `executeEpisodes()` — publish approved evidence-episode rows through the
3
+ * SAME `capture() -> publish()` path skill seeds use (issue #1771). Each
4
+ * episode becomes a synthetic captured task: one step per authored
5
+ * `SeedEpisodeStep` (content), plus a final step carrying the synthesis +
6
+ * attribution (episode-fetch.ts's step convention). `provenance: 'imported'`
7
+ * throughout, same as skill seeds — excluded from the demand signal and
8
+ * emissions eligibility by every provenance-aware reader.
9
+ *
10
+ * Idempotent by seed identity (state.ts, shared with the skill lane):
11
+ * unchanged content republishes nothing; changed content republishes and
12
+ * points the new record's `seed.attribution.supersedes` at the prior
13
+ * envelopeRef.
14
+ */
15
+ import { type HarnessPublishDeps } from '../publish.js';
16
+ import { type EpisodeSource, type SeedEpisode } from './episode-fetch.js';
17
+ import type { EpisodeImportReport } from './episode-report.js';
18
+ import { type SeedImportStateStore } from './state.js';
19
+ export interface EpisodeImportResult {
20
+ imported: Array<{
21
+ id: string;
22
+ envelopeRef: string;
23
+ anchorTx: string | null;
24
+ /** Prior envelopeRef this publish supersedes, or null for a fresh identity. */
25
+ supersedes: string | null;
26
+ /** Publication succeeded, but local lineage state needs operator recovery. */
27
+ stateWarning?: string;
28
+ /** Anchor succeeded, but the local publication ledger needs recovery. */
29
+ ledgerWarning?: string;
30
+ }>;
31
+ skipped: Array<{
32
+ id: string;
33
+ reason: string;
34
+ }>;
35
+ errors: Array<{
36
+ id: string;
37
+ error: string;
38
+ }>;
39
+ }
40
+ /**
41
+ * Episode tags: the `seed-import` marker (shared with skill seeds — the
42
+ * "how did this land in the corpus" signal) plus the episode's own declared
43
+ * tags, deduped and capped. Mirrors `seedTags()` in execute.ts.
44
+ */
45
+ export declare function publishedEpisodeTags(episode: SeedEpisode): string[];
46
+ /** Read-only privacy preflight shared by execute and curated-batch auditing. */
47
+ export declare function seedEpisodePrivacyRedactionCount(episode: SeedEpisode): Promise<number>;
48
+ export declare function executeEpisodes(report: EpisodeImportReport, source: EpisodeSource, deps: HarnessPublishDeps, opts?: {
49
+ state?: SeedImportStateStore;
50
+ }): Promise<EpisodeImportResult>;
@@ -0,0 +1,235 @@
1
+ /**
2
+ * `executeEpisodes()` — publish approved evidence-episode rows through the
3
+ * SAME `capture() -> publish()` path skill seeds use (issue #1771). Each
4
+ * episode becomes a synthetic captured task: one step per authored
5
+ * `SeedEpisodeStep` (content), plus a final step carrying the synthesis +
6
+ * attribution (episode-fetch.ts's step convention). `provenance: 'imported'`
7
+ * throughout, same as skill seeds — excluded from the demand signal and
8
+ * emissions eligibility by every provenance-aware reader.
9
+ *
10
+ * Idempotent by seed identity (state.ts, shared with the skill lane):
11
+ * unchanged content republishes nothing; changed content republishes and
12
+ * points the new record's `seed.attribution.supersedes` at the prior
13
+ * envelopeRef.
14
+ */
15
+ import { capture } from '../capture.js';
16
+ import { buildSeedScrubPipeline } from '@jinn-network/core/scrub';
17
+ import { publish, PublishLedgerError, } from '../publish.js';
18
+ import { episodeContentDigest } from './episode-fetch.js';
19
+ import { createMemorySeedImportState, } from './state.js';
20
+ /** Same caps as seed-import/execute.ts's skill tags (envelope-v0.md size limits). */
21
+ const MAX_TAG_CHARS = 64;
22
+ const MAX_TAGS = 16;
23
+ /**
24
+ * Episode tags: the `seed-import` marker (shared with skill seeds — the
25
+ * "how did this land in the corpus" signal) plus the episode's own declared
26
+ * tags, deduped and capped. Mirrors `seedTags()` in execute.ts.
27
+ */
28
+ export function publishedEpisodeTags(episode) {
29
+ const tags = [];
30
+ for (const candidate of ['seed-import', ...episode.tags]) {
31
+ const tag = candidate.trim().slice(0, MAX_TAG_CHARS);
32
+ if (tag && !tags.includes(tag))
33
+ tags.push(tag);
34
+ }
35
+ return tags.slice(0, MAX_TAGS);
36
+ }
37
+ function toCapturedTask(episode, now, supersedes) {
38
+ const nanoBase = now.getTime();
39
+ const nanoAt = (offset) => `${nanoBase + offset}000000`;
40
+ const contentSteps = episode.steps.map((step, i) => ({
41
+ spanId: `seed-${i + 1}`,
42
+ parentSpanId: null,
43
+ name: `seed:step:${step.label}`,
44
+ startTimeUnixNano: nanoAt(i),
45
+ endTimeUnixNano: nanoAt(i),
46
+ attributes: {
47
+ 'seed.step.label': step.label,
48
+ 'seed.step.title': step.title,
49
+ 'seed.step.text': step.text,
50
+ },
51
+ redactedKeys: [],
52
+ }));
53
+ const metaIndex = contentSteps.length;
54
+ const metaStep = {
55
+ spanId: `seed-${metaIndex + 1}`,
56
+ parentSpanId: null,
57
+ name: 'seed:synthesis',
58
+ startTimeUnixNano: nanoAt(metaIndex),
59
+ endTimeUnixNano: nanoAt(metaIndex),
60
+ attributes: {
61
+ 'seed.synthesis': episode.synthesis,
62
+ 'seed.attribution': {
63
+ repo: episode.repo,
64
+ ...(episode.baseCommit ? { baseCommit: episode.baseCommit } : {}),
65
+ origin: episode.attribution.origin,
66
+ ...(episode.attribution.sourceUrl ? { sourceUrl: episode.attribution.sourceUrl } : {}),
67
+ ...(supersedes ? { supersedes } : {}),
68
+ },
69
+ },
70
+ redactedKeys: [],
71
+ };
72
+ return {
73
+ session: { sessionId: `seed-episode:${episode.id}`, capturedAt: now.toISOString() },
74
+ task: {
75
+ summary: episode.taskSummary,
76
+ distributionTags: publishedEpisodeTags(episode),
77
+ repositorySlug: episode.repo,
78
+ ...(episode.baseCommit ? { baseCommit: episode.baseCommit } : {}),
79
+ },
80
+ environment: {
81
+ harness: { name: 'jinn-layer-seed-episode-import', version: '0.1.0' },
82
+ model: 'none',
83
+ tools: [],
84
+ },
85
+ steps: [...contentSteps, metaStep],
86
+ outcome: { status: episode.outcome.status, verifiabilityTier: episode.outcome.verifiabilityTier },
87
+ cost: { durationMs: 0 },
88
+ provenance: 'imported',
89
+ };
90
+ }
91
+ /** Every episode-originated string that can influence the published envelope. */
92
+ function episodePrivacyAttributes(episode) {
93
+ const attributes = {
94
+ 'episode.id': episode.id,
95
+ 'episode.repo': episode.repo,
96
+ 'episode.taskSummary': episode.taskSummary,
97
+ 'episode.outcome.status': episode.outcome.status,
98
+ 'episode.outcome.verifiabilityTier': episode.outcome.verifiabilityTier,
99
+ 'episode.synthesis': episode.synthesis,
100
+ 'episode.attribution.origin': episode.attribution.origin,
101
+ };
102
+ if (episode.baseCommit)
103
+ attributes['episode.baseCommit'] = episode.baseCommit;
104
+ if (episode.attribution.sourceUrl) {
105
+ attributes['episode.attribution.sourceUrl'] = episode.attribution.sourceUrl;
106
+ }
107
+ episode.tags.forEach((tag, index) => {
108
+ attributes[`episode.tags[${index}]`] = tag;
109
+ });
110
+ episode.steps.forEach((step, index) => {
111
+ attributes[`episode.steps[${index}].label`] = step.label;
112
+ attributes[`episode.steps[${index}].title`] = step.title;
113
+ attributes[`episode.steps[${index}].text`] = step.text;
114
+ });
115
+ return attributes;
116
+ }
117
+ /** Read-only privacy preflight shared by execute and curated-batch auditing. */
118
+ export async function seedEpisodePrivacyRedactionCount(episode) {
119
+ const scrub = await buildSeedScrubPipeline().run(episodePrivacyAttributes(episode));
120
+ return scrub.redactions.length;
121
+ }
122
+ export async function executeEpisodes(report, source, deps, opts = {}) {
123
+ const episodes = new Map();
124
+ for (const episode of await source.list()) {
125
+ if (!episodes.has(episode.id))
126
+ episodes.set(episode.id, episode);
127
+ }
128
+ const result = { imported: [], skipped: [], errors: [] };
129
+ const now = deps.now?.() ?? new Date();
130
+ // Evidence episodes are public, transformed, human-reviewed seeds — like
131
+ // the skill lane, not operator trace data — so they run the seed profile
132
+ // (plan §4.4; spec/2026-07-02-jinn-harness-network.md §7): deterministic
133
+ // key policy, plain-patterns, and secretlint pass-1 only. The strict
134
+ // trace profile's probabilistic stages (openredaction, entropy fallback)
135
+ // false-positive on ordinary words and hex-looking SHAs in this content
136
+ // (#1784) and are not appropriate for a pre-vetted, checked-in corpus.
137
+ const episodeScrubPipeline = buildSeedScrubPipeline();
138
+ const state = opts.state ?? createMemorySeedImportState();
139
+ for (const row of report) {
140
+ if (row.verdict === 'skip') {
141
+ result.skipped.push({ id: row.id, reason: row.reason });
142
+ continue;
143
+ }
144
+ try {
145
+ const episode = episodes.get(row.id);
146
+ if (!episode)
147
+ throw new Error(`episode ${row.id} not found in source ${source.name}`);
148
+ const identity = `episode:${episode.id}`;
149
+ const contentHash = episodeContentDigest(episode);
150
+ if (contentHash !== row.contentDigest) {
151
+ throw new Error(`approved content digest ${row.contentDigest} does not match execute-time digest ${contentHash} for ${row.id}`);
152
+ }
153
+ let prior;
154
+ try {
155
+ prior = state.get(identity);
156
+ }
157
+ catch (err) {
158
+ result.errors.push({
159
+ id: row.id,
160
+ error: err instanceof Error ? err.message : String(err),
161
+ });
162
+ break;
163
+ }
164
+ if (prior && prior.contentHash === contentHash) {
165
+ result.skipped.push({ id: row.id, reason: `unchanged since ${prior.envelopeRef}` });
166
+ continue;
167
+ }
168
+ const privacy = await episodeScrubPipeline.run(episodePrivacyAttributes(episode));
169
+ if (privacy.redactions.length > 0) {
170
+ const detectors = [
171
+ ...new Set(privacy.redactions.map((redaction) => `${redaction.stage}${redaction.detail ? `:${redaction.detail}` : ''}`)),
172
+ ];
173
+ throw new Error(`sensitive content detected (${detectors.join(', ')}); refusing to publish evidence episode ${row.id}`);
174
+ }
175
+ const pending = await capture(toCapturedTask(episode, now, prior?.envelopeRef), {
176
+ pipeline: episodeScrubPipeline,
177
+ });
178
+ const sensitiveRedactions = pending.redactions.filter((redaction) => redaction.stage !== 'fit');
179
+ if (sensitiveRedactions.length > 0) {
180
+ const detectors = [
181
+ ...new Set(sensitiveRedactions.map((redaction) => `${redaction.stage}${redaction.detail ? `:${redaction.detail}` : ''}`)),
182
+ ];
183
+ throw new Error(`sensitive content detected (${detectors.join(', ')}); refusing to publish evidence episode ${row.id}`);
184
+ }
185
+ let published;
186
+ let ledgerWarning;
187
+ try {
188
+ published = await publish(pending, deps);
189
+ }
190
+ catch (err) {
191
+ if (err instanceof PublishLedgerError) {
192
+ published = err.result;
193
+ ledgerWarning = err.message;
194
+ }
195
+ else {
196
+ const detail = err instanceof Error ? err.message : String(err);
197
+ result.errors.push({
198
+ id: row.id,
199
+ error: `publication outcome unknown; do not auto-retry: ${detail}`,
200
+ });
201
+ break;
202
+ }
203
+ }
204
+ if (published.vetoed)
205
+ throw new Error('unexpected veto on seed publish');
206
+ let stateWarning;
207
+ try {
208
+ state.set(identity, { contentHash, envelopeRef: published.envelopeRef, publishedAt: now.toISOString() });
209
+ }
210
+ catch (err) {
211
+ const detail = err instanceof Error ? err.message : String(err);
212
+ stateWarning =
213
+ `published ${published.envelopeRef}, but seed-import state persistence failed: ${detail}; ` +
214
+ 'recovery required before retrying this episode';
215
+ }
216
+ result.imported.push({
217
+ id: row.id,
218
+ envelopeRef: published.envelopeRef,
219
+ anchorTx: published.anchorTx,
220
+ supersedes: prior?.envelopeRef ?? null,
221
+ ...(ledgerWarning ? { ledgerWarning } : {}),
222
+ ...(stateWarning ? { stateWarning } : {}),
223
+ });
224
+ if (ledgerWarning || stateWarning)
225
+ break;
226
+ }
227
+ catch (err) {
228
+ result.errors.push({
229
+ id: row.id,
230
+ error: err instanceof Error ? err.message : String(err),
231
+ });
232
+ }
233
+ }
234
+ return result;
235
+ }