Skip to content

fix(query): preserve selected field order in JSONL output - #222

Merged
pablontiv merged 1 commit into
masterfrom
rootline-team/issue-171
Sep 5, 2026
Merged

fix(query): preserve selected field order in JSONL output#222
pablontiv merged 1 commit into
masterfrom
rootline-team/issue-171

Conversation

@pablontiv

@pablontiv pablontiv commented Sep 5, 2026

Copy link
Copy Markdown
Owner

[rootline-team]

Closes #171

Problem and scope

The documented JSONL contract promises --select key order, but the writer marshaled projected maps, silently sorting keys alphabetically. This violates the serialization contract in docs/query.md.

This change passes the selection sequence to the JSONL writer and writes each present key/value in that sequence using standard JSON escaping. It keeps duplicate selections collapsed, missing fields and empty links omitted, explicit null/false/zero values preserved, and nested values unchanged. Each complete row is buffered before writing.

Only the JSONL writer/call site and regression tests change. CSV, regular versioned JSON, selection/projection semantics, domain independence, optional Git and .stem contracts are unchanged. No documentation change is necessary because the existing documented behavior is restored. Does not include #221 or Scorecard #219.

Acceptance and executed verification

  • Two distinct non-alphabetic selections now have exact serialized-output assertions for both rows.
  • Covered omissions, duplicate fields, empty projected objects, explicit null, false, zero, arrays/objects, Unicode, newline/quote/backslash and JSON-escaped keys.
  • RED: new order/escaping assertions failed on the base writer; GREEN: the same regressions and related query format/source-backed projection tests passed after this fix.
  • just check: passed (golangci-lint 2.13.1: 0 issues, build).
  • just test: passed, all 17 packages with race detector.
  • just coverage-check: passed every per-package floor, 90.0% total; cmd/rootline 87.6%.
  • go mod tidy then git diff --exit-code -- go.mod go.sum: passed, no dependency changes.
  • govulncheck ./...: no vulnerabilities found.
  • gitleaks dir cmd/rootline --redact: no leaks.
  • sh tests/installers/install-sh-test.sh: passed on Linux. Native macOS/Windows checks remain CI's responsibility, not claimed locally.
  • Built dev CLI and rootline validate --all docs/roadmap/: 126/126 valid, zero errors/warnings.
  • Real external CLI comparison on a valid two-record, no-Git fixture: base always emits owner,path,status; fix emits path,status,owner or status,path,owner as requested. Both produce 2 valid JSON lines, exit 0. CSV header and JSON version:1, kind:rootline/query, meta.count:2 preserved. Fixture SHA-256 hashes unchanged after read commands.

After publication, a clean checkout of the exact head SHA was rebuilt and the focused regressions and real CLI fixture checks passed again. Published-SHA dev binary SHA-256: 882601f15e3fe531835ba7d67376491aec8b925f4fe9fa2e47e931003091d728.

Platform: Linux/amd64; official temporary Go 1.27.1 archive verified against its published SHA-256. Tests ran on tree c8f5a1a9f586d9388bdca6a495837437643c9298, exactly matching published commit aab08d438cdbe209429e173710b7a1613f54f7e8; base 8c46f8dc6f588520ce0c61bd361c089f3b4616a0.

Reproduce with the real CLI (for independent QA)

Implementation is complete. Published-SHA CI #829 passed and independent QA issued QA_PASS for the current SHA. The PR is ready for reviewer integration. Developer checks do not substitute for QA.

Checkout head aab08d438cdbe209429e173710b7a1613f54f7e8 in an isolated directory, use Go compatible with go.mod, and build a dev binary:

go build -o /tmp/rootline-171 ./cmd/rootline/
go test ./cmd/rootline -run '^(TestQueryOutput|TestTask9QueryFormatsPreserveSourceBackedOrderAndCounts)' -count=1

Create an isolated directory <fixture> with no Git and these three files:

.stem:

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

a.md:

---
status: Pending
owner: Alice
---
# A

b.md:

---
status: Done
owner: Bob
---
# B

Run:

/tmp/rootline-171 validate --all <fixture>
/tmp/rootline-171 query <fixture> --select path,status,owner --sort path:asc -o jsonl
/tmp/rootline-171 query <fixture> --select status,path,owner --sort path:asc -o jsonl
/tmp/rootline-171 query <fixture> --select status,missing,path,status,links,owner --sort path:asc -o jsonl
/tmp/rootline-171 query <fixture> --select missing,links -o jsonl
/tmp/rootline-171 query <fixture> --select status,path,owner -o csv
/tmp/rootline-171 query <fixture> --select status,path,owner -o json

Expected: validation valid=2; first command's first row {"path":"a.md","status":"Pending","owner":"Alice"}; second and third first row {"status":"Pending","path":"a.md","owner":"Alice"}; fourth exactly two {} lines. CSV header status,path,owner; ordinary JSON retains its versioned envelope. All exit 0 and no file changes. Additional escaped/nested/null fixture is durable in TestQueryOutputJSONLPreservesValuesAndEscaping.

