The explanation says the array and each item are new, so changing the result should leave the input alone. Yet when the caller uppercases a label, the original changes too. The shallow copy kept a reference to the nested object.
Download the parcel preview lab. Its code, data, and deliberately incorrect explanation were constructed from scratch for this tutorial. Use Node.js 22 or later. No installation, credentials, or network access is needed.
Keep the claim beside the source
Check whether changing a copied parcel’s label also changes the input. Open the copying function and its caller together.
Open the extracted parcel-preview-lab folder in Yenpo. Use Command-P (Go to File) to open claims.md, src/copy-parcels.mjs and src/preview.mjs in tabs. Read the claim against both the copy and its caller. Search All Files can locate row.label.text; inspect the surrounding uppercase condition rather than treating a matching line as proof that every call mutates the input.
This reading step needs no model connection or language server. Get Yenpo for Mac to open the same example, or use the file and text search help if you lose your place. Run the observer and checks below in Terminal yourself, then record their evidence in your note.
Break the explanation into claims
Here is the teaching explanation we will check:
copyParcelscreates a new array and a new object for every parcel. Its labels are independent copies, sopreviewcan uppercase them without changing the input.
The first sentence contains two claims. The second adds an ownership claim and a behavioral conclusion. Treating the paragraph as a single “correct or incorrect” answer would lose the useful parts along with the mistake.
| ID | Claim | What would establish or refute it? |
|---|---|---|
C1 |
The returned array is new. | Compare the input and output array identities. |
C2 |
Each outer parcel object is new. | Compare the corresponding parcel identities. |
C3 |
Each nested label is independent. | Compare the label identities, not just their text. |
C4 |
Uppercasing the preview preserves the input. | Reach that branch and inspect the caller’s input afterward. |
C5 |
Any input mutation is a business bug. | Find a requirement or ownership contract; code alone does not supply one. |
C5 is a possible conclusion a reader might add, not part of the quoted explanation. Keep it separate so that disproving C4 does not silently decide the product requirement.
Our scope is a dense array of ordinary mutable parcel objects, each with a plain label object and string text. The lab does not cover getters, proxies, class instances, or external I/O. Keep that scope beside the explanation instead of generalizing the run to every JavaScript value.
Follow the reference across the function boundary
Open src/copy-parcels.mjs:
export function copyParcels(parcels) {
return parcels.map((parcel) => ({ ...parcel }));
}
The object literal creates a new outer object on each callback invocation. It copies the label property’s value, which is a reference to an object. It does not create a new nested label. MDN documents this distinction in spread syntax and shallow copy.
Now open the actual caller, src/preview.mjs:
import { copyParcels } from './copy-parcels.mjs';
export function preview(parcels, { uppercase = false } = {}) {
const rows = copyParcels(parcels);
if (uppercase) {
for (const row of rows) {
row.label.text = row.label.text.toUpperCase();
}
}
return rows;
}
The important write is to row.label.text. Reassigning row.id would affect the new outer object; assigning through row.label reaches the shared nested object. For the fixture, the reference structure immediately after copying is:
input ──> original array ──> original parcel ──┐
├──> one label object
rows ──> new array ──────> new parcel ────────┘
Finding this assignment is not enough to explain a particular run. The default branch skips it. To test C4, supply uppercase: true and text whose uppercase form differs from its current value.
Run the branch that can contradict the explanation
Unzip the download, enter parcel-preview-lab, and run:
node observe.mjs
The observer creates a fresh fixture for each branch. It saves the original text as a string before calling preview, then prints identities and values. These are the results:
| Observation | uppercase: false |
uppercase: true |
|---|---|---|
| Same array? | false |
false |
| Same outer parcel? | false |
false |
| Same nested label? | true |
true |
| Text saved before the call | fragile |
fragile |
| Input text after the call | fragile |
FRAGILE |
| Preview text after the call | fragile |
FRAGILE |
This is a reachable counterexample to C4: the caller passes the input, selects the uppercase branch, and can observe its own nested text changing. C1 and C2 remain true. C3 is already refuted by the shared label identity, including in the branch that makes no assignment.
An already-uppercase label is a weaker probe: the write still happens, but the value before and after is the same. An empty array never enters the loop. Those inputs can pass a “text did not change” check without establishing independent ownership. The evidence tests include both controls.
Do not confuse equal values with independent objects
Before the write, this assertion passes:
assert.deepEqual(rows, input);
That tells us the compared structure and values match. It does not tell us that corresponding nested objects have different identities. These three assertions also pass at the same time:
assert.notStrictEqual(rows, input);
assert.notStrictEqual(rows[0], input[0]);
assert.strictEqual(rows[0].label, input[0].label);
The lab imports assert from node:assert/strict. See Node’s assertion reference for the comparison semantics. Keep both kinds of evidence: identities describe sharing, while before-and-after values show the consequence of a reachable write.
Do not use const before = input as your saved “before” state. That variable would still point at the same array and could show the mutated value too. The observer saves the primitive string beforeText before the call. A captured value and a reference to a mutable object answer different questions.
Turn the result into a revised explanation
Run these commands separately; the first is deliberately expected to fail:
node --test checks/original-claims.test.mjs
node --test checks/evidence.test.mjs
The original-claims run has two passing tests and two failing tests, and exits nonzero. C1 and C2 pass; the independent-label and preserved-input assertions fail. The evidence run has six passing tests and exits zero. Node’s test runner documentation describes running these files. No application source is changed between the runs: the assertions express different claims about the same program.
Record the verdict and its limit, not just a green or red result:
| ID | Verdict | Evidence and limit |
|---|---|---|
C1, C2 |
Supported in scope | New array and outer parcels: map, the object literal, and identity checks agree. |
C3 |
Refuted | Nested labels are shared: input and output hold the same reference. |
C4 |
Refuted | Uppercase changes input: fragile becomes FRAGILE on the true branch. |
C5 |
Undetermined | A requirement violation is not established: no ownership contract was supplied. |
A corrected explanation is:
copyParcelscreates a new array and new outer parcel objects while preserving each nested label reference. Withuppercase: false,previewmakes no text assignment. Withuppercase: true, it writes through the shared label. For thefragilefixture, the caller’s input becomesFRAGILE. Whether that mutation is acceptable depends on the ownership contract.
The ZIP includes this record in claims.md, the observer, both test files, and a file-hash manifest.
Carry the evidence back to the reading task
Keep the source version, the explanation you checked, and the input and output from your test. Then ask: “The two labels share an object, and uppercase changes the original. Rewrite the explanation to account for both.” Check the new answer against the same code.
Yenpo’s AI Code Reader lets you browse local source, ask focused questions, and open references while following callers. Run the lab and your own checks in Terminal or your existing test tools; Yenpo does not run shell commands or tests for you.
If the next decision is whether to change this behavior, first establish the requirement. For an actual proposed patch, continue with reviewing AI-generated code using a counterexample. If you need to record unfamiliar behavior before a refactor, use characterization tests.