This document defines the output architecture for this repository.
It exists to prevent a recurring anti-pattern across the Go CLIs in this repo:
- commands fetch Atlassian data
- commands assemble ad hoc display strings
- shared view helpers rewrite or reshape those strings
That approach is easy to start and hard to scale. It creates weak boundaries, stringly-typed rendering code, and brittle tests.
The target architecture is a clean, testable pipeline:
Atlassian/API domain data -> presenter/formatter -> presentation model -> renderer -> CLI output
- Keep domain concerns separate from presentation concerns
- Make each stage of the output pipeline pure and easy to unit test
- Make new formatters and renderers easy to add
- Prevent
shared/viewfrom becoming a string-massaging utility - Support multiple output styles without embedding formatting rules in commands
This layer contains:
- Atlassian API response types
- internal domain projections
- artifact projections where already established
It should not contain display layout concerns.
Examples:
tools/jtk/api/...tools/cfl/api/...internal/artifact/...
This layer maps domain values into a presentation model.
It is responsible for deciding:
- which fields are shown
- field labels
- field ordering
- section ordering
- truncation
- normalization
- empty-state wording
- stream destination for diagnostics, advisory messages, and primary output
- whether a value is represented as detail, table, message, or nested sections
This layer should be pure:
- input: domain value
- output: presentation model
- no IO
- no CLI side effects
This layer turns a presentation model into a concrete output style.
Examples:
- agent renderer
- human renderer
- table-oriented renderer
- plain text renderer
This layer is responsible for layout and style only. It should not:
- inspect raw API types
- decide which domain fields are important
- normalize arbitrary content strings after the fact
- escape or repair display content that should have been handled by the presenter
It should be pure:
- input: presentation model
- output: rendered string or bytes
- no IO
Commands should orchestrate, not format.
The command layer should:
- fetch domain data
- choose the right presenter
- choose the right renderer or output mode
- write final output
The command layer should not:
- manually build user-facing tables
- manually assemble
Key: Valuestrings - manually construct presenter-owned DTOs, fields, rows, labels, or section order
- perform inline truncation/escaping/label selection except as transitional code during migration
The key boundary in this repo is:
domain model != presentation model != rendered string
If a function accepts or returns [][]string, []string, or preformatted display text as its primary contract, that is usually too low-level for new architecture work.
Prefer explicit presentation types.
The presentation model should be small and boring. It does not need to be generic-heavy or framework-like.
A minimal shape is enough:
type OutputModel struct {
Sections []Section
}
type Section interface {
isSection()
}
type DetailSection struct {
Fields []DetailField
}
type TableSection struct {
Columns []Column
Rows []TableRow
}
type MessageSection struct {
Kind MessageKind
Message string
}
type DetailField struct {
Label string
Value string
}
type Column struct {
Key string
Label string
}
type TableRow struct {
Cells []Cell
}
type Cell struct {
Value string
}This is illustrative, not mandatory. The important point is that presenters return structured display intent, and renderers turn that into final text.
For most cases, prefer a structured presenter plus renderer split:
type Presenter[T any] interface {
Present(T) OutputModel
}
type Renderer interface {
Render(OutputModel) string
}This keeps the renderer dumb and reusable.
Avoid collapsing everything into a single formatter that directly returns strings unless the scope is extremely small and unlikely to grow.
This repository already distinguishes between:
- artifact shape
- output format
- command intent
See docs/ARTIFACT_CONTRACT.md.
That contract still applies. This document adds the architecture for how those outputs should be constructed.
In practice:
- artifact selection decides what information is relevant
- presenter shapes that information into a presentation model
- renderer decides how that presentation model is displayed
JSON and artifact paths are first-class output pathways. They are not fallback representations and should not be incidental byproducts of text rendering.
Rules:
- preserve existing JSON/artifact contracts unless intentionally changing them
- do not route JSON through text-focused helpers
- do not use text-oriented helpers as the canonical output model for JSON
The same domain value may support:
- structured artifact projection for internal shaping and presenter input
- presentation modeling for text output
These are related, but not identical, responsibilities.
For new rendering work in this repo:
- Do not build user-facing strings in commands except for trivial dispatch or temporary migration glue.
- Do not make
shared/viewresponsible for rewriting arbitrary content strings. - Do not normalize, truncate, or escape display content in the renderer if that decision belongs to presentation logic.
- Do not pass raw Atlassian API DTOs directly into low-level rendering helpers.
- Do not encode presentation intent as
[][]stringin new code when a structured presentation model would make the boundary clearer. - Do not add output policy by sprinkling ad hoc conditionals through commands.
- Do not make commands construct presenter-owned DTOs, fields, rows, labels, or section ordering.
New work in this area should be test-first.
Given a domain value, assert the exact presentation model produced.
These should be:
- pure unit tests
- no Cobra
- no stdout/stderr
- no network mocks unless the presenter itself requires a domain object assembled by a higher layer
Given a presentation model, assert the exact rendered output.
These should be:
- pure unit tests
- exact-string assertions where the contract matters
- separated by render style
Commands should have lighter tests that prove:
- the correct presenter/renderer path is used
- the correct mode is chosen
- JSON/artifact behavior is preserved
The deeper contract tests belong in presenter and renderer tests, not only in Cobra command tests.
When refactoring existing output code:
- Identify one command/output shape.
- Introduce a presenter for that domain object.
- Introduce or adapt a renderer for the desired output style.
- Lock the behavior with presenter and renderer tests.
- Replace command-local string building with presenter/renderer wiring.
- Repeat by output shape, not by scattered one-off string fixes.
Preferred rollout order:
- representative detail view
- representative table view
- representative mutation/message output
- composite/nested output
- broader migration
shared/view should evolve toward rendering presentation models, not performing string surgery on arbitrary display text.
Its role should be:
- accept a structured presentation model
- render it according to policy/style
- remain pure where feasible
It should not be the place where content semantics are repaired after commands have already flattened domain data into strings.
The same architectural rules apply to both tools.
It is acceptable for one tool to adopt a renderer/policy earlier than the other, but the shared abstractions should be designed so both can use them without reintroducing command-local formatting logic.
The artifact contract remains the source of truth for intentional output content. This architecture defines how that content should move through the system cleanly.
Treat these as warning signs in PR review:
- command code builds
[]stringrows directly from API response fields - command code uses
fmt.Sprintfonly to create display values - renderer escapes delimiters or rewrites content strings to recover structure
- output helpers require callers to pre-flatten structured data into strings
- tests only check
strings.Containsfor new output contracts - adding a new output style requires editing many unrelated commands
The architecture is in a good state when:
- commands mostly orchestrate
- presenters own content decisions
- renderers own display style only
- new presenters are easy to add
- new renderers are easy to add
- most output behavior is testable without Cobra or stdout
This document does not require:
- a large inheritance hierarchy
- a complicated registry system
- reflection-heavy rendering
- a framework-style abstraction layer
Prefer simple explicit types and composition.
The standard for success is not “abstract.” It is “cleanly separated, easy to test, easy to extend.”