Skip to content

RUNLOG — overnight run, 2026-07-29

Decisions made, deviations from BRIEF.md, open questions. Append-only; newest at the bottom of each section.

Decisions

  • D1 — git2 over gitoxide. BRIEF.md prefers gitoxide with git2 as fallback. Taking the fallback immediately: git2's branch/merge/worktree APIs are complete and battle-tested; gitoxide's write-side merge story is still young. The outcrop-txn public API hides the backend entirely, so swapping later is contained to one crate.
  • D2 — vendored protoc. No system protoc in the build environment; outcrop-server uses protoc-bin-vendored from build.rs so local builds and CI are hermetic.
  • D3 — frozen wire contract is JSON-in-proto for values. Frontmatter values cross the wire as JSON strings (frontmatter_json, *_json fields) rather than a recursive proto Value type. Keeps the contract small, keeps unknown/OKF-provenance keys lossless, and matches what the canvas cards render anyway.
  • D4 — deterministic corpus, committed as a fixture. tools/corpus-gen is seeded (LCG, no clocks) and reproduces byte-identical output; fixtures/corpus/ is committed so tests never depend on generation. Violations are self-describing via x_violation frontmatter keys so tests can assert exact diagnostics.
  • D5 — lossless editing via byte-span splices. "CST-level" is implemented as span-aware frontmatter parsing (each top-level key maps to a byte range in the raw text); edits splice only the affected range. Same guarantee as a rowan/tree-sitter CST with far less machinery; the property test (edit one field → every other byte identical) is the acceptance gate either way.
  • D6 — per-crate commit granularity. The brief's red-green-per-feature commit series is relaxed to per-crate commits: subagents share one working tree, so the integrator commits after each wave. TDD ordering (test first) is preserved inside each crate's development; the commit history records crates, not features.

Deviations from the brief

  • V1 — native clients are source-only. The run executes in a Linux container: no Xcode/Swift toolchain, no Android SDK, no Windows SDK. The SwiftUI canvas, Kotlin stub, and WinUI stub are committed as source with generation instructions, but are not compile-verified tonight. The .proto is the contract; protoc invocations for each client are documented in their READMEs.
  • V2 — MCP verification target. M3's "MCP verified end-to-end from Claude Code itself" is done by driving the stdio MCP server from this session via a scripted JSON-RPC exchange.

