@jinn-network/plugin 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 (56) hide show
  1. package/dist/eligibility.d.ts +12 -0
  2. package/dist/eligibility.js +12 -0
  3. package/dist/history.d.ts +35 -0
  4. package/dist/history.js +140 -0
  5. package/dist/index.d.ts +32 -0
  6. package/dist/index.js +15 -0
  7. package/dist/outcome.d.ts +26 -0
  8. package/dist/outcome.js +23 -0
  9. package/dist/pickup.d.ts +101 -0
  10. package/dist/pickup.js +354 -0
  11. package/dist/plugin.d.ts +157 -0
  12. package/dist/plugin.js +586 -0
  13. package/dist/ports/contribution-port.d.ts +53 -0
  14. package/dist/ports/contribution-port.js +14 -0
  15. package/dist/ports/corpus-port.d.ts +63 -0
  16. package/dist/ports/corpus-port.js +1 -0
  17. package/dist/ports/evidence-port.d.ts +17 -0
  18. package/dist/ports/evidence-port.js +1 -0
  19. package/dist/ports/local-learning-port.d.ts +22 -0
  20. package/dist/ports/local-learning-port.js +1 -0
  21. package/dist/ports/skills-port.d.ts +12 -0
  22. package/dist/ports/skills-port.js +1 -0
  23. package/dist/schemas/contribution-candidate.d.ts +116 -0
  24. package/dist/schemas/contribution-candidate.js +63 -0
  25. package/dist/schemas/eligibility-verdict.d.ts +8 -0
  26. package/dist/schemas/eligibility-verdict.js +7 -0
  27. package/dist/schemas/episode.d.ts +432 -0
  28. package/dist/schemas/episode.js +452 -0
  29. package/dist/schemas/history-entry.d.ts +40 -0
  30. package/dist/schemas/history-entry.js +30 -0
  31. package/dist/schemas/knowledge-hit.d.ts +27 -0
  32. package/dist/schemas/knowledge-hit.js +23 -0
  33. package/dist/schemas/knowledge-packet.d.ts +79 -0
  34. package/dist/schemas/knowledge-packet.js +213 -0
  35. package/dist/schemas/pickup-config.d.ts +20 -0
  36. package/dist/schemas/pickup-config.js +34 -0
  37. package/dist/schemas/session-summary.d.ts +17 -0
  38. package/dist/schemas/session-summary.js +19 -0
  39. package/dist/testing/contract-kits.d.ts +12 -0
  40. package/dist/testing/contract-kits.js +201 -0
  41. package/dist/testing/in-memory-contribution.d.ts +93 -0
  42. package/dist/testing/in-memory-contribution.js +104 -0
  43. package/dist/testing/in-memory-corpus.d.ts +47 -0
  44. package/dist/testing/in-memory-corpus.js +54 -0
  45. package/dist/testing/in-memory-evidence.d.ts +335 -0
  46. package/dist/testing/in-memory-evidence.js +23 -0
  47. package/dist/testing/in-memory-local-learning.d.ts +25 -0
  48. package/dist/testing/in-memory-local-learning.js +30 -0
  49. package/dist/testing/in-memory-skills.d.ts +9 -0
  50. package/dist/testing/in-memory-skills.js +16 -0
  51. package/dist/testing.d.ts +6 -0
  52. package/dist/testing.js +8 -0
  53. package/dist/visibility.d.ts +7 -0
  54. package/dist/visibility.js +9 -0
  55. package/package.json +48 -0
  56. package/process-contract.json +8 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `jinn.knowledge-packet.v1` — the unit of evidence supplied to the agent
