@prodero/curator 0.0.0-stage → 0.2.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/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/SECURITY.md +7 -0
- package/bin/prodero +7 -0
- package/dist/cli.js +266 -0
- package/dist/context-instructions.js +2 -0
- package/dist/contracts.js +51 -0
- package/dist/conversation-presentation.js +297 -0
- package/dist/core/agent-instructions.js +52 -0
- package/dist/core/applicability-label.js +11 -0
- package/dist/core/applicability.js +143 -0
- package/dist/core/bootstrap.js +129 -0
- package/dist/core/consolidation.js +315 -0
- package/dist/core/constants.js +21 -0
- package/dist/core/context-allocation.js +54 -0
- package/dist/core/context-snapshot.js +12 -0
- package/dist/core/context.js +489 -0
- package/dist/core/curation-clauses.js +68 -0
- package/dist/core/curation-evidence.js +98 -0
- package/dist/core/curation-graphical.js +117 -0
- package/dist/core/curation-question.js +28 -0
- package/dist/core/curation-review-presentation.js +4 -0
- package/dist/core/curation-review-state.js +62 -0
- package/dist/core/curation-review.js +573 -0
- package/dist/core/curation-revision.js +106 -0
- package/dist/core/curation-semantic-contract.js +242 -0
- package/dist/core/curation-semantic-jobs.js +340 -0
- package/dist/core/diagnostics.js +17 -0
- package/dist/core/format.js +114 -0
- package/dist/core/ids.js +6 -0
- package/dist/core/imports.js +169 -0
- package/dist/core/index.js +93 -0
- package/dist/core/instruction-review.js +86 -0
- package/dist/core/integrity-files.js +174 -0
- package/dist/core/intelligence.js +95 -0
- package/dist/core/knowledge-analysis.js +46 -0
- package/dist/core/knowledge-authority.js +143 -0
- package/dist/core/knowledge-curation.js +316 -0
- package/dist/core/knowledge-engine.js +475 -0
- package/dist/core/knowledge-graph.js +192 -0
- package/dist/core/knowledge-lifecycle.js +212 -0
- package/dist/core/knowledge-management.js +113 -0
- package/dist/core/knowledge-migration.js +84 -0
- package/dist/core/knowledge-policy.js +38 -0
- package/dist/core/knowledge-provenance.js +90 -0
- package/dist/core/knowledge-write-policy.js +85 -0
- package/dist/core/language.js +14 -0
- package/dist/core/local-preferences.js +76 -0
- package/dist/core/matching.js +6 -0
- package/dist/core/memory-index.js +241 -0
- package/dist/core/memory-intelligence.js +288 -0
- package/dist/core/memory-link-repair.js +70 -0
- package/dist/core/memory-operations.js +122 -0
- package/dist/core/memory-pressure.js +212 -0
- package/dist/core/memory-sync-curator.js +82 -0
- package/dist/core/memory-sync.js +558 -0
- package/dist/core/memory-transformation-bundle.js +163 -0
- package/dist/core/memory-transformation-proposal.js +135 -0
- package/dist/core/memory-transformation.js +108 -0
- package/dist/core/memory.js +186 -0
- package/dist/core/migrations.js +569 -0
- package/dist/core/npm-update.js +289 -0
- package/dist/core/path-scope.js +27 -0
- package/dist/core/plugin-files.js +116 -0
- package/dist/core/project-root.js +39 -0
- package/dist/core/project.js +634 -0
- package/dist/core/promotion.js +279 -0
- package/dist/core/reconciliation.js +61 -0
- package/dist/core/secrets.js +67 -0
- package/dist/core/semantic-safety.js +33 -0
- package/dist/core/session-index.js +116 -0
- package/dist/core/session-review.js +150 -0
- package/dist/core/sessions.js +289 -0
- package/dist/core/settings.js +94 -0
- package/dist/core/skill-curation.js +239 -0
- package/dist/core/skill-polish.js +41 -0
- package/dist/core/skills.js +387 -0
- package/dist/core/storage.js +462 -0
- package/dist/core/sync-transaction.js +290 -0
- package/dist/core/telemetry.js +64 -0
- package/dist/core/templates.js +53 -0
- package/dist/core/types.js +2 -0
- package/dist/core/update.js +326 -0
- package/dist/curation-review-adapter.js +92 -0
- package/dist/diagnostic-boundary.js +55 -0
- package/dist/filesystem/permissions.js +39 -0
- package/dist/hosts/adapters.js +119 -0
- package/dist/hosts/files.js +133 -0
- package/dist/hosts/manager.js +150 -0
- package/dist/hosts/public-entry.js +47 -0
- package/dist/hosts/runtime-command.js +66 -0
- package/dist/hosts/runtime.js +139 -0
- package/dist/hosts/wizard.js +114 -0
- package/dist/knowledge-table.js +38 -0
- package/dist/mcp-development.js +69 -0
- package/dist/mcp-workspace.js +141 -0
- package/dist/memory-presentation.js +182 -0
- package/dist/memory-sync-operation.js +88 -0
- package/dist/plugins/api.js +2 -0
- package/dist/plugins/context.js +20 -0
- package/dist/plugins/mcp.js +72 -0
- package/dist/plugins/registry.js +106 -0
- package/dist/presentation-cards.js +32 -0
- package/dist/presentation-server.js +94 -0
- package/dist/presentation-trace.js +174 -0
- package/dist/presentation.js +443 -0
- package/dist/providers/mcp-sampling.js +45 -0
- package/dist/server.js +664 -0
- package/dist/settings-definition.js +22 -0
- package/dist/skill-instructions.js +2 -0
- package/dist/status-presentation.js +20 -0
- package/dist/text-layout.js +278 -0
- package/dist/update-command.js +53 -0
- package/dist/update-instructions.js +4 -0
- package/dist-ui/index.html +216 -0
- package/docs/APPLICABILITY.md +151 -0
- package/docs/COMPATIBILITY.md +68 -0
- package/docs/CONTEXT-SELECTION-V2.md +61 -0
- package/docs/CURATION-APP-REVIEW.md +156 -0
- package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
- package/docs/IMPORTER-CURATION.md +142 -0
- package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
- package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
- package/docs/MIGRATIONS.md +103 -0
- package/docs/PRIVACY.md +28 -0
- package/docs/RECOVERY.md +58 -0
- package/docs/RELEASING.md +98 -0
- package/docs/hosts.md +152 -0
- package/docs/images/prodero-context-ready-cropped.png +0 -0
- package/docs/images/prodero-overview-cropped.png +0 -0
- package/docs/images/prodero-session-learned-cropped.png +0 -0
- package/docs/plugins.md +83 -0
- package/docs/ui-themes.md +49 -0
- package/instructions/AGENTS.md +39 -0
- package/package.json +92 -4
- package/plugins/migrate_ai_studio/README.md +101 -0
- package/plugins/migrate_ai_studio/index.cjs +396 -0
- package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
- package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
- package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
- package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
- package/plugins/migrate_ai_studio/plugin.json +11 -0
- package/plugins/migrate_ai_studio/schema.cjs +20 -0
- package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
- package/prodero.release.json +5 -0
|
@@ -0,0 +1,340 @@
|
|
|
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, semanticAttemptInputHash, 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(), resumeForJobId: 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.reviewRevisionId !== input.reviewRevisionId || prior.questionId !== input.questionId || prior.questionVersion !== input.questionVersion || prior.choice?.id !== input.choiceId || prior.rawText !== input.rawText || prior.replacesAnswerId !== input.replacesAnswerId || prior.origin !== input.origin)
|
|
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
|
+
const passCount = (history, lineageId) => history.filter(j => j.input.lineageId === lineageId && j.input.reason !== 'validation_correction').length;
|
|
165
|
+
const passLimit = (history, lineageId, limits) => Math.min(limits.maxPasses, ...history.filter(j => j.input.lineageId === lineageId).map(j => j.input.bundle.limits.maxPasses));
|
|
166
|
+
function boundedRecoveryLimits(previous, requested) {
|
|
167
|
+
const limits = semanticLimitsSchema.parse({ ...previous.input.bundle.limits, ...requested });
|
|
168
|
+
return Object.fromEntries(Object.entries(limits).map(([key, value]) => [key, Math.min(value, previous.input.bundle.limits[key])]));
|
|
169
|
+
}
|
|
170
|
+
const semanticInputIdentity = (bundle) => hash([bundle.expectedSources, bundle.sources, bundle.evidence, bundle.answers, bundle.signals, bundle.missingFacts, bundle.completeness, SEMANTIC_CONTRACT, SEMANTIC_VALIDATOR]);
|
|
171
|
+
/** Retry affordances use the same persisted budgets as preparation. Expiry is
|
|
172
|
+
* independent of decision revision; neither may reset a correction allowance. */
|
|
173
|
+
export function semanticRetryState(job, review, history) {
|
|
174
|
+
const room = history.length < SEMANTIC_RETENTION.maxJobs, status = semanticJobStatus(job, review);
|
|
175
|
+
return { canRetry: room && status === 'rejected' && job.input.decisionRevision === review.state.revision && job.input.correction < job.input.bundle.limits.maxCorrections,
|
|
176
|
+
canResume: room && status === 'stale' && ['prepared', 'awaiting_reasoning', 'submitted', 'stale'].includes(last(job).status) && passCount(history, job.input.lineageId) < passLimit(history, job.input.lineageId, job.input.bundle.limits) };
|
|
177
|
+
}
|
|
178
|
+
export async function prepareSemanticCuration(root, raw) {
|
|
179
|
+
const input = semanticPrepareSchema.parse(raw);
|
|
180
|
+
return reviewLock(root, async () => {
|
|
181
|
+
const review = await current(root, input.reviewId, input.reviewRevisionId), history = await semanticJobs(root);
|
|
182
|
+
const targetId = input.reason === 'validation_correction' ? input.correctionForJobId : input.reason === 'resume' ? input.resumeForJobId : undefined;
|
|
183
|
+
const target = targetId ? history.find(j => j.input.jobId === targetId) : undefined;
|
|
184
|
+
if (targetId && (!target || target.input.lineageId !== review.snapshot.revision.lineageId || target.input.bundle.componentId !== input.componentId))
|
|
185
|
+
return reject('semantic_invalid', 'Recovery job does not belong to this component.');
|
|
186
|
+
let limits = semanticLimitsSchema.parse(input.limits ?? {});
|
|
187
|
+
if (target)
|
|
188
|
+
limits = boundedRecoveryLimits(target, input.limits);
|
|
189
|
+
let bundle = await makeBundle(root, review, input.componentId, limits), semanticInputHash = semanticInputIdentity(bundle);
|
|
190
|
+
const matches = history.filter(j => j.input.semanticInputHash === semanticInputHash && j.input.lineageId === bundle.lineageId && j.input.bundle.componentId === bundle.componentId && !history.some(next => next.input.previousJobId === j.input.jobId)).sort((a, b) => b.input.createdAt.localeCompare(a.input.createdAt));
|
|
191
|
+
let same = target ?? matches.find(j => j.input.reviewRevisionId === input.reviewRevisionId && j.input.decisionRevision === review.state.revision) ?? matches[0];
|
|
192
|
+
if (input.reason === 'resume' && !same) {
|
|
193
|
+
// Older callers have no explicit predecessor field. Resolve a unique leaf
|
|
194
|
+
// against full source/answer bindings, then inherit omitted input limits.
|
|
195
|
+
// A changed answer is a fresh analysis, not recovery of that old attempt.
|
|
196
|
+
const candidates = history.filter(j => j.input.lineageId === bundle.lineageId && j.input.bundle.componentId === input.componentId && ['prepared', 'awaiting_reasoning', 'submitted', 'stale'].includes(last(j).status) && !history.some(next => next.input.previousJobId === j.input.jobId) && hash(j.input.bundle.expectedSources) === hash(bundle.expectedSources) && j.input.bundle.answerLogHash === bundle.answerLogHash);
|
|
197
|
+
if (candidates.length > 1)
|
|
198
|
+
return reject('semantic_stale', 'Recovery is ambiguous. Reload the review and retry the exact analysis job.');
|
|
199
|
+
if (candidates.length) {
|
|
200
|
+
same = candidates[0];
|
|
201
|
+
limits = boundedRecoveryLimits(same, input.limits);
|
|
202
|
+
bundle = await makeBundle(root, review, input.componentId, limits);
|
|
203
|
+
semanticInputHash = semanticInputIdentity(bundle);
|
|
204
|
+
if (semanticInputHash !== same.input.semanticInputHash)
|
|
205
|
+
return reject('semantic_stale', 'Reasoning evidence changed. Prepare a fresh analysis.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const priorRequest = history.find(j => j.input.requestId === input.requestId && j.input.lineageId === bundle.lineageId);
|
|
209
|
+
if (priorRequest) {
|
|
210
|
+
if (priorRequest.input.semanticInputHash !== semanticInputHash || priorRequest.input.reviewRevisionId !== input.reviewRevisionId)
|
|
211
|
+
return reject('review_changed', 'Reasoning request changed.');
|
|
212
|
+
return response(priorRequest);
|
|
213
|
+
}
|
|
214
|
+
let correction = 0, previous;
|
|
215
|
+
if (input.reason === 'validation_correction') {
|
|
216
|
+
previous = history.find(j => j.input.jobId === input.correctionForJobId);
|
|
217
|
+
if (!previous || last(previous).status !== 'rejected' || previous.input.lineageId !== bundle.lineageId || previous.input.bundle.componentId !== bundle.componentId || previous.input.semanticInputHash !== semanticInputHash || previous.input.correction >= previous.input.bundle.limits.maxCorrections)
|
|
218
|
+
return reject('semantic_limit', 'No correction retry remains.');
|
|
219
|
+
if (previous.input.reviewRevisionId !== input.reviewRevisionId || previous.input.decisionRevision !== review.state.revision)
|
|
220
|
+
return reject('semantic_stale', 'Reasoning decisions changed. Reload this review.');
|
|
221
|
+
correction = previous.input.correction + 1;
|
|
222
|
+
const previousId = previous.input.jobId, retry = history.find(j => j.input.previousJobId === previousId);
|
|
223
|
+
if (retry)
|
|
224
|
+
return response(retry);
|
|
225
|
+
}
|
|
226
|
+
else if (same) {
|
|
227
|
+
if (target && same.input.semanticInputHash !== semanticInputHash)
|
|
228
|
+
return reject('semantic_stale', 'Reasoning evidence or answers changed. Prepare a fresh analysis.');
|
|
229
|
+
const successor = history.find(j => j.input.previousJobId === same.input.jobId);
|
|
230
|
+
if (target && successor)
|
|
231
|
+
return response(successor);
|
|
232
|
+
if (input.reason !== 'resume' || !['prepared', 'awaiting_reasoning', 'submitted', 'stale'].includes(last(same).status) || semanticJobStatus(same, review) !== 'stale')
|
|
233
|
+
return response(same);
|
|
234
|
+
previous = same;
|
|
235
|
+
correction = previous.input.correction;
|
|
236
|
+
}
|
|
237
|
+
if (previous) {
|
|
238
|
+
// Recovery may tighten limits but cannot silently expand the previous budget.
|
|
239
|
+
limits = boundedRecoveryLimits(previous, input.limits);
|
|
240
|
+
bundle = await makeBundle(root, review, input.componentId, limits);
|
|
241
|
+
semanticInputHash = semanticInputIdentity(bundle);
|
|
242
|
+
if (semanticInputHash !== previous.input.semanticInputHash)
|
|
243
|
+
return reject('semantic_stale', 'Reasoning evidence or answers changed. Prepare a fresh analysis.');
|
|
244
|
+
}
|
|
245
|
+
if (correction > limits.maxCorrections)
|
|
246
|
+
return reject('semantic_limit', 'No correction retry remains within the requested limits.');
|
|
247
|
+
if (history.length >= SEMANTIC_RETENTION.maxJobs || input.reason !== 'validation_correction' && passCount(history, bundle.lineageId) >= passLimit(history, bundle.lineageId, limits))
|
|
248
|
+
return reject('semantic_limit', 'Reasoning budget reached; pending history was preserved.');
|
|
249
|
+
// A TTL-only resume has identical sources/revision. Bind it to its predecessor
|
|
250
|
+
// so it gets a distinct immutable job instead of overwriting expired history.
|
|
251
|
+
const createdAt = new Date().toISOString(), inputHash = semanticAttemptInputHash(bundle, review.state.revision, correction, input.reason === 'resume' ? previous?.input.jobId : undefined), jobId = `semantic_job_${hash([bundle.lineageId, inputHash]).slice(0, 40)}`;
|
|
252
|
+
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, ...(previous ? { previousJobId: previous.input.jobId } : {}), ...(input.adapter ? { adapter: input.adapter } : {}) };
|
|
253
|
+
const job = { version: 1, input: body, inputSeal: hash(body), events: [] };
|
|
254
|
+
await event(root, job, 'prepared');
|
|
255
|
+
await event(root, job, 'awaiting_reasoning');
|
|
256
|
+
return response(job);
|
|
257
|
+
}, { recoverLifecycle: false, waitMs: 2000 });
|
|
258
|
+
}
|
|
259
|
+
export async function inspectSemanticCuration(root, jobId) { return response(await loadSemanticJob(root, jobId)); }
|
|
260
|
+
export async function submitSemanticCuration(root, input) {
|
|
261
|
+
return reviewLock(root, async () => {
|
|
262
|
+
const job = await loadSemanticJob(root, input.jobId), outputHash = hash(input.proposal), status = last(job).status;
|
|
263
|
+
if (input.inputHash !== job.input.inputHash)
|
|
264
|
+
return reject('semantic_invalid', 'Reasoning input hash changed.');
|
|
265
|
+
const submission = job.events.find(e => e.status === 'submitted');
|
|
266
|
+
if (submission && submission.outputHash !== outputHash)
|
|
267
|
+
return reject('review_changed', 'This job already has a different submission.');
|
|
268
|
+
if (['validated', 'rejected', 'stale', 'failed'].includes(status)) {
|
|
269
|
+
if (last(job).outputHash !== outputHash)
|
|
270
|
+
return reject('review_changed', 'This job already has a different terminal result.');
|
|
271
|
+
return response(job);
|
|
272
|
+
}
|
|
273
|
+
if (submission) {
|
|
274
|
+
const resultId = `review_${hash(['semantic', job.input.jobId]).slice(0, 40)}`;
|
|
275
|
+
const file = await checkedDataPath(root, 'local/curation-reviews', `${resultId}.json`);
|
|
276
|
+
if (await fs.stat(file).then(() => true, () => false)) {
|
|
277
|
+
const recovered = await loadCurationReview(root, resultId, { allowExpired: true }), typed = recovered.snapshot.revision?.typedPlan;
|
|
278
|
+
if (typed?.kind !== 'semantic-v2' || typed.analyses.at(-1)?.jobId !== job.input.jobId || typed.analyses.at(-1)?.outputHash !== outputHash)
|
|
279
|
+
return reject('semantic_invalid', 'Published semantic revision changed.');
|
|
280
|
+
await event(root, job, 'validated', { outputHash, resultReviewId: resultId, resultRevisionId: recovered.snapshot.revision.id, reason: 'recovered_staged_revision' });
|
|
281
|
+
return { ...response(job), review: recovered };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let review;
|
|
285
|
+
try {
|
|
286
|
+
review = await current(root, job.input.reviewId, job.input.reviewRevisionId, job.input.decisionRevision);
|
|
287
|
+
if (Date.now() > Date.parse(job.input.expiresAt) || hash(await curationAnswers(root, job.input.lineageId)) !== job.input.bundle.answerLogHash)
|
|
288
|
+
throw new Error('obsolete');
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
await event(root, job, 'stale', { reason: 'obsolete_revision_or_evidence', outputHash });
|
|
292
|
+
return response(job);
|
|
293
|
+
}
|
|
294
|
+
// Size-check before persisting an untrusted output. Store only its hash if over budget.
|
|
295
|
+
if (Buffer.byteLength((JSON.stringify(input.proposal) ?? 'null')) > job.input.bundle.limits.maxOutputBytes || measuredTokens(input.proposal) > job.input.bundle.limits.maxOutputTokens) {
|
|
296
|
+
await event(root, job, 'rejected', { reason: 'output_limit', outputHash });
|
|
297
|
+
return response(job);
|
|
298
|
+
}
|
|
299
|
+
if (!submission)
|
|
300
|
+
await event(root, job, 'submitted', { outputHash, proposal: input.proposal });
|
|
301
|
+
try {
|
|
302
|
+
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, ...(job.input.reason === 'resume' && job.input.previousJobId ? { resumedFromJobId: job.input.previousJobId } : {}), bundle: job.input.bundle, proposal }, previous = review.snapshot.revision.typedPlan;
|
|
303
|
+
const analyses = [...(previous.kind === 'semantic-v2' ? previous.analyses : []), accepted], basePlan = deterministicBaseline(review.snapshot.revision), plan = materializeSemanticPlan(basePlan, analyses);
|
|
304
|
+
const next = await publishSemanticReview(root, review, { kind: 'semantic-v2', validator: SEMANTIC_VALIDATOR, basePlan, analyses, plan }, job.input.jobId);
|
|
305
|
+
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' });
|
|
306
|
+
return { ...response(job), review: next };
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (error instanceof SemanticValidationError || error instanceof z.ZodError) {
|
|
310
|
+
await event(root, job, 'rejected', { reason: error instanceof SemanticValidationError ? error.reason : 'malformed_output', outputHash });
|
|
311
|
+
return response(job);
|
|
312
|
+
}
|
|
313
|
+
if (error instanceof CurationReviewError && ['review_retention_full', 'review_too_large', 'semantic_limit'].includes(error.code)) {
|
|
314
|
+
await event(root, job, 'failed', { reason: 'staging_capacity', outputHash });
|
|
315
|
+
return response(job);
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}, { recoverLifecycle: false, waitMs: 2000 });
|
|
320
|
+
}
|
|
321
|
+
export function semanticJobStatus(job, review) { const status = last(job).status; return ['prepared', 'awaiting_reasoning', 'submitted'].includes(status) && (job.input.reviewRevisionId !== review.snapshot.revision?.id || job.input.decisionRevision !== review.state.revision || Date.now() > Date.parse(job.input.expiresAt)) ? 'stale' : status; }
|
|
322
|
+
/** Unchanged supporting records must not keep a decided component's job active. */
|
|
323
|
+
export function semanticJobSuspended(job, review) {
|
|
324
|
+
const sourceIds = new Set(job.input.bundle.dependencies.groups.flatMap(g => g.proposalIds));
|
|
325
|
+
const affected = review.snapshot.units.filter(u => u.proposalIds.some(id => sourceIds.has(id))).map(u => review.state.items[u.id]);
|
|
326
|
+
const work = affected.filter(item => item.outcome !== 'unchanged');
|
|
327
|
+
return (work.length ? work : affected).every(item => item.execution === 'applied' || ['deferred', 'keep_current'].includes(item.disposition ?? ''));
|
|
328
|
+
}
|
|
329
|
+
export async function semanticReviewStatus(root, review) {
|
|
330
|
+
const history = (await semanticJobs(root)).filter(j => j.input.reviewRevisionId === review.snapshot.revision?.id);
|
|
331
|
+
const active = history.filter(j => !history.some(next => next.input.previousJobId === j.input.jobId) && !semanticJobSuspended(j, review));
|
|
332
|
+
const pending = active.filter(j => ['prepared', 'awaiting_reasoning', 'submitted'].includes(semanticJobStatus(j, review))), failed = active.filter(j => ['failed', 'rejected', 'stale'].includes(semanticJobStatus(j, review)));
|
|
333
|
+
const revision = review.snapshot.revision, issues = revision ? revisionIssues(revision) : [], answers = revision ? await curationAnswers(root, revision.lineageId) : [];
|
|
334
|
+
const bound = new Set(revision?.answerBindings.map(a => a.answerEventId) ?? []);
|
|
335
|
+
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));
|
|
336
|
+
const answerGroups = revision?.dependencies.groups.filter(g => awaiting.some(a => a.sourceIds.some(id => g.proposalIds.includes(id)))).map(g => g.id) ?? [];
|
|
337
|
+
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))])] };
|
|
338
|
+
}
|
|
339
|
+
export async function semanticReferencedReviews(root) { return new Set((await semanticJobs(root)).map(j => j.input.reviewId)); }
|
|
340
|
+
//# 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
|
package/dist/core/ids.js
ADDED
|
@@ -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
|