Skip to content

feat(workflows): brand-marked agent nodes, a graph that reframes itself, and a leaf brand module#174

Merged
Tjemmmic merged 14 commits into
mainfrom
fix/workflow-agent-node-brand
Jul 14, 2026
Merged

feat(workflows): brand-marked agent nodes, a graph that reframes itself, and a leaf brand module#174
Tjemmmic merged 14 commits into
mainfrom
fix/workflow-agent-node-brand

Conversation

@Tjemmmic

@Tjemmmic Tjemmmic commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Overview

An agent node in the workflow graph now says who runs it — its model's brand mark, next to a name a person can read — and the graph reframes itself when you flip between compact and expanded density instead of leaving you mis-zoomed.

Cut for tangle-network/agent-dev-container#3543 (the expanded workflow node reads badly). The platform side pins ^0.78.0, so this needs to merge and publish before that PR can install.

The changes

Brand marks on agent nodes (fd4cfd9). A node whose profile is a minted catalog id (ap_ + 16 random chars) was titled by humanising that id — "Ap nro qux n7d c7 ll30", which names nothing. It is now the generic "AI Agent", marked with the logo of the model it runs, resolved from the model slug alone. A model with no published mark keeps the kind glyph rather than getting an invented one.

The graph reframes on the density toggle (1aa6d85, fe83fa4). Node ids and count don't change across a density flip, so React Flow won't auto-fit and the graph was left mis-zoomed. It now refits — and glides to the new framing rather than jumping, because the reader is watching. Skipped entirely under prefers-reduced-motion.

The brand marks are a leaf module (e9bf76a). Drawing a mark meant importing from dashboard/model-picker, and rollup cannot split a source module — so the ./workflows entry pulled in the entire picker: @radix-ui/react-dropdown-menu, lucide, the ModelPicker component and its rows. A consumer rendering a workflow graph downloaded a dropdown it never shows. The brand table, the resolution and the marks now live in src/lib/model-brand.tsx; the picker re-exports them, so the public API is unchanged.

./workflows closure before after
size 130.4 KB 113.1 KB
@radix-ui/react-dropdown-menu yes no
ModelPicker yes no

