В объяснении сказано, что массив и каждый элемент новые, поэтому изменение результата не затронет вход. Но когда вызывающий код переводит label в верхний регистр, меняется и оригинал. Поверхностная копия сохранила ссылку на вложенный объект.

Скачайте учебный проект предпросмотра посылок. Код, данные и заведомо неверное объяснение созданы с нуля для урока. Используйте Node.js 22 или новее. Установка, учётные данные и сеть не нужны.

Держите утверждение рядом с исходником

Проверьте, меняет ли правка label скопированного parcel исходные данные. Откройте функцию копирования и вызывающий код рядом.

Откройте распакованный parcel-preview-lab в Yenpo. Через Command-P (Перейти к файлу…) откройте во вкладках claims.md, src/copy-parcels.mjs и src/preview.mjs. Сопоставьте утверждение и с копированием, и с вызывающим кодом. Искать во всех файлах поможет найти row.label.text; изучите окружающее условие uppercase, а не считайте совпадение доказательством изменения входа при каждом вызове.

Для этого чтения не нужны модель или языковой сервер. Скачайте Yenpo для Mac, чтобы открыть тот же пример, или воспользуйтесь справкой по поиску файлов и текста, если потеряли нужное место. Самостоятельно запустите наблюдатель и проверки ниже в терминале, затем занесите доказательства в заметку.

Разделите объяснение на утверждения

Вот учебное объяснение, которое мы проверим:

copyParcels создаёт новый массив и новый объект для каждой посылки. Этикетки — независимые копии, поэтому preview может перевести их в верхний регистр, не меняя вход.

В первом предложении два утверждения. Во втором добавлены утверждение о владении объектами и вывод о поведении. Если оценивать весь абзац только как «верный или неверный», вместе с ошибкой потеряются полезные части.

ID Утверждение Что подтвердит или опровергнет его?
C1 Возвращаемый массив новый. Сравнение идентичности входного и выходного массивов.
C2 Каждый внешний объект посылки новый. Сравнение идентичности соответствующих посылок.
C3 Каждая вложенная этикетка независима. Сравнение идентичности этикеток, а не только их текста.
C4 Верхний регистр в предпросмотре не меняет вход. Выполнение этой ветки и проверка входа вызывающего кода после неё.
C5 Любое изменение входа — ошибка бизнес-логики. Требование или контракт владения; сам код их не предоставляет.

C5 — возможный вывод читателя, а не часть цитаты. Отделите его, чтобы опровержение C4 не определяло незаметно продуктовое требование.

Область проверки — плотный массив обычных изменяемых объектов посылок, каждый с простым объектом label и строкой text. Геттеры, прокси, экземпляры классов и внешний ввод-вывод не охвачены. Укажите эти границы рядом с объяснением, не обобщая запуск на все значения JavaScript.

Проследите ссылку через границу функции

Откройте src/copy-parcels.mjs:

export function copyParcels(parcels) {
  return parcels.map((parcel) => ({ ...parcel }));
}

Объектный литерал создаёт новый внешний объект при каждом вызове callback. Он копирует значение свойства label — ссылку на объект. Новую вложенную этикетку он не создаёт. MDN описывает различие в разделах spread-синтаксис и поверхностное копирование.

Теперь откройте реальный вызывающий код — 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;
}

Важна запись в row.label.text. Присваивание row.id изменило бы новый внешний объект; запись через row.label достигает общего вложенного объекта. Для фикстуры структура ссылок сразу после копирования такова:

input ──> original array ──> original parcel ──┐
                                             ├──> one label object
rows  ──> new array ──────> new parcel ────────┘

Найти присваивание недостаточно, чтобы объяснить конкретный запуск. Ветка по умолчанию его пропускает. Для проверки C4 передайте uppercase: true и текст, у которого верхний регистр отличается от текущего значения.

Запустите ветку, способную опровергнуть объяснение

Распакуйте архив, перейдите в parcel-preview-lab и выполните:

node observe.mjs

Наблюдатель создаёт свежую фикстуру для каждой ветки. Перед вызовом preview он сохраняет исходный текст как строку, затем выводит идентичности и значения. Результаты:

