Skip to content

Stage 2: deterministic layered DAG layout in ara-core - #5

Merged
EYH0602 merged 7 commits into
mainfrom
stage2-dag-layout
Jul 11, 2026
Merged

Stage 2: deterministic layered DAG layout in ara-core#5
EYH0602 merged 7 commits into
mainfrom
stage2-dag-layout

Conversation

@EYH0602

@EYH0602 EYH0602 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements plans/stage-2-dag-layout.md.

Adds a deterministic layered (Sugiyama) DAG layout to ara-core via dagre-dgl-rs,
producing node positions + bounding rectangle attached to the Manifest. The same
input yields byte-identical JSON on native and wasm32 targets. Edge routing is
deferred to the Stage 3 client.

Plan Steps

  • Step 1–3: Crate spike + LayoutOptions + coordinate canonicalization (bc74add)
  • Step 4–5: Extend Manifest with pos/bounds + parse_and_layout (f362fbc)
  • Step 6: ara layout <dir> --json and ara validate --layout CLI (3fb71e1)
  • Step 7: Tests — layout integration + positioned snapshot (4d2bfe1)
  • Step 7: Freeze geometry wire shape in docs/manifest-schema.md (673c954)
  • Version bump 0.0.3 → 0.0.4 + CHANGELOG (650ed2a)

Crate selection (Step 1 spike result)

dagre-dgl-rs v0.1.1 (MIT, faithful dagre-js port):

  • ✅ wasm-safe: compiles for wasm32-unknown-unknown, no threads/rand/fs
  • ✅ deterministic: IndexMap insertion-order iteration, no internal randomness
  • ✅ native ≡ wasm: byte-identical JSON output verified via wasmtime on wasm32-wasip1

Review Status

Eng review: CLEAR (PLAN) via /plan-eng-review (7 issues, 0 critical gaps, all folded).

Test Plan

  • cargo test --workspace --locked — 77 tests pass
  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean
  • cargo build -p ara-core -p ara-wasm --target wasm32-unknown-unknown --locked — builds
  • cargo run -p ara-cli -- layout crates/ara-core/tests/fixtures/official/minimal-artifact --json — outputs positioned JSON
  • Stage 1 insta snapshots unchanged (proven by stage1_snapshots_unperturbed_by_new_fields test)

EYH0602 added 7 commits July 9, 2026 11:42
Rewrite plans/stage-2-dag-layout.md per the eng review: layout stays in
ara-core (positions + bounds only, edge routing deferred to the client),
add a step-1 crate spike gated on native≡wasm cross-target determinism,
drop the Layout trait (YAGNI), pin f64 + canonicalization, fixed node
sizing in LayoutOptions, and freeze only the geometry wire shape while
keeping the logical model additive. Correct the stale version bump to
0.0.3 → 0.0.4 and enumerate full test coverage incl. the Stage 1 snapshot
backward-compat regression.

Add T-EDGE-ROUTING and T-LAYOUT-SPIKE-FALLBACK to TODOS.md.
Step 1–3 of Stage 2: select dagre-dgl-rs (=0.1.1) as the layout crate
(passes wasm-safe, deterministic, native≡wasm gates), implement the
layout function with LayoutOptions, Point/Rect geometry types, and
f64 coordinate canonicalization (round to 6 decimals, -0.0 → 0.0).
Step 4–5 of Stage 2: add Node.pos (Option<Point>) and Manifest.bounds
(Option<Rect>) with skip_serializing_if so Stage 1 JSON is unperturbed.
Add parse_and_layout/parse_and_layout_dir entry points that run layout
on successful parse, skip on error.
Step 6 of Stage 2: `ara layout <dir> --json` dumps the positioned
Manifest; `ara validate --layout` reports node/edge counts + bounds.
Shared emit_report helper extracted from validate(). CLI tests cover
JSON output, missing dir, parse-error skip, and validate --layout.
Covers: parse_and_layout ok/error paths, in-process determinism,
positioned manifest insta snapshot, Stage 1 regression (no pos/bounds
when layout off), and scale probe on largest corpus tree (<5s budget).
Step 7 of Stage 2: document Point, Rect, Node.pos, Manifest.bounds as
frozen geometry fields. Document logical model as still additively
extensible. Note node-sizing tradeoff and coordinate conventions.
Update CHANGELOG with Stage 2 entry (deterministic layered DAG node
layout, ara layout command, geometry wire shape frozen).
@EYH0602
EYH0602 merged commit 584893c into main Jul 11, 2026
4 checks passed
@EYH0602
EYH0602 deleted the stage2-dag-layout branch July 11, 2026 03:09
EYH0602 added a commit that referenced this pull request Jul 12, 2026
All 6 plans in plans/ were for features that have shipped, so per CLAUDE.md's
plan→docs lifecycle they move to docs/ as design records and out of plans/.

