@@ -348,122 +348,129 @@ export const layer = Layer.effect(
348348 Effect . catchTag ( "Ripgrep.InvalidPatternError" , ( cause ) => Effect . fail ( failure ( cause . message , cause ) ) ) ,
349349 ) ,
350350 // altimate_change start — upstream_fix: tally skipped records for one aggregate warning.
351- // Upstream returns `run(...)` directly; the body exists only to hold the tally, which must be
352- // per invocation and never per layer so two concurrent searches cannot share it.
353- grep : ( input ) => {
354- const skipped : { count : number ; samples : string [ ] } = { count : 0 , samples : [ ] }
355- return run < RawMatchData > ( {
356- // altimate_change end
357- ...input ,
358- args : [
359- "--no-config" ,
360- "--json" ,
361- "--hidden" ,
362- "--no-messages" ,
363- // altimate_change start — upstream_fix: preserve all debug rg search --glob entries
364- ...( typeof input . include === "string"
365- ? [ `--glob=${ input . include } ` ]
366- : ( input . include ?? [ ] ) . map ( ( pattern ) => `--glob=${ pattern } ` ) ) ,
367- // altimate_change end
368- "--glob=!**/.git/**" ,
369- "--" ,
370- input . pattern ,
371- input . file ?? "." ,
372- ] ,
373- // altimate_change start — upstream_fix: a bad record skips itself, never the search.
374- // `parse` runs inside `Stream.mapEffect`, so ANY failure here aborts the whole stream and
375- // discards every match already collected from unrelated files. A record is independent of
376- // its neighbours, so none of the three ways one can be unusable — oversized, unparseable
377- // JSON, or schema-rejected — justifies destroying the rest of the search.
378- parse : ( line ) => {
379- const bytes = Buffer . byteLength ( line , "utf8" )
380- // Captured during the walk so the aggregate warning can name a file when one is
381- // recoverable. Malformed JSON has no path by definition, hence "when present".
382- let where : string | undefined
383- return Effect . gen ( function * ( ) {
384- // Checked before JSON.parse purely to bound parse cost; `Stream.splitLines` has
385- // already materialized the line, so this cannot bound memory. See MAX_RECORD_BYTES.
386- if ( bytes > MAX_RECORD_BYTES )
387- return yield * Effect . fail ( failure ( `record exceeded ${ MAX_RECORD_BYTES } bytes` ) )
388- const json = yield * Effect . try ( {
389- try : ( ) => JSON . parse ( line ) as unknown ,
390- catch : ( cause ) => failure ( "unparseable JSON" , cause ) ,
391- } )
392- if ( ! json || typeof json !== "object" || ! ( "type" in json ) )
393- return yield * Effect . fail ( failure ( "record has no type" ) )
394- // Captured before the type check so an unrecognised record can still name its file.
395- const pathField = readProp ( readProp ( json , "data" ) , "path" )
396- const pathText = readProp ( pathField , "text" )
397- if ( typeof pathText === "string" ) where = pathText
398- // Control records are expected and simply carry no match. An unrecognised type is a
399- // protocol surprise and is counted rather than dropped on the floor, so a ripgrep
400- // change cannot quietly turn every match into "no matches".
401- if ( json . type !== "match" )
402- return typeof json . type === "string" && CONTROL_TYPES . has ( json . type )
403- ? undefined
404- : yield * Effect . fail ( failure ( `unrecognised record type ${ JSON . stringify ( json . type ) } ` ) )
405- const match = yield * Schema . decodeUnknownEffect ( RawMatch ) ( normalizeMatch ( json ) ) . pipe (
406- Effect . mapError ( ( cause ) => failure ( "unexpected match shape" , cause ) ) ,
351+ // Upstream returns `run(...)` directly. `Effect.suspend` — rather than a plain block body —
352+ // is what makes the tally per EXECUTION: an Effect is a value that can be run more than once
353+ // and concurrently, so a tally captured when the Effect is built would accumulate across runs
354+ // and over-report.
355+ //
356+ // The marked region covers the whole implementation rather than just these lines, because the
357+ // wrapper re-indents every line of the body: an upstream change anywhere in here genuinely
358+ // needs manual reconciliation, which is exactly what the marker is for.
359+ grep : ( input ) =>
360+ Effect . suspend ( ( ) => {
361+ const skipped : { count : number ; samples : string [ ] } = { count : 0 , samples : [ ] }
362+ return run < RawMatchData > ( {
363+ ...input ,
364+ args : [
365+ "--no-config" ,
366+ "--json" ,
367+ "--hidden" ,
368+ "--no-messages" ,
369+ // altimate_change start — upstream_fix: preserve all debug rg search --glob entries
370+ ...( typeof input . include === "string"
371+ ? [ `--glob=${ input . include } ` ]
372+ : ( input . include ?? [ ] ) . map ( ( pattern ) => `--glob=${ pattern } ` ) ) ,
373+ // altimate_change end
374+ "--glob=!**/.git/**" ,
375+ "--" ,
376+ input . pattern ,
377+ input . file ?? "." ,
378+ ] ,
379+ // altimate_change start — upstream_fix: a bad record skips itself, never the search.
380+ // `parse` runs inside `Stream.mapEffect`, so ANY failure here aborts the whole stream and
381+ // discards every match already collected from unrelated files. A record is independent of
382+ // its neighbours, so none of the three ways one can be unusable — oversized, unparseable
383+ // JSON, or schema-rejected — justifies destroying the rest of the search.
384+ parse : ( line ) => {
385+ const bytes = Buffer . byteLength ( line , "utf8" )
386+ // Captured during the walk so the aggregate warning can name a file when one is
387+ // recoverable. Malformed JSON has no path by definition, hence "when present".
388+ let where : string | undefined
389+ return Effect . gen ( function * ( ) {
390+ // Checked before JSON.parse purely to bound parse cost; `Stream.splitLines` has
391+ // already materialized the line, so this cannot bound memory. See MAX_RECORD_BYTES.
392+ if ( bytes > MAX_RECORD_BYTES )
393+ return yield * Effect . fail ( failure ( `record exceeded ${ MAX_RECORD_BYTES } bytes` ) )
394+ const json = yield * Effect . try ( {
395+ try : ( ) => JSON . parse ( line ) as unknown ,
396+ catch : ( cause ) => failure ( "unparseable JSON" , cause ) ,
397+ } )
398+ if ( ! json || typeof json !== "object" || ! ( "type" in json ) )
399+ return yield * Effect . fail ( failure ( "record has no type" ) )
400+ // Captured before the type check so an unrecognised record can still name its file.
401+ const pathField = readProp ( readProp ( json , "data" ) , "path" )
402+ const pathText = readProp ( pathField , "text" )
403+ if ( typeof pathText === "string" ) where = pathText
404+ // Control records are expected and simply carry no match. An unrecognised type is a
405+ // protocol surprise and is counted rather than dropped on the floor, so a ripgrep
406+ // change cannot quietly turn every match into "no matches".
407+ if ( json . type !== "match" )
408+ return typeof json . type === "string" && CONTROL_TYPES . has ( json . type )
409+ ? undefined
410+ : yield * Effect . fail ( failure ( `unrecognised record type ${ JSON . stringify ( json . type ) } ` ) )
411+ const match = yield * Schema . decodeUnknownEffect ( RawMatch ) ( normalizeMatch ( json ) ) . pipe (
412+ Effect . mapError ( ( cause ) => failure ( "unexpected match shape" , cause ) ) ,
413+ )
414+ // `normalizeMatch` already caps submatches and line text, so nothing is re-trimmed.
415+ return { ...match . data , path : { text : match . data . path . text . replace ( / ^ \. [ \\ / ] / , "" ) } }
416+ } ) . pipe (
417+ Effect . catch ( ( cause ) =>
418+ Effect . sync ( ( ) => {
419+ skipped . count ++
420+ if ( skipped . samples . length < SKIP_SAMPLES )
421+ skipped . samples . push ( where ? `${ cause . message } (${ where } )` : cause . message )
422+ return undefined
423+ } ) ,
424+ ) ,
407425 )
408- // `normalizeMatch` already caps submatches and line text, so nothing is re-trimmed.
409- return { ...match . data , path : { text : match . data . path . text . replace ( / ^ \. [ \\ / ] / , "" ) } }
410- } ) . pipe (
411- Effect . catch ( ( cause ) =>
412- Effect . sync ( ( ) => {
413- skipped . count ++
414- if ( skipped . samples . length < SKIP_SAMPLES )
415- skipped . samples . push ( where ? `${ cause . message } (${ where } )` : cause . message )
416- return undefined
417- } ) ,
418- ) ,
419- )
420- } ,
421- // altimate_change end
422- } ) . pipe (
423- // altimate_change start — upstream_fix: one aggregate warning per search, not per record.
424- // A systematic protocol mismatch rejects every record in the tree, and a per-record log
425- // would bury the machine in noise while still answering with an innocent-looking empty
426- // result — the very failure this change exists to prevent.
427- Effect . tap ( ( ) =>
428- skipped . count > 0
429- ? Effect . logWarning ( "skipped unusable ripgrep records" , {
430- skipped : skipped . count ,
431- reasons : skipped . samples ,
426+ } ,
427+ // altimate_change end
428+ } ) . pipe (
429+ // altimate_change start — upstream_fix: one aggregate warning per search, not per record.
430+ // A systematic protocol mismatch rejects every record in the tree, and a per-record log
431+ // would bury the machine in noise while still answering with an innocent-looking empty
432+ // result — the very failure this change exists to prevent.
433+ // `onExit` rather than `tap`: a search that fails or is interrupted part-way is exactly
434+ // when the diagnostic matters, and `tap` would discard the tally in both cases.
435+ Effect . onExit ( ( ) =>
436+ skipped . count > 0
437+ ? Effect . logWarning ( "skipped unusable ripgrep records" , {
438+ skipped : skipped . count ,
439+ reasons : skipped . samples ,
440+ } )
441+ : Effect . void ,
442+ ) ,
443+ // altimate_change end
444+ Effect . map ( ( result ) =>
445+ result . items . map ( ( match ) => {
446+ const relative = match . path . text
447+ . replace ( / ^ (?: \. [ \\ / ] ) + / u, "" )
448+ . replace ( / ^ [ \\ / ] + / u, "" )
449+ . replaceAll ( "\\" , "/" )
450+ const absolute = path . resolve ( input . cwd , relative )
451+ return new Match ( {
452+ entry : new Entry ( {
453+ path : RelativePath . make ( relative ) ,
454+ type : "file" ,
455+ mime : FSUtil . mimeType ( absolute ) ,
456+ } ) ,
457+ line : match . line_number ,
458+ offset : match . absolute_offset ,
459+ // altimate_change start — upstream_fix: capped at parse time, see LINE_TEXT_CAP.
460+ // Re-applied here so the cap still holds if the parser ever stops trimming.
461+ text : capLineText ( match . lines . text ) ,
462+ // altimate_change end
463+ submatches : match . submatches . map ( ( submatch ) => ( {
464+ text : submatch . match . text ,
465+ start : submatch . start ,
466+ end : submatch . end ,
467+ } ) ) ,
432468 } )
433- : Effect . void ,
434- ) ,
435- // altimate_change end
436- Effect . map ( ( result ) =>
437- result . items . map ( ( match ) => {
438- const relative = match . path . text
439- . replace ( / ^ (?: \. [ \\ / ] ) + / u, "" )
440- . replace ( / ^ [ \\ / ] + / u, "" )
441- . replaceAll ( "\\" , "/" )
442- const absolute = path . resolve ( input . cwd , relative )
443- return new Match ( {
444- entry : new Entry ( {
445- path : RelativePath . make ( relative ) ,
446- type : "file" ,
447- mime : FSUtil . mimeType ( absolute ) ,
448- } ) ,
449- line : match . line_number ,
450- offset : match . absolute_offset ,
451- // altimate_change start — upstream_fix: capped at parse time, see LINE_TEXT_CAP.
452- // Re-applied here so the cap still holds if the parser ever stops trimming.
453- text : capLineText ( match . lines . text ) ,
454- // altimate_change end
455- submatches : match . submatches . map ( ( submatch ) => ( {
456- text : submatch . match . text ,
457- start : submatch . start ,
458- end : submatch . end ,
459- } ) ) ,
460- } )
461- } ) ,
462- ) ,
463- // altimate_change start — upstream_fix: closes the block body opened for the skip tally.
464- )
465- } ,
466- // altimate_change end
469+ } ) ,
470+ ) ,
471+ )
472+ } ) ,
473+ // altimate_change end — closes the grep block opened above for the skip tally.
467474 } )
468475 } ) ,
469476)
0 commit comments