Skip to content

Type from origin: eliminate any/cast smells (P1–P5)#15

Merged
IanMayo merged 5 commits into
mainfrom
claude/type-from-origin
Jun 16, 2026
Merged

Type from origin: eliminate any/cast smells (P1–P5)#15
IanMayo merged 5 commits into
mainfrom
claude/type-from-origin

Conversation

@IanMayo

@IanMayo IanMayo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Addresses your note that type-casting is a code smell — "data should be correctly typed from origin." I audited the codebase and applied the five fixes from the to-do list.

⚠️ Stacked on #14 (base = claude/waypoint-hexcell). Merge #14 first; GitHub will retarget this to main. The diff here is just this work (5 commits).

What the audit found

The loud smells were already gone: zero @type {unknown} double-casts, zero @ts-ignore/@ts-nocheck, zero as casts. What remained were @type assertions — a big idiomatic bucket (DOM narrowing, const/tuple literals — not smells) and a real-smell bucket dominated by any.

The five fixes (one commit each)

# Area What
P1 main.js state Typed requirement/handful/plans/steering with generated Requirement/Plan/Constraint. Reading them showed the generated types marked always-present fields optional — so I declared the real invariants required in the schema (Requirement.commitments, Plan.strategy/scores, Scores.satisfaction, Materialisation.schedule/trajectory, TrajectoryPoint.lat/lng/t/fuel_pct) rather than cast at the read sites. Genuine null-guards / ?? null for real nullability. lib→ES2023 (findLast).
P2 orbat.js Removed the (patch).x casts (vestigial — Partial<Asset> already carries the fields); inVocab() helper replaces VOCAB.includes(/** any */(x)); explicit allegiance branch vs a dynamic-index cast. 18 → 0.
P3 globals One app/js/globals.d.ts for the window/context debug handles — drops ~11 (window)/(globalThis) casts across 6 files.
P4 timers ReturnType<typeof setTimeout|setInterval> annotations instead of casting the handle to any.
P5 map.js RenderOpts typedef types the data into the view; accessor parameters annotated with real types.

The key distinction

A /** @type {T} */ (expr) assertion (overriding inference) is the smell — removed. A /** @type {T} */ name parameter/field annotation (declaring a type) is the fix — kept.

Honest residue (documented, not chased)

  • DOM narrowing (getElementByIdHTMLElement) — the required idiom, not a smell.
  • deck.gl accessor props — its Accessor<DataT,…> generics won't accept a plain JSDoc callback, so get* props are cast at that library boundary (like the existing MapOptions cast); the accessor bodies stay type-checked.
  • Consumer ctx (compare/wingman/learn/entities) still cast (s) => s.label because they receive plans via loosely-typed ctx. Typing those ctx params is the same pattern one layer out — a clean follow-up (now unblocked by the required invariants).

App-wide any-casts ~133 → ~78 (the rest are the two categories above).

Verification

  • 0 typecheck errors, 32 unit green.
  • Golden plan ids unchanged — marking fields required reflects the real shapes; the schema is validation-only and never touches the runtime canonical form (NF3).
  • ✅ Adherence still validates whole; regen idempotent/byte-reproducible.

Recorded as ADR-0031; work-log updated.

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf


Generated by Claude Code

claude added 5 commits June 16, 2026 19:46
… any-casts)

P3: a single app/js/globals.d.ts declares the debug/test window handles and the
shared cross-window context (DEC-61) — __remit, __remitFault, __remitShell,
__REMIT_SAMPLE, __map. Removes ~11 `/** @type {any} */ (window)` / (globalThis)
/ (window.opener) casts across context/main/shell/popout/map/data-analysis; the
opener and popIn now type-check through the augmented Window.

P4: timer handles typed at declaration with ReturnType<typeof setTimeout|setInterval>
(matching data-analysis.js) instead of casting the setTimeout/setInterval result
to any — main.js steeringShareTimer, wingman.js playTimer.

Also: shell.js error helper uses `err instanceof Error` instead of two any-casts.