- New design docs: stage-0-ci-tooling, stage-2-dag-layout, real-corpus-no-panic
- Stage 0 (#1), Stage 2 (#5), issue #3 (#4) had no docs/ counterpart → written
- issue #9 layout modes already folded into docs/stage-3-viewer.md → just removed
- ara-runtime-impl-plan + stage-overview were meta-planning artifacts fully
  realized in code + per-stage docs → removed
- Retire completed TODOs T-DOCS (this migration) and T-VIEWER-DIST-PACKAGING
  (resolved by the Stage 4 include_dir! embed), and the obsolete
  T-STAGE4-VERSION-BUMP (Stage 3/4 both shipped)
- Fix dangling plan references in README.md and TODOS.md T-MSRV
EYH0602 added a commit that referenced this pull request Jul 12, 2026
)

* docs(plans): retire shipped issue-7 plan, refresh Stage 5 hub-deploy plan

Remove plans/issue-7-tree-list-mode.md — shipped in #14 (0.1.2) and folded
into docs/stage-3-viewer.md + docs/manifest-schema.md.

Refresh Stage 5 plan against the shipped Stage 4 code: correct the version
target (0.1.2 -> 0.1.3), resolve hub routing (path-based /a/{id}/ with
document-base-relative viewer URLs) and Docker assets (embedded-only), drop
the now-moot precompressed-assets step, and add a --host flag + broken-ARA
skip-on-ingest.

* docs(plans): fold Stage 5 eng review findings into hub-deploy plan

Eng review (14 findings + outside voice) folded into the Stage 5 plan:
Docker needs no wasm toolchain (bundle committed + baked via include_dir!),
harden base-href splice + id charset, no SPA fallback at hub root, share
serve_cached_manifest, define empty/bad-root startup, clap ArgGroup for mode
select, 308 (not cacheable 301) for known ids only, drop unused parsed
Manifest on the hub path, wire --assets end-to-end, add wasm + headless
browser tests for D1. Defer per-ARA figures (ServeDir cannot nest under a
{id} param) and record the static-export alternative (D3: keep server).

Add T-HUB-FIGURES and T-STATIC-EXPORT to TODOS.md.

* feat(viewer): resolve API/live URLs relative to document base

Plan step 1/4: Viewer relative API/WS URLs (T5)

Make ManifestSource default URLs relative (api/manifest, api/live) so they
resolve against document.baseURI — local serve unchanged (page at /), hub
serves under <base href="/a/{id}/">. Refactor the ws:// scheme-swap core into
a pure, wasm-testable ws_url_from_base(base, path). Native test guards the
relative defaults; wasm test proves resolution for root and /a/{id}/ bases.

Note: viewer source changed -> embed regen follows in step 4.

* feat(serve): hub ingest cache — parse-once, immutable, id-guarded

Plan step 2/4: Hub cache ingest (T2, T8, T12)

Add serve/hub.rs: ingest(root) scans immediate child dirs into an immutable
Arc<HashMap<id, Arc<CachedAra>>> (lock-free reads, no watcher). Ids constrained
to [A-Za-z0-9._-]+ (rejects spaces/non-ASCII/slash/..) — one guard covering
path-segment, URL, and HTML-escape safety. Broken ARAs and duplicate ids are
logged/skipped, never fatal; an unreadable root is fatal (Err). Per-entry read
errors are skipped, not dropped.

cache.rs: manifest field is now Option<Arc<Manifest>>; from_dir_lean() drops
the parsed graph the hub never reads (~2x resident memory saved per ARA) while
from_dir() keeps it for local serve. Shared build() produces identical json +
etag + figures_dir either way.

Native tests cover: two good, one broken skipped, empty->empty, missing->Err,
rejected charset, duplicate id kept-first, lean drops manifest.

* feat(serve): hub router + CLI mode selection

Plan step 3/4: Hub routing + CLI args (T3, T4, T7, T9, T10, T11, T13)

- clap ArgGroup models local (positional dir) vs hub (--hub --ara-root) so bad
  combos fail at parse time; add --host (default 127.0.0.1, honors 0.0.0.0 for
  containers) (T9).
- Extract serve_cached_manifest(&CachedAra, &HeaderMap) shared by the local and
  per-ARA hub manifest handlers — one conditional-GET source of truth (T7).
- build_hub_router: /a/{id}/api/manifest (404 if unknown), /a/{id}/ index with
  <base href> injected + no-cache, / index listing, strict shared-asset
  fallback. No /api/live, no watcher.
- Base-href splice guarded: no <head> -> 500, never a silent base-less page (T3).
- Root asset serves real files only, else 404 — no SPA fallback (T4).
- Bare /a/{id}: known -> 308, unknown -> 404 (never 308-to-404) (T10).
- --assets wired end-to-end in hub (on-disk index + shared ServeDir) (T13).
- hub oneshot tests incl. two sequential reads = same etag (pure cache hit, T11)
  + full parse-arg matrix; also applies pending rustfmt to cache.rs/hub.rs.

run() branches to hub when --hub is set; local path behavior unchanged.

* build: Docker image, deploy docs, CI smoke test, embed regen, 0.1.3

Plan step 4/4: Regen embed + Dockerfile + CI + docs (T1, T6, T14)

