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 — nothing staged
Every seed and rung staged so far is returned and banked.
- The collaboration seed is withdrawn: sharing needs iCloud Drive, and the checker will not enable it on the work Mac (»skip the collaboration features for now, i don't want to enable icloud drive on my work mac«). The question it carried stays open below.
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 is complete for 15.4. 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). The index table comes from the app itself:
npm run harvest -- --binary /Applications/Numbers.appreads the registration code in TSTables and names all 363 functions the build registers; the document harvest (--ingest,--emit-sheet,--drive) confirms it and supplies T, which the registry registers by another route. A new release means a new--binaryrun; the merge refuses any disagreement between the two sources. - Tables on pages and slides: the apps read them. Pages and Keynote 15.4 each list the grafted table and answer its cells through their scripting dictionaries, and Keynote's own resave keeps it (
test/e2e/proofs.e2e.test.ts). - Slide backgrounds, slides from a layout, layout changes: the app draws them. Keynote 15.4's PDF export is the set background's colour, and the app reports a slide made from a layout and a slide moved to one as being on that layout with the written title and body (VERIFICATION, the Keynote rows).
- The authored twins are fixtures.
olekristensen-v26.0-mac-authored.numbers,.pagesand.keycarry, built with the apps' own controls, the constructs no other corpus document had app-written: a function-shaped filter rule, a category summary, a table inserted by each app, a section-master object, a page break, a layout change, a slide background and a build. Each settled its row (VERIFICATION). What the twins show and the library still does not author: function-shaped filter rules beyond text-contains and starts-with, and per-group category summaries — the two roadmap rows of the matrix. - Keynote builds: creation shipped and played. Reading is measured whole — effect, timing and delivery decode from
animationAttributesagainst the fixture deck — retiming writes the same fields, andaddBuildwrites an all-at-once or by-paragraph build and its chunks field for field as the deck's app-authored builds carry them; Keynote 15.1.1 played one (VERIFICATION). Not authored: actions and build order. The scripting dictionary has no builds to ask, so a played build stays a person's observation. - Categories and filters: shipped and drawn.
addCategorydefines a by-value, nested or date-bucketed category from nothing in the corpus shape, and Numbers 15.1 draws the groups (VERIFICATION); summaries per group are not authored, and the cheap route is closed: acolumn_agg_typeentry written alone, in the twin's exact shape, draws nothing in Numbers 15.4's export where the twin's own summary draws its count, so the app reads the aggregator cache and the summary model's cells rather than rebuilding them. A writer has to produce all three, and only one Count instance is measured.addFilterRulewrites every condition the filter menu offers, byte for byte as the app-authored rules in the three filter-conditions fixtures, and recomputes the hidden rows offline intoTST.HiddenStateExtentArchive; Numbers 15.4 opened a comparison rule written this way with the right rows hidden, and the other conditions wait for the same look. - Chart appearance: the corpus is thin. Type, series colours, axis visibility and gridlines, tick marks and legend styling all read and write, copy-on-write against shared style archives, and Pages' own export of rung
P20-chart-gridlinesdraws the chart without the gridlines the library switched off. The corpus still has only two charts, so a document with several chart types would widen the evidence. - Keynote v14 upgrade path: settled. The same rungs against the 14.4.1 base — the deck Keynote converts on open, a different code path — open in Keynote 15.4, which reports each change through its scripting dictionary and draws the set background in its export (
test/e2e/keynote-ladder.e2e.test.ts), and the checker played the written build on the converted deck. - 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, and a shared document needs iCloud Drive, which the checker has declined to enable. Deferred; 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 |
|---|---|---|---|---|
| 2026-11-03 | can every measured filter condition be written as the app writes it? | the 44 app-written rules of the three filter-conditions fixtures, each re-authored on its own table and column and compared byte for byte; no app round | settled, all 44 identical: compileCondition rebuilds each condition's formula from the measured templates (one relative-point shape covers the date family in every unit and both directions), writePredicate the codes and operands, and the formula builder gained date, duration, thunk, group and index-call nodes and the whole-column cross reference to carry them; conditionHolds evaluates each offline for the hidden rows. The only mismatch on the first run was the parenthesis the app keeps as a one-item list node in the quarter formula. | src/tst/filter-conditions.ts, test/filter-conditions.test.ts |
| 2026-11-03 | the number conditions beyond comparisons: which predicate_type does each write? | Numbers for iOS 15.4.2 (T15.4.2 7377.1.21), the checker's own save on the phone of a one-table seed, returned as olekristensen-v26.0-ios-filter-number-conditions.numbers | settled, all eight: 11 above average, 12 below average, 15 is a duplicate, 16 is unique, 30 in the top and 31 in the bottom with qualifier1 10 for values and 9 for percent, the N in the formula's trailing constant; each formula is a whole-column SUM, so the engine evaluates these by code, not by formula. The same table in the Mac save had come back empty, so the phone round settled it. Every condition the filter menu offers is now measured. Beside it, the table model's inline hidden-states store carried the rules' formulas, and the +2 of a top-values rule is an addition node, a lone varint 1 the model's reference extractor took for object 1; that field is walked by the schema now. | src/tst/predicates.ts, src/tsa/extractors.ts, test/authored-returns.test.ts |
| 2026-11-03 | the rest of the filter menu: which predicate_type and qualifiers does each condition write, and what formula? | Numbers 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save of the seed dealt out at eight per table, returned as olekristensen-v26.0-mac-filter-conditions.numbers | settled for 36 conditions, one table skipped: the comparisons are 5 to 10 with 13 between and 32 not between; 34 is blank and 35 is not blank; the date family 17 today, 18 yesterday, 19 tomorrow, 20 is, 21 before, 22 after, 23 in the range, 24 in the next, 25 in the last, 26 exactly, 27 before and 28 after a relative point, 29 between two, 33 in this unit, the unit in qualifier1 (2 day, 3 week, 4 month, 5 year, 8 quarter) and the direction in qualifier2 (6 ago, 7 from now); durations 38 equal, 40 greater than, 44 between; checkboxes 55 and 56, compiled through function index 326, which the registry does not name, compared with interaction type 8. The file also exposed three reader faults, each fixed: a date literal's seconds sit in AST_date_node_dateNum and a duration's in AST_duration_node_unitNum, both of which rendered as 0; and a list node's count is AST_list_node_numArgs, not the function count, which left "this quarter" rendering as a dangling stack. "Number rules 2" (above and below average, top and bottom values and percent, duplicate, unique, is blank) came back without rules. | src/tst/predicates.ts, src/tsce/ast.ts, src/tst/formulas.ts, test/authored-returns.test.ts |
| 2026-11-03 | does a build the library wrote play after Keynote converts a 14.x deck? | Keynote 15.4 (macOS), the checker playing rung v14-K11 | settled, pass: "the title dissolved in on the first click - confirmed". The whole v14 ladder is now app-answered. | Keynote rung v14-K11 |
| 2026-11-03 | does a table grafted with the host theme's preset draw in Numbers? | Numbers 15.4 (macOS) driven by script: a blank document with two tables added by addTable, opened and exported | settled, pass: the app lists three tables and draws the two added ones in the theme's table style, header row and column shaded, cells as written, with no duplicate identified style in the file. | the render is not kept; src/tsa/table-donor.ts |
| 2026-11-03 | the filter menu's text conditions: which predicate_type does each write, and what formula? | Numbers 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save of seed-filter-conditions.numbers with eight rules on the Text table, one per column, returned as olekristensen-v26.0-mac-filter-conditions-text.numbers | settled for the eight text conditions: 1 starts with, 2 ends with, 3 contains, 4 does not contain, 36 is, 37 is not, 61 does not start with, 62 does not end with, each paired with the formula the app compiled (is: IF(LEN(needle)=LEN(cell), FIND.CASEINSENSITIVE(needle, cell)=1); is not: the length test, then IF(ISERROR(FIND…), TRUE, FIND…<>1); ends with: FIND over RIGHT(cell, LEN(needle)); the negations through IFERROR(…, TRUE)). The IF branches sit between AST node types 34 and 35, BEGIN_THUNK_NODE and END_THUNK_NODE in the apps' schema, which the renderer now passes through. The panel takes at most eight rules on one table, so the seed deals its conditions out eight per table; the number, date, duration and checkbox tables are still out. | src/tst/predicates.ts, src/tsce/ast.ts, test/authored-returns.test.ts |
| 2026-11-03 | does Numbers fill a group row's summary from the column_agg_type entry alone? | Numbers 15.4 (macOS) driven by script: a blank-donor category with the entry written in the twin's shape (column uid, level 1, agg_type 1, show_as_type 0) exported to PDF, beside the export of olekristensen-v26.0-mac-authored.numbers | refuted: the twin's export carries »Tæl (alle): 2« in every group row and the entry-only document's group rows are empty, the groups themselves drawn. The app reads the aggregator cache and the summary model's cells it wrote; the entry does not make it rebuild them. The writer was not kept. | the renders are not kept; no code |
| 2026-11-03 | can a Data/ file nothing points at be reclaimed safely? | every fixture in the corpus, each object's data_references list against a schema-position walk for TSP.DataReference fields; no app round | settled, both lists are complete: in all 68 decodable fixtures every DataInfo is named by some object's message-info list, and the schema walk over the vendored types names exactly the same set, 0 missing in either; the earlier reading that the lists leave most entries unnamed does not reproduce. compact() now drops the entries and files neither names, refuses a document with an opaque component, and is a no-op over the corpus. | ObjectStore.collectData, test/blank-documents.test.ts |
| 2026-11-03 | does Pages draw the chart without the gridlines the library switched off, on the 14.x-lineage base? | Pages 15.4 (M15.4-7051.0.79-2, macOS) driven by script: rung P20 and its untouched base exported to PDF and rasterised | settled, pass: the two exports differ on the chart page alone inside the chart's own box and on the page carrying the rung's appended paragraph (349 and 3153 differing pixels at 60 dpi, none elsewhere); the chart's horizontal gridlines are absent in the rung's export and present in the base's. | scripts/make-pages-docs.ts P20; the renders are not kept |
| 2026-11-03 | does Keynote accept every ladder rung on the 14.4.1 base it converts on open? | Keynote 15.4 (macOS) driven by script: all seventeen v14 rungs opened, read through the scripting dictionary, the background rung exported | settled for sixteen rungs: every rung opened; slide counts, titles, layouts, the skipped flag, the 1024 by 768 size, the body, the presenter notes, the 2-second automatic transition, the text box's text, the table's six cells and the layout change all read back as written, and the first slide of the background rung exports as the dark green set. The build's playing is a person's observation and is staged. | test/e2e/keynote-ladder.e2e.test.ts |
| 2026-10-27 | does a Display P3 colour render as P3, and a dashed border with our dash lengths? | Pages 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own saves | settled, pass: two greens from the same channels tagged p3 and srgb ("the colourspace tagging works - it's two different greens"); the [4, 2] rule drew dashed ("the dashes look fine, as specified"). The checker's own dashed border, authored in the Borders & Rules controls, is an anonymous variation of Body carrying [2, 2] at 2 pt with the same three paragraph fields as ours. | Pages rungs P25 and P27 as saved by the app |
| 2026-10-27 | does Pages honour a TOC rule we switched off? | Pages 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save | settled, pass: the TOC reopened with 49 entries where 61 were cached, none in Subtitle, and the style unticked ("PASS - the subtitles were not included in the TOC when the doc opened. And subtitle was unticked"). | Pages rung P26 as saved by the app |
| 2026-10-27 | do a created style's paragraph borders draw, solid and dashed? | Pages 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save | no verdict on the write, the base refuted: on the 14.4.1 base (iwork-mcp-v14.5-sample.pages) no rule drew ("no borders in 4b"), and the app's save had moved every paragraph, the document's own headings included, onto the theme's Default style and dropped every style outside the theme's listed set, ours among them. The upgrade path discards them; the rung moved to the blank donor with an authoring slot beside it. | Pages rung P27 as saved by the app |
| 2026-10-27 | does Numbers act on is_enabled both ways? | Numbers 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save | settled, pass: opened flat, switched on and regrouped, switched off and flattened ("i turned it on and off - it worked"). The app re-sorts rows into group order on enabling and keeps the order when disabled; the disabled definition's archive came back byte-identical, and the table model's category_owner_deprecated copy of the flag was synced to 0. | Numbers rung 25, saved by the app |
| 2026-10-27 | is the second conditional id the applied rule? | Numbers 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save | settled, refuted: after the checker changed which rule fired, cells on the first set carried 0 and the cell on the second set 15, matching no change in content. The value is per set, assigned by the app; preserving it verbatim is right. | Numbers rung 26, saved by the app |
| 2026-10-27 | why does the Format sidebar show no line style for a paragraph border we write? | Pages 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's reading of rung P28, and a corpus census | settled, the dash array: the same [4, 2] border as direct formatting (an anonymous variation of the paragraph's style, the app's own header) and as a named style both drew and both showed None in Borders & Rules ("none of them showed their border style in the editor"); the app's own dashed border from rung P27 differs from ours in nothing but its dashes, [2, 2]. A schema walk over every stroke at every site in the corpus finds exactly two dashed shapes at any width: [2, 2] with butt caps and [0.001, 2] with round caps, at paragraph borders, table bands, shape outlines and chart gridlines alike. The checker named the cause ("maybe your stroke type is not covered by the gui?"). Stroke.pattern gains "dashed" and "dotted" writing those shapes; arrays still draw and still read as None in the sidebar. The presets' own showing in the sidebar is rung P28's question now. | src/tsd/style.ts, test/styling.test.ts; Pages rung P28 |
| 2026-10-27 | does the Borders & Rules control read our dash presets back? | Pages 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save of rung P28 | dashed settled, pass; dotted refuted: the [2, 2] border as direct formatting and as a named style both read back as dashed ("worked", "worked"); the [0.001, 2] round-capped border, the apps' dotted shape on every outline and gridline in the corpus, drew (its style came back intact, positions kept) but the control showed None ("not showing up"). The corpus has no app-written dotted paragraph border, so the paragraph control's dots may be another shape; rung P29 asks the app for one. | src/tsd/style.ts; Pages rung P29 |
| 2026-10-30 | does Pages draw a text box the library copied, and what does its own box carry that ours lacks? | Pages 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save of rung P21 with a box inserted by Insert > Text Box beside ours | settled, pass, and the earlier "no ink" was the placement: the checker's comment on the P21 line, "It was almost not visible, think about how you place and format stuff, it was just the tip of a W character visible in the lower right corner of the page." The base is a 396 by 612 page and the rung placed the box at x 360 with a width of 220, so 36 points of it were on the page; the e2e probe's empty rectangle was the same box off the same edge. The app's own box is a floating ShapeInfo in the page group and the z-order list exactly as ours is, with its storage, two stand-in captions and the theme's textbox-0 shape style; ours carries a variation of that style from its grouped donor instead, and the app's resave gave our storage a placeholder smart field the donor's placeholder text brought along. The rung and the probe now place from pageSetup(). | Pages rung P21 as saved by the app; scripts/make-pages-docs.ts, test/e2e/proofs.e2e.test.ts |
| 2026-10-30 | why did copying a grafted table leave seven tables where four were built? | the Markdown round trip of the four-table Pages fixtures with the old copy path replayed, and the clone walk traced | settled, the reference scanner: the grafted donor's two summary header buckets carry only their hash function, 1, which the shape-based scan of library-created objects read as a reference to object 1, the document. The clone walk then reached the body storage, its attachments and every other table, so each copy grew by the tables before it (1, 2, 3: seven). The root message is never a reference now; a copy clones the table's own 60-odd objects and nothing of the document's. | src/tsp/store.ts, test/tables.test.ts |
| 2026-10-30 | what does Pages' paragraph line-style menu offer, and how does it write each item? | Pages 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save of rung P30 with all eleven items applied at 3 pt, and a screenshot of the menu | settled: eleven items after None. Two solid lines (round caps and joins; butt caps and miter joins), four dashed ones ([1, 1], [2, 2] and [6, 6] with butt caps, [0.001, 2] with round caps and round joins), and five brush strokes (Chalk2, Feathered Brush, Calligraphy, Dry Brush, Pen), each a solid pattern plus a smart-stroke archive holding only the name. The dotted item differs from the library's earlier dotted preset in its join alone; with round joins the written stroke is byte-identical to the app's, so the control's None on rung P28 line 3 was the join. Stroke.pattern gains "short-dashed" and "long-dashed"; the brush strokes stay unwritten. Beside it: every 26.x-era colour carries headroom 1.0, which writeColor now stamps too. | src/tsd/style.ts, src/tsp/schema.ts, test/styling.test.ts; Pages rung P28 |
| 2026-11-02 | the full e2e pass before 0.6.0: three probes fail; which side is wrong? | Pages 15.4 and Keynote 15.4 (macOS, Danish UI) driven by npm run test:e2e, the kept exports re-measured, the library's output compared byte for byte with the commit that settled each claim | settled, the probes: the section-master document is byte-identical to the settled one, and the exports show the drawable on every page of its section (pixels differing inside its rectangle: 4% on the dense pages two to four, 17% on pages eight and sixteen, 0% on page 17 and 0% outside the rectangle everywhere), so the "ink gained" metric under-read a drawable that draws behind body text; the probe counts differing pixels now. The Keynote text box was placed where asked; pdftotext puts the "fi" ligature of "files" on a line of its own, so the probe looks for the words before it. The blank deck's first-slide body placeholder resolves to HelveticaNeue-Bold in the library and in Keynote alike; the Palatino expectation was the house Body style, which that placeholder does not use. | test/e2e/proofs.e2e.test.ts, test/e2e/authoring.e2e.test.ts |
| 2026-11-02 | a review of the release stack: what does copying a table that holds a formula do? | the five PRs read for correctness, each candidate run against a blank document | settled, the reference scanner again: a formula's operator node is a lone varint 1 one level below the root, the shape of a TSP.Reference, so a copy of a table holding =B2+1 declared object 1, the document, and the clone walk brought it along (=B2-1 named object 2 and the copy never finished). Created objects of a type the vendored schema knows now declare the fields the schema types as references, walked by type; the shape scan remains for unknown types and for the prune walk, where guessing wide is the safe direction. The same review found a build left aimed at a dropped placeholder after a layout change, an inline picture accepted by the section-master move, an "any"-mode filter with no active rule hiding every row, case-sensitive text in a filter the app compares without case, two uids for one category group, a section's undeclared guide storage, and seven inline-Markdown round-trip faults; all fixed, each with a test. Not fixed and stated as a limitation: exported body text is not escaped, so a paragraph that looks like Markdown reads back as that syntax, and a quoted comment reads back as a block quote. | src/tsp/store.ts, src/tsa/markdown-parser.ts, src/keynote/document.ts, src/pages/document.ts, src/tst/tables.ts, src/tst/categories.ts |
| 2026-10-27 | does Numbers keep a cloned table's identity once its table_id string is the re-minted base? | Numbers 15.4 (M15.4-7051.0.79-2, macOS, Danish UI), the checker's own save | settled, pass: both clones kept their id strings and owner bases through the app's save; =CrossCheck::B3 read 5, =NoCalc::B2 read 8, a typed =CrossCheck::B4 read 10; the clone's ='Table 1'::C4 read 7 and =SUM(B) 30. No comments on the file. | demo-06-formler.numbers, saved by the app; test/e2e/proofs.e2e.test.ts probe switched on |
| 2026-10-26 | the open VERIFICATION claims, asked by script | Pages, Numbers, Keynote 15.4 (macOS, Danish UI), npm run test:e2e | thirteen settled, two refuted: Keynote saves a per-slide-component deck with added and duplicated slides; pictures, text boxes, tables, layouts, layout changes and backgrounds on slides read back or render as written; a build and a category survive the apps' resave; a filter rule hides its rows; band counts, heights and widths, a conditional fill and its re-pointing, banded rows, a typed duration and the formula bar (references and operators) agree. Refuted: a Pages text box is listed but not drawn; a cloned table's identity is re-minted by Numbers (references to it #REF!). Found and fixed: a self-reference stored as a cross-table node rendered with a table prefix. | test/e2e/proofs.e2e.test.ts, VERIFICATION |
| — | 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 | | 2026-10-13 | schema refresh from the installed apps | Pages 15.1.1, Numbers 15.1, Keynote 15.1.1 (descriptor dump; no document round) | the Pages schema doubles, and three names close open questions: the TP* family grows from 21 to 42 messages; TP.DocumentArchive 49 is should_use_anchored_drawable_wrap_slop, so the legacy three-field header switch is not field 49; TSP.Color 13 is headroom (float, default 1); TSWP.PlaceholderSmartFieldArchive field 2 is localizable; 2013's page-master names are section-template names at the same numbers. The shared families gained fields only, KN* and TN* are unchanged since 14.4, and format_type and interaction_type stay plain uint32 | proto/, src/pages/schema.ts, src/tswp/fields.ts | | 2026-10-13 | format types named from the app | Pages 15.1.1, TSUtility framework (decompiled with Ghidra through REA; no document round) | TSK.FormatStructArchive.format_type is the apps' TSUFormatType, and its naming function names every value: 1 Boolean, 254 Conflicting, 255 Automatic, 256 Decimal (the inspector's Number), 257 Currency, 258 Percent, 259 Scientific, 260 Text, 261 DateTime, 262 Fraction, 263 Checkbox, 264 Stepper and 265 Slider (both deprecated), 266 MultipleChoiceList (deprecated; the inspector's Pop-Up Menu), 267 Rating, 268 Duration, 269 Base (the inspector's Numeral System), 270 CustomNumber, 271 CustomText, 272 CustomDateTime, 274 CustomCurrency; 273 is grouped with 255 and 256 under Decimal and has no display name, so the corpus's CustomDuration reading stands on its own. Every corpus-measured value agrees with the app's name; the enum gains the eight values the corpus never showed | src/tst/formats.ts | | 2026-10-13 | interaction types bounded and named from the app | Pages 15.1.1, TSTables and TSUtility frameworks (decompiled with Ghidra through REA; no document round) | TST.CellSpecArchive.interaction_type has exactly five control values, 4 through 8, and the app names them Stepper, Slider, Star Rating, Pop-Up Menu and Checkbox: TSTables' cell-spec class converts an interaction type to a control format code by adding 100 and refuses anything outside 4–8 as unconvertible, and TSUtility's display naming labels 104–108 in that order. The five values measured in the app in August stand, and there is no sixth | src/tst/controls.ts | | 2026-10-13 | three binary leads, partial | Pages 15.1.1 frameworks (Ghidra through REA; no document round) | located, not decoded: (1) the formula function table — the function-name constants are 32-byte CFString records in TSTables' __DATA_CONST, grouped by source file rather than by index, so the index↔name pairing lives in the registry code that builds TSCEFunctionSpec objects with an explicit functionIndex:; two reference lookups from the name constants toward that code ended in Ghidra provider-cleanup failures and the registry was not reached. (2) the duration default format — TSKFormat's default-format factory selects a static per-type record from a table in TSKit for each TSUFormatType; the duration record was not decoded against the format-struct layout. (3) exterior_text_wrap.type names — TSDrawables serialises the type as a bare number in its sync JSON and the enum trait's only string is a log line, so no name table was found | — | | 2026-10-14 | the function index table, read from the app | Numbers 15.1, TSTables framework (static read of the registration code; no Ghidra, no document round) | all 345 functions the build registers are named, and the 270 the document harvest had measured agree entry for entry: the registry is one function in TSTables that loads each name's constant and calls a registration helper, with the index either in the sixth argument register or stored as a halfword to the stack just before the name load; scripts/function-registry.ts reads both shapes and harvest-functions.ts --binary merges the reading with the recorded harvest, refusing any disagreement. 296 is FIND.CASEINSENSITIVE, so the text-contains predicate renders its callee by name; T (149) is the one harvested function the registry registers by another route and stays on the harvest's authority. The two earlier Ghidra reference lookups toward this code were unnecessary: the pairing is readable from the file | src/tst/function-names.ts, scripts/function-registry.ts, src/tst/predicates.ts | | 2026-10-14 | the duration default format, from the app | Pages 15.1.1, TSKit framework (Ghidra through REA for the factory and the archive encoder; the initialiser read from the instruction stream) | a fresh duration format is the duration format class initialised for its type: automatic units on, smallest unit second (16), largest unit hour (4), short style (1), and the encoder writes exactly those four fields after the type, 268. The locale-independent defaults are cached per type in static slots the factory selects; the duration slot is filled by that initialiser. stampDefaultFormat now gives a typed duration this shape, and duration formats are written as 268 with the automatic-units flag rather than the corpus-less 273. Unit and style codes follow numbers-parser's constants, which agree with the initialiser's values. Not yet opened in the app: a manualProof row carries the check | src/tst/formats.ts, src/tst/tables.ts, scripts/coverage-matrix.ts | | 2026-10-14 | the category sidecar fields, and a category defined from nothing | corpus census over every group-by archive; 15.1 schema names | the eight sidecar fields are constant: fields 7–13 and 16 are TSCE.CellCoordinateArchive coordinates of the eight internal formulas the grouping registers with the calc engine (indirect aggregate-type change, grouping columns, aggregates in the root, grouping column headers, column order, row order, row order ignoring recalc, hidden states), at columns 0–7 of row 0 in every archive in the corpus, categorised or not; owner_index is 8 everywhere; row_uid_lookup is the table's row-uid list in map order. Every table carries the archive. A categorised archive adds the group column (column uid, grouping type 0, a fresh grouping-column uid), the lookup list and a tree: a referenced root with no value, leaves carrying value, a format manager (the value again, the cells' format, the row set) and rows, plus the same tree inline. By-value groups list in the code-unit order of their text, strings and numbers alike. addCategory writes exactly that; whether the app accepts a grouping whose internal formulas it did not register is the seed's question | src/tst/categories.ts, src/tst/tables.ts, scripts/make-seeds.ts | | 2026-10-14 | a build written from nothing | corpus deck census (the three app-authored builds) | an all-at-once build is five fields and a chunk: the build carries its drawable, delivery "All at Once", the deprecated duration as 0, attributes stating event trigger 1, chart rotation 60, text delivery by object, delivery option forward and the animation attributes (type "In", the effect, duration, delay 0, a random seed, writing direction not RTL), and chunk seed 1; its one chunk carries the build, delay, duration, automatic false, referent true and a fresh UUID stated twice, as the chunk identifier's build id with chunk id 1 and as the chunk's own build id. Both live in the slide's component and both lists on the slide grow. addBuild writes exactly that; the seed asks whether Keynote plays it | src/keynote/builds.ts, src/keynote/document.ts, scripts/make-seeds.ts | | 2026-10-14 | a filter rule written from nothing | the populated set in olekristensen-v26.3-mac-filters.numbers | a comparison rule is a predicate and a formula over the table's own identity: the predicate states the comparison's code, zero qualifiers, a cell operand as a relative reference to the host table at offset zero with the preserve and spanning flags stated false, the literal operand with its value (a number with its decimal pair), an unused third operand, the compiled formula and the conditional-style flag false; the formula is an ordinary cell-reference node carrying the table's own kind-1 owner uid, which is why the renderer shows it as OTHER_TABLE, compared with the literal. The set's three parallel arrays grow together and the set switches on. addFilterRule rebuilds both of the fixture's own rules byte for byte, the comparison and the text-contains one (type 3, the NOT(ISERROR(FIND.CASEINSENSITIVE(needle, cell))) formula now compiling because 296 is named); the hidden-state extent is left to the app | src/tst/predicates.ts, src/tst/filters.ts, src/tst/tables.ts | | 2026-10-14 | the shape audit over the night's writers | npm run shape:audit with three new bisect rungs (22 duration cell, 23 filter rule, 24 category) | two defects no test saw, both the well-formed-and-incomplete class: the category tree registered no object references (the group-by archive's stored list never gained its root, the root never listed its leaves — the paint-order class, invisible in the archives themselves), and the filter writer stated filter_enabled, which no app-written set carries: absent means enabled. Both fixed; the audit is back at its four standing candidates | src/tst/categories.ts, src/tst/filters.ts, scripts/make-bisect-docs.ts | | 2026-10-14 | nested categories, and the boolean node's format manager | the two nested tables in numbers-parser-v26.0-categories.numbers | a nested tree is the by-value shape repeated per level: one group column per level, each parent node carrying its value, rows that are the union of its children's, its child references and a format manager, with the groups at every level in text order within their parent; and the format manager depends on the value's type at every level — string and number nodes restate the value with the cells' format and the row set, boolean nodes carry the value alone. addCategory takes a column or a list of columns and writes that; the single-level writer's boolean leaves follow the same rule | src/tst/categories.ts, src/tst/tables.ts | | 2026-10-14 | a build delivered by paragraph | the corpus deck's third build | by paragraph is the all-at-once shape minus the text-delivery pair, with one chunk per paragraph: the build states delivery "By Paragraph", chunk seed equal to the paragraph count of the target's text, and attributes carrying only the event trigger, chart rotation and animation attributes; each chunk carries the shared build UUID with its own chunk id, 1 upward. The deck's chunks were set to play after the previous one with a one-second delay by their author; a fresh build's stage timing is the app's default and unmeasured, so a written one plays each stage on click. addBuild resolves the target's storage to count its paragraphs | src/keynote/builds.ts, src/keynote/document.ts | | 2026-10-14 | bucketed categories: the functor is a constant per kind, and the bucket is arithmetic | the six bucketed tables in numbers-parser-v26.0-categories.numbers | a bucketed group column carries the app's bucketing formula as a one-argument functor, identical bytes for every table of a kind, rendering as DATE(YEAR(x),1,1), DATE(YEAR(x),MONTH(x),1), the quarter's first month, DATE(YEAR(x),MONTH(x),DAY(x))+6-WEEKDAY(x,2) (the week's Saturday), the day itself, and DATE(2018,1,6+WEEKDAY(x)) (a reference week), each guarded against non-dates; the group's value is the bucket start, its format a date with the kind's pattern (yyyy, LLLL yyyy, yyyy-QQQ, yyyy'-W'w, d/M/yyyy, EEEE), and the leaf's format manager restates it. The arithmetic reproduces every group of every kind in the corpus, 150 rows. The app's order for bucketed groups is neither sorted nor first-appearance in the corpus; written groups ascend by bucket. addCategory takes { column, bucket } levels and ships the six functors as measured constants the test re-reads from the fixture | src/tst/categories.ts, src/tst/tables.ts | | 2026-10-14 | filter offsets are a boundary list, rules count from a host column, and the hidden rows are written | every filter set in the corpus; the populated one's hidden-state extent | three corrections to the filter model, from the bytes: filter_offsets is not a per-rule column list but a boundary list, [0] on every empty set and [0,1,2] on every two-rule set (the earlier reading took the stray leading zero as a column); a rule's column lives in its predicate, as a relative offset from a host column, and every app-authored rule sits on column 1 or 2 with offset 0 or 1, read here as host column 1 (a rule on column 0 is refused until a document shows its offset, and seed-filter.numbers puts its rule on column 2 so the app's answer tells host-column-1 from first-rule-column); and the hidden-state extent of a filter that has run lists one state per hidden row with filtered true, which addFilterRule now recomputes by evaluating comparison and text-contains rules offline — the fixture's seven hidden rows reproduce entry for entry. The reader's column is absolute now | src/tst/filters.ts, src/tst/tables.ts | | 2026-10-14 | a text box written by copying | the corpus's 45 Pages floating text boxes and the blank deck's own; npm run shape:audit over K12 and P21 | a text box is a deep copy of a text box: the shape info with its owned storage, styles and rectangle path travel with addCopyOf, and the copy differs from its donor in text, geometry and parent alone. A Keynote box's parent is its slide, which is never among the drawable's declared object references; every Pages floating box carries no parent at all, and the two fixture donors found outside a page group are group members whose parent is their group, so a Pages copy clears the field. The audit reports no field the donor lacks. The apps have not opened one | src/tswp/textbox.ts, src/keynote/document.ts, src/pages/document.ts | | 2026-10-14 | a slide instantiated from a layout | all 49 content slides of the nine corpus decks against their masters | a content slide is its master, copied, plus three things and minus five: every content slide carries exactly the placeholder fields its master carries (title, body, slide number: 0 mismatches in 49), none carries the object placeholder (0 of 49), and the slide adds the reference to the master, a note and — in 40 of 44 slide-number storages — a TSWP.NumberAttachmentArchive (0a040a00100010002207646563696d616c: empty string equivalent, kind page number, format 0 "decimal", the bytes insertPageNumber writes); the master alone carries its name, the object placeholder, the classic stylesheet record, the body paragraph and list style lists and the two thumbnail texts. The sage-tag map (field 28) is copied from the master in 12 slides, empty on both sides in 33, and emptied on the slide in 4, so the copy keeps it. The slide's style is the master's, shared. addSlide({ master }) writes exactly that; the app has not opened one | src/keynote/document.ts, src/keynote/schema.ts | | 2026-10-14 | a slide background of the slide's own | the 11 slides of zenodo-v26.1-pptx-lineage.key with a style of their own; 38 of 49 corpus slides share their master's | a per-slide background is a variation style, not a property on the slide: a KN.SlideStyleArchive in the stylesheet's component carrying only a super with the master's style as parent, is_variation true and the document stylesheet, override count 1, and slide properties holding the fill; it has no name or identifier, no stylesheet lists it, and slides with the same colour share one (referrers 4, 4 and 2 for the three greens and off-white, 1 for the near-black). The master's style keeps its white. setBackground writes exactly that and replaces the fill in place when the slide already owns a variation; the app has not opened one | src/keynote/document.ts, src/keynote/schema.ts | | 2026-10-14 | tables on pages and slides, and what the corpus lacks | the 18 inline tables of five Pages fixtures, the 3 slide tables of two decks; every Numbers sheet and Pages section template | a Pages inline table is a drawable attachment and a parent: all 18 hang from a five-field TSWP.DrawableAttachmentArchive (drawable, both offset types and offsets at 0) at a U+FFFC in the body, the table info's parent is the body storage and it carries no exterior wrap; all 3 slide tables have the slide as parent and sit in both of the slide's drawable lists. The neutral donor grafted under either anchors the same way, and npm run shape:audit over it lists seven referrer patterns no corpus Pages or Keynote table shares: the body storage and the slide gained the table info as a declared referrer, now fixed: Apple's table infos never declare their parent (71 of 71 carry one, 0 declare it, every app), so the info joins the drawable types whose container parent the store subtracts; the table info is declared by the formula-owner dependencies, summary model, summary cell vendor and category order it arrived with (corpus: the attachment or the slide alone); the style preset was declared by the model, which no app's model does (63 of 71 carry the reference, none declares it) — closed: a model extractor now declares what Apple's models declare, in their field sequence (4, 3, 19, 20, 21, 18, 25, 26, 27, 24, 60–64, 71–80, 30, 36, 87–89, 45, 46, 49, 86, 70, 34, 35, 38; inside the data store 3, 2, 1, 4, 5, 6, 12, 17, 19, 20, 18, 11, 16, 21, 22; repeats at first place), exactly on 64 of 71 models and on every model of the merge fixture, and the package metadata records a reference to a component's root object as a component-level row, as all 1,641 such references in the corpus are, so a dirtied Apple-written model saves byte for byte; the grafted preset is now listed in the host theme as every corpus preset is in its document's theme, which closes that finding too. The rest closed by measuring the wiring of an app-written table: its cell, text, category, label and pivot styles are its theme preset's network members (Pages 11–12 of 14, Numbers 45–48 of 48, per field), the styles its data lists reference are listed nowhere and hang off a network member as their parent (104 Numbers, 15 Pages and 8 Keynote paragraph styles, 166 Numbers and 11 Pages cell styles), and the formula-owner dependencies, summary model, summary cell vendor and category order never declare their table info (69, 65, 65 and 65 carry one, 0 declare, every app). A grafted table now takes the host theme's first preset, re-parents its cell-level styles onto that network and drops the donor's members, and the four owner-side archives join the drawable parent in the undeclared-container rule; the audit lists nothing on a grafted table; an extractor scanning one field at a time in those two sequences, repeats kept at first occurrence, reproduced Apple's list exactly on 64 of 71 models and on every model of the merge fixture, yet the merge round trip still grew by 918 bytes — all of it in TSP.PackageMetadata: once a type has an extractor, a dirtied Apple-written object re-registers its cross-component references in the metadata, which Apple's file lists differently. The order is settled; the external-reference pass is what keeps the extractor unregistered); the table style is listed by the stylesheet as well as the model; a paragraph style sits outside the table's style network; and the shape style sat unlisted until the graft began listing character, paragraph, list, shape and cell styles in the host stylesheet, as every corpus table's are. Measured negatives: no Numbers sheet in the corpus carries a text box or an image, so neither is written there; the only section templates with master drawables are two legacy fixtures (plain references from the template, drawables without a parent), and the 15.1 schema adds a page template with tag/drawable/z-index pairs, so where modern Pages keeps them is unmeasured and nothing writes them | src/tsa/table-donor.ts, src/pages/document.ts, src/keynote/document.ts | | 2026-10-14 | the authored twins, Numbers: a starts-with rule and a rule on column A | olekristensen-v26.0-mac-authored.numbers, built by the app on request | the host column is column 1 and offsets are signed: the app's "starts with C" rule on column A stores the cell operand's column offset as 2^64 − 1, that is −1 from a host at column 1, which settles the host-column question the seed left open and lifts the refusal of rules on column A; the rule is predicate_type 1 with the needle as the string operand, an unused third operand, and the formula FIND.CASEINSENSITIVE(needle, cell) = 1 — number 1 as a decimal node — beside the text-contains shape's NOT(ISERROR(...)). addFilterRule(0, { operator: "startsWith" }) writes it byte for byte on the app's own table. The app also wrote the rule into the pre-pivot list of a second filter set, which the reader does not need | src/tst/filters.ts, src/tst/predicates.ts, src/tst/tables.ts | | 2026-10-14 | the authored twins, the apps' own Insert ▸ Table | the three olekristensen-v26.0-mac-authored.* fixtures | the apps' new table is 10×5 in Numbers and 5×4 in Pages and Keynote, one header row and one header column, name shown in Numbers and hidden elsewhere, columns of equal width filling the host: 98 points in Numbers, 120.386 in Pages (the body width over four), 432.25 in Keynote; the model carries field 10 and lacks field 38 against the blank donor's table, the info is field for field the donor's, and every style role is the theme preset network's member with the preset listed in the theme — the wiring the graft now reproduces | src/tsa/table-donor.ts | | 2026-10-14 | the authored twins, Pages: master drawables, the page break, an inline text box, a comment | olekristensen-v26.0-mac-authored.pages | a modern master drawable is the legacy shape: a shape moved to the section master is a plain reference from the odd template's section_template_drawables, the drawable without a parent, exactly as the two iWork '19 fixtures carry theirs — the page-template tag/drawable pairs in the 15.1 schema stay unused. Insert ▸ Page Break wrote U+0005 as the paragraph terminator, the character the format notes had as a layout or column break; U+000C, which they had as the page break, appears in two current fixtures whose kind of break is unmeasured. Insert ▸ Text Box anchored the box inline at the insertion point (an attachment with no wrap), the shape 25 of 26 modern-era boxes in the corpus have, where current-era fixtures carry floating boxes from page layout and returned demos. The comment on one word reads back through comments() with author and range | docs/FORMAT.md | | 2026-10-14 | the authored twins, Keynote: a layout change, a table, a text box, a background and a build | olekristensen-v26.0-mac-authored.key | a layout change re-instantiates the placeholders and keeps the rest: the slide points at the new master, its title, body and slide-number placeholders are new objects, its title text survives in the new title, the text box it carried stays in the drawable lists, and the field set is a content slide's (1, 4, 5, 6, 7, 17, 19, 20, 27, 28, 36). The background the app wrote is the variation style setBackground writes, field for field; the build (apple:wipe, all at once) carries the five fields addBuild writes and no others; the text box the app inserted has the fields a copied box has. The inserted table is above | src/keynote/document.ts | | 2026-10-14 | the authored twins, Numbers: a summary in a category's group rows | olekristensen-v26.0-mac-authored.numbers | a summary is three things on the live TST.GroupByArchive, not one: a column_agg_type entry (the summarised column's uid, level 1, agg_type 1, show_as_type 0) that aggregates() reads; an aggregator entry for the same column whose AggNodeArchive tree carries one accumulator per group — on this text column a string_count with the group's min and max strings — and whose root node lists the groups' agg_formula_coords; and the summary model's cells. Reaching the archive is the model's field 86 (TST.CategoryOwnerRefArchive) as the category reader already does. agg_type 1 is Count, as the checker confirmed (»it was count«); the other codes stay unmeasured. The reader now pins the shape; no writer | src/tst/categories.ts | | 2026-10-14 | seed-category: a category this library defined from nothing | Numbers 15.1 (macOS), returned as olekristensen-v26.0-mac-category-seed.numbers | the app grouped it and the checker wrote »Works« as a comment on the sheet: Numbers kept the group column, the lookup and the tree with the library's node ids, re-minted the nodes' uids, re-registered the grouping's formula owners (two fewer owner archives than the seed carried) and dropped the donor's old root node, whose declaration the library had left on the archive beside the new root's. addCategory now retires the previous root with its declaration. The comment sat on the canvas, not on a cell, so cellComments() read a clean file: a canvas comment is a TSWP.CommentInfoArchive shape whose owned storage holds the words and whose comment storage holds the author and date with empty text; canvasComments() reads them on Numbers sheets and Keynote slides | src/tst/categories.ts, src/tswp/comments.ts, test/authored-returns.test.ts | | 2026-10-14 | seed-build: a dissolve build this library wrote from nothing | Keynote 15.1.1 (macOS), returned as olekristensen-v26.0-mac-build-seed.key | the app played it and the checker wrote »It worked« as a comment on the slide: Keynote kept the build and its chunk field for field, reseeded the random number and minted new chunk uids, and dropped the two references the library had declared — a build's drawable and a chunk's build are carried by every corpus build and chunk and declared by none, so both join the Keynote container rule and addBuild leaves them undeclared | src/keynote/schema.ts, test/authored-returns.test.ts | | 2026-10-14 | schema refresh from the 15.4 applications | Pages, Numbers and Keynote 15.4 (7051.0.79): descriptor dumps and a registry read; no document round | the shared families gained three fields, the app families a handful each, and the registry 18 functions: TSS.StylesheetArchive 25–27 are styles_for_15_2, styles_for_15_3 and styles_for_15_4 (the extractor's versioned-style range now reaches 27); TN.SheetArchive 25 is is_hidden and TN.SheetStylePropertiesArchive 2 tab_color, with sidecar tab_fill fields and two sheet commands; KN.TransitionAttributesArchive 19–20 and KN.BuildAttributesArchive 38–41 add custom angle, blur amount, a text-alignment option (from centre or from edge) and a blur flag, and KN.SlideNodeArchive 32 is has_comment; the TP* files are byte-identical to 15.1. The 29 TS* files are byte-identical across the three bundles, and Pages 15.4's own framework now embeds the TN* descriptors as well, identical to Numbers'. TSTables registers 363 functions, the 345 of 15.1 unchanged plus 374–391: ACOT, ACOTH, COT, COTH, CSC, CSCH, SEC, SECH, XOR, COMBINA, GAMMA, PHI, FISHER, FISHERINV, PERMUTATIONA, DAYS, PEARSON and RSQ | proto/, src/tss/schema.ts, src/tst/function-names.ts | | 2026-10-14 | the binary-derived facts re-read on the 15.4 frameworks | Pages 15.4 (7051.0.79) TSKit, TSTables and TSUtility: Ghidra through REA for five procedures, the instruction stream for two; no document round | every fact measured on 15.1 holds on 15.4: TSKFormat's default-format factory selects the same static slot per type over the same switch (0xfe–0x111), the archive encoder's duration branch writes the same four fields after the type, description decompiles identically, and -[TSKDurationFormat initWithFormatType:] still passes automatic units 1, smallest unit 16, largest unit 4, style 1; TSTCellSpec still converts interaction types 4–8 by adding 100 and refuses the rest, and subtracts 100 on the way back; the Swift TSUFormatType.description getter wraps NSStringForTSUFormatType, whose instructions in 15.4 differ from 15.1 only by the runtime's claimAutoreleasedReturnValue replacing retainAutoreleasedReturnValue — same 254–274 range check, same jump table shifted by that one instruction, same grouping of 255 and 273 — and the strings census finds the same 22 type names and 5 control names. Ghidra refused the thinned 15.4 TSUtility twice (provider exit 1, logs discarded), so that function was read from the instruction stream instead | — | | 2026-10-14 | the single-feature seeds: a filter, a table on a page, a table and a text box on a slide, an inline image | Numbers, Pages and Keynote 15.4 (macOS); returned as the four olekristensen-v26.0-mac-*-seed.* fixtures, the inline-image file not kept | every construct survived the app; two verdicts are about size, not shape: the comparison rule on column C applied to column C (»It worked«), which confirms the host column once more; the page table »worked great«; the text box »works«; the inline-image seed came back without a comment, which the protocol reads as both wrap modes drawing where their labels say. The slide table drew but »you've not sized it to fit it's contents«: the seed carried the Numbers donor's 20-point rows and 200-point columns onto a 1920-point slide, and the app keeps whatever a file states — the app's own Insert ▸ Table cells measure 432.25 by 129.8 in Keynote (the 1730 by 650 frame less a point, over four and five), 120.386 by 22.73 in Pages, 98 by 19.93 in Numbers, so a new table now takes that cell size whatever its row and column count, with a geometry of the cells plus the border allowance. The filter verdict's second half, »you did not size the first row to show all content, and you put a long instruction line there«, is about the seed, not the library: the instruction sat in the table name, which the app shows on one unwrapped line; the Numbers seeds now keep names short and carry the instruction in a wrapped first content row. The collaboration seed came back opened and saved but unshared, as agreed, and carries no operation or activity component | src/tsa/table-donor.ts, scripts/make-seeds.ts, test/authored-returns.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 field 13 is headroom, a float defaulting to 1 | Named by the 15.1 schema. 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 localizable (named by the 15.1 schema) = 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 instance 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. |