Open questions

  • Q1 — RESOLVED. OKF v0.2 bundle marker confirmed against the actual spec (GoogleCloudPlatform/knowledge-catalog okf/SPEC.md; OKF = Google Cloud's Open Knowledge Format, announced 2026-06): the marker is okf_version: "0.2" in the frontmatter of a bundle-root index.md (the only index.md permitted frontmatter), and it is optional. Our assumed okf.yaml was wrong and is replaced. Also adopted: index.md/log.md are reserved filenames (listing/history), now excluded from record visibility in outcrop-store and outcrop-txn; type remains the only required key (matches our schemas); the must-not-reject list (unknown keys/types, broken links, missing index.md) matches the flagged-record design. Follow-up noted: corpus provenance:/trust: extras are legal unknown keys but do not exercise the spec's actual sources/generated/verified/status/stale_after families — swap when the corpus next regenerates for content reasons.
  • Q2 — Keep both on scalar (non-list) fields: currently only meaningful for list-valued fields; scalars fall back to requiring mine/theirs. Product call needed for the UI copy.

Wave 1 notes

  • F1 — fixture schema indentation bug (fixed). corpus-gen originally wrote the three _schema.yaml files with Rust \n\ string continuations, which strip leading whitespace — the committed fixtures had flat, duplicate-key YAML. The generator now uses raw strings and the three fixture files were regenerated with proper indentation. outcrop-schema keeps its line-based repair_flat pass (it makes external hand-mangled schemas parse too), but the canonical path no longer needs it.
  • outcrop-schema landed: 57 tests, red-first; corpus integration asserts exactly the 11 x_violation records diagnose with the right rule/field/severity. API additions: LinkResolver/PathSetResolver, FolderSchema::{policy,validate_with,default}, SchemaSet::{load,get,folders,validate_record}, SchemaError::Io.
  • outcrop-store landed: 57 tests, red-first; proptest edit-one-field property (128 cases/property) asserting byte-identical remainders; atomic write path testable between stage/commit; index flags unparsable files instead of failing (BRIEF #2/#5); watcher on notify v8, debounced, drop-to-stop. API additions: Store::{write_raw,upsert,delete,read_raw,exists,resolve,list_record_paths}, store::atomic, Document edit surface, Index/Watcher types, StoreError::Watch.
  • Corpus determinism re-verified after the F1 fix: regenerating produces zero diff against the committed fixture.

Wave 2 notes

  • outcrop-query landed: 24 tests. DataFusion 54.1 + tantivy 0.26, one MemTable per folder captured at engine build (snapshot reads, BRIEF #6; refresh() re-captures). Frontmatter type inference is per-column-union with mixed→Utf8 (so corpus projects.priority is Utf8 because of the planted "high"). H2 sections → section_<slug> columns; computed _path/_type/_links_out/_links_in/_modified/_flagged. Table functions search/neighbors/links_to/paths as DataFusion UDTFs; flagged records keep a row with NULL columns. Cross-folder joins via unnest(_links_out).
  • outcrop-txn landed: 47 tests. Workspace/Transaction/Scenario over git2 with no live repo handles (Clone+Send+Sync, staged state durable on disk — dropping a transaction IS the crash case and is recoverable via pending/resume/discard). Write-time policy enforcement before disk (Block → PolicyViolation error, Warn/Ignore → diagnostics). Scenarios as branches materialized as worktrees under .outcrop/scenarios/. Field-level 3-way merge via lossless splices; per-folder merge: last-write-wins honored; post-merge validation feeds needs_attention without blocking the merge. SIGKILL-mid-transaction verified by subprocess helper. User-facing vocabulary throughout.

Wave 3 notes (server + CLI)

  • outcrop-server/outcrop-cli landed: +34 tests (workspace total 219). tonic 0.12/prost 0.13 with vendored protoc; MCP stdio (protocol 2024-11-05), SSE deferred; policy blocks are normal responses, never transport errors.
  • V3 — snapshot-pinned queries deferred. GetRecord at a snapshot works (git blob read); Query with a snapshot pin returns a clean "not supported yet" error. Pinned queries need an engine built over a committed tree, which is a small follow-up.
  • V4 — CLI conflict resolution is single-process. Merge proposals live in server memory, so the CLI resolves in one invocation: merge <from> --keep 'path[:field]=mine|theirs|both'; resolve is an alias. gRPC/MCP clients use the two-step propose/resolve with proposal ids as designed.
  • ScenarioInfo.created_rfc3339 is empty in ListScenarios (outcrop-txn stores no creation time) — cosmetic, open question for the proto's next revision.

MCP end-to-end verification (M3 gate, deviation V2)

Driven from the orchestrating Claude Code session against the real outcrop mcp binary on a corpus copy, newline-delimited JSON-RPC over stdio. Verified in one scripted session: initialize → tools/list (11 tools) → query (found both planted cancelled projects) → create_scenario(reorg-draft) → upsert_record in the scenario (status→paused, not blocked) → scenario isolation (draft reads paused, main reads active) → diff_scenario (one modified record, field-level status change) → propose_merge (completed, main now paused) → validate('projects') returning exactly the 5 planted violations with correct rule/field/severity. Exit 0, all responses well-formed.

Coverage

  • Library crates (store/schema/txn/query) measured mid-run at 86.15% lines — above the 80% gate. Final measurement under the CI filter (server included, cli/corpus-gen excluded): 84.54% lines — gate is 80%.

Client build-out

  • Proto: dropped swift_prefix option — wire-format no-op; SwiftProtobuf now derives the Outcrop_V1_ prefix the Swift client references. Rust/Kotlin/C# codegen unaffected.
  • .github/workflows/clients.yml: macOS job (brew protoc plugins → generate → swift build), Android job (ubuntu runner's preinstalled SDK, gradlew :app:testDebugUnitTest), dotnet job. Windows lib verified locally too (dotnet 8 in-container): builds clean with Grpc.Tools codegen.
  • Android module reworked: proto srcDir moved inside the android block (kts proto extension import), full (non-lite) protobuf for the stub, JVM toolchain pinned to 17, wrapper committed (Gradle 8.9).
  • scripts/demo-canvas.sh + docs/DISPATCH.md: one-command canvas run staged as a Dispatch target from the mobile app; CI compile-verifies ahead of dispatch.
  • Clients CI round 2 → green across all three jobs. Fixes: brew's grpc-swift formula ships the v2 codegen plugin, so both Swift protoc plugins are now built from the package's pinned SPM deps (workflow, demo script, README all updated); Android modules get no default protobuf builtins, so java was added alongside kotlin. The SwiftUI canvas sources compiled clean on macos-latest on first real attempt.

Post-MVP: V3 + C1

  • V3 closed — snapshot-pinned queries: Query with a snapshot name materializes the pinned tree once under .outcrop/cache/snapshots/<id> (immutable; never invalidated) and serves SQL/search/graph from an engine over it. CLI query --snapshot, MCP query.snapshot arg. +5 tests.
  • C1 shipped — canvas persists as canvases/default.md (type: canvas, name, layout_version: 1; body # <name> + ## Layout with one ``json fence). No engine changes needed; validated bycrates/outcrop-server/tests/canvas_record.rs(2 tests, incl. a lossless name-only upsert asserting exactly one changed line). Locallayout.json` demoted to offline fallback.
  • Card layout JSON is flat x/y (client-neutral) for both the record and the local file; pre-C1 CGPoint-shaped local files are ignored on load, not migrated — the server record supersedes them.
  • Canvas record always lives on the main scenario; the scenario picker re-queries cards, it does not fork the layout document. Known cost: every save commits a History entry (debounce deferred).

Ecosystem strategy

  • Added docs/STRATEGY-OKF-ECOSYSTEM.md: OKF ecosystem survey (7 weeks post-announce) + directory strategy. Call: no hosted marketplace; ride GitHub as registry and ship outcrop-index — a curated index-of-bundles that is itself an OKF bundle — plus outcrop install/publish and trust-queryability (_trust_tier/_stale, outcrop verify) as the differentiator (no surveyed OKF tool validates the v0.2 trust fields). Claim the near-empty okf-bundle GitHub topic. Revisit triggers: an official Google registry (integrate day one) or a community directory at ~1k bundles with an API (consume, don't compete). Unverified (proxy-blocked, marked in doc): BundleDex/okfbundle.com ownership and curation.

Authoring (A1)

  • docs/AUTHORING.md added (authoring/curation design: lossless-transactional-validated principles, schema-driven forms v2+, note card, external-editor co-authoring, curation loops, A1-A4 sequencing). Record card v1 shipped in clients/apple: view/edit frontmatter + body on-canvas through the normal upsert path; policy blocks render as a normal explained outcome, edits kept. OutcropClientProtocol.upsertRecord extended (optional body, message, UpsertResult with blocked+diagnostics). Swift not compile-verified in-container — macOS CI job is the gate. Identified follow-up: pending/adopt/discard/history/restore exist in the service but not the frozen proto; additive RPCs are the only server work A3/A4 needs.

IDE support

  • Added outcrop-lsp (tower-lsp 0.20) + outcrop lsp CLI subcommand + editors/ clients (VS Code extension — compiled in-container; Neovim/JetBrains-LSP4IJ/Zed/Helix configs). One LSP server, N thin clients. 34 new tests against fixtures/corpus (workspace 260); diagnostics map policy block→Error, warn/ignore→Warning on the offending key's line; completion for schema keys/enums/wikilinks; hover; go-to-definition; document links. Works on plain bundles (no git init needed); analyzes the working tree only, matching snapshot-read honesty.
  • Deviation: outcrop-schema keeps its parsed field table pub(crate), so outcrop-lsp re-reads _schema.yaml for presentation metadata (completion/hover) only — validation still goes through SchemaSet. Open question: expose public field accessors in outcrop-schema and delete outcrop-lsp/src/schema_info.rs.

Canvas UI refinement

  • clients/apple split into OutcropCanvasKit (library: model, layout codec, client protocol + mock) + executable (views, generated gRPC, live client) + OutcropCanvasTests (37 tests, swift test in CI). Persisted layout schema unchanged (C1 flat x/y); unknown card kinds are preserved verbatim through load/save — never dropped, never a whole-layout decode failure.
  • UX: debounced saves (~1/s) with toolbar save-state ("Saving…"/"Saved just now"/red error, policy blocks in user vocabulary); macOS menu bar (Canvas menu ⌘1/2/3/⌘S, View: Explorer ⌘E, zoom ⌘+/−/0); collapsible explorer sidebar with FTS search (search() SQL, debounced) and lazy folder tree; right-side card palette strip; drag jump-to-cursor fixed (named coordinate space + translation), live pinch zoom clamped 0.25–3, transient z-order (drag/tap brings to front, never persisted), new cards placed at the visible center under pan/zoom.
  • Explorer folder discovery issues SHOW TABLES; outcrop-query's SessionContext has no information_schema, so live runs degrade to search-only sidebar by design. Open question: enable with_information_schema(true) in outcrop-query to light up the folder tree.

Canvas fixes: card removal, save serialization, app presence

  • Card removal shipped — CanvasModel.removeCard(_:) (drops the card and its transient z-order entry, then saves); UI is a hover close button on each card plus right-click → "Remove from Canvas". 4 new tests incl. persistence of the removal through the record round-trip (package now 42).
  • Save "locking" fixed, two causes. (1) Overlapping saves interleaved: ⌘S cancelled the pending debounce task but not an upsert already in flight, so two writes to canvases/default.md could race (proven empirically: new testOverlappingSavesAreSerialized sees maxConcurrentUpserts == 2 on the old code). Saves now chain behind the previous one — at most one write in flight. (2) gRPC calls had no deadline, so with the server down they queued behind connection retries for tens of seconds and the toolbar sat in "Saving…"; LiveStrataClient now sets a 10s default timeLimit.
  • Menu bar / ⌘-tab presence fixed — a bare swift run executable has no bundle, so AppKit treated it as a background tool (no menu bar, no Dock icon, no ⌘-tab, windows not reliably key). An NSApplicationDelegateAdaptor now sets .regular activation policy and activates on launch; verified via lsappinfo reporting ApplicationType="Foreground".
  • Open question: makeClient()'s mock fallback never triggers — GRPCChannelPool construction is lazy and doesn't fail when the server is down, so the app always uses the live client and cards surface connection errors instead of mock data. Either probe the port before choosing, or drop the "mock when server down" claim from the comment.

Dogfood bundle

  • Added bundle/ — Outcrop's own architecture as an OKF v0.2 bundle (12 components, 6 interfaces, 9 decisions; per-folder _schema.yaml at policy warn). Validates with zero violations; queryable via SQL/search/links_to. First corpus exercising the real v0.2 generated/sources/status families (closes the Q1 follow-up). Every build uploads it as the outcrop-okf-bundle artifact and it renders as "The System" in the docs site.
  • Actor convention: records use process:outcrop-overnight-build per SPEC §7 rather than a free-form producer string.
  • Facts pinned to code at authoring time: per-crate test counts from cargo test -- --list (workspace 260), dep versions from Cargo.lock — regenerate/update bundle/ when these drift, or teach the overnight build to refresh it.

Docs deployment: outcrop.md on Cloudflare Pages

  • docs.yml deploy target switched from GitHub Pages to Cloudflare Pages (project outcrop-docs, custom domain outcrop.md) via cloudflare/wrangler-action@v3 deploying the strata-docs-site artifact. Default-branch pushes deploy production; every other branch push deploys a preview URL (in the job log / PR checks).
  • Deviations from the deployment brief, all deliberate: (1) the brief assumed main — this repo's default branch is claude/strata-repo-setup-o5awin, so the workflow maps default branch → Pages branch main (the project's production branch) via DEPLOY_BRANCH, decoupling Cloudflare config from any future branch rename; (2) the brief's bare mkdocs build --strict step is replaced by the existing scripts/build-site.sh, which now passes --strict itself (verified locally: builds clean, warnings already tuned to info in mkdocs.yml); (3) the Pages project is auto-created in CI (pages project create, continue-on-error as idempotence) so the brief's dashboard Direct Upload step is optional; (4) deployments: write omitted — wrangler-action v3 does not create GitHub deployments; (5) deploy steps skip (not fail) when the Cloudflare secrets are absent, so forks/fresh clones stay green.
  • Remaining manual steps (dashboard/CLI, need account credentials): gh secret set CLOUDFLARE_API_TOKEN (Pages—Edit template token) and gh secret set CLOUDFLARE_ACCOUNT_ID; after first prod deploy add custom domains outcrop.md (+ optionally www) to the Pages project; outcropmd.com zone → Redirect Rule 301 concat("https://outcrop.md", http.request.uri.path) with two proxied placeholder DNS records (A @ 192.0.2.1, CNAME www outcropmd.com); enable DNSSEC on outcrop.md and paste the DS record at nic.md.

Consolidation checkpoint (PR #2)

  • PR #2 merged to main: OKF conformance + spec watch, dogfood bundle, docs pipeline (per-build outcrop-okf-bundle + outcrop-docs-site artifacts), canvas UI refinement + Record card, outcrop-lsp + editor clients, ecosystem strategy, architecture diagram, Claude Code workflow. Working branch restarted from main.
  • Repo settings findings: the default branch is still claude/strata-repo-setup-o5awin (first branch ever pushed), so the Pages deploy gate fires there, and Pages itself is not yet enabled — docs.yml now attempts enablement via configure-pages; if the token can't, flip Settings → Pages → Source: GitHub Actions once. Recommended: switch the default branch to main.
  • Bundle gained three thread-era decision records: canvas-as-record (C1 invariant), one-lsp-many-editors, ride-github-as-registry (status draft, revisit triggers recorded). DISPATCH.md gained a field-tested troubleshooting section (port 7411 zombies, divergent-branch pulls, stale app instances). README links the architecture diagram.
  • Still open: Q2 (Keep-both on scalars), information_schema for the canvas sidebar folder tree, ScenarioInfo.created_rfc3339 empty in ListScenarios.

Canvas UI automation

  • Interaction coverage for the canvas now has three layers, all in CI: (1) OutcropCanvasKit gesture-math tests (CardDragSession start+translation semantics — regression guard for the jump-to-cursor bug; CanvasTransform pins live-preview == committed pan/zoom so release can't jump; GraphRadialLayout), (2) OutcropCanvasViewTests hosting the real CanvasView in an offscreen window and driving it with synthetic mouse events, (3) an XCUITest bundle (project.yml → xcodegen → OutcropCanvasHarness.xcodeproj, gitignored) driving the real app end-to-end with the mock client forced via launch env.
  • Field note — synthetic events and SwiftUI on macOS: window.sendEvent and even NSApp.sendEvent are invisible to SwiftUI's gesture system. Events must go through the application's event queue: NSApp.postEvent then drain via nextEvent(matching:)/sendEvent (see CanvasHost.send). Also: an async @MainActor XCTest body occupies the main queue, so a run-loop pump inside it can never drain main-actor tasks (the debounced save) — interaction tests must be synchronous test methods.
  • Bug found & fixed by the harness: every card's gesture hit area was 420×560 (the flexible frame's maxHeight) regardless of visible size, so an invisible region below each card swallowed background pans. Fixed with .fixedSize(vertical:) so layout/hit bounds equal content bounds; regression test testDragBelowTheVisibleCardPansInsteadOfDraggingTheCard.
  • Field note — lazy AX exposure: cards added after first render (ForEach children with .accessibilityElement(children: .contain)) render fine but their groups can stay invisible to the AX tree for 35s+ (sometimes until another card is added). Real accessibility concern (VoiceOver would miss new cards) and the reason XCUITests anchor on the cards' text fields (AppKit-backed, exposed in <0.5s) instead of card-* group identifiers. Worth an upstream look / revisit on newer macOS.
  • clients.yml gained a swift-macos-ui job (xcodegen + xcodebuild test). The swift test step now also runs the hosted-window view tests (needs the runner's window server — fine on GitHub macOS runners; unverified until the first CI run on this branch).

Ethos pass

  • Added docs/ETHOS.md — intent distilled into 12 principles, each mapped to the mechanism that enforces it (property tests, planted-violation suites, API rules, CI gates, RUNLOG conventions). Written to outrank ambiguous future decisions.
  • Added bundle/ethos/ (7 principle records, schema'd, cross-linked into decisions/components/interfaces) so the values are queryable like everything else; bundle revalidates clean (now 4 folders, 34 records). CLAUDE.md now sends every future session to ETHOS.md first and makes keeping bundle/ true part of changing the architecture; README and the docs-site nav link it.

Engine-on-demand-instance (E0)

  • Decision landed (docs/ENGINE.md + bundle decision record, bundle at 35 records/13 decisions, revalidated clean): the engine becomes a shared on-demand instance per bundle. Three-layer coordination (flock authority / engine.json hints / Hello truth test), UDS transport with both planes on one socket, Attach-stream leases, 45s injectable linger, solo-mode degradation on network filesystems, version-mismatch-never-spawns. Adoption: canvas spawn-or-adopt, MCP adopt-or-spawn, CLI adopt-if-present with --no-engine, LSP standalone (non-goal). Control plane in new proto/engine.proto; outcrop.proto stays frozen. Prerequisite accepted: merge proposals become durable under .outcrop/proposals/ (machinery, not records — ethos §8 answered in ENGINE.md). Terminology note: "sidecar" rejected (wrong sharing + lifecycle semantics); pattern named after LocalDB automatic instances / Gradle daemon lineage.
  • Implementation phases E1-E5 planned (engine core; durable proposals; CLI Backend abstraction; canvas Swift launcher; MCP adoption) — each PR-sized, red-first, with the SIGKILL/race/stale crash suite in the repo tradition.

Engine implementation (E1 + E2)

  • E2 landed — merge proposals durable under .outcrop/proposals/<id>.json (atomic temp+fsync+rename); survive engine death, resolvable cross-instance (service + gRPC tests). StoredProposal persists open proposals only, so MergeProposal's public serde shape is unchanged (golden test). Corrupt file = warned clean miss; unresolvable stored commit = ProposalStale "propose again".
  • E1 landed — engine core: proto/engine.proto (outcrop.engine.v1), outcrop-server::engine (instance/lease/control/rendezvous/governor), serve_engine_uds (both planes, one socket, ActivityLayer ticking the linger clock), CLI outcrop engine run|start|stop|status. 22 tests red-first: 16 in-process incl. paused-clock linger trio, 6 cross-process incl. SIGKILL-mid-lease and a 4-way spawn race — death always verified by acquiring the flock.
  • E1 deviations: relocated-socket name uses an FNV-1a 64 root hash (not sha256) to avoid a new dep — hints remain the authoritative socket pointer; hyper-util (already in-tree via tonic) promoted to a direct dep for tonic 0.12's UDS connector (TokioIo); fs4 returns Ok(false) on contention, mapped to AlreadyRunning; SIGTERM fallback shells out to kill (no libc dep). Idle-cap semantics: engine exits at last_activity+idle_cap even with leases held; linger applies once the lease table is empty.
  • Workspace: 290 tests green, fmt + clippy clean.

Engine implementation (E3)

  • E3 landed — CLI adopts a shared engine behind outcrop_server::backend::Backend (enum Local/Remote, sync surface, owned current-thread runtime). Wire ops (the 11 outcrop.v1 RPCs) route to an adopted engine; maintenance verbs without wire representation (history/restore/pending/discard/adopt/listings) stay local beside it — hybrid routing documented in backend.rs. CLI policy is adopt-if-present only (never spawns, never leases); --no-engine / non-empty OUTCROP_NO_ENGINE force solo. CLI main is now sync; serve/mcp/lsp/engine arms build their runtime explicitly.
  • E3 deviation: the frozen proto.Conflict carries no kind, so remote conflict kinds are inferred (field / delete-vs-modify / body); resolution semantics unaffected. Revisit only if the contract ever unfreezes.
  • Parity proven by backend_parity_remote_vs_local_all_ops — every wire op serde-identical Local vs Remote over a real UDS engine (volatile timestamps/proposal ids normalized). 297 workspace tests green.

Engine implementation (E4 + E5)

  • E5 landed — MCP opens Backend with AdoptOrSpawn, holds an Attach lease (kind mcp) for the session's life; --no-engine/OUTCROP_NO_ENGINE = solo, fully functional; solo degradation warns on stderr. Design: the MCP stdio loop is now synchronous (std::io) — lossless for line-oriented JSON-RPC and honors Backend's never-inside-a-runtime contract. Tests: MCP-layer Local/Remote parity, cross-process spawn+lease, SIGKILL drain + 200ms linger-out verified by the flock, and two MCP sessions cross-resolving one durable proposal through a single engine pid.
  • E4 landed — canvas spawns-or-adopts the shared engine over UDS: EngineLauncher (Swift re-implementation of the ENGINE.md dance, verbatim steps), mock-first startup with a visible engine status, Attach lease held for app lifetime, OUTCROP_CANVAS_FORCE_MOCK hermetic path preserved for the XCUITest harness. Port-7411 default removed everywhere (demo script, doctor, docs); TCP stays opt-in via LiveOutcropClient(host:port:). Pure discovery logic (hints parsing, socket paths, binary lookup) lives in the Kit with unit tests; full-dance integration tests are mac-gated and skip unless OUTCROP_BIN is set.
  • E4 deviations: Swift root comparison tolerates macOS's /private prefix (Foundation strips it, Rust's canonicalize keeps it — literal compare would break adoption under $TMPDIR); demo-canvas.sh now runs outcrop init on the scratch bundle since engine run requires an initialized workspace.
  • The engine-on-demand plan (E0-E5) is fully implemented. Remaining verification: macOS CI compile of the E4 Swift; then the on-Mac demo — open the canvas with no server step, outcrop engine status shows canvas + any MCP session sharing one pid.
  • Engine plan verified in CI — Clients workflow green on all five jobs after two shallow fixes: kotlinx-coroutines-core became a required Android compile dep the moment engine.proto introduced the project's first streaming RPC (Attach → Flow in generated Kotlin), and the pre-existing debounce test was deflaked (poll-for-collapse instead of a fixed 90ms sleep). The blind-written EngineLauncher Swift compiled first try; the XCUITest harness passed untouched via the hermetic mock path.

Rename: Strata → Outcrop (2026-07-30)

  • Broadbase rename landed — project renamed Strata → Outcrop, docs site hosted at https://outcrop.md (mkdocs site_url + CNAME emitted by build-site.sh). Everything renamed in one change, per the frozen-contract rule (contract change = update every consumer in the same change): crates (outcrop-*), proto files (proto/outcrop.proto, packages outcrop.v1 / outcrop.engine.v1), runtime dir (.outcrop/), env vars (OUTCROP_*), all four clients, editors, bundle records, docs, CI.
  • Namespace decision: reverse-DNS namespaces move dev.strata → md.outcrop (we host on outcrop.md; dev.outcrop would name a domain we don't own). Affects proto java_package, Android package/namespace, Apple bundleIdPrefix.
  • Kept as-is: literal git branch names (claude/strata-repo-setup-o5awin) — they name real branches; renaming the references would break the commands that mention them.
  • Rode along: tracked .NET bin//obj/ build outputs under clients/windows/ removed from version control (they carried stale Strata codegen) and gitignored.
  • Preexisting macOS-only test failure found (not caused by the rename, verified at 9c82507): engine_tests::acquire_creates_lock_socket_hints compares a canonicalized socket path (/private/var/…) against the raw tempdir path (/var/…); Ubuntu CI doesn't hit the symlink. Fix tracked separately.
  • Follow-ups outside the repo: rename the GitHub repo to jeffreyclegg/outcrop (old URLs redirect), set the Pages custom domain to outcrop.md, and point outcrop.md DNS at GitHub Pages.
  • CI cost: the two macOS jobs moved to .github/workflows/clients-apple.yml, running only on PRs touching clients/apple/** or proto/** (plus workflow_dispatch). macOS minutes bill at 10x; running the full Apple harness on every branch push exhausted the plan's 3,000 min/month. Trade-off: pushes without a PR no longer get Apple CI — the PR is the gate.
  • Post-rename cleanup (2026-07-31): repo renamed on GitHub to jeffreyclegg/outcrop; main is the default branch (fast-forwarded to the rename merge before the flip). Operational references to the retired first branch (claude/strata-repo-setup-o5awin) in DISPATCH.md and doctor.sh now say main; RUNLOG mentions above stay as written — they describe the state at the time. The retired branch and the merged rename/docs-deployment branches are deleted on the remote (all fully contained in main).

Cross-platform client MVP prep

  • Survey + docs pass ahead of the client MVP (branch claude/canvas-cross-platform-mvp). docs/CANVAS.md gained a "Cross-platform clients" section; the porting contract is recorded in the bundle as interfaces/canvas-client-surface (draft): five methods (query/neighbors/listScenarios/getRecord/upsertRecord) + C1 convention + two obligations (unknown-kind passthrough, port-the-mock).
  • Parity at time of writing: Apple 5/5 (reference), Windows 2/5, Android 1/5 (and Android's Query row parsing is naive comma-splitting — must become real JSON parsing before canvas use). Neither stub has any UI scaffolding: Android is com.android.library with no Compose deps; Windows is plain net8.0 classlib, no WinUI/Windows App SDK, and its CI builds on ubuntu — a -windows TFM will need a windows runner.
  • iOS blockers verified small: platforms declaration, Color(nsColor:) in CanvasView, .searchable(placement: .sidebar) in SidebarView, app-shell/project.yml split; OutcropCanvasKit is already portable (MagnifyGesture ⇒ iOS 17 floor). Hosted-window tests stay macOS-only.
  • Three cross-cutting gaps to close once, not per platform: (1) no remote-host configuration — since E4 the Mac canvas adopts a local engine over a Unix socket, so off-machine clients (phone, tablet) have nothing to adopt and need a server-address setting against an explicit outcrop serve; (2) no TLS story off loopback (Android also refuses cleartext without a network-security-config exception) — needs a decision before any off-device client; (3) folder discovery: expose the service's existing list_folders/list_record_paths as additive RPCs with the A3/A4 batch instead of per-platform SHOW TABLES workarounds; also fills the sidebar on live servers.
  • Field note (superseded by E4): serve refuses an uninitialized workspace and demo-canvas.sh depended on the old behavior. The script inits its scratch copy — now via the engine binary it exports as OUTCROP_BIN, since the canvas starts the engine itself and the manual serve step is gone.

macOS test fix: canonical paths in engine hint assertions

  • Symptom: acquire_creates_lock_socket_hints fails on macOS only — assertion left == right failed: left: "/private/var/.../engine.sock", right: "/var/.../engine.sock". Preexisting at 9c82507; Ubuntu CI never hits it.
  • Cause: EngineInstance::acquire canonicalizes the bundle root (by design — adoption requires canonical bundle_root equality, tests 7/8), so hint contents carry /private/var/...; the test derived expected paths from the un-canonicalized tempdir, and macOS's $TMPDIR sits behind the /var -> /private/var symlink. Same family as the E4 Swift /private prefix deviation.
  • Fix: the test canonicalizes tmp.path() before deriving expected paths. Audited the other 15 engine tests: only this one compared an engine-written path value against un-canonicalized expectations — existence checks and file writes through the symlink resolve to the same inodes and are symlink-safe; long_root_relocates_socket compares engine-produced values against each other and std::env::temp_dir() un-canonicalized on both sides, consistent by construction.
  • Rule of thumb: any assertion on a path value the engine wrote (hints, Hello bundle_root) must compare against a canonicalized expectation; assertions that merely dereference a path may use the symlinked form.

Open-PR reconciliation after the rename (2026-07-31)

Four agent-authored PRs were open against claude/strata-repo-setup-o5awin, all written before the Strata → Outcrop rename landed. Resolved one by one:

  • #7 (macOS canonical-path test fix) — no code conflict; only the RUNLOG append collided. Merged base in, kept both entries. Engine tests green (16/16).
  • #10 (cross-platform canvas prep) — kept the porting-contract docs and the new interfaces/canvas-client-surface record, renamed its Swift identifiers, and dropped its demo-canvas.sh init line (E4 already inits through the engine binary it exports). One claim was stale and rewritten rather than merged as-is: "the Swift app hardcodes 127.0.0.1:7411" stopped being true at E4 — the gap is remote-host configuration, since a phone has no local engine to adopt.
  • #9 (card removal, serialized saves, foreground presence) — kept its hover-close/context-menu affordances alongside main's accessibility identifiers, its AppActivator alongside main's mock-first engine startup, and rebased its drag state onto main's CardDragSession. Its 10s call deadline moved to a shared callOptions used by both client initializers: the adopted-engine channel can stall the toolbar in "Saving…" exactly like the TCP one it was written for.
  • #8 (launch-time TCP probe of 127.0.0.1:7411) — closed as superseded, not rebased: makeClient() and the port are gone. Its insight survives in the design — GRPCChannelPool connects lazily, so constructing a client proves nothing and reachability must be settled by a real round trip. Hello is that round trip, and a stricter one (right version, right bundle root).

  • Field fix (Jeff's Mac, canvas reported no binary): EngineDiscovery.locateBinary now falls back to the checkout's own target/{release,debug}/outcrop after OUTCROP_BIN and PATH — swift run OutcropCanvas from clients/apple has neither, and dropping to solo mode while a good engine sits two directories up is the wrong answer. An installed binary still wins over a stale checkout build.

  • CI is dark, and not because of the code: every workflow run since ~03:23 fails in ~3s on every branch including main, with runner_id: 0 and no logs — jobs are never assigned a runner. That is the account's Actions minutes, spent as the rename entry above predicted. Nothing merges green until the quota resets or the spending limit is raised; the workspace suite was run locally instead (green) and Swift changes here are unverified by compiler until then.

CI runners: cloud with a local fallback (2026-07-31)

  • Why: GitHub-hosted minutes ran out (every job, every branch, runner_id: 0, ~3s, no logs). Self-hosted runners are not billed against Actions minutes, so a Mac runner is the way back to green — and the way to tell "minutes exhausted" (self-hosted still runs) from "Actions disabled for the account" (nothing runs).
  • Mechanism: runs-on reads vars.RUNNER_LINUX / vars.RUNNER_MACOS, defaulting to GitHub-hosted when unset. Deliberately a variable and not a commit: the trigger for using it is CI being unable to run at all, and a workflow edit would itself land unverified. Value must be valid JSON.
  • claude.yml excluded on purpose. Every other workflow runs a fixed reviewed script; that one runs an agent that derives its commands from comment text. That belongs on a disposable VM, not a personal machine with real credentials.
  • Ruleset (docs/CI-RUNNERS.md, bundle decision ci-runner-fallback): cost multiplier, fidelity, availability, trust. Only the Apple jobs are better locally on both cost and fidelity (10x billing, faster warm) and are the sole candidates to move permanently; everything else is temporary fallback. Anything whose absence is invisible stays in the cloud — a sleeping runner queues jobs rather than failing them, which reads as a hang, not an error.
  • Known gap on a Mac runner: GitHub's Ubuntu image ships an Android SDK; a Mac does not, so the Android job needs ANDROID_HOME installed before it can pass. scripts/runner-doctor.sh reports readiness per job family rather than pass/fail overall, since a Mac that runs CI + Docs + Apple but not Android is still useful.
  • Also: first time the Rust suite will run on macOS in CI. New failures there may be real macOS bugs (cf. the /private/var canonicalization fix in PR #7), not flakes.

Architecture artifact: portable beyond the browser

  • The page was already one self-contained file (~16KB, zero external requests), but every role explanation lived in a hover tooltip — so a PDF, a screenshot, a slide paste, or any touch device lost the entire explanatory layer. "Portable as a file" and "portable as a document" are not the same property.
  • Fixed three ways, no script added: a pure-CSS "Show all descriptions" toggle (sibling checkbox) that puts every tooltip in the flow; a @media print block that forces the expanded layout and light theme, so Save-as-PDF is the whole document rather than labelled boxes; and :focus alongside :focus-visible plus an always-expanded layout under 780px, because a phone has no hover and a floating 340px card covers its neighbours.
  • Rode along, keeping the artifact true: the diagram gained the Rendezvous band (the engine-on-demand instance and the adoption matrix were missing entirely from the picture), and the footer counts were corrected from 260/37 to 301 Rust / 90 Swift.

Decision: every client hosts its own engine

  • Product call (Jeff, 2026-07-31): no remote client-server interaction model. Recorded as bundle/decisions/clients-host-their-own-engine (stable); docs/CANVAS.md "Cross-platform clients" rewritten around it; interfaces/canvas-client-surface updated. Desktop = shared on-demand instance (E0); iOS/Android = engine linked in-process (iOS cannot spawn). Cross-device consistency = git sync against a remote — the remote's TLS+auth replace the whole off-device transport/pairing project, so the TLS-vs-LAN-plaintext question is closed as moot for clients (revisit only if a hosted multi-user Outcrop appears).
  • Verified enablers behind the call: outcrop-txn/store drive git via git2 (libgit2, zero subprocess use), and DataFusion+tantivy are pure Rust — the stack cross-compiles to iOS/Android. Unverified until a first device build: stripped in-process engine binary size.
  • Coordination item for the E0 workstream: the engine needs a library facade — a lib target exposing the same proto surface in-process — alongside the spawnable binary; spawn/flock/linger machinery is desktop-only and must not be assumed in the engine API. Crystallized into docs/ENGINE.md (per Jeff): "The engine as a library" section, the transport bullet's not-a-remote-surface note, and a library-facade entry in known limitations with the first-device-build trigger.
  • New client-side gap this opens: a sync surface ("Sync now", merge resolution in user vocabulary) must reach every platform including touch; folder-discovery RPCs remain the other open gap.

Decision: outcrop init confirms before importing an existing directory (issue #16)

  • Silent behavior was surprising and irreversible-looking from the CLI's point of view: pointing init at any existing non-empty, non-workspace directory immediately created a git repo and committed every file as "Initial import" with no explanation or way to back out.
  • Fix is CLI-layer only, per BRIEF's isolation-honesty spirit for library crates: outcrop-txn gained one small, pure predicate — init_will_import_existing_files(root) — that's true only when Workspace::init would both create a brand-new repo (no repo open-able yet) and the directory already has entries. It stays false for empty/not-yet-created dirs and for the idempotent reinit case, so outcrop-txn's own behavior and its existing tests are unchanged. outcrop-cli calls this before OutcropService::init, and only then does interactive I/O (never inside outcrop-txn, which does no user-facing I/O at all).
  • Prompt text is pure product vocabulary ("workspace", "transaction log", "initial import") — never git terms — per bundle/decisions/user-facing-vocabulary.md.
  • Never blocks scripts/CI (BRIEF: "outcrop-cli is a thin cover... for scripting and CI"): the prompt is skipped automatically whenever stdin isn't a terminal (IsTerminal::is_terminal), and init also grew an explicit -y/--yes flag to bypass it regardless of TTY.
  • Testing tradeoff, recorded because it's non-obvious: the confirm/decline interactive paths are unit-tested against confirm_init_with(root, &mut reader, &mut writer) — a small pure function with dependency-injected I/O — rather than spawned end-to-end through a pty. Early attempts to drive the real terminal prompt via rexpect and then portable-pty both forked the whole multi-threaded test binary, which is unsafe and was observed to flake/hang under cargo's parallel test execution (concurrent forks racing on locks held by unrelated test threads). The four always-skip-the-prompt paths (non-tty, --yes, empty dir, idempotent reinit) are still fully exercised end-to-end in crates/outcrop-cli/tests/cli_tests.rs via the real outcrop binary, since none of those need a real tty to reach.
  • Follow-up (same issue #16, second round): audited real (non-test) automation callers of init and found three — the docs CI workflow, scripts/demo-canvas.sh, and DEMO.md's walkthrough — all of which point at a non-empty, not-yet-git directory and so would hit the new prompt. TTY-detection alone already keeps them working (none run at a real terminal), but relying solely on that felt fragile for known automation, so all three now pass --yes explicitly: .github/workflows/docs.yml (bundle job), scripts/demo-canvas.sh, and DEMO.md's documented command (with a note that a human running it interactively, without --yes, will still see the prompt).
  • Also added init --dry-run: prints what init would do (import existing files into a new workspace, create an empty workspace, or "already initialized, nothing to do") without touching anything — no repo, no commit, no gitignore edit — and exits 0. It short-circuits ahead of all prompt/confirm logic, since a dry run never needs confirmation. Supports --json like other read-only commands.

Fixture corpus speaks real OKF v0.2 (closes the Q1 follow-up for the fixture)

  • Product call (Jeff, 2026-07-31): keep tools/corpus-gen — regeneration will be wanted — and run the swap Q1 flagged. The fixture's ad-hoc provenance:/trust: extras are replaced with the spec's actual families: generated/sources (with author/last_modified credibility signals) + both legal verified forms on the same 18 people records; usage_count/usage_window on every 5th project; lifecycle status/stale_after on notes (projects' domain status enum collides with OKF's, so lifecycle lives on notes).
  • Regeneration was rng-stream-aligned: the new people block consumes exactly the two draws the old one did, and the projects/notes additions are index-keyed with zero draws — so all names, owners, tags, and bodies are byte-identical across the swap (46 files changed, no renames; people/ada-berners-lee.md and every other test-pinned record untouched). Determinism re-verified: second run produces zero diff.
  • The docs/okf/README.md conformance claim that corpus-gen tests the bundle-root okf_version marker was aspirational (corpus-gen had zero tests); it is now true (tools/corpus-gen/tests/marker.rs).

Workflow resilience: OKF spec watch without PR automation permission

  • The scheduled okf-spec-watch.yml job failed on 2026-09-14 not because GITHUB_TOKEN expired, but because the repository-level Actions setting Allow GitHub Actions to create and approve pull requests was disabled. The workflow already had contents: write and pull-requests: write; GitHub rejected the PR creation step with GitHub Actions is not permitted to create or approve pull requests.
  • Fixed in-repo by making the workflow query /actions/permissions/workflow before calling peter-evans/create-pull-request: if can_approve_pull_request_reviews is false, the job now records a clear warning in the step summary and exits green instead of failing the weekly watch. Also corrected the vendored-spec metadata fetch to use the same token explicitly via curl's bearer-token flag rather than a malformed masked header literal.

Canvas: layout.json shadow copy removed — the record is the only store

  • Product call (Jeff): remove the local layout.json fallback. Its rationale ("offline" when the server might be down) predates the in-client-engine decision; with the engine spawned or adopted locally there is no launch where the app can reach Application Support but not the record store. What remained was a second persistence format (against ETHOS #8), a doubled write on every save, and layoutURL: plumbing through the test targets.
  • Red-first: two failing tests pinned the target behavior (save writes no local file; a stale file never resurrects cards), then the removal made both properties structural — CanvasModel no longer touches the filesystem at all, so the red tests were deleted with the parameter they exercised. The interaction test's persistence assert now reads a fresh model over the same mock client instead of decoding the file.
  • Known trade: mock-only launches (XCUITest, OUTCROP_CANVAS_FORCE_MOCK) hold the canvas in memory only, so layout no longer survives relaunch without an engine — that was a test-harness behavior, not a user scenario, and UITests get their clean-slate-per-run for free now (the OUTCROP_CANVAS_STATE_DIR isolation env var is gone).

App icon: material round + platform exports

  • Design brief supplied out-of-band; the study files it names arrived as a zip (studies/, 82 files) and are now in-tree. Two of the brief's pointers are stale: _proof.svg is the e-round contact sheet (e-s52 … e-s64-50), so it cannot cross-check the locked coords, and Outcrop Icon - Rev B Studies.dc.html was not in the zip. s-outcrop.svg is the latest round and is treated as the source of truth.
  • Geometry lifted verbatim into tools/icon-gen/geometry.json — the generator never re-derives the mark. Measured against the brief's prose and it reconciles: leg width ~26u, tilt 30°, stagger ~42u measured along strata (≈1.6 leg-widths, the brief's "1.5 leg-widths RIGHT"), perpendicular channel ~54u (brief's ">=16u minimum" holds). The stagger only reads as a right-offset in strata space, not in canvas space — worth knowing before anyone re-measures it and thinks it is wrong.
  • The s9* files are a divergent branch: they use a different, simplified mark plus a landscape silhouette, not the locked two-bracket geometry. Left untouched, not built on.
  • Bedding angle is absolute, not relative to the mark. First pass drew bands at 30° to match the tilt; because the legs are themselves at 30° the bands ran along them and read as brushed metal. Slate now beds horizontally (0°), sandstone at 13°.
  • Treatment picked: t1-slate. Sandstone drifts to wood grain and its warm hue fights the flat-ink fences (brief requires fences stay ink in all treatments); two-tone is the safest small but its single hint line is nearly invisible at 512. Switching is one argument: scripts/build-icons.sh sandstone.
  • Apple dark mode — empirical finding. The brief requires ink to invert to white on dark. Icon Composer's image-name-specializations and fill-specializations are accepted by the document parser but are not honoured by the renderer; only opacity-specializations is (confirmed by probing ictool and by reading a shipping .icon — iMazing's — which uses opacity/blend/shadow specializations exclusively). Both ink sets therefore ship stacked and cross-fade per appearance slot. Also note the specialization array needs a base element with no appearance key before the variants; omitting it makes the whole array a no-op.
  • Verification is part of the build: build_icons.py renders five ictool renditions and reports ground-vs-ink luminance, so a regression that kills the inversion shows as a sign flip rather than a silently dark icon.
  • Open issue — 16px does not resolve. Two staggered brackets plus six fence dashes is too much for 16 device pixels; the brief's prescribed cuts (drop bedding, weathering, chamfer) are implemented and are not enough. Fences are additionally thickened at ≤32px (18u → 26/30u) because the locked row is 0.56px at 16px and vanishes — a deviation that applies to the small cuts only. A real fix is a dedicated favicon glyph (single bracket, one fence pair), which is a design call and is not made here.
  • Not delivered: the material section was written as a standalone studies/_material.html rather than prepended to Outcrop Icon - Rev B Studies.dc.html, which does not exist in-tree. Fold it in as the top turn-section when that file resurfaces.

Correction: the Apple path was built against the wrong paradigm

  • The entry above described hand-authored material (gradient + bedding bands + top-edge highlight + weathering) shipping into the .icon layers. That was wrong, and it was wrong because the Icon Composer work was reverse-engineered from ictool and a shipping bundle instead of read from Apple's specs. Jeff called it.
  • WWDC25 Create icons with Icon Composer is explicit: keep source art "flat, opaque, and easy to control later"; do not bake drop shadows, specular highlights, gradient overlays, blur, background colours, or the corner mask. Those are dynamic properties applied per appearance, and baking them makes the icon muddy. Canvas 1024 (watchOS 1088), up to 4 groups for Z-depth, layer files numbered in Z-order.
  • Apple assets rebuilt: three flat groups (fences / lower bracket / upper bracket) at increasing shadow + translucency, background moved to a document-level linear-gradient fill. Depth now comes from the compositor rather than from paint. Clear and Tinted only became correct after this change — baked material cannot produce them at all.
  • Consequence worth recording: Apple and the other platforms need different source art. Icon Composer supplies material; Android, Windows and web have no compositor, so they keep the hand-authored treatment. build_apple() no longer takes a treatment.
  • Verified inert, not merely unused: fill-specializations and image-name-specializations give byte-identical output with and without them, at both document and layer level (max channel difference 0 on a Dark render diff). Only opacity-specializations moves the renderer. Established against ictool only — the GUI and the on-device compositor are unverified.
  • Verification widened to nine renditions (six appearances + macOS Dark + watchOS).
  • Unchanged by any of this: the mark is not nameable at a glance, and 16px does not resolve. Both are Study 8 geometry, not treatment.

Rev C: strata treatment

  • Direction from Jeff: ramp up bracket detail, make the sedimentary layers distinct colours, zoom the mark, let the fence dashes bleed off the edges and get cut, go brown with thin black accents, match the detail level of the reference icons.
  • Implemented as a new strata treatment, now the shipped default on every platform. Eleven beds in the 6-17u range (legs are only ~26u across, so anything thicker and a leg catches a single bed and the mark reads as colour patches, not stratification), weighted toward mid/dark browns with pale marker horizons, 1.6u near-black bedding planes and a 5.5u keyline.
  • Both brackets carry the same stratigraphy, offset by a 14u fault throw. First pass registered the beds in canvas space, so the two brackets sampled different parts of the sequence and read as two different rocks; the upper bracket's bed origin has to be lifted by the 90u inter-bracket separation first.
  • Fences rebuilt as a bleeding dash run: same 91.5u width and 130.2u period as the locked rows, extended two steps outward so the outer dashes are cut by the canvas. Mark scaled 1.28x. Both are deviations from the locked placement, at Jeff's direction.
  • The treatment is Apple-legal: flat opaque fills and hard strokes only, so it survives the Icon Composer rule. Slate/sandstone are not (gradient + painted specular) and are now non-Apple only. Verified across all nine renditions.
  • Small cuts unified into the family: <=32px collapses to one warm mass plus keyline rather than falling back to the old grey two-tone.
  • 16px remains a smudge. Better colour did not fix it and was never going to.

Rev D: gaps not lines, three dashes, bigger brackets

  • Jeff: keep exactly three dashes top and bottom, scale the brackets up, and replace the dark bedding rules with gaps to the background, with each successive layer a different/rotating tan or brown.
  • Bedding separation inverted: the bracket is filled with the ground colour first and beds are inset inside it, so the separation is the background rather than a drawn line. Palette reduced to eight mid-to-dark tans/browns on rotation — the near-bone beds from Rev C had to go, because they matched the ground and swallowed the gaps.
  • Fences back to three dashes per row (Rev C had five). The bleed is kept by widening the dashes so the outer two overhang and get cut. Ratio, not size, controls the read: 176/26 looks like one notched bar; 150/48 reads as ---.
  • Mark scale 1.28 -> 1.42. At 1.55 the brackets start covering the dashes.
  • Still true: 16px does not resolve, and the mark is not nameable at a glance.
  • New, self-inflicted: with uniform bed thickness and uniform gaps the stripes read a little like awning/candy stripe rather than rock. Varying bed thickness more widely (10-18u) and jittering the gaps would fix it; not done yet.

  • Closed: "16px does not resolve" was over-weighted through Rev A-D. It ships in three files total (one favicon.ico frame, two Windows Explorer slots). Apple's floor is a 1024 source, Android's is 48, web's is 180. Noted as cosmetic, not a constraint.

Build hardening before the PR

  • Dropped the hard Pillow dependency. It was carrying exactly one thing the build needs -- assembling favicon.ico -- so that is now ~15 lines of struct packing (write_ico), PNG payloads in an ICO container, which Vista+ reads directly. Pillow stays optional and only adds the study contact sheet and the ink checks. tools/icon-gen/test_ico.py covers the byte layout, payload round-trip and the 256px-encoded-as-0 trap, since nothing else validates that writer.
  • Fixed two bugs in scripts/build-icons.sh found by running it, not reading it: it guarded on the venv directory (so a venv whose pip install failed was never repaired, and every later run failed confusingly), and it joined ICON_VENV with a relative prefix so an absolute override broke.
  • Wrapper default was still slate while the README said strata ships. Now strata.
  • ictool aborts with an IconComposerKit assertion while Icon Composer.app is open. Confirmed environmental rather than a fault in our document: a known-good third-party .icon fails identically under the same conditions. The nine-rendition verification passed repeatedly before the GUI was launched.