[GH-ISSUE #6696] [Bug] Empty URL path segments collapse distinct environment collections and couple their writes #4070

Closed
opened 2026-10-31 00:46:34 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6696

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

Environment state extraction merges the distinct captured collections /api/books and /api//books. Both use item ID 1, so only one state entity remains. Updating /api/books/1 then also changes the response served for /api//books/1, even though the capture explicitly records that second route retaining its original value after the update.

This was observed through the actual state projection, serving, and comparison functions using synthetic HAR Evidence. No live app, server, or network request was used.

Reproduction

With locked dependencies installed at the source revision below, save this as /tmp/rea-empty-segments.mts and run ./node_modules/.bin/tsx /tmp/rea-empty-segments.mts from the checkout root. This script was executed before drafting:

import {pathToFileURL} from 'node:url';
const load=(p:string)=>import(pathToFileURL(`${process.cwd()}/${p}`).href);
const {servedFrom}=await load('tests/fixtures/environmentStateServing.ts');
const {timedCaptureEvidence}=await load('tests/fixtures/analysisView.ts');
const {compareEnvironment}=await load('src/application/environment/environmentCompare.ts');
const rows=[
 ['GET','/api/books',[{id:1,name:'Normal'}]],
 ['GET','/api/books/1',{id:1,name:'Normal'}],
 ['GET','/api//books',[{id:1,name:'Separate'}]],
 ['GET','/api//books/1',{id:1,name:'Separate'}],
 ['PATCH','/api/books/1',{id:1,name:'Updated'},200,{name:'Updated'}],
 ['GET','/api/books/1',{id:1,name:'Updated'}],
 ['GET','/api//books/1',{id:1,name:'Separate'}],
];
const s=servedFrom(rows);
const find=(path:string,method='GET')=>s.source.manifest.tools.find((t:any)=>t.path_template===path&&t.method===method).name;
const tool=find('/api//books/{numeric}');
const before=s.answer(tool,{id:'1'}).body;
const update=s.answer(find('/api/books/{numeric}','PATCH'),{id:'1',body:{name:'Updated'}});
const after=s.answer(tool,{id:'1'}).body;
const capture=timedCaptureEvidence(rows);
const compared=compareEnvironment({source:s.source,captureValue:capture,probes:[{label:'same-capture',value:capture}],operation:'environment-compare'});
console.log(JSON.stringify({before,update:update.body,after,entities:s.store.snapshot().rea_state.entities,agreement:compared.value?.summary.agreement,steps:compared.value?.probes[0].results.map((r:any)=>({path:r.path,equal:r.equal,differences:r.differences}))},null,2));

The fixture records Normal under /api/books/1 and Separate under /api//books/1. A recorded PATCH changes only the former to Updated; the last recorded GET confirms the latter still says Separate.

Expected behavior

Preserve internal empty URL path segments when identifying collections and entities. /api/books and /api//books should remain separate unless evidence establishes equivalence. Serving the captured PATCH should preserve the distinct collection's recorded value.

The environment contract describes entities belonging to the collection their path names and write effects checked against later recorded reads. The manifest already preserves both path spellings as separate tools.

Impact and scope

The local reproduction merges independent identities and lets one collection's write alter another collection's answers. Comparing the complete capture against itself returns agreement 0.857 (6/7), with a false /name value difference at the final GET. This report concerns internal double slashes; real application prevalence and other platforms were not tested. Source captures are unchanged.

REA version

Current source 8c4f96ef4759fd78da66c4b9726825ca71d7ee0c; package metadata 6.7.0. Published-release behavior was not exercised for this finding.

Runtime and platform

Node.js v26.12.1; Linux x86_64; repository-locked dependencies.

Target and artifact context

Synthetic HAR Evidence with two literal paths, the same numeric item ID, and a recorded PATCH followed by reads of both paths.

Interface

Other

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

Selected actual output:

{"before":{"id":1,"name":"Separate"},"update":{"id":1,"name":"Updated"},"after":{"id":1,"name":"Updated"},"agreement":0.857}

The final comparison step is /api//books/1, equal:false, with /name live "Separate" versus environment "Updated". The generated state has a single entity with type and collection /api/books.

Typed outcome or recovery expected

Separate collection identities and writes. If these routes cannot be represented safely, withhold the affected state/write model instead of silently coupling the routes.

Analysis or suspected cause

Source-derived: segmentsOf filters every empty segment. normalizedPath and collectionPathOf rebuild collection paths from that result. Consequently internal repeated separators disappear before state identity is assigned. Initially retained view-specific names hide the conflation until an update removes those field overrides.

Additional context

This differs from #6691, which drops repeated query values during probe matching. Here URL path identity is lost during state extraction, causing writes to cross collection boundaries. No fix is included.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Actual shared-state execution conflates two captured routes and changes the unrelated collection's returned data after a write; self-comparison reproduces the resulting mismatch.
  • Workaround status: unverified
  • Workaround basis: Replay-only serving might avoid state coupling, but that behavior was not tested and would omit the requested mutable state semantics.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6696 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior Environment state extraction merges the distinct captured collections `/api/books` and `/api//books`. Both use item ID `1`, so only one state entity remains. Updating `/api/books/1` then also changes the response served for `/api//books/1`, even though the capture explicitly records that second route retaining its original value after the update. This was observed through the actual state projection, serving, and comparison functions using synthetic HAR Evidence. No live app, server, or network request was used. ### Reproduction With locked dependencies installed at the source revision below, save this as `/tmp/rea-empty-segments.mts` and run `./node_modules/.bin/tsx /tmp/rea-empty-segments.mts` from the checkout root. This script was executed before drafting: ```typescript import {pathToFileURL} from 'node:url'; const load=(p:string)=>import(pathToFileURL(`${process.cwd()}/${p}`).href); const {servedFrom}=await load('tests/fixtures/environmentStateServing.ts'); const {timedCaptureEvidence}=await load('tests/fixtures/analysisView.ts'); const {compareEnvironment}=await load('src/application/environment/environmentCompare.ts'); const rows=[ ['GET','/api/books',[{id:1,name:'Normal'}]], ['GET','/api/books/1',{id:1,name:'Normal'}], ['GET','/api//books',[{id:1,name:'Separate'}]], ['GET','/api//books/1',{id:1,name:'Separate'}], ['PATCH','/api/books/1',{id:1,name:'Updated'},200,{name:'Updated'}], ['GET','/api/books/1',{id:1,name:'Updated'}], ['GET','/api//books/1',{id:1,name:'Separate'}], ]; const s=servedFrom(rows); const find=(path:string,method='GET')=>s.source.manifest.tools.find((t:any)=>t.path_template===path&&t.method===method).name; const tool=find('/api//books/{numeric}'); const before=s.answer(tool,{id:'1'}).body; const update=s.answer(find('/api/books/{numeric}','PATCH'),{id:'1',body:{name:'Updated'}}); const after=s.answer(tool,{id:'1'}).body; const capture=timedCaptureEvidence(rows); const compared=compareEnvironment({source:s.source,captureValue:capture,probes:[{label:'same-capture',value:capture}],operation:'environment-compare'}); console.log(JSON.stringify({before,update:update.body,after,entities:s.store.snapshot().rea_state.entities,agreement:compared.value?.summary.agreement,steps:compared.value?.probes[0].results.map((r:any)=>({path:r.path,equal:r.equal,differences:r.differences}))},null,2)); ``` The fixture records `Normal` under `/api/books/1` and `Separate` under `/api//books/1`. A recorded PATCH changes only the former to `Updated`; the last recorded GET confirms the latter still says `Separate`. ### Expected behavior Preserve internal empty URL path segments when identifying collections and entities. `/api/books` and `/api//books` should remain separate unless evidence establishes equivalence. Serving the captured PATCH should preserve the distinct collection's recorded value. The [environment contract](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/docs/agent-environments.md) describes entities belonging to the collection their path names and write effects checked against later recorded reads. The manifest already preserves both path spellings as separate tools. ### Impact and scope The local reproduction merges independent identities and lets one collection's write alter another collection's answers. Comparing the complete capture against itself returns agreement `0.857` (6/7), with a false `/name` value difference at the final GET. This report concerns internal double slashes; real application prevalence and other platforms were not tested. Source captures are unchanged. ### REA version Current source `8c4f96ef4759fd78da66c4b9726825ca71d7ee0c`; package metadata 6.7.0. Published-release behavior was not exercised for this finding. ### Runtime and platform Node.js v26.12.1; Linux x86_64; repository-locked dependencies. ### Target and artifact context Synthetic HAR Evidence with two literal paths, the same numeric item ID, and a recorded PATCH followed by reads of both paths. ### Interface Other ### Earliest failure stage Artifact or evidence registration ### Relevant output or evidence Selected actual output: ```json {"before":{"id":1,"name":"Separate"},"update":{"id":1,"name":"Updated"},"after":{"id":1,"name":"Updated"},"agreement":0.857} ``` The final comparison step is `/api//books/1`, `equal:false`, with `/name` live `"Separate"` versus environment `"Updated"`. The generated state has a single entity with type and collection `/api/books`. ### Typed outcome or recovery expected Separate collection identities and writes. If these routes cannot be represented safely, withhold the affected state/write model instead of silently coupling the routes. ### Analysis or suspected cause Source-derived: [`segmentsOf`](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/domain/analysisView/environmentStateRules.ts#L210-L211) filters every empty segment. [`normalizedPath` and `collectionPathOf`](https://github.com/morluto/rea/blob/8c4f96ef4759fd78da66c4b9726825ca71d7ee0c/src/domain/analysisView/environmentStateRules.ts#L299-L310) rebuild collection paths from that result. Consequently internal repeated separators disappear before state identity is assigned. Initially retained view-specific names hide the conflation until an update removes those field overrides. ### Additional context This differs from #6691, which drops repeated query values during probe matching. Here URL path identity is lost during state extraction, causing writes to cross collection boundaries. No fix is included. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Actual shared-state execution conflates two captured routes and changes the unrelated collection's returned data after a write; self-comparison reproduces the resulting mismatch. - Workaround status: unverified - Workaround basis: Replay-only serving might avoid state coupling, but that behavior was not tested and would omit the requested mutable state semantics.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4070
No description provided.