@@ -6,6 +6,7 @@ import { budgetReport, runProvider } from './provider.mjs';
66import { databaseStats , indexRepository , ingestModels } from './indexer.mjs' ;
77import { auditRepository } from './quality.mjs' ;
88import { 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
1011const 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 - z 0 - 9 ] / gi, '' ) . toLowerCase ( ) ;
49+ for ( const container of containers ) {
50+ for ( const [ key , value ] of Object . entries ( container ) ) {
51+ if ( key . replace ( / [ ^ a - z 0 - 9 ] / gi, '' ) . toLowerCase ( ) === expected && value && typeof value === 'object' && ! Array . isArray ( value ) ) return value ;
52+ }
53+ }
54+ return null ;
55+ }
4556function 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
5466async 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}
150175function 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}
155180function pageInputHash ( page , contextHash ) { return stableHash ( { page, contextHash } ) ; }
156181function 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 - z 0 - 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}
165199function 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