@prodero/curator 0.0.0-stage → 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 (137) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +94 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +228 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +50 -0
  11. package/dist/conversation-presentation.js +262 -0
  12. package/dist/core/agent-instructions.js +39 -0
  13. package/dist/core/applicability-label.js +11 -0
  14. package/dist/core/applicability.js +143 -0
  15. package/dist/core/bootstrap.js +129 -0
  16. package/dist/core/consolidation.js +315 -0
  17. package/dist/core/constants.js +21 -0
  18. package/dist/core/context-allocation.js +54 -0
  19. package/dist/core/context-snapshot.js +12 -0
  20. package/dist/core/context.js +489 -0
  21. package/dist/core/curation-clauses.js +68 -0
  22. package/dist/core/curation-evidence.js +98 -0
  23. package/dist/core/curation-graphical.js +105 -0
  24. package/dist/core/curation-review-presentation.js +4 -0
  25. package/dist/core/curation-review-state.js +62 -0
  26. package/dist/core/curation-review.js +573 -0
  27. package/dist/core/curation-revision.js +106 -0
  28. package/dist/core/curation-semantic-contract.js +239 -0
  29. package/dist/core/curation-semantic-jobs.js +280 -0
  30. package/dist/core/diagnostics.js +17 -0
  31. package/dist/core/format.js +114 -0
  32. package/dist/core/ids.js +6 -0
  33. package/dist/core/imports.js +169 -0
  34. package/dist/core/index.js +93 -0
  35. package/dist/core/instruction-review.js +86 -0
  36. package/dist/core/integrity-files.js +174 -0
  37. package/dist/core/intelligence.js +95 -0
  38. package/dist/core/knowledge-analysis.js +46 -0
  39. package/dist/core/knowledge-authority.js +143 -0
  40. package/dist/core/knowledge-curation.js +316 -0
  41. package/dist/core/knowledge-engine.js +475 -0
  42. package/dist/core/knowledge-graph.js +191 -0
  43. package/dist/core/knowledge-lifecycle.js +212 -0
  44. package/dist/core/knowledge-management.js +113 -0
  45. package/dist/core/knowledge-migration.js +84 -0
  46. package/dist/core/knowledge-policy.js +38 -0
  47. package/dist/core/knowledge-provenance.js +90 -0
  48. package/dist/core/knowledge-write-policy.js +85 -0
  49. package/dist/core/language.js +14 -0
  50. package/dist/core/matching.js +6 -0
  51. package/dist/core/memory-index.js +239 -0
  52. package/dist/core/memory-intelligence.js +288 -0
  53. package/dist/core/memory-link-repair.js +66 -0
  54. package/dist/core/memory-operations.js +122 -0
  55. package/dist/core/memory-pressure.js +212 -0
  56. package/dist/core/memory-sync-curator.js +62 -0
  57. package/dist/core/memory-sync.js +463 -0
  58. package/dist/core/memory-transformation-bundle.js +163 -0
  59. package/dist/core/memory-transformation-proposal.js +135 -0
  60. package/dist/core/memory-transformation.js +108 -0
  61. package/dist/core/memory.js +186 -0
  62. package/dist/core/migrations.js +569 -0
  63. package/dist/core/path-scope.js +27 -0
  64. package/dist/core/plugin-files.js +116 -0
  65. package/dist/core/project-root.js +39 -0
  66. package/dist/core/project.js +634 -0
  67. package/dist/core/promotion.js +279 -0
  68. package/dist/core/reconciliation.js +61 -0
  69. package/dist/core/secrets.js +67 -0
  70. package/dist/core/semantic-safety.js +33 -0
  71. package/dist/core/session-index.js +112 -0
  72. package/dist/core/session-review.js +150 -0
  73. package/dist/core/sessions.js +289 -0
  74. package/dist/core/settings.js +76 -0
  75. package/dist/core/skill-curation.js +239 -0
  76. package/dist/core/skill-polish.js +41 -0
  77. package/dist/core/skills.js +368 -0
  78. package/dist/core/storage.js +408 -0
  79. package/dist/core/sync-transaction.js +254 -0
  80. package/dist/core/telemetry.js +64 -0
  81. package/dist/core/templates.js +53 -0
  82. package/dist/core/types.js +2 -0
  83. package/dist/core/update.js +320 -0
  84. package/dist/curation-review-adapter.js +92 -0
  85. package/dist/diagnostic-boundary.js +54 -0
  86. package/dist/filesystem/permissions.js +39 -0
  87. package/dist/hosts/adapters.js +119 -0
  88. package/dist/hosts/files.js +133 -0
  89. package/dist/hosts/manager.js +150 -0
  90. package/dist/hosts/public-entry.js +47 -0
  91. package/dist/hosts/runtime-command.js +66 -0
  92. package/dist/hosts/runtime.js +139 -0
  93. package/dist/hosts/wizard.js +114 -0
  94. package/dist/knowledge-table.js +38 -0
  95. package/dist/mcp-development.js +69 -0
  96. package/dist/mcp-workspace.js +141 -0
  97. package/dist/memory-presentation.js +172 -0
  98. package/dist/plugins/api.js +2 -0
  99. package/dist/plugins/context.js +20 -0
  100. package/dist/plugins/mcp.js +72 -0
  101. package/dist/plugins/registry.js +106 -0
  102. package/dist/presentation-cards.js +32 -0
  103. package/dist/presentation-server.js +79 -0
  104. package/dist/presentation-trace.js +174 -0
  105. package/dist/presentation.js +412 -0
  106. package/dist/providers/mcp-sampling.js +45 -0
  107. package/dist/server.js +656 -0
  108. package/dist/settings-definition.js +22 -0
  109. package/dist/skill-instructions.js +2 -0
  110. package/dist-ui/index.html +214 -0
  111. package/docs/APPLICABILITY.md +151 -0
  112. package/docs/COMPATIBILITY.md +49 -0
  113. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  114. package/docs/CURATION-APP-REVIEW.md +156 -0
  115. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  116. package/docs/IMPORTER-CURATION.md +142 -0
  117. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  118. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  119. package/docs/MIGRATIONS.md +99 -0
  120. package/docs/PRIVACY.md +28 -0
  121. package/docs/RECOVERY.md +56 -0
  122. package/docs/RELEASING.md +40 -0
  123. package/docs/hosts.md +134 -0
  124. package/docs/plugins.md +83 -0
  125. package/docs/ui-themes.md +43 -0
  126. package/instructions/AGENTS.md +33 -0
  127. package/package.json +88 -4
  128. package/plugins/migrate_ai_studio/README.md +101 -0
  129. package/plugins/migrate_ai_studio/index.cjs +396 -0
  130. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  131. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  132. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  133. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  134. package/plugins/migrate_ai_studio/plugin.json +11 -0
  135. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  136. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  137. package/prodero.release.json +5 -0