- Regenerate the embedded viewer bundle (step 1 changed viewer source, so
  viewer-embed-fresh required a regen).
- Dockerfile: multi-stage musl -> distroless, cargo-chef dep cache, NO wasm
  toolchain (viewer baked in via include_dir!). Pins the repo toolchain before
  adding the musl target and points CC/linker at musl-gcc. Verified: image
  builds, 3.1 MB compressed (< 20 MB budget) (T1).
- .dockerignore keeps the context small (target/, corpus, git, plans/docs).
- docs/deploy.md: hub routing, ingest, Docker/compose, systemd, Caddy/nginx
  reverse-proxy compression, and the manifest.json-fallback-inert-on-hub note
  (T14). Folds the plan into docs per CLAUDE.md.
- CI: docker job (build + image-size budget + container smoke test:
  manifest 200/etag/304, /a/{id}/ html+base, wasm application/wasm).
- viewer wasm test: relative api/manifest resolves under <base href="/a/x/">
  in a real browser (T6).
- Bump workspace 0.1.2 -> 0.1.3 + ara-core pins; CHANGELOG [Unreleased].

* docs: retire stage-5 plan (folded into docs/deploy.md)

Plan fully implemented across the 4 preceding commits; per CLAUDE.md the plan is
rewritten as a design doc (docs/deploy.md) and removed from plans/.

* fix(docker): link the musl binary statically for distroless

The CI docker job failed with 'exec /ara: no such file or directory'. The
CARGO_TARGET_..._LINKER=musl-gcc override made musl-gcc the link driver, which
links dynamically against /lib/ld-musl-x86_64.so.1 — absent from
distroless/static, so the loader-less image can't exec the binary.

Drop the linker override (rustc's musl target links fully static via its
self-contained startup objects) and pin RUSTFLAGS=-C target-feature=+crt-static.
Keep only the CC=musl-gcc override, which is what actually fixed the build
script's -m64 error. Verified: the extracted binary is static-pie with no
interpreter, and the container passes the full smoke test locally.

* style(design): FINDING-001/004 — fix WCAG AA contrast for muted + accent text

- --muted: #90856f → #726751 (3.38:1 → 5.16:1 on --bg, 3.10:1 → 4.75:1 on --panel2)
- Add --accent-text: #8c4414 for text-on-light uses (3.67:1 → 6.60:1 on --bg)
- Original --accent preserved for borders/backgrounds where 3:1 suffices

* style(design): FINDING-002 — add focus-visible ring to tree-list rows

Tree rows are role="button" + tabindex="0" but had no visible keyboard focus
indicator. Adds a 2px accent outline matching the graph-mode node focus style.

* style(design): FINDING-003 — respect prefers-reduced-motion

Disable skeleton shimmer animation when the user's OS has motion reduction
enabled. The only animation in the viewer is this loading shimmer.

* style(design): FINDING-005/006 — enlarge touch targets, bump body font

- Controls: height 1.75rem → min-height 2.25rem (28px → 34-36px)
- Body font-size: 14px → 15px (closer to the 16px accessibility guideline
  while preserving data-dense tool proportions)
- Applies to .toolbar-search/.toolbar-select, .layout-toggle, and .btn

* style(design): FINDING-007 — use color-mix for --warn strikethrough

Replace raw rgba(162, 59, 45, 0.4) with color-mix(in srgb, var(--warn) 40%,
transparent) so the decoration color tracks the token if --warn changes.
Matches the same technique used in the status pills.

* build: regenerate embedded viewer bundle for design fixes

The design-review CSS fixes (FINDING-001..007) changed the ara-viewer source,
so `ara serve` shipped stale UI when run without --assets. Regenerated via
scripts/embed-viewer.sh so the embedded bundle matches the source and CI's
viewer-embed-fresh check passes.

* docs: fold shipped plans into docs/, remove plans/

All 6 plans in plans/ were for features that have shipped, so per CLAUDE.md's
plan→docs lifecycle they move to docs/ as design records and out of plans/.

- New design docs: stage-0-ci-tooling, stage-2-dag-layout, real-corpus-no-panic
- Stage 0 (#1), Stage 2 (#5), issue #3 (#4) had no docs/ counterpart → written
- issue #9 layout modes already folded into docs/stage-3-viewer.md → just removed
- ara-runtime-impl-plan + stage-overview were meta-planning artifacts fully
  realized in code + per-stage docs → removed
- Retire completed TODOs T-DOCS (this migration) and T-VIEWER-DIST-PACKAGING
  (resolved by the Stage 4 include_dir! embed), and the obsolete
  T-STAGE4-VERSION-BUMP (Stage 3/4 both shipped)
- Fix dangling plan references in README.md and TODOS.md T-MSRV

* docs: drop dangling plan references in README and TODOS

Point README at docs/ instead of the removed master plan, refresh T-MSRV
context to the crates.io publish, and retire completed/obsolete TODOs
(T-DOCS, T-VIEWER-DIST-PACKAGING, T-STAGE4-VERSION-BUMP).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant