Commit Graph
10 Commits
Author SHA1 Message Date
rzen ce92c24190 Hero image for cards — one of the card's own attachments, banded across its face
A card whose `hero:` names one of its own attachments draws that picture as a
banner across the full width of its plate, above the icon-and-title row,
aspect-fill cropped into a fixed 2.75 em band — 36pt at the standard body, and
em-scaled like every other figure the board draws, so it grows with the system
text size and with the board's zoom rather than shrinking against a title twice
its usual size. The figure sits deliberately under the 44pt a plain one-line
card is tall: a hero card should read as a card with a picture on it rather than
a picture with a caption, which is 03's standing rule that the title dominates.

The key's grammar is a **bare filename**, and that is what separates it from the
board background's `image` subkey rather than a nervousness about paths. A board
names a file anywhere under its root, so a path is that key's reading and where
it leads is the renderer's question. A card names one of the files it already
owns — the flat `attachments/` folder the app lists, relocates into, and carries
through every move, copy, trash and restore — so `hero: art/sketch.png` is not an
awkward spelling of a hero image, it is a value the key cannot mean. It therefore
has no reading at all: a value carrying a separator, or spelling `.`/`..`, or
empty, is malformed at the document layer, which renders it as absent and leaves
the coerce tier's trace, exactly as `width: 1.5` does. The bytes stay as written,
the resolver re-checks containment anyway, and the whole degrade family below
that — a name pointing at a missing file, an unreadable one, or one that is not
an image — ends the same way: no banner, no defect, nothing written.

That last promise is about *height* as much as about ink, so the band is given no
height at all until a picture has actually decoded. A card whose hero cannot be
drawn lays out identically to a card with no key, structurally rather than by a
branch somebody has to remember; the price is one settle per hero as a board
opens, and none after that. Everything else the face draws is attached outside
the new stack and is untouched by it — the accent stripe still runs the plate's
full leading edge across the band's corner, the selection and file-hover strokes
still ring the whole plate, the cut and drag dims still cover it, and the drop
model still registers the plate's real height, so a hero card is simply a taller
card the masonry already understands. The trash draws it too, by the one-face
rule.

Decoding is ImageIO's downsampling path off the main actor at a quarter of the
backdrop's pixel budget (`BoardBackdrop.decode` gained the limit as a parameter
rather than being copied), and the results live in one app-wide, deliberately
non-observable cache keyed on path plus the file's date and size. Non-observable
because a tracked write there would invalidate every hero face on the board,
which is the O(board) invalidation this view was rebuilt once already to shed;
each face holds its own picture in view state and seeds it from the cache, which
is also what lets the drag replica — whose preview builder is non-escaping and
cannot await anything — carry the band at the face's real height. Taking a stamp
twice from one URL value turned out to answer with the first read's date and size
however many times the bytes had changed, so `stamp(of:)` now drops its cached
resource values first; noticing a replacement is the only thing a stamp is for.

The face takes the resolved URL as a compared input rather than resolving it, for
selected-ness's reason one axis over: resolving needs the card's folder, which a
face does not know, and finding it from the snapshot would be a board walk per
face. The lane and the trash column each know their own container and compute it
once for the whole strip.

There is no in-app setter this version — the key is written by hand or by an
agent, which is why the guide bumps to v13 with a clause spelling the grammar out
beside the other card keys, and why `attachments/` gets the one-line pointer an
agent that has just written `![](attachments/x.png)` will need. "Set as Hero"
from the attachment row is future work, as is the card window and print, which
draw the same model and show no banner today.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 23:41:15 -04:00
rzen bab456c08d Collapsible lanes — frontmatter-backed slim strips outside the width division
A lane folds to a fixed slim vertical strip carrying its glyph, its card-count
badge and its title turned on its side, and the strip is deliberately not part
of the window's division: the expanded lanes' units divide what is left once
each folded strip's fixed width has come off the top, so folding a lane is a
re-divide trigger of the Show/Hide Trash family — the window never moves and
the siblings grow into what the lane gave up.

The state is a first-class lane frontmatter key, `collapsed: true`, and
document state exactly as `width` is: the files are the board, so an agent
folds a lane by writing one key. Absent means expanded, expanding removes the
key rather than writing `false` (the remove-at-default family beside a
one-unit `width`, the empty rename's `title` and the None well's
`background`), and the lane's `width` rides along untouched so expanding
restores the lane the user had. The read is `width`'s leniency one type over —
a boolean scalar or a quoted boolean word reads as itself, everything else has
no reading at all and renders as expanded, bytes preserved either way.

