Skip to content

migrate diff silently ignores repeated --field paths #249

Description

@pablontiv

[rootline-team/investigador] 2026-09-08T18:24:25Z

Problem and impact

migrate diff output accepts repeated --field flags but silently drops every path after the first, in both single-stem and batch mode. An invalid second path is also ignored: the command emits a plausible partial result and exits 0. Consumers cannot rely on the documented ordered-array or all-paths-must-resolve contract.

This is a reproduced residual bug, not a new selector design or a regression attributed to the most recent merge.

Accepted contract and dependencies

Priority: P2 — deterministic incorrect machine-readable output and hidden input errors, without observed writes or data loss. No unresolved implementation dependency.

Evidence and root cause

Examined and built current master 514e42823275222b6e14cd99b4a354661b4a139c.

  • cmd/rootline/migrate.go: renderMigrateBatch and renderMigrateJSON call extractField(data, fieldPath[0]) (lines 255 and 270).
  • cmd/rootline/validate.go: the working shared outputJSON uses effectiveFieldPaths() and extractFields, resolving all paths before emitting.
  • migrate --rename and --scaffold already call the shared writer; the bounded defect is in diff rendering.

Runtime provenance: Linux/x86_64; freshly downloaded official Go 1.27.1, compatible with go.mod (Go 1.26). Archive SHA-256 matched current go.dev metadata: 63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445.

Build:

go build -mod=readonly -ldflags '-X main.version=dev' -o /absolute/rootline-repro ./cmd/rootline

Binary reported rootline version dev; SHA-256: be77e9658696380d340a3aa561a0b2f55b71e8afa1fbedd0efb0eacfac099edc. The checkout remained clean.

Minimal reproduction

Create these files in a disposable directory outside any Git repository.

Both single/.stem and batch/.stem:

version: 2
root: true
scope:
  match: "*.md"
schema:
  status:
    type: string

batch/child/.stem:

version: 2
schema:
  note:
    type: string

Run the fresh binary as an external process:

rootline-repro migrate single/.stem --from single/.stem
rootline-repro migrate single/.stem --from single/.stem --field kind
rootline-repro migrate single/.stem --from single/.stem --field kind --field version
rootline-repro migrate single/.stem --from single/.stem --field version --field kind
rootline-repro migrate single/.stem --from single/.stem --field kind --field nonexistent
rootline-repro migrate single/.stem --from single/.stem --field nonexistent
rootline-repro migrate batch
rootline-repro migrate batch --field kind --field version
rootline-repro migrate batch --field kind --field nonexistent
rootline-repro describe single --field kind --field version

The single-stem case explicitly compares a valid file against itself, so it requires neither Git nor a previous revision. For batch, absence of Git exercises the existing new-schema fallback; full output first confirmed two results and summary.stems_checked == 2. That fallback is not being changed.

Actual results

Case Expected Observed
Single, no projection Existing v1 diff envelope, no changes Correct; exit 0
Single, --field kind "rootline/migrate-diff" Correct; exit 0
Single, kind then version ["rootline/migrate-diff",1] "rootline/migrate-diff"; exit 0
Single, version then kind [1,"rootline/migrate-diff"] 1; exit 0
Single, valid then nonexistent Nonzero; diagnostic; no partial stdout "rootline/migrate-diff"; exit 0; empty stderr
Single, nonexistent alone Nonzero; diagnostic; no stdout Exit 1; Error: field "nonexistent": key "nonexistent" not found
Batch, kind then version ["rootline/migrate-batch",1] "rootline/migrate-batch"; exit 0
Batch, valid then nonexistent Nonzero; diagnostic; no partial stdout "rootline/migrate-batch"; exit 0; empty stderr
Working control: describe, kind then version ["rootline/describe",1] Correct; exit 0

All valid-path invocations had empty stderr. All three fixture files retained their bytes, mode 0644 and modification times throughout.

Scope and acceptance

Implement the already-selected #132 projection contract in both migrate diff renderers:

  • No projection retains the current single/batch envelope, kind/version, values and newline behavior.
  • One effective path retains the bare JSON value.
  • Two or more effective paths produce one JSON array in flag order, including reversed-order and repeated-identical-path cases.
  • A missing path in any position fails nonzero with a diagnostic and no partial stdout, for single and batch rendering.
  • Reuse the shared effective-path/extraction behavior, including existing empty-path handling; do not invent a second selector implementation.
  • Keep unsupported-format rejection and table output unchanged.
  • Exercise the command boundary with valid single-stem and multi-stem fixtures, and verify the read-only calls do not write.
  • Update the stale CLAUDE.md residual and relevant migration/output documentation or distributed reference if affected.

Exclusions: no migration diff algorithm, Git fallback, schema grammar, file mutation, atomicity, rename/scaffold/split semantics, new format or envelope-version changes. Do not reopen the other sub-defects of #63.

Compatibility note: callers relying on the known broken multi-field behavior will see an array instead of the first scalar, and an invalid later path will now fail. This is the specifically deferred application of #132's accepted contract, not authority to introduce other compatibility changes. Document the correction and follow the repository's Conventional Commit/release conventions.

Verification handoff and deduplication

Add focused command tests that fail before the correction; run current repository quality gates and leave independent QA to execute the above flows against the PR SHA. Researcher ran the build and the ten external-process cases above, not the full test/coverage/lint suite, and did not emit a QA verdict or modify production code.

Searched open and closed issues/PRs for migrate field, repeatable, and fieldPath; found #63 and its merged implementation #132 but no dedicated residual ticket. This ticket tracks the explicitly deferred follow-up rather than reopening the completed broad issue. #190 and #194 retain their needs-info decisions.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified and ready for an autonomous agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions