skillfid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dataset.js ADDED
@@ -0,0 +1,486 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { access, mkdir, rename, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ import { buildCalibrationPrompt, parseCalibrationResponse } from './calibration.js';
6
+ import { AsyncLimiter, limitRunner } from './concurrency.js';
7
+ import { CopilotSdkRunner } from './copilot-sdk.js';
8
+ import { corpusRevision, loadCorpus, splitDocument } from './corpus.js';
9
+ import { readJson, readJsonl, writeJson, writeJsonl } from './files.js';
10
+ import { buildInventoryPrompt, parseInventoryResponse } from './inventory.js';
11
+ import { stableStringify } from './json.js';
12
+ import { operationId, OperationJournal } from './journal.js';
13
+ import { ORACLE_PROMPT_VERSION, oraclePrompt } from './prompts.js';
14
+ import { buildQuestionPrompt, parseQuestionResponse } from './questions.js';
15
+ import { runStructured } from './structured.js';
16
+
17
+ export const DATASET_SCHEMA_VERSION = 6;
18
+ const DATASET_PIPELINE_VERSION = '0.4.1';
19
+ const RECALIBRATION_PIPELINE_VERSION = '0.5.7';
20
+ const JUDGE_CONSENSUS_POLICY_VERSION = '1';
21
+ const INITIAL_JUDGMENTS = 3;
22
+ const DISAGREEMENT_JUDGMENTS = 2;
23
+ const SUPERMAJORITY_VOTES = 4;
24
+ export class DatasetBuildError extends Error {}
25
+
26
+ export async function recalibrateDataset({ datasetPath, outputRoot = 'datasets', workRoot = '.work/recalibrate', options = {}, runner, judgeRunner, progress }) {
27
+ const settings = {
28
+ model: undefined, judgeModel: undefined, reasoningEffort: undefined, timeoutSeconds: 600,
29
+ timeoutRetries: 1, maxAttempts: 3, concurrency: 10, resume: true, ...options,
30
+ };
31
+ progress?.({ type: 'start', workflow: 'recalibration', title: 'Recalibrating dataset', current: 'Verifying source dataset · extraction will not run' });
32
+ const dataset = await loadDataset(datasetPath);
33
+ settings.model ??= dataset.manifest.calibration.model;
34
+ settings.judgeModel ??= dataset.manifest.calibration.judgeModel;
35
+ settings.reasoningEffort ??= dataset.manifest.calibration.reasoningEffort;
36
+ const sourceRoot = path.resolve(datasetPath);
37
+ const [documents, knowledge, evidence, coverage, audit, verifications] = await Promise.all([
38
+ readJsonl(path.join(sourceRoot, 'documents.jsonl')),
39
+ readJsonl(path.join(sourceRoot, 'knowledge.jsonl')),
40
+ readJsonl(path.join(sourceRoot, 'evidence.jsonl')),
41
+ readJson(path.join(sourceRoot, 'coverage.json')),
42
+ readJson(path.join(sourceRoot, 'audit.json')),
43
+ readJson(path.join(sourceRoot, 'verifications.json')),
44
+ ]);
45
+ const generatedRunner = !runner;
46
+ const generatedJudge = !runner && !judgeRunner;
47
+ const baseRunner = runner ?? new CopilotSdkRunner({ model: settings.model, reasoningEffort: settings.reasoningEffort, timeoutSeconds: settings.timeoutSeconds, maxTimeoutRetries: settings.timeoutRetries, isolatedHome: path.join(workRoot, 'home', 'subject'), progress });
48
+ const baseJudge = judgeRunner ?? runner ?? new CopilotSdkRunner({ model: settings.judgeModel, reasoningEffort: settings.reasoningEffort, timeoutSeconds: settings.timeoutSeconds, maxTimeoutRetries: settings.timeoutRetries, isolatedHome: path.join(workRoot, 'home', 'judge'), progress });
49
+ const copilotVersion = await baseRunner.version();
50
+ progress?.({ type: 'update', workflow: 'recalibration', current: `Running oracle and judge · Copilot CLI ${dataset.manifest.copilotCliVersion} → ${copilotVersion}`, progress: { done: 0, total: dataset.questions.length, label: 'existing questions' } });
51
+ const operationInputs = { sourceDatasetId: dataset.datasetId, pipelineVersion: RECALIBRATION_PIPELINE_VERSION, oraclePromptVersion: ORACLE_PROMPT_VERSION, judgeConsensusPolicyVersion: JUDGE_CONSENSUS_POLICY_VERSION, copilotVersion, model: settings.model, judgeModel: settings.judgeModel, reasoningEffort: settings.reasoningEffort, maxAttempts: settings.maxAttempts };
52
+ const baseOperationId = operationId('recalibration', operationInputs);
53
+ const journalPath = path.resolve(workRoot, 'operations.sqlite');
54
+ const journal = await OperationJournal.open(journalPath);
55
+ const activeOperationId = settings.resume ? journal.findResumableOperation('recalibration', operationInputs)?.operationId ?? baseOperationId : `${baseOperationId}_${randomUUID().slice(0, 8)}`;
56
+ journal.startOperation({ operationId: activeOperationId, kind: 'recalibration', inputs: operationInputs, config: { concurrency: settings.concurrency, timeoutRetries: settings.timeoutRetries, fresh: !settings.resume } });
57
+ progress?.({ type: 'operation', workflow: 'recalibration', message: `Recalibration operation ${activeOperationId}; concurrency ${settings.concurrency}; journal ${journalPath}`, details: { operationId: activeOperationId, concurrency: settings.concurrency, journalPath } });
58
+ const evidenceById = new Map(evidence.map((item) => [item.evidenceId, item]));
59
+ const documentById = new Map(documents.map((item) => [item.documentId, item.content]));
60
+ const previousCalibration = new Map(dataset.calibrations.map((item) => [item.testId, item]));
61
+ const limiter = new AsyncLimiter(settings.concurrency);
62
+ let completed = 0;
63
+ try {
64
+ const tasks = dataset.questions.map((question) => limiter.run(async () => {
65
+ const documentIds = [...new Set(question.evidenceIds.map((id) => evidenceById.get(id).documentId))].sort();
66
+ const sources = documentIds.map((id) => documentById.get(id));
67
+ const job = journal.ensureJob({ operationId: activeOperationId, stage: 'calibration', entityId: question.testId, inputs: { question, sources, model: settings.model, judgeModel: settings.judgeModel, reasoningEffort: settings.reasoningEffort, pipelineVersion: RECALIBRATION_PIPELINE_VERSION } });
68
+ if (job.status === 'completed') {
69
+ completed += 1;
70
+ progress?.({ type: 'update', workflow: 'recalibration', current: 'Restoring completed calibration from this operation', progress: { done: completed, total: dataset.questions.length, label: 'existing questions' } });
71
+ return job.output;
72
+ }
73
+ const workerId = randomUUID();
74
+ const claimedJob = journal.claimJob(job.jobId, workerId);
75
+ if (!claimedJob) throw new DatasetBuildError(`Could not claim calibration job ${question.testId}`);
76
+ try {
77
+ const workspace = path.join(workRoot, 'questions', question.testId);
78
+ await mkdir(workspace, { recursive: true });
79
+ const calibration = await recalibrateQuestion({ runner: baseRunner, judgeRunner: baseJudge, workspace, question, sources, maxAttempts: settings.maxAttempts, generationAttempt: previousCalibration.get(question.testId)?.generationAttempt ?? 1, oracleAttempt: claimedJob.attempts });
80
+ const output = { ...calibration, runtime: { copilotCliVersion: copilotVersion, model: settings.model, judgeModel: settings.judgeModel, reasoningEffort: settings.reasoningEffort, oraclePromptVersion: ORACLE_PROMPT_VERSION } };
81
+ if (output.judgeConsensus?.verdict !== 'pass' || output.score !== 1 || output.integrity?.passed !== true) {
82
+ const failedCriteria = output.criterionResults.filter((result) => result.score !== 1).map((result) => result.criterionIndex);
83
+ const integrity = Object.fromEntries(['directlyEntailed', 'contradictionChecked', 'qualificationsIncluded', 'authorityResolved', 'proxyAnswer', 'passed'].map((field) => [field, output.integrity?.[field]]));
84
+ const failedRationales = output.criterionResults.filter((result) => result.score !== 1).map((result) => `${result.criterionIndex}:${result.rationale}`);
85
+ throw new DatasetBuildError(`Question ${question.testId} did not retain stable perfect oracle calibration: consensus=${output.judgeConsensus?.verdict}; votes=${output.judgeConsensus?.passVotes}/${output.judgeConsensus?.totalJudgments}; score=${output.score}; failedCriteria=${failedCriteria.join(',') || 'none'}; criterionRationales=${stableStringify(failedRationales)}; integrity=${stableStringify(integrity)}; integrityRationale=${JSON.stringify(output.integrity?.rationale)}`);
86
+ }
87
+ journal.completeJob(job.jobId, workerId, output);
88
+ completed += 1;
89
+ progress?.({ type: 'update', workflow: 'recalibration', current: 'Checking existing questions against the new runtime', progress: { done: completed, total: dataset.questions.length, label: 'existing questions' } });
90
+ return output;
91
+ } catch (error) {
92
+ journal.failJob(job.jobId, workerId, error);
93
+ throw error;
94
+ }
95
+ }));
96
+ const calibrations = await settleAll(tasks);
97
+ journal.assertAllJobsCompleted(activeOperationId);
98
+ const destination = await publishDataset({
99
+ outputRoot, documents, sections: Array.from({ length: coverage.sections }), items: knowledge, evidence,
100
+ questions: dataset.questions, verifications, calibrations: Object.fromEntries(calibrations.map((item) => [item.testId, item])),
101
+ audits: audit.sections, options: settings, copilotVersion, sourceDatasetId: dataset.datasetId,
102
+ });
103
+ journal.completeOperation(activeOperationId, destination);
104
+ progress?.({ type: 'complete', workflow: 'recalibration', title: 'Dataset recalibrated without extraction', summary: [`${dataset.questions.length} existing questions passed oracle calibration`, `Copilot CLI ${dataset.manifest.copilotCliVersion} → ${copilotVersion}`, `New dataset ${path.basename(destination)} · source ${dataset.datasetId}`], output: destination });
105
+ return destination;
106
+ } finally {
107
+ journal.close();
108
+ if (generatedRunner) await baseRunner.close();
109
+ if (generatedJudge) await baseJudge.close();
110
+ await rm(path.join(workRoot, 'questions'), { recursive: true, force: true });
111
+ if (generatedRunner) await rm(path.join(workRoot, 'home'), { recursive: true, force: true });
112
+ }
113
+ }
114
+
115
+ export async function buildDataset({ corpusPath, outputRoot = 'datasets', workRoot = '.work/dataset', options = {}, runner, judgeRunner, progress }) {
116
+ const settings = {
117
+ model: 'gpt-5.6-sol', judgeModel: undefined, reasoningEffort: 'medium', maxAiCredits: 100,
118
+ timeoutSeconds: 600, timeoutRetries: 1, maxAttempts: 3, cleanResidualPasses: 1, concurrency: 10, resume: true,
119
+ maxResidualPasses: 3, maxSectionChars: 12_000, ...options,
120
+ };
121
+ settings.judgeModel ??= settings.model;
122
+ progress?.({ type: 'start', workflow: 'dataset', title: 'Building dataset', current: 'Reading Markdown corpus', progress: { done: 0, total: 0, label: 'knowledge items covered' } });
123
+ const documents = await loadCorpus(corpusPath);
124
+ const documentById = new Map(documents.map((document) => [document.documentId, document]));
125
+ const sections = documents.flatMap((document) => splitDocument(document, settings.maxSectionChars));
126
+ if (!sections.length) throw new DatasetBuildError('Corpus has no non-empty sections');
127
+ const dashboard = { documents: documents.length, sections: sections.length, completed: 0, running: 0, resumed: 0, reusedCovered: 0, sectionItems: new Map(), sectionCovered: new Map(), sectionGenerated: new Map(), sectionCalibrated: new Map() };
128
+ progress?.({ type: 'update', workflow: 'dataset', title: 'Building dataset', current: `Preparing ${formatCount(sections.length, 'section')}`, progress: datasetProgress(dashboard) });
129
+ const generatedRunner = !runner;
130
+ const generatedJudge = !runner && !judgeRunner;
131
+ const limiter = new AsyncLimiter(settings.concurrency);
132
+ const baseRunner = runner ?? new CopilotSdkRunner({ model: settings.model, reasoningEffort: settings.reasoningEffort, timeoutSeconds: settings.timeoutSeconds, maxTimeoutRetries: settings.timeoutRetries, isolatedHome: path.join(workRoot, 'home', 'subject'), progress });
133
+ const activeRunner = limitRunner(baseRunner, limiter);
134
+ const workspace = path.join(workRoot, 'generation');
135
+ await mkdir(workspace, { recursive: true });
136
+ const allItems = [];
137
+ const allEvidence = [];
138
+ const allQuestions = [];
139
+ const verifications = {};
140
+ const calibrations = {};
141
+ const audits = [];
142
+ const baseJudge = judgeRunner ?? runner ?? new CopilotSdkRunner({ model: settings.judgeModel, reasoningEffort: settings.reasoningEffort, timeoutSeconds: settings.timeoutSeconds, maxTimeoutRetries: settings.timeoutRetries, isolatedHome: path.join(workRoot, 'home', 'judge'), progress });
143
+ const activeJudge = baseJudge === baseRunner ? activeRunner : limitRunner(baseJudge, limiter);
144
+ const copilotVersion = await activeRunner.version();
145
+ const operationInputs = { corpusRevision: corpusRevision(documents), pipelineVersion: DATASET_PIPELINE_VERSION, judgeConsensusPolicyVersion: JUDGE_CONSENSUS_POLICY_VERSION, copilotVersion, model: settings.model, judgeModel: settings.judgeModel, reasoningEffort: settings.reasoningEffort, maxAttempts: settings.maxAttempts, cleanResidualPasses: settings.cleanResidualPasses, maxResidualPasses: settings.maxResidualPasses, maxSectionChars: settings.maxSectionChars };
146
+ const baseOperationId = operationId('dataset', operationInputs);
147
+ const journalPath = path.resolve(workRoot, 'operations.sqlite');
148
+ const journal = await OperationJournal.open(journalPath);
149
+ const activeOperationId = settings.resume ? journal.findResumableOperation('dataset', operationInputs)?.operationId ?? baseOperationId : `${baseOperationId}_${randomUUID().slice(0, 8)}`;
150
+ journal.startOperation({ operationId: activeOperationId, kind: 'dataset', inputs: operationInputs, config: { concurrency: settings.concurrency, timeoutRetries: settings.timeoutRetries, fresh: !settings.resume } });
151
+ progress?.({ type: 'operation', workflow: 'dataset', message: `Dataset operation ${activeOperationId}; concurrency ${settings.concurrency}; journal ${journalPath}`, details: { operationId: activeOperationId, concurrency: settings.concurrency, journalPath } });
152
+ try {
153
+ const sectionTasks = sections.map(async (section, sectionIndex) => {
154
+ const document = documentById.get(section.documentId);
155
+ const job = journal.ensureJob({ operationId: activeOperationId, stage: 'section', entityId: section.sectionId, inputs: { section, documentRevision: document.revision, pipelineVersion: DATASET_PIPELINE_VERSION } });
156
+ if (job.status === 'completed') {
157
+ dashboard.completed += 1; dashboard.resumed += 1; dashboard.reusedCovered += job.output.items.length;
158
+ updateSectionResults(dashboard, section.sectionId, { items: job.output.items.length, covered: job.output.items.length, generated: job.output.questions.length, calibrated: job.output.questions.length });
159
+ progress?.({ type: 'update', workflow: 'dataset', current: `Reusing section ${sectionIndex + 1} of ${sections.length}`, progress: datasetProgress(dashboard) });
160
+ return job.output;
161
+ }
162
+ const workerId = randomUUID();
163
+ if (!journal.claimJob(job.jobId, workerId)) throw new DatasetBuildError(`Could not claim section job ${section.sectionId}`);
164
+ const lease = setInterval(() => journal.renewLease(job.jobId, workerId), 5 * 60 * 1000);
165
+ lease.unref();
166
+ dashboard.running += 1;
167
+ progress?.({ type: 'update', workflow: 'dataset', current: `Inventorying section ${sectionIndex + 1} of ${sections.length}`, progress: datasetProgress(dashboard) });
168
+ try {
169
+ const output = await processSection({ section, sectionIndex, sectionCount: sections.length, document, runner: activeRunner, judgeRunner: activeJudge, workspace: path.join(workspace, section.sectionId), settings, progress, onResults: (results) => {
170
+ updateSectionResults(dashboard, section.sectionId, results);
171
+ progress?.({ type: 'update', workflow: 'dataset', progress: datasetProgress(dashboard) });
172
+ } });
173
+ journal.completeJob(job.jobId, workerId, output);
174
+ dashboard.running -= 1; dashboard.completed += 1;
175
+ progress?.({ type: 'update', workflow: 'dataset', current: `Finalized section ${sectionIndex + 1} of ${sections.length}`, progress: datasetProgress(dashboard) });
176
+ return output;
177
+ } catch (error) {
178
+ dashboard.running -= 1;
179
+ journal.failJob(job.jobId, workerId, error);
180
+ throw error;
181
+ } finally {
182
+ clearInterval(lease);
183
+ }
184
+ });
185
+ const settled = await Promise.allSettled(sectionTasks);
186
+ const failure = settled.find((result) => result.status === 'rejected');
187
+ if (failure) { progress?.({ type: 'error', workflow: 'dataset', message: failure.reason.message, progress: datasetProgress(dashboard) }); throw failure.reason; }
188
+ for (const result of settled) {
189
+ const output = result.value;
190
+ allItems.push(...output.items);
191
+ allEvidence.push(...output.evidence);
192
+ allQuestions.push(...output.questions);
193
+ audits.push(output.audit);
194
+ Object.assign(verifications, output.verifications);
195
+ Object.assign(calibrations, output.calibrations);
196
+ }
197
+ assertCompleteCoverage(allItems, allQuestions);
198
+ journal.assertAllJobsCompleted(activeOperationId);
199
+ const finalCounts = journal.jobCounts(activeOperationId);
200
+ progress?.({ type: 'checkpoint', workflow: 'dataset', current: 'Publishing immutable dataset', progress: datasetProgress(dashboard), details: { jobs: finalCounts } });
201
+ const destination = await publishDataset({ outputRoot, documents, sections, items: allItems, evidence: deduplicateEvidence(allEvidence), questions: allQuestions, verifications, calibrations, audits, options: settings, copilotVersion });
202
+ journal.completeOperation(activeOperationId, destination);
203
+ progress?.({ type: 'complete', workflow: 'dataset', title: 'Dataset built', summary: [`${formatCount(documents.length, 'document')}, ${formatCount(sections.length, 'section')}`, `${formatCount(allItems.length, 'knowledge item')}, ${formatCount(allQuestions.length, 'question')}`, 'Coverage 100%, calibration passed'], details: { destination, operationId: activeOperationId } });
204
+ return destination;
205
+ } finally {
206
+ journal.close();
207
+ if (generatedRunner) await baseRunner.close();
208
+ if (generatedJudge) await baseJudge.close();
209
+ await rm(workspace, { recursive: true, force: true });
210
+ if (generatedRunner) await rm(path.join(workRoot, 'home'), { recursive: true, force: true });
211
+ }
212
+ }
213
+
214
+ async function processSection({ section, sectionIndex, sectionCount, document, runner, judgeRunner, workspace, settings, progress, onResults }) {
215
+ await mkdir(workspace, { recursive: true });
216
+ sectionProgress(progress, 'Inventorying knowledge', sectionIndex, sectionCount);
217
+ const initial = await extractInventory(runner, workspace, section, document, settings, []);
218
+ let items = [...initial.items];
219
+ let evidence = [...initial.evidence];
220
+ onResults?.({ items: items.length, covered: 0 });
221
+ let cleanPasses = 0;
222
+ let residualPass = 0;
223
+ let residualRecovered = false;
224
+ let lastResidual;
225
+ while (cleanPasses < settings.cleanResidualPasses) {
226
+ residualPass += 1;
227
+ if (residualPass > settings.maxResidualPasses) throw new DatasetBuildError(`Inventory did not converge for section ${section.sectionId}`);
228
+ sectionProgress(progress, 'Checking knowledge coverage', sectionIndex, sectionCount);
229
+ const residual = await extractInventory(runner, workspace, section, document, settings, items);
230
+ lastResidual = residual;
231
+ const known = new Set(items.map((item) => item.knowledgeId));
232
+ const additions = residual.items.filter((item) => !known.has(item.knowledgeId));
233
+ if (additions.length) {
234
+ residualRecovered = true;
235
+ const evidenceIds = new Set(additions.flatMap((item) => item.evidenceIds));
236
+ items.push(...additions);
237
+ evidence.push(...residual.evidence.filter((record) => evidenceIds.has(record.evidenceId)));
238
+ onResults?.({ items: items.length, covered: 0 });
239
+ cleanPasses = 0;
240
+ } else cleanPasses += 1;
241
+ }
242
+ sectionProgress(progress, 'Preparing questions', sectionIndex, sectionCount);
243
+ onResults?.({ items: items.length, covered: 0, generated: 0, calibrated: 0 });
244
+ let merged = { sectionId: section.sectionId, classification: items.length ? 'informational' : 'non_informational', reason: initial.reason, items, evidence: deduplicateEvidence(evidence) };
245
+ const recoveredKnowledgeIds = [];
246
+ let audit = lastResidual;
247
+ let converged = !residualRecovered;
248
+ for (let auditPass = 1; residualRecovered && auditPass <= settings.maxResidualPasses; auditPass += 1) {
249
+ sectionProgress(progress, 'Auditing completeness', sectionIndex, sectionCount);
250
+ audit = await extractInventory(runner, workspace, section, document, settings, merged.items);
251
+ const known = new Set(merged.items.map((item) => item.knowledgeId));
252
+ const missing = audit.items.filter((item) => !known.has(item.knowledgeId));
253
+ if (!missing.length) { converged = true; break; }
254
+ recoveredKnowledgeIds.push(...missing.map((item) => item.knowledgeId));
255
+ const evidenceIds = new Set(missing.flatMap((item) => item.evidenceIds));
256
+ merged = { ...merged, classification: 'informational', items: [...merged.items, ...missing], evidence: deduplicateEvidence([...merged.evidence, ...audit.evidence.filter((record) => evidenceIds.has(record.evidenceId))]) };
257
+ onResults?.({ items: merged.items.length, covered: 0 });
258
+ sectionProgress(progress, 'Updating knowledge inventory', sectionIndex, sectionCount);
259
+ }
260
+ if (!converged) throw new DatasetBuildError(`Completeness audit did not converge for section ${section.sectionId}`);
261
+ const sectionAudit = { sectionId: section.sectionId, passed: true, missingKnowledgeIds: [], recoveredKnowledgeIds, reason: audit.reason };
262
+ let generated = { questions: [], verifications: {}, calibrations: {} };
263
+ if (merged.items.length) {
264
+ generated = await generateVerifiedQuestions({ runner, judgeRunner, workspace, inventory: merged, options: settings, progress, sectionIndex: sectionIndex + 1, sectionCount, source: document.content, onResults });
265
+ }
266
+ return { items: merged.items, evidence: merged.evidence, audit: sectionAudit, ...generated };
267
+ }
268
+
269
+ async function extractInventory(runner, workspace, section, document, options, existingItems) {
270
+ return runStructured({ runner, workspace, prompt: buildInventoryPrompt(section, existingItems), validator: (response) => parseInventoryResponse(response, { section, document }), maxAttempts: options.maxAttempts });
271
+ }
272
+
273
+ async function generateVerifiedQuestions({ runner, judgeRunner, workspace, inventory, options, progress, sectionIndex, sectionCount, source, onResults }) {
274
+ const retained = [];
275
+ const verifications = {};
276
+ const calibrations = {};
277
+ let pendingItems = inventory.items;
278
+ let feedback = '';
279
+ let lastFailure = '';
280
+ let generatedCount = 0;
281
+ let calibratedCount = 0;
282
+ let generationAttempt = 0;
283
+ while (pendingItems.length) {
284
+ generationAttempt += 1;
285
+ const pendingEvidenceIds = new Set(pendingItems.flatMap((item) => item.evidenceIds));
286
+ const basePrompt = buildQuestionPrompt(pendingItems, inventory.evidence.filter((record) => pendingEvidenceIds.has(record.evidenceId)));
287
+ const prompt = feedback ? [basePrompt, '', 'The previous question set failed independent verification:', feedback, 'Generate replacement questions for all supplied knowledge IDs.', 'Correct every reported issue without changing the source facts.'].join('\n') : basePrompt;
288
+ sectionProgress(progress, 'Generating questions', sectionIndex - 1, sectionCount);
289
+ const questions = await runStructured({ runner, workspace, prompt, validator: (response) => parseQuestionResponse(response, pendingItems), maxAttempts: options.maxAttempts });
290
+ generatedCount += questions.length;
291
+ onResults?.({ items: inventory.items.length, generated: generatedCount, calibrated: calibratedCount });
292
+ sectionProgress(progress, 'Calibrating questions', sectionIndex - 1, sectionCount);
293
+ const outcomes = await Promise.all(questions.map(async (question, questionIndex) => {
294
+ const questionWorkspace = path.join(workspace, 'questions', question.testId);
295
+ await mkdir(questionWorkspace, { recursive: true });
296
+ progress?.({ type: 'update', workflow: 'dataset', current: `Calibrating question ${questionIndex + 1} of ${questions.length} · section ${sectionIndex} of ${sectionCount}` });
297
+ const calibration = await calibrateQuestion({ runner, judgeRunner, workspace: questionWorkspace, question, source, maxAttempts: options.maxAttempts, generationAttempt });
298
+ calibratedCount += 1;
299
+ onResults?.({ items: inventory.items.length, generated: generatedCount, calibrated: calibratedCount });
300
+ if (calibration.judgeConsensus.verdict !== 'pass' || calibration.score < 1 || !calibration.verification.passed || !calibration.integrity.passed) {
301
+ return { question, failure: `Question ${question.testId}: judge consensus ${calibration.judgeConsensus.verdict} (${calibration.judgeConsensus.passVotes}/${calibration.judgeConsensus.totalJudgments} pass); oracle calibration scored ${calibration.score.toFixed(3)}; verification passed: ${calibration.verification.passed}; integrity passed: ${calibration.integrity.passed}. ${calibration.verification.reason} ${calibration.integrity.rationale} ${calibration.criterionResults.filter((item) => item.score < 1).map((item) => item.rationale).join(' ')}` };
302
+ }
303
+ return { question, verification: calibration.verification, calibration };
304
+ }));
305
+ const failures = [];
306
+ for (const outcome of outcomes) {
307
+ if (outcome.failure) { failures.push(outcome.failure); continue; }
308
+ const { question, verification, calibration } = outcome;
309
+ retained.push(question);
310
+ verifications[question.testId] = verification;
311
+ calibrations[question.testId] = calibration;
312
+ }
313
+ const coveredIds = new Set(retained.flatMap((question) => question.rubric.flatMap((criterion) => criterion.knowledgeItemIds)));
314
+ onResults?.({ items: inventory.items.length, covered: coveredIds.size, generated: generatedCount, calibrated: calibratedCount });
315
+ pendingItems = inventory.items.filter((item) => !coveredIds.has(item.knowledgeId));
316
+ if (!pendingItems.length) return { questions: retained, verifications, calibrations };
317
+ lastFailure = failures.join('\n');
318
+ sectionProgress(progress, `Regenerating ${formatCount(pendingItems.length, 'unresolved item')} (attempt ${generationAttempt + 1})`, sectionIndex - 1, sectionCount);
319
+ feedback = lastFailure;
320
+ }
321
+ }
322
+
323
+ async function calibrateQuestion({ runner, judgeRunner, workspace, question, source, maxAttempts, generationAttempt }) {
324
+ const oracle = await runner.run(workspace, oraclePrompt(question.question, [source]));
325
+ const judged = await judgeWithConsensus({ judgeRunner, workspace: path.join(workspace, 'judgments'), question, sources: [source], answer: oracle.answer, maxAttempts });
326
+ return { testId: question.testId, answer: oracle.answer, ...judged, generationAttempt, oracleAttempts: 1 };
327
+ }
328
+
329
+ async function recalibrateQuestion({ runner, judgeRunner, workspace, question, sources, maxAttempts, generationAttempt, oracleAttempt }) {
330
+ await Promise.all([mkdir(path.join(workspace, 'oracle'), { recursive: true }), mkdir(path.join(workspace, 'judge'), { recursive: true })]);
331
+ const oracle = await runner.run(path.join(workspace, 'oracle'), oraclePrompt(question.question, sources, question.rubric));
332
+ const judged = await judgeWithConsensus({ judgeRunner, workspace: path.join(workspace, 'judge'), question, sources, answer: oracle.answer, maxAttempts });
333
+ return { testId: question.testId, answer: oracle.answer, ...judged, generationAttempt, oracleAttempts: oracleAttempt };
334
+ }
335
+
336
+ async function judgeWithConsensus({ judgeRunner, workspace, question, sources, answer, maxAttempts }) {
337
+ const prompt = buildCalibrationPrompt({ question: question.question, source: sources, candidateAnswer: answer, rubric: question.rubric });
338
+ const judgments = [];
339
+ const runJudgment = async () => {
340
+ const judgmentWorkspace = path.join(workspace, `judgment-${judgments.length + 1}`);
341
+ await mkdir(judgmentWorkspace, { recursive: true });
342
+ const judgment = await runStructured({ runner: judgeRunner, workspace: judgmentWorkspace, prompt, validator: (response) => parseCalibrationResponse(response, question.rubric.length), maxAttempts });
343
+ const score = judgment.criterionResults.reduce((sum, result) => sum + question.rubric[result.criterionIndex].weight * result.score, 0);
344
+ const passed = judgmentPassed({ ...judgment, score });
345
+ judgments.push({ ...judgment, score, passed });
346
+ };
347
+ for (let index = 0; index < INITIAL_JUDGMENTS; index += 1) await runJudgment();
348
+ if (!judgments.every((judgment) => judgment.passed === judgments[0].passed)) {
349
+ for (let index = 0; index < DISAGREEMENT_JUDGMENTS; index += 1) await runJudgment();
350
+ }
351
+ const passVotes = judgments.filter((judgment) => judgment.passed).length;
352
+ const failVotes = judgments.length - passVotes;
353
+ const verdict = judgments.length === INITIAL_JUDGMENTS
354
+ ? (passVotes === INITIAL_JUDGMENTS ? 'pass' : 'fail')
355
+ : (passVotes >= SUPERMAJORITY_VOTES ? 'pass' : failVotes >= SUPERMAJORITY_VOTES ? 'fail' : 'unstable');
356
+ const representative = judgments.find((judgment) => judgment.passed === (verdict === 'pass')) ?? judgments[0];
357
+ return {
358
+ criterionResults: representative.criterionResults,
359
+ unsupportedClaims: representative.unsupportedClaims,
360
+ verification: representative.verification,
361
+ integrity: representative.integrity,
362
+ score: representative.score,
363
+ judgments,
364
+ judgeConsensus: { policyVersion: JUDGE_CONSENSUS_POLICY_VERSION, verdict, passVotes, failVotes, totalJudgments: judgments.length },
365
+ };
366
+ }
367
+
368
+ async function settleAll(promises) {
369
+ const settled = await Promise.allSettled(promises);
370
+ const failure = settled.find((result) => result.status === 'rejected');
371
+ if (failure) throw failure.reason;
372
+ return settled.map((result) => result.value);
373
+ }
374
+
375
+ function assertCompleteCoverage(items, questions) {
376
+ const covered = new Set(questions.flatMap((question) => question.rubric.flatMap((criterion) => criterion.knowledgeItemIds)));
377
+ const missing = items.map((item) => item.knowledgeId).filter((id) => !covered.has(id)).sort();
378
+ if (missing.length) throw new DatasetBuildError(`Dataset does not cover accepted knowledge items: ${missing.join(', ')}`);
379
+ }
380
+
381
+ function datasetProgress(state) {
382
+ const items = [...state.sectionItems.values()].reduce((sum, count) => sum + count, 0);
383
+ const covered = [...state.sectionCovered.values()].reduce((sum, count) => sum + count, 0);
384
+ return { done: covered, total: items, etaDone: Math.max(0, covered - state.reusedCovered), label: 'knowledge items covered' };
385
+ }
386
+
387
+ function sectionProgress(progress, phase, sectionIndex, sectionCount) {
388
+ progress?.({ type: 'update', workflow: 'dataset', current: `${phase} · section ${sectionIndex + 1} of ${sectionCount}` });
389
+ }
390
+
391
+ function updateSectionResults(state, sectionId, { items, covered, generated, calibrated }) {
392
+ if (items !== undefined) state.sectionItems.set(sectionId, items);
393
+ if (covered !== undefined) state.sectionCovered.set(sectionId, covered);
394
+ if (generated !== undefined) state.sectionGenerated.set(sectionId, generated);
395
+ if (calibrated !== undefined) state.sectionCalibrated.set(sectionId, calibrated);
396
+ }
397
+
398
+ function formatCount(count, singular) {
399
+ return `${count} ${singular}${count === 1 ? '' : 's'}`;
400
+ }
401
+
402
+ async function publishDataset({ outputRoot, documents, sections, items, evidence, questions, verifications, calibrations, audits, options, copilotVersion, sourceDatasetId }) {
403
+ const sourceDocuments = documents.map(({ documentId, revision, content }) => ({ documentId, revision, content }));
404
+ const calibrationRecords = Object.values(calibrations).sort((a, b) => a.testId.localeCompare(b.testId, 'en'));
405
+ const canonicalValue = { corpusRevision: corpusRevision(documents), documents: sourceDocuments, knowledge: items, questions, evidence, calibrations: calibrationRecords };
406
+ const datasetId = `ds_${sha256(stableStringify(canonicalValue)).slice(0, 16)}`;
407
+ const destination = path.resolve(outputRoot, datasetId);
408
+ try { await access(destination); return destination; } catch {}
409
+ await mkdir(path.resolve(outputRoot), { recursive: true });
410
+ const staging = path.resolve(outputRoot, `.${datasetId}.building`);
411
+ await rm(staging, { recursive: true, force: true });
412
+ await mkdir(staging);
413
+ try {
414
+ await writeJsonl(path.join(staging, 'documents.jsonl'), sourceDocuments);
415
+ await writeJsonl(path.join(staging, 'knowledge.jsonl'), items);
416
+ await writeJsonl(path.join(staging, 'evidence.jsonl'), evidence);
417
+ await writeJsonl(path.join(staging, 'questions.jsonl'), questions);
418
+ await writeJsonl(path.join(staging, 'calibrations.jsonl'), calibrationRecords);
419
+ await writeJson(path.join(staging, 'coverage.json'), { acceptedItems: items.length, coveredItems: items.length, coverage: 1, sections: sections.length, informationalSections: new Set(items.map((item) => item.sectionId)).size });
420
+ await writeJson(path.join(staging, 'audit.json'), { sections: audits });
421
+ await writeJson(path.join(staging, 'verifications.json'), verifications);
422
+ await writeJson(path.join(staging, 'manifest.json'), { schemaVersion: DATASET_SCHEMA_VERSION, datasetId, createdAt: new Date().toISOString(), ...(sourceDatasetId ? { sourceDatasetId } : {}), corpusRevision: canonicalValue.corpusRevision, extractorVersion: '0.4.0', copilotCliVersion: copilotVersion, model: options.model, reasoningEffort: options.reasoningEffort, timeoutSeconds: options.timeoutSeconds, timeoutRetries: options.timeoutRetries, calibration: { model: options.model, judgeModel: options.judgeModel, reasoningEffort: options.reasoningEffort, oraclePromptVersion: ORACLE_PROMPT_VERSION, judgeConsensus: { policyVersion: JUDGE_CONSENSUS_POLICY_VERSION, initialJudgments: INITIAL_JUDGMENTS, additionalJudgmentsOnDisagreement: DISAGREEMENT_JUDGMENTS, supermajorityVotes: SUPERMAJORITY_VOTES }, requiredScore: 1 }, documentRevisions: Object.fromEntries(documents.map((document) => [document.documentId, document.revision])) });
423
+ await rename(staging, destination);
424
+ } catch (error) { await rm(staging, { recursive: true, force: true }); throw error; }
425
+ return destination;
426
+ }
427
+
428
+ export async function loadDataset(datasetPath, { progress } = {}) {
429
+ const root = path.resolve(datasetPath);
430
+ const manifest = await readJson(path.join(root, 'manifest.json'));
431
+ if (manifest.schemaVersion !== DATASET_SCHEMA_VERSION) throw new Error(`Dataset schema version ${DATASET_SCHEMA_VERSION} is required; rebuild the dataset with this JavaScript version`);
432
+ if (typeof manifest.datasetId !== 'string' || manifest.datasetId !== path.basename(root)) throw new Error('Dataset ID does not match its directory name');
433
+ progress?.({ done: 1, total: 7, current: 'Reading dataset records' });
434
+ const [knowledge, questions, evidence, documents, calibrations] = await Promise.all(['knowledge.jsonl', 'questions.jsonl', 'evidence.jsonl', 'documents.jsonl', 'calibrations.jsonl'].map((name) => readJsonl(path.join(root, name))));
435
+ progress?.({ done: 2, total: 7, current: 'Checking content hash' });
436
+ const canonical = stableStringify({ corpusRevision: manifest.corpusRevision, documents, knowledge, questions, evidence, calibrations });
437
+ const expectedId = `ds_${sha256(canonical).slice(0, 16)}`;
438
+ if (expectedId !== manifest.datasetId) throw new Error(`Dataset content hash mismatch: expected ${expectedId}, found ${manifest.datasetId}`);
439
+ progress?.({ done: 3, total: 7, current: 'Checking evidence references' });
440
+ const evidenceIds = new Set(evidence.map((item) => item.evidenceId));
441
+ const missingEvidence = [...new Set(questions.flatMap((question) => question.evidenceIds).filter((id) => !evidenceIds.has(id)))].sort();
442
+ if (missingEvidence.length) throw new Error(`Questions reference missing evidence: ${missingEvidence.join(', ')}`);
443
+ progress?.({ done: 4, total: 7, current: 'Checking source documents' });
444
+ const documentMap = Object.fromEntries(documents.map((document) => [document.documentId, document.content]));
445
+ const missingDocuments = [...new Set(evidence.map((item) => item.documentId).filter((id) => !(id in documentMap)))].sort();
446
+ if (missingDocuments.length) throw new Error(`Evidence references missing source documents: ${missingDocuments.join(', ')}`);
447
+ progress?.({ done: 5, total: 7, current: 'Checking inventory coverage' });
448
+ if ((await readJson(path.join(root, 'coverage.json'))).coverage !== 1) throw new Error('Dataset does not have 100% inventory coverage');
449
+ progress?.({ done: 6, total: 7, current: 'Checking oracle calibrations' });
450
+ const calibrationByTestId = new Map(calibrations.map((item) => [item.testId, item]));
451
+ const requiresConsensus = manifest.calibration?.judgeConsensus?.policyVersion !== undefined;
452
+ const invalidCalibrations = questions.filter((question) => {
453
+ const calibration = calibrationByTestId.get(question.testId);
454
+ return calibration?.score !== 1 || calibration?.integrity?.passed !== true || (requiresConsensus && !hasValidPassingConsensus(calibration, question));
455
+ }).map((question) => question.testId);
456
+ if (invalidCalibrations.length || calibrationByTestId.size !== questions.length) throw new Error(`Dataset does not have a perfect oracle calibration for every question: ${invalidCalibrations.join(', ')}`);
457
+ progress?.({ done: 7, total: 7, current: 'Integrity verified' });
458
+ return { datasetId: manifest.datasetId, questions, evidence, documents: documentMap, calibrations, manifest };
459
+ }
460
+
461
+ function hasValidPassingConsensus(calibration, question) {
462
+ const judgments = calibration?.judgments;
463
+ const consensus = calibration?.judgeConsensus;
464
+ if (!Array.isArray(judgments) || !consensus || consensus.policyVersion !== JUDGE_CONSENSUS_POLICY_VERSION || consensus.verdict !== 'pass') return false;
465
+ const validJudgments = judgments.every((judgment) => {
466
+ const score = judgment.criterionResults?.reduce((sum, result) => sum + question.rubric[result.criterionIndex].weight * result.score, 0);
467
+ return score === judgment.score && judgment.passed === judgmentPassed(judgment);
468
+ });
469
+ const passVotes = judgments.filter(judgmentPassed).length;
470
+ const validCount = judgments.length === INITIAL_JUDGMENTS || judgments.length === INITIAL_JUDGMENTS + DISAGREEMENT_JUDGMENTS;
471
+ const sufficientVotes = judgments.length === INITIAL_JUDGMENTS ? passVotes === INITIAL_JUDGMENTS : passVotes >= SUPERMAJORITY_VOTES;
472
+ const representative = { criterionResults: calibration.criterionResults, unsupportedClaims: calibration.unsupportedClaims, verification: calibration.verification, integrity: calibration.integrity, score: calibration.score, passed: true };
473
+ const representativeMatches = judgments.some((judgment) => stableStringify(judgment) === stableStringify(representative));
474
+ return validJudgments && validCount && sufficientVotes && representativeMatches && consensus.passVotes === passVotes && consensus.failVotes === judgments.length - passVotes && consensus.totalJudgments === judgments.length;
475
+ }
476
+
477
+ function judgmentPassed(judgment) {
478
+ return judgment.score === 1 && judgment.verification?.passed === true && judgment.integrity?.passed === true;
479
+ }
480
+
481
+ function deduplicateEvidence(records) {
482
+ return [...new Map(records.map((record) => [record.evidenceId, record])).values()].sort((a, b) => a.evidenceId.localeCompare(b.evidenceId, 'en'));
483
+ }
484
+
485
+ function sha256(value) { return createHash('sha256').update(value, 'utf8').digest('hex'); }
486
+ function formatJobCounts(counts) { return `completed ${counts.completed}, running ${counts.running}, pending ${counts.pending}, failed ${counts.failed}`; }
@@ -0,0 +1,40 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { stableStringify } from './json.js';
5
+ import { InvalidStructuredResponse, parseJsonObject } from './structured.js';
6
+
7
+ export async function buildSkillAuditPrompt({ skillPath, question, source }) {
8
+ const files = {};
9
+ for (const filePath of await allFiles(path.resolve(skillPath))) {
10
+ files[path.relative(skillPath, filePath).split(path.sep).join('/')] = await readFile(filePath, 'utf8');
11
+ }
12
+ return [
13
+ 'Audit whether the supplied skill contains the knowledge required to answer the question. Compare meaning, not exact wording. Return only JSON:',
14
+ '{"present":true,"complete":true,"contradictory":false,"files":["SKILL.md"],"rationale":"..."}',
15
+ 'present means at least some required knowledge exists. complete means all required knowledge exists. contradictory means the skill conflicts with the supplied source. files must name only supplied files containing relevant text.',
16
+ '',
17
+ 'INPUT:',
18
+ stableStringify({ question, source, skillFiles: files }),
19
+ ].join('\n');
20
+ }
21
+
22
+ export function parseSkillAudit(response) {
23
+ const data = parseJsonObject(response);
24
+ for (const field of ['present', 'complete', 'contradictory']) {
25
+ if (typeof data[field] !== 'boolean') throw new InvalidStructuredResponse(`${field} must be a boolean`);
26
+ }
27
+ if (!Array.isArray(data.files) || data.files.some((file) => typeof file !== 'string')) throw new InvalidStructuredResponse('files must be an array of strings');
28
+ if (typeof data.rationale !== 'string' || !data.rationale.trim()) throw new InvalidStructuredResponse('rationale must be a non-empty string');
29
+ return { present: data.present, complete: data.complete, contradictory: data.contradictory, files: [...new Set(data.files.filter((file) => file.trim()))].sort(), rationale: data.rationale.trim() };
30
+ }
31
+
32
+ async function allFiles(root) {
33
+ const files = [];
34
+ for (const entry of (await readdir(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name, 'en'))) {
35
+ const entryPath = path.join(root, entry.name);
36
+ if (entry.isDirectory()) files.push(...await allFiles(entryPath));
37
+ else if (entry.isFile()) files.push(entryPath);
38
+ }
39
+ return files;
40
+ }