Toggling is the header's always-visible collapse chevron, the lane context
menu's single Collapse Lane / Expand Lane row, and a plain click anywhere on
the strip; a modified click on the strip stays the ordinary selection grammar,
so a folded lane is still selectable by pointer. The title reads bottom-up and
is justified to the top of the room below the strip's chrome (owner ruling
2026-08-08), truncating against the strip's own height.

While folded the lane draws no cards at all, which is what makes every
exclusion true by construction rather than by a guard per gesture: no card
face means no marquee target and no navigation frame, and no registered grid
means the masonry's drop zones have nothing to resolve against. What did need
code is the half that names absolute destinations — the option-arrow jumps and
the arrow seed scan past a folded lane, the lane domain's down-arrow is inert
on one, and New Card skips it (a selection inside one falls through to the
last-active lane, the stale selection's rule). A drop on the strip appends at
the lane's end, cards and Finder files alike, with an accent edge standing in
for the shadow the strip has no masonry to open; there is no hover-to-auto-
expand yet. Lane reorder works on the strip, and a dragged folded lane carries
its fold, so its shadow and its replica are the strip rather than its units.

The write is `writeLaneWidths` clause for clause — one `updateIndex` bracket,
the same stamp behaviour, the same three do-nothing paths — with two new
`WriteOperation` cases and two new undo verbs rather than one of each, because
a banner or an Edit-menu row that said "resize" after Collapse Lane would name
a control the user never touched.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-08 22:53:10 -04:00
rzen d5ad21c3da The board wears a picture — background becomes a mapping, and the window chrome follows it under a thin frost
background is {color:, image:} and only a mapping at every level; the board's image paints the full window under a transparent title bar, with a thin-material frost strip keeping the chrome legible and the standard accommodations intact.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-07 10:15:03 -04:00
rzen 274ccd9ff5 Realign code with the 2026-07-31 findings-resolution rulings
The full bullet list from Implementation card bf080d9a — both ruling
batches, including the three appended mid-session by 16ef377:

- Restore subjects compose the inverse, never nest: crossing "Undo: S"
  emits "Redo: S" and vice versa; parity, not stack depth, reads a
  legacy double prefix (GitHistoryProvider.restoreSubject).
- Git-operation failures join the one-shot failure banner tier:
  BannerCenter.GitFailureBanner (undo/redo/branchSwitch/addGit), error
  tone at failure rank merged with write one-shots by recency; the
  postLoss compromise is retired at both AppModel wirings.
- order/schema optional below the board root: append-at-end reading
  (ordered siblings first, folder-name tie-break among the order-less),
  schema reads 1, both coerce-tier logged; the root keeps its
  requirements. Ranks.resolvedOrders materializes finite ranks so
  models and placement math stay untouched; first Writer rewrite
  stamps a real rank on touch, placement against an order-less sibling
  stamps that sibling inline in the same bracket. Agent guide v10
  teaches optional keys and zero-read filing. Hostile-YAML order
  shapes become coercion tests; Fixtures/Valid/optional-keys.kanban
  replaces the four retired Malformed boards.
- .gitignore is the relocation-heal noise gate: GitignoreRules pure
  matcher (standard semantics, board-root file only), loader consults
  it once per walk so matched loose files keep the stray posture;
  seeded (.DS_Store + .*.lanework-*) at board creation and template
  instantiation, healed in when missing at open — repo-nested
  included; empty file honored, existing files never edited; the
  committer's obedience via libgit2 status is pinned by test.
- Comments crash-residue sweep gates on step ownership: HistoryStep
  derives backing from its own undo expectations, backedContent unions
  both stacks, the sweep purges per-entry only what no live step owns.
- Skip-purge decoupled (16ef377): a stale-skipped coarse step strands
  whole in NativeHistoryProvider.strandedSteps — still backing, retired
  only at session end; clean exits purge as before.
- Coarse close step named "Changes to '<card>'"; the fine body-edit
  wording never leaks onto the board menu.
- Branch-switch settle clears every open card window's fine stack on
  Save All and Discard alike; the empty fold registers no coarse step.
- Close flush awaits its covering snapshot (quiesce + one generation
  bump, 1s bound), and an explicit flush now queues behind an
  in-flight one instead of skipping — the audit-caught interleaving
  could lose a close flush permanently when the debounce fired inside
  the close sequence; regression tests force both races.
- Commit comment bullets sort chronologically by created, not UUID.
- The production-unwired CardBodyEditSession.editSessionDidChange seam
  is deleted with its seam-only tests.
- Composition-root pins: beginSession composes the committer with the
  store's own EchoLedger and binds the announcer (the miswire class).
- Deliberate 06 conformance pass over every 2026-07-31-tagged
  sentence: fixed Change-custom-key subjects (the retired named
  generic was the only producer), the unbuilt Replace attachment
  vocabulary, heal commits now authored Lanework Integrity, the config
  reader scopes identity to plain [user] sections, add-git re-runs
  detection at create (a stale mode-none could initialize inside the
  user's repo), and add-git failures answer at the form or the banner.
  Structural residue filed on the Redesign board.

2554 tests / 439 suites green.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-08-01 07:43:45 -04:00
rzen bea6d02d1d Realign read-side rules — width range coercion, finite order, symlink pins
The design corpus ratified that ranges are part of a sensible reading:
an exact-integer width below 1 now coerces to 1 read-side (bytes
untouched) instead of reading as malformed — the width division must
never see a zero or negative unit — while a non-finite order (.nan,
.inf) is now the same loud malformed-order rejection as a non-numeric
one, guarded at the single point where the double arrives so loader
and Writer inherit it together. The symlink-never-traversed rule
turned out to be already enforced (the loader has filtered symlinks
ahead of the directory check since the first commit); it and the
copy-preserves-the-link-verbatim behavior are now pinned by tests,
alongside the two hostile shapes the corpus names (width: 0,
order: .nan). Five new tests.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-27 14:21:03 -04:00
rzen 19f67bdb78 Build BoardWriter — atomic, minimal-touch writes
The single point through which every mutation becomes a filesystem
operation: read fresh from disk (strict byte-faithful UTF-8), refuse
readable-but-uneditable frontmatter shapes, apply the edit through the
span engine, stamp modified / clear modified-by, then temp-file+rename
atomically. Renumber-visible-children is the sole minimal-touch
exception, tombstones untouched. Structured BoardWriteError carries
operation + path + reason for the future banner surface.

Alongside, three edges the card surfaced:
- The loader now decodes byte-faithfully too, so a BOM'd or non-UTF-8
  index.md is rejected at load per the encoding contract, instead of
  loading via NSString's silent BOM strip and then refusing every write.
- An appended frontmatter line adopts the file's prevailing line ending
  (a CRLF file stays uniformly CRLF when a stamp first lands in it).
- Empty-but-not-blank frontmatter ({}, null, ~) is detected as
  uneditable — appending after it would be unparseable YAML.

30 new unit tests; 176 total green.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 16:49:42 -04:00
rzen 5a459f0ee4 Exclude inline comments from raw value reads
rawValue and the coercion/malformed paths now stop at the quote-aware
inline-comment boundary, so 'title: 2048  # note' coerces to "2048"
and a read-then-write no longer compounds the comment (pinned
idempotent). Strictly read-side; round-trip bytes untouched. +6 tests
(124 total).

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 16:10:32 -04:00
rzen 4085dd7a46 Engine — duplicate keys last-wins, inline-comment re-splice
Duplicates: Yams' duplicate-key error is disarmed by placeholder-
renaming first occurrences and re-composing (line count preserved, so
span layout stays exact); reads take the last occurrence, set()
rewrites the winner and clears earlier twins, remove() clears all.
Top-level only — nested duplicates stay YAML errors (doc updated).
Re-splice: an inline comment on a rewritten scalar line survives, with
quote-aware detection so a # inside quotes is never a comment. +31
tests (118 total).

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 16:07:06 -04:00
rzen 8e7c565e07 Adopt read-side coercion for schema-owned fields
Design-review resolutions: schema-owned display fields coerce where a
sensible reading exists (wrong-type scalars read as source text, width
accepts exact-integer strings/doubles) and default where none does;
null reads as missing; duplicate keys last-one-wins and inline-comment
re-splicing recorded in the design (engine change follows). Strict
schema/order fail-fast unchanged. 85 tests green.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 15:47:33 -04:00
rzen cb6f6100a7 Build the frontmatter engine with byte-perfect round-trip
Yams validates and reads; writes are surgical line-span edits over
retained raw text, so untouched bytes — unknown keys, comments, odd
formatting, bodies — round-trip identically by construction. Strict
schema/order readers distinguish valid/missing/malformed for the
loader's fail-fast; lenient fields preserve malformed values verbatim;
modified-by is schema-owned. Adds the Yams package and the fixture
folder-reference wiring. 44 tests (105 cases).

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 15:26:12 -04:00