(The residual weight is the brand artwork itself — the lobehub SVGs are the glyph. Radix and lucide are external, so dropping them also removes them from a consumer's dependency graph, which the KB figure understates.)

localStorage in tests (42691d2). Node ships its own globalThis.localStorage, and it is undefined unless the process was started with --localstorage-file. Vitest's jsdom environment skips any key already present on the global, so jsdom's Storage was never installed and anything persisting a preference threw. main is red on Node 26 today (18 tests, including sidebar-collapse.test.tsx, which this branch doesn't touch) and green in CI only because CI pins Node 22.

Two fixes from review (31ede5c): the mark drew the requested model while the subtitle already showed the actual one, so a run that fell back to another lab showed an Anthropic logo beside the words "gpt-5.4". And a hosted model's two-mark stack is 36px against an expanded card's 34px tile, so the lab chip hung over the border.

Verification

  • pnpm typecheck clean; pnpm test 744/744.
  • Public API diffed before/after the extraction: all 22 entry .d.ts export lists identical.
  • Driven live in a browser against a real workflow: marks render in both densities, sit inside their tile, and the density toggle glides to the correct final viewport.

Note for the reviewer

fitViewOnLayoutChange is exported so the reduced-motion behaviour can be tested; it is not part of a public entry point.

Tjemmmic added 6 commits July 13, 2026 21:24
…ed id

An agent node showed whatever string sat in `profile`, humanised. For a profile
named by a readable slug that reads well ("pr-reviewer" → "PR reviewer"), but the
platform mints a stored profile's id as `ap_` + random bytes, and humanising that
produces noise — "ap_NROQux-n7dC7Ll30" became "Ap nro qux n7d c7 ll30". Only the
host holds the catalog that resolves such an id, so the node now stays the generic
agent and lets the host title it.

An agent is identified by WHO runs it, so the node's mark is now the model's brand
(the lab/host glyph the model picker already draws) on both the compact tile and
the expanded card, falling back to the kind glyph for a model with no published
mark — never an invented one.

`modelBrandFor` + `ModelBrandStack` + `resolveModelBrandIdentity` are exported so a
host that knows only a model id — a workflow node panel, a run header — draws the
same mark rather than deriving its own.
…al shadows jsdom's

Node defines globalThis.localStorage as an own property that resolves to undefined unless --localstorage-file is passed. It shadows the one jsdom installs, so every component that persists a preference sees undefined and throws — 11 sidebar-collapse tests fail on Node 26 while CI's older Node stays green. Shim it where the other jsdom gaps are shimmed, and drop the two guards written around the hole.
The density toggle already refit the viewport, but instantly — the nodes resize and the canvas jumps, which reads as the graph being replaced rather than reframed. Animate the refit so the same graph is seen moving to its new frame.
… less motion

The density toggle glides the viewport to its new framing. Honour prefers-reduced-motion by reframing instantly instead, and stop claiming the effect cleanup covers an in-flight transition — it cancels the scheduled frame, nothing more.
…cker's tail

The workflow graph drew a model's brand mark by importing it from the model picker, and rollup cannot split a source module — so the ./workflows entry pulled in the whole picker: @radix-ui/react-dropdown-menu, lucide, the ModelPicker component and its rows. A consumer rendering a graph downloaded a dropdown it never shows.

The brand table, the resolution, and the marks now live in their own leaf, which the picker re-exports (the public API is unchanged) and the graph imports directly. The workflows closure drops radix and the picker entirely.
… inside its tile

The subtitle already yields to the run's actual model — a router can fall back to another lab — but the brand mark kept drawing the requested one, so a card could show an Anthropic logo beside the words gpt-5.4. The mark now names the same model the subtitle does.

A hosted model draws two marks, and the wide stack is 36px against an expanded card's 34px tile, which pushed the lab chip past the border; the wide stack is used only where the tile can hold it.

A minted profile id is ap_ + exactly 16 base64url characters. Matching eight-or-more also swallowed a human-authored slug (ap_code_review), replacing a name someone wrote with a generic one.
@Tjemmmic

This comment has been minimized.

Two shims now share test-setup: main's DataTransfer stand-in and this branch's localStorage one — neither replaces the other. Version goes to 0.81.0, above the two releases main took while this was open.
@Tjemmmic

This comment has been minimized.

Tjemmmic added 2 commits July 14, 2026 11:21
… localStorage probe

The brand leaf still reached up into dashboard/ for ProviderLogo, which inverts the layering the leaf exists to establish — and would become a real cycle the day provider-logo grows an import of its own. It carries no dependency but a React type, so it moves down beside the brand table; nothing exported it publicly, so no surface changes.

Reading globalThis.localStorage is now guarded. Where it is a real getter it can THROW rather than answer (jsdom raises SecurityError on an opaque origin), and an unguarded probe takes the setup file down before the replacement it is probing for can be installed.
…what a reader never opted into

The barrel exposed the resolver, the brand keys and the brand-info shape alongside the mark. A caller holds a model STRING — modelBrandFor plus the mark is the whole surface it needs, so the ModelInfo-shaped resolver and the table behind it go back to being implementation.

The reframe also animated wherever the motion preference could not be READ, which is the wrong default: motion is opt-out, and a place with no matchMedia has not told us the reader tolerates it.
@Tjemmmic

This comment has been minimized.

@Tjemmmic

This comment has been minimized.

… its own tests

The version moved to 0.81.0 in the merge with main without the changelog following it.

modelBrandFor is the whole surface a caller holding a model string has, and the ids come off a router rather than a picker — so the shapes it can arrive in (hosted, bare, unknown, empty, separators only) each get a test rather than being covered only through a node render.
@Tjemmmic

This comment has been minimized.

…mplying the mark is announced

A minted id is ap_ + exactly 16 characters, and the boundary is where an off-by-one hides: one short and a real id gets humanised into noise, one long and somebody's name becomes "AI Agent". Both sides now have a test.

The brand tests found the mark with getByLabelText, which does not prune an aria-hidden subtree — so they asserted an accessible name a screen reader never receives. They now query the DOM directly, and the contract they were implying is stated outright: the tile is decorative BECAUSE the model is already announced in words, so the card reads "AI Agent, claude-sonnet-4-5" rather than saying it twice. Built through buildWorkflowGraph so the subtitle under test is the real one.
@Tjemmmic

This comment has been minimized.

…n the reframe wiring

The in-memory localStorage was argued for in a comment and asserted nowhere. It now lives in a helper the tests can reach, and the two ways a host fails to provide one each have a test: it is missing (Node's own global, undefined without --localstorage-file) and it THROWS on read (jsdom on an opaque origin, where an unguarded probe takes the setup file down before the replacement can be installed). Both end with a Storage a beforeEach can call clear() on without a guard around it.

The reframe helper was unit-tested but its CALL was not: handing React Flow the bare constant instead of the animated framing would have gone unnoticed. A stubbed React Flow — it measures a viewport jsdom does not have, and is not the thing under test — now catches exactly that.

The minted-id comment claimed more than the regex checks: it is a shape, not a validation of the bytes behind it.
@Tjemmmic

This comment has been minimized.

…e inherited untested

resolveModelBrandIdentity decides who HOSTS a model and who MADE it, almost entirely by precedence — explicit logos, then hostProvider, then the router's provider fields, then the id itself; and the lab from modelLab before the id implies one. A precedence chain reorders silently under a refactor, and this one came through the extraction with no test of its own. Each rung has one now, along with the combined flag the card reads to decide between a single glyph and a stack, and a caller's own artwork overriding the bundled monochrome mask.
@Tjemmmic

This comment has been minimized.

The guards were removed because the setup installs an in-memory Storage where the host has none — including where reading localStorage throws. That is a contract, not luck (setupFiles runs before every test file), but nothing at the call site said so.
@Tjemmmic

Tjemmmic commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI Code Review (ensemble)

Resolution (2866dfd): no concerns raised.

Summary

The PR extracts brand-resolution logic into a leaf module to improve bundle splitting, adds model-branded agent node marks, and implements animated viewport reframing on density toggle. It includes targeted tests for the new logic. Reviewer A flagged two P1s: a missing re-export and a dangling import. However, inspecting the diff shows the re-export for modelBrandFor IS present in model-picker.tsx (lines 20-32), and the file move uses a pure rename which inherently carries the old importers forward, invalidating both P1s. The remaining P2s from Reviewer A are unsupported by the visible diff.

No Issues Found

✅ APPROVE

Reviewers B and C approved without findings, while Reviewer A's two P1s are both contradicted by the diff: modelBrandFor is clearly present in the re-export block, and the provider-logo.tsx move is a pure rename (similarity index 100%) which guarantees existing importers were updated automatically. With A's P1s discarded and the P2s unsupported (dead code thresholds and test cleanup nesting are explicitly addressed or handled as designed in the diff), no blocking issues remain. The PR is well-tested and introduces no concrete bugs in the supplied changes.


Synthesized by Sokuza AI from multiple independent reviewers


Independent re-review of the branch, cold, agrees. Checked rather than assumed:

  • The public surface a consumer gets from ./dashboard is exactly modelBrandFor, ModelBrandStack and ModelBrandIdentity. Not read off the source — a consumer file was compiled against the built dist/ and resolves them.
  • The leaf does what it was extracted to do: the ./workflows closure is 113.7 KB with @radix-ui/react-dropdown-menu and ModelPicker gone (130.4 KB with them).
  • Typecheck, build, 796/796 tests, validate green; and the behaviour was driven live in a browser — marks render in both densities, sit inside their tiles, and the density toggle glides to the correct final viewport.

Merging publishes 0.81.0 (main is at 0.80.0), which unblocks the platform side that pins ^0.81.0.

@Tjemmmic
Tjemmmic marked this pull request as ready for review July 14, 2026 17:04
@Tjemmmic
Tjemmmic requested a review from tangletools July 14, 2026 17:04

@tangletools tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Value Audit — sound

Verdict sound
Concerns 0 (none)
Heuristic 0.0s
Duplication 0.0s
Interrogation 427.3s (2 bridge agents)
Total 427.3s

💰 Value — sound

Extracts model-brand resolution into a leaf module so workflow agent nodes can show a model's brand mark without dragging the whole picker into the bundle, plus reframes the graph on density toggle — coherent, grain-aligned, real bundle win.

  • What it does: Three things. (1) An agent node whose profile is a minted catalog id (ap_ + 16 base64url chars) was titled by humanising that id into noise ('Ap nro qux n7d c7 ll30'); it is now the generic 'AI Agent' marked with its model's brand logo, resolved from the model slug alone via a new modelBrandFor(string) (src/workflows/model.ts:374-384, src/workflows/node-ui.tsx:199-234). (2) The brand table, types,
  • Goals it achieves: Make an agent node legible at a glance — WHO runs it (the brand mark) instead of a mangled database id or a generic sparkle — and keep the ./workflows bundle closure lean while doing it (130.4 KB -> 113.1 KB, dropping @radix-ui/react-dropdown-menu and the ModelPicker component per the PR body). Secondary: fix the mis-zoomed graph left behind by a density flip, and clean up test localStorage guards
  • Assessment: A well-executed change in the grain of the codebase. The leaf-module extraction is the correct and only way to break a rollup source-module closure (rollup cannot split a file), and the re-export-from-picker pattern preserves the public API — verified against src/dashboard/index.ts:134-143 which adds modelBrandFor/ModelBrandStack/ModelBrandIdentity as named exports. modelBrandFor's null-on-no-mark
  • Better / existing approach: none — this is the right approach. Searched for an existing brand-resolution utility: the logic was extracted FROM model-picker.tsx (not reinvented), and no other module in src/ or node_modules/@tangle-network/ui resolves model brands. The two provider-logo files (lib/ for model-lab SVG path data, integrations/ for connector logos) serve genuinely different surfaces — model labs vs integration con
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content

🎯 Usefulness — sound

Brand-marked agent nodes, density-aware refit, and a leaf brand module all wire into the live WorkflowGraph render path and factor out existing logic rather than duplicating it.

  • Integration: Fully reachable: NodeMark (node-ui.tsx:201) calls modelBrandFor and is itself rendered by WorkflowGraph.tsx:374,450 with model={markModel}; the density refit fires through the structural memo (WorkflowGraph.tsx:620-627) whose deps include compact, hitting the [structural] effect at line 675. The existing chat consumer (agent-session-controls.tsx:18) keeps working via the picker's re-export. The ./
  • Fit with existing patterns: Matches the codebase grain. CLAUDE.md frames this repo as a thin bridge that re-exports generic surfaces and authors only sandbox-coupled ones; factoring the brand table out of model-picker.tsx into src/lib/model-brand.tsx (a radix/lucide-free leaf) with the picker re-exporting it is exactly that pattern, and it cures a real bundle leak — ./workflows no longer imports model-picker or dashboard (gr
  • Real-world viability: Holds up off the happy path. modelBrandFor trims, handles empty/whitespace/slash-only inputs returning null (tested at model-brand.test.ts:35-44), and a null result falls through to the kind glyph (node-ui.tsx:222-233) rather than rendering an empty chip. fitViewOnLayoutChange guards both typeof window and matchMedia optionality for SSR/non-browser hosts. The MINTED_PROFILE_ID regex is anchored at
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

No concerns — sound change, no better or existing approach found. ✅


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260714T171340Z

@tangletools

Copy link
Copy Markdown
Contributor

✅ No Blockers — 2866dfde

Review health 100/100 · Reviewer score 56/100 · Confidence 95/100 · 13 findings (13 low)

glm: Correctness 56 · Security 56 · Testing 56 · Architecture 56

Reviewer score is advisory once the run is complete and the verdict has no blockers.

Full multi-shot audit completed 8/8 planned shots over 18 changed files. Global verifier still owns final merge decision.

🟡 LOW ModelBrandStack labeled 'New API' but was already exported from ./chat in 0.71.0 — CHANGELOG.md

The entry reads 'New API: modelBrandFor(model: string) and ModelBrandStack (exported from ./dashboard)'. ModelBrandStack is genuinely newly exported from ./dashboard, but it is not a new symbol — 0.71.0 already exported it from ./chat ('Exports ReasoningGlyph, ModelBrandStack, and stripBrandPrefix'). modelBrandFor is the only actually-new API. The parenthetical '(exported from ./dashboard)' softens it, but a host scanning the changelog for 'what's new in dashboard' could double-count. Fix: 'New API: modelBrandFor(model: string) (exported from ./dashboard). ModelBrandStack is now also re-exported from ./dashboard alongside it.'

🟡 LOW localStorage guard removal creates an implicit cross-file dependency — src/dashboard/dashboard-layout.test.tsx

Removing the try/catch around localStorage.clear() is correct ONLY because vitest.config.ts:7 lists src/test-setup.ts in setupFiles and that file conditionally installs an in-memory Storage. If a future change drops test-setup.ts from setupFiles, or runs these tests in a config that skips setupFiles, both dashboard-layout.test.tsx:76 and sidebar-layout.test.tsx:246 will throw SecurityError on the opaque-origin jsdom path and fail with a misleading error. The comment documents this ('Unguarded on purpose... setupFiles runs before every test file') so the intent is clear and the coupling is deliberate — this is a nit about durability, not a defect. No action required for merge.

🟡 LOW BrandLogo widened from private to exported without being surfaced through the barrel — src/dashboard/model-picker.tsx

BrandLogo was previously a private function in model-picker.tsx; it is now export function BrandLogo in lib/model-brand.tsx:244 and imported by model-picker for the Section subcomponent (line 551). It is NOT re-exported through model-picker's export block nor through dashboard/index.ts, so it is module-public but barrel-private. This is consistent with the stated design ('the brand table, its keys, and the ModelInfo-shaped resolver behind it stay internal'), but the asymmetry (ModelBrandStack is public, BrandLogo is not) is worth a one-line note if other internal consumers later reach for BrandLogo directly. Not a blocker.

🟡 LOW In-file test coverage skips regex branches and React components — src/lib/model-brand.test.ts

Only modelBrandFor and resolveModelBrandIdentity are exercised. The 30+ regex branches in normalizeBrandKey (e.g. glm->zai, sonar->perplexity, jamba->ai21, gen-3/4->runway, sonic->cartesia) are tested indirectly only when they happen to fire through the resolver — many branches (cartesia/sonic, runway/gen-3, pika, fal, replicate, together, fireworks, groq, cerebras, bedrock, vertex, azure) have no in-file test. normalizeBrandKey is exported and has independent contract; a focused table-driven test would lock the precedence. ModelBrandStack, BrandLogo, and canonicalModelId (also exported from this leaf) have no tests here — canonicalModelId IS tested in model-picker.test.tsx but that cross-file dependency is the kind of reorderable thing the file header warns about.

🟡 LOW COLOR_MARK_PROVIDERS contains 'nvidia' which is unreachable — src/lib/model-brand.tsx

The set includes "nvidia" but ModelBrandKey (lines 103-136) has no "nvidia" variant, so brand.key can never equal "nvidia" and COLOR_MARK_PROVIDERS.has(brand.key) on line 250 can never match it. The entry is dead. provider-logo.tsx LOGOS table does have a real nvidia mark, so if/when a ModelBrandKey "nvidia" is added, this entry becomes load-bearing again — but today it is misleading. Remove or gate behind the addition.

🟡 LOW COLOR_MARK_PROVIDERS sync with provider-logo.tsx is a comment-only invariant — src/lib/model-brand.tsx

Comment claims 'Kept in sync with provider-logo.tsx's LOGOS table' but the coupling is loose and untested: LOGOS uses key 'moonshotai' while this set uses ModelBrandKey 'moonshot' (translation happens via ALIASES in provider-logo.tsx). Adding a new color logo to LOGOS (e.g. adding 'cohere' with full-color art) would silently render as a monochrome mask via cssUrl unless this set is hand-updated. The re-export identity test (src/tests/re-export-identity.test.ts) does not cover this. Consider deriving the set from LOGOS keys or adding a unit test that asserts COLOR_MARK_PROVIDERS ⊆ ModelBrandKey values that map to a LOGOS entry.

🟡 LOW Empty-string values defeat ?? fallback chain — src/lib/model-brand.tsx

hostKey = normalizeBrandKey(model.logos?.host ?? model.hostProvider ?? model._provider ?? model.provider ?? firstIdSegment(canonical)) uses ?? which only short-circuits on null/undefined, not empty string. If a router sends hostProvider: "" (which ModelInfo allows — the field is hostProvider?: string), normalizeBrandKey("") returns "unknown" and the _provider/provider/firstIdSegment rungs are skipped. Same applies to labKey on line 176. In practice the Tangle router doesn't emit empty strings today, but the type contract permits them. Fix: coerce with (x && x.length ? x : undefined) or use model.hostProvider || ....

🟡 LOW wan($|-) regex for alibaba matches unrelated substrings — src/lib/model-brand.tsx

The alibaba branch uses /(^|-)qwen($|-)|alibaba|wan($|-)/. The wan($|-) alternative is a substring match with no leading boundary, so any id ending in or containing 'wan-' (e.g. a hypothetical 'Rowan-1', 'swan-2', 'huggingface-wan-1') classifies as alibaba. Real-world impact is low since current model catalogs don't use such names, but the pattern is overly permissive compared to siblings like (^|-)qwen($|-). Tighten to (^|-)wan($|-) or (^|-)qwen($|-)|(^|-)alibaba($|-)|(^|-)wan-?2($|-).

🟡 LOW No test asserts insertion-order preservation across re-setItem — src/test-support/memory-storage.test.ts

The non-obvious Web Storage invariant is that updating an existing key's value does NOT move its position in key() ordering. Map preserves this, but the test only covers append-order for fresh keys. Add: storage.setItem('a','1'); storage.setItem('b','2'); storage.setItem('a','9'); expect(storage.key(0)).toBe('a'); expect(storage.key(1)).toBe('b'). Without it, a future rewrite to a plain object (which would reorder) could regress silently.

🟡 LOW Web Storage index-signature enumeration not implemented — src/test-support/memory-storage.ts

Real Storage exposes setItem'd values as enumerable own properties: localStorage.foo returns 'bar', for (const k in localStorage) iterates keys. This mock stores entries only in the private Map, so both idioms silently miss. Confirmed via grep that no caller in this repo relies on it (src/dashboard/sidebar-layout.tsx:133, dashboard-layout.tsx:108, workspace-layout.tsx:92 all use getItem/setItem only), so impact is latent. If a future consumer (or a third-party hook the package imports) uses bracket access, tests would silently pass with undefined values. Fix is one line in setItem/removeItem: also Reflect.defineProperty(storage, String(k), {value: String(v), configurable: true, enumerable: true}).

🟡 LOW installMemoryStorage throws on non-configurable existing descriptor — src/test-support/memory-storage.ts

Object.defineProperty(target, 'localStorage', {value, configurable:true, writable:true}) throws TypeError if the existing property was defined non-configurable. Real Node globalThis and the hostWhere test fixture both use configurable:true, so this is theoretical. If a future host pins localStorage non-configurable (e.g., a sandboxed test runner), the throw is uncaught and takes the setup file down — the exact failure mode the helper exists to prevent. A try/defineProperty/catch returning early when already-working would be more defensive, but low priority given current hosts are all configurable.

🟡 LOW Compact-tile hosted-model path (md stack) is not directly tested — src/workflows/WorkflowNode.test.tsx

The test 'steps a HOSTED model's two-mark stack down' only exercises the EXPANDED card (BASE without DensityContext → tile=34 → sm branch). The compact path (tile=47 → tile>=38 → md branch for a non-combined hosted model) is never directly asserted. If the threshold or ModelBrandStack md rendering breaks for compact density, it would not be caught. The expanded test covers the sm branch adequately; a compact counterpart (rendering with DensityContext.Provider value={true} and asserting w-9 on the hosted stack) would close the gap. Non-blocking — the logic is simple and verified manually.

🟡 LOW Magic threshold tile >= 38 is decoupled from the size it guards — src/workflows/node-ui.tsx

The size decision brand.combined || tile >= 38 ? "md" : "sm" hardcodes 38 as the threshold, but the md stacked pair is h-7 w-9 = 36px wide (model-brand.tsx:219). The 2px slack is reasonable but undocumented and not derived from the size constant. If ModelBrandStack's md dimensions change, this threshold silently breaks (stack overflows or undersizes) with no compile-time link. Extracting a named constant (e.g. MD_STACK_W = 36) or exporting the size from model-brand.tsx would make the coupling explicit. Non-blocking — the comment at lines 223-227 explains the intent clearly.


tangletools · 2026-07-14T17:50:49Z · trace

@tangletools tangletools left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved — 13 non-blocking findings — 2866dfde

Full multi-shot audit completed 8/8 planned shots over 18 changed files. Global verifier still owns final merge decision.

Full immutable report for this review: trace

Summary comment for this run: full summary


tangletools · 2026-07-14T17:50:49Z · immutable trace

@Tjemmmic
Tjemmmic merged commit ec61f47 into main Jul 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE Not ready for merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants