Technical Decision Register (TDR)¶
This register records important architectural and product decisions for Prince of Space. Use it as the primary source of why design choices exist.
How to use this document¶
- Read this file first when making non-trivial changes.
- Add new entries as append-only records (do not rewrite history).
- If a decision is superseded, mark it as superseded and link the replacement entry.
Decision entries¶
TDR-001: Small, curated configuration surface¶
- Date: 2026-04
- Status: Superseded by TDR-014
- Decision: Keep a bounded set of public formatter knobs (now 7 options), not zero-config and not highly granular.
- Rationale: Java teams need some style flexibility, but too many options cause bikeshedding and inconsistent output.
- Consequences:
FormatterConfigremains intentionally small; feature requests for new options require strong justification. - Related docs:
docs/formatting-rules.md,docs/architecture.md
TDR-002: JavaParser-based formatting pipeline¶
- Date: 2026-04
- Status: Accepted
- Decision: Use JavaParser AST + custom pretty-printing for formatting.
- Rationale: Good API ergonomics, practical language coverage, and comment-aware workflow for formatter development velocity.
- Consequences: Language-level handling depends on JavaParser support; parser upgrades are part of maintenance.
- Related docs:
docs/architecture.md, TDR-016
TDR-003: Separation of public API and internal implementation¶
- Date: 2026-04
- Status: Accepted
- Decision: Keep public API minimal (
io.princeofspace,io.princeofspace.model); implementation belongs inio.princeofspace.internal. - Rationale: Preserves API stability while allowing internal refactoring.
- Consequences: New public classes are rare;
Formatterdelegates to internal engine classes. - Related docs:
docs/architecture.md
TDR-004: Single line length threshold¶
- Date: 2026-04
- Status: Accepted (revised)
- Decision: Use a single
lineLengththreshold instead of dualpreferredLineLength+maxLineLength. - Rationale: The dual-threshold model added complexity without meaningful benefit — the gap between preferred and max was rarely useful and made the API harder to understand. A single threshold is simpler, matches Prettier's
printWidthmodel, and produces equivalent output. - Consequences: Single wrapping threshold; simpler config surface.
- Related docs:
docs/formatting-rules.md,modules/core/src/test/java/io/princeofspace/WrappingFormattingTest.java
TDR-005: Wrap styles are strategy-level, not per-construct settings¶
- Date: 2026-04
- Status: Accepted
- Decision: Expose
WIDE,BALANCED,NARROWwrap styles globally, rather than many per-node options. - Rationale: Keeps configuration understandable and predictable.
- Consequences: Some edge cases are solved in formatter heuristics, not by adding bespoke knobs.
- Related docs:
docs/formatting-rules.md
TDR-006: Idempotency is a hard invariant¶
- Date: 2026-04
- Status: Accepted
- Decision: Treat
format(format(x)) == format(x)as mandatory behavior. - Rationale: Non-idempotent formatters are unstable in CI and editor workflows.
- Consequences: Every new formatter behavior requires idempotency tests.
- Related docs:
docs/architecture.md,modules/core/src/test/java/io/princeofspace
TDR-007: Module split includes both normal and bundled core artifacts¶
- Date: 2026-04
- Status: Accepted
- Decision: Publish both
core(normal deps) andcore-bundled(shaded) artifacts. - Rationale: Supports both regular build integrations and classloader-sensitive environments.
- Consequences: Behavior parity between artifacts is tested and documented.
- Related docs:
docs/architecture.md
TDR-008: Integrations are first-class (CLI, Spotless, IntelliJ, VS Code)¶
- Date: 2026-04
- Status: Accepted
- Decision: Treat integrations as product features, not side projects.
- Rationale: Formatter adoption depends on integration quality as much as formatting quality.
- Consequences: Integration modules are maintained with tests/docs and kept aligned with core behavior.
- Related docs:
README.md,modules/intellij-plugin/README.md,modules/vscode-extension/README.md
TDR-009: Real-world eval harness for Guava and Spring¶
- Date: 2026-04
- Status: Accepted
- Decision: Use evaluation runs on large external codebases (Guava and Spring) as regression quality gates.
- Rationale: Synthetic tests alone miss important style and stability edge cases.
- Consequences: Eval reports are tracked under
docs/eval-results/; parse errors and idempotency failures must remain zero. - Related docs:
docs/evaluation.md,docs/eval-results/
TDR-010: Documentation structure shifts from plans to decisions¶
- Date: 2026-04
- Status: Accepted
- Decision: Prefer decision records + architecture docs over active “implementation plan” narrative docs.
- Rationale: The project is beyond early scaffolding; historical plans are useful context but no longer primary guidance.
- Consequences: Keep research/priorities historical context, and remove stale implementation-plan/roadmap checklists from active docs.
- Related docs: TDR-016
TDR-011: WrapStyle behavior for string concatenation is construct-uniform¶
- Date: 2026-04
- Status: Accepted
- Decision: Treat
+string concatenation wrapping the same as other list-like constructs forWrapStylepolicy. - Rationale:
BALANCEDshould mean fit-or-tall consistently; allowing greedy packing only for string concatenation made behavior surprising and undermined predictability. - Consequences:
BALANCEDandNARROWnow put each+operand on its own continuation line when wrapping;WIDEretains greedy packing. - Related docs:
docs/formatting-rules.md,modules/core/src/test/java/io/princeofspace/WrappingFormattingTest.java
TDR-012: continuationIndentSize is additive¶
- Date: 2026-04
- Status: Accepted
- Decision: Interpret
continuationIndentSizeas an indent delta added on top of the active enclosing indent, not as an absolute column from statement start. - Rationale: Additive continuation indent yields consistent visual depth across nested contexts and avoids surprising left shifts for wrapped chains inside expressions.
- Consequences: Wrapped segments in nested expressions use the same continuation math as top-level wrapped segments; docs and tests should assert additive behavior.
- Related docs:
docs/formatting-rules.md,modules/core/src/test/java/io/princeofspace/WrappingFormattingTest.java
TDR-014: Remove continuationIndentSize config, hardcode to 2 × indentSize¶
- Date: 2026-04
- Status: Accepted
- Decision: Remove
continuationIndentSizeas a public configuration knob. Continuation indent is now always2 * indentSize, following the Oracle/IntelliJ convention. - Rationale: When
continuationIndentSize == indentSize(the previous default), wrapped method parameters and the method body are indented to the same column, making them visually indistinguishable. The2×convention eliminates this ambiguity by construction. No well-known opinionated Java formatter (google-java-format, Prettier, Black, ktlint) exposes continuation indent as a config knob. Reducing from 8 to 7 options simplifies the configuration surface and halves the showroom golden matrix (48→24 files). - Consequences: The
FormatterConfigrecord no longer has acontinuationIndentSizerecord component; a derived methodcontinuationIndentSize()returns2 * indentSize. Showroom goldens drop thecont4/cont8filename axis. IntelliJ plugin settings UI no longer shows a continuation indent spinner. TDR-012 (additive continuation indent) still applies — the indent is additive, just no longer user-configurable. - Related docs:
docs/formatting-rules.md,docs/canonical-formatting-rules.md,docs/architecture.md
TDR-013: Showroom rule-uniformity migration is complete¶
- Date: 2026-04
- Status: Accepted
- Decision: The showroom rule-uniformity work is complete:
wrapStylebehavior is consistent across the showroom’s list-like and wrapping constructs, with regression coverage inWrappingFormattingTestand an overview check inRuleUniformityTest. (Earlier stepwise tasks spannedWidthMeasurerintroduction,BALANCEDstring concat alignment with TDR-011, shared comma-list wrapping for enum/array/type parameters,extendsclause wrapping,closingParenOnNewLineunification, try-with-resources/for/switchwrapping, andAnnotationArranger/BlankLineNormalizeralignment.) - Rationale: One wrap vocabulary (
wide/balanced/narrow) keeps configuration predictable; the migration aligned docs, the Java printer, and golden outputs. - Consequences: Further wrapping tweaks should update
docs/formatting-rules.mdand the showroom in lockstep; avoid reintroducing per-construct ad-hoc wrap semantics without a TDR. - Related docs:
docs/formatting-rules.md,modules/core/src/test/java/io/princeofspace/RuleUniformityTest.java
TDR-015: Wrapped method chains use indentSize, not 2 × indentSize¶
- Date: 2026-04
- Status: Accepted
- Decision: When a method chain wraps and each
.method(...)segment goes on its own continuation line, indent each segment by exactly oneindentSizestep beyond the receiver's line — not the2 * indentSizecontinuation indent used for delimited list continuations (Rule 3 / TDR-014). - Rationale: The
2 * indentSizecontinuation indent exists to make wrapped parameters visually distinct from the method body inside (e.g.void foo(\n String x) {\n body();). Method chains do not need that disambiguation: every segment already begins with a leading., which is its own visual delimiter, and the receiver itself sits at the enclosing block's indent. With the old2 * indentSizerule, deeply nested chains (a stream inside a.map(...)inside another stream) drifted far to the right and visually compounded the depth of plain Java code. Reducing the chain step to a single indent unit keeps wrapped chains readable while leaving non-chain continuations (parameter lists, binary expressions, ternaries, etc.) at the well-established2 * indentSizedepth. - Consequences:
MethodChainFormatteremits chain continuations via a newLayoutContext.printChainIndent()helper that prints exactly one indent step.- When a wrapped method chain appears as an operand of a wrapped binary chain (Rule 6),
BinaryExprFormatterpushes one extraindentSizeso chain segments remain visually distinct from the operator line that introduces them. Without this, segments would be flush with the operator continuation column and the operator/operand separation would be ambiguous. - All 24 showroom golden files were regenerated; existing chain assertions in
WrappingFormattingTestwere updated to reflect the new column math (chain at base + indentSize, lambda body inside a chain segment at chain + indentSize, text-block-receiver chain at base + indentSize). - TDR-012 (additive continuation indent) and TDR-014 (continuation indent is fixed at
2 * indentSize) still apply to every other wrapping construct; this TDR is a Rule 7 carve-out only. - Related docs:
docs/canonical-formatting-rules.md(Rules 3, 7),docs/formatting-rules.md(Part 1 §3, Part 3 "Method Chaining"),modules/core/src/main/java/io/princeofspace/internal/MethodChainFormatter.java,modules/core/src/main/java/io/princeofspace/internal/LayoutContext.java,modules/core/src/main/java/io/princeofspace/internal/BinaryExprFormatter.java
TDR-016: Mission, ecosystem context, and research bibliography¶
- Date: 2026-04
- Status: Accepted
- Decision: Retain the following as durable context (consolidated from former
docs/project-priorities.mdanddocs/research-notes.mdwhen those historical docs were retired). - Mission: Build a Java formatter that is readable, meaningfully configurable (small public surface: 7 options; see TDR-001, TDR-014), and straightforward to wire into real projects (see TDR-007, TDR-008).
- Ecosystem — pain points in other Java formatters (informal):
| Tool | Usual pain points (not exhaustive) |
|---|---|
| google-java-format | Effectively unconfigurable; 2-space default (non-Android); heavy rightward indent / lambdas often criticized |
| palantir-java-format | Very limited configurability; still a GJF-style fork in spirit |
| Eclipse JDT | Opaque XML; painful to use without the Eclipse config workflow |
| IntelliJ | No stable standalone CLI; hundreds of options encourage drift |
| Prettier (Java) | Node runtime; teams care about version churn vs JVM-native stacks |
| Spring Java Format | Fixed style, Eclipse-centric integration patterns |
Commentary is opinionated; teams differ. The point of the table is the product gap PoS is aimed at: Prettier/ktlint-like bounded config plus good JVM/CI/IDE story.
- Configuration sweet spot (research): Ecosystems show
gofmt-style 0 options work where the culture is uniform;black-style “few” options (line length, indents) cover most real disagreements; very large option sets (e.g. rustfmt-scale) add fatigue. Prince of Space targets a small curated surface (7 options) — see TDR-001. - What Java teams often rank highly when choosing formatters (informal): indent width; line length; lambda layout; method-chain layout; continuation indent; import policy (here delegated to Spotless, README non-goals); wrapping policy; blank-line policy.
- Parser choice (extends TDR-002): JavaParser was chosen for a public, comment-friendly AST, practical API/visitor model, and formatting-friendly workflows. Alternatives considered: Eclipse JDT — heavier, more IDE-coupled. javac internal tree (as used by some formatters) — strong language parity but comment handling and API stability are awkward for a new formatter. Spoon — JDT-based; more transformation-oriented than we need. The canonical “use JavaParser” decision remains TDR-002; this entry preserves why alternatives were less attractive.
- Spotless: First-party
PrinceOfSpaceStepand Spotless as the build-tool integration path are product decisions in TDR-008. Early research also noted Spotless’sFormatterStepmodel andcustom/ classpath integration patterns; seedocs/evaluation.mdfor the harness. - Bibliography (external background): Why are there no decent code formatters for Java? (Jan Ouwens); Prettier option philosophy; Google Java Style Guide; rustfmt configuration; Black; Spotless; Oracle Java code conventions (indentation).
- Consequences: Product positioning and ecosystem comparisons live here; normative formatter behavior remains
docs/canonical-formatting-rules.md. Historical priority-stack items (P0–P3) are subsumed by shipped modules and the decision register; treat them as background, not a roadmap checklist. - Related docs: TDR-002, TDR-007, TDR-008,
README.md,docs/evaluation.md
TDR-017: Nested wrapped (...) lists and type-body comment spacing¶
- Date: 2026-04
- Status: Accepted
- Decision: (1) For wrapped comma-separated argument lists only, push extra
SourcePrinterindent levels while printing list items so nested calls stack continuation indent correctly; suppress redundant explicitprintCont()in that scope so binary/operator continuations inside an argument do not double-count against the printer prefix. Do not activate that scope for a single wrapped call expression (e.g.new X("""...""".formatted(...))) so method-chain segments on the same argument keep Rule 7 column math. Wrapped<T, U>type-parameter breaks inside an argument still emit an explicit continuation viaLayoutContext.printRawContinuation(). Formal parameter lists continue to use the same indent push whenever parameters wrap. (2) Emit blank lines between type members instead of an unconditional leading newline before every member, so consecutive line comments before the first member are not separated by a manufactured blank line. - Rationale: Continuation was previously applied as a flat
2 * indentSizeprint on every wrapped line regardless of nesting, so inner)delimiters aligned with outer ones and looked “stacked” at the wrong column;printMembers’ leading newline interacted badly with orphan comment draining before the first field. - Consequences:
PrincePrettyPrinterVisitor.printArguments,ArgumentListFormatter,LayoutContext,DeclarationFormatter, andprintMembers; showroom goldens and wrapping/comment tests updated. - Related docs:
docs/canonical-formatting-rules.md(Rules 3, 8, 9, 10),WrappingFormattingTest,CommentPreservationTest
TDR-018: Conventional Commits for showroom vs. Nyx patch/minor¶
- Date: 2026-04
- Status: Accepted
- Decision: Document explicit guidance: prefer
feat:(andfeat!:/BREAKING CHANGE:when appropriate) for showroom and golden updates that reflect new or changed formatting behavior or new showcase coverage; reservefix:for bugfixes (incorrect output relative to the intended rules). This aligns changelog sections (“Added” vs “Fixed”) and version bumps (minor vs patch) with user-visible meaning. Nyx’s highest-bump-wins rule is unchanged: more patch releases in practice require release lines where only patch-level types appear, or separate releases. - Revision (same entry): (1) New numbered showroom scenario (or large showcase expansion) usually →
feat:— signals a broader, intentional change (example7e619f8), not a one-line hotfix. (2) Substantive edits todocs/canonical-formatting-rules.md(redefined rules, public knob removal, contract change) usually →feat!:with a footer when required — e.g.bd21397(TDR-014),846fa82(TDR-015). (3) Small canonical amendments that mainly document a bugfix (wrong output or a violated invariant) can stayfix:or usefeat:— e.g.db0658c. This does not mean “every output change is major” (TDR-018 discussion). - Rationale: Showroom diffs are often categorized as
fixby habit, which understates product impact and mis-files notes under “Fixed” when the work is a feature or contract change. Calling out breaking golden churn explicitly helps integrators. - Consequences: Maintainers and contributors follow
docs/contributing.md; no.nyx.ymlchange required. - Related docs:
docs/contributing.md,RELEASING.md,docs/showroom-scenarios.md
TDR-019: Nested wrapped-call closer alignment and single-arg wrap policy¶
- Date: 2026-04
- Status: Accepted
- Decision: Refine nested wrapped
(...)behavior in three parts: (1) for co-line nested wrapped call openers,closingParenOnNewLine=truemay compact closer runs on one closer line ());,)));) instead of emitting one)line per nesting level; line-separated nested openers keep separate aligned closer lines. (2) Wrapped delimited-list scope indentation is continuation-line aware: when a list opens on an already-continued line, list continuation lines are based on that effective continuation start column plus Rule 3's2 * indentSize. (3) The "single wrapped argument stays inline" carve-out is narrowed to scoped method-chain receivers only; other wrapped single arguments break before the argument body. - Rationale: Repeated user reports showed stacked closer columns and under-indented nested argument lists in ternary/binary continuation contexts. Earlier TDR-017 scope handling improved nested list indentation but left closer placement and single-arg wrapping edge cases unresolved.
- Consequences:
PrincePrettyPrinterVisitor,LayoutContext, andArgumentListFormatternow coordinate co-line closer compaction, continuation-aware wrapped-list scope entry, and single-arg break-before behavior. New regression coverage lives inClosingParenAlignmentTest; showroom goldens were regenerated to reflect updated nested-call output. - Related docs:
docs/canonical-formatting-rules.md(Rules 3, 8),docs/formatting-rules.md,modules/core/src/test/java/io/princeofspace/internal/ClosingParenAlignmentTest.java
TDR-020: Default convergence pass budget¶
- Date: 2026-04
- Status: Accepted
- Decision: Raise the engine's default
maxConvergencePasses(additional single-format attempts after the first) to 11, so the hard idempotency guarantee can be met in oneformat()call for WIDE mode at a short line length on large corpora (for example Spring Framework eval inputs). Override remainsprince.maxConvergencePasses(non-negative integer). - Rationale: The prior default (3 extra passes ⇒ 4 attempts total) was sufficient for typical inputs and comment re-attachment, but real sources showed monotonic refinement across many passes—greedy comma/call wrapping moving breakpoints until stable—not oscillation. Hitting
NonConvergentthere was a budget failure, not proof of non‑existence of a fixed point. - Consequences: Worst-case formatting work scales with the budget only when outputs keep changing; stable outputs still exit on the first equality check. Regression fixtures live in
WideSpringCorpusConvergenceRegressionTestunder test resources. - Related docs:
FormattingEngine,RealWorldEvalTest,RELEASING.md
TDR-021: Trailing-lambda layout keeps the lambda header on the call line¶
- Date: 2026-04-28 (extended 2026-04-29)
- Status: Accepted
- Decision: When the last argument of a wrapped method/constructor call is a lambda — block- or expression-bodied — keep any leading arguments and the lambda header (
() -> {,(a, b) -> {,s ->,value ->, etc.) on the call line, let the lambda body wrap according to its own rules (block body via its block indent; expression body via the receiver chain or other inner wrap mechanic), and place the closing)immediately after the lambda body (});,)),.lastSegment()), etc.) at the call's indent column — regardless ofclosingParenOnNewLine. This holds even when the resulting opener line slightly exceedslineLength. The rule covers single-argument calls and multi-argument calls alike; the only fallback is when a leading argument itself carries a leading line/block comment, or another leading argument is itself a block lambda (multi-block-lambda calls remain ambiguous and use the regular per-arg break path). Single-parameter unparenthesized lambda parameters (e.g.s,value) never themselves wrap because there is no syntactic break point — only multi-parameter parenthesized lambda parameter lists may wrap. - Rationale: Other mainstream formatters — palantir-java-format, Prettier, ktlint, and Google's style for Kotlin trailing-lambda — uniformly treat a trailing lambda as the "expanded" argument and keep its header inline so the body reads as a natural block or chain. The previous "one arg per line" wrap path placed
() ->/s ->on its own continuation line, which breaks the visual coupling between the call and the lambda body and reads worse on any non-trivial line. Soft overflow on the opener line is the standard tradeoff in those formatters because the alternative is uglier per-arg breakage. The extension to expression-bodied and single-arg lambdas was driven by feedback that.map(\n s -> s\n .toLowerCase()\n ...\n)reads strictly worse than.map(s -> s\n .toLowerCase()\n ...)for stream-like chains. - Consequences: Wrapped calls with a trailing lambda — single- or multi-arg, block- or expression-bodied — now render in palantir-style trailing-lambda layout, with the closing
)always inline with the lambda body andclosingParenOnNewLineoverridden in those cases. Idempotency holds because the layout is a fixed point of the trailing-lambda branch. TheprintArgumentstrailing-lambda branch also clears any stalecontinuationLineStartColumnso an inner wrapped call inside the lambda body anchors its indent to the surrounding block, not to a leftover continuation column from an earlier statement. Coverage:WrappingFormattingTest.trailingBlockLambda_*andWrappingFormattingTest.trailingLambda_*. - Related docs:
docs/canonical-formatting-rules.md(Rule 8),modules/core/src/main/java/io/princeofspace/internal/ArgumentListFormatter.java,modules/core/src/main/java/io/princeofspace/internal/PrincePrettyPrinterVisitor.java
TDR-022: Wrapped lambda parameter list closer aligns to the opener column¶
- Date: 2026-04-29
- Status: Accepted
- Decision: When a parenthesized lambda's formal parameter list wraps, the closing
) ->line aligns to the opener(column — i.e. the same indentation as the line containing(— rather than oneindentSizestep past it. This brings the closer placement into agreement with Rule 8's general statement ("closing delimiter is on its own line at the opener's indentation column") and matches the constructor/method/try-resource wrap shape. - Rationale: The earlier
openParen + indentSizeplacement was a deliberate visual carve-out intended to make the) ->arrow stand out from the parameter lines, but it left the closer dangling halfway between the opener column and the parameter column, breaking the visual rhyme readers rely on for every other own-line)in the formatter. Aligning to the opener column matches the canonical Rule 8 text, removes the special case from the lambda path, and produces a layout that reads as a normal wrapped delimited list with the arrow trailing the closer (still visually distinct because of the->itself). - Consequences: Showroom scenario 44 (
longLambdaParameters) shifts the) -> ...line byindentSizeto the left in every level/wrap-style/closer combination (24 golden files updated). TheprintLambdaParametersblock-indent step is dropped (padToColumn0(openParenStartColumn)instead of+ indentSize).ContinuationIndentStepPropertyTestwas retargeted to assert the new alignment;WrappingFormattingTest.lambdaParameterList_insideWrappedChainCall_*updated likewise. Idempotency holds since closer placement is a deterministic function of the opener column. Coverage:ContinuationIndentStepPropertyTest,WrappingFormattingTest.lambdaParameterList_insideWrappedChainCall_alignsParametersAndCloseParen,FormatterShowcaseGoldenTest. - Related docs:
docs/canonical-formatting-rules.md(Rule 8),modules/core/src/main/java/io/princeofspace/internal/PrincePrettyPrinterVisitor.java,examples/outputs/**
TDR-023: Enum constant lists are never collapsed¶
- Date: 2026-04-29
- Status: Accepted
- Decision: Regardless of
wrapStyleorlineLength, never collapseenumconstant declarations onto fewer lines—no greedy horizontal packing (WIDE-style grouping of multiple constants per line), and no single-brace{ A, B, C }form when there is at least one constant. Every constant occupies its own line after{(DeclarationFormatter): same shape as readable source and typical style-guide expectations (enum-specific exception to Rule 5’s generic list semantics—seedocs/canonical-formatting-rules.md). - Rationale: Enums behave like small tables of identifiers; cramming constants onto fewer lines hides structure and defeats diff-friendly editing. Packing also interacted badly with comment re-attachment widths in greedy mode.
- Consequences:
DeclarationFormatter#printEnumConstantsignoresWrapStyle; the prior one-line shortcut when the enum had only constants under the line budget is removed. Tests and showroom goldens (FormatterShowcaseenum sections) reflect the stacked layout everywhere. - Related docs:
docs/canonical-formatting-rules.md(Rule 5),docs/formatting-rules.md(Enum constants),modules/core/src/main/java/io/princeofspace/internal/DeclarationFormatter.java