Skip to content

feat(jtk): pass raw ADF through for rich-text fields - #486

Merged
zzwong merged 2 commits into
mainfrom
zzwong/484-raw-adf-passthrough
Aug 28, 2026
Merged

feat(jtk): pass raw ADF through for rich-text fields#486
zzwong merged 2 commits into
mainfrom
zzwong/484-raw-adf-passthrough

Conversation

@zzwong

@zzwong zzwong commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #484.

#191 shipped markdown→ADF conversion but not raw ADF input, so documents markdown cannot express — inlineCard smart links being the everyday case — had no jtk path at all. This adds auto-detect passthrough: input to a rich-text field that parses as JSON shaped {"type":"doc","version":1,...} with non-empty content is sent as a structured ADF object; everything else takes the existing markdown path byte-for-byte unchanged. No new flags.

Covers all four entry points via the shared conversion path (MarkdownToADF): issues create/issues update --description, comments add --body, and textarea --field overrides.

Behavior notes:

  • Raw ADF input skips \n/\t/\\ escape interpretation (interpreting escapes would corrupt the JSON before detection). README rows, flag help, and CHANGELOG updated to state this.
  • Detection requires version == 1 and non-empty content; a content-less doc falls through to markdown rather than marshalling "content": null, which Jira rejects.
  • Passthrough is an unmarshal/re-marshal through the internal ADF types, not a byte copy: node keys outside type/attrs/content/text/marks are dropped and numeric attrs round-trip through float64. Spec-conformant ADF is unaffected; the doc comment states the limitation.
  • The escape guard lives in text.InterpretEscapesUnlessRawADF, one helper for all three cmd-layer call sites; api/ stays free of internal/* imports.

Known gap, deliberately out of scope: built-in doc-typed fields (e.g. environment) via --field still send a bare string — FormatFieldValue routes only customfieldtypes:textarea to ADF. Pre-existing; worth its own issue if wanted.

Testing: detection unit tests (valid doc, doc with inlineCard, invalid JSON, non-doc JSON including arrays, markdown starting with {, versions 0/-1/99, content-less doc); request-body fixture tests for all four entry points proving inlineCard nodes and literal \n sequences survive to the wire, including a --field value whose inlineCard URL contains = in query params. go build, go vet, golangci-lint run clean; go test -race ./... green except the pre-existing TestRunList_Pagination failure in internal/cmd/boards, confirmed identical on unmodified origin/main (d97a005).

## Problem

#191 shipped markdown-to-ADF conversion but not raw ADF input.
`jtk issues update KEY --description "$(cat doc.adf.json)"` converted
the JSON as markdown text, writing a literal JSON paragraph. There was
no way to send an already-built ADF document (e.g. one containing an
inlineCard node the markdown converter has no syntax to produce).

## Change

MarkdownToADF (tools/jtk/api/markdown.go) now checks, before any
markdown or wiki handling, whether the input is JSON shaped like an ADF
document ({"type":"doc","version":1,...} with at least one content
node). If so it's unmarshaled into the internal ADF types and
re-marshaled from them, skipping markdown conversion entirely. This is
not a byte-for-byte passthrough: unknown node keys outside
type/attrs/content/text/marks are dropped and numeric attrs round-trip
through float64, though spec-conformant ADF is unaffected. Anything
else — invalid JSON, valid JSON that isn't a doc, an unrecognized
version, a content-less doc, or markdown that merely starts with "{"
— falls through to markdown conversion unchanged. Version must be
exactly 1 (int zero value doubles as "missing"), and content must be
non-empty: marshaling a nil/empty Content produces "content": null,
which Jira's API rejects with a 400.

This is the single choke point for --description on `issues create` and
`issues update`, `comments add --body`, and rich-text `--field`
overrides (textarea custom fields) — all four route through
api.NewADFDocument -> MarkdownToADF.

Three call sites (issues create/update description, comments add body)
pre-apply escape interpretation for CLI \n/\t convenience before handing
text to the converter. Escape interpretation would corrupt raw ADF JSON
(e.g. turning an escaped "\n" inside a JSON string into a literal
newline byte, making it invalid JSON) if applied first. The
per-call-site guard is now a single shared helper,
text.InterpretEscapesUnlessRawADF (internal/text/escapes.go), which all
three sites call instead of duplicating the api.IsRawADFDocument check.
internal/text importing api does not create a cycle (api has no
internal/ imports); confirmed with `go list -deps`.

Flag help (`issues create/update -d`, `comments add -b`) and the README
flag tables now note the raw-ADF passthrough behavior alongside the
existing escape-sequence note. CHANGELOG gets an Added entry under
[Unreleased] referencing #484.

## Testing

- go build, go vet, and go test -race for tools/jtk/ all pass (1946
  tests). The only failure in the full suite is TestRunList_Pagination
  (internal/cmd/boards), confirmed pre-existing: reproduced identically
  with these changes stashed out of the working tree.
- golangci-lint run clean; go mod tidy produces no diff.
- api/markdown_test.go: detector/passthrough unit tests now also cover
  version 99, version -1, version 0 (explicit and missing), and a
  version-1 doc with no/empty content — all correctly rejected and
  falling through to markdown conversion.
- issues create_test.go, issues update_test.go, and
  comments/comments_test.go: the raw-ADF passthrough fixtures for
  --description/--body now include a text node with an escaped "\n" in
  the JSON source (mirroring the existing update_test.go coverage),
  proving escape interpretation does not run ahead of raw-ADF detection
  and corrupt the JSON.
- issues update_test.go adds an end-to-end test for the rich-text
  --field path: `--field customfield_XXXXX=<raw ADF>` with an inlineCard
  URL containing query-string "=" and "&" characters, locking in that
  ResolveFieldArg's SplitN(arg, "=", 2) only splits on the first "=".
- internal/text/escapes_test.go adds coverage for the new
  InterpretEscapesUnlessRawADF helper.
@zzwong
zzwong force-pushed the zzwong/484-raw-adf-passthrough branch from 62e5606 to 23cb362 Compare August 28, 2026 05:22

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: 23cb362d2d59
Profile: pi-codex-monit-reviewer - Posting as: monit-reviewer

Summary

Reviewer Findings
go:implementation-tests 0
policies:conventions 0
architecture:solid-reviewer-agnostic 1
architecture:solid-reviewer-agnostic (1 finding)

Minor - tools/jtk/internal/cmd/comments/comments.go:172

[U-L1] The user-visible contract says raw ADF passes through “verbatim,” but api/markdown.go explicitly unmarshals/re-marshals it, dropping unknown node keys and potentially changing numeric attrs. Accepted documents from extended/newer ADF tooling can therefore be silently altered. Either preserve validated input with json.RawMessage, or change this and the matching create/update help text to describe parsed/normalized structured ADF and its limitations.

Reviewer Coverage

  • go:implementation-tests — complete (broad); inspected 11 assigned files (13 inspected across reviewers): tools/jtk/api/fields_test.go, tools/jtk/api/markdown.go, tools/jtk/api/markdown_test.go, tools/jtk/internal/cmd/comments/comments.go, tools/jtk/internal/cmd/comments/comments_test.go, tools/jtk/internal/cmd/issues/create.go, tools/jtk/internal/cmd/issues/create_test.go, tools/jtk/internal/cmd/issues/update.go, tools/jtk/internal/cmd/issues/update_test.go, tools/jtk/internal/text/escapes.go, tools/jtk/internal/text/escapes_test.go; skipped: none; constraints: Inspection was limited to the pinned diff and read-only repository tools; tests were not executed.
  • policies:conventions — complete (broad); inspected 6 assigned files (13 inspected across reviewers): tools/jtk/CHANGELOG.md, tools/jtk/README.md, tools/jtk/internal/cmd/comments/comments.go, tools/jtk/internal/cmd/issues/create.go, tools/jtk/internal/cmd/issues/update.go, tools/jtk/internal/text/escapes.go; skipped: none; constraints: Shared CLI standards were not present in the review context; this review is limited to conventions evidenced by the changed repository files and provided dossier.
  • architecture:solid-reviewer-agnostic — complete (broad); inspected 5 assigned files (13 inspected across reviewers): tools/jtk/api/markdown.go, tools/jtk/internal/cmd/comments/comments.go, tools/jtk/internal/cmd/issues/create.go, tools/jtk/internal/cmd/issues/update.go, tools/jtk/internal/text/escapes.go; skipped: none; constraints: Read-only CR tools do not support executing build, test, lint, or race checks; I inspected the diff and surrounding source only. The external SignalFT harness principles file was outside the disposable repository and unavailable through permitted CR tools.
Inspected files (13)
  • tools/jtk/CHANGELOG.md
  • tools/jtk/README.md
  • tools/jtk/api/fields_test.go
  • tools/jtk/api/markdown.go
  • tools/jtk/api/markdown_test.go
  • tools/jtk/internal/cmd/comments/comments.go
  • tools/jtk/internal/cmd/comments/comments_test.go
  • tools/jtk/internal/cmd/issues/create.go
  • tools/jtk/internal/cmd/issues/create_test.go
  • tools/jtk/internal/cmd/issues/update.go
  • tools/jtk/internal/cmd/issues/update_test.go
  • tools/jtk/internal/text/escapes.go
  • tools/jtk/internal/text/escapes_test.go

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 4m 09s | $0.21 | openai-codex/gpt-5.6-terra, openai-codex/gpt-5.6-sol | cr dev
Field Value
Model openai-codex/gpt-5.6-terra, openai-codex/gpt-5.6-sol
Reviewers go:implementation-tests, policies:conventions, architecture:solid-reviewer-agnostic
Engine pi_rpc · openai-codex/gpt-5.6-terra, openai-codex/gpt-5.6-sol
Reviewed by cr · monit-reviewer
Duration 4m 09s wall · 7m 14s compute
Cost $0.21
Tokens 28.2k in / 3.6k out

Per-workstream usage

  • orchestrator-selection — openai-codex/gpt-5.6-terra
    • In: 8.9k
    • Out: 474
    • Cache read: 0
    • Cache create: 0
    • Cost: $0.03
    • Duration: 11s
  • go:implementation-tests — openai-codex/gpt-5.6-terra
    • In: 2.0k
    • Out: 466
    • Cache read: 17.9k
    • Cache create: 0
    • Cost: $0.02
    • Duration: 2m 09s
  • policies:conventions — openai-codex/gpt-5.6-terra
    • In: 1.7k
    • Out: 331
    • Cache read: 8.7k
    • Cache create: 0
    • Cost: $0.01
    • Duration: 1m 04s
  • architecture:solid-reviewer-agnostic — openai-codex/gpt-5.6-sol
    • In: 10.7k
    • Out: 2.1k
    • Cache read: 50.7k
    • Cache create: 0
    • Cost: $0.14
    • Duration: 3m 43s
  • orchestrator-rollup — openai-codex/gpt-5.6-terra
    • In: 5.0k
    • Out: 164
    • Cache read: 0
    • Cache create: 0
    • Cost: $0.01
    • Duration: 5s

}

cmd.Flags().StringVarP(&body, "body", "b", "", "Comment text (required)")
cmd.Flags().StringVarP(&body, "body", "b", "", "Comment text (raw ADF JSON passes through verbatim) (required)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[U-L1] The user-visible contract says raw ADF passes through “verbatim,” but api/markdown.go explicitly unmarshals/re-marshals it, dropping unknown node keys and potentially changing numeric attrs. Accepted documents from extended/newer ADF tooling can therefore be silently altered. Either preserve validated input with json.RawMessage, or change this and the matching create/update help text to describe parsed/normalized structured ADF and its limitations.

Reply inline to this comment.

@zzwong
zzwong marked this pull request as ready for review August 28, 2026 05:42
@zzwong
zzwong merged commit ea45879 into main Aug 28, 2026
16 checks passed
@zzwong
zzwong deleted the zzwong/484-raw-adf-passthrough branch August 28, 2026 05:44
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.

jtk: pass raw ADF through for rich-text fields (completes #191)

2 participants