Open questions, and exactly how to settle each one
Everything not done, with the one fact it is blocked on and the shortest path to that fact. Settled work is not narrated here: app-verified claims live in VERIFICATION.md (generated, with the evidence quoted), measured findings live in the ledger at the bottom, and the full history is in git.
Three kinds of blocker exist. A: an integer enum Apple never published — reading works (the value is carried through), writing cannot, because a wrong code produces a file that loads and misbehaves. B: a structure no fixture contains — the schema lets the code be written, nothing proves it right. C: a layout nobody wrote down in files that already exist — no app needed, just measurement. Before writing a manual procedure, check the question is not really kind C: most of this page's history was.
Both A and B are settled the same way: one document made in the app, read back with npm run probe -- <file> (reports every unknown in one pass) or npm run stress -- <dir> (every reader over every file; what throws?).
Needs a Mac — two seeds staged
npm run seeds -- out writes both; each carries its own instructions and its own expected result.
seed-inline-image.pages— an eye, not a probe. Two pictures in a paragraph indented 113 pt from the page margin: the first written with the in-the-text-flow wrap, the second placed against the page. The first starting where its own paragraph's text starts is the pass; the two looking identical means only one mode is ever used, whatever the file says. Nothing to run, just open and look.seed-collaboration.pages— open, share via the Samarbejd button (no invitation needs sending), save, run the probe, send the file back, stop sharing. Collaboration rewrites the package with the two components nothing else writes —OperationStorage.iwaandActivityStream.iwa, LZFSE-framed beside Snappy — and the decoder now reads the framing but has never seen a real payload: the returned file is both the first redistributable specimen and the measurement of what the decoded bytes mean.
Every earlier seed program is returned and banked; the returned files are fixtures with pins (see ATTRIBUTION.md).
Before staging a seed, remember the technique that closed most of this page: somebody has already made the document you need. Parser projects keep per-feature test files; public spreadsheets demonstrate whole function libraries. The rule that keeps borrowing clean: read the properties, keep the measurement, discard the file — nothing borrowed is committed; what survives is constants naming their evidence and tests that rebuild the structures from them.
Blocked on evaluation or the app — not on format knowledge
- Formula authoring: the function table's edge. Authoring is shipped and proven — every parseable corpus formula rebuilds byte-identical to Apple's AST, and the e2e recompute probe settled the dependency-ledger question (Numbers rebuilds it on open; see VERIFICATION). What remains is coverage: any function outside the 272 with measured ids has no index. Widening the table is the same harvest against more documents:
npm run harvest -- --ingest doc.numbers,--emit-sheet probe.tsv, or--drive(macOS, drives Numbers directly). It refuses to guess — a name is accepted only when every observation agrees. - Keynote builds: creation. Reading is measured whole — effect, timing and delivery decode from
animationAttributesagainst the fixture deck — and retiming writes the same fields. Creation stays withheld until the app confirms a build this library writes from nothing: a build the app silently drops looks exactly like one never written. (Cell-control creation shipped and is app-confirmed; see VERIFICATION.) - Creating a category group. Regrouping shipped (byte-identical on unchanged data). Creation is blocked on a group's identity, sort position and eight unexplained sidecar fields; bucketed groupings ("dates by quarter") additionally mean evaluating the grouping formula. Recomputing filtered rows is the same class: which rows a filter hides lives in
TST.HiddenStateExtentArchive, and computing it means evaluating predicates. (Filter rule reading is pinned against the populated set inolekristensen-v26.3-mac-filters.numbers; rule synthesis waits for an app round-trip of a library-written rule.) - Chart appearance: only the app's word left. Type, series colours, axis visibility and gridlines, tick marks and legend styling all read and write, copy-on-write against shared style archives. Remaining: Pages ladder rung
P20-chart-gridlinesin the app (the corpus still has only two charts, so a document with several chart types would widen the evidence). - Keynote v14 upgrade path. The current-format ladder (v26, all eleven rungs) is app-confirmed; the same rungs against the 14.4.1 base — the deck Keynote converts on open, a different code path — are generated (
npm run keynote:docs) and unchecked. - Reclaiming a Data/ file nothing points at.
compact()collects unreachable archives, so removing a picture's anchor and compacting takes the drawable and its attachment; the image bytes stay. The obvious collector is unsafe: every one of the 46 corpus documents hasDataInfoentries that no object's data-reference list mentions — most of them all of their entries — because the apps link data through aTSP.DataReferencefield inside the archive, not through the reference list this library maintains. A reachability scan keyed on the list would delete every image in the corpus. Doing this properly means finding data references by schema position, which is exactly the knowledgereferencedIdsdeliberately does without. - Collaboration components: framing decoded, payload unmeasured.
decodeLzfseStreamreads the LZFSE container (raw and LZVN blocks; FSE blocks refused precisely) and the probe reports what it makes of any opaque component, but no redistributable document carries anOperationStorage.iwato measure the decoded payload against — the staged collaboration seed above is the path. Until then the document model keeps LZFSE components opaque and byte-preserved.
Standing caveats — deliberate, not pending
- Pre-BNC (iWork '13-era) cell storage: read-only, by position. All 123 corpus records decode and mean something (a month of 2009 transactions in date order). The reader anchors on the record end and refuses unmeasured shapes; bit 2's size, the leading words and the trailing word stay unnamed and are reported raw. Writing pre-BNC is not planned — current apps convert on open, and saving modern is the useful operation. Formulas there are not decoded (
isFormulafalse throughout, never guessed). Re-derive the measurements:npm run prebnc. - Password-protected documents are refused (
EncryptedDocumentError) — a legal load-bearing property (docs/LEGAL.md), not a TODO. - iWork '09 XML is detected and rejected, never mis-parsed.
- Editing a document an app has open, and live iCloud collaboration, are out of scope (FORMAT.md §13).
- Transition effect strings are opaque. Exposed raw, never invented; the vocabulary arrives with a deck whose slides carry named transitions — the seed build deck measures builds, not transitions.
- Eleven corpus components came from a stronger Snappy encoder. They decode as perfectly ordinary Snappy in standard 64 KiB chunks — the codec is not in question — but their matches are found better than google's greedy encoder finds them (a 263 KiB stylesheet in 42 KiB), so re-encoding one with the byte-exact port produces a valid, larger component rather than Apple's bytes. Some old iOS-era builds evidently linked a different (perhaps optimal-parse) encoder; identifying which would mean matching its output the way the two google vintages were matched. Harmless unless one is edited, and pinned as the known gap in
test/byte-identity.test.ts. - Two fixtures are re-zipped wrapper bundles, not app-written files. Their entries were deflated by whatever tool zipped the bundle; byte-identity would mean cloning that tool's deflate, which is not this format. Content round-trips; container bytes differ, and the byte-identity test names them as the exceptions.
Ledger
Every protocol run gets a row; failed and partial attempts stay.
| Date | Question | App version | Result | Artifact |
|---|---|---|---|---|
| — | function-index harvest (probe sheet) | — | superseded: 271 names harvested from public documents instead; table in effect | src/tst/function-names.ts |
| 2026-08-03 | border positions | Pages (macOS, Danish UI), via seed-borders | solved, and the old guess refuted: a bitmask — 1 top, 2 bottom, 3 top+bottom, 15 all four; the enum reading (ALL = 4) would have drawn one vertical edge. Which of bits 4/8 is left vs right — and whether the pair is visual or logical (an RTL paragraph flips a logical pair) — remains unassigned | src/tswp/schema.ts, test/styling.test.ts |
| 2026-08-03 | border bits 4/8 + RTL | Pages (macOS, Danish UI), via seed-borders v2 | left and right assigned: red left-only = 4, blue right-only = 8, the probe's stroke colours naming their paragraphs. The RTL leg failed its precondition — the Hebrew-first paragraph rendered LTR with the Danish tail folded into the same line, so unset writing_direction is not "natural" — leaving visual-vs-logical open; the regenerated seed writes an explicit RTL paragraph style instead | src/tswp/schema.ts, test/styling.test.ts |
| 2026-08-03 | paragraph writing_direction value | Pages (macOS, Danish UI), via seed-borders v4 | value 2 refuted as RTL: a Hebrew paragraph styled writingDirection: 2 rendered left-aligned. The caret's behaviour inside the Hebrew (a typed space appears to the caret's right) is run-level Unicode bidi, present in any paragraph, and says nothing about paragraph base direction. The honoured value is unmeasured; the v5 seed ladders 0/1/2 and has the person border the line that stands right-aligned | scripts/make-seeds.ts, src/tss/stylesheet.ts |
| 2026-08-03 | end-of-storage editing smear | field report: an agent editing a real letterhead template, verified by Pages rendering + a bisect ladder | bug confirmed and fixed: edits whose range reached text.length left the new final empty paragraph without its para-style entry, and Pages drops body styling whole when any paragraph lacks one. Corpus measurement made the rule exact — an entry at text.length exists iff the text ends with a terminator (31/31 vs 0/1270) — and the writer now derives it from the new text. The same report drove the offset-safety layer (stale ranges throw; applyEdits) | src/tswp/textstorage.ts, test/text-endedit.test.ts |
| 2026-08-03 | writing_direction ladder (style bag) | Pages (macOS, Danish UI), via seed-borders v5 | the whole style-bag route refuted: styled 0, 1 and 2 rendered identically left-aligned, and the caret differed in-word vs line-start exactly as run-level bidi inside an LTR paragraph predicts. With no corpus style carrying the field, direction does not live there; the v6 seed writes the storage's table_para_bidi pairs instead (1 as the RTL candidate beside the observed 0 and 65535) | scripts/make-seeds.ts, scripts/probe-unknowns.ts |
| 2026-08-03 | border side bits under RTL | Pages (iOS, T15.3 writer), via the final seed-borders round | CLOSED — the side bits are logical: a green left-edge border on a genuinely RTL paragraph (direction written by this library; the (1, 0) pair survived the app's resave untouched) stored 8, so 4 is the leading edge and 8 the trailing, swapping visual sides with the paragraph's direction. Red 4 / blue 8 re-confirmed in LTR in the same file. BorderPosition gains LEADING/TRAILING, with LEFT/RIGHT staying as the LTR aliases | src/tswp/schema.ts, test/styling.test.ts |
| 2026-08-03 | paragraph direction | Pages (iOS, T15.3 writer), app-flipped seed returned | SOLVED — the app's own write names the mechanism: flipping a paragraph writes only the storage's bidi pair, (1, 0) — first slot the direction (0 LTR / 1 RTL / 65535 natural), second slot 0 — with the paragraph style untouched and alignment still natural. Our ladder's (1, 1) was one slot off, which is why it rendered LTR, and the derived-cache reading falls with it. setParagraphDirection now writes the measured pair | src/tswp/textstorage.ts, test/direction.test.ts |
| 2026-08-03 | placeholder authoring | Pages (iOS, T15.3 writer), via seed-placeholder + the returned file | confirmed through the native lifecycle: one tap selected the library-defined span whole, typing replaced it, and the resave shows the field consumed — the app treated our archive exactly as its own. The filled line edited as plain text. Also the first iOS-written artifact over library-authored bytes | scripts/coverage-matrix.ts |
| 2026-08-03 | bidi-pair ladder (table_para_bidi) | Pages (iOS, Danish UI), via seed-borders v6 | pairs refuted as an input too: bidi (1,1), (0,0) and (65535,65535) all rendered left-aligned — and the donor's Body alignment is measured natural (4, own and resolved), so alignment masked nothing. Together with the style-bag refutation the model that fits is that both fields are derived values the app recomputes, like the calc engine's dependency ledger. The v8 seed inverts the measurement: the person flips a staged Hebrew line with the app's own direction control and returns the file, and the diff names the mechanism | scripts/make-seeds.ts |
| 2026-07-31 | cross-table names | n/a — file analysis | solved without an app: AST table_id is a calc-engine owner id (TSCE.FormulaOwnerDependenciesArchive); all 1020 corpus cross-table references resolve | src/tsce/owners.ts |
| 2026-08-03 | predicate_type 7/8 | Numbers (macOS, Danish UI), via seed-rules + seed-filters | solved — the menu-order prediction confirmed whole: 7 = > (twice over: a conditional rule and a filter), 8 = >=, each stated by its own formula. All six comparison codes observed; setConditionalRules writes all six | src/tst/predicates.ts, test/conditional-writing.test.ts |
| 2026-08-03 | filter rules + predicate_type 3 | Numbers (macOS, Danish UI), via seed-filters | first non-empty filter set anywhere — rules read, filters and conditional formatting sharing the predicate encoding. Type 3 is "text contains": NOT(ISERROR(f(needle, cell))) with f the unnamed function index 296 (SEARCH is 131). Filter formulas render OTHER_TABLE:: — the filter owner's references resolve to no named table yet | scripts/probe-unknowns.ts |
| 2026-08-03 | Keynote build vocabulary | Keynote (macOS, Danish UI), via seed-builds | half confirmed, half refuted: three builds survive authoring; the slide↔build graph and delivery read correctly, delivery storing English display strings ("All at Once", "By Paragraph") under a Danish UI. Every database_* field (effect, duration, delay) was absent — even with 3 s / 1 s set by hand — so effect and timing live in animationAttributes (field 18), undecoded. The saved deck's bytes are the outstanding evidence | src/keynote/builds.ts |
| 2026-08-03 | animationAttributes decode | Keynote (macOS M15.3 writer), the returned seed deck as fixture | SOLVED — the field is KN.AnimationAttributesArchive and it was in the vendored schema all along: effect (2) an identifier string in two schemes (apple:dissolve character, apple:move in character, com.apple.iWork.Keynote.BUKAnvil), animation_type (1) "In", duration (3) and delay (5) seconds as doubles, plus random_number_seed (11) and writing_direction_is_rtl (16). Readers now take these with database_* fallback; retiming writes them. The deck also shows per-chunk timing (two automatic chunks, delay 1 s, duration 1.75 s) on staged delivery | src/keynote/builds.ts, test/keynote.test.ts |
| 2026-08-03 | filters + rules as corpus evidence | Numbers (macOS M15.3 writer), returned seeds as fixtures | pinned against real bytes: the populated two-rule row filter (col A >10 type 7, col B contains-"ko" type 3, mode all, arrays consistent) and the conditional sets >5/>=7/contains-"pear"/is-blank (7/8/3/34) are fixtures with tests; the earlier probe-output measurements now have standing evidence | test/predicates.test.ts |
| 2026-08-03 | macOS re-measurement of iOS findings | Pages (macOS M15.3 writer), returned seeds as fixtures | both writers agree: borders {4, 8, 8} with the RTL visual-left edge at 8, the library's (1, 0) direction pair surviving the resave, and a library-defined placeholder consumed by click-and-type to zero fields | test/styling.test.ts, test/placeholders.test.ts |
| 2026-08-09 | section table anchoring | n/a — found building the demo suite | the section table is run-boundary, not point-anchored: an entry marks where a section begins, whatever text lands there next; 25 of 25 sectioned corpus bodies keep their first entry at 0. Treating it as point-anchored made any edit covering a section's first character silently drop the section list — rewriting paragraph 0 destroyed pagination. Also: formulas() could not see authored formulas (no cached value until the app recomputes; the sweep now walks row records) | src/tswp/schema.ts, src/tst/tables.ts, test/long-document.test.ts |
| 2026-08-07 | paragraph left indent | Pages (macOS), via the inline-image seed's own precondition | left_indent alone does not indent: the seed's indented paragraphs rendered flush at the page margin though the file carried leftIndent: 113.4 and read it back. Apple writes the pair — 8645 of the 8647 corpus styles setting left_indent also set first_line_indent — so the writer now supplies a matching first line when the caller gives only the left. The seed's other fault was its own: an empty paragraph appended last is not one paragraphs() lists, so the first picture landed inside the text paragraph before it | src/tss/stylesheet.ts, test/long-document.test.ts |
| 2026-08-03 | inline image placement, list bleed, style-name loss | n/a — file analysis, from a field report building a ten-chapter manual | three faults, all measured: (1) an inserted image carried no exterior_text_wrap — the field all 102 corpus inline attachments have — so the app placed it against the page margin instead of the text column, and geometry could not move it because for an in-flow attachment position is a cache the app recomputes; type 0 is the in-flow mode (56 of 102 inline, 0 of 175 floating). (2) appendParagraph let list membership run on from a previous bullet: Apple states it per paragraph instead, 222 of 222 corpus list-table entries naming a style and 82 naming "None". (3) A directly formatted paragraph reported no style at all, because the name lives on the named ancestor — 644 of 644 anonymous-style corpus paragraphs have one, and one fixture's every paragraph is anonymous | src/pages/document.ts, src/tswp/textstorage.ts, test/long-document.test.ts |
| 2026-08-03 | mixed-codec packages (LZFSE beside Snappy) | n/a — ported from Apple's published lzfse reference (BSD-3-Clause) | framing decoded: decodeLzfseStream walks bvx- / bvxn / bvx$ blocks and the full LZVN opcode table, validated against hand-assembled vectors per opcode family; bvx1/bvx2 (FSE) refused precisely. Payload semantics unmeasured — no redistributable OperationStorage.iwa exists; the collaboration seed is staged to produce one | src/base/lzfse.ts, test/lzfse.test.ts |
| 2026-08-01 | cell-control interaction_type | n/a — borrowed documents | solved: 4 stepper, 5 slider, 6 star, 7 pop-up, 8 checkbox; also found and fixed the dropped-checkbox bug | src/tst/controls.ts |
| 2026-08-01/02 | Pages ladder P00–P19 | Pages (macOS 26.x) | all rungs confirmed; twelve well-formed-but-wrong defects found, fixed, pinned | VERIFICATION.md |
| 2026-08-02 | Keynote ladder K00–K10 (v26) | Keynote (macOS + iOS) | all rungs confirmed; six defects found (four offline by shape:audit, two by the app), fixed, pinned | VERIFICATION.md |
| 2026-08-10 | character-style run left open at end of text | Pages (macOS), via the demo suite's feedback lines | bleed confirmed and closed at the append seam: styling a range ending at text.length writes no closing entry — correctly, since no corpus storage (0 of 2896) carries a character-table entry there — but the open run then swallowed every later append, so one grey-italic line turned the rest of the document grey and italic. Apple's run-end shape is an objectless entry (624 of 2079 corpus character-table entries; 459 directly after a styled run), sitting on a terminator (×80), just after one (×69) or mid-text (×430) — so appendParagraph now writes that boundary at the old end before inserting, and a styled range means [start, end) whatever is appended later. Push/pop styling state was considered and rejected: the format's run table has no writer state, and a forgotten pop is this same bug with an API blessing | src/tswp/textstorage.ts, test/long-document.test.ts |
| 2026-08-10 | paragraph direction run left open past setDirection | Pages (macOS), returned demo-01 | the same open-run failure, one table over: setParagraphDirection wrote a per-paragraph bidi table for the paragraphs that existed at call time, so the flipped Hebrew line's (1, 0) entry was the table's last and ruled every paragraph appended after it — the rest of the document rendered RTL: right-aligned Latin text, end-of-sentence punctuation at the visual line start, tabs measured from the right. The corpus statement: 2594 of 2896 bidi-bearing storages cover every paragraph start, the one mid-document (1, 0) is followed by an explicit (0, 0), and an open-ended RTL entry appears only where no paragraph follows. appendParagraph now states each new paragraph's own pair, copying the storage's baseline at position 0 | src/tswp/textstorage.ts, test/long-document.test.ts |
| 2026-08-10 | authored border strokes render as «Ingen» | Pages (macOS), returned demo-01 (T-10, in-document feedback) | the first library-authored border ever opened drew nothing, and the archive diff names the cause: our stroke stated color, width and a pattern message holding only its type; every app-written paragraph border (167 of 167 in the corpus) also states cap, join, miter limit 4 and the full pattern message — phase 0, count 0 and six pattern floats even for solid. The app showed the width (1 pt / 3 pt reached the inspector) but «Ingen» for the stroke, drew no border, and zeroed border_positions on resave. writeStroke now writes the complete corpus shape; positions bitmask semantics (settled 2026-08-03) unchanged | src/tsd/style.ts, test/styling.test.ts |
| 2026-08-10 | authored borders round two: stroke honoured, toggles unselected | Pages (macOS), returned demo-01 (T-10, in-document feedback) | the position toggles key on deprecated_borders (15), not the bitmask: with the complete stroke the inspector selected our colours, stroke type and width, but the side toggles stayed unselected and nothing drew. The app writes the historical enum beside border_positions on every bordered style — 17 of 17 corpus styles with non-zero positions carry both (1·1 ×10, 2·2 ×1, 4·8 ×2, 8·16 ×4), 6474 agree on 0·0, and 12 old-era styles carry only the enum. The old scale keeps 1 top and 2 bottom, states top-and-bottom as 3 and all four as 4, and moves the sides to 8 leading / 16 trailing with horizontal bits added (9–11, 17–19); leading beside trailing short of all four has no value. The writer now states both; the reader falls back to the enum for old documents | src/tswp/schema.ts, src/tss/stylesheet.ts, test/styling.test.ts |
| 2026-08-10 | sections shared their page masters | Pages (macOS), returned demo-02 | a library-created section could never differ from its neighbour: insertSectionBreak cloned the section object but kept the master references, so section 3's header write was overwritten by section 2's — the returned file shows both sections pointing at the same template ids while no two sections in the corpus's 25 sectioned documents share one. The insert now clones the three master variants and their six storages, so each section owns its chrome | src/pages/document.ts, test/long-document.test.ts |
| 2026-08-10 | header text in always-empty columns never drew | Pages (macOS), returned demo-02 | the render gate is the storage's shape, not any single field: text set bare into a master's always-empty header storages kept the donor's blank default shape and did not draw, while the one column that had text rendered. No single field explains it — the DOCX-imported donor's rendered header carries a char-style table and no language table, an app-authored footer the reverse — so a filled column now adopts its non-empty sibling's shape whole: paragraph style, char-style and language entries. Also measured: the column↔position mapping is open — corpus header text sits at storage [1] almost exclusively, and the checker saw [1] render at the left edge, against the assumed left/center/right; demo-02's SPALTE-A/B/C rung settles the order. Column indexes outside 0..2 now throw (the demo's third column silently went nowhere) | src/pages/document.ts, src/tswp/textstorage.ts, test/long-document.test.ts |
| 2026-08-10 | header slot model (demo-02 round two) | Pages (macOS), returned demo-02 | modern Pages draws one page-wide header field, bound to storage slot 1: with all three slots filled, »Sektion 1« rendered once, centred, and of SPALTE-A/B/C only SPALTE-B appeared — left-aligned at the page edge, so the drawn field spans the page and the text follows the storage's own paragraph alignment (section 1's donor style centres, section 2's left-aligns). Slots 0 and 2 are the legacy three-field layout's outer slots — tudortmund (v4.2 lineage) fills them — and the byte that switches the legacy mode remains unfound: document-archive field 49, settings show_ct_deletions (13) and section first_page_hides_header_footer (28) all refuted as candidates by name or by counterexample; no corpus header uses tab-separated parts either. Verified silently in the same round: date field, bookmark, footnote, comment, both placeholders, and section 3's own cloned header — the master-cloning fix holds | src/pages/document.ts, scripts/make-demos.ts |
| 2026-08-12 | rule offset closed; demo-01 settled whole | Pages (macOS), returned demo-01 | positive is outward, and the UI scale is calibrated: ruleOffset +12 rendered the wider gap, and the two informational inspector readings settle the mapping — stored 0 displays 6 pt, stored +12 displays 18 pt, so the stored value is relative to the app's 6 pt default gap and the inspector shows the absolute number. (The earlier −2-for-stored-−12 reading was the display floor or a misread; the linear fit from two clean points stands.) All fifteen demo-01 checks now render as written | src/tss/stylesheet.ts, scripts/coverage-matrix.ts |
| 2026-08-13 | demo-05 (cells & formats) settled whole | Numbers (macOS), fourth round | all eight checks confirmed rendering: cell types (text, number, date, duration), currency and percentage formats, the checkbox drawing as a control (»Checkboks vist«), the merged cell centred, the styled cell with fill, padding and the terracotta border all the way round (»2pt terrakotta hele vejen rundt«), column widths and row heights, banded rows, wrap and clip, the 24 library-inserted rows, and the visible »Demotabel« name. Five writer defects were found and fixed across the rounds — currency's record type and format tail, the checkbox control trio, the extras word, the stroke-sidecar mechanism and its grid clip, and alignment on the text layer — two of them nailed by the person's own one-delta measurements inside the returned files | scripts/coverage-matrix.ts |
| 2026-08-14 | demo-11 settled whole: the crash fix holds in the app, and the round returns three app-written shadow shapes | Pages (macOS 15.3.1), returned demo-11 v2 — now fixtures/olekristensen-v26.3-demo11-shadows-returned.pages | »det virkede« — the seven-field rewrite survives the popup that aborted the six-field archive, and every rung passes (S-01–S-07 silently, S-09 contact, S-10 curved, S-11 reflection »ja, det virker«). The round's measurements, banked: (1) the popup wrote Apple's fresh drop preset over our disabled archive — stored angle 90 (inspector 270°), offset 2, blur 5, 50 % opacity — a second measured default beside the archive-level 315/5/1/full the untouched theme shadows carry. (2) the contact shadow's blur floor in the UI is 10 pt and the reviewer's 40 pt »gav mening« — the demo rung now ships 40; the app added the contactShadow sub-archive (one float at sub-field 2, matching the corpus's two rougier contact shadows). (3) the reviewer curved the S-10 shadow »mere indad« and the app wrote the corpus's first curvedShadow sub-archive — one float at sub-field 1, negative for inward. set() now carries per-type sub-archives forward on a same-type rewrite so re-parameterising an app-edited shadow keeps its curve; a type change drops them, the corpus shape | src/tsd/style.ts, src/tsd/drawables.ts, test/styling.test.ts, fixtures/ATTRIBUTION.md |
| 2026-08-14 | demo-11 round one: nine rungs render; re-enabling our disabled shadow aborts Pages | Pages (macOS 15.3.1), returned crash report — SIGABRT in TSStyles under the shadow popup | the file rendered whole and crashed the app only under edit — two measured deltas, both closed: every parameter (angles both ways, offset, blur, opacity, colour, the disabled state, contact and curved types) rendered as stated, and choosing Slagskygge on the disabled black square aborted Pages in TSStyles' assert. (1) our shadows stated six of the seven fields all 929 corpus shadow archives carry — type missing — and the demo carried its own control: S-11's untouched clone kept the theme's disabled shadow, type 0 explicit, colour space stated. writeShadow now writes the archive whole (929 of 929 seven-field; colour space on every current-era shadow colour, 822 of 822). (2) the copy-on-write clone kept its source's style_identifier — no two of the corpus's 18554 identified styles share one — and was unlisted where 183 of 183 drawable-referenced styles are listed in their stylesheet. privatise now writes the app's own style-one-object shape: anonymous, parented on the identified source (the 30-of-183 override shape), listed with the reference declared. The re-sent file states the S-08 toggle as its own check; the popup path is the app behaviour nothing offline can prove | src/tsd/style.ts, src/tsd/drawables.ts, test/styling.test.ts, scripts/make-demos.ts |
| 2026-08-13 | field report 5: a tail edit strands run boundaries at text.length, and the app repairs the file document-wide | n/a — field report (three Pages documents on 0.2.0/main), reproduced in-library, corpus census | the letterhead corruption reproduced byte-for-byte, and the table law is now measured whole: the reported sequence — last paragraph carrying a grey character run and a placeholder field, setStyle, deleteRange(len−1, len), save — left the run's and the field's closing boundaries at exactly text.length (table_char_style: [0, 76, 98] with length 98, table_smartfield likewise). The census over all 2921 corpus storages: no entry sits past the length in any table; none sits at the length in the character-content tables (0 of 2079 char-style, 0 of 220 smart-field, 0 anywhere in attachment/bookmark/footnote/insertion/deletion/highlight/language/dictation); the paragraph-family tables carry one there only over empty or terminator-ending text (the phantom paragraph — 1565–1597 per table). The app's reaction to an end-of-text entry it never writes was already app-measured once from the other side (K02's subtitle: »the refilled table gained an end-of-text entry Apple never writes — the app dropped the style run«), which corroborates the report's document-wide font loss. shiftIndexedTable now drops entries stranded at ≥ the new length per table class, and save() refuses any edited storage violating the law (verifyTextStorageIntegrity). Also from the report: the tail-state claim measured (15 of 26 corpus bodies end bare, 8 with terminator, 3 empty — the skill's »the apps write it« corrected; appendParagraph preserves the document's own convention on 0.2.0 and main both, so their always-terminator flow came in through another door, likely a trailing-\n setText); normalizeTail() added as the safe tail edit; fillPlaceholder fills by fieldId live so one listing fills in any order (the index-shift trap pinned); the spent-id fill throws. Deferred to the API queue with reasons: a built-in donor for insertInlineTable on table-less documents, doc.audit() | src/tswp/textstorage.ts, src/tsa/document.ts, src/pages/document.ts, test/table-positions.test.ts, test/placeholders.test.ts |
| 2026-08-13 | the checkbox seed and the sidecar's grid: both remaining cell findings closed | Numbers (macOS), returned seed-checkbox + demo-05 round three — seed now fixtures/olekristensen-v26.3-seed-checkbox-returned.numbers | the app's one-delta toggle names the checkbox whole, and the person's own D13 border names the sidecar clip: (1) a checkbox is bool format {format_type: 263} plus the record's control id plus a control-spec entry (interaction_type 8, the type-12 list) plus extras 0x20 — the format alone, as 263 or as the corpus file's bare {1:1}, showed as Automatic. 263 is re-confirmed (the borrowed documents were right; the earlier bare-1 corpus reading was a different animal), and setCellFormat({kind:"checkbox"}) now routes through the control path. (2) our sidecar runs, indexing and strokes were byte-right — the person's app-written red 3 pt border on D13 landed in the same layer families with the same run shape — but the donor's reused sidecar still declared its old 2×11 grid and the app clips runs outside column_count/row_count; the sidecar is now brought to the table's size on every border write | src/tst/tables.ts, src/tst/formats.ts, src/tst/cellrecord.ts, test/controls.test.ts |
| 2026-08-13 | mask editability CLOSED: the gate is the drawable's arrangement | Pages (macOS), returned demo-03 rounds nine and ten | »der åbnede den!« — the editor engages on the floating cropped copy and refuses the in-flow one, exactly as the ladder predicted: ten rounds of byte-side candidates were all innocent — the mask editor simply does not open for images »integreret med tekst«, by app design. The person had also rewrapped before cropping in the crop-delta seed, which is what pointed at the arrangement. The crop itself renders in both arrangements (round nine confirmed the clone-ownership fix restored it), reset works in-flow, and setCrop's output is byte-shaped like the app's own. The saga's yield along the way: the full modern drawable super, drawn-space windows, originalSize/naturalSize semantics, traced_path, and the live-reference clone walk | scripts/make-demos.ts, scripts/coverage-matrix.ts |
| 2026-08-13 | demo-05 round two: currency passes; cell borders and centring mechanisms found | Numbers (macOS), returned demo-05 + corpus | currency and percentage confirmed rendering; two mechanisms measured and implemented: (1) cell borders live in TableModelArchive.stroke_sidecar — TST.StrokeSidecarArchive with per-edge layer families (left/right indexed by column, runs spanning rows; top/bottom by row spanning columns), each run a complete stroke with a draw order — and in none of the corpus's 4139 cell-style bags, which is why the bag-only border drew nothing. setCellFormatting borders now write sidecar runs; cellBorders(row, column) reads them back, bag fallback for old files. (2) horizontal alignment rides the text layer: corpus cell text styles are TSWP.ParagraphStyleArchives whose paragraph bag carries alignment (127 left, 121 right); CellFormatting.horizontalAlignment creates an anonymous per-cell paragraph style, null drops to the band default. Checkbox stayed Automatic even at the corpus-exact {format_type: 1} — the remaining record difference is content-borne (a suggest string), so the crop-delta pattern goes again: seed-checkbox.numbers stages the app's own toggle for diffing | src/tst/tables.ts, src/tst/styles.ts, test/tables.test.ts |
| 2026-08-13 | the crop-delta seed: the mask editor, Apple's own crop, and asset dedupe | Pages (macOS 26.3), returned seed-crop.pages — now fixtures/olekristensen-v26.3-seed-crop-returned.pages | the editor engages on a library-inserted image, and the app's crop is this library's shape: the person copy-pasted our photo, rewrapped it, cropped the copy with the app's own mask tool, and saved. The app's mask matches setCrop field for field — window in the image's drawn space, parent back-pointer, wrap type 4 with the 12 pt margin, the six-element rectangle path — and the modern super written since round eight (lock pair, stand-in title/caption, hidden flags) appears verbatim on Apple's own cropped image. Also measured: the paste shared the data object (two drawables, one JPEG, one reference id — the SHA-1 dedupe model confirmed as Apple's own), the app renames stored assets <name>-<dataId>.jpg, and its generated thumbnail is a downscaled source copy (<name>-small-<dataId>.jpeg), shared by both drawables, not a render of the crop | fixtures/ATTRIBUTION.md, test/masks.test.ts |
| 2026-08-13 | demo-03's crop stopped rendering (batch round) | Pages (macOS), user report + build diff | the only un-attested state in the file was a clone sharing its source's stand-ins, and the clone walk's selection was the fault: deepCloneObject selected from declared references, which refresh only at save, so a copy of a just-inserted image cloned nothing it referenced — the floating Earthrise copy shared the inline original's title/caption stand-ins, a state no corpus file shows (0 of 288 stand-in references have two owners). Selection now computes references live (ObjectStore.currentReferencesOf), so a copy owns its stand-ins and its mask. Whether the shared stand-ins were what stopped the crop drawing is the re-send's question; every other archive in the file is byte-equal to the round that rendered | src/tsp/store.ts, src/tsp/clone.ts, test/long-document.test.ts |
| 2026-08-13 | demo-04: the chart vanished | Pages (macOS), returned demo-04 | setText("") severed the chart's anchor: the old-era donor has no page groups, so its floating chart renders only through its text anchor, and wiping the body deleted the anchor entry while the attachment object survived. The demo re-anchors the surviving attachment after the rewrite (insertAttachment with the existing object). The library lesson joins the safe-editing docs: replacing a storage's whole text detaches every drawable anchored in it | scripts/make-demos.ts |
| 2026-08-13 | demo-05: currency, checkbox, table name (three of six findings) | Numbers (macOS), returned demo-05 | all three measured and closed: (1) a currency-formatted record stores type 10 — ours stayed NUMBER and the app showed Automatic with the format ignored; the format archive also carries an explicit tail (negative style, separator, accounting — {1,2,3,4,5,6}) and the record's extras word is 0x802 (1757 of 1966 corpus currency records), not the 0x02 we wrote. (2) the checkbox is a bare boolean format — {format_type: 1}, the shape all thirty corpus checkbox cells carry; 263, measured off two borrowed documents, drew as Automatic with the word SAND — it stays readable, no longer written; bool records carry extras 0x0 (90 of 90), not 0x20. (3) the table name lives behind table_name_enabled (22) — true on 29 corpus tables — and the demo had also never actually called the rename it promised; nameVisible is now on the model and the demo states both. Still open from the same file: the merged cell's horizontal centring (an API gap — alignment rides the text layer) and the cell border that did not draw despite the complete stroke shape | src/tst/formats.ts, src/tst/cellrecord.ts, src/tst/tables.ts, test/controls.test.ts, test/tables.test.ts |
| 2026-08-13 | field report 4's API queue: dividers, inheritance, inline tables, the "no-op" | n/a — corpus measurement + in-library repro | four asks landed, one refuted: (1) TableFormatting now covers the divider flags (42/43/44), the header-border toggle and all sixteen modern band strokes — 286 of 302 corpus table-style bags carry each — and every key is three-state: null removes the field so the style inherits again; absent has never meant false. (2) insertRows/insertColumns inherit the displaced row's or column's cell and text styles as blank-but-styled records — Apple's own shape, 2043 of 2275 corpus empty records carry a text style — with the style table's refcounts bumped per copy. textStyleId/textStyle join the existing cellStyleId/cellStyle pair. (3) insertInlineTable(pos, {copyOf?, name?, withContent?}) clones a donor table and anchors it at a U+FFFC with the measured five-field attachment, parent = the body storage; document-unique naming; no donor throws. (4) the reported createParagraphStyle paragraph-option no-op does not reproduce — every ParagraphFormatting key round-trips (pinned) — but the likely experience behind it does: a string where the alignment enum belongs surfaced as a BigInt error deep inside save. alignment now takes the names too (left/right/center/justified/natural) and rejects garbage at the call | src/tst/styles.ts, src/tst/tables.ts, src/tss/stylesheet.ts, src/pages/document.ts, test/tables.test.ts, test/styling.test.ts |
| 2026-08-13 | cloned-table content loss (field report 4, ranked first) | n/a — reproduced in-library | mechanism named, loss made loud, ordering stabilised: two tables sharing one string table undercount refcounts — each entry claims one owner while two tables' records reference it — so the first overwrite in one table releases entries the other still needs, and that table reloads all-empty ("records referencing strings absent from the data list", exactly as reported). The state is always an authoring fault: no two corpus tables share a data list or a tile (0 of 52 tables, 46 documents), and no corpus record is orphaned (0 of 5019). save() now runs verifyCellStorageIntegrity — scoped to tables whose storage changed this session — and refuses the file naming the table, cells and keys instead of persisting silent loss. The report's deterministic contributor is fixed separately: tables() on both apps now enumerates document order (Numbers: sheets in tab order, drawables per sheet; Pages: body anchors by text position, then paint order), so tables()[0] is the first table on the page whatever was added this session. addTable itself forks every data list — measured — so the library's own clone path was never the trigger | src/tst/tables.ts, src/tsa/document.ts, src/numbers/document.ts, src/pages/document.ts, test/tables.test.ts |
| 2026-08-13 | mask editability rounds five–seven: reset engages, the editor still does not | Pages (macOS), returned demo-03 | the mask is now valid enough to clear but not to open — and the last uniform differences are on the image's super: »nulstil masken« worked where earlier rounds offered nothing, so the crop model itself is accepted; double-click editing still refused. traced_path (present on 30 of 31 masked corpus Pages images, written since round seven) did not open the editor alone. The remaining corpus-uniform deltas, now closed: every masked image states aspect_ratio_locked true (87 of 87, all apps) with locked stated beside it (85 false), and every current-era image carries title/caption references to empty TSD.StandinCaptionArchives with both hidden flags stated (88 images, 176 of 176 targets empty, 87 of 88 false/false) — ours wrote none of these. insertInlineImage now writes the full modern super; setCrop states the lock pair on any image it masks, preserving an existing lock. Placement refuted as the gate: masked corpus images are 10 text-anchored, 15 group children, 6 z-order only — ours is a corpus-attested construction | src/pages/document.ts, src/tsd/images.ts, test/long-document.test.ts |
| 2026-08-13 | shadow angle calibration | Pages (macOS), returned demo-03 + inspector reading | the inspector displays 360 − stored: our default 315 rendered up-right and read 45° in the UI; the proto's [default = 315] is the legacy scale. DEFAULT_SHADOW.angle is now 45, which displays 315° and renders the light-from-upper-left, shadow-down-right convention | src/tsd/drawables.ts |
| 2026-08-11 | field report 4: typesetting a report into a branded template | n/a — field report, five documents | three silent failures named, two fixed here, one deferred: (1) wiping a body with applyEdits could leave text ending in a terminator with no paragraph-style entry at text.length — Pages refuses such a file; every edit now restores the entry. (2) listInThemeStyles with a character style put a 2021 archive in paragraph_style_presets — refused too; the method now routes by type across the three preset lists (1 list, 6 character, 7 paragraph) and throws otherwise. (3) deferred, ranked first by the reporter: cloned tables intermittently lose their cell text on reload — every table in the document, including untouched ones — suspected string-interning or id allocation in the clone path; tablesOf(store)[0] instability after a clone is one deterministic contributor. Also banked: formatTable trap fixed into a real setter; wire layer rejects NaN field numbers; the report's API asks (TableFormatting divider keys 42/43 and border strokes 47…, insertRows style inheritance, textStyleId getter, insertInlineTable, null-unset in applyTableFormatting, createParagraphStyle paragraph-option no-op, style-of-paragraph-n recipe, negative-indent full-bleed recipe, proto-first skill lesson) are the follow-up queue | src/tswp/textstorage.ts, src/pages/document.ts, src/tst/tables.ts, src/base/protobuf.ts |
| 2026-08-11 | demo-03 rounds two and three: wrap and PDF confirmed; masks made editable | Pages (macOS), returned demo-03 | four checks confirmed silently: the inline photo flush with its indented reference line (closing the inline-image saga), the margin mode, the floating copy wrapping text (the round-one fix holds), and the vector PDF staying sharp under zoom — PDF as insertable media is app-verified on its first check. The crop rendered exactly as stated (Fuji centred, the wave's claw gone) but the mask editor would not engage: app masks are full drawables — parent = the image they mask (79 of 79 corpus masks carry the field), their own exterior_text_wrap, and explicit locked/aspect_ratio_locked/title_hidden/caption_hidden — where ours was bare geometry. buildRectangularMask now writes the whole shape. Also adopted as demo policy, from the checker: assets must make non-proportional scaling visible — a disc (Earthrise) or a well-known motif does; anonymous colour blocks do not | src/tsd/masks.ts, src/tsd/images.ts, test/long-document.test.ts |
| 2026-08-11 | floating drawables' text wrap (demo-03 round one) | Pages (macOS), returned demo-03 | a floating copy of an inline image kept the in-the-text-flow wrap, and the app wrapped nothing while its inspector read «automatisk»: the dominant floating corpus shape is type 4 with a 12 pt margin (1136 of the floating drawables; type 0 appears on only 36, none of them Pages page content). The floating container now normalises an in-flow or missing wrap to the on-page shape on attach. Also from the round: the crop was unverifiable by design — a solid-colour source masks to the same solid colour, and a squashed scale is indistinguishable from a crop — so the demo asset is now two-tone with the expected seam described in the check; and the M-01/M-02 indent checks promised indented text the demo never wrote, so the reference line exists now | src/pages/document.ts, scripts/png.ts, test/long-document.test.ts |
| 2026-08-11 | demo-02 (structure & fields) settled whole | Pages (macOS), third round | all seven checks confirmed rendering: three sections with independent cloned masters, the page-wide slot-1 headers with style-borne alignment (centred vs left-aligned as their storages state), footers with live page-number and page-count fields, the date field, bookmark, footnote, comment, and both placeholder behaviours — the tap-to-replace span and the library-filled one editing as plain text. Two writer defects and one refuted layout model on the way, one per round | scripts/coverage-matrix.ts |
| 2026-08-10 | rule offset direction (T-15 round one) | Pages (macOS), returned demo-01 | negative is inward, zero is the default, and the inspector is not the archive: ruleOffset: −12 rendered the rules overlapping the paragraph — negative pulls toward the text, so the stock templates' −3 tightens the default gap rather than widening it. The resave preserved our −12 verbatim while the inspector displayed −2 pkt, and it back-filled the offset-less border styles with explicit (0, 0) and rule_width 1 — including under 3 pt strokes, so rule_width is legacy fill, not kept in step with stroke width. The outward direction is unattested (every non-zero corpus value is negative); T-15 now writes +12 to measure it | src/tss/stylesheet.ts, scripts/make-demos.ts |
| 2026-08-10 | demo-01 (text & typography) settled whole | Pages (macOS), third round | all fourteen checks confirmed rendering: character formatting (weights, colour, highlight, strikethrough, font, capitals, super/subscript), links, created and edited named styles, alignment, lists ending at their own paragraphs, block and hanging indents, paragraph borders on the enum+bitmask pair (top/bottom, red leading, blue trailing — with value 3's structural mapping thereby app-confirmed), paragraph background, RTL confined to its one paragraph, double line spacing, decimal tabs. The text-to-rule gap on horizontal borders is the app's default — no rule offset is written, matching the app's own authored border styles, only donor templates carry historical_rule_offset (-3, -3). Three writer defects found and fixed on the way, one per round, each named by in-document feedback | scripts/coverage-matrix.ts |
| 2026-08-14 | demo-06 round one: formulas compute, but a clone answers to its donor's name | Numbers (macOS), returned demo-06 | every formula evaluated — and the cross-table rung exposed clone identity whole: SUM, AVERAGE, IF, TODAY, cross-column references all computed, so authored ASTs are app-verified beyond the e2e probe. But the F-03 cross-references to the copied table showed empty cells: addTable's clone byte-copied the donor's calc-engine identity, making one owner with two names — the engine resolved Krydstjek:: to the donor, whose referenced cells were blank. Fixed by re-minting the whole derived owner family (remintFormulaOwnerIdentity: every UUID within base+[0,256) rewritten to a fresh base, same deltas) on every clone path — addTable, addSheet with content, insertInlineTable. Two builder faults on the way: the demo's Krydstjek was a full copy of the data table (structure made no sense for a cross-check target) and its SUM(B) summed a column the demo itself wrote prose into | src/tsce/owners.ts, src/tst/tables.ts, test/owners.test.ts, test/tables.test.ts |
| 2026-08-14 | demo-07 round one: rules show but never fire; the engine's ledger is the gate | Numbers (macOS), returned demo-07 → olekristensen-v26.3-demo07-rules-returned.numbers | conditional rules are formulas the engine only evaluates when registered — the inverse of cell formulas: every rule was correct in the inspector, no fill drew, and deleting + re-typing a covered value made that cell's rules fire (closing the cell was not enough — commit, not focus). The returned file names the mechanism: the app registered exactly the five re-committed cells under the table's kind-3 owner as CellRecordExpandedArchives — {column, row, expanded_edges {edge_with_owner_rows [row], edge_with_owner_columns [column], internal_owner_id_for_edge [the kind-1 owner]}} — the shape all 1973 registered corpus rule cells state unanimously (tiles 32 columns wide, split where xlsx-lineage splits its own). setConditionalRules/setConditionalStyleKey now maintain the ledger through the model's conditional_style_formula_owner_id. Confirmed in the same round: library-built pop-up menus offer their choices and record answers; controls all draw. Observed, unproven: our number cells render left-aligned until the same re-commit — plausibly the same cause | src/tst/tables.ts, src/tsce/owners.ts, test/conditional-writing.test.ts |
| 2026-08-14 | demo-08 round one: filters honoured both ways; tab order is not the open tab | Numbers (macOS), returned demo-08 | the library-disabled filter opened disabled with all ten rows visible, and the app's own panel re-enabled it correctly (»Da jeg slog dem til fungerede det«) — the first library-flipped filter flag honoured in the app, settling that proof. The document opened on the data tab despite the read-me sheet being moved first: Numbers picks the open tab from the UI state's TN.SheetSelectionArchive references, not tab order — setActiveSheet now re-points them (honouring awaits round two). Builder faults fixed: the cloned read-me table kept the donor's name so both tabs showed »Tabel 1« (renamed, and the data table with it), and the data table's A column still carried the seed-era send-back instructions (rewritten) | src/numbers/document.ts, scripts/make-demos.ts |
| 2026-08-14 | demo-07 round two: rules fire on open, the app keeps our ledger, and the alignment fault stands alone | Numbers (macOS), returned demo-07 v2 → olekristensen-v26.3-demo07-rules-round2.numbers | every fill drew with no cell touched, and the app's save preserved all seven library-written records — registration is app-confirmed end to end. The reviewer's only notes, »Ikke højrestillet« on exactly the number cells no rule matched (3, 2) plus the slider and stepper (60, 25), isolated the left-alignment as its own fault: cells the engine re-rendered (a rule applied) came out right-aligned, cells rendered from our stored state did not, and the saved records are uniform after the app's normalization — so the discriminator had to be something our writer omits. Measured: every plain value cell in the corpus carries its type's format id (449/449 numbers, 736/736 texts, 108/108 dates, 60/60 booleans), and ours carried none. setCell now stamps the app's default per type (automatic number {256, 253, 0, 0}, text {260}, boolean {1}, date {261, "d MMM yyyy"}); durations stay unstamped, unmeasured. Fallout closed: a control's format must beat the stamp (the checkbox 263-vs-1 distinction), and a bare boolean format now reads as boolean, not checkbox — the old reading survives only where a control spec says so. Right-alignment on open is the v3 rung | src/tst/tables.ts, src/tst/formats.ts, test/authored-shape.test.ts, test/conditional-writing.test.ts |
| 2026-08-17 | demo-06 round three: the first comparative round, and the clone the engine never met | Numbers (macOS), returned demo-06 | the YOUR TURN mechanism proved itself on its first outing: the checker's own =SUM(C16:C17) and =CrossCheck::B3, typed in the app, came back computing beside the library's — and the library's =CrossCheck::B2 opened as a ref error (»There's a ref error«) in the same document. The diff named the fault whole: the clone carried no FormulaOwnerDependenciesArchive at all — they live in CalculationEngine.iwa, outside the cloned object graph — so the app re-registered CrossCheck under a brand-new identity (kind-1, internal 57, formula_owner → the info object, plus derived kinds 3/4/11 at internals 58–60) and every reference compiled against the written identity dangled. Archives without a formula_owner reference get discarded on save, which is also what the blank donor's two stray kind-1 archives always were. Fix: mintTableOwnerArchive on every re-mint, mirroring the app's own minimal archive with empties stated; the registry's haunted fallback stands down when an archive-backed entry already names the table | src/tsce/owners.ts, src/tst/tables.ts, test/tables.test.ts |
| 2026-08-17 | demo-07 round three: rules settled whole; automatic alignment is the absence of the style | Numbers (macOS), returned demo-07 → olekristensen-v26.3-demo07-rules-round3.numbers | seven of eight rungs OK — rules on open, reuse, all controls, library pop-up menus, and both YOUR TURN twins authored (the checker's own rule keys rows 22–24; an app-built menu at C27). The deviation was alignment, and the checker's fix measured it: setting three cells to automatic in the inspector removed their per-cell text style ids — automatic is the absence of the style. The blank template stamps every cell with a do-nothing style whose para_properties state alignment: 0; rows this library inserts inherit it record by record, and any value cell carrying it renders left-pinned. The conditional-rules fixture's app-typed cells carry the very same style — the reason two rounds of archive diffs found nothing: nothing differed, the style was the left-pin everywhere, unremarked in a file built to test rules. The format-stamp hypothesis from round two is refuted as the alignment lever (the stamp stays — it is the corpus law for value formats). Value writes now drop the bare/alignment-0 style; control cells keep theirs (every measured control cell carries one) | src/tst/tables.ts, test/conditional-writing.test.ts |
| 2026-08-17 | demo-08 round two: the app honours the re-pointed sheet selection | Numbers (macOS), returned demo-08 | the document opened on the read-me sheet — the rung asks for a note only on failure and came back without one — so setActiveSheet's re-pointing of TN.SheetSelectionArchive references is honoured by the app; renames »All good«, the disabled filter and rewritten seed text clean. The app's own save keeps a mix of selections (five naming the read-me sheet, four the data sheet), so all-point-one-sheet is this library's write pattern, not the app's invariant — it keeps per-context view state and picks the open tab from among them. The two YOUR TURN rungs (a new sheet left active; the filter re-enabled) were not exercised and are re-asked rather than assumed. The clone's inherited filter rules on the read-me table remain — no deletion API exists yet | src/numbers/document.ts |
| 2026-08-17 | the decorated mint shipped malformed; demo-07 settled whole on resave | Numbers (macOS), damaged demo-06/08 + all-green demo-07 v9 | an empty message is not a smaller message: the first registration mint copied the app's decoration of empty dependency bags onto fresh owner archives, and two of those nest messages whose fields are required — an empty RangeCoordinateArchive (four required coordinates) inside the spanning bags, and a uuid_references entry missing its required owner_uuid. Numbers reported both clone-carrying demos damaged; the clone-free demo-07 from the same build opened green, isolating the mint in one comparison. required:check validates authored archives' top-level fields, not nested sub-messages — the gap that let it ship. The mint now writes identity only (uid, internal id, kind, formula_owner ref / base_owner_uid): omitting an optional field can never be malformed, and the e2e recompute probe already pins that the engine rebuilds dependency state on open. The same round settled demo-07 whole: every rung green including right-alignment on open — the bare-style drop is app-confirmed — and the checker's untouched resave is the app's own normalization of a fully correct document | src/tsce/owners.ts, test/tables.test.ts |
| 2026-08-17 | demo-06/08 round four: registration is three sites, and the engine's map is the one that decides | Numbers (macOS), returned demo-06 → olekristensen-v26.3-demo06-formulas-round2.numbers, returned demo-08 → olekristensen-v26.3-demo08-structure-round2.numbers | both clone-carrying demos opened — the damage class is closed — and the app still discarded the minted identity in each: it re-registered both clones under fresh uids (full 11-kind families, internals 57–67) because the mint had written the owner archives alone. The engine consults dependency_tracker.formula_owner_dependencies (an archive the list does not name is never loaded) and owner_id_map (uid ↔ internal id — the registry proper; the template's map runs to internal 54 while its archives stop at 35, and the app's own re-registration allocated 55 up, so the map is also the allocator's authority). References to the discarded identity were re-pointed at a fresh kind-0 owner parked on the map's unregistered_internal_owner_id list — the tombstone behind both rounds' #ERROR, now named OwnerKind.TOMBSTONE. One app deviation recorded: re-registering demo-08's clone, Numbers wrote the base uid into the model's haunted field, so one haunted lookup resolves kind-1. The mint now enrolls the full measured family at all three sites with internals past the map's max; whether the app then keeps the identity is the next round's question | src/tsce/owners.ts, test/tables.test.ts, test/owners.test.ts |
| 2026-08-17 | demo-06 round four: formula results left-pinned — the one write path the style drop missed | Numbers (macOS), returned demo-06 | plain values right-aligned and every library formula result sat left-pinned beside them (»Nice, the numbers right-align« / »But these don't they're left aligned«, and the same note on three more rungs): setFormula only ran the value write — stamp and bare-style drop included — when a cached value was passed, and the demo builder passed none, so formula cells kept the template's do-nothing style and no format. The checker's own =SUM(C17:C18), typed into a library-styled slot, carried the number format the library's cells lacked. Measured before widening the law: 1266 formula cells across the corpus, none with a value-typed cache missing its format. setFormula now drops the bare style regardless of cached value, the demos pass cached values, and the audit's format check covers formula cells | src/tst/tables.ts, scripts/make-demos.ts, test/audit.test.ts |
| 2026-08-17 | the API refuses what the app would: save-time validation and the offline audit | n/a — checker's directive after two damage rounds | »The API should make sure that we can't create faulty docs«: save() now sweeps every dirty object against the vendored schema's required fields and throws instead of writing a document Numbers would call damaged — the malformed class is unshippable through any path, raw-layer edits included. The well-formed-but-wrong class stays audit()'s: registration gaps at any of the three engine sites, cross-references whose target uid no TABLE owner carries (each earlier round's #ERROR, visible offline in the very file that shipped), unledgered rules, missing formats, bare-style pins. The audit's first run against a supposedly clean build found a real fault on its own: a content-less clone kept its donor's conditional-rule keys — rules the inspector shows and nothing evaluates — so withContent: false now clears conditional styles too. One check was written and then withdrawn by measurement: flagging the template's do-nothing text style on value cells — the measured left-pin on cells this library wrote — over-fires on app content, where a corpus sweep found 1071 such cells (660 in the categories fixture alone) as the normal state. The offline discriminator is unmeasured; the writers guarantee the state instead | src/tsp/store.ts, src/tsp/required.ts, src/tst/tables.ts, src/tsa/document.ts, test/audit.test.ts |
| 2026-08-17 | demo-06 round five: three sites are kept, not adopted — the app upgrades what it will not use | Numbers (macOS), returned demo-06 (»still ref error«) | the tombstone is gone and the ref error is not: with all three registration sites written, the app kept the library's kind-1 archive — tracked, and upgraded in place with the app's own empty-payload decoration — but parked its internal id on the map's unregistered_internal_owner_id list, deleted the ten derived archives, and re-registered the table under a fresh family anyway. So payload presence is not the adoption test the app runs at open (it back-fills payloads itself), but the decision ran against the identity-only shape that was sent. The mint now writes the app's exact empty-payload template from the start — stated bags, sentinel extents (int16/int32 maxima with all four required coordinates), an empty dependency tile beside the table's owner — and the next demo carries a formula-less control clone: if the control's cross-reference computes while the formula-bearing clone's still errors, adoption also wants per-formula dependency records; if both compute, registration is settled; if both error, the gate is outside the owner archives | src/tsce/owners.ts, scripts/make-demos.ts, test/tables.test.ts | | 2026-08-17 | demo-08 round three: an enabled empty filter set shows as a filter | Numbers (macOS), reviewer report (»opened with the filter on now«) | is_enabled defaults to true, and one enabled set — rules or none — lights the document's filter: the seed's empty columns filter set carries the default, and the demo builder's disable loop had grown a rules-only condition that skipped it, a regression against the v8 build that had opened correctly. Every set on every table is disabled again, both axes. The same return settled a mis-banked rung: the round-two fixture carries is_enabled = true on the data table's row set — the checker had exercised the re-enable YOUR TURN, and the app's own enabled state is banked in that fixture | scripts/make-demos.ts |
| 2026-08-17 | the wrap-delta seed carries a deleted table's whole owner family | n/a — fixture arrival (#66), measured offline | a registered owner family outlives its table: the seed — a Pages document with no tables — holds eleven FormulaOwnerDependenciesArchives (kinds 1, 3, 4, 5, 6, 8, 9, 10, 11, 12, 35, the standard family) whose kind-1 formula_owner names object 847874, which does not exist in the file, with every internal id mapped in the engine's owner_id_map and nothing on the unregistered list. Pages wrote and preserves this state, so a dangling formula_owner is the app's own leaving for a deleted table, not a malformation. The owners test's claim narrows accordingly: every owner naming an object that exists names a table; the dangling case is pinned at its measured count | test/owners.test.ts, fixtures/olekristensen-v26.3-seed-picture-wrap-returned.pages |
| 2026-08-17 | demo-06 rounds four and five: records match the app's shape and buy nothing yet — and the checker's twins answer the inverse question | Numbers (macOS), returned demo-06 r4 → olekristensen-v26.3-demo06-formulas-round4.numbers, r5 in hand | rounds four and five flattened the library's cross-table formulas identically — round five's owner carried dependency records byte-shaped like the app's own, and the app still reduced both refs to cached values. The formulas that survived at the next column were the checker's, typed by hand and said so in the notes (»It was just a hardcoded 5, I've done the right formula in D«) — a first reading mistook them for relocated library formulas and briefly blamed the uuid-references index; that hypothesis is withdrawn with the misreading. What the round actually established: the formula-less NoCalc clone kept its library identity a second time, and the checker's own =NoCalc::B2, typed in the app, computes against that library-minted registration — the inverse probe answered, our identity works as a target when the app authors the formula. The remaining unwritten site is the engine's uuid_reference_map, which the returned file now carries populated | src/tst/tables.ts, src/tsce/owners.ts, test/tables.test.ts | | 2026-08-17 | demo-08 round four: enabled-empty filter fixed, and a dead toggle measured | Numbers (macOS), reviewer report | with every set disabled and no hidden rows stored, the app still displayed the table as filtered and the panel toggle changed nothing — the stored state contradicts the display, so the app computed it against half-adopted hidden-state owners. Expected to resolve with the dependency-records work; re-asked with the next round rather than assumed | scripts/make-demos.ts | | 2026-08-17 | demo-08 round five: the toggle works exactly once | Numbers (macOS), reviewer report | opened filtered, one toggle-off showed all ten rows, and every toggle after that changed nothing — so the round-four expectation is withdrawn: registration records did not resolve it. With no hidden rows stored and every set disabled, a display the store contradicts plus a panel that works once and then dies both point at the app's open-time hidden-state computation rather than the stored flags, and the untested variable is the cloned read-me table's minted hidden-state owners (kinds 4 and 11). A control file splits the fault: the checker's own filter document, nothing cloned, both sets library-disabled — correct behaviour there convicts the clone's hidden-state registration; the same fault there convicts the disable write itself | scripts/make-demos.ts | | 2026-08-17 | demo-08 control round: the disable write is innocent | Numbers (macOS), reviewer report (»Demo 08 worked … within a blip it rendered the full table. So I guess it just needed the first recalculation«) | the checker's own filter file — nothing cloned, both sets library-disabled — opened and filtered correctly: the window showed the filtered table for a blip and the first recalculation rendered all ten rows. So the library's is_enabled write stands on an app-registered table, and round five's fault isolates to the cloned read-me table — its minted hidden-state owners or its donor-inherited hidden-state chain feeding the open-time computation. The blip also measured a smaller truth: the library does not regenerate the package's stored preview, so an edited document shows its donor's stale preview (and a moment of stale state at open) until the app's own save replaces it | scripts/make-demos.ts | | 2026-08-17 | demo-06 round eight: every stored site confirmed — the gate is the adoption pass itself | Numbers (macOS), returned demo-06 → olekristensen-v26.3-demo06-formulas-round8.numbers, the first comments-protocol return | the reviewer's verdicts arrived as cell comments, and the archives behind them settle the reference map's semantics: the reviewer's own surviving formula (=CrossCheck::B4, typed in the F-05 YOUR TURN, computing 10) is indexed in uuid_reference_map under its target's base uid as an owner-entry naming the referring table's internal id and cell — exactly the shape the library writes, so every stored engine site is now confirmed correct in form. The app still re-registered both formula-touched tables as complete fresh families (internals 77–87 and 88–98), kept the library's uids beside the new identities as map aliases, preserved the library's map entries verbatim as fossils, flattened both library-written outbound references (»Still just showing the number 5« / »Still just a cached number. No formula or reference«), and rewrote the clone-hosted reverse reference into a self-reference computing 0 (»Shows 0, references own table, not the other one« — its target re-pointed at the host's own new identity). The formula-less NoCalc kept its minted derived family but its kind-1 base was pruned without replacement. So the discriminator is in no stored site: the app's open-time adoption pass rebuilds identities and keeps only the formulas whose state it built itself. The sent/returned pair is banked as the full before/after of that pass; mining it is the next step, and until it yields, a library-written cross-table formula may open as its cached value — the working pattern stands: the person types the reference against library-registered tables | fixtures/ATTRIBUTION.md, test/tables.test.ts | | 2026-08-17 | demo-08: the stale hidden states measured and closed | Numbers corpus + returned round-2 file, measured offline | the fault the control convicted is a stored-state law: a hidden-state extent stores RowOrColumnState entries only while its filter actively hides — 44 of 44 corpus disabled-filter tables store none; the seed, saved with the filter on, stores 7. The library broke the law twice over: setEnabled(false) left the 7 states the checker's filter had produced, and the clone inherited the same 7 naming the donor's row uids — two tables declaring hidden state over the same rows, which the round-2 return shows the app preserving (Instructions: 7 states, filter off) rather than healing, and choking on (opens filtered; the panel toggle works once and dies). Disabling now drops the stored states, a reminted copy starts without any, audit() flags the stale shape (table/hidden-states-stale, pinned against the round-2 bytes), and every Numbers demo build gates on a clean audit. Whether the rebuilt demo-08 opens and toggles correctly in the app is the next round's question | src/tst/filters.ts, src/tst/tables.ts, test/tables.test.ts, scripts/make-demos.ts |
| 2026-08-17 | demo-08 round six: the state list is machinery, not a cache | Numbers (macOS), returned demo-08 → olekristensen-v26.3-demo08-structure-round6.numbers (comments »Opened on data tab« / »Nothing happened turning the filter on«) | the v9 build's cleared state list left the filter permanently inert, and the returned file proves the app never rebuilds it: the checker enabled the filter, nothing hid, and the app saved is_enabled = true with zero RowOrColumnState entries — a panel toggle engages an existing list, never builds one. With the control round (stale list present: open recovered, toggling worked) and round one (flag-only disable beside the list: everything worked), the law lands: the list is the filter machinery's working substrate, kept across enable/disable; the corpus's zero-state filters are filters that never ran. setEnabled is flag-only again, a fresh copy still starts with no list, and the round-old stale-states audit check is withdrawn with a pinned silence. The same return refutes the sheet-selection story: every selection pointed at the read-me sheet and the document opened on the data tab anyway, and no sheet reference exists in TN.UIStateArchive — the open-tab authority is unlocated and setActiveSheet is best-effort until it is found | src/tst/filters.ts, src/tst/tables.ts, test/tables.test.ts, scripts/coverage-matrix.ts |
| 2026-08-17 | demo-08 round seven: the minted hidden-state owners are the break | Numbers (macOS), reviewer report (»It opened with the filter toggle disabled, but the list filtered. Enabling it did nothing … disabling the toggle showed all rows, but from then on the toggle stopped working«) | keeping the state list beside the flag reproduced the rounds-four-and-five disease in a build whose copy carried no list at all — so the list is not the disease either: with the seed's seven states kept and the flag off, the document opened filtered against its flag, one disable transition worked, then the toggle died. The control (same states, same flags, no copy) healed at open and toggled freely; round one (copy present, no minted engine owners) passed everything. The variable separating every broken round from every working one is the copy's minted kind-4 and kind-11 hidden-state owner archives — probed model-and-map consistent (the copy's model owner uid is the minted base + 4 and mapped, its extents mapped) and still breaking the app's document-wide hidden-state pass. The mint now leaves the two hidden-state kinds to the app: a library copy's family is nine kinds, and whatever the app creates beside its own kind-4/11 to make them work stays unmeasured | src/tsce/owners.ts, test/tables.test.ts |
| 2026-08-17 | demo-08 round eight: the app back-mints what the mint leaves out — the copy itself is the poison | Numbers (macOS), reviewer report (»It opened with filtered rows and the toggle off, turning it on did nothing, turning off reveals all, and from then turning on is broken«) | the nine-kind mint changed nothing, and the returned file shows why: Numbers created the two missing hidden-state owners onto the library's family itself at open — the family arrives back eleven kinds — and the document's filter broke identically. So the poison is not which kinds the mint writes. Every arrangement of the copy's hidden-state registration measured so far — donor-inherited states, cleared states, mapped chain, unmapped chain, full family, nine kinds — breaks the app's document-wide filter pass, while the same document without a copy heals at open and toggles freely. What an app-made duplicate carries that a library copy lacks is the unmeasured difference; one specimen — the data table duplicated in the app itself and saved — would name it. Demo-08 parks here until that specimen arrives or the round-8 demo-06 mining reopens it | CHANGELOG.md |
| 2026-08-17 | adoption-pass mining, session one: three suspects eliminated, one registry identified | n/a — offline diff of the demo-06 sent/returned pair, the round-6 fixture's app-made table, and a corpus sweep | the copy-breaking difference is in none of the structures suspected so far: (1) the app's own fresh table's kind-4/11 hidden-state owners are byte-equivalent to the library's mint — same fields, same empty payloads, no references — so the owner archives are ruled out; (2) the model's hidden-state formula-owner links (fields 34/35 → the kind-4/11 uids) are coherent in library builds and merely re-uid'd in place at adoption; (3) row/column uid duplication across tables — which library copies produce — is legal: app-authored corpus files share uids freely (1350 cross-map duplicates in the categories fixture alone). Identified along the way: TST.HeaderNameMgrArchive + its tile are the document's header-word index — lowercased words from header cells mapped through table-uid → row/column-uid chains, one archive entry per adopted table (base uid, slot, anchor uid) — which the app rebuilds at open and library-added tables are never enrolled in. Stale for name-based header references, irrelevant to uid-based ones. The unexamined rewrites remaining in the pair: TSCE.TrackedReferenceStoreArchive, TST.TableInfoArchive, the summary/group-by pair, and the wholesale selection-object replacement | docs/FORMAT.md |
| 2026-08-17 | adoption-pass mining, session two: the pair is exhausted and the model lands | n/a — offline decode of every remaining rewritten object in the demo-06 sent/returned pair | no unwritten gate site remains in the pair — the evidence supports an ordering model instead: TSCE.NamedReferenceManagerArchive is one pointer to a document-level TrackedReferenceStoreArchive whose entries are {node-type 36, monotonic id}; the returned file's eight new ids match the re-registered Table 1's eight surviving formulas exactly — ours included — so tracking is adoption's output, allocated while the app recompiles a re-registered table's formulas, not a precondition. TST.TableInfoArchive fields 7 and 8 carry the +8 and +4 derived-owner uid lows as bare varints, coherent in library builds, re-pointed only for re-registered tables. The model that fits all eight demo-06 rounds: the app re-registers tables whose identity it distrusts, recompiles their formulas (same-table ones survive, library-written and app-written alike), and flattens cross-table references whose targets are themselves mid-re-registration in the same pass — the checker's typed references survive because they compile after the rebuild, against settled identities. If that model is right, no additional stored site can fix library-written cross-references; what would fix them is the app not re-registering our tables, and the most promising route to that is a table the app itself made, embedded and transplanted whole (the queued neutral-donor work) | docs/FORMAT.md |
| 2026-08-17 | the neutral default table: addTable grafts the embedded Apple-made donor | n/a — implementation over the measured donor closure | a table added without a named source is now the embedded blank donor's, imported whole: the donor table's closure is 81 self-contained objects plus its style preset's 30 (reached through model field 48, which the curated extractor skips — seeded explicitly), the drawable's parent is the one reference allowed to escape and is rebound to the gaining sheet, and the new deepCloneObjectInto guard refuses any import whose curated skeleton would leave a reference pointing into the source document. A shape-blind walk was tried and refuted in the same session: cell-record varints masquerade as ids and drag the whole document. The graft is also the queued adoption experiment — a wholly Apple-authored table structure arriving in a foreign document — and the next review round's demo-06 builds its comparison tables this way | src/tsp/clone.ts, src/numbers/document.ts, test/tables.test.ts |
| 2026-08-17 | the open-tab authority is not in the ViewState component | n/a — offline hash-diff of the round-two and round-six demo-08 builds | two builds with byte-identical ViewState objects opened on different tabs: the build the app opened on the read-me sheet (round two) and the build it opened on the data sheet (round six) carry the same eighteen ViewState objects, hash for hash — every sheet selection, the UI state, the document selections. So the stored view state cannot be what decides the open tab, no sheet reference exists in TN.UIStateArchive (measured earlier), and setActiveSheet's re-pointing is influence at most. The deciding state is outside anything these builds vary — possibly the app's own recency heuristics — and locating it would need app-side experiment rounds this project is not spending; the chase parks with setActiveSheet documented as best-effort | scripts/coverage-matrix.ts |
| 2026-08-17 | styling-defaults audit: four writer gaps measured and closed against the corpus | n/a — corpus sweep of 9,493 strokes, 8,000+ colours, 876 gradients, 495 reflections and 5,072 paddings at every site the writers serve | four places the library's output was a shape the current-era app never writes, none caught by any earlier check because all were well-formed: (1) colours authored without a space went out bare, while every iwork19/modern/current-era colour at every measured fill and stroke site names one — writeColor now stamps sRGB, generalising the shadow-session rule (822/822) to the whole value layer; (2) dashed stroke patterns went out as bare dash floats while the app states phase 0, the run count, and the float list padded to six — and the reader took the padding for dashes, so a corpus dashed border read as six segments, four of them zero; (3) an empty TSD.ReflectionArchive — the app's usual reflection, 377 of 495, presence meaning "on at the 0.5 proto default" — read back as no reflection at all; (4) authored gradients omitted the opacity, advanced flag and angle the app states on every fresh gradient (876/876 opacity, 875/876 advanced false, 755/765 angle 3π/2 top-to-bottom), and the stop-midpoint 0.5 default is now measured (1,993/1,993) rather than assumed. Confirmed already right: writeStroke's cap 0 / join 0 / miter 4 is the dominant shape at every site (8,992 of 9,493), the sidecar cell-border site included; solidStroke's 1 pt width is the app's border default (139/169 paragraph borders, every legend outline); cell padding supplies no library default and Apple's norm is all four sides at 4 pt. The text layer closed the audit: lineSpacing wrote an explicit mode 0 the app leaves to its proto default — every multiple-spacing archive in the corpus states the amount alone, explicit mode appearing only on the rare exact-height spacings (mode 1, twelve archives) — fixed to amount-only; underline and strikethru confirmed bare (113 of 118 on-states carry neither width nor colour, matching our pass-through), tab archives confirmed position-plus-optionals, and createParagraphStyle's sparse-versus-dense base question stays documented as open with copyOf as the dense path. Every default the task named is now measured, aligned or documented | src/tsd/style.ts, src/tsd/drawables.ts, src/tss/stylesheet.ts, test/styling.test.ts |
| 2026-09-23 | field report 6, the blocker: a copied slide must bring its own package component | Keynote (Keynote Creator Studio, format 26.0.0), relayed findings from the session that built a real deck with this library; measurements re-run against olekristensen-v26.3-mac-builds-effects.key | a Keynote-saved deck keeps each slide in a Slide-<slideId> component, and a library copy squatting in its donor's component makes the deck unsaveable: it opens and renders, and saving fails — »Dokumentet kunne ikke gemmes automatisk«, AppleScript save error -10000 — with a clean bisect (untouched saves, load→save saves, text edits save, duplicateSlide alone fails) and controls both ways: a library-built deck whose slides share the blank donor's one component saves fine, and the report's confirmed workaround — »Ole duplicated the slide in Keynote (⌘D twice) and saved. The library then changed only text, notes and one text box's geometry and path on those Keynote-made copies. The result saves in Keynote« — proves edits inside existing components are safe and new slides need their own. Measured from the checker's own three-slide deck: a slide's ComponentInfo carries the slide id as identifier, bare-kind preferred locator, Slide-<id> locator, document versions, an explicit is_stored_outside_object_archive: false, the save token, per-object UUID map entries, and external references — both real slides' reference lists byte-identical (component-level row for the master's TemplateSlide component, object rows for the stylesheet styles, one weak stylesheet component row), the document's own info holding one component-level row per slide component. ObjectStore.createComponent + moveObjectsToComponent now give every slide copy a fresh component modeled on its donor's — versions, save token and reference rows mirrored — with the document reciprocal declared. Two deliberate deltas from the app's shape, each measured-tolerated separately (the shared-component control saved with both): no UUID map entries, and the save pass adds an object row for the master where the app writes the component-level row alone — a wildcard reading of component-level rows was tried and refuted the same session (the apps' own Document infos carry hundreds of object rows beside a non-weak component-level stylesheet entry), so the superset stands and what licenses the app's omission is unmeasured. Final proof is a checker save round: duplicate → open → edit → save | src/tsp/store.ts, src/keynote/document.ts, test/keynote.test.ts, scripts/coverage-matrix.ts |
| 2026-09-23 | field report 6, the small findings: notes are never absent, style refs nest, transition ids are the app's | same relayed findings; measurements re-run against the corpus decks | three measured misalignments, none visible to any offline gate: (1) every corpus slide carries a note archive — 49 of 49, the empty ones with hasNote false — so addSlide({withContent:false}) stripping the note was the absent-field defect class, and it left notes = throwing on the slide it built; the note now stays, emptied, matching the app's fresh-slide shape. (2) TSWP.ShapeInfoArchive keeps its drawable-style reference on the embedded ShapeArchive super at field 1 — one level below the top-level scan — so every text box answered style() undefined; the scan now descends the super chain, and the copy-on-write handle carries the containing message so privatising repoints where the reference lives instead of minting a bogus top-level field. (3) no corpus fixture carries a slide transition at all — every deck reads effect "none" — so the only measured effect ids are the report's two families, both read back through this library from Keynote-authored edits: apple:ca-push (CoreAnimation-class, with direction — the reviewer's Push read {"effect":"apple:ca-push","duration":1.5,"delay":0.5,"direction":14}) and com.apple.iWork.Keynote.BLTFadeThruColor (bespoke, KN.SlideArchive 4.2.8 with duration double at 3, delay at 5, colour at 7); the docs' example id apple:transition/dissolve was a guess the app discards and is replaced. Unbuilt from the same report, queued: an image-on-slide API (the report's working adaptation is the measured recipe), slide background, numbered-list label styling, text-box path resize at setGeometry, and the SVG data/thumbnail/original triplet | src/keynote/document.ts, src/tsd/drawables.ts, skills/cupertino-files/SKILL.md, test/keynote.test.ts |
| 2026-09-23 | the path-space scale model: why a resized text box shrinks its text | n/a — corpus sweep of every TSWP.ShapeInfoArchive bezier path against its frame | a shape's path renders scaled from the path source's naturalSize to the geometry frame, text and all — the model that reconciles the field report's clipping with the corpus's contradictions. The report measured a copied text box shrinking and clipping after a frame-only resize until the path (1.3.5.2 size, 1.3.5.3 rectangle) was rewritten too. The corpus refutes every simpler rule: naturalSize == frame is not universal (266 of 376 bezier boxes), not even for plain rectangles (185 of 279), and the per-app split is an inversion — Pages text boxes keep path and frame equal (97 of 97 text-bearing rectangles) while Keynote text boxes mostly do not (93 of 103 diverge, legally: theme- and import-authored boxes drawn scaled). So no auto-sync is defensible offline: which state Keynote's own drag-resize writes is unmeasured, and a silent rewrite would alter states the apps themselves keep. The shipped shape is the explicit pairing the report's session proved in-app — setGeometry for the frame, the new setPathRectangle for the text's coordinate space, refusing non-rectangle paths rather than flattening them | src/tsd/drawables.ts, src/tsd/paths.ts, test/keynote.test.ts |
Findings from file analysis alone
Expensive to establish, easy to lose, no app involved:
| Finding | Evidence |
|---|---|
AST_function_node_index 212 = DURATION | In numbers-parser-v26.1-custom-formats.numbers, =$A$11+FUNCTION_212(,,8,22,11,500) lands exactly 8h22m11.5s after A11's midnight, and sibling rows differing only in the third argument (8→12→24) shift by that many hours. |
| The function index is not alphabetical, nor category-then-name | DURATION sorts before SUM in both orderings, yet is 212 against SUM's 168. The table must be measured. |
TOKEN_NODE marks an omitted argument, not a boolean | It carries AST_token_node_boolean, so it rendered as TRUE; the DURATION arithmetic shows the two leading tokens contribute zero. |
AST_function_node_index 168 = SUM | libetonyek-pages5-extra-dir.pages sums 5500 + 1170 + 1250 to a cached 7920; the Cats TOTAL row in both numbers-parser-*-issue102.numbers uses the same index. |
Merges live in the calc engine, not merge_region_map | No fixture has a region map. Colon-tract nodes in merge_owner.formula_store decode to rectangles where every anchor holds a value and no covered cell does, and the 14.4 and 26.0 saves of one document agree. |
TSP.Color gained an undocumented fixed32 at field 13 in the 26.x era | Present only in 26.x files, always paired with an explicit rgbspace, always exactly 1.0 across every document examined. |
| Cell and table styles have no shadow field | TST.CellStylePropertiesArchive and TableStylePropertiesArchive contain none; shadows are on the drawable's ShapeStyleArchive/MediaStyleArchive. |
Media style bags omit fill, shifting later fields down one | Confirmed structurally on 1475 style objects: field 2 is a message in a shape bag and a float in a media bag. |
An AST cross-table table_id is a calc-engine owner id, not a table id | TSCE.FormulaOwnerDependenciesArchive maps owner → object; 418 of 524 owners resolve and every one lands on a TST.TableInfoArchive. |
TSP.CFUUIDArchive and TSP.UUID are the same 128 bits | Four uint32 words pack as lo = w0 | w1<<32, hi = w2 | w3<<32; the AST's CFUUID then matches the calc engine's UUID exactly. |
Derived owner ids are base + owner_kind in current files | Holds for 339 of 409 entries carrying a base; older files use unrelated random UUIDs, so the stored base_owner_uid is authoritative. |
Nine of thirteen owner_kind values named from files alone | Each derived owner is used by a field; matching the field's UUID back to its owner entry names the kind. 1 table, 3 conditional style, 4 hidden rows, 5 merge, 8 categories, 9 summary aggregates, 11 hidden columns, 35 haunted, 200 document — each unanimous across every file that exercises it. |
owner_kind 200 is the document, with a hardcoded identity | Every kind-200 owner in all 23 files that have one, across three apps and every era, is uid = 666 derived from base = 466. |
| Conditional formatting and filters share one predicate archive | TST.FormulaPredicateArchive is the rule body in both ConditionalStyleSetArchive and FilterRuleArchive, told apart only by for_conditional_style. |
predicate_type 5 = = and 9 = < | The three rule sets in numbers-parser-v26.1-xlsx-lineage.numbers each carry a formula whose terminal AST node is the documented comparison enum, independently stating the condition. |
| Conditional rule sets are interned and refcounted like strings | In the same fixture, three sets cover 1921 cells and each data-list refcount (957/734/230) equals its cell count exactly. |
| Filter sets belong to a hidden-state extent, not the table | Reached via hidden_states_owner → HiddenStatesArchive → HiddenStateExtentArchive.filter_set; the traversal finds every FilterSetArchive in every fixture that has one, across all three apps. |
| Every filter set in the corpus is empty | 20 fixtures contain one; all are mode "all" (one row set is "any"), disabled, with no rules — so filter rule layout is schema-derived, not fixture-proven. |
Category row_lookup_uids holds row indexes, not UIDs | Across every categorised table in the corpus, the rows a group names hold exactly that group's value in the grouping column, and the groups partition the data rows exactly once. |
The complete grouping_type enum: 0 value, 1 year, 2 year+month, 3 weekday, 4 day, 5 year+week, 6 year+quarter | numbers-parser-v26.0-categories.numbers has one table per bucketing. Each code is confirmed by the shape of the dates it produces, not the table's name: year groups are all 1 January, quarter groups only in months 1/4/7/10, week groups each land on the same weekday, weekday groups collapse to ≤7 dates in one reference week. |
series_theme_styles is a six-colour palette, not a per-series list | Both corpus charts carry exactly 6, one with 2 series and one with 5. Per-series overrides live in the sparse arrays instead. |
SparseReferenceArray.count equals its entry count | Three observations across two charts: 6 entries/count 6, 5 entries/count 5, 0 entries/count 0. |
| A footnote reference is a U+000E, not the U+FFFC every other attachment uses | It lives in table_footnote, its own table; the U+FFFC inside the note storage is the number's placeholder, anchored in that storage's own attachment table. |
| A comment's author is shared, not per-comment | Every comment in both comment-bearing fixtures points at the same TSK.AnnotationAuthorArchive, which is listed once in the document's AnnotationAuthorStorageArchive. |
| An image mask's frame is in the image's coordinate space | Across the 79 masked images, image.pos + mask.pos puts the visible rect at a non-negative position 78 times and the crop window inside the image 75 times, versus 48 for the page-local reading. Full-bleed cases settle it: an image at (-91,-102) carries a mask at (91,102), cropping exactly at the page origin. |
A mask's path is stretched to naturalSize, per axis | Of 79 masks, 30 write the path at exactly naturalSize, 12 at a uniform scale, 37 at another scale; one is a plain 100×100 box stretched to 860×880. naturalSize equals the mask's frame in every file. |
| Row and column UIDs are not unique across a document | A table duplicated from another keeps its source's identities: in the categories fixture, Uncategorized row 0 and Categories row 0 share a UID. A UID keys a row within its table only. |
| A Pages floating drawable draws only if the document's z-order lists it | Both P19 rungs failed until the copy joined TP.DrawablesZOrderArchive; with the entry, both render. Keynote/Numbers keep paint order in-container instead. |
The paragraph-styles panel reads the theme's paragraph_style_presets | A style with name, identifier, map entry and both bags applies but never lists; adding the theme-list entry (the fourth requirement) lists it. |
A bookmark's ranged flag describes the run, not the name | Corpus: ranged=true at run lengths 13 and 46, false at exactly 1, name orthogonal. Pages, handed a named ranged=false bookmark over 13 characters, bookmarked one character; with the flag corrected the same bookmark spans its phrase — accepted by the app. |
| Footnote marks are superscripted by one shared anonymous char style | Bag exactly {superscript: 1}, run over the mark only, body U+000E and note U+FFFC alike — 8 body marks and their notes in the footnote fixture all point at one style object. Without it Pages drew the reference on the baseline at body size. |
| Native hyperlinks are styled by the template's own Link style | Identifier character-style-hyperlink, name "Link", bag exactly {underline: 1} — on every native link run, in every corpus template. Comments and date fields carry no styling convention (5 of 6 comment ranges bare; the sixth sits on a link). |
A pop-up menu's slot 0 is a bare NIL_TYPE None entry | Without it Numbers dropped the first choice; with a valued slot 0 the choices returned but the current-value checkmark vanished. Three-way experiment in the app. |
| A Keynote slide's paint-order membership for placeholders is a per-deck convention | 8 of 12 placeholders listed in one deck's owned_drawables/z-order, 0 of 33 in another — which is why no ubiquity threshold catches unlisting them, and why the app had to (an added slide rendered entirely empty). |
| Placeholders carry their slide as drawable parent and never declare it | 546 of 546 corpus placeholders, super-depth 3 — the container rule at Keynote-local type ids. |
| Placeholder text is a bare smart field, and an image placeholder is the same field over U+FFFC | TSWP.PlaceholderSmartFieldArchive (2031) is the smart-field super plus one varint = 1 across every modern instance — 73 measured: the corpus's 64 and a donated document made for the question, whose image placeholder is a 2031 spanning the attachment character with no TP.PlaceholderArchive anywhere. The lone 0-valued varint is a v10-era file. |
Per-paragraph direction lives in table_para_bidi, not the paragraph style | No style in 37 fixtures sets writing_direction, while the pptx-lineage deck writes a bidi ParaData pair per storage — the NSWritingDirection scale at uint16 widths. The pair's semantics are app-confirmed: first slot the direction (0 LTR, 1 RTL, 65535 natural), second slot 0 (65535 only with natural); an app-flipped paragraph carries exactly (1, 0) with its style untouched. One wrong slot — (1, 1) — renders LTR. |