docs(divergences): broaden into an Ink relationship record; strengthen the alignment-is-a-means principle (#204)
ink-divergences.md used to record only divergences from Ink. Broaden it into the
single record point for the *whole* Ink relationship: it now also records deliberate
alignments (intentional sameness) as first-class entries, not just differences.
- Reframe the title + intro: three relationship kinds (deliberate alignments,
intentional divergences, non-behavioral notes), each a conscious decision.
- Elevate + strengthen the governing principle: aligning to Ink is only a means to
reduce bugs, never the goal — correctness and Vue philosophy outrank parity, and
"because Ink does it" is never on its own a justification.
- Add a first-class "Deliberate Alignments" section; promote the two deliberate-
sameness records (commit-timing Ink-alignment, literal-tab measure-vs-paint, the
latter keeping its existing [VOUCHED @hyf0]) out of Non-Behavioral Notes into it.
- Restructure the classification flow to split deliberate match vs deliberate
divergence first ("How to Classify an Entry").
- Update AGENTS.md guidance and the two stale cross-references to the renamed
headings (accessibility-api.md + the in-file ARIA entry).
No existing divergence entry's substance changed; no [VOUCHED] stamp was added or
removed (AI cannot self-bless). Reviewed by Codex + an independent reviewer.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,8 @@ to receive them and no browser to read them.
|
||||
`aria-role` ports from Ink/HTML unchanged — it is the runtime-compatible escape, not the
|
||||
type-safe path.
|
||||
- This is a Vue-idiom + reasonableness choice, **not parity** (Ink is kebab). See the
|
||||
"Why align to Ink — and when not to" principle in [ink-divergences](./ink-divergences.md).
|
||||
"The governing principle: correctness first, alignment is only a means" section in
|
||||
[ink-divergences](./ink-divergences.md).
|
||||
|
||||
## Type-safety boundary (run-verified: `tsc` + `vue-tsc`)
|
||||
|
||||
|
||||
+111
-63
@@ -1,31 +1,53 @@
|
||||
# vue-tui - Intentional Divergences from Ink
|
||||
# vue-tui ↔ Ink — Relationship Record
|
||||
|
||||
vue-tui started as a Vue 3 port of [Ink](https://github.com/vadimdemedes/ink), and it
|
||||
still tracks Ink closely: the aim is behavioral parity except where a difference is
|
||||
deliberate. It is no longer only a port, though. It has its own design decisions,
|
||||
additive features, and Vue-native choices.
|
||||
still tracks Ink closely. It is no longer only a port, though: it has its own design
|
||||
decisions, additive features, and Vue-native choices.
|
||||
|
||||
This document records the places vue-tui intentionally differs from Ink by design. A
|
||||
difference that is not listed here is treated as a bug, or simply unverified behavior,
|
||||
not a design choice.
|
||||
This document is the single record point for **how vue-tui relates to Ink** — not only
|
||||
where it differs. It records three kinds of relationship, each treated as a conscious
|
||||
decision:
|
||||
|
||||
- **Deliberate alignments** — places vue-tui consciously _matches_ Ink (including where it
|
||||
could easily have diverged, or where it keeps an Ink quirk on purpose) because matching is
|
||||
the most reasonable behavior. A load-bearing alignment is worth recording so a later
|
||||
"improvement" knows the match was a choice, not an accident.
|
||||
- **Intentional divergences** — places vue-tui deliberately _differs_ from Ink, each with a
|
||||
real reason; a kept (human-blessed) divergence also carries an explicit `[VOUCHED @handle]`
|
||||
stamp.
|
||||
- **Non-behavioral notes** — Vue-facing conventions and internal mechanics that are not
|
||||
behavioral claims but are easy to mistake for parity gaps.
|
||||
|
||||
A behavioral difference that is **not** recorded here as a deliberate divergence is treated
|
||||
as a bug, or as simply unverified behavior — never as an implicit design choice. An
|
||||
alignment, conversely, earns an entry only when it is load-bearing or non-obvious; the vast,
|
||||
unremarkable majority of parity needs no record.
|
||||
|
||||
Reference baseline: Ink **v7.0.4** (commit
|
||||
`40b3a7578811fd616341ca4e31cc7748aeeff12f`). When bumping the target Ink version,
|
||||
re-validate every entry below against the new source.
|
||||
|
||||
## Why align to Ink — and when not to
|
||||
## The governing principle: correctness first, alignment is only a means
|
||||
|
||||
Aligning to Ink is a **means, not an end**. Ink is a mature, battle-tested implementation, so
|
||||
matching its public surface and behavior lets vue-tui inherit years of bug-fixes and edge-case
|
||||
handling for free. That — reducing bugs by reusing proven behavior — is the entire point of
|
||||
alignment.
|
||||
**Aligning to Ink is a means, never the goal.** The goal is the _most correct, most
|
||||
Vue-idiomatic behavior_. Ink is a mature, battle-tested implementation, so wherever Ink is
|
||||
already right, matching it is simply the **cheapest way to be correct** — vue-tui inherits years
|
||||
of bug-fixes and edge-case handling for free. Reducing bugs by reusing proven behavior is the
|
||||
_entire_ reason alignment has any value here.
|
||||
|
||||
It follows that **alignment is not the top priority**. When Ink's behavior is itself a defect,
|
||||
is unreasonable, or is un-idiomatic for Vue, **conformance to Vue's philosophy and the plain
|
||||
reasonableness/correctness of the behavior outrank parity.** There vue-tui deliberately diverges,
|
||||
and records it here so the choice is conscious and vouched, not drift.
|
||||
It follows directly that **parity never outranks correctness.** When Ink's behavior is itself a
|
||||
defect, is unreasonable, or is un-idiomatic for Vue, **the plain correctness/reasonableness of the
|
||||
behavior and conformance to Vue's philosophy win — and vue-tui deliberately diverges.** "Ink does
|
||||
it this way" is never, on its own, a justification; it is only shorthand for "Ink is already
|
||||
correct here, so matching is the cheap path to correctness."
|
||||
|
||||
This guards against two opposite failure modes:
|
||||
This priority ordering is _why this file records alignments too_. If alignment were the goal,
|
||||
matching Ink would need no record — it would be "the right thing" by definition. Because alignment
|
||||
is only a _means_, a deliberate match is a genuine decision ("Ink is correct here, so we match")
|
||||
exactly as much as a deliberate divergence is — and a load-bearing one deserves the same written
|
||||
rationale, so a future change that would break it knows it was chosen, not stumbled into.
|
||||
|
||||
The principle guards against two opposite failure modes:
|
||||
|
||||
- **Blind alignment** — copying Ink even where Ink is wrong, or where matching would force
|
||||
un-Vue machinery, merely to match. (Rejected e.g. in the `useCursor` corner-zombie, the
|
||||
@@ -35,38 +57,82 @@ This guards against two opposite failure modes:
|
||||
better" with no genuine Vue-philosophy or correctness reason. Mere presence in this file is
|
||||
**not** a vouch; every kept divergence needs a real reason and an explicit `[VOUCHED @handle]` stamp.
|
||||
|
||||
So the test for any difference is never just "does it match Ink?" but "is this the most
|
||||
reasonable, most Vue-idiomatic behavior — and where it diverges from Ink, is that because Ink is
|
||||
wrong or un-Vue, recorded with a `[VOUCHED @handle]` stamp?" Reasonableness and Vue idiom come first;
|
||||
alignment is simply the cheapest way to get there whenever Ink is already right.
|
||||
So the test for any behavior is never "does it match Ink?" but: **"is this the most reasonable,
|
||||
most Vue-idiomatic behavior?"** — and then, separately, "is the relationship to Ink (a match or a
|
||||
divergence) a conscious, recorded decision?" Correctness and Vue idiom come first; alignment is
|
||||
just the cheapest route to them whenever Ink is already right.
|
||||
|
||||
## How to Classify a Divergence
|
||||
## How to Classify an Entry
|
||||
|
||||
Classify each divergence by the first rule that applies. The order matters: earlier
|
||||
sections are narrower, while later sections are broader fallbacks.
|
||||
Classify each entry by the first rule that applies; the order matters.
|
||||
|
||||
1. If Ink's supported subset still behaves the same and vue-tui only accepts more inputs,
|
||||
supports more contexts, or exposes an extra capability, put it in **Additive
|
||||
Supersets**.
|
||||
2. If the primary reason is alignment with Vue's API shape, framework model, mental
|
||||
model, or user expectations, put it in **Vue API and Mental Model Divergences**.
|
||||
- Use **Model-Implied Differences** when the difference comes from the React/Vue
|
||||
framework-model boundary. Matching Ink would require React-shaped machinery inside
|
||||
Vue, changing a core Vue-facing contract, or dealing with a React-only concept that
|
||||
has no Vue equivalent.
|
||||
- Use **Vue-Idiomatic Choices** when Ink could be copied, but vue-tui chooses the
|
||||
behavior or public surface that better fits Vue's reactivity, lifecycle, component
|
||||
boundaries, current-props model, or API conventions.
|
||||
3. If the divergence is intentional but is not additive and is not primarily Vue-aligned,
|
||||
put it in **Intentional Divergence Choices**.
|
||||
4. If the note is not a divergence, put it in **Non-Behavioral Notes**.
|
||||
1. If it is **not a behavioral claim** — a Vue-facing naming convention or an internal
|
||||
mechanic that is only easy to mistake for a parity gap — put it in **Non-Behavioral
|
||||
Notes**.
|
||||
2. If it is a **deliberate decision to _match_ Ink** that is worth recording — a load-bearing
|
||||
parity point, a place vue-tui could easily have diverged but consciously did not, or an
|
||||
Ink quirk kept on purpose — put it in **Deliberate Alignments**.
|
||||
3. Otherwise it is a **deliberate _divergence_**. Classify it by the first sub-rule that
|
||||
applies (earlier sections are narrower, later ones broader fallbacks):
|
||||
1. If Ink's supported subset still behaves the same and vue-tui only accepts more inputs,
|
||||
supports more contexts, or exposes an extra capability → **Additive Supersets**.
|
||||
2. If the primary reason is alignment with Vue's API shape, framework model, mental model,
|
||||
or user expectations → **Vue API and Mental Model Divergences**.
|
||||
- **Model-Implied Differences** when the difference comes from the React/Vue
|
||||
framework-model boundary: matching Ink would require React-shaped machinery inside
|
||||
Vue, changing a core Vue-facing contract, or dealing with a React-only concept that
|
||||
has no Vue equivalent.
|
||||
- **Vue-Idiomatic Choices** when Ink could be copied, but vue-tui chooses the behavior
|
||||
or public surface that better fits Vue's reactivity, lifecycle, component boundaries,
|
||||
current-props model, or API conventions.
|
||||
3. If the divergence is intentional but is neither additive nor primarily Vue-aligned →
|
||||
**Intentional Divergence Choices**.
|
||||
|
||||
Each divergence entry states what Ink does, what vue-tui does, and why the difference is
|
||||
deliberate. Some entries also record consequences, costs, tests, or the reasoning behind a vouch
|
||||
where those details are needed to understand the decision.
|
||||
Each **divergence** entry states what Ink does, what vue-tui does, and why the difference is
|
||||
deliberate; a kept divergence also carries an explicit `[VOUCHED @handle]` stamp. Each
|
||||
**alignment** entry states what is shared, that the match is deliberate, and why matching is the
|
||||
most reasonable behavior — not merely "because Ink does it". Some entries also record
|
||||
consequences, costs, tests, or the reasoning behind a vouch where those details aid understanding.
|
||||
|
||||
---
|
||||
|
||||
## Deliberate Alignments
|
||||
|
||||
These are places vue-tui consciously **matches** Ink — recorded not because every parity point
|
||||
needs an entry (the unremarkable majority does not), but because each is a _decision_: a behavior
|
||||
vue-tui could plausibly have done differently, or an Ink quirk kept on purpose, where matching is
|
||||
the most reasonable choice. Recording it means a later change that breaks the match knows it was
|
||||
chosen, not accidental. Per the governing principle, the justification is always "this is the
|
||||
correct/reasonable behavior and Ink already has it," never "because Ink does it." Alignment
|
||||
carve-outs that are tightly bound to a specific divergence are noted inline within that divergence
|
||||
entry instead (e.g. the global screen-reader carve-out under "Invalid input is validated at the
|
||||
component layer").
|
||||
|
||||
### Commit timing (throttle cadence, FPS, synchronous resize)
|
||||
|
||||
Commit timing is deliberately Ink-aligned: leading+trailing throttle at
|
||||
`Math.max(1, Math.ceil(1000/maxFps))` ms behind a `maxFps > 0` guard (34ms at the default
|
||||
`maxFps=30` — both engines compute exactly this), synchronous resize. The scheduler mirrors the
|
||||
observable timing of Ink's es-toolkit throttle (run-verified vs v7.0.4): the trailing timer
|
||||
re-arms on every deferred call, so the trailing commit fires at `lastCall+wait` (not
|
||||
`windowStart+wait`), and a call arriving a full window after the first deferral commits
|
||||
synchronously (es-toolkit's `maxWait`), keeping a ~`wait` cadence under sustained updates. This
|
||||
remains true even though re-renders come from Vue's fine-grained reactivity, not a React subtree
|
||||
re-render. Matching Ink's well-tuned cadence buys the same perceived responsiveness and flush
|
||||
guarantees without re-deriving them. One deliberate exception: resize cancels the pending trailing
|
||||
commit — see the divergence "Resize unconditionally cancels the pending trailing commit".
|
||||
|
||||
### Literal tabs in `<Text>` are not normalized (measure vs paint width)
|
||||
|
||||
Tabs in `<Text>` aren't normalized — measured width can disagree with painted width (shared with
|
||||
Ink, KEEP). `string-width` counts `\t` as 0 columns, but paint expands it to the next 8-column tab
|
||||
stop (`wrap-ansi` / terminal), so a `<Text>` with a literal tab reserves fewer columns than it
|
||||
draws (`ab\tcd` measures 4, paints ~10). Ink v7.0.4 does the same, so vue-tui is aligned here;
|
||||
KEEP — literal tabs in TUI text are vanishingly rare, so inheriting the quirk costs less than
|
||||
re-deriving tab handling for input that essentially never occurs. If ever fixed, expand tabs to
|
||||
spaces at the shared squash step (the only place with the column context an isolated tab lacks),
|
||||
upstream of `string-width` — and that fix would then become a divergence entry. [VOUCHED @hyf0]
|
||||
|
||||
## Additive Supersets
|
||||
|
||||
vue-tui supports more than Ink in these cases. Ink-supported inputs and common use cases
|
||||
@@ -416,7 +482,7 @@ Full design, type-safety findings, and precedent survey: [accessibility-api](./a
|
||||
- **vue-tui:** the same vocabulary as typed **camelCase** props (`ariaLabel`/`ariaHidden`/
|
||||
`ariaRole`/`ariaState`; `AriaRole`/`AriaState` exported, identical to Ink's). Ink's kebab still
|
||||
works at runtime (Vue camelizes onto the declared prop), so `aria-role` ports unchanged.
|
||||
- **Why (Vue idiom + reasonableness > parity — see "Why align to Ink"):** Vue's `prop-name-casing`
|
||||
- **Why (Vue idiom + reasonableness > parity — see "The governing principle"):** Vue's `prop-name-casing`
|
||||
mandates camelCase, and — run-verified with `tsc`/`vue-tsc` — **camelCase is the only spelling
|
||||
type-checked** (value/typo/compound mistakes compile-error in both TSX and templates), while
|
||||
kebab `aria-*` is not (Vue/Volar treat it as a global attr). So `ariaRole` is the type-safe
|
||||
@@ -769,8 +835,8 @@ different runtime behavior, ownership rule, or out-of-contract handling.
|
||||
|
||||
## Non-Behavioral Notes
|
||||
|
||||
These notes are not divergence entries. They document Vue-facing conventions or internal
|
||||
mechanics so they are not mistaken for parity gaps.
|
||||
These notes are neither divergence nor alignment entries. They document Vue-facing conventions
|
||||
or internal mechanics so they are not mistaken for parity gaps.
|
||||
|
||||
- The exported host-node type is **`TuiNode`** (`TuiContainer | TuiTextLeaf | TuiComment`,
|
||||
from `@vue-tui/runtime/internal`), not Ink's DOM-emulation `DOMElement`
|
||||
@@ -784,21 +850,3 @@ mechanics so they are not mistaken for parity gaps.
|
||||
`screen-reader.ts`; `G52`). This is renderer mechanics, not a divergence entry by
|
||||
itself. The observable `<Transform>` literal-`false` edge is documented above as a
|
||||
**model-implied divergence**.
|
||||
- Commit timing is deliberately Ink-aligned: leading+trailing throttle at
|
||||
`Math.max(1, Math.ceil(1000/maxFps))` ms behind a `maxFps > 0` guard (34ms at the
|
||||
default `maxFps=30` — both engines compute exactly this), synchronous resize. The
|
||||
scheduler mirrors the observable timing of Ink's es-toolkit throttle (run-verified vs
|
||||
v7.0.4): the trailing timer re-arms on every deferred call, so the trailing commit
|
||||
fires at `lastCall+wait` (not `windowStart+wait`), and a call arriving a full window
|
||||
after the first deferral commits synchronously (es-toolkit's `maxWait`), keeping a
|
||||
~`wait` cadence under sustained updates. This remains true even though re-renders come
|
||||
from Vue's fine-grained reactivity, not a React subtree re-render. One deliberate
|
||||
exception: resize cancels the pending trailing commit — see the divergence entry
|
||||
"Resize unconditionally cancels the pending trailing commit".
|
||||
- **Tabs in `<Text>` aren't normalized — measured width can disagree with painted width
|
||||
(shared with Ink, KEEP).** `string-width` counts `\t` as 0 columns, but paint expands it to
|
||||
the next 8-column tab stop (`wrap-ansi` / terminal), so a `<Text>` with a literal tab reserves
|
||||
fewer columns than it draws (`ab\tcd` measures 4, paints ~10). Ink v7.0.4 does the same, so
|
||||
vue-tui is aligned here; KEEP — literal tabs in TUI text are vanishingly rare. If ever fixed,
|
||||
expand tabs to spaces at the shared squash step (the only place with the column context an
|
||||
isolated tab lacks), upstream of `string-width`. [VOUCHED @hyf0]
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
- Never commit anything under `docs/`. That directory is for local working notes and specs — it must stay out of git.
|
||||
- To read Ink's source (parity / divergence work), clone it once to a fixed local path and read from there — `git clone https://github.com/vadimdemedes/ink /tmp/ink` only when `/tmp/ink` is missing, otherwise reuse the existing clone (Ink isn't an npm dependency, so it's not in `node_modules`). Before trusting anything you read, check out and confirm the pinned baseline — `.agents/docs/ink-divergences.md` records the exact version/commit (currently v7.0.4); a claim read against the wrong Ink version is worse than none.
|
||||
- Behavior claims must be **run, not reasoned**: a parity assertion or `ink-divergences.md` "what Ink does" line is a hypothesis until a real harness against the pinned version (real frames/stderr/exit) confirms it. Source-reading and memory can mislead; when a run contradicts the code you read, trust the run.
|
||||
- When documenting an intentional Ink divergence, update `.agents/docs/ink-divergences.md` and follow its "How to Classify a Divergence" flow. Do not add placeholder or unsorted entries; if the classification is unclear, state that uncertainty in the entry's rationale.
|
||||
- `.agents/docs/ink-divergences.md` is the single record point for how vue-tui relates to Ink — deliberate divergences AND deliberate alignments (load-bearing or non-obvious parity), plus non-behavioral notes. When you make or discover such a decision, record it there and follow its "How to Classify an Entry" flow. Its governing principle: aligning to Ink is only a means to reduce bugs, never the goal — correctness and Vue philosophy outrank parity, so "because Ink does it" is never on its own a justification. Do not add placeholder or unsorted entries; if the classification is unclear, state that uncertainty in the entry's rationale. (Adding a `[VOUCHED @handle]` stamp still requires explicit human say-so.)
|
||||
|
||||
## Project Context Records (PCR)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user