@@ -233,6 +233,23 @@ function parseSpineText(text: string): CarriedFacts {
233233 return carried ;
234234}
235235
236+ const HANDOFF_SCHEMA_HEADINGS = new Set ( [
237+ "Goal" ,
238+ "Constraints" ,
239+ "Decisions" ,
240+ "Evidence markers (cumulative echo)" ,
241+ "Files" ,
242+ "Commands" ,
243+ "Verification" ,
244+ "Dead ends" ,
245+ "Next actions" ,
246+ "Exact facts (verbatim — do not paraphrase)" ,
247+ ] ) ;
248+
249+ const HANDOFF_SUMMARY_HEADING = "Summary (this fold — may paraphrase)" ;
250+ const HANDOFF_EXACT_FACTS_HEADING =
251+ "Exact facts (verbatim — do not paraphrase)" ;
252+
236253function listItems ( body : string ) : string [ ] {
237254 if ( body . length === 0 || body === "(none)" ) return [ ] ;
238255 const items : string [ ] = [ ] ;
@@ -247,55 +264,85 @@ function listItems(body: string): string[] {
247264
248265/** Parse a previously written fat handoff file into structured sections. */
249266function parseHandoffFile ( text : string ) : Partial < HandoffArtifact > {
250- const sections = new Map < string , string > ( ) ;
251- const heading = / ^ # # ( .+ ) $ / gm;
252- const matches = [ ...text . matchAll ( heading ) ] ;
253- for ( let i = 0 ; i < matches . length ; i ++ ) {
254- const match = matches [ i ] ;
255- if ( match === undefined ) continue ;
256- const title = ( match [ 1 ] ?? "" ) . trim ( ) ;
257- const start = ( match . index ?? 0 ) + match [ 0 ] . length ;
258- const end = matches [ i + 1 ] ?. index ?? text . length ;
259- sections . set ( title , text . slice ( start , end ) . trim ( ) ) ;
267+ const sections = new Map < string , string [ ] > ( ) ;
268+ let current : string | undefined ;
269+ let inSummary = false ;
270+ for ( const line of text . split ( "\n" ) ) {
271+ const heading = / ^ # # ( .+ ) $ / . exec ( line ) ;
272+ if ( heading !== null ) {
273+ const title = ( heading [ 1 ] ?? "" ) . trim ( ) ;
274+ if ( title === HANDOFF_SUMMARY_HEADING ) {
275+ inSummary = true ;
276+ current = undefined ;
277+ continue ;
278+ }
279+ if ( title === HANDOFF_EXACT_FACTS_HEADING ) inSummary = false ;
280+ if ( inSummary || ! HANDOFF_SCHEMA_HEADINGS . has ( title ) ) {
281+ current = undefined ;
282+ continue ;
283+ }
284+ if ( sections . has ( title ) ) {
285+ current = undefined ;
286+ continue ;
287+ }
288+ current = title ;
289+ sections . set ( title , [ ] ) ;
290+ continue ;
291+ }
292+ if ( current !== undefined ) sections . get ( current ) ?. push ( line ) ;
260293 }
261- const goal = sections . get ( "Goal" ) ;
294+
295+ const body = ( title : string ) : string =>
296+ ( sections . get ( title ) ?? [ ] ) . join ( "\n" ) . trim ( ) ;
297+ const maybeList = ( title : string ) : string [ ] | undefined => {
298+ const items = listItems ( body ( title ) ) ;
299+ return items . length > 0 ? items : undefined ;
300+ } ;
301+ const goal = body ( "Goal" ) ;
302+ const constraints = maybeList ( "Constraints" ) ;
303+ const decisions = maybeList ( "Decisions" ) ;
304+ const evidenceMarkers = maybeList ( "Evidence markers (cumulative echo)" ) ;
305+ const files = maybeList ( "Files" ) ;
306+ const commands = maybeList ( "Commands" ) ;
307+ const verification = maybeList ( "Verification" ) ;
308+ const deadEnds = maybeList ( "Dead ends" ) ;
309+ const nextActions = maybeList ( "Next actions" ) ;
310+ const exactFacts = maybeList ( HANDOFF_EXACT_FACTS_HEADING ) ;
262311 return {
263- ...( goal !== undefined && goal . length > 0 && goal !== "(none)"
264- ? { goal }
265- : { } ) ,
266- constraints : listItems ( sections . get ( "Constraints" ) ?? "" ) ,
267- decisions : listItems ( sections . get ( "Decisions" ) ?? "" ) ,
268- evidenceMarkers : listItems (
269- sections . get ( "Evidence markers (cumulative echo)" ) ?? "" ,
270- ) ,
271- files : listItems ( sections . get ( "Files" ) ?? "" ) ,
272- commands : listItems ( sections . get ( "Commands" ) ?? "" ) ,
273- verification : listItems ( sections . get ( "Verification" ) ?? "" ) ,
274- deadEnds : listItems ( sections . get ( "Dead ends" ) ?? "" ) ,
275- nextActions : listItems ( sections . get ( "Next actions" ) ?? "" ) ,
276- exactFacts : listItems (
277- sections . get ( "Exact facts (verbatim — do not paraphrase)" ) ?? "" ,
278- ) ,
312+ ...( goal . length > 0 && goal !== "(none)" ? { goal } : { } ) ,
313+ ...( constraints !== undefined ? { constraints } : { } ) ,
314+ ...( decisions !== undefined ? { decisions } : { } ) ,
315+ ...( evidenceMarkers !== undefined ? { evidenceMarkers } : { } ) ,
316+ ...( files !== undefined ? { files } : { } ) ,
317+ ...( commands !== undefined ? { commands } : { } ) ,
318+ ...( verification !== undefined ? { verification } : { } ) ,
319+ ...( deadEnds !== undefined ? { deadEnds } : { } ) ,
320+ ...( nextActions !== undefined ? { nextActions } : { } ) ,
321+ ...( exactFacts !== undefined ? { exactFacts } : { } ) ,
279322 } ;
280323}
281324
282325// Prefer the full prior-file text over a spine-truncated prefix of the same
283- // fact. Distinct facts append until the cap.
326+ // fact. Distinct facts append until the cap. Prefix collapse is only for
327+ // known truncated spine fragments (80-char cuts); paths and commands use
328+ // exact equality so `src/auth` and `src/auth.ts` stay distinct.
284329function mergeUnique (
285330 primary : readonly string [ ] ,
286331 extra : readonly string [ ] ,
287332 cap : number ,
288333 maxChars = MAX_ITEM_CHARS ,
334+ mode : "prefix" | "exact" = "prefix" ,
289335) : string [ ] {
290336 const merged : string [ ] = [ ] ;
291337 const consider = ( raw : string ) : void => {
292338 const clean = raw . trim ( ) ;
293339 if ( clean . length === 0 ) return ;
294340 const item =
295341 clean . length > maxChars ? `${ clean . slice ( 0 , maxChars ) } ...` : clean ;
296- const related = merged . findIndex (
297- ( entry ) =>
298- entry === item || entry . startsWith ( item ) || item . startsWith ( entry ) ,
342+ const related = merged . findIndex ( ( entry ) =>
343+ mode === "exact"
344+ ? entry === item
345+ : entry === item || entry . startsWith ( item ) || item . startsWith ( entry ) ,
299346 ) ;
300347 if ( related >= 0 ) {
301348 const existing = merged [ related ] ;
@@ -513,12 +560,19 @@ export function extractHandoffArtifact(
513560 narrative ,
514561 ...( priorFile . evidenceMarkers ?? [ ] ) ,
515562 ] ) ;
516- const mergedFiles = mergeUnique ( priorFile . files ?? [ ] , files , MAX_FILES ) ;
563+ const mergedFiles = mergeUnique (
564+ priorFile . files ?? [ ] ,
565+ files ,
566+ MAX_FILES ,
567+ MAX_ITEM_CHARS ,
568+ "exact" ,
569+ ) ;
517570 const mergedCommands = mergeUnique (
518571 priorFile . commands ?? [ ] ,
519572 commands . map ( ( entry ) => entry . command ) ,
520573 MAX_COMMANDS ,
521574 MAX_COMMAND_CHARS ,
575+ "exact" ,
522576 ) ;
523577 const mergedVerification = mergeUnique (
524578 priorFile . verification ?? [ ] ,
0 commit comments