tsconfig includes app/js/**/*.d.ts. 0 typecheck errors; 32 unit green; type-level
only (no runtime behaviour change).

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf
…drop any-casts)

orbat.js leaned on `/** @type {any} */ (patch).x` to read nested patch/next
fields even though patch is Partial<Asset> and the generated Red/Green/BlueParams
carry every field — vestigial casts. Removed them all:

- tuneAsset reads patch.kind/symbol/.../red/green/blue + availability_window
  through the real types; the availability_window assignment/delete is typed
  (BlueParams.availability_window: TimeWindow)
- validate(): a small `inVocab(vocab, string)` helper replaces
  `VOCAB.includes(/** @type {any} */ (x))` (LinkML emits enum slots as `string`,
  which trips the readonly-tuple literal narrowing); the param-group check is an
  explicit blue/red/green branch instead of a dynamic-index cast; the Omit `rest`
  returns without a cast
- position/inAO/self params typed HexCell (not any); sanitizeWindows takes
  TimeWindow[]; cleanStr takes unknown

orbat.js any-casts 18 → 0. 0 typecheck errors; 32 unit green; behaviour identical.

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf
…invariants in schema

Types the Overview lap's serialisable state with the LinkML-generated types instead
of `/** @type {any} */ (null)` casts: requirement: Requirement|null, handful: Plan[],
selected/preview/execPlan: Plan|null, steering: Constraint[].

Reading those surfaced that the generated types marked always-present fields optional
(LinkML declares few required), which would have forced read-site casts/guards — the
very smell we're removing. The principled fix is to declare the real invariants at the
origin (the schema):

- Requirement.commitments, Plan.strategy, Plan.scores, Scores.satisfaction,
  Materialisation.schedule, Materialisation.trajectory → required: true (regenerated)

Plus genuine null-safety (no casts): `if (!state.requirement) return` guards in the
compare/execute stages (requirement is genuinely null pre-capture), `?? null` on the
COA `.find()`, and optional-chaining on the observe-band lookup. tsconfig lib → ES2023
(the cards already use Array.findLast at runtime).

main.js any-casts 23 → 12 (the rest are deck.gl view-state, P5). schema-required is a
faithful reconciliation (schema ≡ code, DEC-57); golden plan ids unchanged (runtime
untouched); adherence still validates whole; 32 unit green; 0 typecheck errors.

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf
…any-casts)

views/map.js took its render options as `any`. Added a RenderOpts typedef (plans:
Plan[], selected: Plan|null, assets: Asset[] — the serialisable data — plus typed
view-state fields) and typed buildLayers/setData/render against it, so the plan/
asset/candidate/obstruction iterations infer real types instead of casting each
callback param to any. Accessor params (col/alpha/path/…) are annotated with real
types (Asset/Plan/HexCell), not any.

The residual casts in map.js are the genuine library boundary: deck.gl's typed
accessor props (Accessor<DataT,…>) don't accept a plain JSDoc callback, so the get*
props are cast there (documented, like the existing MapOptions cast) — the accessor
BODIES stay type-checked. Plus idiomatic `keyof typeof` index narrowing.

Also marked TrajectoryPoint.lat/lng/t/fuel_pct required (always present on a rendered
point) so route coords type as number, not number|undefined — regenerated.

map.js any-casts 39 → 25. 0 typecheck errors; 32 unit green; golden ids unchanged.

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf
ADR-0031 documents P1–P5 (the maintainer-directed cast elimination), the schema
`required`-invariant declarations that let main.js consume generated types without
read-site casts, the assertion-vs-annotation distinction, and the consumer-ctx
follow-up. Work-log row added.

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-16 21:42 UTC

Base automatically changed from claude/waypoint-hexcell to main June 16, 2026 20:36
@IanMayo IanMayo merged commit 801a10b into main Jun 16, 2026
5 checks passed
@IanMayo IanMayo deleted the claude/type-from-origin branch June 16, 2026 21:41
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.

2 participants