@@ -0,0 +1,280 @@
1
+ /** Bounded host-agent adapter. Writes only local job/answer/review state.
2
+ * No provider, sampling, grant, canonical writer, promotion or Apply imports. */
3
+ import { assertSafeCurationData } from './curation-evidence.js';
4
+ import fs from 'node:fs/promises';
5
+ import { z } from 'zod';
6
+ import { zodToJsonSchema } from 'zod-to-json-schema';
7
+ import { checkedDataPath, ensureDataDirectory, writeFileAtomic } from './storage.js';
8
+ import { withSyncLock, ownsSyncLock } from './sync-transaction.js';
9
+ import { loadMemoryCatalog } from './memory-index.js';
10
+ import { planKnowledgeCuration } from './knowledge-curation.js';
11
+ import { loadCurationReview, assertCurationHead, publishSemanticReview, CurationReviewError } from './curation-review.js';
12
+ import { deterministicBaseline, revisionIssues } from './curation-revision.js';
13
+ import { SEMANTIC_CONTRACT, SEMANTIC_VALIDATOR, SEMANTIC_INSTRUCTIONS, semanticProposalSchema, semanticLimitsSchema, semanticHash as hash, semanticClauses, measuredTokens, validateSemanticProposal, materializeSemanticPlan, SemanticValidationError } from './curation-semantic-contract.js';
14
+ export const SEMANTIC_RETENTION = { maxJobs: 64, maxBytes: 2 * 1024 * 1024, maxAnswers: 128 };
15
+ export const jobStatuses = ['prepared', 'awaiting_reasoning', 'submitted', 'validated', 'rejected', 'stale', 'failed'];
16
+ const adapterSchema = z.object({ host: z.string().max(120).optional(), model: z.string().max(120).optional() }).strict();
17
+ export const semanticPrepareSchema = z.object({ reviewId: z.string().regex(/^review_[a-zA-Z0-9_]+$/), reviewRevisionId: z.string().regex(/^revision_[a-f0-9]{64}$/), componentId: z.string().regex(/^component_[a-f0-9]{24}$/), requestId: z.string().min(1).max(100), reason: z.enum(['initial', 'new_evidence', 'answer', 'validation_correction', 'resume']).default('initial'), correctionForJobId: z.string().regex(/^semantic_job_[a-f0-9]{40}$/).optional(), limits: semanticLimitsSchema.partial().optional(), adapter: adapterSchema.optional() }).strict();
18
+ const dir = 'local/curation-reasoning';
19
+ const reviewLock = (root, operation, options) => ownsSyncLock(root) ? operation() : withSyncLock(root, operation, options);
20
+ const last = (job) => job.events.at(-1);
21
+ const reject = (code, message) => { throw new CurationReviewError(code, message); };
22
+ async function jobPath(root, id) { if (!/^semantic_job_[a-f0-9]{40}$/.test(id))
23
+ return reject('semantic_invalid', 'Invalid reasoning job.'); return checkedDataPath(root, dir, `${id}.json`); }
24
+ export async function semanticJobs(root) { const names = await fs.readdir(await checkedDataPath(root, dir)).catch((e) => { if (e.code === 'ENOENT')
25
+ return []; throw e; }); return Promise.all(names.filter(n => /^semantic_job_[a-f0-9]{40}\.json$/.test(n)).sort().map(n => loadSemanticJob(root, n.slice(0, -5)))); }
26
+ export async function loadSemanticJob(root, id) {
27
+ const file = await jobPath(root, id);
28
+ if ((await fs.stat(file)).size > SEMANTIC_RETENTION.maxBytes)
29
+ return reject('semantic_limit', 'Reasoning history reached its limit.');
30
+ const job = JSON.parse(await fs.readFile(file, 'utf8'));
31
+ assertSafeCurationData(job);
32
+ if (job.version !== 1 || job.input?.jobId !== id || job.inputSeal !== hash(job.input) || job.input.contractVersion !== SEMANTIC_CONTRACT || job.input.validatorVersion !== SEMANTIC_VALIDATOR || job.input.bundleHash !== hash(job.input.bundle) || job.input.projectIdentity !== await fs.realpath(root))
33
+ return reject('semantic_invalid', 'Reasoning job integrity failed.');
34
+ let previous = job.inputSeal;
35
+ for (const [i, event] of job.events.entries()) {
36
+ const { hash: eventHash, ...body } = event;
37
+ if (event.sequence !== i + 1 || event.previousHash !== previous || eventHash !== hash(body) || !jobStatuses.includes(event.status))
38
+ return reject('semantic_invalid', 'Reasoning history integrity failed.');
39
+ previous = eventHash;
40
+ }
41
+ if (!job.events.length)
42
+ return reject('semantic_invalid', 'Reasoning history is empty.');
43
+ return job;
44
+ }
45
+ async function save(root, job) { assertSafeCurationData(job); const data = JSON.stringify(job); if (Buffer.byteLength(data) > SEMANTIC_RETENTION.maxBytes)
46
+ return reject('semantic_limit', 'Reasoning history reached its limit.'); await ensureDataDirectory(root, dir); await writeFileAtomic(await jobPath(root, job.input.jobId), data); }
47
+ async function event(root, job, status, data = {}) { const body = { sequence: job.events.length + 1, at: new Date().toISOString(), status, previousHash: last(job)?.hash ?? job.inputSeal, ...data }; job.events.push({ ...body, hash: hash(body) }); await save(root, job); }
48
+ async function noPendingTransaction(root) { if (await fs.stat(await checkedDataPath(root, '.transactions/pending.json')).then(() => true, () => false))
49
+ return reject('recovery_required', 'Finish the pending transaction before semantic analysis.'); }
50
+ async function current(root, reviewId, revisionId, decisionRevision) {
51
+ await noPendingTransaction(root);
52
+ const review = await loadCurationReview(root, reviewId);
53
+ if (review.snapshot.revision?.id !== revisionId || decisionRevision !== undefined && review.state.revision !== decisionRevision)
54
+ return reject('semantic_stale', 'Reasoning input is obsolete.');
55
+ await assertCurationHead(root, review);
56
+ const baseline = deterministicBaseline(review.snapshot.revision);
57
+ const now = await planKnowledgeCuration(root, { memorySyncSources: baseline.memorySyncSources });
58
+ if (now.planId !== baseline.planId)
59
+ return reject('semantic_stale', 'Knowledge or evidence changed.');
60
+ return review;
61
+ }
62
+ async function answerPath(root, lineageId) { if (!/^review_[a-zA-Z0-9_]+$/.test(lineageId))
63
+ return reject('semantic_invalid', 'Invalid answer lineage.'); return checkedDataPath(root, dir, `${lineageId}.answers.json`); }
64
+ export async function curationAnswers(root, lineageId) {
65
+ const file = await answerPath(root, lineageId);
66
+ let data;
67
+ try {
68
+ if ((await fs.stat(file)).size > SEMANTIC_RETENTION.maxBytes)
69
+ return reject('semantic_limit', 'Answer history reached its limit.');
70
+ data = JSON.parse(await fs.readFile(file, 'utf8'));
71
+ }
72
+ catch (e) {
73
+ if (e.code === 'ENOENT')
74
+ return [];
75
+ throw e;
76
+ }
77
+ // Historical sealed logs fail closed; never rewrite their contents during replay.
78
+ assertSafeCurationData(data);
79
+ if (data.version !== 1 || data.projectIdentity !== await fs.realpath(root) || data.hash !== hash(data.events) || !Array.isArray(data.events) || data.events.length > SEMANTIC_RETENTION.maxAnswers)
80
+ return reject('semantic_invalid', 'Answer history integrity failed.');
81
+ return data.events;
82
+ }
83
+ /** Internal input foundation for a future trusted App/form adapter. Not an MCP
84
+ * tool; an agent cannot claim App origin via the reasoning contract. */
85
+ export async function appendCurationAnswer(root, input) {
86
+ input = z.object({ reviewId: z.string(), reviewRevisionId: z.string(), questionId: z.string(), questionVersion: z.number().int().positive(), requestId: z.string().min(1).max(100), origin: z.enum(['app', 'form', 'agent_relay']), choiceId: z.string().max(100).optional(), rawText: z.string().max(8192).optional(), replacesAnswerId: z.string().optional(), decisionRevision: z.number().int().nonnegative().optional(), answerLogHash: z.string().optional() }).strict().parse(input);
87
+ assertSafeCurationData(input);
88
+ return reviewLock(root, async () => {
89
+ const review = await current(root, input.reviewId, input.reviewRevisionId, input.decisionRevision), revision = review.snapshot.revision, issue = revisionIssues(revision).find(i => i.question.id === input.questionId && i.question.version === input.questionVersion);
90
+ if (!issue)
91
+ return reject('semantic_invalid', 'Unknown question version.');
92
+ if (!!input.choiceId === !!input.rawText || input.rawText && (!issue.question.allowFreeText || Buffer.byteLength(input.rawText) > 8192))
93
+ return reject('semantic_invalid', 'Invalid answer.');
94
+ const choice = input.choiceId ? issue.question.choices.find(c => c.id === input.choiceId) : undefined;
95
+ if (input.choiceId && !choice)
96
+ return reject('semantic_invalid', 'Unknown answer choice.');
97
+ const events = await curationAnswers(root, revision.lineageId), prior = events.find(e => e.requestId === input.requestId);
98
+ if (prior) {
99
+ if (prior.questionId !== input.questionId || prior.choice?.id !== input.choiceId || prior.rawText !== input.rawText)
100
+ return reject('review_changed', 'Answer request changed.');
101
+ return prior;
102
+ }
103
+ if (input.answerLogHash !== undefined && input.answerLogHash !== hash(events))
104
+ return reject('review_changed', 'Another answer was saved. Reload this review.');
105
+ if (events.length >= SEMANTIC_RETENTION.maxAnswers)
106
+ return reject('semantic_limit', 'Answer history reached its limit.');
107
+ if (events.some(e => e.questionId === input.questionId && !events.some(a => a.replacesAnswerId === e.id)) && !input.replacesAnswerId)
108
+ return reject('review_changed', 'Replacing an answer requires its exact prior event.');
109
+ if (input.replacesAnswerId && !events.some(e => e.id === input.replacesAnswerId && e.questionId === input.questionId && !events.some(a => a.replacesAnswerId === e.id)))
110
+ return reject('semantic_invalid', 'Unknown previous answer.');
111
+ const answer = { id: `answer_${hash([revision.id, input.requestId]).slice(0, 32)}`, reviewId: input.reviewId, reviewRevisionId: revision.id, lineageId: revision.lineageId, questionId: issue.question.id, questionVersion: issue.question.version, issueId: issue.id, basisHash: issue.basisHash, sourceIds: issue.sourceIds, origin: input.origin, requestId: input.requestId, at: new Date().toISOString(), ...(choice ? { choice } : {}), ...(input.rawText ? { rawText: input.rawText } : {}), ...(input.replacesAnswerId ? { replacesAnswerId: input.replacesAnswerId } : {}) };
112
+ assertSafeCurationData(answer);
113
+ events.push(answer);
114
+ await ensureDataDirectory(root, dir);
115
+ await writeFileAtomic(await answerPath(root, revision.lineageId), JSON.stringify({ version: 1, projectIdentity: await fs.realpath(root), events, hash: hash(events) }));
116
+ return answer;
117
+ }, { recoverLifecycle: false, waitMs: 2000 });
118
+ }
119
+ async function makeBundle(root, review, componentId, limits) {
120
+ const revision = review.snapshot.revision, base = deterministicBaseline(revision), groups = revision.dependencies.groups.filter(g => g.component === componentId);
121
+ if (!groups.length)
122
+ return reject('semantic_invalid', 'Unknown dependency component.');
123
+ const ids = [...new Set(groups.flatMap(g => [...g.proposalIds, ...g.policyDependencies]))].sort();
124
+ const catalog = await loadMemoryCatalog(root, { persistCache: false }), sources = [], missing = [];
125
+ for (const id of ids) {
126
+ const summary = catalog.summaries.find(s => s.id === id);
127
+ if (!summary) {
128
+ missing.push(`missing-source:${id}`);
129
+ continue;
130
+ }
131
+ const record = await catalog.load(id);
132
+ sources.push({ id, revision: catalog.originalHashes.get(id), record, layer: summary.relativePath.startsWith('local/') ? 'local' : 'versioned', clauses: semanticClauses(id, catalog.originalHashes.get(id), record.content), editable: base.proposals.some(p => p.id === id) });
133
+ }
134
+ const expectedSources = sources.map(s => ({ id: s.id, revision: s.revision }));
135
+ if (expectedSources.length > 32)
136
+ return reject('semantic_limit', 'Component exceeds the maximum accountable source count.');
137
+ const evidence = [...new Map(base.proposals.filter(p => ids.includes(p.id)).flatMap(p => (p.investigation?.evidence ?? []).map(e => { const id = `evidence_${hash([e.path, e.revision, e.line]).slice(0, 24)}`; return [id, { id, revision: e.revision, excerpt: e.excerpt, kind: e.kind }]; }))).values()];
138
+ const allAnswers = await curationAnswers(root, revision.lineageId), issues = revisionIssues(revision);
139
+ const answers = allAnswers.filter(a => !allAnswers.some(b => b.replacesAnswerId === a.id) && issues.some(i => i.id === a.issueId && i.basisHash === a.basisHash && i.question.id === a.questionId) && a.sourceIds.some(id => ids.includes(id)));
140
+ const bundle = { version: 1, id: '', contract: SEMANTIC_CONTRACT, reviewId: review.snapshot.reviewId, reviewRevisionId: revision.id, lineageId: revision.lineageId, componentId, baseline: revision.baseline, expectedSources, sources, evidence, answers, answerLogHash: hash(allAnswers), dependencies: { ...revision.dependencies, groups }, signals: base.proposals.filter(p => ids.includes(p.id)).map(p => ({ sourceId: p.id, attention: p.attention, changes: p.changes })), missingFacts: base.proposals.filter(p => ids.includes(p.id) && p.attention.length && ((p.issueBasis?.length ?? 0) > 1 || p.before.status === 'disputed' || p.before.relations?.some(r => r.type === 'contradicts'))).map(p => ({ sourceIds: (p.issueBasis ?? [{ id: p.id }]).map(s => s.id).filter(id => ids.includes(id)), reason: 'Existing conflicting policy requires a project fact; neither source is authoritative merely because it is newer.' })), completeness: { complete: !missing.length && !base.evidenceTruncated, reasons: [...missing, ...(base.evidenceTruncated ? ['evidence-incomplete'] : [])] }, limits };
141
+ const mark = (reason) => { bundle.completeness.complete = false; if (!bundle.completeness.reasons.includes(reason))
142
+ bundle.completeness.reasons.push(reason); };
143
+ if (bundle.sources.length > limits.maxItems) {
144
+ bundle.sources = bundle.sources.slice(0, limits.maxItems);
145
+ mark('source-limit');
146
+ }
147
+ if (bundle.evidence.length > limits.maxEvidence) {
148
+ bundle.evidence = bundle.evidence.slice(0, limits.maxEvidence);
149
+ mark('evidence-limit');
150
+ }
151
+ while (Buffer.byteLength(JSON.stringify(bundle)) > limits.maxBytes - 128 || measuredTokens(bundle) > limits.maxTokens - 32) {
152
+ mark('bundle-limit');
153
+ if (bundle.evidence.length)
154
+ bundle.evidence.pop();
155
+ else if (bundle.sources.length)
156
+ bundle.sources.pop();
157
+ else
158
+ return reject('semantic_limit', 'The minimum component manifest exceeds the reasoning budget.');
159
+ }
160
+ bundle.id = `bundle_${hash(bundle)}`;
161
+ return bundle;
162
+ }
163
+ function response(job) { const { bundle, ...input } = job.input; const history = job.events.map(({ proposal, ...event }) => event); return { jobId: job.input.jobId, status: last(job).status, job: { ...job, input, events: history }, task: { instructions: SEMANTIC_INSTRUCTIONS, proposalSchema: zodToJsonSchema(semanticProposalSchema, { $refStrategy: 'none' }), data: bundle }, acceptance: 'staged_candidate_only', validation: last(job).reason, resultReviewId: last(job).resultReviewId }; }
164
+ export async function prepareSemanticCuration(root, raw) {
165
+ const input = semanticPrepareSchema.parse(raw);
166
+ return reviewLock(root, async () => {
167
+ const review = await current(root, input.reviewId, input.reviewRevisionId), limits = semanticLimitsSchema.parse(input.limits ?? {}), bundle = await makeBundle(root, review, input.componentId, limits), history = await semanticJobs(root);
168
+ const semanticInputHash = hash([bundle.expectedSources, bundle.sources, bundle.evidence, bundle.answers, bundle.signals, bundle.missingFacts, bundle.completeness, SEMANTIC_CONTRACT, SEMANTIC_VALIDATOR]);
169
+ const priorRequest = history.find(j => j.input.requestId === input.requestId && j.input.lineageId === bundle.lineageId);
170
+ if (priorRequest) {
171
+ if (priorRequest.input.semanticInputHash !== semanticInputHash || priorRequest.input.reviewRevisionId !== input.reviewRevisionId)
172
+ return reject('review_changed', 'Reasoning request changed.');
173
+ return response(priorRequest);
174
+ }
175
+ const matches = history.filter(j => j.input.semanticInputHash === semanticInputHash && j.input.lineageId === bundle.lineageId).sort((a, b) => b.input.createdAt.localeCompare(a.input.createdAt));
176
+ const same = matches.find(j => j.input.reviewRevisionId === input.reviewRevisionId && j.input.decisionRevision === review.state.revision) ?? matches[0];
177
+ let correction = 0;
178
+ if (input.reason === 'validation_correction') {
179
+ const previous = history.find(j => j.input.jobId === input.correctionForJobId);
180
+ if (!previous || last(previous).status !== 'rejected' || previous.input.semanticInputHash !== semanticInputHash || previous.input.correction >= previous.input.bundle.limits.maxCorrections)
181
+ return reject('semantic_limit', 'No correction retry remains.');
182
+ correction = previous.input.correction + 1;
183
+ const retry = history.find(j => j.input.previousJobId === previous.input.jobId);
184
+ if (retry)
185
+ return response(retry);
186
+ }
187
+ else if (same && !(input.reason === 'resume' && ['prepared', 'awaiting_reasoning', 'submitted', 'stale'].includes(last(same).status) && (same.input.reviewRevisionId !== input.reviewRevisionId || same.input.decisionRevision !== review.state.revision)))
188
+ return response(same);
189
+ if (history.length >= SEMANTIC_RETENTION.maxJobs || correction === 0 && history.filter(j => j.input.lineageId === bundle.lineageId && j.input.correction === 0).length >= Math.min(limits.maxPasses, ...history.filter(j => j.input.lineageId === bundle.lineageId).map(j => j.input.bundle.limits.maxPasses)))
190
+ return reject('semantic_limit', 'Reasoning budget reached; pending history was preserved.');
191
+ const createdAt = new Date().toISOString(), inputHash = hash([bundle.id, review.state.revision, bundle.answerLogHash, SEMANTIC_VALIDATOR, correction]), jobId = `semantic_job_${hash([bundle.lineageId, inputHash]).slice(0, 40)}`;
192
+ const body = { jobId, projectIdentity: review.snapshot.projectIdentity, reviewId: input.reviewId, reviewRevisionId: input.reviewRevisionId, lineageId: bundle.lineageId, decisionRevision: review.state.revision, bundleId: bundle.id, bundleHash: hash(bundle), bundle, contractVersion: SEMANTIC_CONTRACT, validatorVersion: SEMANTIC_VALIDATOR, requestId: input.requestId, reason: input.reason, inputHash, semanticInputHash, createdAt, expiresAt: new Date(Date.now() + limits.ttlMs).toISOString(), correction, ...(input.correctionForJobId ? { previousJobId: input.correctionForJobId } : {}), ...(input.adapter ? { adapter: input.adapter } : {}) };
193
+ const job = { version: 1, input: body, inputSeal: hash(body), events: [] };
194
+ await event(root, job, 'prepared');
195
+ await event(root, job, 'awaiting_reasoning');
196
+ return response(job);
197
+ }, { recoverLifecycle: false, waitMs: 2000 });
198
+ }
199
+ export async function inspectSemanticCuration(root, jobId) { return response(await loadSemanticJob(root, jobId)); }
200
+ export async function submitSemanticCuration(root, input) {
201
+ return reviewLock(root, async () => {
202
+ const job = await loadSemanticJob(root, input.jobId), outputHash = hash(input.proposal), status = last(job).status;
203
+ if (input.inputHash !== job.input.inputHash)
204
+ return reject('semantic_invalid', 'Reasoning input hash changed.');
205
+ const submission = job.events.find(e => e.status === 'submitted');
206
+ if (submission && submission.outputHash !== outputHash)
207
+ return reject('review_changed', 'This job already has a different submission.');
208
+ if (['validated', 'rejected', 'stale', 'failed'].includes(status)) {
209
+ if (last(job).outputHash !== outputHash)
210
+ return reject('review_changed', 'This job already has a different terminal result.');
211
+ return response(job);
212
+ }
213
+ if (submission) {
214
+ const resultId = `review_${hash(['semantic', job.input.jobId]).slice(0, 40)}`;
215
+ const file = await checkedDataPath(root, 'local/curation-reviews', `${resultId}.json`);
216
+ if (await fs.stat(file).then(() => true, () => false)) {
217
+ const recovered = await loadCurationReview(root, resultId, { allowExpired: true }), typed = recovered.snapshot.revision?.typedPlan;
218
+ if (typed?.kind !== 'semantic-v2' || typed.analyses.at(-1)?.jobId !== job.input.jobId || typed.analyses.at(-1)?.outputHash !== outputHash)
219
+ return reject('semantic_invalid', 'Published semantic revision changed.');
220
+ await event(root, job, 'validated', { outputHash, resultReviewId: resultId, resultRevisionId: recovered.snapshot.revision.id, reason: 'recovered_staged_revision' });
221
+ return { ...response(job), review: recovered };
222
+ }
223
+ }
224
+ let review;
225
+ try {
226
+ review = await current(root, job.input.reviewId, job.input.reviewRevisionId, job.input.decisionRevision);
227
+ if (Date.now() > Date.parse(job.input.expiresAt) || hash(await curationAnswers(root, job.input.lineageId)) !== job.input.bundle.answerLogHash)
228
+ throw new Error('obsolete');
229
+ }
230
+ catch {
231
+ await event(root, job, 'stale', { reason: 'obsolete_revision_or_evidence', outputHash });
232
+ return response(job);
233
+ }
234
+ // Size-check before persisting an untrusted output. Store only its hash if over budget.
235
+ if (Buffer.byteLength((JSON.stringify(input.proposal) ?? 'null')) > job.input.bundle.limits.maxOutputBytes || measuredTokens(input.proposal) > job.input.bundle.limits.maxOutputTokens) {
236
+ await event(root, job, 'rejected', { reason: 'output_limit', outputHash });
237
+ return response(job);
238
+ }
239
+ if (!submission)
240
+ await event(root, job, 'submitted', { outputHash, proposal: input.proposal });
241
+ try {
242
+ const proposal = validateSemanticProposal(job.input.bundle, input.proposal), accepted = { jobId: job.input.jobId, inputHash: job.input.inputHash, outputHash, decisionRevision: job.input.decisionRevision, correction: job.input.correction, bundle: job.input.bundle, proposal }, previous = review.snapshot.revision.typedPlan;
243
+ const analyses = [...(previous.kind === 'semantic-v2' ? previous.analyses : []), accepted], basePlan = deterministicBaseline(review.snapshot.revision), plan = materializeSemanticPlan(basePlan, analyses);
244
+ const next = await publishSemanticReview(root, review, { kind: 'semantic-v2', validator: SEMANTIC_VALIDATOR, basePlan, analyses, plan }, job.input.jobId);
245
+ await event(root, job, 'validated', { outputHash, resultReviewId: next.snapshot.reviewId, resultRevisionId: next.snapshot.revision.id, reason: proposal.sources.every(s => s.disposition !== 'proposed') ? 'terminal_no_rewrite' : 'structurally_validated_candidate' });
246
+ return { ...response(job), review: next };
247
+ }
248
+ catch (error) {
249
+ if (error instanceof SemanticValidationError || error instanceof z.ZodError) {
250
+ await event(root, job, 'rejected', { reason: error instanceof SemanticValidationError ? error.reason : 'malformed_output', outputHash });
251
+ return response(job);
252
+ }
253
+ if (error instanceof CurationReviewError && ['review_retention_full', 'review_too_large', 'semantic_limit'].includes(error.code)) {
254
+ await event(root, job, 'failed', { reason: 'staging_capacity', outputHash });
255
+ return response(job);
256
+ }
257
+ throw error;
258
+ }
259
+ }, { recoverLifecycle: false, waitMs: 2000 });
260
+ }
261
+ export function semanticJobStatus(job, review) { const status = last(job).status; return ['prepared', 'awaiting_reasoning', 'submitted'].includes(status) && (job.input.decisionRevision !== review.state.revision || Date.now() > Date.parse(job.input.expiresAt)) ? 'stale' : status; }
262
+ /** Unchanged supporting records must not keep a decided component's job active. */
263
+ export function semanticJobSuspended(job, review) {
264
+ const sourceIds = new Set(job.input.bundle.dependencies.groups.flatMap(g => g.proposalIds));
265
+ const affected = review.snapshot.units.filter(u => u.proposalIds.some(id => sourceIds.has(id))).map(u => review.state.items[u.id]);
266
+ const work = affected.filter(item => item.outcome !== 'unchanged');
267
+ return (work.length ? work : affected).every(item => item.execution === 'applied' || ['deferred', 'keep_current'].includes(item.disposition ?? ''));
268
+ }
269
+ export async function semanticReviewStatus(root, review) {
270
+ const history = (await semanticJobs(root)).filter(j => j.input.reviewRevisionId === review.snapshot.revision?.id);
271
+ const active = history.filter(j => !history.some(next => next.input.previousJobId === j.input.jobId) && !semanticJobSuspended(j, review));
272
+ const pending = active.filter(j => ['prepared', 'awaiting_reasoning', 'submitted'].includes(semanticJobStatus(j, review))), failed = active.filter(j => ['failed', 'rejected', 'stale'].includes(semanticJobStatus(j, review)));
273
+ const revision = review.snapshot.revision, issues = revision ? revisionIssues(revision) : [], answers = revision ? await curationAnswers(root, revision.lineageId) : [];
274
+ const bound = new Set(revision?.answerBindings.map(a => a.answerEventId) ?? []);
275
+ const awaiting = answers.filter(a => review.snapshot.units.some(u => u.proposalIds.some(id => a.sourceIds.includes(id)) && !['deferred', 'keep_current'].includes(review.state.items[u.id]?.disposition ?? '')) && !bound.has(a.id) && !answers.some(b => b.replacesAnswerId === a.id) && issues.some(i => i.id === a.issueId && i.basisHash === a.basisHash && i.question.id === a.questionId));
276
+ const answerGroups = revision?.dependencies.groups.filter(g => awaiting.some(a => a.sourceIds.some(id => g.proposalIds.includes(id)))).map(g => g.id) ?? [];
277
+ return { pending: pending.length + awaiting.filter(a => !pending.some(j => j.input.bundle.answers.some(bound => bound.id === a.id))).length, answeredIssueIds: awaiting.map(a => a.issueId), failed: failed.length, jobIds: history.map(j => j.input.jobId), blockedGroupIds: [...new Set([...answerGroups, ...[...pending, ...failed].flatMap(j => j.input.bundle.dependencies.groups.map(g => g.id))])] };
278
+ }
279
+ export async function semanticReferencedReviews(root) { return new Set((await semanticJobs(root)).map(j => j.input.reviewId)); }
280
+ //# sourceMappingURL=curation-semantic-jobs.js.map
@@ -0,0 +1,17 @@
1
+ import { hasSecret } from "./secrets.js";
2
+ /** Public diagnostics never include parser source excerpts or rejected schema values. */
3
+ export function publicDiagnostic(error) {
4
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Operation failed";
5
+ if (message.startsWith("PRODERO_") && !hasSecret(message))
6
+ return message;
7
+ if (error instanceof SyntaxError || /Unexpected token|Unexpected end of JSON|not valid JSON|JSON at position/.test(message)) {
8
+ return "PRODERO_INVALID_JSON: Invalid JSON input or stored data. Inspect the selected input/configuration or preserved transaction journal; restore valid data before retrying.";
9
+ }
10
+ if (/Input validation error|Output validation error|"code":\s*"(?:invalid_|too_|unrecognized_)/.test(message)) {
11
+ return "PRODERO_INVALID_ARGUMENT: Tool arguments or metadata do not match the schema. Check the tool's field types and allowed values before retrying.";
12
+ }
13
+ if (hasSecret(message))
14
+ return "PRODERO_UNSAFE_DIAGNOSTIC: Rejected input contains a potential credential. Remove secrets and retry; the rejected value is omitted.";
15
+ return message;
16
+ }
17
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1,114 @@
1
+ export function parseFrontmatter(content, filename = "canonical Markdown") {
2
+ content = content.replace(/\r\n/g, "\n");
3
+ if (!content.startsWith("---\n"))
4
+ return { metadata: {}, body: content };
5
+ const end = content.indexOf("\n---\n", 4);
6
+ if (end < 0)
7
+ throw new Error(`Invalid canonical frontmatter: ${filename} has no closing delimiter.`);
8
+ const metadata = {};
9
+ for (const line of content.slice(4, end).split("\n")) {
10
+ const match = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
11
+ if (!line.trim())
12
+ continue;
13
+ if (!match)
14
+ throw new Error(`Invalid canonical frontmatter: ${filename} has a malformed metadata line.`);
15
+ const [, key, raw] = match;
16
+ if (Object.hasOwn(metadata, key))
17
+ throw new Error(`Invalid canonical frontmatter: ${filename} has duplicate ${key}.`);
18
+ if (raw === "null" || raw === "")
19
+ metadata[key] = null;
20
+ else if (raw === "true" || raw === "false")
21
+ metadata[key] = raw === "true";
22
+ else if (raw.startsWith("[") || raw.startsWith("{") || raw.startsWith('"')) {
23
+ try {
24
+ metadata[key] = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ throw new Error(`Invalid canonical frontmatter: ${filename} has malformed ${key}.`);
28
+ }
29
+ }
30
+ else if (/^-?\d+$/.test(raw))
31
+ metadata[key] = Number(raw);
32
+ else
33
+ metadata[key] = raw;
34
+ }
35
+ return { metadata, body: content.slice(end + 5).replace(/^\r?\n/, "") };
36
+ }
37
+ export function frontmatter(metadata, body) {
38
+ const lines = Object.entries(metadata).map(([key, value]) => {
39
+ if (value === null || value === undefined)
40
+ return `${key}: null`;
41
+ if (typeof value === "number" || typeof value === "boolean")
42
+ return `${key}: ${value}`;
43
+ if (Array.isArray(value) || typeof value === "object")
44
+ return `${key}: ${JSON.stringify(value)}`;
45
+ const stringValue = String(value);
46
+ return `${key}: ${/[\n:[\]{}#,]|^\s|\s$|^"|^(?:true|false|null|-?\d+)$/.test(stringValue) ? JSON.stringify(stringValue) : stringValue}`;
47
+ });
48
+ return `---\n${lines.join("\n")}\n---\n\n${body.trim()}\n`;
49
+ }
50
+ export function sectionBullets(body, headings) {
51
+ const result = Object.fromEntries(headings.map((heading) => [heading, []]));
52
+ let current = null;
53
+ let bullet = null;
54
+ const flush = () => {
55
+ if (current && bullet) {
56
+ const value = bullet.join("\n").trim();
57
+ if (value)
58
+ result[current].push(value);
59
+ }
60
+ bullet = null;
61
+ };
62
+ for (const line of body.split(/\r?\n/)) {
63
+ if (bullet && line.startsWith(" ")) {
64
+ bullet.push(line.slice(2));
65
+ continue;
66
+ }
67
+ const heading = line.match(/^##\s+(.+?)\s*$/)?.[1];
68
+ if (heading) {
69
+ flush();
70
+ current = headings.includes(heading) ? heading : null;
71
+ continue;
72
+ }
73
+ const item = line.match(/^\s*[-*]\s+(.+)$/)?.[1];
74
+ if (item && current) {
75
+ flush();
76
+ bullet = [item];
77
+ }
78
+ else if (bullet && /^\s{2,}\S/.test(line))
79
+ bullet.push(line.trim());
80
+ }
81
+ flush();
82
+ return result;
83
+ }
84
+ export function parseObservationAnnotations(raw) {
85
+ if (!raw.startsWith("["))
86
+ return { metadata: {}, content: raw.trim() };
87
+ let end = -1;
88
+ let quoted = false;
89
+ for (let i = 1; i < raw.length; i += 1) {
90
+ if (raw[i] === "\\" && quoted) {
91
+ i += 1;
92
+ continue;
93
+ }
94
+ if (raw[i] === '"')
95
+ quoted = !quoted;
96
+ if (raw[i] === "]" && !quoted) {
97
+ end = i;
98
+ break;
99
+ }
100
+ }
101
+ if (end < 0)
102
+ return { metadata: {}, content: raw.trim() };
103
+ const metadata = {};
104
+ for (const match of raw.slice(1, end).matchAll(/([a-z_]+)=("(?:\\.|[^"\\])*"|[^\s;]+)/g)) {
105
+ try {
106
+ metadata[match[1]] = match[2].startsWith('"') ? JSON.parse(match[2]) : match[2];
107
+ }
108
+ catch {
109
+ metadata[match[1]] = match[2].replace(/^"|"$/g, "");
110
+ }
111
+ }
112
+ return { metadata, content: raw.slice(end + 1).trim() };
113
+ }
114
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,6 @@
1
+ import crypto from "node:crypto";
2
+ export function sortableId(prefix, now = new Date()) {
3
+ const stamp = now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 17);
4
+ return `${prefix}_${stamp}_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
5
+ }
6
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1,169 @@
1
+ import { applicabilitySchema, withApplicability, sameApplicability, applicabilityKey, inferApplicability } from "./applicability.js";
2
+ import { assertCompatibleProject } from "./storage.js";
3
+ import fs from "node:fs/promises";
4
+ import { sortableId } from "./ids.js";
5
+ import { z } from "zod";
6
+ import { listKnowledge, renderKnowledge } from "./memory.js";
7
+ import { listProjectSkills, renderSkill, skillInputSchema } from "./skills.js";
8
+ import { readConfig, sha256, listCheckpoints } from "./storage.js";
9
+ import { checkedPath, hasSecret, readSource, safeRelative, snapshotImportState } from "./plugin-files.js";
10
+ import { commitSyncTransaction, recoverSyncTransaction, withSyncLock } from "./sync-transaction.js";
11
+ import { buildIndex } from "./index.js";
12
+ import { estimateTokens, knowledgeText, MAX_CANONICAL_ITEM_TOKENS } from "./telemetry.js";
13
+ import { assertEnglishProse } from "./language.js";
14
+ const hash = z.string().regex(/^[a-f0-9]{64}$/);
15
+ const text = z.string().trim().min(1);
16
+ export const importEvidenceSchema = z.object({ path: text.max(400), sha256: hash, startLine: z.number().int().positive(), endLine: z.number().int().positive() }).strict();
17
+ export const importItemSchema = z.object({
18
+ applicability: applicabilitySchema.optional(), scopeKind: z.enum(["path", "topic"]).optional(),
19
+ key: text.max(100), destination: z.enum(["project_instruction", "knowledge", "skill"]),
20
+ title: text.max(200).regex(/^[^\r\n]+$/), content: text.max(8000),
21
+ scope: z.enum(["global", "module", "task"]), scopeRef: text.max(300).nullable(),
22
+ category: z.enum(["principle", "invariant", "decision", "constraint", "lesson", "state", "instruction"]),
23
+ evidence: z.array(importEvidenceSchema).min(1).max(12), reason: text.max(1000),
24
+ persistEvidence: z.boolean().optional(),
25
+ skill: skillInputSchema.strict().nullable(), activateSkill: z.boolean(),
26
+ }).strict();
27
+ export const importBatchSchema = z.object({
28
+ version: z.literal(1), planId: hash, approvedPlanId: hash,
29
+ stateFingerprint: hash, items: z.array(importItemSchema).max(100),
30
+ }).strict();
31
+ export async function readImportState(root) {
32
+ if (!(await readConfig(root)))
33
+ throw new Error("Initialize Prodero before importing knowledge");
34
+ const files = await snapshotImportState(root);
35
+ return { knowledge: await listKnowledge(root), skills: await listProjectSkills(root), fingerprint: sha256(JSON.stringify(files)), files, importedSources: (await listCheckpoints(root)).flatMap(c => c.importReceipt ? [...c.importReceipt.created, ...c.importReceipt.duplicates].flatMap(i => i.source ?? []) : []), importedKeys: (await listCheckpoints(root)).flatMap(c => c.importReceipt ? [...c.importReceipt.created, ...c.importReceipt.duplicates].map(i => i.key) : []) };
36
+ }
37
+ const normalized = (content) => content.trim().replace(/\s+/g, " ").toLowerCase();
38
+ /** Read-only preflight is shared by proposal review and the durable apply boundary. */
39
+ export async function validateImportItems(root, raw) {
40
+ await assertCompatibleProject(root);
41
+ const items = z.array(importItemSchema).max(100).parse(raw);
42
+ if (new Set(items.map(i => i.key)).size !== items.length)
43
+ throw new Error("Duplicate import item key");
44
+ if (hasSecret(JSON.stringify(items)))
45
+ throw new Error("Potential secret in import proposal");
46
+ // Validate every proposal and evidence before taking the writer lock or touching durable state.
47
+ for (const item of items) {
48
+ assertEnglishProse([item.title, item.content, item.reason, item.skill?.title, item.skill?.description, item.skill?.scope, ...(item.skill?.triggers ?? []), item.skill?.content], "Imported Prodero knowledge");
49
+ if (item.destination !== "skill" && estimateTokens(knowledgeText(item)) > MAX_CANONICAL_ITEM_TOKENS)
50
+ throw new Error("Knowledge exceeds canonical token budget");
51
+ if ((item.scope === "global") !== (item.scopeRef === null))
52
+ throw new Error("Invalid scope boundary");
53
+ if (item.destination === "project_instruction" && item.category !== "instruction")
54
+ throw new Error("Project rules require instruction category");
55
+ if (item.destination === "skill" ? !item.skill : item.skill !== null || item.activateSkill)
56
+ throw new Error("Invalid skill destination");
57
+ if (item.skill && (item.title !== item.skill.title || item.content !== item.skill.content || item.skill.relatedKnowledge.length))
58
+ throw new Error("Skill proposal must match its reviewed content and use evidence references");
59
+ if (item.skill && estimateTokens(renderSkill({ ...item.skill, id: "validation", status: "candidate", origin: "legacy", createdAt: "", updatedAt: "", sourceSessions: [], derivedFrom: [], proposedReason: item.reason, reviewedAt: null, reviewReason: null, path: "skills/validation.md" })) > 1500)
60
+ throw new Error("Skill exceeds context budget; distill the procedure");
61
+ for (const reference of item.skill?.references ?? [])
62
+ safeRelative(reference);
63
+ for (const evidence of item.evidence) {
64
+ safeRelative(evidence.path);
65
+ const source = await readSource(root, evidence.path);
66
+ if (source.sha256 !== evidence.sha256 || evidence.endLine < evidence.startLine || evidence.endLine > source.content.split("\n").length)
67
+ throw new Error("Source evidence changed or line range invalid");
68
+ }
69
+ }
70
+ }
71
+ /** No plugin-supplied write paths, overwrites or model execution. Exact approved input only. */
72
+ export async function applyImport(root, plugin, raw) {
73
+ const batch = importBatchSchema.parse(raw);
74
+ if (batch.approvedPlanId !== batch.planId)
75
+ throw new Error("Exact plan approval required");
76
+ await validateImportItems(root, batch.items);
77
+ const batchHash = sha256(JSON.stringify(batch));
78
+ return withSyncLock(root, async () => {
79
+ await recoverSyncTransaction(root);
80
+ const checkpoints = await listCheckpoints(root);
81
+ const previousImport = checkpoints.find(c => c.importReceipt?.plugin === plugin.id && c.importReceipt?.planId === batch.planId);
82
+ if (previousImport?.importReceipt) {
83
+ const receipt = previousImport.importReceipt;
84
+ if (receipt.batchHash !== batchHash)
85
+ throw new Error("Applied plan differs from requested batch");
86
+ await checkedPath(root, ".prodero/index.md");
87
+ await buildIndex(root);
88
+ return receipt;
89
+ }
90
+ // Reuse Core's chronological ID contract; the receipt is the stable replay key.
91
+ const checkpoint = sortableId("chk");
92
+ const state = await readImportState(root);
93
+ if (state.fingerprint !== batch.stateFingerprint)
94
+ throw new Error("Prodero state changed since audit; create a fresh reviewed plan");
95
+ const writes = [];
96
+ const now = new Date().toISOString();
97
+ const receipt = { version: 1, plugin: plugin.id, pluginVersion: plugin.version, planId: batch.planId, batchHash, checkpoint, created: [], duplicates: [], before: state.files };
98
+ const knowledge = [...state.knowledge], skills = [...state.skills];
99
+ for (const item of batch.items) {
100
+ const applicability = item.applicability ?? (item.scopeRef ? { kind: "scoped", ...(item.scopeKind === "path" || /[/.]/.test(item.scopeRef) ? { paths: [item.scopeRef] } : { subsystems: [item.scopeRef] }) } : inferApplicability(item.content));
101
+ const signature = item.destination === "skill" ? `skill:${item.skill.scope}:${normalized(item.content)}` : `${item.destination}:${applicabilityKey({ ...item, applicability })}:${item.category}:${normalized(item.content)}`;
102
+ const id = `${item.destination === "skill" ? "skill" : "mem"}_import_${sha256(signature).slice(0, 32)}`;
103
+ const provenance = item.persistEvidence !== false ? item.evidence.map(e => `${e.path}#L${e.startLine}-L${e.endLine}@${e.sha256}`) : [];
104
+ const derivedFrom = [...provenance, `plugin:${plugin.id}@${plugin.version}/plan:${batch.planId}`];
105
+ let target, after;
106
+ if (item.destination === "skill") {
107
+ const prior = skills.find(s => s.status !== "superseded" && normalized(s.content) === normalized(item.content) && s.scope === item.skill.scope);
108
+ if (prior) {
109
+ const p = `.prodero/${prior.path}`;
110
+ receipt.duplicates.push({ key: item.key, path: p, sha256: sha256(await fs.readFile(await checkedPath(root, p), "utf8").catch(() => writes.find(w => `.prodero/${w.path}` === p).after)), destination: item.destination, source: item.evidence });
111
+ continue;
112
+ }
113
+ const skill = { ...item.skill, id, status: item.activateSkill ? "active" : "candidate", origin: "legacy", createdAt: now, updatedAt: now, sourceSessions: [], derivedFrom, proposedReason: item.reason, reviewedAt: item.activateSkill ? now : null, reviewReason: item.activateSkill ? `Approved plan ${batch.planId}` : null, path: `skills/${id}.md` };
114
+ after = renderSkill(skill);
115
+ if (estimateTokens(after) > 1500)
116
+ throw new Error("Skill with provenance exceeds context budget");
117
+ target = skill.path;
118
+ skills.push(skill);
119
+ }
120
+ else {
121
+ const prior = knowledge.find(k => k.status === "active" && sameApplicability(k, { ...item, applicability }) && k.category === item.category && normalized(k.content) === normalized(item.content));
122
+ if (prior) {
123
+ const p = `.prodero/memory/${prior.id}.md`;
124
+ receipt.duplicates.push({ key: item.key, path: p, sha256: state.files[p] ?? sha256(writes.find(w => `.prodero/${w.path}` === p).after), destination: item.destination, source: item.evidence });
125
+ continue;
126
+ }
127
+ const knowledgeItem = withApplicability({ version: 3, contextPolicy: "relevant", authority: "imported", id, scope: item.scope, scopeRef: item.scopeRef, category: item.category, priority: "normal", status: "active", title: item.title, content: item.content, createdAt: now, updatedAt: now, sourceSessions: [], derivedFrom, supersedes: [], related: [], evidence: provenance.map(ref => ({ kind: "source_file", ref })) }, applicability);
128
+ if (estimateTokens(knowledgeText(knowledgeItem)) > MAX_CANONICAL_ITEM_TOKENS)
129
+ throw new Error("Knowledge exceeds canonical token budget");
130
+ after = renderKnowledge(knowledgeItem);
131
+ target = `memory/${id}.md`;
132
+ knowledge.push(knowledgeItem);
133
+ }
134
+ writes.push({ path: target, before: null, after });
135
+ receipt.created.push({ key: item.key, path: `.prodero/${target}`, sha256: sha256(after), destination: item.destination, source: item.evidence });
136
+ }
137
+ await checkedPath(root, ".prodero/index.md");
138
+ const record = { id: checkpoint, createdAt: now, previousCheckpoint: checkpoints.at(-1)?.id ?? null, sessions: [], consolidation: "not-performed", createdMemory: receipt.created.filter(i => i.destination !== "skill").map(i => i.path.split("/").at(-1).slice(0, -3)), createdSkills: receipt.created.filter(i => i.destination === "skill").map(i => i.path.split("/").at(-1).slice(0, -3)), updatedMemory: [], supersededMemory: [], duplicateMemory: [], conflicts: [], summary: { sessions: 0, candidates: batch.items.length, created: receipt.created.length, consolidated: 0, duplicates: receipt.duplicates.length, superseded: 0, conflicts: 0 }, importReceipt: receipt };
139
+ try {
140
+ await commitSyncTransaction(root, [...writes, { path: `checkpoints/${checkpoint}.json`, before: null, after: JSON.stringify(record, null, 2) + "\n" }]);
141
+ await buildIndex(root);
142
+ }
143
+ catch {
144
+ throw new Error("Import interrupted and may be partially applied. Core retains its recovery journal/checkpoint; retry this exact approved plan after resolving the filesystem or concurrent-write error.");
145
+ }
146
+ return receipt;
147
+ });
148
+ }
149
+ export async function verifyImport(root, receipt) {
150
+ await assertCompatibleProject(root);
151
+ const issues = [];
152
+ if (!/^chk_[0-9]{17}_[a-f0-9]{12}$/.test(receipt.checkpoint))
153
+ throw new Error("Invalid receipt checkpoint");
154
+ const record = JSON.parse(await fs.readFile(await checkedPath(root, `.prodero/checkpoints/${receipt.checkpoint}.json`), "utf8"));
155
+ if (JSON.stringify(record.importReceipt) !== JSON.stringify(receipt))
156
+ throw new Error("Receipt does not match durable checkpoint");
157
+ const state = await readImportState(root);
158
+ for (const item of [...receipt.created, ...receipt.duplicates])
159
+ if (state.files[item.path] !== item.sha256)
160
+ issues.push(`Imported item missing or changed: ${item.key}`);
161
+ const allowed = new Set([...receipt.created.map(i => i.path), `.prodero/checkpoints/${receipt.checkpoint}.json`, ".prodero/index.md"]);
162
+ for (const p of new Set([...Object.keys(receipt.before), ...Object.keys(state.files)]))
163
+ if (!allowed.has(p) && receipt.before[p] !== state.files[p])
164
+ issues.push(`Other canonical state changed: ${p}`);
165
+ if (new Set(receipt.created.map(i => i.path)).size !== receipt.created.length)
166
+ issues.push("Duplicate created destination");
167
+ return { valid: issues.length === 0, issues };
168
+ }
169
+ //# sourceMappingURL=imports.js.map