This document is the durable, session-surviving plan for HyperCard#. It
records what has been diagnosed and verified, what remains to build, in what
order, and which class of AI model (or human) each task suits. It supersedes
docs/PLAN.md (kept for history).
Last full verification pass: 2026-07-11, at the merge of the fidelity
diagnosis branch with the Phase 9 work (0669b6b). Every finding below was
re-tested against that HEAD by building the solution and rendering the sample
stacks headless to PNG. Line numbers drift; re-verify before fixing.
How to use this document. Each task has an ID (A1, B2...), a goal, the
files involved, an explicit spec, acceptance criteria, and a model
recommendation. When a task is finished, change its status line, note the
commit hash, and keep the spec text so later readers understand what was done
and why. Never mark a phase complete while any task in it is pending (see
the Phase Completion Guardrail in AGENTS.md). Start every session with
git pull and re-verify a finding's status before working it: the original
diagnosis was made on a stale clone that was several large commits behind
origin, and half its findings were already fixed upstream.
Model routing guide.
| Model | Use for | Rule of thumb |
|---|---|---|
| haiku | Mechanical, fully-specified, single-file edits with a clear pass/fail test | The spec contains every constant needed; no design decisions remain |
| sonnet | Multi-file feature work following an explicit spec or a named reference implementation | Design is settled; the task is careful implementation and testing |
| opus | Cross-cutting design, porting from foreign-language references, debugging without a known cause | Judgment calls remain; the spec defines goal and constraints, not shape |
| fable | Format reverse engineering with sparse references, architectural rewrites, anything that may invalidate assumptions across modules | Highest ambiguity and blast radius |
A larger model can always take a smaller model's task. The reverse is the risk.
Diagnosis method: headless render harness (see task A0) run against all six
sample files, plus targeted source inspection. Statuses: open, partial,
fixed.
- F1 [fixed, B2] Part chrome rendering. Originally buttons/fields were
never drawn (the old
PartRendererassumed chrome was baked into the WOBA bitmap, which is false; HyperCard drew all part chrome live). By0669b6bmost styles drew; B2 (fa0c9ca) completed the audit: everyPartStylenow renders in both normal and hilited states (checkBox X-mark, radioButton filled dot, popup dropdown arrow, and SKBlendMode.Difference hilite inversion for push buttons, none of which existed before), fixed a bug where Opaque buttons never drew their label, added verified flag accessors with 21 fixture-byte tests, and corrected the Part Record byte table indocs/stack-format.md. Golden-image lock-in is task B7. Two small gaps deferred: see the B2 follow-ups below. - F2 [fixed] Message-passing hierarchy.
StackViewModel.HandleCardClicknow walks part -> card -> background -> stack with Handled/Passed semantics, andopenCardlifecycle messages fire on navigation. VerifyopenStack/closeCard/mouseDowncoverage during C1 before closing that task. - F3 [fixed] .sit extraction encoding failure. The old code called
Encoding.GetEncoding("macintosh"), which always throws on .NET 8 without a registeredCodePagesEncodingProvider; a blanket catch swallowed it, so every StuffIt extraction silently returned nothing. Fixed by the custom table inCore/Text/MacRomanEncoding.cs(no external package, better). Verified:NEUROBLAST_Cyberdelia.sitextracts by name and renders. - F4 [worked around, not root-cause fixed] Wrong HFS signature constant.
HfsReader.csandHfsExtractor.csstill defineHfsMdbSignature = 0xD2D7. That is the MFS (400K floppy) signature; classic HFS is0x4244(ASCII "BD"), confirmed present at offset 1024 in both sample .img files. The primary signature check therefore still fails on real HFS volumes. Both .img samples open correctly today only becauseHfsReader.IsHfs()(verified 2026-07-11 via the new render harness) falls back to a heuristic: if the MDB+2 field looks like a plausible Mac timestamp, it accepts the volume regardless of the signature word. That masks the bug rather than fixing it, and risks a false positive on non-HFS data with a coincidentally plausible timestamp. Task A2. - F5 [fixed, B1] WOBA mask layer discarded. Was:
WobaDecoder.csdecoded the mask then output only the image layer;CardRendererdrew the card layer opaque in B&W mode regardless. Fixed by carrying a realMaskarray throughBitmapImageand compositing all three WOBA pixel states (opaque black / opaque white / transparent) uniformly in both render modes. The sample corpus never exercised this (its art is card-level, self-masked, full-bleed) — proven instead by two new hand-built fixture tests. See B1 for the full account, including a partial prior compensation (a Color-mode-only, heuristic-based transparency path) that this superseded. - F6 [fixed] Resource forks discarded.
Containers/StackEntry.csnow carriesResourceFork,Resources/MacResourceForkReader.csparses it, and button icons already consume it. Fork-dependent features (sound, AddColor, embedded fonts) build on this (Phase D). - F7 [fixed] Latin1 instead of MacRoman. No
Encoding.Latin1remains insrc/text decoding; all call sites useMacRomanEncoding. - F8 [fixed] Styled text runs unused.
TextRenderer.DrawStyledTextresolvesStyleRuns againstStyleTableBlock/FontTableBlock. B7 (75b6901) added pixel-level golden coverage via a synthetic styled-text fixture (plain/bold/italic/bold-italic/large/underline runs). - F9 [fixed, B3] Font fidelity. Multi-tier
FontMapper(userfonts/directory, system fonts, embedded ChicagoFLF + Noto Sans, generic fallbacks) was already implemented per the Font Policy inAGENTS.md. B3 (c91544b) closed the remainder: allSKFonts now useEdging=Alias/no-subpixel/no-hinting (in SkiaSharp 3.x the paint'sIsAntialiasno longer controls text edging, so glyphs were being smoothed), the gray "shadow" text style was made pure black, and a B&W pixel-purity regression test was added. Nearest-neighbor display scaling was already present (BitmapInterpolationMode.None). - F10 [crash fixed by A4; underlying formats still unsupported] Unsupported
container variants degrade, no longer crash.
(a)
BeavisEmulatorV2.situses StuffIt compression method 5, which is LZAH (LZSS plus adaptive Huffman). The currentDecompressLzssis plain LZSS, so it still emits 43,040 bytes of garbage — but as of A4,StackParser.Parsenow rejects that garbage with a typedStackFormatExceptioninstead of crashing withArgumentOutOfRangeException. Actually decoding this archive still needs the Huffman stage (D5). (b)ContextualMenus.sitis StuffIt 5 format (StuffIt (c)1997-magic);StuffItExtractor.IsStuffIt5already detected it, and A4 madeContainerPipelinereport that fact with a specific message instead of a generic one. Actually extracting it still needs a SIT5 implementation (D6).
Commit-history lesson. Earlier development spent ~15 consecutive commits on window chrome while the card canvas had the defects above. Chrome fidelity stays a goal (Phase E), sequenced after card fidelity: the preservation mission lives inside the 512x342 rectangle.
Researched 2026-07-11. Classic Mac OS and HyperCard are not public domain and were never freeware in a legally durable sense. Apple made some old system software freely downloadable years ago but retained copyright; "abandonware" is a community label with no legal force.
Policy (aligned with the Font Policy already in AGENTS.md):
- Bundle nothing extracted from Apple software. No NFNT bitmaps ripped
from a System file, no HyperCard built-in icon bitmaps, no Apple
sndor PICT resources. - Render everything the user's own files contain. Stacks and disk
images supplied by the user often embed the fonts, icons, sounds, and
color resources they need. Decoding user-supplied data is the product's
core function and mirrors how emulator-based preservation works. This is
already the pattern for button icons and the user
fonts/directory; extend it to sounds, PICTs, AddColor, and embedded NFNT fonts (Phase D). - Open recreations for defaults. Bundled: ChicagoFLF (public domain), Noto Sans (SIL OFL). Candidates to evaluate (verify license text before bundling): Urban Renewal set (Kreative Korp), ChiKareGo2.
- Built-in icon IDs. Stacks reference HyperCard's built-in icons by ID; those bitmaps live in HyperCard itself and must not be copied. Maintain a table of well-known IDs mapped to original replacement pixel art drawn from scratch for this repo (task R4). Until then: neutral 32x32 placeholder frame plus one log line per missing ID.
Goal: any single task in this roadmap can be executed by a small model or a new human contributor without holding the whole system in their head.
Module boundaries (enforced by project references, no cycles):
Coreknows bytes and models; never references SkiaSharp, Avalonia, or the interpreter.Renderingknows Core and SkiaSharp; never references Avalonia.HyperTalkknows Core (object resolution) and nothing visual.Appcomposes the other three; all Avalonia types stay here.Core/Resources/owns resource-fork decoding (icons, sounds, color, fonts).App/Chrome/(to be created, task E1) owns all System 7 look-alike window furniture.
Code rules:
- One binary block type, one file; one decoder, one file.
- Byte-offset knowledge lives inline next to the reads (the header-comment
style of
CardBlock.csis the model). - Public APIs carry XML doc comments stating units, coordinate systems (HyperCard rects are top,left,bottom,right in card coordinates), and endianness assumptions.
- Files stay under ~400 lines; split by responsibility past that.
(Current debt:
HyperTalkInterpreter.csis ~1,600 lines after Phase 9; splitting it into statement/expression/property partials is task M1.) - Every
src/project gets a one-screenREADME.md: what it owns, what it must not reference, where its tests live (task M2). - No emoji, no em-dashes in code or docs (house style).
Maintenance tasks:
- M1. Split the interpreter [sonnet] [status: pending]: partial classes or focused files (statements, expressions, built-in functions, properties, chunk expressions) with no behavior change; tests must pass unchanged. Do after C1 to avoid churn.
- M2. Per-project READMEs + AGENTS.md file-map refresh [haiku]
[status: pending]: after each phase completes, regenerate the Quick File
Reference table in
AGENTS.mdand each project README. - M3. Create missing research docs [haiku] [status: pending]:
docs/stack-format.md(block map; CARD/BKGD/part layouts from code comments; WOBA semantics; container formats incl. SIT compression method table: 0 none, 1 RLE90, 2 LZW, 3 Huffman, 5 LZAH, 13 LZSS+Huffman, 15 Arsenic; HFS MDB0x4244at offset 1024 vs MFS0xD2D7) anddocs/hypertalk-coverage.md(statement/function/property table with status, generated by readingHyperTalkInterpreter.csandAstNodes.cs). Both are already referenced byAGENTS.mdbut do not exist.
Retired task IDs: A1 (MacRoman encoding — already fixed upstream, verified by A0 render harness), A3 (StuffIt verification — already works via upstream A1 fix), A5 (resource-fork carrying — already implemented upstream). Task numbering is never reused to keep IDs stable across sessions; gaps are documented here.
- Goal: A repeatable CLI rendering any stack's cards to PNGs, so rendering tasks show before/after evidence and CI can diff golden images.
- Files:
tools/RenderDump/RenderDump.csproj,Program.cs,README.md; added toHyperCardSharp.slnunder atoolssolution folder. Also fixedtests/HyperCardSharp.Core.Tests/HyperCardSharp.Core.Tests.csproj, which was missing the same Linux native-asset package and madePhase24CorpusTests.AllSamples_FirstCardRendersWithoutExceptionfail on this dev machine with aSKObjecttype-initializer exception (not a product bug; confirmed identical failure on a clean stash before this task touched anything). - Spec (as built): Console app referencing Core + Rendering. Args:
<input-file> <out-dir> [max-cards]renders;--info <input-file> [max-cards]prints the block inventory (StackParser.GetBlockInventory) and a per-card part table without rendering. UsesContainerPipeline.UnwrapEntries(the fork-carrying API, not the olderUnwrapMultipletuple form) so resource-fork size is visible in the log. All pipeline log lines are echoed.SkiaSharp.NativeAssets.Linux.NoDependenciespinned to the same version asSkiaSharp(3.119.2). - Verified: Ran against all six samples. Five render cleanly
(
NEUROBLAST_HyperCard,.img,NEUROBLAST_Cyberdelia.sit,.img, and — confirming F10(a) is still open —BeavisEmulatorV2.sitfails itsStackParser.Parsecall with the expectedArgumentOutOfRangeException, caught and reported by the tool rather than crashing it).ContextualMenus.sitcorrectly reports zero stacks found (F10(b), SIT5 still unsupported).dotnet buildanddotnet test: 0 errors, 0 warnings, 39/39 tests green. - Finding surfaced by this task (updates F4 below):
HfsExtractorlogged"HFS volume found with 1 stack(s)"for both.imgsamples, which looked like F4 was already fixed. It is not:HfsMdbSignatureis still the wrong constant (0xD2D7, the MFS signature) in bothHfsReader.csandHfsExtractor.cs. What actually makes it work is a heuristic added toHfsReader.IsHfs()that accepts any volume whose MDB+2 field looks like a plausible Mac timestamp, bypassing the signature check entirely when it fails. This is a symptom patch, not the root-cause fixAGENTS.md's Root-Cause Policy calls for: the constant is still mislabeled, and the heuristic could accept non-HFS data that happens to have a plausible-looking timestamp field. A2 is revised below to fix the constant itself and tighten the heuristic to a documented fallback rather than the primary path.
A2. Fix the HFS signature constant; demote the timestamp heuristic to a fallback [model: haiku] [status: done, commit 3c2ba0f]
- Goal: Close F4 for real.
.imgsamples already extract via HFS today (A0 confirmed this), but only because a heuristic masks a wrong constant. This task fixes the constant so the primary, well-defined check is the one that actually matches real HFS volumes, and keeps the heuristic only as a documented last resort for non-standard imaging tools. - Files:
Core/Containers/HfsReader.cs,HfsExtractor.cs. - Spec: Change
HfsMdbSignaturefrom0xD2D7to0x4244in both files (add a one-line comment noting0xD2D7is the MFS signature, to stop the next reader making the same mistake). LeaveIsHfs()'s timestamp-plausibility fallback in place for the non-standard-tool case it was added for, but reorder/comment it so it reads as an explicit fallback path after the corrected primary check, not as the thing silently doing all the work. - Accept:
HfsReader.IsHfs()returns true for both sample .img files via the primary signature check alone (add a unit test asserting this directly, bypassing the heuristic, e.g. by truncating the MDB+2 bytes to an implausible value and confirming the signature check alone still passes). Render harness output for both samples is unchanged. Existingdotnet testsuite (39 tests) stays green.
- Goal: Close the F10 crash path; enforce the graceful-degradation UX rule ("never a crash").
- Files (as built):
Core/Stack/StackParser.cs,Core/Stack/StackFormatException.cs(new),Core/Containers/ContainerPipeline.cs,Core/Containers/StuffItExtractor.cs.Core/Binary/MagicDetector.csandApp/ViewModels/StackViewModel.cswere not touched:MagicDetectorturned out to be dead code (zero call sites outside its own file/test — detection actually happens per-extractor viaCanHandle()insideContainerPipeline), andStackViewModel.LoadStack/MainWindow.OpenFileAsyncalready forward exception/log messages verbatim intoStatusText, so fixing the message at the source was sufficient without App changes. - Spec (as built):
EnumerateBlocksrejects headers withSize < 16orFileOffset + Size > data.Length, logging the specific reason and stopping enumeration (same graceful-stop pattern as the pre-existingsize <= 0check) — this is the actual fix, since it prevents the bad header from ever reachingParse()'s slicing.StackFormatException(carries a byte offset) replacesInvalidDataException/raw BCL exceptions;Parse()wraps both the per-block dispatch loop and the PAGE-parsing loop so inner-content failures also get offset context.StuffItExtractor.IsStuffIt5(already existed privately, detecting the"StuffIt "banner) promoted to public;ContainerPipeline.UnwrapandUnwrapMultipleboth check it to log "StuffIt 5.x/Aladdin archives are not yet supported..." instead of the generic "found no STAK entries." - Verified:
dotnet test50/50 (8 new: 4 StackParser bounds/exception tests incl. a random-garbage fuzz case, 4 SIT5-message tests). RenderDump swept all six samples: the four healthy ones render identically to before A4 (unchanged card counts/IDs);BeavisEmulatorV2.sitnow fails withStackFormatException: No STAK block found in file (at offset 0x0)instead of a rawArgumentOutOfRangeException;ContextualMenus.sitreports the specific SIT5-unsupported message instead of a generic one. Random 256-byte garbage also fails gracefully with the same typed exception, confirming the fix generalizes beyond the one sample that exposed it. (A third occurrence of A2's0xD2D7bug was noticed while reading nearby code during this task — see the Phase A exit-criteria note below; left unfixed here as out of A4's scope.)
Phase A exit criteria: met, 2026-07-11. All six samples either open
with correct names via their intended container path (4/6) or fail with a
specific, readable message (BeavisEmulatorV2.sit, ContextualMenus.sit
— both still unsupported formats, per D5/D6, but no longer crash and no
longer show a misleading message). dotnet test: 50/50 green.
Known follow-up left for a later session (not blocking Phase A):
ContainerPipeline.TryExtractHfsResourceForks has its own inline
hfsSig = 0xD2D7 constant — a third occurrence of the bug A2 fixed in
HfsReader.cs/HfsExtractor.cs, masked by the same timestamp heuristic
inline in that method so it isn't currently user-visible. One-line fix,
same pattern as A2's commit (3c2ba0f).
Reference implementation throughout: HyperCardPreview (Swift, GPL, github.com/PierreLorenzi/HyperCardPreview), especially part drawing and text layout. Use it as a specification (read, understand, re-express); never translate GPL code line by line into this MIT project.
- Goal: Close F5: correct compositing (background paint -> background parts -> card paint -> card parts) with true card-layer transparency.
- Files (as built): same four files as planned, plus
tests/HyperCardSharp.Rendering.Tests/HyperCardSharp.Rendering.Tests.csproj(needed the sameSkiaSharp.NativeAssets.Linux.NoDependenciespackage A0 added elsewhere, to makeSKBitmap.GetPixelwork in tests on Linux). - Found before implementing (revises the original F5 diagnosis): a prior
session had already added a second
BitmapRendererconversion method and wiredisColor-gated transparency intoCardRenderer— but it only activated in Color mode (never in B&W, the default/primary mode), and its "transparency" was a naive "any white pixel is transparent" heuristic applied post-hoc to the SKBitmap conversion, not derived from the real WOBA mask (WobaDecoderstill discarded the decoded mask array unconditionally). So F5's user-visible symptom was unchanged; the fix below replaces this heuristic rather than building alongside it. - Spec (as built):
BitmapImagegainedbyte[] Mask. WOBA semantics (HyperCardPreview + the Wildfire format doc): pixel is black when Data bit = 1; opaque white when Mask bit = 1 and Data bit = 0; transparent when both are 0. Self-masking (MaskDataSize == 0, emptyMaskRect) setsMask = Data(aliased, not copied — decoded arrays are never mutated post-decode elsewhere in the codebase, so this is safe and avoids an allocation on the common case).BitmapRenderer's two conversion methods consolidated into one three-stateToSKBitmap, with a documented fallback to fully-opaque-white whenMaskis absent/wrong length (covers hand-built fixtures).CardRendererdraws both bg and card bitmap layers through the single method unconditionally (not gated on Color mode — a transparent pixel over the white canvas looks identical to opaque white, so this is correct in B&W mode too, and improves Color-mode AddColor fidelity as a side effect). Dropped the now-redundant_bitmapCacheAlpha.CardBlock.HideCardPicture/BackgroundBlock.HideBackgroundPicture(existed, were never consulted anywhere) are now wired in. - Verified: 6 new tests (2
WobaDecoderTestsagainst a hand-built 16x16 three-state BMAP fixture with real separate mask data, verifyingMaskis distinct fromDataand each of the three states decodes correctly, plus a self-masking case verifyingMask == Data; 4BitmapRendererTestsasserting the three composited SKBitmap pixel states directly viaGetPixel, including the no-mask-provided fallback). Suite: 58/58 green (was 52). RenderDump swept all six samples: output byte-identical to pre-B1 (the sample corpus is self-masked/full-bleed, so it never exercised the separate-mask path — the fix is proven by the fixture tests, not the samples, exactly as anticipated in the original accept criteria below). - Accept (met): Unit tests on a hand-built BMAP fixture with known mask/image bits assert all three pixel states in the composited output. NEUROBLAST renders unchanged (self-contained cards). A fixture stack whose background carries art would no longer be whited out (no sample in the corpus exercises this scenario; covered by the fixture tests instead — worth sourcing such a sample for the B7 golden-image suite).
-
Outcome: All 12 styles render in normal + hilited states (checkBox, radioButton, popup, and push-button hilite inversion were newly added; Opaque-label bug fixed). Added Part flag accessors (
LockText,SharedText,FileHilite,AutoHilite,SharedHilite,ShowLines,WideMargins,Family) with 21 fixture-byte tests; corrected the Part Record byte table and added flag/enum tables todocs/stack-format.md; built the reusablePartShowcaseFixture(intests/HyperCardSharp.Rendering.Tests/Fixtures/, consumed by B7). Suite 86/86. Flag bit positions were derived from HyperCardPreview'sFilePart.swiftas a spec only (GPL, no code copied) and independently reimplemented. Notable correction: buttonhiliteand fieldlockTextdo NOT share a bit (the sharedFlagsbit 0 isenabled/lockText;hiliteisMoreFlagsbit 6). -
Follow-ups found during B2 (deferred, tagged):
- B8. Field ruled lines (
showLines) [model: haiku]: theShowLinesaccessor and doc entry exist, but neitherPartRenderernorTextRendererdraws the ruled underline per text row. Needed for faithful ruled-line fields. Small, self-contained. - Button
enabledbit not wired to runtime [model: haiku]:Flagsbit 0 (inverted) is the buttonenabledflag, documented but not connected to the existingPart.Enabled/EnabledOverrideruntime properties, and disabled buttons are not drawn dimmed. Fold into C2 (autoHilite/click feedback) or do standalone. Popup "pressed" hilite feedback is likewise inert and also belongs with C2.
- B8. Field ruled lines (
-
Goal (original): Take F1 from partial to done: every
PartStylerenders correctly in both normal and hilited states. -
Files:
Rendering/PartRenderer.cs,Core/Parts/PartStyle.cs,docs/stack-format.md. -
Spec: Verify the style enum against the format doc (0 transparent, 1 opaque, 2 rectangle, 3 roundRect, 4 shadow, 5 checkBox, 6 radioButton, 7 scrolling, 8 standard, 9 default, 10 oval, 11 popup) and audit the existing chrome for each: checkBox (12x12 box, x/check when hilited), radioButton (12x12 circle, filled dot when hilited), default (extra 3px rounded outer ring), oval (ellipse hit region and frame), popup (frame, title-width label region, drop-down arrow). Hilite = invert the part rect (SkiaSharp
SKBlendMode.Differencewhite fill or layer inversion). Verify flag semantics with fixture-byte unit tests and record them indocs/stack-format.md: hidden bit, hilite bit, autoHilite, showName, sharedHilite, showLines, wideMargins, lockText. -
Accept: A synthetic part-showcase fixture stack (built by a test utility writing CARD/BKGD bytes, or a freely-shareable stack documented in
docs/samples.md) renders every style; each style has a golden image in both states.
-
Outcome: All seven
SKFontsites inTextRenderer/PartRendererrouted through a singleFontMapper.CreateAliasedFont(Alias/no-subpixel/no-hinting). Fixed the gray shadow-text style to pure black and de-antialiased the checkbox/radio glyphs B2 had introduced. Added a B&W pixel-purity regression test onPartShowcaseFixture. Nearest-neighbor scaling (BitmapInterpolationMode.None) was already present inSkiaBitmapControland left as-is. Suite 87/87. -
Goal (original): Close the remainder of F9: authentic 1-bit text rendering.
-
Files:
Rendering/TextRenderer.cs,App/Controls/SkiaBitmapControl.cs. -
Spec: All
SKFontinstances:Edging = SKFontEdging.Alias,Subpixel = false,Hinting = SKFontHinting.None. The display control scales the 1x card bitmap withSKSamplingOptions(SKFilterMode.Nearest)at integer zoom factors. Add a B&W purity test: inRenderMode.BlackAndWhite, every rendered pixel is pure#000000or#FFFFFF(this also guards future work). -
Accept: Purity test green; a 2x zoom screenshot shows blocky doubled pixels, not smoothed edges.
- Outcome:
tests/.../Golden/GoldenImageAssertdecodes committed golden PNGs and the fresh render to raw BGRA pixels and compares tolerance-0;UPDATE_GOLDENS=1 dotnet testregenerates. Two goldens, both CI-enforced on Linux: the B2PartShowcaseFixture(every style, normal + hilited) and a newStyledTextFixture(mixed STBL runs, F8 coverage). Determinism:FontMapper.UseEmbeddedFontsOnlyskips the user/system tiers AND (fix added during orchestration) routes tier-4 generic families to the embedded Noto substitute, so no host fontconfig lookup can leak in; tests gated to Linux viaSkippableFactsince Skia's rasterizer differs by OS backend (windows/macos CI skip these, run everything else). A non-trivial guard prevents a golden passing against a blank canvas. Goldens verified idempotent (identical md5) and visually inspected. Suite 89/89. Nobuild.ymlchange needed (ubuntu-latest already runsdotnet test). - Follow-ups found during B7 (deferred, tagged):
- Sample-based goldens [model: sonnet]: intentionally skipped.
samples/is gitignored to avoid redistributing that stack content, and committing a PNG rendered from it raises the same concern; it also adds an un-guarded determinism surface (stack-embedded sfnt fonts, AddColor, resource forks). If local-only NEUROBLAST/Cyberdelia coverage is wanted, add it gated on Linux AND file-presence so it stays a no-op in CI. - Static font-flag parallelism [model: haiku]:
GoldenImageTeststoggles the process-wide staticFontMapper.UseEmbeddedFontsOnly. xunit parallelizes across test classes, so in principle this could leak into another font-sensitive test running concurrently. Harmless today (the only other showcase renderer, the B&W purity test, asserts black/white regardless of font), but a future font-sensitive parallel test could flake. Fix by putting all font-dependent test classes in one non-parallel xunit collection.
- Sample-based goldens [model: sonnet]: intentionally skipped.
Phase B exit criteria: met, 2026-07-11. All part styles render faithfully in both states (B2), text is pixel-crisp 1-bit (B3), and the golden suite (B7) locks it in on Linux CI. A stack with standard buttons and fields renders indistinguishably from period screenshots at 1x except for resources the file does not contain. Findings F1, F5, F8, F9 all fixed. Full suite 89/89 green.
- Goal: Finish F2's remainder. The part -> card -> background -> stack
walk and
openCardexist; complete the system-message set and target context. - Files:
HyperTalk/MessagePassing/MessageDispatcher.cs,App/ViewModels/StackViewModel.cs, interpreter environment. - Spec: Fire
openStackon load,closeCardbefore navigation,mouseDownon press (mouseUp only when release lands in the same part, as HyperCard does). Ensurepass mouseUpin a part script continues the climb (add tests for Handled vs Passed vs NotFound at each level). Setmeandthe targetfor every dispatch. Document the message order indocs/hypertalk-coverage.md. - Accept: Fixture stack tests: background-script navigation with
empty-scripted buttons works;
openCardpopulates a field on arrival;passchains verified by unit tests.
- Spec: mouseDown over an autoHilite button renders it hilited until
release; checkBox/radioButton toggle their persistent
hiliteproperty instead.hilitereadable and writable from HyperTalk (set the hilite of button X to true). Radio buttons in the same family (low nibble of the family field) unset siblings. - Accept: Golden pair (normal/hilited) for standard, checkBox, radioButton; interpreter get/set tests; family-exclusivity test.
- Spec: Hit-testing top-down: card parts before background parts,
last-listed first within a layer (verify the current loop order; fix if
reversed). Unlocked fields swallow clicks without dispatching button
messages (edit mode is out of scope; log once);
lockTextfields dispatchmouseUp. Browse-hand cursor over the card area. - Accept: Unit tests for overlap ordering and lockText routing.
- Spec: Work from
docs/hypertalk-coverage.md(M3): complete chunk expressions (char/word/item/line x of yas containers and sources),find,sort, date/time functions, and the properties real sample scripts use (collect misses by running the samples and logging unhandled constructs). Keep the AST explicit; no string re-parsing at execution time. - Accept: Coverage doc rows flip to done with linked tests; sample stacks' scripts execute without "can't understand" noise in the log.
- Spec: Button icons from stack
ICONresources work; addcicn(B&W path: mask + bitmap planes) and the built-in-ID fallback table per Asset Policy 4 (placeholder + one log line per missing ID until R4 supplies art). - Accept: Stack with
cicnicons renders; missing built-in IDs log once each, render placeholder, never crash.
- Spec:
Resources/SoundDecoder.csexists (verify format 1/2 coverage: sampledSynth, bufferCmd 0x8051, 8-bit unsigned mono, typical 11/22 kHz). Missing piece is output: pick a cross-platform audio path (evaluate feeding PCM to LibVLC, already a planned dependency, vs an OpenAL/miniaudio binding;System.Mediais Windows-only). Wire HyperTalkplay "name"andbeep(generated sine, never Apple's sample). - Accept:
playin a fixture script produces audio on Windows, macOS, Linux; unsupported snd formats log and continue.
- Spec:
Resources/AddColorDecoder.cshas a start; finish per hypercard.org/addcolor_resource_format/:HCcd/HCbgelement records (rect/button/field/PICT/resource elements with RGB and blend flags), applied in record order as a color overlay pass inRenderMode.Color. PICT elements depend on thePictDecoder; render what decodes, log the rest. B&W mode must remain bit-identical (guarded by B3's purity test). - Accept: A known AddColor stack (source via archive.org, add to
docs/samples.md) renders colored; B&W goldens unchanged.
- Goal: Close F10(a). The current
DecompressLzssemits garbage for method 5 because LZAH couples LZSS with adaptive Huffman coding of literals/lengths (LHarc lh1 family). - Spec: Clean-room from format documentation (The Unarchiver wiki, LHA lh1 descriptions); do not port GPL/LGPL sources verbatim. 4KB window, adaptive frequency tree with rebuild at cap, positions coded with the fixed prefix table. Implement the SIT entry CRC-16 check while here and validate all methods against it (it would have caught this bug).
- Accept:
BeavisEmulatorV2.sitextracts; data fork (43,040 bytes) has STAK magic at offset 4; CRCs verify for every extractable sample.
- Spec: New
StuffIt5Extractorfor theStuffIt (c)1997-container: 80-byte archive header, variable-length entry headers, methods 0/13 first; method 15 (Arsenic: BWT + MTF + RLE + arithmetic coder) as a separately-committed follow-up; until then enumerate the file table and report per-file unsupported methods. Register ahead of the classic extractor. - Accept:
ContextualMenus.sitextracts and renders, or (pre-Arsenic) lists its contents with a per-file "method 15 unsupported" message.
- Spec: When a user's stack carries FOND/NFNT bitmap fonts, decode the
NFNT strike (bit image, location table, offset/width table) and render
with the actual embedded font at native size, as tier 0 above the
existing FontMapper tiers. Structure as an
IGlyphSourceabstraction over (TTF typeface | NFNT strike) soTextRendererstays single-purpose. Fully within Asset Policy (user-supplied data); this is the maximum-fidelity endgame for text. - Accept: Fixture stack with embedded NFNT renders using it (compare against an emulator screenshot); stacks without embedded fonts unaffected.
- Spec: Original plan Phase 9:
MediaServicewrapping LibVLCSharp; embedded VideoView positioned over the card rect the script names; wire theplay movieXCMD-style invocations found in real stacks (log-first: run samples, collect the exact calls, implement those). - Accept: A stack referencing a MOV plays it in-window; missing codecs degrade to a log entry.
The shell should read as a 1-bit Macintosh regardless of host OS (see the
B&W dialog conventions in AGENTS.md; the UI deliberately avoids gray
tones). Chrome work resumes once Phases A-C are done; card fidelity
outranks chrome, a lesson paid for in commit history.
- Spec: Move
System7MenuBar,System7TitleBar, and window styling intoApp/Chrome/with a README documenting the metrics (menu bar height, title-bar rake pattern, border widths) and the known platform landmines from commit history (window transparency + CornerRadius broke Windows launch twice). Add an Avalonia headless smoke test that MainWindow materializes, run on the CI OS matrix. - Accept: Headless test green on Windows/macOS/Linux CI; one code path, no per-OS visual divergence beyond what the README documents.
- Spec: Go menu exists (verify Back/Home/Recent semantics); visual
effects exist in
TransitionRenderer(verify: dissolve, wipe l/r/u/d, iris open/close, checkerboard, timed per fast/slow qualifiers, driven byvisual effectbeforego). Add the message box (msg): a single-line System 7 window that accepts and evaluates one line of HyperTalk;putwithout a target writes to it. - Accept:
visual effect dissolve+go next cardanimates; message box round-tripsthe number of cards.
- Spec: Audit every dialog against the B&W System 7 conventions table
in
AGENTS.md(pure black/white, no grays, correct default-button ring); ensure keyboard completeness (Return = default, Esc = cancel). - Accept: Screenshot checklist per dialog in the PR description.
- R1. HC 1.x format divergences [fable]: version switch off the STAK
FormatVersion; acquire 1.x samples; document deltas indocs/stack-format.md. AGENTS.md targets HC 2.4.1 as primary; 1.x detect-and-warn is the interim behavior. - R2. PICT decoder completion [fable]:
Rendering/PictDecoder.csexists (~490 lines); audit opcode coverage against real stack PICTs, log unknown opcodes with offsets, extend as needed by D4. - R3. Password-protected stacks [fable]: detect and report per AGENTS.md; decryption without the password is out of scope.
- R4. Built-in icon replacement art [human + haiku]: original 32x32 pixel art for the ~40 most-referenced built-in icon IDs; wire into D2's table. Human judgment on art style and licensing.
dotnet build: zero errors, no new warnings beyond the knownRawStackScanner.csCS8600.dotnet test: green; new behavior has a unit test or golden image.- Render harness (A0) against all six samples: no crashes, clean logs.
- Net-diff review per the Regression Check Policy in
AGENTS.md. - Update
docs/stack-format.md/docs/hypertalk-coverage.mdwhen format or language knowledge changed. - Commit per Commit Policy (imperative message, Copilot co-author trailer). Pull at session start and before push: work happens from more than one machine, and a stale clone once produced a diagnosis of already-fixed bugs.