2f452014125a761c6ec7bbec0fa991f87809ecef
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c87616f3fb |
Comments join attachments on the card face — a quiet bubble-and-count chip, present-only
A card whose thread holds one comment or more now draws a second trailing chip beside the paperclip: a secondary-tinted bubble glyph plus its count, shown only when the count is above zero (design ruling 2026-08-09, card e729e30a). Same styling family as the attachments chip — caption size, secondary tint, decorative and hidden outright from the accessibility tree — but this one carries a visible count rather than staying icon-only, per the ruling's own "bubble-style SF Symbol + count." It sits after the attachments chip at the row's trailing edge, in both the live title row and the drag replica. The count is a new `Card.commentCount` field the loader fills with a readdir over `comments/`'s identity-shaped children that carry their own `index.md` — `BoardLoader.commentCount(in:)`, built on the same `identityShapedChildren` predicate a trash entry's held-card count already uses. Never a parse: `.draft` and `.trash/` are excluded for free, the same dot-prefixed hidden-entry skip `CommentThread.load` documents for both, so the walk stays exactly the O(cards) shape 01-storage-format.md § Enhanced schema already commits to. Because the count rides inside the `card: Card` parameter `CardFaceView` already takes — not a new parameter of its own — drawing the chip costs nothing beyond a field read on an already-compared value: no new Observable read joins the body, and the equatable gate already covers it via `Card`'s synthesized `Equatable`. The one divergence from the comments pane's parsed count is documented rather than hidden: a comment folder whose `index.md` exists but fails to parse is a `Stray` the thread read excludes by opening and rejecting it, a cost this readdir does not pay. The face may then read one comment high until that folder is fixed or removed — the trade the ruling's "cheap directory-entry count… not a parse" asks for, over paying full parse cost on every card of every load. Every well-formed comment, and every card with no malformed one, agrees with the pane exactly. VoiceOver: `AccessibilityPhrases.cardValue` gains a `comments: Int` parameter, appended after attachments and before the cut-pending phrase — the same left-to-right order the two chips draw in, so a sighted read and a VoiceOver read never disagree about which comes first. The trashed lane row's own call site (an opaque unit with no comments to speak of) passes `comments: 0`. Docs: DESIGN/03-board-ui.md's card-face section describes both chips and retires the stale "closed with no growth" sentence, honestly recording the 2026-08-09 growth (the hero banner landed hours earlier, this chip after it) as exposure of facts the card already carries rather than a body excerpt. DESIGN/10-accessibility.md's flattened-element sentence gains the comment count. DESIGN/01-storage-format.md's Enhanced schema paragraph records the chip as shipped. WISHLIST #9 is marked shipped in place — not renumbered, since #10 and #11 are cross-referenced elsewhere. Tests: CardCommentCountListingTests (BoardLoaderTests.swift) pins the readdir against a synthetic tree — no comments/ folder, an empty one, non-identity-shaped and index-less strays excluded, .draft/.trash/ excluded for free, agreement with CommentThread.load's parsed count in the well-formed case, and the one documented divergence on a malformed index.md. AccessibilityPhrasesTests covers cardValue's new parameter alone, alongside attachments, and all three fragments together. ViewEquatableTests pins that a comment landing on a card is a gate difference. BoardRenderPerformanceTests adds a render-cost guard: one comment added to one card on a hosted 180-card board re-renders a handful of bodies, not the board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
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 `` 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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
bec75e4282 |
Realign code with the 2026-07-31 rulings
The trash sorts by modified descending — the arrival rank mint retires (Ranks.isOrderedForTrash one comparator, loader + merged order agree; the legacy deleted: migration stamps modified from the tombstone timestamp where parseable; delete undo steps validate existence-only; agent guide v8). Trash selection goes kind-blind — ranges, marquee, Select All, and the successor walk sweep both kinds; the guard moves to the exits (mixed-payload drop refusal, copy/cut validation). The copy stamping preflight widens back to comment depth (load-scoped posture — the board always loads, the gesture refuses whole). Fixes a latent no-op: trashed-lane drag restore never fired (DragSession.beginLanes hard-coded the board container). 2403 tests in 413 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
8014bde7c6 |
Lanes delete into the trash — storage, loader, writer, and undo
Phase 1 of the lanes-in-trash card (2026-07-29 ruling, docs led the
code): lane delete is a move into .trash/ with the subtree intact,
arriving at top trash rank — no destructive delete remains outside
the trash.
TrashedLane opaque unit (id/schema/title/order/heldCards) beside
trash cards — deliberately not a Lane, so no card-shaped surface can
believe an empty subtree. Loader's trash walk trusts the kind VALUE
(lane → opaque unit w/ held-card count counted at the loader's own
unit; card → ordinary card; absent/unrecognized → UUID-children
shape, empty-kindless falls to card per 01's honest limit). Writer:
moveIntoTrash generalized with kind passed never derived (an empty
lane would re-derive as card), deleteLaneToTrash mints against the
whole-container rank ladder. Retired: migrateTombstonedLane (lane
deleted: now ignored — loads live, bytes inert, tolerate-tier
warning), removeLane, captureSubtree/recreateSubtree and the
subtree-snapshot machinery. Undo inverse = move back to captured
strip position, redo replays at captured trash rank. Purge walks
lane subtrees; TrashModel.Freight phrases confirms with lane freight
("…and its 5 cards"). ItemPath gains .trashLane; resolve interleaves
the trash by rank; SearchFilter matches lane rows by title only.
Trashed-lane card windows dismiss and pending cuts void via the
ordinary vanish rule — no new plumbing.
Phase 2 (rendering, selection grammar, drag, a11y, agent guide)
follows. Both schemes 1858 tests / 318 suites green.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|
||
|
|
3a9db2e78b |
Build the integrity service - IntegrityRules and the HealScheduler
The 2026-07-29 integrity design pass, consolidated (DESIGN/01 -
Validation and healing; DESIGN/02 - Components): IntegrityRules
(Storage, pure) is the one home for the identity predicate and
canonical form (BoardWriter.canonicalIdentity deleted, ItemID and the
loader forward to it), the per-field rulebook, uneditable shapes,
per-kind index validation, the reserved-name tables, and the trash
kind discriminator (values trusted - kind: lane/card explicit,
unrecognized falls to shape). LoadResult's ad-hoc channels fold into
one typed Defect stream (looseCardFiles / legacyTombstone /
claimedNameSquatted, per-defect heal signatures); the old accessors
survive as computed views.
HealScheduler (LiveStore) states the six-step heal pattern once -
resting-clear, lock gate, isWritableFile gate (now covering all four
heals), signature memo armed-before-attempt with explicit
clear-on-success, disk re-verify in each write half, one banner-posture
table (BannerCenter keeps all phrasing). The three hand-rolled healers
run on it with behavior preserved - including the
relocation-notice-despite-partial-failure quirk, deliberately. Heals
run at the reload tail AND at registry acquire, closing the
migration-never-fires-at-open asymmetry. Displacement runs first: a
squatted .trash would otherwise fail the migration and arm its memo
against an unchanged picture.
Claimed-name squatters (ruled today,
|
||
|
|
4cf5f09d93 |
Materialize the trash — storage layer
Phase 1 of the trash pivot: the file format learns .trash/. The loader parses the reserved root container — cards only, one shared parseCard for both containers so fail-fast, attachments, and verbatim documents are literally the same code; absent means empty; symlinks and lane-shaped nestings fall out as strays by construction. BoardModel grows snapshot.trash as a plain rank-ordered card list — the container has no identity to carry. Legacy deleted: keys keep flowing through the retiring flag path so every tombstone consumer stays green, and are additionally reported through LoadResult.legacyTombstones in the loose-file idiom for phase 2's migration scheduling — nothing vanishes from view before its folder has actually moved, which is also 01's lock-deferral posture. Writer primitives land value-passing: move to trash with caller-minted rank and the deliberate modified stamp, tombstone migrations that surgically remove the key, physical lane removal, per-card and whole-container purge that leaves strays verbatim, and byte-faithful whole-subtree capture/recreate for lane undo. Board-wide identity now spans the trash, so an import colliding with a trashed UUID remints instead of colliding. The watcher already delivered .trash events — isGitInternal tests a component, not a dot — now stated and pinned rather than relied on. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
1020d9fca4 |
Remove the face carousel — one card presentation
03's resettlement reverses the pathfinder carry-over: the selection-keyed dual presentation proved undesirable, so a card has one presentation — selection changes styling, never geometry, and the masonry never reflows on click. Deleted the carousel view (page dots, glass underlay, scroll-tick monitor), the QuickLook thumbnail cache (sole consumer), the pure paging/suppression rules, and the sole-selected animation key — Motion now keys transactions on the search query and the drop proposal only. The attachment chip stays as the face's whole attachment story; viewing media is the card window's job. No carousel state had leaked beyond the view layer. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
b4c90838b4 |
Build card faces with edge-accent styling
The card face becomes real: leading SF Symbol (card default doc.text, tinted by a valid hand-written iconColor — schema yes, control no), title or the quiet untitled placeholder, and a quiet paperclip when the card has attachments — title-only by design, no body excerpt. Color is the settled K1 edge accent, not a fill: background paints a 4pt stripe down the left edge, resolved through the ported pathfinder palette (12 icon tints + 12 backgrounds carried over verbatim, plus raw #RRGGBB[AA]); anything unresolvable paints nothing and stays on disk exactly as written. The snapshot now carries each card's flat attachment names — the loader's one read inside a card folder, shared with the Writer's listing so the m5 carousel and m6 sidebar can never disagree on order (Finder order, the Writer's existing comparator). The face keeps its top-aligned structure so the sole-selection carousel can expand inside the card without moving masonry neighbors. 18 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
4418b7f981 |
Align the identity predicate with the ratified shape-only rule
Accept liberally, emit conservatively (
|
||
|
|
31f7691062 |
Gate level detection on UUID folder-name shape
Only lowercase-hex 8-4-4-4-12 folder names are lane/card candidates; anything else is a stray — skipped with a distinct warning, never descended, never able to fail-fast a load. UUID-shaped folders keep the prior contract (missing index skips, malformed frontmatter fail-fasts). Design resolution from the Redesign board. +5 tests. Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A |
||
|
|
c7cc97aeec |
Define the board, lane, and card model types
Immutable snapshot value types encoding the frontmatter tables: ItemID wraps the exact folder name (not Foundation UUID, which uppercases); lanes/cards carry FieldValue-typed fields, display-ordered children including flagged tombstones, and their full FrontmatterDocument so unknown and reserved keys ride along uninterpreted. Boards are identified by rootURL — package folders are human-named, not UUIDs. Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A |