Наблюдение uppercase: false uppercase: true
Тот же массив? false false
Тот же внешний объект посылки? false false
Та же вложенная этикетка? true true
Текст, сохранённый до вызова fragile fragile
Текст входа после вызова fragile FRAGILE
Текст предпросмотра после вызова fragile FRAGILE

Это достижимый контрпример к C4: вызывающий код передаёт вход, выбирает верхний регистр и наблюдает изменение собственного вложенного текста. C1 и C2 остаются верными. C3 уже опровергается общей идентичностью этикетки, в том числе в ветке без присваивания.

Этикетка уже в верхнем регистре — более слабая проверка: запись происходит, но значение до и после одинаково. Пустой массив вообще не входит в цикл. Такие входы проходят проверку «текст не изменился», не доказывая независимое владение. В тестах доказательств есть оба контрольных случая.

Не путайте равные значения с независимыми объектами

До записи эта проверка проходит:

assert.deepEqual(rows, input);

Это говорит о совпадении сравниваемой структуры и значений. Но не доказывает различие идентичности соответствующих вложенных объектов. Одновременно проходят и эти три проверки:

assert.notStrictEqual(rows, input);
assert.notStrictEqual(rows[0], input[0]);
assert.strictEqual(rows[0].label, input[0].label);

Проект импортирует assert из node:assert/strict. Семантика сравнений описана в справочнике проверок Node. Сохраняйте оба вида доказательств: идентичности показывают общие объекты, а значения до и после — последствия достижимой записи.

Не используйте const before = input как сохранённое состояние «до». Переменная продолжит ссылаться на тот же массив и тоже покажет изменённое значение. Наблюдатель сохраняет примитивную строку beforeText до вызова. Зафиксированное значение и ссылка на изменяемый объект отвечают на разные вопросы.

Превратите результат в исправленное объяснение

Выполните команды отдельно; первая намеренно должна завершиться неудачей:

node --test checks/original-claims.test.mjs
node --test checks/evidence.test.mjs

Запуск исходных утверждений даёт два успешных и два проваленных теста и ненулевой код завершения. C1 и C2 проходят; проверки независимости этикеток и неизменности входа падают. Запуск доказательств даёт шесть успешных тестов и код ноль. Запуск файлов описан в документации тестового раннера Node. Между запусками код приложения не меняется: проверки выражают разные утверждения об одной программе.

Запишите вывод и его границы, а не только зелёный или красный результат:

ID Вывод Доказательство и границы
C1, C2 Подтверждены в заданных границах Новый массив и внешние посылки: map, объектный литерал и проверки идентичности согласуются.
C3 Опровергнуто Вложенные этикетки общие: вход и выход содержат одну ссылку.
C4 Опровергнуто Верхний регистр меняет вход: fragile становится FRAGILE в истинной ветке.
C5 Не определено Нарушение требований не установлено: контракт владения не предоставлен.

Исправленное объяснение:

copyParcels создаёт новый массив и новые внешние объекты посылок, сохраняя ссылки на вложенные этикетки. При uppercase: false функция preview не присваивает текст. При uppercase: true она пишет через общую этикетку. Для фикстуры fragile вход вызывающего кода становится FRAGILE. Допустимость изменения зависит от контракта владения.

ZIP содержит эту запись в claims.md, наблюдатель, оба файла тестов и манифест хешей.

Верните доказательства в задачу чтения

Сохраните версию исходников, проверенное объяснение, вход и выход теста. Затем спросите: «Два label ссылаются на один объект, а перевод в верхний регистр меняет оригинал. Перепиши объяснение с учётом этого». Проверьте новый ответ по тому же коду.

ИИ для чтения кода Yenpo позволяет просматривать локальные исходники, задавать точные вопросы и открывать ссылки, следуя по вызовам. Запускайте учебный проект и свои проверки в терминале или привычных тестовых инструментах; Yenpo не выполняет команды оболочки и тесты за вас.

Если дальше нужно решить, менять ли поведение, сначала установите требование. Для конкретной правки перейдите к проверке кода ИИ с помощью контрпримера. Чтобы зафиксировать незнакомое поведение перед рефакторингом, используйте характеризационные тесты.