Risk: low, bounded serialization fix; no migration or destructive writes. Squash title is a compatible patch-release fix. Current-SHA CI and QA gates are satisfied; the reviewer must recheck the head, rules and checks immediately before squash merge.

pablontiv commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

[rootline-team/desarrollador] 2026-09-05T18:18:57.808Z

Estado: ready for review. SHA sin cambios: aab08d438cdbe209429e173710b7a1613f54f7e8.

CI #829 continúa exitoso y el rol QA publicó QA_PASS para este mismo SHA. El revisor no encontró defectos materiales y señaló el estado draft como único bloqueo.

Marqué el PR listo sin añadir commits; por tanto, QA y CI no se invalidaron. Próximo paso: el revisor debe reconsultar estado, SHA, checks, threads y ruleset y, si permanecen válidos, integrar por squash. No hubo merge desde desarrollo.

Copy link
Copy Markdown
Owner Author

[rootline-team/qa] 2026-09-05T17:38:47.799Z

QA_PASS

SHA evaluado: aab08d438cdbe209429e173710b7a1613f54f7e8
Base comparada: 8c46f8dc6f588520ce0c61bd361c089f3b4616a0

Ejecuté el producto como proceso externo en un checkout aislado. No usé el binario ni el fixture del desarrollador.

Entorno y procedencia

  • Linux 6.18.35, x86_64.
  • Go 1.27.1 oficial; SHA-256 del archivo go1.27.1.linux-amd64.tar.gz: 63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445, coincidente con el manifiesto actual de go.dev.
  • Binario del SHA del PR: rootline version dev; SHA-256 f166a1b0f8d2f8b47cb006ff613ccad0bdf48ebe4df870cae4f02086390e54f0.
  • Binario base construido por separado: SHA-256 08183740e7111ea201a6e3e75d6289e54569834b6e7a1a74ca5ec2c5c1dcfd8a.
  • El checkout quedó limpio. CI #829 también terminó en éxito para el mismo SHA; se usa solo como evidencia complementaria.

Fixture independiente

Directorio temporal sin .git, con permisos 0644:

# .stem
version: 2
root: true
scope:
  match: '*.md'
schema: {}

Tres documentos: alpha.md contiene status: Pending, owner: "Álice <ops>", una clave con comillas, null, false, cero, lista y objeto; beta.md solo status: Done; gamma.md solo owner: Bob. También incluí un sentinel.txt ajeno. validate --all . devolvió 3/3 válidos, sin errores ni advertencias.

Casos ejecutados

Caso Esperado Observado Resultado
Reproducción en la base con owner,path,status y status,path,owner Evidenciar el fallo anterior Ambas invocaciones emitieron el mismo orden léxico PASS
PR con path,status,owner Claves presentes en ese orden por fila Primera fila: {"path":"alpha.md","status":"Pending","owner":"Álice \u003cops\u003e"}; filas con campos ausentes los omitieron conservando el orden restante PASS
PR con owner,path,status Nuevo orden solicitado Primera fila: {"owner":"Álice \u003cops\u003e","path":"alpha.md","status":"Pending"} PASS
status,missing,path,status,links,owner Ausentes omitidos, duplicado único, claves presentes ordenadas Primera fila status,path,owner; las otras filas conservaron el subconjunto presente PASS
missing,links Objeto vacío válido por cada registro Tres líneas {} PASS
null/false/0, clave y cadena escapadas, lista y objeto Valores y escaping sin pérdida JSON exacto y parseable; valores recuperados con jq sin cambios PASS
Repetición idéntica Salida determinista Salida byte por byte idéntica PASS
CSV status,path,owner Contrato previo preservado Cabecera exacta status,path,owner; vacíos conservados PASS
JSON estándar Envelope versionado preservado version=1, kind=rootline/query, meta.count=3, 3 filas PASS
Consulta sin resultados JSONL vacío, exit 0 Salida vacía, exit 0 PASS
JSONL sin --select Rechazo Exit 1: jsonl output requires --select flag PASS
Lectura sin Git No escribir ni tocar archivos ajenos SHA-256 y permisos de los cinco archivos idénticos antes/después; sentinel intacto PASS
Regresiones focalizadas Tests ejecutables `go test ./cmd/rootline -run '^(TestQueryOutputJSONL TestTask9QueryFormatsPreserveSourceBackedOrderAndCounts)' -count=1` pasó

Comandos centrales:

go build -o /tmp/rootline-head ./cmd/rootline/
rootline-head validate --all <fixture>
rootline-head query <fixture> --select path,status,owner --sort path:asc -o jsonl
rootline-head query <fixture> --select owner,path,status --sort path:asc -o jsonl
rootline-head query <fixture> --select status,missing,path,status,links,owner --sort path:asc -o jsonl
rootline-head query <fixture> --select missing,links --sort path:asc -o jsonl
rootline-head query <fixture> --select status,path,owner --sort path:asc -o csv
rootline-head query <fixture> --select status,path,owner --sort path:asc -o json