3
+ * (rescope design §3.2). A versioned, pure, deterministic projection of a
4
+ * `CorpusRecord`: presentation-only, re-derivable at any time, never stored
5
+ * as new truth.
6
+ */
7
+ import { z } from 'zod';
8
+ import { TIER_ORDER } from './pickup-config.js';
9
+ export const KNOWLEDGE_PACKET_SCHEMA_VERSION = 'jinn.knowledge-packet.v1';
10
+ export const KnowledgePacketExcerptSchema = z.strictObject({
11
+ label: z.enum(['failure', 'fix', 'command', 'diff', 'note']),
12
+ text: z.string().min(1),
13
+ });
14
+ export const KnowledgePacketSchema = z.strictObject({
15
+ schemaVersion: z.literal(KNOWLEDGE_PACKET_SCHEMA_VERSION),
16
+ ref: z.string().min(1),
17
+ task: z.strictObject({
18
+ summary: z.string().min(1),
19
+ repositorySlug: z.string().min(1).optional(),
20
+ }),
21
+ outcome: z.strictObject({
22
+ status: z.enum(['completed', 'failed', 'abandoned']),
23
+ verifiabilityTier: z.enum(TIER_ORDER),
24
+ }),
25
+ /** The record's own synthesis, when the record carries one. Never generated here. */
26
+ synthesis: z.string().min(1).optional(),
27
+ excerpts: z.array(KnowledgePacketExcerptSchema),
28
+ attribution: z.strictObject({
29
+ provenance: z.enum(['imported', 'contributed', 'derived-from-history']),
30
+ capturedAt: z.iso.datetime(),
31
+ origin: z.string().min(1),
32
+ }),
33
+ });
34
+ export const DEFAULT_PACKET_CHAR_BUDGET = 3_500;
35
+ /**
36
+ * Read a well-known string-shaped step attribute. Steps follow the existing
37
+ * capture convention seen throughout this codebase's episode/trace fixtures:
38
+ * `tool.args` (command invoked) / `tool.result` (output) / `tool.exitCode`
39
+ * (numeric exit status) / `diff` (a unified-diff excerpt) / `note` or
40
+ * `turn.text` (free-form remark, mirroring the episode `jinn.agent_turn`
41
+ * attribute convention).
42
+ */
43
+ function attrString(attributes, key) {
44
+ const value = attributes[key];
45
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
46
+ }
47
+ function stepCommand(step) {
48
+ return attrString(step.attributes, 'tool.args') ?? attrString(step.attributes, 'command');
49
+ }
50
+ function stepOutput(step) {
51
+ return attrString(step.attributes, 'tool.result') ?? attrString(step.attributes, 'result');
52
+ }
53
+ function stepExitCode(step) {
54
+ const value = step.attributes['tool.exitCode'] ?? step.attributes['exitCode'];
55
+ return typeof value === 'number' ? value : undefined;
56
+ }
57
+ function stepDiff(step) {
58
+ return attrString(step.attributes, 'diff');
59
+ }
60
+ function stepNote(step) {
61
+ return attrString(step.attributes, 'note') ?? attrString(step.attributes, 'turn.text');
62
+ }
63
+ const EXCERPT_LABELS = new Set([
64
+ 'failure', 'fix', 'command', 'diff', 'note',
65
+ ]);
66
+ /**
67
+ * Seed-authored evidence (`packages/layer/src/seed-import/
68
+ * episode-execute.ts`) carries its own excerpt directly on the step —
69
+ * `seed.step.label` (one of this schema's five excerpt labels) and
70
+ * `seed.step.text` (the excerpt content, already selected by the seed
71
+ * author offline) — rather than the `tool.args`/`tool.exitCode` real-capture
72
+ * convention `stepCommand`/`stepExitCode` read. Trust it verbatim: the
73
+ * heuristic below exists to *infer* an excerpt from a raw trajectory; a seed
74
+ * step has already done that inference by hand, so re-deriving it from a
75
+ * synthetic exit code would only risk corrupting what the seed author wrote.
76
+ */
77
+ function seedStepExcerpt(step) {
78
+ const label = step.attributes['seed.step.label'];
79
+ const text = attrString(step.attributes, 'seed.step.text');
80
+ if (typeof label !== 'string' || text === undefined)
81
+ return undefined;
82
+ return EXCERPT_LABELS.has(label)
83
+ ? { label: label, text }
84
+ : undefined;
85
+ }
86
+ /**
87
+ * Deterministic excerpt selection over a record's steps, in step order.
88
+ * Seed-authored steps (see `seedStepExcerpt`) are trusted verbatim, in step
89
+ * order, when present. Otherwise: the first failing command with its
90
+ * output, the next command after it that is not itself a failure (the
91
+ * correction), the last passing command overall (the final backstop), and a
92
+ * diff step when present. Falls back to a single free-form note when
93
+ * nothing command-shaped is found. Selects only — never paraphrases.
94
+ */
95
+ function selectExcerpts(steps) {
96
+ const seedExcerpts = steps
97
+ .map(seedStepExcerpt)
98
+ .filter((candidate) => candidate !== undefined);
99
+ if (seedExcerpts.length > 0)
100
+ return seedExcerpts;
101
+ const excerpts = [];
102
+ let failureIndex = -1;
103
+ for (let i = 0; i < steps.length; i++) {
104
+ const step = steps[i];
105
+ const command = stepCommand(step);
106
+ const exitCode = stepExitCode(step);
107
+ if (command !== undefined && exitCode !== undefined && exitCode !== 0) {
108
+ const output = stepOutput(step);
109
+ excerpts.push({ label: 'failure', text: output ? `${command}\n${output}` : command });
110
+ failureIndex = i;
111
+ break;
112
+ }
113
+ }
114
+ if (failureIndex >= 0) {
115
+ for (let i = failureIndex + 1; i < steps.length; i++) {
116
+ const step = steps[i];
117
+ const command = stepCommand(step);
118
+ const exitCode = stepExitCode(step);
119
+ if (command !== undefined && exitCode !== undefined && exitCode === 0) {
120
+ excerpts.push({ label: 'fix', text: command });
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ let lastPassing;
126
+ for (const step of steps) {
127
+ const command = stepCommand(step);
128
+ const exitCode = stepExitCode(step);
129
+ if (command !== undefined && exitCode === 0)
130
+ lastPassing = command;
131
+ }
132
+ if (lastPassing !== undefined)
133
+ excerpts.push({ label: 'command', text: lastPassing });
134
+ const diffStep = steps.find((step) => stepDiff(step) !== undefined);
135
+ if (diffStep !== undefined)
136
+ excerpts.push({ label: 'diff', text: stepDiff(diffStep) });
137
+ if (excerpts.length === 0) {
138
+ const noteStep = steps.find((step) => stepNote(step) !== undefined);
139
+ if (noteStep !== undefined)
140
+ excerpts.push({ label: 'note', text: stepNote(noteStep) });
141
+ }
142
+ return excerpts;
143
+ }
144
+ /**
145
+ * Line-boundary-aware truncation ending with an explicit, pointer-bearing
146
+ * tail. Returns an empty string when the budget cannot fit both meaningful
147
+ * source text and the complete tail.
148
+ */
149
+ export function truncateLineBoundary(text, maxChars, ref) {
150
+ if (text.length <= maxChars)
151
+ return text;
152
+ const tail = `\n[truncated — full episode: corpus_fetch ${ref}]`;
153
+ const budget = maxChars - tail.length;
154
+ if (budget <= 0)
155
+ return '';
156
+ let cut = text.slice(0, budget);
157
+ const lastNewline = cut.lastIndexOf('\n');
158
+ if (lastNewline > 0)
159
+ cut = cut.slice(0, lastNewline);
160
+ if (cut.trim().length === 0)
161
+ return '';
162
+ return `${cut}${tail}`;
163
+ }
164
+ /**
165
+ * Pure deterministic projection of a `CorpusRecord` into a `KnowledgePacket`
166
+ * (rescope §3.2). Selects and truncates; never paraphrases. Enforces the
167
+ * combined synthesis+excerpts char budget (default 3,500 — two packets at the
168
+ * default budget total 7,000, satisfying §3.5's combined ceiling).
169
+ */
170
+ export function projectKnowledgePacket(record, budget = {}) {
171
+ const maxChars = budget.maxChars ?? DEFAULT_PACKET_CHAR_BUDGET;
172
+ let synthesis = record.synthesis;
173
+ if (synthesis !== undefined && synthesis.length > maxChars) {
174
+ synthesis = truncateLineBoundary(synthesis, maxChars, record.ref);
175
+ }
176
+ const candidates = selectExcerpts(record.steps);
177
+ const excerpts = [];
178
+ let used = synthesis?.length ?? 0;
179
+ for (const candidate of candidates) {
180
+ const remaining = maxChars - used;
181
+ if (remaining <= 0)
182
+ break;
183
+ if (candidate.text.length <= remaining) {
184
+ excerpts.push(candidate);
185
+ used += candidate.text.length;
186
+ }
187
+ else {
188
+ const text = truncateLineBoundary(candidate.text, remaining, record.ref);
189
+ if (text.length > 0)
190
+ excerpts.push({ label: candidate.label, text });
191
+ break;
192
+ }
193
+ }
194
+ return KnowledgePacketSchema.parse({
195
+ schemaVersion: KNOWLEDGE_PACKET_SCHEMA_VERSION,
196
+ ref: record.ref,
197
+ task: {
198
+ summary: record.task.summary,
199
+ ...(record.task.repositorySlug ? { repositorySlug: record.task.repositorySlug } : {}),
200
+ },
201
+ outcome: {
202
+ status: record.outcome.status,
203
+ verifiabilityTier: record.outcome.verifiabilityTier,
204
+ },
205
+ ...(synthesis ? { synthesis } : {}),
206
+ excerpts,
207
+ attribution: {
208
+ provenance: record.provenance,
209
+ capturedAt: record.capturedAt,
210
+ origin: record.origin,
211
+ },
212
+ });
213
+ }
@@ -0,0 +1,20 @@
1
+ /** Pickup policy config — ported from pickup.py DEFAULT_CONFIG. Injected as a
2
+ * typed object; no pickup.json fs read in core (architecture §6). */
3
+ import { z } from 'zod';
4
+ export declare const TIER_ORDER: readonly ["user-accepted", "tests-passed", "evaluator-verified"];
5
+ export type Tier = (typeof TIER_ORDER)[number];
6
+ export declare const PickupConfigSchema: z.ZodObject<{
7
+ enabled: z.ZodDefault<z.ZodBoolean>;
8
+ autoAdopt: z.ZodDefault<z.ZodBoolean>;
9
+ autoAdoptTier: z.ZodDefault<z.ZodEnum<{
10
+ "user-accepted": "user-accepted";
11
+ "tests-passed": "tests-passed";
12
+ "evaluator-verified": "evaluator-verified";
13
+ }>>;
14
+ maxCandidates: z.ZodDefault<z.ZodNumber>;
15
+ }, z.core.$strict>;
16
+ export type PickupConfig = z.infer<typeof PickupConfigSchema>;
17
+ export declare const DEFAULT_PICKUP_CONFIG: PickupConfig;
18
+ /** Merge partial input over defaults; a bad autoAdoptTier coerces to default
19
+ * (mirrors load_config()'s TIER_ORDER guard). Never throws — bad input → defaults. */
20
+ export declare function parsePickupConfig(input?: unknown): PickupConfig;
@@ -0,0 +1,34 @@
1
+ /** Pickup policy config — ported from pickup.py DEFAULT_CONFIG. Injected as a
2
+ * typed object; no pickup.json fs read in core (architecture §6). */
3
+ import { z } from 'zod';
4
+ // Weakest → strongest; mirrors VERIFIABILITY_TIERS in the frozen envelope schema.
5
+ export const TIER_ORDER = ['user-accepted', 'tests-passed', 'evaluator-verified'];
6
+ export const PickupConfigSchema = z.strictObject({
7
+ enabled: z.boolean().default(true),
8
+ /** @deprecated Legacy host compatibility only — a pre-rescope `pickup.json`
9
+ * may still send this; evidence-first pickup (rescope §3.3) never reads
10
+ * it. Auto-adopt and skill classification were removed from the pickup
11
+ * path entirely by R1/R3 (skills are excluded from selection outright,
12
+ * not gated by a threshold), so there is no adopt decision left for a
13
+ * tier to threshold. */
14
+ autoAdopt: z.boolean().default(false),
15
+ /** @deprecated Legacy host compatibility only — same as `autoAdopt`. */
16
+ autoAdoptTier: z.enum(TIER_ORDER).default('evaluator-verified'),
17
+ /** @deprecated Legacy host compatibility only — selection has a fixed cap
18
+ * (`MAX_SELECTED_PACKETS` in `pickup.ts`), not an operator-configurable one. */
19
+ maxCandidates: z.number().int().positive().default(3),
20
+ });
21
+ export const DEFAULT_PICKUP_CONFIG = PickupConfigSchema.parse({});
22
+ /** Merge partial input over defaults; a bad autoAdoptTier coerces to default
23
+ * (mirrors load_config()'s TIER_ORDER guard). Never throws — bad input → defaults. */
24
+ export function parsePickupConfig(input) {
25
+ if (input == null || typeof input !== 'object')
26
+ return { ...DEFAULT_PICKUP_CONFIG };
27
+ const raw = input;
28
+ const merged = { ...raw };
29
+ if (!TIER_ORDER.includes(String(raw.autoAdoptTier))) {
30
+ delete merged.autoAdoptTier; // fall back to schema default
31
+ }
32
+ const result = PickupConfigSchema.safeParse(merged);
33
+ return result.success ? result.data : { ...DEFAULT_PICKUP_CONFIG };
34
+ }
@@ -0,0 +1,17 @@
1
+ /** Assembled at session end() (product design §4.2 legibility requirement — "when Jinn found nothing, it says so"). */
2
+ import { z } from 'zod';
3
+ export declare const SessionSummarySchema: z.ZodObject<{
4
+ episodeRef: z.ZodString;
5
+ searchedTerms: z.ZodDefault<z.ZodArray<z.ZodString>>;
6
+ providedPackets: z.ZodDefault<z.ZodArray<z.ZodObject<{
7
+ ref: z.ZodString;
8
+ title: z.ZodString;
9
+ }, z.core.$strict>>>;
10
+ eligibility: z.ZodObject<{
11
+ eligible: z.ZodBoolean;
12
+ reason: z.ZodString;
13
+ checkedAt: z.ZodISODateTime;
14
+ }, z.core.$strict>;
15
+ nothingFound: z.ZodBoolean;
16
+ }, z.core.$strict>;
17
+ export type SessionSummary = z.infer<typeof SessionSummarySchema>;
@@ -0,0 +1,19 @@
1
+ /** Assembled at session end() (product design §4.2 legibility requirement — "when Jinn found nothing, it says so"). */
2
+ import { z } from 'zod';
3
+ import { EligibilityVerdictSchema } from './eligibility-verdict.js';
4
+ export const SessionSummarySchema = z.strictObject({
5
+ episodeRef: z.string().min(1),
6
+ /** Evidence-first pickup facts (rescope §3.6) — the only knowledge-activity
7
+ * shape a session summary carries. The pre-rescope `surfacedRefs`/
8
+ * `fetchedRefs`/`surfacedHits`/`fetchedHits`/`installedSkillRefs` quintet
9
+ * is dropped as of R3+R5 (the host and acceptance gate that consumed it
10
+ * have flipped); `Episode.activity` still accepts those fields on read for
11
+ * old episode files (rescope §3.6), but no summary emits them again. */
12
+ searchedTerms: z.array(z.string().min(1)).default([]),
13
+ providedPackets: z.array(z.strictObject({
14
+ ref: z.string().min(1),
15
+ title: z.string().min(1),
16
+ })).default([]),
17
+ eligibility: EligibilityVerdictSchema,
18
+ nothingFound: z.boolean(),
19
+ });
@@ -0,0 +1,12 @@
1
+ import type { ContributionPort } from '../ports/contribution-port.js';
2
+ import type { CorpusPort } from '../ports/corpus-port.js';
3
+ import type { EvidencePort } from '../ports/evidence-port.js';
4
+ import type { LocalLearningPort } from '../ports/local-learning-port.js';
5
+ import type { SkillsPort } from '../ports/skills-port.js';
6
+ import type { EpisodeV1 } from '../schemas/episode.js';
7
+ import type { ContributionCandidateV1 } from '../schemas/contribution-candidate.js';
8
+ export declare function describeCorpusPortContract(makeAdapter: () => CorpusPort): void;
9
+ export declare function describeEvidencePortContract(makeAdapter: () => EvidencePort, sampleEpisode: EpisodeV1): void;
10
+ export declare function describeContributionPortContract(makeAdapter: (candidate?: ContributionCandidateV1) => ContributionPort): void;
11
+ export declare function describeLocalLearningPortContract(makeAdapter: () => LocalLearningPort): void;
12
+ export declare function describeSkillsPortContract(makeAdapter: () => SkillsPort): void;
@@ -0,0 +1,201 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ function contributionCandidate(overrides = {}) {
3
+ return {
4
+ schemaVersion: 'jinn.contribution-candidate.v1',
5
+ sourceId: 'episode-1',
6
+ repositorySlug: 'Jinn-Network/mono',
7
+ baseCommit: '0123456789abcdef',
8
+ acceptedDiff: 'diff --git a/a.ts b/a.ts\n+fixed\n',
9
+ testRuns: [],
10
+ intermediateFailureDiffs: [],
11
+ skillEvents: [],
12
+ publishMinedTasksConsent: false,
13
+ createdAt: '2026-07-15T12:00:00.000Z',
14
+ ...overrides,
15
+ };
16
+ }
17
+ export function describeCorpusPortContract(makeAdapter) {
18
+ describe('CorpusPort contract', () => {
19
+ it('search() returns an ok PortResult with an array value', async () => {
20
+ const adapter = makeAdapter();
21
+ const result = await adapter.search('anything');
22
+ expect(result.status).toBe('ok');
23
+ if (result.status === 'ok')
24
+ expect(Array.isArray(result.value)).toBe(true);
25
+ });
26
+ it('get() on an unknown ref returns ok(null)', async () => {
27
+ const adapter = makeAdapter();
28
+ const result = await adapter.get('does-not-exist');
29
+ expect(result).toEqual({ status: 'ok', value: null });
30
+ });
31
+ });
32
+ }
33
+ export function describeEvidencePortContract(makeAdapter, sampleEpisode) {
34
+ describe('EvidencePort contract', () => {
35
+ it('put() then get() round-trips the episode', async () => {
36
+ const adapter = makeAdapter();
37
+ const putResult = await adapter.put(sampleEpisode);
38
+ expect(putResult.status).toBe('ok');
39
+ const getResult = await adapter.get(sampleEpisode.episodeId);
40
+ expect(getResult).toEqual({ status: 'ok', value: sampleEpisode });
41
+ });
42
+ it('list() includes a put episode', async () => {
43
+ const adapter = makeAdapter();
44
+ await adapter.put(sampleEpisode);
45
+ const listResult = await adapter.list();
46
+ expect(listResult.status).toBe('ok');
47
+ if (listResult.status === 'ok') {
48
+ expect(listResult.value.map((e) => e.episodeId)).toContain(sampleEpisode.episodeId);
49
+ }
50
+ });
51
+ it('retention() returns a policy result', async () => {
52
+ const adapter = makeAdapter();
53
+ const result = await adapter.retention();
54
+ expect(result.status).toBe('ok');
55
+ if (result.status === 'ok') {
56
+ expect(['local-private', 'contribution-eligible']).toContain(result.value.policy);
57
+ }
58
+ });
59
+ });
60
+ }
61
+ export function describeContributionPortContract(makeAdapter) {
62
+ describe('ContributionPort contract', () => {
63
+ it('records a candidate locally even when publication consent is disabled', async () => {
64
+ const input = contributionCandidate();
65
+ const adapter = makeAdapter(input);
66
+ const recordResult = await adapter.recordMineable(input);
67
+ expect(recordResult.status).toBe('ok');
68
+ if (recordResult.status !== 'ok')
69
+ return;
70
+ const statusResult = await adapter.mintStatus(recordResult.value.recordId);
71
+ expect(statusResult).toEqual({
72
+ status: 'ok',
73
+ value: {
74
+ localState: 'recorded',
75
+ publicationState: 'disabled',
76
+ status: 'recorded',
77
+ },
78
+ });
79
+ const ledgerResult = await adapter.ledger();
80
+ expect(ledgerResult.status).toBe('ok');
81
+ if (ledgerResult.status === 'ok') {
82
+ expect(ledgerResult.value).toContainEqual({
83
+ recordId: recordResult.value.recordId,
84
+ sourceId: 'episode-1',
85
+ createdAt: '2026-07-15T12:00:00.000Z',
86
+ verifiabilityTier: 'user-accepted',
87
+ repositorySlug: 'Jinn-Network/mono',
88
+ baseCommit: '0123456789abcdef',
89
+ localState: 'recorded',
90
+ publicationState: 'disabled',
91
+ status: 'recorded',
92
+ });
93
+ }
94
+ });
95
+ it('requires preview authorization before a consented candidate is queued', async () => {
96
+ const input = contributionCandidate({
97
+ publishMinedTasksConsent: true,
98
+ });
99
+ const adapter = makeAdapter(input);
100
+ const recordResult = await adapter.recordMineable(input);
101
+ if (recordResult.status !== 'ok')
102
+ throw new Error('recordMineable failed');
103
+ expect(await adapter.mintStatus(recordResult.value.recordId)).toEqual({
104
+ status: 'ok',
105
+ value: {
106
+ localState: 'recorded',
107
+ publicationState: 'preview-required',
108
+ status: 'preview-required',
109
+ },
110
+ });
111
+ const authorizeResult = await adapter.authorize(recordResult.value.recordId);
112
+ expect(authorizeResult).toEqual({
113
+ status: 'ok',
114
+ value: { recordId: recordResult.value.recordId, publicationState: 'queued', status: 'queued' },
115
+ });
116
+ });
117
+ it('veto() changes only publication state and preserves the local record', async () => {
118
+ const input = contributionCandidate({
119
+ publishMinedTasksConsent: true,
120
+ });
121
+ const adapter = makeAdapter(input);
122
+ const recordResult = await adapter.recordMineable(input);
123
+ if (recordResult.status !== 'ok')
124
+ throw new Error('recordMineable failed');
125
+ const vetoResult = await adapter.veto(recordResult.value.recordId);
126
+ expect(vetoResult).toEqual({
127
+ status: 'ok',
128
+ value: { recordId: recordResult.value.recordId, publicationState: 'vetoed', status: 'vetoed' },
129
+ });
130
+ const statusResult = await adapter.mintStatus(recordResult.value.recordId);
131
+ expect(statusResult).toEqual({
132
+ status: 'ok',
133
+ value: { localState: 'recorded', publicationState: 'vetoed', status: 'vetoed' },
134
+ });
135
+ });
136
+ it('ledger() lists recorded entries', async () => {
137
+ const input = contributionCandidate();
138
+ const adapter = makeAdapter(input);
139
+ await adapter.recordMineable(input);
140
+ const ledgerResult = await adapter.ledger();
141
+ expect(ledgerResult.status).toBe('ok');
142
+ if (ledgerResult.status === 'ok')
143
+ expect(ledgerResult.value.length).toBeGreaterThan(0);
144
+ });
145
+ });
146
+ }
147
+ export function describeLocalLearningPortContract(makeAdapter) {
148
+ describe('LocalLearningPort contract', () => {
149
+ it('run() then status() reports the run', async () => {
150
+ const adapter = makeAdapter();
151
+ const runResult = await adapter.run({ episodeIds: ['episode-1'] });
152
+ expect(runResult.status).toBe('ok');
153
+ if (runResult.status !== 'ok')
154
+ return;
155
+ const statusResult = await adapter.status(runResult.value.runId);
156
+ expect(statusResult.status).toBe('ok');
157
+ });
158
+ it('list() includes a started run', async () => {
159
+ const adapter = makeAdapter();
160
+ const runResult = await adapter.run({ episodeIds: [] });
161
+ if (runResult.status !== 'ok')
162
+ throw new Error('run failed');
163
+ const listResult = await adapter.list();
164
+ expect(listResult.status).toBe('ok');
165
+ if (listResult.status === 'ok') {
166
+ expect(listResult.value.map((r) => r.runId)).toContain(runResult.value.runId);
167
+ }
168
+ });
169
+ it('skills() is typed when the adapter exposes persisted skill provenance', async () => {
170
+ const adapter = makeAdapter();
171
+ if (!adapter.skills)
172
+ return;
173
+ const skills = await adapter.skills();
174
+ expect(skills.status).toBe('ok');
175
+ });
176
+ });
177
+ }
178
+ export function describeSkillsPortContract(makeAdapter) {
179
+ describe('SkillsPort contract', () => {
180
+ it('install() then list() includes the skill', async () => {
181
+ const adapter = makeAdapter();
182
+ const installResult = await adapter.install('org/skill@1.0.0');
183
+ expect(installResult.status).toBe('ok');
184
+ const listResult = await adapter.list();
185
+ expect(listResult.status).toBe('ok');
186
+ if (listResult.status === 'ok') {
187
+ expect(listResult.value.map((s) => s.ref)).toContain('org/skill@1.0.0');
188
+ }
189
+ });
190
+ it('uninstall() removes the skill', async () => {
191
+ const adapter = makeAdapter();
192
+ await adapter.install('org/skill@1.0.0');
193
+ await adapter.uninstall('org/skill@1.0.0');
194
+ const listResult = await adapter.list();
195
+ expect(listResult.status).toBe('ok');
196
+ if (listResult.status === 'ok') {
197
+ expect(listResult.value.map((s) => s.ref)).not.toContain('org/skill@1.0.0');
198
+ }
199
+ });
200
+ });
201
+ }
@@ -0,0 +1,93 @@
1
+ import type { ContributionPort, ContributionRecordOptions, ContributionStatusSnapshot } from '../ports/contribution-port.js';
2
+ import { type ContributionCandidateV1 } from '../schemas/contribution-candidate.js';
3
+ export declare class InMemoryContributionPort implements ContributionPort {
4
+ private readonly records;
5
+ private counter;
6
+ recordMineable(candidate: ContributionCandidateV1, options?: ContributionRecordOptions): Promise<{
7
+ status: "unavailable";
8
+ reason: string;
9
+ } | {
10
+ status: "ok";
11
+ value: {
12
+ recordId: string;
13
+ };
14
+ } | {
15
+ status: "degraded";
16
+ reason: string;
17
+ value?: {
18
+ recordId: string;
19
+ } | undefined;
20
+ }>;
21
+ ledger(): Promise<import("../outcome.js").PortResult<{
22
+ status: import("../index.js").ContributionStatus;
23
+ localState: import("../index.js").ContributionLocalState;
24
+ publicationState: import("../index.js").ContributionPublicationState;
25
+ mintRef?: string;
26
+ publicationRef?: string;
27
+ recordId: string;
28
+ sourceId: string;
29
+ createdAt: string;
30
+ verifiabilityTier: "user-accepted" | "tests-passed";
31
+ repositorySlug: string;
32
+ baseCommit: string;
33
+ }[]>>;
34
+ mintStatus(recordId: string): Promise<{
35
+ status: "unavailable";
36
+ reason: string;
37
+ } | {
38
+ status: "ok";
39
+ value: ContributionStatusSnapshot;
40
+ } | {
41
+ status: "degraded";
42
+ reason: string;
43
+ value?: ContributionStatusSnapshot | undefined;
44
+ }>;
45
+ authorize(recordId: string): Promise<{
46
+ status: "unavailable";
47
+ reason: string;
48
+ } | {
49
+ status: "ok";
50
+ value: {
51
+ recordId: string;
52
+ publicationState: "queued";
53
+ status: "queued";
54
+ };
55
+ } | {
56
+ status: "degraded";
57
+ reason: string;
58
+ value?: {
59
+ recordId: string;
60
+ publicationState: "queued";
61
+ status: "queued";
62
+ } | undefined;
63
+ }>;
64
+ veto(recordId: string): Promise<{
65
+ status: "unavailable";
66
+ reason: string;
67
+ } | {
68
+ status: "ok";
69
+ value: {
70
+ recordId: string;
71
+ publicationState: "vetoed";
72
+ status: "vetoed";
73
+ };
74
+ } | {
75
+ status: "degraded";
76
+ reason: string;
77
+ value?: {
78
+ recordId: string;
79
+ publicationState: "vetoed";
80
+ status: "vetoed";
81
+ } | undefined;
82
+ }>;
83
+ disableUnpublished(): Promise<import("../outcome.js").PortResult<{
84
+ recordIds: string[];
85
+ }>>;
86
+ /** Testing control for the sidecar-owned forward state. */
87
+ markMinted(recordId: string, mintRef: string): void;
88
+ /** Testing control for the sidecar-owned terminal local state. */
89
+ markRejected(recordId: string): void;
90
+ /** Testing control for the sidecar-owned outbound state. */
91
+ markPublished(recordId: string, publicationRef: string): void;
92
+ getCandidate(recordId: string): ContributionCandidateV1 | undefined;
93
+ }