Skip to content

Commit 5d46ac3

Browse files
fix: normalize provider artifacts and repair incomplete model bundles
1 parent 8cd7359 commit 5d46ac3

7 files changed

Lines changed: 351 additions & 157 deletions

File tree

global-template/docgen/lib/indexer.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import fs from 'node:fs';
22
import path from 'node:path';
33
import { DatabaseSync } from 'node:sqlite';
44
import { buildInventory } from './inventory.mjs';
5-
import { ensureDir, estimateTokens, now, projectPaths, readJson, sha256, stableHash } from './core.mjs';
5+
import { ensureDir, estimateTokens, now, projectPaths, readJson, sha256, stableHash, writeJson } from './core.mjs';
6+
import { normalizeSemanticDocument, semanticMetadata } from './semantic.mjs';
67

78
function lineNumber(text, offset) { return text.slice(0, offset).split('\n').length; }
89
function snippet(text, start, radius = 4) {
@@ -138,8 +139,8 @@ function walkItems(value, model, out, parent = '') {
138139
if (Array.isArray(value)) { for (const item of value) walkItems(item, model, out, parent); return; }
139140
if (!value || typeof value !== 'object') return;
140141
if (value.id || value.name || value.statement) {
141-
const semanticId = String(value.id ?? sha256(`${model}\0${parent}\0${JSON.stringify(value)}`).slice(0, 24)); const id = `${model}:${semanticId}`;
142-
out.push({ id, semanticId, model, kind: String(value.kind ?? parent ?? 'item'), name: String(value.name ?? value.title ?? semanticId), statement: String(value.statement ?? value.summary ?? value.description ?? ''), classification: String(value.classification ?? 'UNKNOWN'), confidence: Number(value.confidence ?? 0), evidence: value.evidence ?? [], payload: value });
142+
const semanticId = String(value.id ?? sha256(`${model}\0${parent}\0${JSON.stringify(value)}`).slice(0, 24)); const id = `${model}:${semanticId}`; const metadata = semanticMetadata(value);
143+
out.push({ id, semanticId, model, kind: String(value.kind ?? parent ?? 'item'), name: String(value.name ?? value.title ?? semanticId), statement: String(value.statement ?? value.summary ?? value.description ?? ''), classification: metadata.classification, confidence: metadata.confidence, evidence: metadata.evidence, payload: value });
143144
}
144145
for (const [key, child] of Object.entries(value)) if (!['evidence','sourceModelRefs'].includes(key)) walkItems(child, model, out, key);
145146
}
@@ -152,7 +153,7 @@ export function ingestModels(root) {
152153
try {
153154
db.exec('DELETE FROM model_items; DELETE FROM model_fts;');
154155
for (const name of files) {
155-
const model = path.basename(name, '.json'); const items = []; walkItems(readJson(path.join(paths.model, name), {}), model, items);
156+
const model = path.basename(name, '.json'); const file = path.join(paths.model, name); const document = readJson(file, {}); const before = stableHash(document); normalizeSemanticDocument(document); if (stableHash(document) !== before) writeJson(file, document); const items = []; walkItems(document, model, items);
156157
for (const item of items) { insert.run(item.id, item.semanticId, item.model, item.kind, item.name, item.statement, item.classification, item.confidence, JSON.stringify(item.evidence), JSON.stringify(item.payload), stableHash(item.payload)); insertFts.run(item.id, item.model, item.kind, item.name, item.statement); count++; }
157158
}
158159
db.exec('COMMIT');

global-template/docgen/lib/pipeline.mjs

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { budgetReport, runProvider } from './provider.mjs';
66
import { databaseStats, indexRepository, ingestModels } from './indexer.mjs';
77
import { auditRepository } from './quality.mjs';
88
import { ensureDir, fileSha256, kitVersion, loadConfig, now, projectPaths, readJson, rel, sha256, slug, sourceSnapshot, stableHash, updateStage, writeJson } from './core.mjs';
9+
import { evidenceFromAliases, normalizeSemanticDocument, normalizeSourceModelRefs, semanticMetadata } from './semantic.mjs';
910

1011
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
1112

@@ -42,30 +43,54 @@ export function index(root, options = {}) {
4243
catch (error) { failStage(root, 'index', error); throw error; }
4344
}
4445

46+
function bundleObject(bundle, name) {
47+
const containers = [bundle, bundle?.models, bundle?.model, bundle?.modules, bundle?.result].filter((value) => value && typeof value === 'object' && !Array.isArray(value));
48+
const expected = name.replace(/[^a-z0-9]/gi, '').toLowerCase();
49+
for (const container of containers) {
50+
for (const [key, value] of Object.entries(container)) {
51+
if (key.replace(/[^a-z0-9]/gi, '').toLowerCase() === expected && value && typeof value === 'object' && !Array.isArray(value)) return value;
52+
}
53+
}
54+
return null;
55+
}
4556
function splitBundle(root, bundle, names) {
46-
ensureDir(projectPaths(root).model);
57+
ensureDir(projectPaths(root).model); const missing = [];
4758
for (const name of names) {
48-
const value = bundle[name] ?? bundle[name.replaceAll('-', '')] ?? bundle[name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
49-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Model bundle is missing object: ${name}`);
50-
writeJson(modelPath(root, name), { schemaVersion: '2.0', generatedAt: now(), ...value });
59+
const value = bundleObject(bundle, name);
60+
if (!value) { missing.push(name); continue; }
61+
normalizeSemanticDocument(value); writeJson(modelPath(root, name), { schemaVersion: '2.0', generatedAt: now(), ...value });
5162
}
63+
return missing;
5264
}
5365

5466
async function synthesizeBundle(root, stage, names, query) {
5567
const paths = projectPaths(root); const context = compileContext(root, { stage, query, target: stage, metadata: { expectedModels: names } });
5668
const output = path.join(paths.model, `${stage}-bundle.json`); const inputHash = context.payload.inputHash; const outputs = names.map((name) => modelPath(root, name));
5769
if (stageCurrent(root, stage, inputHash, outputs)) return { skipped: true, inputHash };
5870
ensureDir(paths.model); updateStage(root, stage, 'running', { inputHash, contextId: context.payload.id });
59-
const body = prompt(root, stage === 'modelCore' ? 'model-core.md' : 'model-enterprise.md', { CONTEXT_PATH: rel(root, context.file), OUTPUT_PATH: rel(root, output), MODEL_NAMES: JSON.stringify(names) });
60-
const outputBefore = artifactStamp(output); let bundleWritten = false; const acceptBundle = () => {
61-
try { if (!artifactChanged(output, outputBefore)) return false; const bundle = validateJson(output); splitBundle(root, bundle, names); bundleWritten = true; return outputs.every((file) => fs.existsSync(file)); }
62-
catch { return false; }
63-
};
71+
const renderPrompt = (expected, target) => prompt(root, stage === 'modelCore' ? 'model-core.md' : 'model-enterprise.md', { CONTEXT_PATH: rel(root, context.file), OUTPUT_PATH: rel(root, target), MODEL_NAMES: JSON.stringify(expected) });
72+
async function requestBundle(expected, target, repair = false) {
73+
const before = artifactStamp(target); let accepted = false; let missing = expected;
74+
const accept = () => {
75+
try { if (!artifactChanged(target, before)) return false; const bundle = validateJson(target); missing = splitBundle(root, bundle, expected); accepted = missing.length === 0; return accepted; } catch { return false; }
76+
};
77+
const provider = await runProvider(root, { stage, target: repair ? `${stage}:repair:${expected.join(',')}` : stage, prompt: renderPrompt(expected, target), acceptArtifacts: accept });
78+
if (!accepted) {
79+
if (!artifactChanged(target, before)) throw new Error(`${stage}: provider completed without writing a fresh bundle artifact`);
80+
missing = splitBundle(root, validateJson(target), expected);
81+
}
82+
return { provider, missing };
83+
}
6484
try {
65-
const provider = await runProvider(root, { stage, target: stage, prompt: body, acceptArtifacts: acceptBundle });
66-
if (!bundleWritten) { if (!artifactChanged(output, outputBefore)) throw new Error(`${stage}: provider completed without writing a fresh bundle artifact`); const bundle = validateJson(output); splitBundle(root, bundle, names); }
67-
fs.rmSync(output, { force: true }); completeStage(root, stage, inputHash, { models: names, contextId: context.payload.id, contextTokens: context.payload.estimatedTokens, recovered: provider.recovered === true });
68-
return { skipped: false, recovered: provider.recovered === true, inputHash };
85+
const first = await requestBundle(names, output); let missing = first.missing; let recovered = first.provider.recovered === true;
86+
if (missing.length) {
87+
console.warn(`[docgen] ${stage} REPAIR | bundle omitted ${missing.join(', ')}; requesting only missing model object(s).`);
88+
const repairOutput = path.join(paths.model, `${stage}-repair-${sha256(missing.join('|')).slice(0, 10)}-bundle.json`);
89+
const repaired = await requestBundle(missing, repairOutput, true); recovered ||= repaired.provider.recovered === true; missing = repaired.missing; fs.rmSync(repairOutput, { force: true });
90+
}
91+
if (missing.length) throw new Error(`Model bundle is missing object(s) after targeted repair: ${missing.join(', ')}`);
92+
fs.rmSync(output, { force: true }); completeStage(root, stage, inputHash, { models: names, contextId: context.payload.id, contextTokens: context.payload.estimatedTokens, recovered });
93+
return { skipped: false, recovered, inputHash };
6994
} catch (error) { failStage(root, stage, error, { inputHash, contextId: context.payload.id }); throw error; }
7095
}
7196

@@ -149,17 +174,26 @@ function writeTrace(root, page, claims, inputHash, contextId = null) {
149174
}
150175
function renderDeterministic(root, page, kind, inputHash) {
151176
const data = referenceData(root, kind); const text = `${frontmatter(page)}# ${page.title}\n\n${page.summary}\n\n${markdownTable(data.headers, data.rows)}\n`; const file = path.join(root, page.path); ensureDir(path.dirname(file)); fs.writeFileSync(file, text);
152-
const claims = data.items.map((item, indexValue) => ({ id: `${page.id}:${item.id ?? indexValue + 1}`, section: page.title, statement: item.statement ?? item.summary ?? item.description ?? item.name ?? item.id, classification: String(item.classification ?? (item.evidence?.length ? 'FACT' : 'UNKNOWN')).toUpperCase(), confidence: Number(item.confidence ?? (item.evidence?.length ? 1 : 0)), evidence: item.evidence ?? [], sourceModelRefs: [`${data.model}:${item.id ?? indexValue + 1}`] }));
177+
const claims = data.items.map((item, indexValue) => { const metadata = semanticMetadata(item); return { id: `${page.id}:${item.id ?? indexValue + 1}`, section: page.title, statement: item.statement ?? item.summary ?? item.description ?? item.name ?? item.id, classification: metadata.classification, confidence: metadata.confidence, evidence: metadata.evidence, sourceModelRefs: [`${data.model}:${item.id ?? indexValue + 1}`] }; });
153178
writeTrace(root, page, claims, inputHash); return { items: data.items.length, hash: sha256(text) };
154179
}
155180
function pageInputHash(page, contextHash) { return stableHash({ page, contextHash }); }
156181
function finalizeProviderTrace(root, page, inputHash, contextId) {
157182
const file = tracePath(root, page); const trace = validateJson(file); if (!Array.isArray(trace.claims)) throw new Error(`${rel(root, file)} must contain claims[].`);
158-
const ids = new Set();
183+
const contextFile = path.join(projectPaths(root).context, 'generate', `${page.id.replace(/[^a-z0-9_.-]+/gi, '-')}.json`); const context = fs.existsSync(contextFile) ? readJson(contextFile, {}) : {};
184+
const modelItems = new Map(); const aliases = new Map(); const perModel = new Map();
185+
for (const item of context.modelItems ?? []) { modelItems.set(item.id, item); const ordinal = (perModel.get(item.model) ?? 0) + 1; perModel.set(item.model, ordinal); aliases.set(`${item.model}:${ordinal}`, item.id); }
186+
const dedupeEvidence = (entries) => { const seen = new Set(); return entries.filter((entry) => { const key = `${entry.path}\0${entry.startLine ?? ''}\0${entry.endLine ?? ''}`; if (seen.has(key)) return false; seen.add(key); return true; }); };
187+
const ids = new Set(); const normalizedClaims = [];
159188
for (const claim of trace.claims) {
160189
claim.id = String(claim.id ?? `${page.id}:claim-${ids.size + 1}`); if (ids.has(claim.id)) throw new Error(`${page.id}: duplicate claim id ${claim.id}`); ids.add(claim.id);
161-
claim.statement = String(claim.statement ?? '').trim(); claim.classification = String(claim.classification ?? 'UNKNOWN').toUpperCase(); claim.confidence = Number(claim.confidence ?? (claim.classification === 'FACT' ? 1 : 0)); claim.evidence = Array.isArray(claim.evidence) ? claim.evidence : []; claim.sourceModelRefs = Array.isArray(claim.sourceModelRefs) ? claim.sourceModelRefs : [];
190+
const requestedRefs = normalizeSourceModelRefs(claim.sourceModelRefs ?? claim.modelRefs).map((ref) => aliases.get(ref) ?? ref); const refs = requestedRefs.filter((ref) => modelItems.has(ref)); const referenced = refs.map((ref) => modelItems.get(ref)); const metadata = semanticMetadata(claim);
191+
const inherited = referenced.flatMap((item) => evidenceFromAliases(item)); const evidence = dedupeEvidence([...metadata.evidence, ...inherited]); const fallbackStatement = referenced.map((item) => String(item.statement ?? item.payload?.statement ?? item.name ?? '')).find(Boolean);
192+
claim.statement = String(claim.statement ?? fallbackStatement ?? '').trim(); claim.classification = metadata.requestedClassification === 'FACT' && evidence.length ? 'FACT' : metadata.classification; claim.confidence = claim.classification === 'FACT' ? Math.max(metadata.confidence, 0.8) : metadata.confidence; claim.evidence = evidence; claim.sourceModelRefs = refs;
193+
if (!claim.statement && !claim.evidence.length && !claim.sourceModelRefs.length) continue;
194+
normalizedClaims.push(claim);
162195
}
196+
trace.claims = normalizedClaims;
163197
trace.schemaVersion = '2.0'; trace.pageId = page.id; trace.pagePath = page.path; trace.pageHash = sha256(fs.readFileSync(path.join(root, page.path))); trace.inputHash = inputHash; trace.contextId = contextId; trace.generatedAt = now(); writeJson(file, trace); return trace;
164198
}
165199
function validatePage(root, page, inputHash = null) {
@@ -229,6 +263,7 @@ export async function generate(root) {
229263
}
230264
const context = compileContext(root, { stage: 'generate', target: page.id, query: page.query, maxTokens: config.context?.maxTokens?.generate ?? 30000, metadata: { page } }); const inputHash = pageInputHash(page, context.payload.inputHash);
231265
try {
266+
if (fs.existsSync(tracePath(root, page))) finalizeProviderTrace(root, page, inputHash, context.payload.id);
232267
const checked = validatePage(root, page, inputHash); updatePage(root, page.id, { status: 'completed', renderer: 'provider', inputHash, pageHash: checked.hash, contextId: context.payload.id, recovered: pageState(root, page.id).status !== 'completed', error: null }); reusedPages++; continue;
233268
} catch {}
234269
pending.push({ page, context, inputHash });

0 commit comments

Comments
 (0)