@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,34 @@
|
|
|
1
|
+
import type { DistillCluster } from './distill.js';
|
|
2
|
+
export type ClusterSelectionTier = DistillCluster['tier'];
|
|
3
|
+
export interface ClusterSelectionOptions {
|
|
4
|
+
maxClusters?: number;
|
|
5
|
+
maxContrastive?: number;
|
|
6
|
+
maxLessons?: number;
|
|
7
|
+
maxPatterns?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ClusterScore {
|
|
10
|
+
clusterId: string;
|
|
11
|
+
tier: ClusterSelectionTier;
|
|
12
|
+
score: number;
|
|
13
|
+
estimatedInputTokens: number;
|
|
14
|
+
groupSize: number;
|
|
15
|
+
nPass: number;
|
|
16
|
+
nFail: number;
|
|
17
|
+
reasons: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface SelectedCluster {
|
|
20
|
+
cluster: DistillCluster;
|
|
21
|
+
score: ClusterScore;
|
|
22
|
+
}
|
|
23
|
+
export interface ClusterSelectionResult {
|
|
24
|
+
selected: SelectedCluster[];
|
|
25
|
+
rejected: Array<{
|
|
26
|
+
clusterId: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
score: ClusterScore;
|
|
29
|
+
}>;
|
|
30
|
+
caps: Required<ClusterSelectionOptions>;
|
|
31
|
+
}
|
|
32
|
+
export declare function scoreCluster(cluster: DistillCluster): ClusterScore;
|
|
33
|
+
export declare function resolveClusterSelectionCaps(opts?: ClusterSelectionOptions): Required<ClusterSelectionOptions>;
|
|
34
|
+
export declare function selectUsefulClusters(clusters: DistillCluster[], opts?: ClusterSelectionOptions): ClusterSelectionResult;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const DEFAULT_CAPS = {
|
|
2
|
+
maxContrastive: 8,
|
|
3
|
+
maxLessons: 6,
|
|
4
|
+
maxPatterns: 4,
|
|
5
|
+
maxClusters: 18,
|
|
6
|
+
};
|
|
7
|
+
const TIER_BASE = {
|
|
8
|
+
contrastive: 10_000,
|
|
9
|
+
lesson: 6_000,
|
|
10
|
+
pattern: 3_000,
|
|
11
|
+
};
|
|
12
|
+
function estimateTokens(value) {
|
|
13
|
+
return Math.ceil(Buffer.byteLength(JSON.stringify(value ?? ''), 'utf8') / 4);
|
|
14
|
+
}
|
|
15
|
+
function numberField(input, key) {
|
|
16
|
+
if (input && typeof input === 'object' && key in input) {
|
|
17
|
+
const value = input[key];
|
|
18
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function inputItems(input) {
|
|
23
|
+
if (Array.isArray(input))
|
|
24
|
+
return input;
|
|
25
|
+
if (input && typeof input === 'object' && Array.isArray(input.items)) {
|
|
26
|
+
return input.items;
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
function outcomeStatus(item) {
|
|
31
|
+
if (!item || typeof item !== 'object')
|
|
32
|
+
return null;
|
|
33
|
+
const outcome = item.outcome;
|
|
34
|
+
if (!outcome || typeof outcome !== 'object')
|
|
35
|
+
return null;
|
|
36
|
+
const status = outcome.status;
|
|
37
|
+
return typeof status === 'string' ? status : null;
|
|
38
|
+
}
|
|
39
|
+
function clusterCounts(cluster) {
|
|
40
|
+
const items = inputItems(cluster.input);
|
|
41
|
+
const derivedPass = items.filter((item) => outcomeStatus(item) === 'completed').length;
|
|
42
|
+
const derivedFail = items.filter((item) => outcomeStatus(item) === 'failed').length;
|
|
43
|
+
const nPass = numberField(cluster.input, 'nPass') ?? derivedPass;
|
|
44
|
+
const nFail = numberField(cluster.input, 'nFail') ?? derivedFail;
|
|
45
|
+
const groupSize = numberField(cluster.input, 'groupSize') ?? Math.max(items.length, nPass + nFail, cluster.evidenceRefs.length);
|
|
46
|
+
return { groupSize, nPass, nFail };
|
|
47
|
+
}
|
|
48
|
+
export function scoreCluster(cluster) {
|
|
49
|
+
const { groupSize, nPass, nFail } = clusterCounts(cluster);
|
|
50
|
+
const estimatedInputTokens = estimateTokens(cluster.input);
|
|
51
|
+
const reasons = [cluster.tier];
|
|
52
|
+
let score = TIER_BASE[cluster.tier];
|
|
53
|
+
if (cluster.tier === 'contrastive') {
|
|
54
|
+
score += 1_500;
|
|
55
|
+
score += Math.min(nPass, nFail) * 250;
|
|
56
|
+
reasons.push('contrastive-pass-fail-delta');
|
|
57
|
+
}
|
|
58
|
+
if (cluster.tier === 'lesson') {
|
|
59
|
+
score += Math.min(nFail, 8) * 180;
|
|
60
|
+
if (nFail > 1)
|
|
61
|
+
reasons.push('high-attempt-lesson');
|
|
62
|
+
}
|
|
63
|
+
if (cluster.tier === 'pattern') {
|
|
64
|
+
score += Math.min(nPass, 8) * 120;
|
|
65
|
+
if (nPass > 1)
|
|
66
|
+
reasons.push('multi-attempt-pattern');
|
|
67
|
+
}
|
|
68
|
+
score += Math.min(groupSize, 8) * 80;
|
|
69
|
+
if (groupSize > 1)
|
|
70
|
+
reasons.push('multi-attempt');
|
|
71
|
+
const tokenPenalty = Math.ceil(estimatedInputTokens / 500);
|
|
72
|
+
score -= tokenPenalty;
|
|
73
|
+
if (tokenPenalty > 0)
|
|
74
|
+
reasons.push('token-penalty');
|
|
75
|
+
return {
|
|
76
|
+
clusterId: cluster.clusterId,
|
|
77
|
+
tier: cluster.tier,
|
|
78
|
+
score,
|
|
79
|
+
estimatedInputTokens,
|
|
80
|
+
groupSize,
|
|
81
|
+
nPass,
|
|
82
|
+
nFail,
|
|
83
|
+
reasons,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function resolveClusterSelectionCaps(opts = {}) {
|
|
87
|
+
return {
|
|
88
|
+
maxContrastive: opts.maxContrastive ?? DEFAULT_CAPS.maxContrastive,
|
|
89
|
+
maxLessons: opts.maxLessons ?? DEFAULT_CAPS.maxLessons,
|
|
90
|
+
maxPatterns: opts.maxPatterns ?? DEFAULT_CAPS.maxPatterns,
|
|
91
|
+
maxClusters: opts.maxClusters ?? DEFAULT_CAPS.maxClusters,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function selectUsefulClusters(clusters, opts = {}) {
|
|
95
|
+
const caps = resolveClusterSelectionCaps(opts);
|
|
96
|
+
const ranked = clusters
|
|
97
|
+
.map((cluster) => ({ cluster, score: scoreCluster(cluster) }))
|
|
98
|
+
.sort((a, b) => {
|
|
99
|
+
if (b.score.score !== a.score.score)
|
|
100
|
+
return b.score.score - a.score.score;
|
|
101
|
+
return a.cluster.clusterId.localeCompare(b.cluster.clusterId);
|
|
102
|
+
});
|
|
103
|
+
let contrastive = 0;
|
|
104
|
+
let lessons = 0;
|
|
105
|
+
let patterns = 0;
|
|
106
|
+
const selected = [];
|
|
107
|
+
const rejected = [];
|
|
108
|
+
for (const row of ranked) {
|
|
109
|
+
if (selected.length >= caps.maxClusters) {
|
|
110
|
+
rejected.push({ clusterId: row.cluster.clusterId, reason: 'total-cap', score: row.score });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (row.cluster.tier === 'contrastive' && contrastive >= caps.maxContrastive) {
|
|
114
|
+
rejected.push({ clusterId: row.cluster.clusterId, reason: 'contrastive-cap', score: row.score });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (row.cluster.tier === 'lesson' && lessons >= caps.maxLessons) {
|
|
118
|
+
rejected.push({ clusterId: row.cluster.clusterId, reason: 'lesson-cap', score: row.score });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (row.cluster.tier === 'pattern' && patterns >= caps.maxPatterns) {
|
|
122
|
+
rejected.push({ clusterId: row.cluster.clusterId, reason: 'pattern-cap', score: row.score });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
selected.push(row);
|
|
126
|
+
if (row.cluster.tier === 'contrastive')
|
|
127
|
+
contrastive += 1;
|
|
128
|
+
else if (row.cluster.tier === 'lesson')
|
|
129
|
+
lessons += 1;
|
|
130
|
+
else
|
|
131
|
+
patterns += 1;
|
|
132
|
+
}
|
|
133
|
+
selected.sort((a, b) => a.cluster.clusterId.localeCompare(b.cluster.clusterId));
|
|
134
|
+
return { selected, rejected, caps };
|
|
135
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The clustering step — group gate-eligible layer-1 evidence into
|
|
3
|
+
* `DistillCluster[]` for the distiller (spec/2026-07-06-distillation-v1.md §7).
|
|
4
|
+
*
|
|
5
|
+
* v1 clustering is deliberately simple: run each item through the promotion
|
|
6
|
+
* gate (`evaluateEligibility`, §6), drop ineligible evidence, then group the
|
|
7
|
+
* survivors by `(tier, instanceId)` — one cluster per distinct instance id per
|
|
8
|
+
* tier. A `pattern` and a `lesson` for the same instance are SEPARATE clusters
|
|
9
|
+
* (distinct-instance clustering; recursion/hierarchy are v3). Pure and
|
|
10
|
+
* deterministic: clusters come back sorted by `clusterId`.
|
|
11
|
+
*/
|
|
12
|
+
import type { DistillCluster, SkillKind } from './distill.js';
|
|
13
|
+
import type { SkillPackage } from './skill-package.js';
|
|
14
|
+
import type { TraceEnvelopeV0 } from './envelope.js';
|
|
15
|
+
export interface ClusterItem {
|
|
16
|
+
/** Source evidence envelope ref → `metadata.jinn.provenance`. */
|
|
17
|
+
ref: string;
|
|
18
|
+
/** The SWE-bench-style instance id this evidence belongs to. */
|
|
19
|
+
instanceId: string;
|
|
20
|
+
/** Lineage equivalence key — echo instances collapse to source (§7). */
|
|
21
|
+
lineageKey?: string;
|
|
22
|
+
/** The scrubbed layer-1 trace envelope. */
|
|
23
|
+
env: TraceEnvelopeV0;
|
|
24
|
+
}
|
|
25
|
+
export interface ClusterOptions {
|
|
26
|
+
/** Held-out cap-v0 slate predicate — an item's instance in the slate is dropped. */
|
|
27
|
+
heldOut?: (instanceId: string) => boolean;
|
|
28
|
+
/** Overrides the gate's placeholder-density defacement threshold (§6). */
|
|
29
|
+
maxPlaceholderDensity?: number;
|
|
30
|
+
/**
|
|
31
|
+
* `network-strict` preserves the evaluator-verified promotion gate. The
|
|
32
|
+
* local experimental path may draft private skills from user-accepted traces,
|
|
33
|
+
* but those are not publishable network evidence.
|
|
34
|
+
*/
|
|
35
|
+
eligibilityMode?: 'network-strict' | 'local-experimental';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Group eligible layer-1 evidence into distill clusters. Ineligible items (per
|
|
39
|
+
* the gate) are dropped. An instance with a single polarity yields one
|
|
40
|
+
* `pattern:`/`lesson:` cluster; an instance carrying BOTH an
|
|
41
|
+
* (eligible) pattern and lesson folds into ONE `contrastive:` cluster whose
|
|
42
|
+
* evidence refs link both polarities, and the singles are suppressed (§7 v0.5,
|
|
43
|
+
* ExpeL). The returned clusters are sorted by `clusterId` for determinism.
|
|
44
|
+
*/
|
|
45
|
+
export declare function clusterEvidence(items: ClusterItem[], opts?: ClusterOptions): DistillCluster[];
|
|
46
|
+
/**
|
|
47
|
+
* A stage-1 published skill joined to its originating cluster's provenance —
|
|
48
|
+
* the stage-2 (cross-instance meta-distill) input (issue #1463). `pkg` supplies
|
|
49
|
+
* the meta-distiller's reading material (the distilled skill); `evidenceRefs` /
|
|
50
|
+
* `instanceIds` supply the union provenance and the ≥2-distinct-instance test.
|
|
51
|
+
*/
|
|
52
|
+
export interface Stage1PublishedSkill {
|
|
53
|
+
clusterId: string;
|
|
54
|
+
skillKind: SkillKind;
|
|
55
|
+
pkg: SkillPackage;
|
|
56
|
+
evidenceRefs: string[];
|
|
57
|
+
instanceIds: string[];
|
|
58
|
+
lineageKeys?: string[];
|
|
59
|
+
}
|
|
60
|
+
/** One labelled source inside a meta-cluster (its id is what the model echoes in `supports`). */
|
|
61
|
+
export interface MetaSource {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
description: string;
|
|
65
|
+
body: string;
|
|
66
|
+
evidenceRefs: string[];
|
|
67
|
+
instanceIds: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The three stage-1 polarities eligible for stage-2 — `cross-instance` is
|
|
71
|
+
* excluded (no meta-of-meta), so it is deliberately absent from this union.
|
|
72
|
+
*/
|
|
73
|
+
export type MetaPolarity = 'strategic-pattern' | 'failure-lesson' | 'contrastive';
|
|
74
|
+
/** A same-polarity batch of ≥2-distinct-instance stage-1 skills for the meta-distiller. */
|
|
75
|
+
export interface MetaCluster {
|
|
76
|
+
metaClusterId: string;
|
|
77
|
+
polarity: MetaPolarity;
|
|
78
|
+
gateTier: DistillCluster['tier'];
|
|
79
|
+
sources: MetaSource[];
|
|
80
|
+
}
|
|
81
|
+
export declare function lineageEquivalenceKey(item: ClusterItem): string;
|
|
82
|
+
/**
|
|
83
|
+
* Group stage-1 published skills into one meta-cluster per polarity. A polarity
|
|
84
|
+
* is meta-distillable only when it spans **≥2 distinct instances** (a
|
|
85
|
+
* cross-instance rule needs corroboration from different problems). Meta-of-meta
|
|
86
|
+
* is impossible: a `cross-instance` polarity is skipped. Deterministic —
|
|
87
|
+
* meta-clusters and their sources come back in a stable order.
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildMetaClusters(published: Stage1PublishedSkill[]): MetaCluster[];
|
package/dist/cluster.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The clustering step — group gate-eligible layer-1 evidence into
|
|
3
|
+
* `DistillCluster[]` for the distiller (spec/2026-07-06-distillation-v1.md §7).
|
|
4
|
+
*
|
|
5
|
+
* v1 clustering is deliberately simple: run each item through the promotion
|
|
6
|
+
* gate (`evaluateEligibility`, §6), drop ineligible evidence, then group the
|
|
7
|
+
* survivors by `(tier, instanceId)` — one cluster per distinct instance id per
|
|
8
|
+
* tier. A `pattern` and a `lesson` for the same instance are SEPARATE clusters
|
|
9
|
+
* (distinct-instance clustering; recursion/hierarchy are v3). Pure and
|
|
10
|
+
* deterministic: clusters come back sorted by `clusterId`.
|
|
11
|
+
*/
|
|
12
|
+
import { evaluateEligibility } from './gate.js';
|
|
13
|
+
function projectInput(items) {
|
|
14
|
+
return items.map((it) => ({
|
|
15
|
+
ref: it.ref,
|
|
16
|
+
instanceId: it.instanceId,
|
|
17
|
+
taskSummary: it.env.task.summary,
|
|
18
|
+
distributionTags: it.env.task.distributionTags,
|
|
19
|
+
outcome: { status: it.env.outcome.status, summary: it.env.outcome.summary },
|
|
20
|
+
steps: it.env.steps.map((s) => ({ name: s.name, attributes: s.attributes })),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
function buildCluster(clusterId, tier, instanceId, pass, fail) {
|
|
24
|
+
const items = [...pass, ...fail];
|
|
25
|
+
return {
|
|
26
|
+
clusterId,
|
|
27
|
+
tier,
|
|
28
|
+
evidenceRefs: items.map((it) => it.ref),
|
|
29
|
+
instanceIds: [instanceId],
|
|
30
|
+
// The distiller reads group-relative signal off the pass/fail split; carry
|
|
31
|
+
// it EXPLICITLY (§7, #1478) rather than making the model re-derive it from
|
|
32
|
+
// per-item outcomes. `items` is the flat projection (pass then fail).
|
|
33
|
+
input: {
|
|
34
|
+
instanceId,
|
|
35
|
+
tier,
|
|
36
|
+
groupSize: items.length,
|
|
37
|
+
nPass: pass.length,
|
|
38
|
+
nFail: fail.length,
|
|
39
|
+
items: projectInput(items),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Group eligible layer-1 evidence into distill clusters. Ineligible items (per
|
|
45
|
+
* the gate) are dropped. An instance with a single polarity yields one
|
|
46
|
+
* `pattern:`/`lesson:` cluster; an instance carrying BOTH an
|
|
47
|
+
* (eligible) pattern and lesson folds into ONE `contrastive:` cluster whose
|
|
48
|
+
* evidence refs link both polarities, and the singles are suppressed (§7 v0.5,
|
|
49
|
+
* ExpeL). The returned clusters are sorted by `clusterId` for determinism.
|
|
50
|
+
*/
|
|
51
|
+
export function clusterEvidence(items, opts = {}) {
|
|
52
|
+
// Bucket eligible items by (instanceId → polarity), preserving input order.
|
|
53
|
+
const byInstance = new Map();
|
|
54
|
+
let order = 0;
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
const heldOut = opts.heldOut?.(item.instanceId) ?? false;
|
|
57
|
+
const result = evaluateEligibility(item.env, {
|
|
58
|
+
heldOut,
|
|
59
|
+
maxPlaceholderDensity: opts.maxPlaceholderDensity,
|
|
60
|
+
acceptUserAccepted: opts.eligibilityMode === 'local-experimental',
|
|
61
|
+
});
|
|
62
|
+
const tier = result.eligible ? result.tier : null;
|
|
63
|
+
if (tier === null)
|
|
64
|
+
continue;
|
|
65
|
+
let slot = byInstance.get(item.instanceId);
|
|
66
|
+
if (!slot) {
|
|
67
|
+
slot = { pattern: [], lesson: [], order: order++ };
|
|
68
|
+
byInstance.set(item.instanceId, slot);
|
|
69
|
+
}
|
|
70
|
+
slot[tier].push(item);
|
|
71
|
+
}
|
|
72
|
+
const clusters = [];
|
|
73
|
+
for (const [instanceId, slot] of byInstance) {
|
|
74
|
+
const hasPattern = slot.pattern.length > 0;
|
|
75
|
+
const hasLesson = slot.lesson.length > 0;
|
|
76
|
+
if (hasPattern && hasLesson) {
|
|
77
|
+
// Precedence: the contrastive fold SUPPRESSES the pattern-only/lesson-only
|
|
78
|
+
// singles. Pattern refs precede lesson refs in the merged provenance.
|
|
79
|
+
clusters.push(buildCluster(`contrastive:${instanceId}`, 'contrastive', instanceId, slot.pattern, slot.lesson));
|
|
80
|
+
}
|
|
81
|
+
else if (hasPattern) {
|
|
82
|
+
clusters.push(buildCluster(`pattern:${instanceId}`, 'pattern', instanceId, slot.pattern, []));
|
|
83
|
+
}
|
|
84
|
+
else if (hasLesson) {
|
|
85
|
+
clusters.push(buildCluster(`lesson:${instanceId}`, 'lesson', instanceId, [], slot.lesson));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
clusters.sort((a, b) => (a.clusterId < b.clusterId ? -1 : a.clusterId > b.clusterId ? 1 : 0));
|
|
89
|
+
return clusters;
|
|
90
|
+
}
|
|
91
|
+
/** The structural-gate tier each meta-eligible polarity keeps in stage-2. */
|
|
92
|
+
const META_GATE_TIER = {
|
|
93
|
+
'strategic-pattern': 'pattern',
|
|
94
|
+
'failure-lesson': 'lesson',
|
|
95
|
+
'contrastive': 'contrastive',
|
|
96
|
+
};
|
|
97
|
+
/** Meta-eligible polarity guard — the type-level "never recurse on cross-instance" gate. */
|
|
98
|
+
function isMetaPolarity(kind) {
|
|
99
|
+
return kind in META_GATE_TIER;
|
|
100
|
+
}
|
|
101
|
+
export function lineageEquivalenceKey(item) {
|
|
102
|
+
return item.lineageKey ?? item.instanceId;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Group stage-1 published skills into one meta-cluster per polarity. A polarity
|
|
106
|
+
* is meta-distillable only when it spans **≥2 distinct instances** (a
|
|
107
|
+
* cross-instance rule needs corroboration from different problems). Meta-of-meta
|
|
108
|
+
* is impossible: a `cross-instance` polarity is skipped. Deterministic —
|
|
109
|
+
* meta-clusters and their sources come back in a stable order.
|
|
110
|
+
*/
|
|
111
|
+
export function buildMetaClusters(published) {
|
|
112
|
+
const byPolarity = new Map();
|
|
113
|
+
for (const p of published) {
|
|
114
|
+
if (!isMetaPolarity(p.skillKind))
|
|
115
|
+
continue; // never recurse on cross-instance
|
|
116
|
+
const group = byPolarity.get(p.skillKind) ?? [];
|
|
117
|
+
group.push(p);
|
|
118
|
+
byPolarity.set(p.skillKind, group);
|
|
119
|
+
}
|
|
120
|
+
const clusters = [];
|
|
121
|
+
for (const [polarity, group] of byPolarity) {
|
|
122
|
+
const distinctInstances = new Set(group.flatMap((g, gi) => g.instanceIds.map((id, ii) => g.lineageKeys?.[ii] ?? id)));
|
|
123
|
+
if (distinctInstances.size < 2)
|
|
124
|
+
continue;
|
|
125
|
+
const sources = group.map((g, i) => ({
|
|
126
|
+
id: `s${i + 1}`,
|
|
127
|
+
name: g.pkg.name,
|
|
128
|
+
description: g.pkg.description,
|
|
129
|
+
body: g.pkg.body,
|
|
130
|
+
evidenceRefs: g.evidenceRefs,
|
|
131
|
+
instanceIds: g.instanceIds,
|
|
132
|
+
}));
|
|
133
|
+
clusters.push({
|
|
134
|
+
metaClusterId: `cross-instance:${polarity}`,
|
|
135
|
+
polarity,
|
|
136
|
+
gateTier: META_GATE_TIER[polarity],
|
|
137
|
+
sources,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
clusters.sort((a, b) => (a.metaClusterId < b.metaClusterId ? -1 : a.metaClusterId > b.metaClusterId ? 1 : 0));
|
|
141
|
+
return clusters;
|
|
142
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-layer consume path — the embeddable corpus surface.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over core's `createCorpus()` and the configured corpus
|
|
5
|
+
* discovery port. No new query logic lives
|
|
6
|
+
* here: `search` delegates ref discovery to the discovery port and manifest
|
|
7
|
+
* retrieval to the corpus, then applies a client-side substring match
|
|
8
|
+
* (solverType lives in the IPFS manifest body, not in the on-chain envelope
|
|
9
|
+
* payload, so the indexer cannot filter on it — see
|
|
10
|
+
* core's HTTP discovery `queryEnvelopes`). `get` is
|
|
11
|
+
* fetchManifest + acquire.
|
|
12
|
+
*
|
|
13
|
+
* Plan: docs/superpowers/plans/2026-07-02-jinn-harness-network-v0-plan.md
|
|
14
|
+
* Task 1 (issue #1308).
|
|
15
|
+
*/
|
|
16
|
+
import { type AcquireResult, type ArtifactContent, type CorpusDiscoveryPort, type CorpusStorePort, type EnvelopeRef } from '@jinn-network/core/corpus-read';
|
|
17
|
+
import { type SignedEnvelope } from '@jinn-network/core';
|
|
18
|
+
export declare const DEFAULT_TESTNET_DISCOVERY_URL = "https://jinn-indexer-production.up.railway.app";
|
|
19
|
+
/**
|
|
20
|
+
* Default public IPFS gateway. Mirrors the `ipfsGatewayUrl` zod default in
|
|
21
|
+
* client/src/config.ts (inline schema default; not exported as a constant).
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_IPFS_GATEWAY_URL = "https://gateway.autonolas.tech";
|
|
24
|
+
export interface HarnessLayerConfig {
|
|
25
|
+
/** Discovery indexer URL. Default: the testnet Ponder indexer. */
|
|
26
|
+
discoveryUrl?: string;
|
|
27
|
+
/** IPFS gateway for manifest / donated-artifact fetches. */
|
|
28
|
+
ipfsGatewayUrl?: string;
|
|
29
|
+
/** SQLite path for the artifact cache. Default: ~/.jinn-client/harness-layer/corpus-cache.db */
|
|
30
|
+
dbPath?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Capture-meta search endpoint (#1344) — the content-aware fast path.
|
|
33
|
+
* Default: `<discoveryUrl base>/capture-meta` (the indexer serves it next
|
|
34
|
+
* to /graphql). Set to '' to disable.
|
|
35
|
+
*/
|
|
36
|
+
captureMetaUrl?: string;
|
|
37
|
+
/** Injectable HTTP fetch for the capture-meta fast path (tests). */
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
/** Injectable DiscoveryAPI (tests). Overrides discoveryUrl. */
|
|
40
|
+
discovery?: CorpusDiscoveryPort;
|
|
41
|
+
/** Injectable core read-side store (tests/embedders). Overrides dbPath. */
|
|
42
|
+
store?: CorpusStorePort & {
|
|
43
|
+
path?: string;
|
|
44
|
+
};
|
|
45
|
+
/** Injectable IPFS fetch (tests). */
|
|
46
|
+
fetchFromIpfs?: (gatewayUrl: string, cid: string) => Promise<unknown>;
|
|
47
|
+
/** Injectable origin acquire fn (tests). */
|
|
48
|
+
acquireFn?: (endpoint: string, sha256: string, privateKey?: string) => Promise<Buffer | null | AcquireResult>;
|
|
49
|
+
}
|
|
50
|
+
export interface ResolvedHarnessLayerConfig {
|
|
51
|
+
discoveryUrl: string;
|
|
52
|
+
ipfsGatewayUrl: string;
|
|
53
|
+
dbPath: string;
|
|
54
|
+
/** '' when the content-aware fast path is disabled. */
|
|
55
|
+
captureMetaUrl: string;
|
|
56
|
+
}
|
|
57
|
+
/** One search result — a corpus record ref plus its provenance fields. */
|
|
58
|
+
export interface CorpusSearchHit {
|
|
59
|
+
/** Human-readable title: `<solverType> / <role>`. */
|
|
60
|
+
title: string;
|
|
61
|
+
/** Record ref (the envelope manifest CID) — pass to `get()`. */
|
|
62
|
+
ref: string;
|
|
63
|
+
solverType: string;
|
|
64
|
+
role: string;
|
|
65
|
+
artifactTypes: string[];
|
|
66
|
+
/**
|
|
67
|
+
* `'skill'` when the record carries a `jinn.skill.v1` artifact, else
|
|
68
|
+
* `'trace'`. Note: legacy pre-#1394 seeded-trace records (skill.md as a step
|
|
69
|
+
* attribute, no first-class artifact) classify as `'trace'` here even though
|
|
70
|
+
* `extractSkill()` still recognises them via its fallback shape — acceptable
|
|
71
|
+
* because the layer-2 re-import supersedes them with first-class records.
|
|
72
|
+
*/
|
|
73
|
+
kind: 'skill' | 'trace';
|
|
74
|
+
evidenceTier: EnvelopeRef['evidenceTier'];
|
|
75
|
+
generatedAt: number;
|
|
76
|
+
publishedAt: number;
|
|
77
|
+
operator: {
|
|
78
|
+
agentId: string;
|
|
79
|
+
safeAddress: string;
|
|
80
|
+
};
|
|
81
|
+
task: {
|
|
82
|
+
cid: string;
|
|
83
|
+
requestId: string;
|
|
84
|
+
} | null;
|
|
85
|
+
/** Distribution tags, when the hit came via the capture-meta fast path. */
|
|
86
|
+
tags?: string[];
|
|
87
|
+
/** Scrubbed task summary, when the hit came via the capture-meta fast path. */
|
|
88
|
+
summary?: string;
|
|
89
|
+
/** Repository identity from indexed canonical capture metadata. */
|
|
90
|
+
repositorySlug?: string;
|
|
91
|
+
/** Authored outcome/seed synthesis from indexed canonical capture metadata. */
|
|
92
|
+
synthesis?: string;
|
|
93
|
+
/** Named W2 allowlist decision; absent on legacy index rows. */
|
|
94
|
+
retrievalVisible?: boolean;
|
|
95
|
+
}
|
|
96
|
+
export interface CorpusArtifact {
|
|
97
|
+
sha256: string;
|
|
98
|
+
artifactType: string;
|
|
99
|
+
content: Buffer;
|
|
100
|
+
source: ArtifactContent['source'];
|
|
101
|
+
sizeBytes: number;
|
|
102
|
+
}
|
|
103
|
+
/** A fully-fetched corpus record: the signed envelope plus artifact bytes. */
|
|
104
|
+
export interface CorpusRecord {
|
|
105
|
+
ref: string;
|
|
106
|
+
envelope: SignedEnvelope;
|
|
107
|
+
provenance: {
|
|
108
|
+
operator: {
|
|
109
|
+
agentId: string;
|
|
110
|
+
safeAddress: string;
|
|
111
|
+
};
|
|
112
|
+
evidenceTier: EnvelopeRef['evidenceTier'];
|
|
113
|
+
publishedAt: number;
|
|
114
|
+
};
|
|
115
|
+
artifacts: CorpusArtifact[];
|
|
116
|
+
}
|
|
117
|
+
export interface HarnessLayer {
|
|
118
|
+
readonly config: ResolvedHarnessLayerConfig;
|
|
119
|
+
corpus: {
|
|
120
|
+
/**
|
|
121
|
+
* Query the corpus and return records whose solverType / role /
|
|
122
|
+
* artifactType / ref / task cid contains `query` (case-insensitive).
|
|
123
|
+
* Empty query returns everything fetched (up to `limit`).
|
|
124
|
+
*
|
|
125
|
+
* By default the result is collapsed to lineage heads: a skill record
|
|
126
|
+
* superseded (or deprecated) by a same-operator record in the fetched page
|
|
127
|
+
* is dropped (#1462). Pass `includeSuperseded: true` to disable the
|
|
128
|
+
* collapse and see every record.
|
|
129
|
+
*/
|
|
130
|
+
search(query: string, opts?: {
|
|
131
|
+
limit?: number;
|
|
132
|
+
kind?: 'skill' | 'trace';
|
|
133
|
+
includeSuperseded?: boolean;
|
|
134
|
+
}): Promise<CorpusSearchHit[]>;
|
|
135
|
+
/** Fetch a record by ref (manifest CID from a search hit), including artifact bytes. */
|
|
136
|
+
get(ref: string): Promise<CorpusRecord>;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export declare function createHarnessLayer(config?: HarnessLayerConfig): HarnessLayer;
|