feat(jtk): pass raw ADF through for rich-text fields - #486
Conversation
## 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.
62e5606 to
23cb362
Compare
monit-reviewer
left a comment
There was a problem hiding this comment.
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.goexplicitly 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 withjson.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.mdtools/jtk/README.mdtools/jtk/api/fields_test.gotools/jtk/api/markdown.gotools/jtk/api/markdown_test.gotools/jtk/internal/cmd/comments/comments.gotools/jtk/internal/cmd/comments/comments_test.gotools/jtk/internal/cmd/issues/create.gotools/jtk/internal/cmd/issues/create_test.gotools/jtk/internal/cmd/issues/update.gotools/jtk/internal/cmd/issues/update_test.gotools/jtk/internal/text/escapes.gotools/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)") |
There was a problem hiding this comment.
[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.
Closes #484.
#191 shipped markdown→ADF conversion but not raw ADF input, so documents markdown cannot express —
inlineCardsmart 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-emptycontentis 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--fieldoverrides.Behavior notes:
\n/\t/\\escape interpretation (interpreting escapes would corrupt the JSON before detection). README rows, flag help, and CHANGELOG updated to state this.version == 1and non-emptycontent; a content-less doc falls through to markdown rather than marshalling"content": null, which Jira rejects.type/attrs/content/text/marksare dropped and numeric attrs round-trip throughfloat64. Spec-conformant ADF is unaffected; the doc comment states the limitation.text.InterpretEscapesUnlessRawADF, one helper for all three cmd-layer call sites;api/stays free ofinternal/*imports.Known gap, deliberately out of scope: built-in doc-typed fields (e.g.
environment) via--fieldstill send a bare string —FormatFieldValueroutes onlycustomfieldtypes:textareato 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 provinginlineCardnodes and literal\nsequences survive to the wire, including a--fieldvalue whoseinlineCardURL contains=in query params.go build,go vet,golangci-lint runclean;go test -race ./...green except the pre-existingTestRunList_Paginationfailure ininternal/cmd/boards, confirmed identical on unmodifiedorigin/main(d97a005).