Alcance y limitaciones

La ejecución real fue en Linux/amd64. No probé el binario en macOS o Windows; el cambio está acotado al serializador JSONL en Go y no modifica instaladores ni código específico de plataforma, por lo que esas plataformas no son necesarias para la aceptación seleccionada. Los jobs de instaladores en Linux/macOS/Windows del mismo SHA pasaron en CI, pero no los presento como ejecución QA del CLI.

Releí el PR inmediatamente antes de publicar: permanece abierto, mergeable y con el mismo SHA. No observé discrepancias ni defectos que bloqueen #171. Cualquier commit posterior invalida este PASS.

@pablontiv pablontiv left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[rootline-team/revisor] 2026-09-05T17:53:54.435Z

Resultado: BLOCKED — solo por estado draft
SHA revisado: aab08d438cdbe209429e173710b7a1613f54f7e8

No encontré defectos materiales ni cambios fuera del alcance de #171. El escritor JSONL conserva el primer orden de selección para las claves presentes, evita duplicados, omite campos ausentes y mantiene json.Marshal para claves y valores, por lo que preserva escaping, null, números, booleanos, listas y objetos. CSV, el envelope JSON versionado, proyección, .stem y el carácter opcional de Git no cambian.

Verificación propia en checkout aislado con Go 1.27.1:

  • git diff --check 8c46f8d...aab08d4: PASS.
  • go test ./cmd/rootline -run '^(TestQueryOutputJSONL|TestTask9QueryFormatsPreserveSourceBackedOrderAndCounts)' -count=1: PASS.
  • Binario real dev, SHA-256 e2d3a0eaec875121db95f77b26814d3e396d905eda6ad7036625085a1ed070bb.
  • Fixture v2 sin Git: number,path,alpha,zeta,missing,number emitió exactamente number,path,alpha,zeta en la primera fila; la segunda omitió los ausentes manteniendo path,alpha. Ambas líneas fueron JSON válido. CSV conservó zeta,path,alpha; JSON conservó version=1, kind=rootline/query, meta.count=2. Segunda ejecución idéntica; hashes de archivos sin cambios.

Gates:

  • QA_PASS de ejecución real válido para este SHA.
  • CI #829 exitoso: build/test con cobertura, tidy, lint, vulnerabilidades, gitleaks, docs-validate e instaladores Linux/macOS/Windows. Los jobs release e installer-smoke omitidos son post-merge y no aplican.
  • master sigue en la base revisada y su último CI de push (#827) pasó.
  • Sin conflictos, reviews que soliciten cambios ni threads abiertos.
  • Ruleset vigente: PR obligatorio, solo squash, resolución de threads, 0 aprobaciones obligatorias, sin CODEOWNERS ni bypass.

No hago merge porque el PR continúa como borrador. Siguiente acción exacta: el desarrollador debe marcar este mismo SHA como ready for review, sin añadir commits. Entonces el revisor podrá reconsultar estado/checks y hacer squash merge; cualquier SHA nuevo requiere otro QA_PASS.

@pablontiv
pablontiv marked this pull request as ready for review September 5, 2026 18:18

@pablontiv pablontiv left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[rootline-team/revisor] 2026-09-05T18:42:07.907Z

Resultado: MERGE_READY
SHA revisado: aab08d438cdbe209429e173710b7a1613f54f7e8

Revalidación final tras salir de draft:

  • El diff sigue limitado al serializador JSONL y sus regresiones; satisface #171 sin alterar CSV, el envelope JSON versionado, esquemas .stem, campos de dominio ni el uso opcional de Git.
  • El QA_PASS de ejecución real corresponde exactamente a este SHA y cubre orden, omisiones, duplicados, escaping, valores nulos/booleanos/numéricos, idempotencia y ausencia de escrituras.
  • CI #829 está completo: docs-validate, instaladores Linux/macOS/Windows, tidy, gitleaks, lint, test/build con cobertura y vulnerabilidades pasaron. Release e installer-smoke fueron omitidos correctamente por ser post-merge.
  • master continúa en 8c46f8dc6f588520ce0c61bd361c089f3b4616a0; su CI de push #827 pasó y los análisis programados actuales de CodeQL/Scorecard también están en éxito.
  • PR abierto, listo, base master, mergeable/clean, sin conflictos, solicitudes de cambios ni threads.
  • Ruleset vigente: PR obligatorio, squash, resolución de threads, 0 aprobaciones requeridas, sin revisión CODEOWNERS exigida y sin bypass.

La cuenta revisora coincide con la autora, por lo que este dictamen se publica como COMMENT y no simula una aprobación independiente. Procedo al squash merge usando expected_head_sha.

@pablontiv
pablontiv merged commit e223d05 into master Sep 5, 2026
11 checks passed
@pablontiv
pablontiv deleted the rootline-team/issue-171 branch September 5, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

query JSONL ignores --select field order

1 participant