Skip to content

refactor: move backend-contract runtime helpers into core#533

Open
charliecreates[bot] wants to merge 4 commits intomainfrom
charlie/issue-524-runtime-to-core-20260225
Open

refactor: move backend-contract runtime helpers into core#533
charliecreates[bot] wants to merge 4 commits intomainfrom
charlie/issue-524-runtime-to-core-20260225

Conversation

@charliecreates
Copy link
Contributor

Summary

  • move backend-contract runtime implementations into packages/core/src/spice-runtime/*
  • update backend and tspice consumers to import runtime helpers from @rybosome/tspice-core
  • keep packages/backend-contract focused on contract defs/constants (remove runtime helper exports)
  • migrate runtime-helper test coverage into core and add backend-contract export-boundary assertions

Validation

  • pnpm -C packages/backend-contract build
  • pnpm -C packages/backend-contract typecheck
  • pnpm -C packages/backend-contract test
  • pnpm -C packages/core build
  • pnpm -C packages/core typecheck
  • pnpm -C packages/core test
  • pnpm -C packages/backend-fake typecheck
  • pnpm -C packages/backend-node typecheck
  • pnpm -C packages/backend-wasm typecheck
  • pnpm -C packages/tspice typecheck

Refs #524

@github-actions
Copy link

orrery preview: https://pr-533.orrery-c4f.pages.dev/

Copy link
Contributor Author

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

The main concern is architectural: @rybosome/tspice-core now depends directly on @rybosome/tspice-backend-contract, which blurs layering and can create long-term coupling/cycle pressure. Secondarily, the core root export surface in src/index.ts is growing into an unfocused barrel; consider a dedicated spice-runtime subpath. Tests are comprehensive but packages/core/test/index.test.ts has become a monolith and would be easier to maintain if split.

Additional notes (3)
  • Maintainability | packages/core/package.json:20-25
    packages/core now has a hard dependency on @rybosome/tspice-backend-contract (both in package.json and via imports in the new spice-runtime/* files). That creates an architectural coupling (and potential for circular dependency pressure over time) where “core” cannot be used without the contract package.

If the intent of the refactor is that runtime helpers are shared by backends and other packages, it’s reasonable for them to depend on the types they operate on—but making core depend on backend-contract makes the layering ambiguous (core → contract), while other packages (e.g. tspice) already depend on backend-contract too. This increases the blast radius of changes and makes it harder to reuse @rybosome/tspice-core independently.

Consider either:

  • moving the relevant branded types (Mat3RowMajor, Vec3, SpiceHandle, etc.) into core (or a third @rybosome/tspice-types package) and having backend-contract depend on that, or

  • keeping spice-runtime in a package that is explicitly allowed to depend on backend-contract (e.g. backend-runtime), while core stays contract-agnostic.

  • Maintainability | packages/core/src/index.ts:81-142
    packages/core/src/index.ts is becoming a large grab-bag of exports (unrelated utilities like normalizeVirtualKernelPath alongside SPICE runtime validators/branders and handle registries). This increases the chance of accidental public API expansion and makes it harder for consumers to understand what is stable vs internal.

Given the PR goal (“move backend-contract runtime helpers into core”), it would be cleaner to introduce a dedicated subpath export (e.g. @rybosome/tspice-core/spice-runtime) and re-export from there, keeping the root index focused on core primitives.

  • Maintainability | packages/core/test/index.test.ts:1-1
    The new core tests are now asserting runtime exports via dynamic import() of the package name (@rybosome/tspice-core). This is fine, but the file has grown substantially and now mixes multiple concerns (@rybosome/tspice-core invariants/path normalization + Mat/Vec branding + ids/names normalization + kernel-kind utilities) in one file.

This makes failures noisier and increases runtime for a single test file; it also reduces debuggability when only one area is broken.

Summary of changes

What changed

Runtime helpers moved out of @rybosome/tspice-backend-contract

  • Removed runtime exports/implementations from backend-contract (e.g. assertGetmsgWhich, assertSpiceInt32, createSpiceHandleRegistry, normalizeBodItem, kernel-kind helpers, Mat3/Mat6/Vec branding helpers).
  • Updated contract docs/comments to explicitly point to runtime helpers living in @rybosome/tspice-core.

New spice-runtime surface in @rybosome/tspice-core

  • Added packages/core/src/spice-runtime/* implementations:
    • domains/error.ts (GETMSG_WHICH_VALUES, isGetmsgWhich, assertGetmsgWhich)
    • shared/spice-int.ts (assertSpiceInt32, assertSpiceInt32NonNegative, bounds)
    • shared/spice-handles.ts (createSpiceHandleRegistry)
    • Renamed/moved existing helpers (kernels-utils.ts, ids-names-normalize.ts, mat3.ts, mat6.ts, vec.ts, errors.ts).
  • Re-exported these helpers from packages/core/src/index.ts so consumers can import from @rybosome/tspice-core.

Consumer updates

  • Updated backend-node, backend-wasm, backend-fake, and tspice to import runtime helpers from @rybosome/tspice-core.
  • Added TS project references to core where needed (e.g. backend-fake, tspice).

Tests adjusted

  • backend-contract tests now assert runtime helpers are not exported.
  • core tests now cover the moved runtime helpers and kernel-kind utilities.
  • Deleted backend-contract/test/kernels-utils.test.ts (migrated coverage into core).

Comment on lines 1 to 12
export const SPICE_INT32_MIN = -0x80000000; // -2147483648
export const SPICE_INT32_MAX = 0x7fffffff; // 2147483647

/**
* Optional bounds used by runtime `SpiceInt` validators in `@rybosome/tspice-core`.
*/
export type AssertSpiceInt32Options = {
/** If provided, enforce `value >= min`. */
min?: number;
/** If provided, enforce `value <= max`. */
max?: number;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The contract package now only exports SPICE_INT32_MIN/MAX and AssertSpiceInt32Options, but the JSDoc says these bounds are used by validators in @rybosome/tspice-core. That creates a subtle coupling in the opposite direction (core depending on contract at runtime), and it also makes backend-contract look like it still provides runtime helper surface.

Given the PR goal (“keep backend-contract focused on contract defs/constants”), it would be cleaner if AssertSpiceInt32Options lived in core with the validators, and backend-contract only kept the numeric constants if they are truly part of the contract.

Right now this isn’t a type error, but it’s an architectural smell: core already exports AssertSpiceInt32Options, and backend-contract exporting it too increases the chance of consumers importing the wrong one and reintroducing circular-ish dependencies.

Suggestion

Move AssertSpiceInt32Options out of @rybosome/tspice-backend-contract and into @rybosome/tspice-core (next to assertSpiceInt32). In backend-contract, keep only SPICE_INT32_MIN/MAX if you want them as contract constants.

Concretely:

  • In packages/backend-contract/src/shared/spice-int.ts, remove AssertSpiceInt32Options and its comment.
  • In packages/backend-contract/src/index.ts, remove export type { AssertSpiceInt32Options } ....
  • In packages/core/src/spice-runtime/shared/spice-int.ts, keep/own AssertSpiceInt32Options (already present) and ensure it’s re-exported from core/src/index.ts (already done).
  • Update any consumers still importing the type from backend-contract.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 100 to 109
return {
register,
lookup,
close,
size: () => handles.size,

// Internal hook used by the Node backend to best-effort dispose all open handles.
// Not part of the public backend contract.
__entries: () => Array.from(handles.entries()).map(([handleId, entry]) => [asSpiceHandle(handleId), entry] as const),
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

createSpiceHandleRegistry() exposes an internal __entries() hook (documented as internal) but returns a mutable snapshot of registry state. While it’s intended for backend cleanup, returning entries that include SpiceHandleEntry objects could allow accidental mutation of the entry objects (e.g. entry.kind = ...) in consumers of __entries().

Even if only internal backends call it today, this is an easy footgun and can cause confusing handle-kind mismatches later.

Suggestion

Make __entries() return deeply readonly data or defensive copies.

For example, clone entries:

  • __entries: () => Array.from(handles.entries()).map(([handleId, entry]) => [asSpiceHandle(handleId), { ...entry }] as const)

Or freeze them in dev if you prefer.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

@charliecreates charliecreates bot removed the request for review from CharlieHelps February 25, 2026 19:36
@rybosome
Copy link
Owner

The main concern is architectural: @rybosome/tspice-core now depends directly on @rybosome/tspice-backend-contract, which blurs layering and can create long-term coupling/cycle pressure. Secondarily, the core root export surface in src/index.ts is growing into an unfocused barrel; consider a dedicated spice-runtime subpath. Tests are comprehensive but packages/core/test/index.test.ts has become a monolith and would be easier to maintain if split.

Additional notes (3)

  • Maintainability | packages/core/package.json:20-25
    packages/core now has a hard dependency on @rybosome/tspice-backend-contract (both in package.json and via imports in the new spice-runtime/* files). That creates an architectural coupling (and potential for circular dependency pressure over time) where “core” cannot be used without the contract package.

If the intent of the refactor is that runtime helpers are shared by backends and other packages, it’s reasonable for them to depend on the types they operate on—but making core depend on backend-contract makes the layering ambiguous (core → contract), while other packages (e.g. tspice) already depend on backend-contract too. This increases the blast radius of changes and makes it harder to reuse @rybosome/tspice-core independently.

Consider either:

  • moving the relevant branded types (Mat3RowMajor, Vec3, SpiceHandle, etc.) into core (or a third @rybosome/tspice-types package) and having backend-contract depend on that, or
  • keeping spice-runtime in a package that is explicitly allowed to depend on backend-contract (e.g. backend-runtime), while core stays contract-agnostic.

@CharlieHelps I agree, that is a big concern. Let's move all of the necessary types into core such that tspice-core does not depend on tspice-backend-contract. tspice-core should have zero project-internal dependencies.

@charliecreates
Copy link
Contributor Author

Implemented — I moved the shared runtime types into @rybosome/tspice-core and removed the core -> backend-contract dependency.

What changed:

  • Added canonical runtime/shared types in packages/core/src/spice-runtime/shared/types.ts.
  • Updated packages/core runtime helpers + root exports to use core-local types.
  • Removed @rybosome/tspice-backend-contract from packages/core/package.json and removed the ../backend-contract TS project reference.
  • Flipped dependency direction so backend-contract depends on core and re-exports/aliases those types to preserve its public API.

Validation run:

  • pnpm -C packages/core typecheck
  • pnpm -C packages/core test
  • pnpm -C packages/backend-contract typecheck
  • pnpm -C packages/backend-contract test
  • pnpm -C packages/tspice typecheck

Commit:

packages/core now has zero @rybosome/* internal dependencies.

@charliecreates
Copy link
Contributor Author

Follow-up: I verified the commit is now on the actual PR head branch (charlie/issue-524-runtime-to-core-20260225).

Current PR head:

  • 47c1c9e8b7cd49c42886d49024167aeb8a40d0df

So the refactor is now fully reflected in this PR branch.

@rybosome
Copy link
Owner

@CharlieHelps please review all relevant issue and PR context, then conduct a thorough code review.

What “good” looks like

  • Blocking vs non-blocking are clearly separated: “Blocking feedback” items are required for merge; everything else is “Non-blocking feedback”.
  • Review feedback is durable: leave a GitHub Review (not a chat-only summary).
  • Ownership + follow-through: if you request changes, track the re-review and resolve threads when addressed.
  • Scope control: don’t expand requirements; propose follow-ups for larger work.

Workflow

  1. Orient + sanity check

    • Read the PR title/body and any linked Linear issue.
    • Read all comments and past PR reviews (including review comments) on the PR.
    • Check ownership expectations via CODEOWNERS (don’t override owners; align with them).
    • If the PR is huge/messy, consider recommending a clean replacement PR (don’t bikeshed the current diff).
  2. Review the diff with repo guardrails in mind

    • Validate correctness, types, tests, and operational risk.
    • Use the repo’s canonical coding guardrails (especially around TypeScript safety).
  3. Leave one structured GitHub Review

Use this exact structure in the review summary:

Blocking feedback

**Non-blocking feedback**

Rules:

  • Use numbered lists.
  • Do not restart numbering for non-blocking feedback; continue from the Blocking feedback section.
  • Each item should be:
    • a short, clear claim (what’s wrong / what to change),
    • why it matters (correctness, safety, maintainability, performance, etc),
    • the smallest reasonable fix (or a concrete suggestion).
  1. Approve / request changes
  • If there are no Blocking feedback items and you are not the PR author, Approve.
  • If there are Blocking feedback items, Request changes.
  • If you are the author, do not approve your own PR; leave a comment/review without approval.
  1. Follow-through after updates
  • When Blocking feedback items are addressed:
    • Resolve the corresponding GitHub review threads, and
    • leave a short note like: “Resolved in <commit-sha>.”
  • If you previously requested changes, convert to Approve once Blocking feedback items are resolved.

Copy link
Contributor Author

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

Blocking feedback

  1. scripts/docs-api.mjs currently compiles backend-contract before core, but this PR now makes backend-contract import @rybosome/tspice-core; on a clean build this causes TS2307 module-resolution failures and is already failing multiple CI checks. This is merge-blocking because Docs build, check, and check-native jobs are red for this reason. Please update docs API generation so core declarations are built before backend-contract (or switch to a dependency-aware tsc -b flow) so build order is deterministic.
**Non-blocking feedback**
  1. SpiceHandleRegistry still exposes optional __entries() through exported core types, which leaves an internal cleanup/debug hook reachable from the public API surface. That increases long-term API maintenance risk and blurs boundary intent. Consider splitting public vs internal registry types (or otherwise hiding __entries from root exports) so the hook stays internal.

@charliecreates
Copy link
Contributor Author

charliecreates bot commented Feb 25, 2026

Finished a thorough review and posted a structured review with blocking/non-blocking feedback: #533 (review). GitHub blocked REQUEST_CHANGES because this PR is authored by app/charliecreates.

@rybosome
Copy link
Owner

Blocking feedback

  1. scripts/docs-api.mjs currently compiles backend-contract before core, but this PR now makes backend-contract import @rybosome/tspice-core; on a clean build this causes TS2307 module-resolution failures and is already failing multiple CI checks. This is merge-blocking because Docs build, check, and check-native jobs are red for this reason. Please update docs API generation so core declarations are built before backend-contract (or switch to a dependency-aware tsc -b flow) so build order is deterministic.

Non-blocking feedback
2. SpiceHandleRegistry still exposes optional __entries() through exported core types, which leaves an internal cleanup/debug hook reachable from the public API surface. That increases long-term API maintenance risk and blurs boundary intent. Consider splitting public vs internal registry types (or otherwise hiding __entries from root exports) so the hook stays internal.

@CharlieHelps please address both of these issues

@charliecreates
Copy link
Contributor Author

Implemented — both items from your feedback are now addressed in 32b2c8c9c23982ad922a63448ce442040cffefe4.

  1. Blocking docs build order

    • Updated scripts/docs-api.mjs so declaration prebuild order is now core -> backend-contract -> backend-wasm -> tspice.
    • This fixes the clean-build TS2307 ordering failure.
  2. Non-blocking SpiceHandleRegistry API leakage

    • Removed __entries from the exported public SpiceHandleRegistry type.
    • Kept __entries internal-only in runtime implementation for debug/internal usage.
    • Updated internal backend-wasm callsite to use internal casted access instead of public surface typing.

Validation run:

  • node scripts/docs-api.mjs
  • pnpm -C packages/core typecheck
  • pnpm -C packages/backend-contract typecheck
  • pnpm -C packages/backend-wasm typecheck

All passing locally in the PR branch.

@rybosome
Copy link
Owner

@CharlieHelps please review all relevant issue and PR context, then conduct a thorough code review.

What “good” looks like

  • Blocking vs non-blocking are clearly separated: “Blocking feedback” items are required for merge; everything else is “Non-blocking feedback”.
  • Review feedback is durable: leave a GitHub Review (not a chat-only summary).
  • Ownership + follow-through: if you request changes, track the re-review and resolve threads when addressed.
  • Scope control: don’t expand requirements; propose follow-ups for larger work.

Workflow

  1. Orient + sanity check

    • Read the PR title/body and any linked Linear issue.
    • Read all comments and past PR reviews (including review comments) on the PR.
    • Check ownership expectations via CODEOWNERS (don’t override owners; align with them).
    • If the PR is huge/messy, consider recommending a clean replacement PR (don’t bikeshed the current diff).
  2. Review the diff with repo guardrails in mind

    • Validate correctness, types, tests, and operational risk.
    • Use the repo’s canonical coding guardrails (especially around TypeScript safety).
  3. Leave one structured GitHub Review

Use this exact structure in the review summary:

Blocking feedback

**Non-blocking feedback**

Rules:

  • Use numbered lists.
  • Do not restart numbering for non-blocking feedback; continue from the Blocking feedback section.
  • Each item should be:
    • a short, clear claim (what’s wrong / what to change),
    • why it matters (correctness, safety, maintainability, performance, etc),
    • the smallest reasonable fix (or a concrete suggestion).
  1. Approve / request changes
  • If there are no Blocking feedback items and you are not the PR author, Approve.
  • If there are Blocking feedback items, Request changes.
  • If you are the author, do not approve your own PR; leave a comment/review without approval.
  1. Follow-through after updates
  • When Blocking feedback items are addressed:
    • Resolve the corresponding GitHub review threads, and
    • leave a short note like: “Resolved in <commit-sha>.”
  • If you previously requested changes, convert to Approve once Blocking feedback items are resolved.

Copy link
Contributor Author

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

Blocking feedback

  1. packages/backend-contract build and typecheck are now order-dependent on prebuilt @rybosome/tspice-core declarations. Why this matters: from a clean checkout, pnpm -C packages/backend-contract build and pnpm -C packages/backend-contract typecheck fail with TS2307 until packages/core has already been built, which makes package-level validation brittle and non-deterministic. Smallest fix: make backend-contract scripts dependency-aware (prefer tsc -b to honor project references, or prebuild packages/core inside those scripts).
**Non-blocking feedback**
  1. AssertSpiceInt32Options is still declared/exported from @rybosome/tspice-backend-contract even though runtime SpiceInt helpers moved to @rybosome/tspice-core. Why this matters: duplicate type ownership can drift and leaves consumers with an ambiguous source for runtime-helper-adjacent types. Smallest fix: re-export/type-alias this from @rybosome/tspice-core (or remove the contract export if compatibility allows).
  2. createSpiceHandleRegistry().__entries() still returns live SpiceHandleEntry object references. Why this matters: internal/debug consumers can mutate returned objects and accidentally violate registry invariants. Smallest fix: return defensive read-only snapshots from __entries() (for example, clone each entry before returning).

@charliecreates
Copy link
Contributor Author

Completed final durable review (comment fallback): review.

@rybosome
Copy link
Owner

Blocking feedback

  1. packages/backend-contract build and typecheck are now order-dependent on prebuilt @rybosome/tspice-core declarations. Why this matters: from a clean checkout, pnpm -C packages/backend-contract build and pnpm -C packages/backend-contract typecheck fail with TS2307 until packages/core has already been built, which makes package-level validation brittle and non-deterministic. Smallest fix: make backend-contract scripts dependency-aware (prefer tsc -b to honor project references, or prebuild packages/core inside those scripts).
**Non-blocking feedback**
  1. AssertSpiceInt32Options is still declared/exported from @rybosome/tspice-backend-contract even though runtime SpiceInt helpers moved to @rybosome/tspice-core. Why this matters: duplicate type ownership can drift and leaves consumers with an ambiguous source for runtime-helper-adjacent types. Smallest fix: re-export/type-alias this from @rybosome/tspice-core (or remove the contract export if compatibility allows).
  2. createSpiceHandleRegistry().__entries() still returns live SpiceHandleEntry object references. Why this matters: internal/debug consumers can mutate returned objects and accidentally violate registry invariants. Smallest fix: return defensive read-only snapshots from __entries() (for example, clone each entry before returning).

@CharlieHelps Please address all of these issues within this PR

@charliecreates
Copy link
Contributor Author

Implemented — I addressed all three items in ff05ef3.

  1. Blocking (backend-contract order dependence):

    • Made packages/backend-contract scripts dependency-aware by adding prebuild and pretypecheck to build ../core first.
    • pnpm -C packages/backend-contract build and pnpm -C packages/backend-contract typecheck now pass from a clean package state without manually prebuilding core.
  2. AssertSpiceInt32Options ownership:

    • Removed duplicate local ownership by switching packages/backend-contract/src/shared/spice-int.ts to re-export AssertSpiceInt32Options and related SPICE_INT32_* symbols from @rybosome/tspice-core, so core is the runtime-helper-adjacent source of truth.
  3. createSpiceHandleRegistry().__entries() mutability:

    • Updated __entries() to return cloned, frozen snapshots instead of live map value references.
    • Added a core test that verifies mutating a returned entry does not affect registry internal state.

Validation run (all passing):

  • pnpm -C packages/backend-contract build
  • pnpm -C packages/backend-contract typecheck
  • pnpm -C packages/backend-contract test
  • pnpm -C packages/core build
  • pnpm -C packages/core typecheck
  • pnpm -C packages/core test
  • pnpm -C packages/backend-node typecheck
  • pnpm -C packages/backend-wasm typecheck

If you want, I can also run the remaining package typechecks from the PR checklist before merge.

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