Sc

Operations verticalVertical
@wabbit/tome-scv0.4.13

Tome Star Citizen wrapper — fleet management, ship database, RSI integration, SC locations, gameplay activity taxonomy, operation/force templates, squadrons, task force coordination, and military rank presets over @wabbit/tome-org. Bundles the SC block pack, Signal editorial theme, and the COP military design-token theme.

Install
  1. Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.

  2. Add the registry and your token to the .npmrc at the root of your project, with your token in place of YOUR_TOKEN:

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
  3. Then install:

    npm install @wabbit/tome-sc

Overview

@wabbit/tome-sc

The Star Citizen wrapper over Tome — the one package an SC org installs to make Tome Star-Citizen-appropriate.

What this is

Two deliverables in one package:

  1. The SC data layer — 12 collections extending @wabbit/tome-org: fleet management (Ships, Fleet, FleetLogs, AssetAvailability, ResourceRequests), SC operations (Locations, GameplayActivities, OperationTemplates, ForceTemplates), and military coordination (Squadrons, TaskForces, TaskForceAssignments). Plus with* extension functions that inject SC fields onto org's Member/Event/Membership/Rank collections, RSI hooks, and military rank presets.
  2. The SC bundle — tome-sc hard-includes @wabbit/tome-blocks-sc-pack (fleet/task-force/RSI display blocks), @wabbit/tome-blocks-signal-theme (30+ Signal editorial blocks), and @wabbit/tome-cop (the military/ops design-token theme) as real dependencies. Installing @wabbit/tome-sc pulls the whole SC-appropriate platform in one shot. The underlying packages remain individually consumable — this is a wrapper, not a fusion (same precedent as the @wabbit/tome-blocks meta-package).

Why hard dependencies when no code here imports them? Nothing in src/ imports any of the three packs; they are dependencies purely so that one install gives an SC org everything it needs, at versions released together. The trade-off is deliberate: a site that wants only the SC data layer still downloads the packs (they are inert until you register them), and package.json dependencies cannot be opted out of — the blocks flags below only record intent. If the download matters to you, install the packs you want directly and use this package's collections through createSCLayer.

createSCPlatform(config) is the one-call composition surface. createSCLayer(config) is the SC-collections-only convenience function for sites doing manual composition.

Peer dependencies

| Peer | Range | Required | |---|---|---| | payload | >=3.67.0 | yes | | @payloadcms/richtext-lexical | >=3.67.0 | yes | | @wabbit/tome-core | >=1.17.0 <2.0.0 | yes | | @wabbit/tome-org | >=0.3.0 <1.0.0 | yes | | @wabbit/tome-lms | >=0.9.0 <1.0.0 | no (optional) — only for the certificationsSlug relationship on ForceTemplates; omit that option and the field is left out. No runtime import | | @wabbit/tome-workflow | >=0.1.0 <1.0.0 | no (optional) — import type only; see the registered-assets section |

The three bundled block packs (@wabbit/tome-blocks-sc-pack, @wabbit/tome-blocks-signal-theme, @wabbit/tome-cop) are real dependencies, not peers — see Bundled packs below for the opt-out.

Quickstart — createSCPlatform

// payload.config.ts
import { buildConfig } from 'payload'
import { createSCPlatform } from '@wabbit/tome-sc'

const sc = createSCPlatform({
  organization: 'Example Org',
  terminology: 'milsim',            // preset: Wing/Unit/Group/Billet; or pass a full/partial OrgTerminology
  mediaCollection: 'media',
  blocks: { scPack: true, signalTheme: true },   // bundled packs — both default true, opt out per pack
  rsi: { validateHandle: true, syncOrgAffiliation: true },
})

export default buildConfig({
  collections: sc.collections,     // org's 13 collections (extended) + tome-sc's 12 SC collections
})

createSCPlatform composes, in order:

  1. createOrgLayer — SC terminology preset (or your own OrgTerminology override) + orgSlugs overrides.
  2. applyExtensions — wraps org's output by slug (the only supported seam): Members get withRSIFields + the opt-in RSI hooks (rsi.validateHandle / rsi.syncOrgAffiliation); Events get withFleetFields; Memberships get withSquadronMembership then withSquadronMemberCountSync (see "Rank presets & the squadron memberCount fix" below); Ranks get withSCRankPresets.
  3. createSCLayer — the 12 SC collections, with memberSlug/eventSlug/teamsSlug/divisionsSlug/campaignSlug resolved from org's actual (possibly overridden) slugs — never a hardcoded 'org-members' guess.
  4. registerScPermissions() (via createSCLayer, redundant-safe against the module-import self-registration).
  5. registerScGdpr() (via createSCLayer) — registers this package's GDPR redaction dispositions by default. Pass registerGdpr: false if your site owns those dispositions itself.
  6. registerLayer('@wabbit/tome-sc', …) — the layer-registry entry (admin sidebar group 'Organization' unless you pass adminGroup).

Returns exactly { collections: CollectionConfig[] }. createSCLayer(config?) returns the 12 SC collections as a plain CollectionConfig[], and createOrgLayer (from @wabbit/tome-org) likewise returns an array, which is what applyExtensions takes below.

Advanced path — manual composition

What createSCPlatform does internally, spelled out, for sites that need to interleave other layers or override individual pieces:

import { createOrgLayer, ORG_DEFAULT_SLUGS } from '@wabbit/tome-org'
import {
  createSCLayer,
  applyExtensions,
  withRSIFields,
  withFleetFields,
  withSquadronMembership,
  withSquadronMemberCountSync,
  withSCRankPresets,
} from '@wabbit/tome-sc'

All of these resolve from the root barrel, but not from the same internal module: withRSIFields/withFleetFields/withSquadronMembership/withSCRankPresets are defined under src/extensions/ and re-exported via export { ... } from './extensions'. withSquadronMemberCountSync is different — it's defined in src/collections/military/squadrons.ts (the Squadron collection factory's own module) and reaches the root barrel through export * from './collections', not through ./extensions. The import path from @wabbit/tome-sc is identical either way; only the internal source module differs.

const orgCollections = createOrgLayer({
  terminology: {
    division: { singular: 'Wing', plural: 'Wings' },
    team: { singular: 'Unit', plural: 'Units' },
    squad: { singular: 'Group', plural: 'Groups' },
    position: { singular: 'Billet', plural: 'Billets' },
    organization: 'Example Org',
  },
})

const extendedOrg = applyExtensions(orgCollections, {
  [ORG_DEFAULT_SLUGS.members]: withRSIFields,
  [ORG_DEFAULT_SLUGS.events]: withFleetFields,
  [ORG_DEFAULT_SLUGS.memberships]: (c) => withSquadronMemberCountSync(withSquadronMembership(c)),
  [ORG_DEFAULT_SLUGS.ranks]: withSCRankPresets,
})

const scCollections = createSCLayer({
  memberSlug: ORG_DEFAULT_SLUGS.members,   // 'org-members' — verified against packages/org/src/types.ts, not assumed
  eventSlug: ORG_DEFAULT_SLUGS.events,     // 'org-events'
  mediaCollection: 'media',
})

export default buildConfig({
  collections: [...extendedOrg, ...scCollections],
})

Rank presets & the squadron memberCount fix — two documented call-order flips

Rank presets — collection-first, not config-first. Wrapping the INPUT config (createRankCollection(withSCRankPresets({ slug: 'ranks' }))) is a no-op: RankCollectionConfig has no category-options extension point (createRankCollection hardcodes its own generic options). The correct, as-shipped order wraps the factory's OUTPUT instead:

withSCRankPresets(createRankCollection({ slug: 'ranks' }))

createSCPlatform already does this correctly via applyExtensions's [ORG_DEFAULT_SLUGS.ranks]: withSCRankPresets entry — this note is for anyone composing manually against a bare createRankCollection() call outside createOrgLayer.

Squadron memberCount — `withSquadronMemberCountSync` must wrap `withSquadronMembership`, not stand alone. withSquadronMembership alone only injects the squadron relationship field onto org's Membership collection — it wires no hooks. Without withSquadronMemberCountSync layered on top, every Squadron's memberCount field stays permanently 0 (the field exists, nothing ever updates it). Always compose them together:

withSquadronMemberCountSync(withSquadronMembership(membershipCollection))

createSCPlatform does this automatically.

Co-installation note — @wabbit/tome-accounts

createAccountsLayer defaults memberSlug: 'members', while org (and therefore tome-sc) defaults 'org-members'. A site running org + accounts + sc must pass memberSlug: 'org-members' (or its site's own override) into createAccountsLayer() explicitly, or accounts' Membership relationships point at a nonexistent collection. tome-sc itself always takes this slug via config (see orgSlugs.members above) — this warning is about wiring a third layer alongside it correctly.

Bundled packs

Real dependencies (not peers) — installing @wabbit/tome-sc installs all three:

| Package | Role | Opt-out | |---|---|---| | @wabbit/tome-blocks-sc-pack | 5 SC display blocks: fleet-summary, signal-hero-sc, task-force-roster, op-briefing-panel, rsi-handle-card. Self-contained data — zero dependency on this package's collections. | blocks: { scPack: false } | | @wabbit/tome-blocks-signal-theme | 23 offered Signal editorial blocks (33 total registered) — accordion, callout, data table, personnel/ship cards, threat panels, and more. Reclassified SC-tier under this umbrella; independently installable on any editorial site. | blocks: { signalTheme: false } | | @wabbit/tome-cop | Military/ops design-token theme — the SC default aesthetic. Imported by the starter template. | Not gated by createSCPlatform's blocks config — import (or skip) it directly in your theme setup. |

Block registration is a consumer-side wiring step, not a plugin side-effect

createSCPlatform's blocks config ({ scPack: true, signalTheme: true }, both default true) is data describing intent — it does not call any pack's register() function for you. It hands the flags back, defaults applied, as sc.blocks (type SCBlockPacks); the opt-outs only take effect if your registration code reads them, as below. Payload registers block DEFINITIONS via block lists on a blocks-type field; there is no plugin hook where createSCPlatform could silently mutate a shared registry without your knowledge. Wire the packs yourself, same as any @wabbit/tome-blocks-* consumer:

import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register as registerScPack } from '@wabbit/tome-blocks-sc-pack'
import { register as registerSignalTheme } from '@wabbit/tome-blocks-signal-theme'
import { createSCPlatform } from '@wabbit/tome-sc'

const sc = createSCPlatform({ blocks: { signalTheme: false } })

const blockRegistry = new BlockRegistry()
const bundleRegistry = new BundleRegistry()
if (sc.blocks.scPack) registerScPack(blockRegistry, bundleRegistry)
if (sc.blocks.signalTheme) registerSignalTheme(blockRegistry, bundleRegistry)

// then reference blockRegistry.resolveAll() from whichever collection's
// `blocks`-type field should offer them (e.g. a Pages collection's `layout` field)

RSI integration — hook-based, opt-in, no official API

Nothing calls any RSI-adjacent service unless a site explicitly enables it via SCIntegrationConfig.rsi. There is no official, stable RSI API. Every integration this package supports is one of:

  • Handle validation — a plain existence check (HTTP 200) against RSI's own public citizen dossier page (https://robertsspaceindustries.com/citizens/{handle}). This confirms the handle is registered, not that the caller owns it.
  • Ship catalog data — pulled from a third-party, RSI-unaffiliated community API (star-citizen.wiki), because RSI itself has never published a ship-catalog API.

@wabbit/tome-sc/rsi exports RSIClient, a thin, resilient wrapper over both surfaces (never throws — network/parse failures degrade to a typed empty/false result). The subpath is where you import RSIClient and the adapter yourself; it is not a way to keep the RSI code out of your bundle. The root barrel's RSI hooks (validateRSIHandle, syncMemberOrg, createSyncShipDatabase) import RSIClient statically, and createSCPlatform imports those hooks, so the client module is always loaded. It makes no network call unless you enable rsi config. Every hook (validateRSIHandle, syncMemberOrg, createSyncShipDatabase) also accepts a custom function (validateHandle, syncFn) so a site can swap in its own implementation — or a future official RSI API — without touching call sites.

createSCPlatform's rsi.validateHandle and rsi.syncOrgAffiliation are wired onto the Members collection automatically. `rsi.syncShips` is accepted in `SCPlatformConfig` for parity with the rest of its config shape but is NOT auto-wired by `createSCPlatform` — a scheduled ship-sync is a Payload Endpoint + external cron trigger, not a CollectionConfig, so it doesn't fit this function's contracted { collections, blocks } return shape. Wire it yourself with createSyncShipsTask from ./tasks (snippet at the end of the next section).

RSIProfileAdapter — the ONE place RSI's markup regexes live

RSIClient.validateHandle is now backed internally by RSIProfileAdapter (@wabbit/tome-sc/rsi's createRSIProfileAdapter), the concrete ExternalProfileAdapter<RSIProfile> implementation for @wabbit/tome-core/verification's neutral verification/cache/reconcile machinery (resilient fetch with retry/backoff, an optional distributed rate limiter). Every RSI-specific regex — the citizen dossier's class="citizen-record" / class="entry bio" / org-affiliation markup — lives in src/rsi/adapter.ts and nowhere else in this package. Ported field-for-field from a production consumer's scrapeRsiDossier.ts / parseRsiHandle.ts, producing the same extracted values against the same fixture HTML.

import { createRSIProfileAdapter } from '@wabbit/tome-sc/rsi'
import { createDistributedRateLimiter, createInMemoryLimiterClient } from '@wabbit/tome-core/verification'

const adapter = createRSIProfileAdapter({
  rateLimiter: createDistributedRateLimiter({
    key: 'rsi-scrape',
    ttlMs: 5_000,
    pollMs: 250,
    timeoutMs: 30_000,
    client: createInMemoryLimiterClient(), // swap for a real Redis client in production
  }),
})

const identifier = adapter.parseIdentifier('https://robertsspaceindustries.com/citizens/SomeHandle')
if (identifier) {
  const result = await adapter.fetchProfile(identifier)
  // result.status: 'success' | 'not_found' | 'hidden' | 'error'
}

Deliberate divergences from that original scraper, all covered by tests in tests/rsi/rsiProfileAdapter.test.ts:

  • Fail-CLOSED `exists()`/`checkExistence()`. The original verifyRSIHandle returns true on ANY thrown network error ("allow the application to proceed for manual review"). This adapter returns false — a network failure is not evidence a handle exists. checkExistence exposes the structured reason ('confirmed' | 'not_found' | 'http_error' | 'network_error' | 'rate_limited') behind the boolean, since the ExternalProfileAdapter contract's exists() only has room for Promise<boolean>.
  • 404 is never retried and never mislabelled. The original scraper retries a stable 404 like any transient failure (wasting its whole retry budget) and then surfaces it as "RSI website unreachable: HTTP 404: Not Found". This adapter's fetchProfile maps a 404 straight to status: 'not_found' on the first attempt.
  • No `E2E_TEST_` bypass. The original verifyRSIHandle has a hardcoded handle-prefix bypass reachable in production (not gated on NODE_ENV or a flag). That was a consumer-side test hook — it does not move upstream. Inject fetchImpl for tests instead.
  • `visibility` detection is new. The original has no automated hidden/redacted detection at all (it relies on a manual admin override field). This adapter detects a fully private profile structurally (total absence of citizen-record/enlistment/avatar markup on an HTTP 200) and a redacted org affiliation from RSI's literal "REDACTED" org-name text. Both map to fetchProfile's status: 'hidden'; the distinction lives on profile.visibility ('public' | 'hidden' | 'redacted').

RSIClient.validateHandle keeps its original {valid, orgTag?} return shape and pass/fail semantics, but now gets the adapter's retry + optional rate-limiting for free (previously a single unretried fetch), and widens accepted input to the same URL forms parseIdentifier accepts (previously bare handles only — a superset, not a narrowing).

import { createSyncShipsTask } from '@wabbit/tome-sc/tasks'

const { endpoint } = createSyncShipsTask({ shipsSlug: 'ships', schedule: '0 4 * * *' })

export default buildConfig({
  collections: sc.collections,
  endpoints: [endpoint],   // CRON_SECRET-guarded — point your external scheduler (Vercel Cron, systemd timer, etc.) at it
})

Access control — v1 posture (honest, not redesigned)

tome-sc's access model is permission-key-only — there is no raw admin-role concept (checkScRole), matching org's own checkOrgRole convention. SC_PERMISSIONS mints three new permissions (MANAGE_FLEET, MANAGE_LOCATIONS, MANAGE_REGISTERED_ASSETS — the third added in 0.4.0 for the registered-assets sub-cluster) and consumes three of org's existing keys (MANAGE_STRUCTURE, MANAGE_LOGISTICS, MANAGE_TEMPLATES). Three deliberate v1 simplifications, carried over from the collection factories' own header comments rather than silently dropped:

  • Fleet cluster collapses "admin"/"super-admin" onto `MANAGE_FLEET`. SC_SUPER_PERMISSIONS ships empty at v1 (no EDIT_FLEET/DELETE_FLEET split exists yet), so every Fleet/FleetLogs/Ships write gates on the single MANAGE_FLEET key regardless of how prose elsewhere distinguishes "admin" from "super-admin." Trigger to refine: when SC_SUPER_PERMISSIONS gains a dedicated split.
  • Operations cluster drops a leadership-profile bypass some consumers implement. One production consumer's app-side data-access layer grants division/team LEADERS visibility into out-of-scope OperationTemplates/ForceTemplates even without a direct membership record. tome-sc has no DAL layer and no Position/billet leadership resolution of its own, so OperationTemplateCollectionConfig/ForceTemplateCollectionConfig's visibility Where clause omits that bypass. Sites that need it supply their own site-level access override on top.
  • Military cluster allows member self-service on TaskForceAssignments. The assigned member can accept/decline/leave their own assignment record directly — this is a direct corollary of the "supports invitation workflow (pending → active/declined)" contract, not an added ABAC extension. Broader leadership-hierarchy checks (a canManageTaskForceAssignment-style function) are not ported; access beyond self-service and the task force's coordinator/deputy is MANAGE_STRUCTURE-gated only.

No consumer's billet/unit/wing leadership-hierarchy ABAC model is ported anywhere in this package (squadrons, task forces, or templates) — every "admin" resolves to a permission-key check, and every consumer-specific leadership bypass is either dropped with a documented reason or left as an explicit extension point for a site's own access override.

registered-assets sub-cluster

The registered-assets sub-cluster ports a production consumer's commissioned-vessels feature (CommissionedShips / ShipAssignments / ShipTransferRequests) under a neutral name. It is NOT part of createSCLayer/createSCPlatform's fixed collection tuple — same posture shipsCollection/certificationsSlug have held for years — a consumer composes it explicitly:

import {
  createRegisteredAssetCollection,
  createAssetAssignmentCollection,
  createAssetTransferRequestCollection,
  registerRegisteredAssetsGdpr,
} from '@wabbit/tome-sc'

const registeredAssets = createRegisteredAssetCollection({
  parentScopeSlug: 'units',      // example: a Team-equivalent slug
  ancestorScopeSlug: 'wings',    // example: a Division-equivalent slug
  linkedAssetSlug: 'fleet',      // OPTIONAL — omit for no per-member-asset link
  assignmentsSlug: 'asset-assignments', // wires the auto-close-on-terminal-status hook
})

const assetAssignments = createAssetAssignmentCollection({
  assetSlug: registeredAssets.slug,
  // participatesInPositionCascade defaults to false (Option C) — see the
  // factory's header before ever setting this true.
})

const assetTransferRequests = createAssetTransferRequestCollection({
  parentScopeSlug: 'units',
  ancestorScopeSlug: 'wings',
  resolveLeadership: async (req) => resolveMyOwnLeadershipProfile(req), // injectable
})

registerRegisteredAssetsGdpr() // registered-assets, asset-assignments, asset-transfer-requests

Option C (asset-assignments.officerPosition): a vessel officer billet's holder-of-record lives on the ASSIGNMENT, not on the referenced Position document's currentHolder — deliberately outside @wabbit/tome-org's org-wide 1-to-1 position cascade. participatesInPositionCascade defaults to false. Flipping it to true re-arms that cascade and introduces a real hazard: two independent writers (org's normal Position-assignment flow, and this collection's own sync hook) can now target the same Position's currentHolder with no cross-collection concurrency check — one write silently disappears with no error. See createOfficerPositionSyncHook's doc comment before enabling it.

`@wabbit/tome-workflow` relationship: packages/sc itself has no runtime dependency on @wabbit/tome-workflow — it's an OPTIONAL peer, referenced only via import type for assetTransferTopology's return shape (SingleApproverTopology). This is despite at least one production consumer's own cluster having a genuine runtime dependency on it: that consumer's transfer-request workflow declares the topology for real and its apply-hook runs the settle/unwind write through @wabbit/tome-workflow/server's executeClaimed. assetTransferTopology() returns the same shape as that consumer's own workflow topology; createTransferApplyHook reimplements the same settle/unwind pattern by hand rather than calling executeClaimed (a documented open follow-up, not an oversight — see the factory's header). A consumer without @wabbit/tome-workflow installed still gets a plain, structurally-typed config object back from assetTransferTopology(); installing the package only sharpens the type.

GDPR dispositions (registerRegisteredAssetsGdpr): registered-assets.createdBy → retain (organisational record); asset-assignments.member → hard-delete (a service record scoped to one member's tenure, unlike Fleet/TaskForceAssignments' retain-because-soft-anonymise posture elsewhere in this package); asset-transfer-requests → redact (the transfer record itself is a structural audit trail that survives; only rationale/decisionNotes/lastExecutionError are cleared). Each of the three is independently skippable ({ registeredAssets: false } etc.) for a consumer with its own redact-engine ownership over one of them.

Package structure

./rsi (RSIClient, createRSIProfileAdapter) and ./tasks (createSyncShipsTask) are separate subpath exports — neither name is re-exported from the main barrel, mirroring how @wabbit/tome-core keeps its own scheduled-task exports (auth/jobs/*) off its main barrel. (The RSI client module is still loaded by the root barrel's hooks; see above.)

createSyncShipsTask(config?) returns { run, schedule, endpoint }. The endpoint (default path /jobs/sync-ships) answers 500 until the CRON_SECRET environment variable is set, and rejects any request that does not present it.

Also on the root barrel (not covered above): SC_DEFAULT_SLUGS; permission keys and helpers (SC_PERMISSIONS, SC_SUPER_PERMISSIONS, SC_CONSUMED_ORG_PERMISSIONS, registerScPermissions, buildScSuperPermissionMap, SC_OVERRIDABLE_PERMISSIONS, hasScPermission, hasAnyScPermission, hasScPermissionAsync); GDPR registration (registerScGdpr, unregisterScGdpr); reference data (SC_MANUFACTURERS, SC_RANK_CATEGORIES, createSCRankSeedData); mergeHooks; the terminology presets (SC_TERMINOLOGY_PRESETS, resolveScTerminology); every collection factory under src/collections; and every hook under src/hooks.

Server / client posture

No React components ship from this package. Everything is Payload config, hooks and server helpers; ./tasks builds a Payload Endpoint. Do not import any of it into a client bundle.

Testing

pnpm --filter @wabbit/tome-sc test runs the Vitest suite in tests/ (RSI adapter fixtures, the registered-assets cluster, GDPR registration, fleet helpers, layer version). The RSI tests inject fetchImpl, so they make no network calls.

What this package does not cover

Frontend components (ship cards, fleet dashboards, force-composition builder UI), a Discord bot bridge, a Spectrum integration, in-game economy tracking, and the Signal block definitions themselves (they live in @wabbit/tome-blocks-signal-theme) are all out of scope. A production consumer's own org-owned CommissionedShips/ShipAssignments/ShipTransferRequests cluster is now covered by the registered-assets sub-cluster above — TaskForces' optional shipsCollection config knob predates it and still points at the SC ship-class registry (ships), not registered-assets.

Exports

  • @wabbit/tome-sc
  • @wabbit/tome-sc/rsi
  • @wabbit/tome-sc/tasks

Changelog

v0.4.13patch

@wabbit/tome-blocks-sc-pack@0.28.5

  • @wabbit/tome-blocks-sc-pack@0.28.5
  • @wabbit/tome-blocks-signal-theme@0.35.0
  • @wabbit/tome-cop@0.2.2
v0.4.12patch

Updated dependencies [b8ccf85] - @wabbit/tome-blocks-signal-theme@0.31.0 - @wabbit/tome-blocks-sc-pack@0.28.5 - @wabbit/tome-cop@0.2.2

  • Updated dependencies [b8ccf85] - @wabbit/tome-blocks-signal-theme@0.31.0 - @wabbit/tome-blocks-sc-pack@0.28.5 - @wabbit/tome-cop@0.2.2
v0.4.11patch

775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
  • Updated dependencies [775f90a] - @wabbit/tome-blocks-sc-pack@0.28.5 - @wabbit/tome-blocks-signal-theme@0.28.5 - @wabbit/tome-cop@0.2.2
v0.4.10patch

Updated dependencies [d08fc38]

  • Updated dependencies [d08fc38]
  • Updated dependencies [58655f4] - @wabbit/tome-blocks-signal-theme@0.28.0 - @wabbit/tome-blocks-sc-pack@0.19.0
v0.4.9patch

Updated dependencies [c14a133] - @wabbit/tome-blocks-signal-theme@0.27.0 - @wabbit/tome-blocks-sc-pack@0.19.0 - @wabbit/tome-cop@0.2.1

  • Updated dependencies [c14a133] - @wabbit/tome-blocks-signal-theme@0.27.0 - @wabbit/tome-blocks-sc-pack@0.19.0 - @wabbit/tome-cop@0.2.1
v0.4.8patch

@wabbit/tome-blocks-sc-pack@0.19.0

  • @wabbit/tome-blocks-sc-pack@0.19.0
  • @wabbit/tome-blocks-signal-theme@0.26.0
  • @wabbit/tome-cop@0.2.1
v0.4.7patch

Updated dependencies - @wabbit/tome-blocks-signal-theme@0.23.0

  • Updated dependencies - @wabbit/tome-blocks-signal-theme@0.23.0
v0.4.6patch

Updated dependencies [d5d72e1]

  • Updated dependencies [d5d72e1]
  • Updated dependencies [9ac8d3d] - @wabbit/tome-cop@0.2.0 - @wabbit/tome-blocks-signal-theme@0.20.0 - @wabbit/tome-blocks-sc-pack@0.19.0
v0.4.5patch

Updated dependencies [f6cfffa]

  • Updated dependencies [f6cfffa]
  • Updated dependencies [89a8338] - @wabbit/tome-blocks-sc-pack@0.19.0
v0.4.4patch

fcf6d33: `createSCPlatform` now returns the resolved `blocks` flags so bundled-pack opt-outs can take effect. `createSCPlatform` now returns the resolved `blocks` flags (`{ scPack, signalTheme }`, both default `true`) alongside `collections`, and exports the `SCBlockPacks` type. The flags were previously accepted and never read anywhere, so opting out of a bundled pack did nothing; read `sc.blocks` in your block-registration code to honour them. Additive — `sc.collections` is unchanged.

  • fcf6d33: `createSCPlatform` now returns the resolved `blocks` flags so bundled-pack opt-outs can take effect. `createSCPlatform` now returns the resolved `blocks` flags (`{ scPack, signalTheme }`, both default `true`) alongside `collections`, and exports the `SCBlockPacks` type. The flags were previously accepted and never read anywhere, so opting out of a bundled pack did nothing; read `sc.blocks` in your block-registration code to honour them. Additive — `sc.collections` is unchanged.
  • 3ca8002: Task-force member counts are recalculated with `payload.count()` instead of reading every active assignment. No API or behaviour change. Internal: collection slugs are typed through the shared `typedSlug()` helper instead of inline casts.
  • 0bd7c3f: customer-facing wording: internal references removed from admin descriptions, error messages and block metadata.
  • Updated dependencies [89161af]
  • Updated dependencies [0aa80a3] - @wabbit/tome-blocks-signal-theme@0.18.1 - @wabbit/tome-cop@0.1.9 - @wabbit/tome-blocks-sc-pack@0.18.0
v0.4.3patch

67eb3dc: Local relationship-id helpers are replaced by `@wabbit/tome-core/utilities/relationId`. Each call site maps to the core reader with the same return shape (raw vs. stringified id, `null` vs. `undefined`, polymorphic), so behaviour is unchanged. The `@wabbit/tome-core` peer floor goes up to `>=1.17.0` because that is the first core version exporting `relationIdRaw`, `relationIds` and `relationIdsRaw`.

  • 67eb3dc: Local relationship-id helpers are replaced by `@wabbit/tome-core/utilities/relationId`. Each call site maps to the core reader with the same return shape (raw vs. stringified id, `null` vs. `undefined`, polymorphic), so behaviour is unchanged. The `@wabbit/tome-core` peer floor goes up to `>=1.17.0` because that is the first core version exporting `relationIdRaw`, `relationIds` and `relationIdsRaw`.
  • Updated dependencies [c3468b0]
  • Updated dependencies [c3468b0] - @wabbit/tome-blocks-sc-pack@0.18.0 - @wabbit/tome-blocks-signal-theme@0.18.0 - @wabbit/tome-cop@0.1.8
v0.4.2patch

Updated dependencies [404d325] - @wabbit/tome-blocks-signal-theme@0.17.0 - @wabbit/tome-blocks-sc-pack@0.17.0 - @wabbit/tome-cop@0.1.8

  • Updated dependencies [404d325] - @wabbit/tome-blocks-signal-theme@0.17.0 - @wabbit/tome-blocks-sc-pack@0.17.0 - @wabbit/tome-cop@0.1.8
v0.4.1patch

ce3d12d: Adopt `@wabbit/tome-core/fields/address` and `@wabbit/tome-core/utilities/relationId` at the sites the audit counted (2026-09-01 sale-readiness audit §5.1, T3(g)). **No stored field name, and no emitted field array, changes anywhere in this changeset** — each adopter passes the vocabulary it already stores, and each ships a characterisation test that was written from the pre-change source, run green against the untouched factory, and run green again after. **Address group — five sites, one implementation.** - `@wabbit/tome-crm` — `accounts` and `contacts` each carried a byte-identical seven-field `address` group. Both now spread `postalAddressFields({ vocabulary: 'legacy-crm' })` after their own `name` line (`name` is the company/contact line, not a postal line). `tests/address-characterisation.test.ts` pins both groups whole. - `@wabbit/tome-deals` — `billingAddress` and `shippingAddress` inside the frozen Customer Snapshot were copies three and four. They now come from one `buildSnapshotAddressGroup` helper: `name` + `company` prepended locally, the six postal lines from core, and the eight per-field labels plus the `'US'` country default passed through core's `fieldOverrides` seam. The snapshot is a legal-offer record frozen after send, so a field-name change would orphan the address on every deal already sent; `tests/address-characterisation.test.ts` pins both groups and the fact that they differ only in the group label and the recipient line's label. - `@wabbit/tome-fulfillment` — the fifth copy, and the only one that validated `country`. Its postal lines stay FLAT at collection top level (they are stored columns with PII rows and a GDPR registration behind them), now via `postalAddressFields({ vocabulary: 'postal', required: true, validateCountry: true })`. The ISO-3166 validator and its uppercase-normalising hook moved into core verbatim; because a moved function is a new object, `tests/address-characterisation.test.ts` pins the whole top-level field ORDER plus the validator's and hook's BEHAVIOUR (accepts `US`, rejects `usa`, rewrites `' us '` to `'US'`), not their identity. **`relationId` — the four-return-types problem.** - `@wabbit/tome-lms` — twelve modules under `src/server` (`academy`, `catalog`, `certificates`, `course`, `dashboard`, `enrollment`, `grades`, `leaderboard`, `learnerShell`, `notes`, `profile`, `reviews`) carried a byte-identical `string | null` copy. They import `relationId` from core now. One behavioural difference, strictly an improvement: on a malformed populated doc (`{ id: null }`, `{ id: {} }`) the old copy returned the STRING `'null'` / `'[object Object]'` as an id; core returns `null`. `tests/relation-id-adoption.test.ts` pins the adoption itself, because adoption is the thing that decays — the July 2026 audit's finding, repeated verbatim in September, was "extraction keeps happening, adoption does not." **Not migrated, deliberately:** `src/guards`, `src/utilities/{grading,prerequisites,progress}.ts`, `src/hooks/**`, `src/server/mutations/helpers.ts` and `src/server/awardGate.ts` return `string | number` or `undefined`. Migrating those is a semantic change, not an import change, and belongs in a pass that owns their call sites. The new test names them as out of scope so the next reader does not have to re-derive why. - `@wabbit/tome-sc` — the registry sub-cluster's copy is gone; `collections/registry/shared.ts` re-exports core's `relationId`, keeping `extractId` as a local alias (the module is private to that sub-cluster). **This one WIDENS:** the sc copy returned `string | number`, so a populated doc's numeric id came through unstringified. It is now stringified, which makes `===` between two resolved ids agree — the behaviour every call site in the cluster already assumed. Ids handed back to `payload.find`/`update` are unaffected, since Payload accepts either form in a `where` clause. sc's 179 tests stay green. - `@wabbit/tome-crm` — the inline ternary in `integration/deals.ts` (`typeof oppRaw === 'object' ? oppRaw.id : oppRaw`) was the fifth shape and had the same numeric-id asymmetry; it is one `relationId(deal.opportunity)` call now. **`fetchMemberId` ×4 — one implementation (sc).** `asset-availability`, `fleet-logs` and `fleet` each carried a verbatim copy of the auth-user → Member-row lookup, and `resource-requests` carried its projecting twin. All four now import from `src/access/fetchMemberId.ts`, which documents why each query knob is load-bearing: `overrideAccess: true` (the member collection's own read access may itself depend on membership, so without the bypass this is a circular check that denies the owner their own row), `depth: 0`, `pagination: false`. The id is returned in its STORED type here rather than through `relationId` — this is an identity read fed straight back into a `where` clause, not a relationship read. `tests/fleet-shared-helpers.test.ts` pins the adoption, the three knobs, and the null-for-anonymous contract.

  • ce3d12d: Adopt `@wabbit/tome-core/fields/address` and `@wabbit/tome-core/utilities/relationId` at the sites the audit counted (2026-09-01 sale-readiness audit §5.1, T3(g)). **No stored field name, and no emitted field array, changes anywhere in this changeset** — each adopter passes the vocabulary it already stores, and each ships a characterisation test that was written from the pre-change source, run green against the untouched factory, and run green again after. **Address group — five sites, one implementation.** - `@wabbit/tome-crm` — `accounts` and `contacts` each carried a byte-identical seven-field `address` group. Both now spread `postalAddressFields({ vocabulary: 'legacy-crm' })` after their own `name` line (`name` is the company/contact line, not a postal line). `tests/address-characterisation.test.ts` pins both groups whole. - `@wabbit/tome-deals` — `billingAddress` and `shippingAddress` inside the frozen Customer Snapshot were copies three and four. They now come from one `buildSnapshotAddressGroup` helper: `name` + `company` prepended locally, the six postal lines from core, and the eight per-field labels plus the `'US'` country default passed through core's `fieldOverrides` seam. The snapshot is a legal-offer record frozen after send, so a field-name change would orphan the address on every deal already sent; `tests/address-characterisation.test.ts` pins both groups and the fact that they differ only in the group label and the recipient line's label. - `@wabbit/tome-fulfillment` — the fifth copy, and the only one that validated `country`. Its postal lines stay FLAT at collection top level (they are stored columns with PII rows and a GDPR registration behind them), now via `postalAddressFields({ vocabulary: 'postal', required: true, validateCountry: true })`. The ISO-3166 validator and its uppercase-normalising hook moved into core verbatim; because a moved function is a new object, `tests/address-characterisation.test.ts` pins the whole top-level field ORDER plus the validator's and hook's BEHAVIOUR (accepts `US`, rejects `usa`, rewrites `' us '` to `'US'`), not their identity. **`relationId` — the four-return-types problem.** - `@wabbit/tome-lms` — twelve modules under `src/server` (`academy`, `catalog`, `certificates`, `course`, `dashboard`, `enrollment`, `grades`, `leaderboard`, `learnerShell`, `notes`, `profile`, `reviews`) carried a byte-identical `string | null` copy. They import `relationId` from core now. One behavioural difference, strictly an improvement: on a malformed populated doc (`{ id: null }`, `{ id: {} }`) the old copy returned the STRING `'null'` / `'[object Object]'` as an id; core returns `null`. `tests/relation-id-adoption.test.ts` pins the adoption itself, because adoption is the thing that decays — the July 2026 audit's finding, repeated verbatim in September, was "extraction keeps happening, adoption does not." **Not migrated, deliberately:** `src/guards`, `src/utilities/{grading,prerequisites,progress}.ts`, `src/hooks/**`, `src/server/mutations/helpers.ts` and `src/server/awardGate.ts` return `string | number` or `undefined`. Migrating those is a semantic change, not an import change, and belongs in a pass that owns their call sites. The new test names them as out of scope so the next reader does not have to re-derive why. - `@wabbit/tome-sc` — the registry sub-cluster's copy is gone; `collections/registry/shared.ts` re-exports core's `relationId`, keeping `extractId` as a local alias (the module is private to that sub-cluster). **This one WIDENS:** the sc copy returned `string | number`, so a populated doc's numeric id came through unstringified. It is now stringified, which makes `===` between two resolved ids agree — the behaviour every call site in the cluster already assumed. Ids handed back to `payload.find`/`update` are unaffected, since Payload accepts either form in a `where` clause. sc's 179 tests stay green. - `@wabbit/tome-crm` — the inline ternary in `integration/deals.ts` (`typeof oppRaw === 'object' ? oppRaw.id : oppRaw`) was the fifth shape and had the same numeric-id asymmetry; it is one `relationId(deal.opportunity)` call now. **`fetchMemberId` ×4 — one implementation (sc).** `asset-availability`, `fleet-logs` and `fleet` each carried a verbatim copy of the auth-user → Member-row lookup, and `resource-requests` carried its projecting twin. All four now import from `src/access/fetchMemberId.ts`, which documents why each query knob is load-bearing: `overrideAccess: true` (the member collection's own read access may itself depend on membership, so without the bypass this is a circular check that denies the owner their own row), `depth: 0`, `pagination: false`. The id is returned in its STORED type here rather than through `relationId` — this is an identity read fed straight back into a `where` clause, not a relationship read. `tests/fleet-shared-helpers.test.ts` pins the adoption, the three knobs, and the null-for-anonymous contract.
  • 8fc9702: Import `mergeHooks`, `fieldShape` and the select-option override contract from `@wabbit/tome-core` instead of keeping local copies (2026-09-01 sale-readiness audit §5.1, T3(a)). No API change: every symbol these packages exported before is still exported, now re-exported from core, and every factory produces byte-identical output. Deleted, with every call site repointed: - `org/src/hooks/mergeHooks.ts`, `lms/src/collections/shared/mergeHooks.ts`, `sc/src/extensions/mergeHooks.ts` → `@wabbit/tome-core/hooks/mergeHooks`. Core has exported this since July; sc's copy still carried a header claiming "Neither @wabbit/tome-core nor @wabbit/tome-org exports this." - `lms/src/server/jobs/paginate.ts`, `crowdfund/src/server/jobs/paginate.ts`, `workflow/src/server/paginate.ts` → `@wabbit/tome-core/jobs`. - `org/src/fieldShape.ts` + `org/src/insertFieldsAfter.ts`, `lms/src/collections/shared/fieldShape.ts` → `@wabbit/tome-core/fields/fieldShape`. Org's `resolveFieldDescription` / `FieldDescriptionOverride` were NOT part of the duplicated set and stay in the package, moved to `org/src/fieldDescriptions.ts`. - `org/src/optionOverrides.ts`, `lms/src/collections/shared/optionOverrides.ts` → `@wabbit/tome-core/fields/selectOptions`; `sc/src/collections/registry/shared.ts` now re-exports it (its `extractId` is a separate audit item and is untouched). **Peer floor raised to `@wabbit/tome-core` `>=1.14.0 <2.0.0`** in all five packages, because each now imports a subpath or a named export that first exists in that core minor: `./fields/fieldShape` and `./fields/selectOptions` are new subpaths, and `findPaged`/`chunk`/`readPositiveNumber` are new named exports on the pre-existing `./jobs`. Crowdfund's floor moves from `>=1.7.0` even though `./jobs` itself shipped in 1.7.0 — the subpath resolving is not the same thing as the export existing, which is the sharper version of the lesson its own 0.1.1 CHANGELOG records (`ERR_PACKAGE_PATH_NOT_EXPORTED`). Org moves from `>=1.2.0`, lms from `>=1.0.0`, sc from `>=1.11.0`, workflow from `>=1.7.0`. Header comments that pointed at the deleted files, or asserted core did not export these, were corrected rather than left dangling.
  • 4aeedad: One `LayerFactoryConfig` every layer factory's config extends, and one factory verb. Fourteen layer packages end in the same one call a consumer writes into `payload.config.ts`, and no two agreed on what `config` may contain: full seam vocabulary in three (org, lms, ledger), partial in six, NONE in six (2026-09-01 sale-readiness audit §5.3). A site that learned `adminGroup` from org and `hooks` from sc discovered, package by package, that six factories accept neither — not because the seam had been rejected, but because nothing said it existed. **New in core (a NEW exports-map subpath, hence the minor):** `@wabbit/tome-core/utilities/layerFactoryConfig` exports the `LayerFactoryConfig` interface — `adminGroup`, `access` (per-collection override map), `hooks` (appended via `mergeHooks`, never replacing), `extraFields`, `fieldOverrides`, `omitFields`, `fieldOrder`, `slugs` — and `applyLayerFactoryConfig(collections, config)`, which honours the whole vocabulary in one call and one fixed order (adminGroup → access → hooks → field shape, the last delegated to `fields/fieldShape`'s `applyFieldShape` so the order cannot drift between layers). Pure: new array, new objects, identity return on an empty config. It is a separate subpath from `./utilities/layerRegistry` deliberately — that module is in core's `sideEffects` array, and a pure type/vocabulary module should not drag a declared side-effecting module into every factory's type graph. The slug convention is documented rather than forced, because both live shapes are right for what they do: a typed `slugs?: Partial<XSlugs>` map for the slugs a layer OWNS (org, sc, accounts — the typed key set makes a typo a compile error, and a homomorphic mapped type satisfies the base's `Record<string, string | undefined>`), and named `<name>Slug?: string` scalars for relationship targets in OTHER layers (`memberSlug`, `mediaSlug`, `eventSlug`, `rolesSlug`) — those are pointers out of a layer, not entries in its key set. **Every `create*Layer` config now extends it.** Twelve extend `LayerFactoryConfig` directly and APPLY it through `applyLayerFactoryConfig` (accounts, catalog, crm, crowdfund, deals, fulfillment, lms, marketing, org, sc) or through a targeted application (chrome). Additive in every case: for the six that accepted none of the seams (deals, economy, gamification, marketing, plus forms/intake, see below), the fields are new; for the rest, `adminGroup` and friends keep their existing meaning and the applier is a no-op when they are omitted. Two packages accept the vocabulary but do NOT yet apply it, and say so in their type's JSDoc in the required form ("accepted, not yet applied — trigger: …"). **economy** and **gamification** both declare `@wabbit/tome-core` as an OPTIONAL peer and hold zero runtime imports of it — gamification reaches `registerLayer` through a lazy `require()` in a try/catch for exactly this reason. `applyLayerFactoryConfig` is a runtime VALUE, so importing it at module scope would convert an optional peer into a required one and break every site that installs those packages without core; copying the applier locally is barred by `assert:no-forked-primitives`. The trigger is stated: the day core becomes a required peer, delete the note and add one line. Both take the type via `import type`, which is erased at runtime. Two packages drop seams EXPLICITLY rather than accept-and-ignore. **chrome** extends `Omit<LayerFactoryConfig, 'access' | 'hooks' | 'extraFields' | 'fieldOverrides' | 'omitFields' | 'fieldOrder' | 'slugs'>` because it returns Payload GLOBALS, not collections — those seven are keyed by collection slug and typed against `CollectionConfig`, and chrome's slugs already have direct per-surface knobs (`header.slug`, `footer.slug`) a parallel map could contradict. The one seam it keeps, `adminGroup`, IS applied: globals carry `admin.group` exactly as collections do. **rpg** extends `Omit<LayerFactoryConfig, 'access'>` because `CharacterSheetsConfig` is a single collection's config that doubles as the layer factory's config, and its own `access` already means "this collection's access object" — one level shallower than the base's slug-keyed map. Two meanings under one name is the confusion this interface exists to end. **Factory-verb convergence.** Three verbs were live. `createWorkflowLayer(config?)` is new in `@wabbit/tome-workflow` (a new export — hence the minor) and returns a spreadable, deliberately EMPTY `CollectionConfig[]`: this layer is an engine, not a collection set, so the empty array is the honest answer and lets `...createWorkflowLayer()` compose exactly like every sibling. Its `WorkflowLayerConfig` omits every seam for the same reason, and exists as the stable place a real option will land. `createGamificationLayer` and `createRpgLayer` are pure aliases of `registerGamificationLayer` / `registerRpgLayer`. `initWorkflow`, `registerGamificationLayer` and `registerRpgLayer` are all `@deprecated` with sunset at each package's next major; none is removed. **Forcing function:** `scripts/assert-layer-factory-contract.mjs` + `pnpm assert:layer-factory-contract`, wired into `platform-discipline.yml` after `assert:layer-version` (source reading only, pre-build). Every exported `create*Layer` must take a config parameter whose type resolves to `LayerFactoryConfig` — through `extends`, an intersection, or an explicit `Omit<…>` — with verb aliases followed to their `register*`/`init*` target. Before this change it reported 12 violations and 0 conforming; it now reports 15 conforming, 0 violations. Deliberately NOT checked: whether a factory actually applies what it accepts, because a machine cannot tell a documented deferral from an accident, and a gate that forced silent application would be worse than one that forces a stated deferral. `docs/guides/create-a-new-layer-package.md` gains a "The factory contract" section stating the rule and the three permitted responses. Three factories are ALLOWLISTED with a reason each: `createAiLayer` returns credential wiring and owns no collections, so every seam is meaningless to it; `createFormsLayer` and `createIntakeLayer` are owned by the forms+intake access wave running in parallel, whose changes rewrite the same files. **Peer floors:** accounts, catalog, chrome, crm, deals, economy, fulfillment, gamification, marketing and rpg raise `@wabbit/tome-core` to `>=1.14.0 <2.0.0`. The new subpaths do not exist below that, and a too-low floor is how `ERR_PACKAGE_PATH_NOT_EXPORTED` reached crowdfund's consumers once already. These are marked `patch` because the config widening is purely additive; the raised required-peer floor is the reason a release manager may prefer to cut them as minors instead.
  • 73081e6: Manifest metadata: `homepage`, `bugs`, `engines`. All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change. - `homepage` deep-links to that package README on GitHub (`.../tree/main/packages/<dir>#readme`). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want. - `bugs.url` points at the repo issue tracker, so a paying customer has a place to report a defect that is not email. - `engines.node` is `>=22`, matching the root `engines` and `.nvmrc` set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for `node --test`, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version. The forcing function ships with the change: `scripts/assert-manifest-metadata.mjs` (root `pnpm assert:manifest-metadata`, wired into `platform-discipline.yml` beside `assert:license-metadata`) fails when any publishable manifest lacks `description`, `repository.directory` matching its own folder, `homepage`, `bugs`, `engines.node` equal to the repo floor, `license`, `files` or `sideEffects`. It reported 138 violations before this change and 0 after.
  • 04309f5: Collapse the audited serial-await fan-outs in Payload hooks, jobs and access checks. No behaviour changes — every try/catch, failure reporter and `overrideAccess` justification is preserved; only the number of round-trips changes. **`find({ limit: 0 })` → `payload.count()`** — `limit: 0` sets `pagination: false` in the Mongo adapter, so the query loads every matching row into memory to produce one number. `@wabbit/tome-org` documents this as a production incident in `hooks/attendance-count-sync.ts:6-11` and had reintroduced it in `collections/recruiting/JoinRequest.ts`'s one-pending-request guard; `@wabbit/tome-sc`'s squadron member recount had the same shape. **Independent lookups → `Promise.all` / `Promise.allSettled`** — org's `createGiverWindowAccess` (a per-request access check that took two serial round-trips), `EventAttendance`'s display-name composer and `AwardPresentation`'s, sc's RSI profile+org page fetches (the hot path for handle validation, against a third-party host), squadron recalcs, and accounts' three offboarding teardown callbacks. `allSettled` wherever a branch had its own fallback, so a failed member lookup still cannot stop the event title from resolving. sc's RSI adapter keeps its 404 short-circuit exactly, and ledger's per-leg balance guard decides in leg order so the thrown `NegativeBalanceError` still names the same wallet the serial version did. **Independent per-row writes → `batchWrite`** (`@wabbit/tome-core/utilities/batch`) — org's notification open/resolve fan-outs, the division/team cleanup and sunset cascades (up to 1,000 rows each), the event cascade-delete, the non-atomic `memberCount` fallback; lms's certification-expiry sweep (now paced in `WRITE_CHUNK` chunks like its sibling reconciler) and the course-delete enrollment drop; workflow's deadline sweep; crowdfund's tier-claim reconcile. **Same `data` for every row → one bulk `payload.update({ where, data })`** — sc's asset-assignment auto-close and the transfer-request GDPR redaction, matching `sc/src/gdpr.ts`'s `makeNullRefHandler`. The auto-close also drops a latent correctness hazard: its page cursor advanced while its own writes removed rows from the filter it was paging over, so a page boundary could skip assignments. **Two collection-level fixes.** sc's Fleet had two field-level `beforeChange` hooks each issuing a `findByID` for the SAME ship on every write; they are now one collection-level hook that reads the ship once and sets both `chassisName` and `name`. lms's `checkCertificationExpiry` re-derived `recountHolders` once per expired award with no cache; it now recounts once per affected certification, after the sweep — which is also more correct, since only the final count was ever right. `@wabbit/tome-crowdfund`'s `settleCampaign` pledge loop is untouched and now carries an explicit `eslint-disable` plus the reason: it captures money one pledge at a time against a `maxCapturesPerRun` budget that only bounds anything if the iterations are serialized. `@wabbit/tome-org`, `@wabbit/tome-lms`, `@wabbit/tome-workflow` and `@wabbit/tome-crowdfund` raise their `@wabbit/tome-core` peer floor to `>=1.14.0`, the release that adds `./utilities/batch`. org and lms were also understating their floor before this change — both already imported `@wabbit/tome-core/jobs`, added in core 1.7.0, while declaring `>=1.2.0` / `>=1.0.0`.
  • 73081e6: README peer tables, and the gate that now requires them. Sixteen packages declared `peerDependencies` and documented them nowhere a reader could scan — in prose inside an install paragraph, in a transposed "compatibility matrix" with the peers as columns, or not at all. Docs only; no source, no manifest, no runtime change (the one manifest change in this PR, admin's `sonner` peer, has its own changeset). Each of the sixteen gains a `## Peer dependencies` section generated from its own `package.json` — `| Peer | Range | Required |`, one row per peer, the range verbatim, `no (optional)` read from `peerDependenciesMeta`, plus one sentence on what is a real `dependency` rather than a peer and why the optional ones are optional. The worst omissions this surfaced: `@wabbit/tome-core` documented 2 of its 13 peers and left out both `next` and `@payloadcms/richtext-lexical`, which are required; `@wabbit/tome-admin` listed 5 of 20; `@wabbit/tome-readout` and `@wabbit/tome-sc` listed none. Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still `>=18` after the peer floor moved to `>=19.0.0` — and none of the eight listed `react-dom` at all. Those tables are retired in favour of the generated one, with a line saying what they used to claim so the next reader does not reinstate them. The forcing function ships with the fix: `scripts/assert-readme-contract.mjs` now FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existing `Optional?` and `Notes` third columns still pass, so the thirty already-conforming READMEs were not touched). It is deliberately shape-only, not row-level: asserting that each row agrees with the manifest is the Tier 2 generation work. Verified non-vacuous by breaking one table's header and watching the gate fail, then restoring it. `CONTRIBUTING.md`'s assert-script list — which said "five" while sixteen existed — and the three guides that describe this gate were corrected in the same pass.
  • b01ca1f: `createSCLayer` now registers `@wabbit/tome-sc` with the platform layer registry. Through 0.4.0 tome-sc exported a full layer factory — twelve collections, a permission namespace, GDPR registration — and never called `registerLayer`. `hasLayer('@wabbit/tome-sc')` was therefore permanently `false` for every consumer, and any cross-layer feature gate keyed on SC's presence could never fire. Nothing surfaced it, because a missing registration is indistinguishable from a layer that is genuinely not installed. The registration follows the sibling pattern exactly: a static import of `@wabbit/tome-core/utilities/layerRegistry` (core is a required peer here, and the registry is `globalThis`-backed, so a static import shares one store across ESM/CJS/bundler/Payload-CLI contexts — never a lazy `require()`), wrapped in `try/catch` to swallow only the duplicate-registration throw under HMR or a `createSCLayer` call followed by `createSCPlatform`. The manifest is real, not a stub: `version` reads the new `SC_LAYER_VERSION` leaf const, `collections` is the twelve resolved slugs, and the `adminNav` entry uses `config.adminGroup ?? 'Organization'` — the same label the twelve collection factories resolve — under `domain: 'people'` at `order: 10`. Both halves matter: the sidebar resolver pairs manifest entries to collections by that group string, and its "first manifest wins for a given group label" rule means SC's domain must agree with org's registration for the same label, or the rendered domain would depend on config-build order. Additive: a consumer that never read the registry is unaffected; one that did now sees SC where it previously saw nothing.
  • Updated dependencies [b01ca1f]
  • Updated dependencies [0836ef5]
  • Updated dependencies [73081e6]
  • Updated dependencies [73081e6] - @wabbit/tome-blocks-sc-pack@0.16.0 - @wabbit/tome-blocks-signal-theme@0.16.0 - @wabbit/tome-cop@0.1.8
v0.4.0minor

3e4af2b: Add the `registered-assets` sub-cluster (Wave 2R / 2R-I6): `createRegisteredAssetCollection`, `createAssetAssignmentCollection`, `createAssetTransferRequestCollection`, and `registerRegisteredAssetsGdpr`, ported from the reference consumer's `CommissionedShips`/`ShipAssignments`/`ShipTransferRequests` cluster under the neutral name adopted for this package (Wave 2 residue extraction plan §1c/§5a). NOT part of `createSCLayer`/`createSCPlatform`'s fixed collection tuple — same opt-in posture `shipsCollection`/`certificationsSlug` held for years before this increment; a consumer composes the three factories explicitly. - `createRegisteredAssetCollection`: every stored field name overridable via `fieldNames` (the reference consumer keeps `shipClass`/`currentAsset`/`affiliatedUnit`/`affiliatedWing`/`bio` — zero data migration); `linkedAsset` (reference consumer: `currentAsset`) emitted only when `linkedAssetSlug` is configured; `canManage` replaces the reference consumer's documented `authenticated` placeholder, defaulting to the new `MANAGE_REGISTERED_ASSETS` permission; absorbs `createParentScopeMirrorHook` (reference consumer: `mirrorAffiliatedWingFromUnit`) and `createAssignmentAutoCloseHook` (reference consumer: `autoCloseAssignmentsOnDecommission`, terminal statuses configurable); `statusOptions` defaults to `active|decommissioned|retired` — the reference consumer replaces wholesale with its three including `lost-in-action`. - `createAssetAssignmentCollection`: absorbs `computeActiveFlag` verbatim. Models the reference consumer's Option C design decision (a ship officer billet's holder-of-record lives on the assignment, not on `@wabbit/tome-org`'s Position `currentHolder`) as `participatesInPositionCascade` (default `false`). The `true` opt-in wires `createOfficerPositionSyncHook`, whose doc comment documents the concurrent-write hazard re-arming the cascade introduces — no cross-collection concurrency guard exists, so a raced write from org's own Position flow can silently disappear. - `createAssetTransferRequestCollection`: generalizes the reference consumer's `maybeApplyTransfer` into `createTransferApplyHook`, hand-rolling the same settle-on-success/unwind-on-throw-with-error-field pattern the reference consumer's real `tome-integration` branch delegates to `@wabbit/tome-workflow/server`'s `executeClaimed` (Wave 4 I2) — `lastExecutionError` itself already exists verbatim in that branch's schema, not new. The wing-scoped access matrix (`decideReadTransferRequest`/`decideUpdateTransferRequest`) is `Where`-returning throughout (`@wabbit/tome-core/access/scoped`'s `andWhere`/`orWhere`) with an injectable `resolveLeadership` seam, unaffected by the above — `access.ts` is byte-identical between the reference consumer's main branch and `tome-integration`. Exports `assetTransferTopology`, structurally identical to the reference consumer's real `shipTransferRequestsWorkflowTopology` (`ShipTransferRequests/workflow.ts`, `tome-integration` branch) — `{ kind: 'single', approverPoolResolver }`, no escalation/recusal used there either. `@wabbit/tome-workflow` is added as an OPTIONAL peer, referenced only via `import type`: `packages/sc` itself has no prior runtime relationship to it, even though the reference consumer's real cluster does — not every `registered-assets` consumer should be forced into that runtime dependency for one collection factory. - `MANAGE_REGISTERED_ASSETS` added to `SC_PERMISSIONS`. - `registerRegisteredAssetsGdpr`/`unregisterRegisteredAssetsGdpr` (sibling to `registerScGdpr`, matching `@wabbit/tome-core/field-reports`'s `registerFieldReportsGdpr` precedent — not auto-wired): `registered-assets.createdBy` → retain; `asset-assignments.member` → hard-delete; `asset-transfer-requests` → redact (row retained, `rationale`/`decisionNotes`/`lastExecutionError` cleared). Each collection's registration is independently skippable for a consumer with its own redact-engine ownership.

  • 3e4af2b: Add the `registered-assets` sub-cluster (Wave 2R / 2R-I6): `createRegisteredAssetCollection`, `createAssetAssignmentCollection`, `createAssetTransferRequestCollection`, and `registerRegisteredAssetsGdpr`, ported from the reference consumer's `CommissionedShips`/`ShipAssignments`/`ShipTransferRequests` cluster under the neutral name adopted for this package (Wave 2 residue extraction plan §1c/§5a). NOT part of `createSCLayer`/`createSCPlatform`'s fixed collection tuple — same opt-in posture `shipsCollection`/`certificationsSlug` held for years before this increment; a consumer composes the three factories explicitly. - `createRegisteredAssetCollection`: every stored field name overridable via `fieldNames` (the reference consumer keeps `shipClass`/`currentAsset`/`affiliatedUnit`/`affiliatedWing`/`bio` — zero data migration); `linkedAsset` (reference consumer: `currentAsset`) emitted only when `linkedAssetSlug` is configured; `canManage` replaces the reference consumer's documented `authenticated` placeholder, defaulting to the new `MANAGE_REGISTERED_ASSETS` permission; absorbs `createParentScopeMirrorHook` (reference consumer: `mirrorAffiliatedWingFromUnit`) and `createAssignmentAutoCloseHook` (reference consumer: `autoCloseAssignmentsOnDecommission`, terminal statuses configurable); `statusOptions` defaults to `active|decommissioned|retired` — the reference consumer replaces wholesale with its three including `lost-in-action`. - `createAssetAssignmentCollection`: absorbs `computeActiveFlag` verbatim. Models the reference consumer's Option C design decision (a ship officer billet's holder-of-record lives on the assignment, not on `@wabbit/tome-org`'s Position `currentHolder`) as `participatesInPositionCascade` (default `false`). The `true` opt-in wires `createOfficerPositionSyncHook`, whose doc comment documents the concurrent-write hazard re-arming the cascade introduces — no cross-collection concurrency guard exists, so a raced write from org's own Position flow can silently disappear. - `createAssetTransferRequestCollection`: generalizes the reference consumer's `maybeApplyTransfer` into `createTransferApplyHook`, hand-rolling the same settle-on-success/unwind-on-throw-with-error-field pattern the reference consumer's real `tome-integration` branch delegates to `@wabbit/tome-workflow/server`'s `executeClaimed` (Wave 4 I2) — `lastExecutionError` itself already exists verbatim in that branch's schema, not new. The wing-scoped access matrix (`decideReadTransferRequest`/`decideUpdateTransferRequest`) is `Where`-returning throughout (`@wabbit/tome-core/access/scoped`'s `andWhere`/`orWhere`) with an injectable `resolveLeadership` seam, unaffected by the above — `access.ts` is byte-identical between the reference consumer's main branch and `tome-integration`. Exports `assetTransferTopology`, structurally identical to the reference consumer's real `shipTransferRequestsWorkflowTopology` (`ShipTransferRequests/workflow.ts`, `tome-integration` branch) — `{ kind: 'single', approverPoolResolver }`, no escalation/recusal used there either. `@wabbit/tome-workflow` is added as an OPTIONAL peer, referenced only via `import type`: `packages/sc` itself has no prior runtime relationship to it, even though the reference consumer's real cluster does — not every `registered-assets` consumer should be forced into that runtime dependency for one collection factory. - `MANAGE_REGISTERED_ASSETS` added to `SC_PERMISSIONS`. - `registerRegisteredAssetsGdpr`/`unregisterRegisteredAssetsGdpr` (sibling to `registerScGdpr`, matching `@wabbit/tome-core/field-reports`'s `registerFieldReportsGdpr` precedent — not auto-wired): `registered-assets.createdBy` → retain; `asset-assignments.member` → hard-delete; `asset-transfer-requests` → redact (row retained, `rationale`/`decisionNotes`/`lastExecutionError` cleared). Each collection's registration is independently skippable for a consumer with its own redact-engine ownership.
v0.3.1patch

e97db45: Fix a module cycle in `@wabbit/tome-sc/rsi` between `client.ts` and `adapter.ts` (Wave 2R / I2.1 hotfix). `adapter.ts` imported `ADAPTER_VERSION` from `client.ts` to build its top-level `DEFAULT_USER_AGENT`, while `client.ts` imports `adapter.ts` for `createRSIProfileAdapter` — a live cycle, not just a type-only one, so it broke depending on which side of the cycle a consumer's module graph reached first: entering via `client.js`/`index.js` (as Payload's `generate:types` does when it resolves `RSIClient`) hit `adapter.js`'s top-level `ADAPTER_VERSION` read before `client.js` had reached its own assignment, throwing `ReferenceError: Cannot access 'ADAPTER_VERSION' before initialization`. `ADAPTER_VERSION` now lives in a new dependency-free `src/rsi/version.ts`; both `adapter.ts` and `client.ts` import it from there instead of from each other, eliminating the cycle. `client.ts` still re-exports `ADAPTER_VERSION` for backward compatibility — no import site needs to change. Adds `tests/rsi/rsiModuleLoadability.test.ts`, a regression guard that spawns a fresh `node` process against every built file in `dist/rsi/*` (both the ESM and CJS twin) plus every `./rsi*` entry in the package's `exports` map, and asserts a clean load — closing the gap that let this ship: `scripts/assert-node-loadable.mjs` only walks exports-map targets (never `adapter.js` directly, since there's no standalone subpath for it) and its `classify()` treats a thrown `ReferenceError` as a SKIP ("runtime side effect"), not a FAIL, so it stayed green through this defect on both counts.

  • e97db45: Fix a module cycle in `@wabbit/tome-sc/rsi` between `client.ts` and `adapter.ts` (Wave 2R / I2.1 hotfix). `adapter.ts` imported `ADAPTER_VERSION` from `client.ts` to build its top-level `DEFAULT_USER_AGENT`, while `client.ts` imports `adapter.ts` for `createRSIProfileAdapter` — a live cycle, not just a type-only one, so it broke depending on which side of the cycle a consumer's module graph reached first: entering via `client.js`/`index.js` (as Payload's `generate:types` does when it resolves `RSIClient`) hit `adapter.js`'s top-level `ADAPTER_VERSION` read before `client.js` had reached its own assignment, throwing `ReferenceError: Cannot access 'ADAPTER_VERSION' before initialization`. `ADAPTER_VERSION` now lives in a new dependency-free `src/rsi/version.ts`; both `adapter.ts` and `client.ts` import it from there instead of from each other, eliminating the cycle. `client.ts` still re-exports `ADAPTER_VERSION` for backward compatibility — no import site needs to change. Adds `tests/rsi/rsiModuleLoadability.test.ts`, a regression guard that spawns a fresh `node` process against every built file in `dist/rsi/*` (both the ESM and CJS twin) plus every `./rsi*` entry in the package's `exports` map, and asserts a clean load — closing the gap that let this ship: `scripts/assert-node-loadable.mjs` only walks exports-map targets (never `adapter.js` directly, since there's no standalone subpath for it) and its `classify()` treats a thrown `ReferenceError` as a SKIP ("runtime side effect"), not a FAIL, so it stayed green through this defect on both counts.
v0.3.0minor

cf2802b: Add `RSIProfileAdapter` (`createRSIProfileAdapter`, `@wabbit/tome-sc/rsi`) — the concrete `ExternalProfileAdapter<RSIProfile>` implementation for `@wabbit/tome-core/verification`'s neutral verification/cache/reconcile primitives (Wave 2R I2). This is now the ONE place RSI's markup regexes live, ported field-for-field from the reference consumer's `scrapeRsiDossier.ts`/`parseRsiHandle.ts` and characterised against the same fixture HTML (Wave 2R I0). `RSIClient.validateHandle` is rewired to use the adapter internally (same `{valid, orgTag?}` return shape) so it now benefits from retry/backoff and an optional distributed rate limiter — previously a single unretried fetch. Deliberate divergences from the reference consumer, all test-covered: `exists()`/`checkExistence()` fail CLOSED on a network error (the reference consumer fails open); a 404 resolves to `status: 'not_found'` on the first attempt with no wasted retries (the reference consumer retries a stable 404 like a transient failure and then mislabels it); no `E2E_TEST_` production bypass moved upstream; new structural `visibility` detection (`'public' | 'hidden' | 'redacted'`) that the reference consumer's scraper never had. Bumps this package's `@wabbit/tome-core` peer floor to `>=1.11.0 <2.0.0` (the `./verification` subpath this adapter is built on).

  • cf2802b: Add `RSIProfileAdapter` (`createRSIProfileAdapter`, `@wabbit/tome-sc/rsi`) — the concrete `ExternalProfileAdapter<RSIProfile>` implementation for `@wabbit/tome-core/verification`'s neutral verification/cache/reconcile primitives (Wave 2R I2). This is now the ONE place RSI's markup regexes live, ported field-for-field from the reference consumer's `scrapeRsiDossier.ts`/`parseRsiHandle.ts` and characterised against the same fixture HTML (Wave 2R I0). `RSIClient.validateHandle` is rewired to use the adapter internally (same `{valid, orgTag?}` return shape) so it now benefits from retry/backoff and an optional distributed rate limiter — previously a single unretried fetch. Deliberate divergences from the reference consumer, all test-covered: `exists()`/`checkExistence()` fail CLOSED on a network error (the reference consumer fails open); a 404 resolves to `status: 'not_found'` on the first attempt with no wasted retries (the reference consumer retries a stable 404 like a transient failure and then mislabels it); no `E2E_TEST_` production bypass moved upstream; new structural `visibility` detection (`'public' | 'hidden' | 'redacted'`) that the reference consumer's scraper never had. Bumps this package's `@wabbit/tome-core` peer floor to `>=1.11.0 <2.0.0` (the `./verification` subpath this adapter is built on).
  • @wabbit/tome-cop@0.1.7
v0.2.4patch

5c4f840: Registers this package's member-referencing collections with core's GDPR export/delete pipelines from inside the package (Wave 5 / W5-I3), mirroring `@wabbit/tome-fulfillment`'s `registerFulfillmentGdpr`. New `registerScGdpr(options?)`/`unregisterScGdpr(options?)` in `./gdpr.ts`, re-exported from the package barrel; `createSCLayer` (and therefore `createSCPlatform`) calls `registerScGdpr` automatically unless `config.registerGdpr: false`. Ten collections register, all `post-identity` (none hold subject handle text). Fleet/AssetAvailability/TaskForceAssignments (personal asset ownership) default to `retain` — this package's real consumer soft-anonymises the Member document on erasure rather than deleting it, so these FKs stay valid and resolve to "Former Member" with nothing to clear; `retain` is the correct disposition for that consumer, not a fallback. A `personalAssetMode: 'null-ref'` opt-in is available for a consumer that instead hard-deletes Members: it clears the owner field via `payload.update` (Local API, `overrideAccess: true`, full validation and hooks run) rather than any database-adapter bypass. Because those three fields are `required: true` by default, choosing `'null-ref'` also requires `memberFieldRequired: false` (new option on `SCLayerConfig`/`SCPlatformConfig`, forwarded to the three factories AND to `registerScGdpr` from the same config so the two can't drift) — `registerScGdpr` throws loudly at registration time if `'null-ref'` is requested while the fields are still required, rather than registering a mode that would fail validation on every row. FleetLogs registers `retain` (immutable service-history log). Squadrons/TaskForces/ResourceRequests/Locations/OperationTemplates/ForceTemplates register `retain` too — none of these six is named in the Wave 5 compliance plan's disposition table; registered under the plan's own stated fallback rule rather than left unregistered, flagged in `gdpr.ts`'s header for a follow-up ratification pass. Bumps the `@wabbit/tome-core` peer/dev floor to `>=1.8.0` for the `mode`/`phase`/`onNullRef` registry contract this depends on. - @wabbit/tome-cop@0.1.7

  • 5c4f840: Registers this package's member-referencing collections with core's GDPR export/delete pipelines from inside the package (Wave 5 / W5-I3), mirroring `@wabbit/tome-fulfillment`'s `registerFulfillmentGdpr`. New `registerScGdpr(options?)`/`unregisterScGdpr(options?)` in `./gdpr.ts`, re-exported from the package barrel; `createSCLayer` (and therefore `createSCPlatform`) calls `registerScGdpr` automatically unless `config.registerGdpr: false`. Ten collections register, all `post-identity` (none hold subject handle text). Fleet/AssetAvailability/TaskForceAssignments (personal asset ownership) default to `retain` — this package's real consumer soft-anonymises the Member document on erasure rather than deleting it, so these FKs stay valid and resolve to "Former Member" with nothing to clear; `retain` is the correct disposition for that consumer, not a fallback. A `personalAssetMode: 'null-ref'` opt-in is available for a consumer that instead hard-deletes Members: it clears the owner field via `payload.update` (Local API, `overrideAccess: true`, full validation and hooks run) rather than any database-adapter bypass. Because those three fields are `required: true` by default, choosing `'null-ref'` also requires `memberFieldRequired: false` (new option on `SCLayerConfig`/`SCPlatformConfig`, forwarded to the three factories AND to `registerScGdpr` from the same config so the two can't drift) — `registerScGdpr` throws loudly at registration time if `'null-ref'` is requested while the fields are still required, rather than registering a mode that would fail validation on every row. FleetLogs registers `retain` (immutable service-history log). Squadrons/TaskForces/ResourceRequests/Locations/OperationTemplates/ForceTemplates register `retain` too — none of these six is named in the Wave 5 compliance plan's disposition table; registered under the plan's own stated fallback rule rather than left unregistered, flagged in `gdpr.ts`'s header for a follow-up ratification pass. Bumps the `@wabbit/tome-core` peer/dev floor to `>=1.8.0` for the `mode`/`phase`/`onNullRef` registry contract this depends on. - @wabbit/tome-cop@0.1.7
v0.2.3patch

4e59529: Two upstream corrections surfaced by the first consumer's line-read cutover audits. **`membershipsSlug` added to `SCLayerConfig`.** The Squadron factory hardcoded its `memberships` join target to org's `'org-memberships'` — the only cross-layer slug in the package without a config knob — forcing the first consumer to patch the emitted field post-hoc. A join pointing at an unregistered collection does not error; it renders empty. **Resource-requests control flow corrected to match upstream.** The 2026-08-15 restoration had the permission keys right and the ordering wrong: logistics authority was granted before the id/status guards (officers could edit fulfilled/cancelled requests and perform id-less bulk ops) and `delete` gained a logistics branch upstream never shipped. Corrected: update = super-role → require id → block fulfilled/cancelled → requester-own → logistics-triple; delete = super-role → require id → block fulfilled → requester-own only. The integrity hook also gains upstream's priority-400 (logistics-exempt) — the field-access-only approach silently stripped the change with a 200, breaking consumers that key error handling off the thrown message. Five new tests pin the ordering.

  • 4e59529: Two upstream corrections surfaced by the first consumer's line-read cutover audits. **`membershipsSlug` added to `SCLayerConfig`.** The Squadron factory hardcoded its `memberships` join target to org's `'org-memberships'` — the only cross-layer slug in the package without a config knob — forcing the first consumer to patch the emitted field post-hoc. A join pointing at an unregistered collection does not error; it renders empty. **Resource-requests control flow corrected to match upstream.** The 2026-08-15 restoration had the permission keys right and the ordering wrong: logistics authority was granted before the id/status guards (officers could edit fulfilled/cancelled requests and perform id-less bulk ops) and `delete` gained a logistics branch upstream never shipped. Corrected: update = super-role → require id → block fulfilled/cancelled → requester-own → logistics-triple; delete = super-role → require id → block fulfilled → requester-own only. The integrity hook also gains upstream's priority-400 (logistics-exempt) — the field-access-only approach silently stripped the change with a 200, breaking consumers that key error handling off the thrown message. Five new tests pin the ordering.
v0.2.2patch

7b66dcd: `dist` is now loadable by raw Node. tsup builds with `bundle: false`, so it emitted relative specifiers exactly as the TypeScript source wrote them — extensionless (`from "./hierarchy"`, `require("./hierarchy")`). Bundlers and tsx resolve those; raw Node does not. ESM raised `ERR_MODULE_NOT_FOUND`, and CJS was worse: `require("./x")` resolved to the ESM `.js` twin (`.cjs` is not in Node's CJS extension search list), and Node 22+ `require(esm)` then died on _that_ file's own extensionless import. Any consumer outside a bundler — the payload CLI under plain node, `generate:types`, ops scripts, codegen tools — hit this on every subpath that had relative imports; single-file subpaths loaded fine, which is why it went unnoticed. A post-build step (`scripts/fix-dist-extensions.mjs --strict`) now appends explicit extensions (`.js` / `/index.js`, `.cjs` / `/index.cjs`) and fails the build on any specifier it cannot resolve rather than guessing. No source changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable. - @wabbit/tome-cop@0.1.7

  • 7b66dcd: `dist` is now loadable by raw Node. tsup builds with `bundle: false`, so it emitted relative specifiers exactly as the TypeScript source wrote them — extensionless (`from "./hierarchy"`, `require("./hierarchy")`). Bundlers and tsx resolve those; raw Node does not. ESM raised `ERR_MODULE_NOT_FOUND`, and CJS was worse: `require("./x")` resolved to the ESM `.js` twin (`.cjs` is not in Node's CJS extension search list), and Node 22+ `require(esm)` then died on _that_ file's own extensionless import. Any consumer outside a bundler — the payload CLI under plain node, `generate:types`, ops scripts, codegen tools — hit this on every subpath that had relative imports; single-file subpaths loaded fine, which is why it went unnoticed. A post-build step (`scripts/fix-dist-extensions.mjs --strict`) now appends explicit extensions (`.js` / `/index.js`, `.cjs` / `/index.cjs`) and fails the build on any specifier it cannot resolve rather than guessing. No source changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable. - @wabbit/tome-cop@0.1.7
v0.2.1patch

dbac581: Import `authenticated`/`anyone` from the `@wabbit/tome-core/auth/guards` leaf instead of the `/auth` barrel. The barrel statically re-exports `createBetterAuth`, which drags `@delmaredigital/payload-better-auth` into the module graph — so any consumer loading this package outside a bundler (payload CLI under tsx: `generate:types`, seeds, migrations) crashed with ERR_MODULE_NOT_FOUND unless it installed an auth stack it may deliberately not use. Leaf-import discipline is what this platform's own consumer docs mandate; the layers now follow it themselves. No behavioural change — same functions, same leaf they always resolved to.

  • dbac581: Import `authenticated`/`anyone` from the `@wabbit/tome-core/auth/guards` leaf instead of the `/auth` barrel. The barrel statically re-exports `createBetterAuth`, which drags `@delmaredigital/payload-better-auth` into the module graph — so any consumer loading this package outside a bundler (payload CLI under tsx: `generate:types`, seeds, migrations) crashed with ERR_MODULE_NOT_FOUND unless it installed an auth stack it may deliberately not use. Leaf-import discipline is what this platform's own consumer docs mandate; the layers now follow it themselves. No behavioural change — same functions, same leaf they always resolved to.
  • 19ba623: Retire the local `authorizedCronRequest` copy in favour of `@wabbit/tome-core/jobs`. The copy's own comment explained the duplication — the function "is not exported from `@wabbit/tome-core`'s public surface" — which stopped being true in core 1.7.0. This was the last of three copies; the two in core were retired alongside the export. All three shared a timing leak: an early `authHeader.length !== expected.length` return, which is precisely the flaw `utilities/timingSafeEqual` was promoted into core to eliminate. The shared implementation hashes both inputs to a fixed-length digest before comparing, so no length branch remains to short-circuit on and a wrong-length header costs the same work as a right-length one. It also rejects a blank secret outright, so a misconfigured `CRON_SECRET` cannot authorize a blank header. Behaviour is otherwise unchanged: same 500 on unconfigured secret, same 401 on a bad or missing header.
v0.2.0minor

196d642: Wave 2 absorb — prepares the layer for its first consumer. Each item restores an upstream production-tested behaviour this layer had diverged from. - **Consumer hooks seam.** `SCLayerConfig` gains `hooks?:`, merged via `mergeHooks` (append, never replace) across all twelve collection factories. The layer is right to omit cache revalidation, media reference tracking and audit-log writes — a platform layer must not know a consumer's infrastructure — but it previously offered nowhere to put them back, so the failure mode was silent omission. Untracked media is not merely degraded: it stays eligible for a ghost-pointer sweep while still in use. - **`squadrons.leader` is no longer `required`.** Offboarding a leader with no assistant to promote clears the field; a required constraint makes that write fail validation and blocks member retirement outright. Applies to any consumer with an offboarding path — a vacant leader is a valid state. - **`operation-templates.forceTemplate` added.** Present upstream, absent here, so adoption would have dropped the relationship and orphaned existing rows. Slug-configurable. - **`resource-requests` access restored** to `MANAGE_LOGISTICS | MANAGE_ECONOMY | CREATE_EVENTS`. The prior single-permission check silently revoked access from every economy and events officer, whose roles carry the other two. - **Break-glass integrity bypass**, opt-in via core's `registerSuperRoles`. Off by default, so a site declaring no super-roles keeps the stricter behaviour. Note a consumer cannot supply this through `config.hooks` — `mergeHooks` appends, so the layer's hook runs and throws first; the seam is additive-only and cannot suppress layer behaviour. Adds the package's first vitest config and test suite. **Requires `@wabbit/tome-core` >= 1.7.0** for `isSuperRoleUser`; the peer range is bumped accordingly.

  • 196d642: Wave 2 absorb — prepares the layer for its first consumer. Each item restores an upstream production-tested behaviour this layer had diverged from. - **Consumer hooks seam.** `SCLayerConfig` gains `hooks?:`, merged via `mergeHooks` (append, never replace) across all twelve collection factories. The layer is right to omit cache revalidation, media reference tracking and audit-log writes — a platform layer must not know a consumer's infrastructure — but it previously offered nowhere to put them back, so the failure mode was silent omission. Untracked media is not merely degraded: it stays eligible for a ghost-pointer sweep while still in use. - **`squadrons.leader` is no longer `required`.** Offboarding a leader with no assistant to promote clears the field; a required constraint makes that write fail validation and blocks member retirement outright. Applies to any consumer with an offboarding path — a vacant leader is a valid state. - **`operation-templates.forceTemplate` added.** Present upstream, absent here, so adoption would have dropped the relationship and orphaned existing rows. Slug-configurable. - **`resource-requests` access restored** to `MANAGE_LOGISTICS | MANAGE_ECONOMY | CREATE_EVENTS`. The prior single-permission check silently revoked access from every economy and events officer, whose roles carry the other two. - **Break-glass integrity bypass**, opt-in via core's `registerSuperRoles`. Off by default, so a site declaring no super-roles keeps the stricter behaviour. Note a consumer cannot supply this through `config.hooks` — `mergeHooks` appends, so the layer's hook runs and throws first; the seam is additive-only and cannot suppress layer behaviour. Adds the package's first vitest config and test suite. **Requires `@wabbit/tome-core` >= 1.7.0** for `isSuperRoleUser`; the peer range is bumped accordingly.
  • @wabbit/tome-cop@0.1.7
v0.1.8patch

Updated dependencies [510036f] - @wabbit/tome-blocks-sc-pack@0.15.0 - @wabbit/tome-blocks-signal-theme@0.15.0

  • Updated dependencies [510036f] - @wabbit/tome-blocks-sc-pack@0.15.0 - @wabbit/tome-blocks-signal-theme@0.15.0
v0.1.7patch

@wabbit/tome-blocks-sc-pack@0.14.0

  • @wabbit/tome-blocks-sc-pack@0.14.0
  • @wabbit/tome-blocks-signal-theme@0.14.0
  • @wabbit/tome-cop@0.1.5
v0.1.6patch

@wabbit/tome-blocks-sc-pack@0.13.0

  • @wabbit/tome-blocks-sc-pack@0.13.0
  • @wabbit/tome-blocks-signal-theme@0.13.0
v0.1.5patch

@wabbit/tome-blocks-signal-theme@0.12.1

  • @wabbit/tome-blocks-signal-theme@0.12.1
  • @wabbit/tome-cop@0.1.5
v0.1.4patch

@wabbit/tome-blocks-sc-pack@0.11.2

  • @wabbit/tome-blocks-sc-pack@0.11.2
  • @wabbit/tome-blocks-signal-theme@0.11.2
v0.1.3patch

26dfa07: `vitest run` exited 1 with "No test files found" in these two packages — neither has any test files yet. Added `--passWithNoTests` to the `test` script so CI doesn't fail on an empty suite. Script-only change; no runtime behavior changed. Both packages still need real test coverage added (tracked separately, not fixed here).

  • 26dfa07: `vitest run` exited 1 with "No test files found" in these two packages — neither has any test files yet. Added `--passWithNoTests` to the `test` script so CI doesn't fail on an empty suite. Script-only change; no runtime behavior changed. Both packages still need real test coverage added (tracked separately, not fixed here).
  • 36e537a: Every package now declares an explicit `sideEffects` field (38 added; motion/engine/forms already correct). Registration-bearing modules (render files' `registerRenderer`, `blocks/*/index.ts` `defineBlock` self-registration, widget `register.ts` files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket `false` on a package with registration or CSS.
  • Updated dependencies [6bc419c]
  • Updated dependencies [36e537a]
  • Updated dependencies [5f78397] - @wabbit/tome-blocks-sc-pack@0.11.0 - @wabbit/tome-blocks-signal-theme@0.11.0 - @wabbit/tome-cop@0.1.5
v0.1.2patch

dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here. - @wabbit/tome-cop@0.1.4

  • dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here. - @wabbit/tome-cop@0.1.4
v0.1.1patch

Updated dependencies [ec4b7bc]

  • Updated dependencies [ec4b7bc]
  • Updated dependencies [cd59894] - @wabbit/tome-cop@0.1.4 - @wabbit/tome-blocks-signal-theme@0.10.2 - @wabbit/tome-blocks-sc-pack@0.10.2
v0.1.0minor

Initial release — the Star Citizen wrapper over Tome. **SC data layer** — 12 collections extending `@wabbit/tome-org`: fleet management (`createShipCollection`, `createFleetCollection`, `createFleetLogCollection`, `createAssetAvailabilityCollection`, `createResourceRequestCollection`), SC operations (`createLocationCollection`, `createGameplayActivityCollection`, `createOperationTemplateCollection`, `createForceTemplateCollection`), and military coordination (`createSquadronCollection`, `createTaskForceCollection`, `createTaskForceAssignmentCollection`). Post-hoc extension functions (`withRSIFields`, `withFleetFields`, `withSquadronMembership`, `withSquadronMemberCountSync`, `withSCRankPresets`) wrap org's Member/Event/Membership/Rank collections without modifying `@wabbit/tome-org` itself. Hook-based, opt-in RSI integration (`validateRSIHandle`, `syncMemberOrg`, `createSyncShipDatabase`, the tree-shakeable `./rsi` `RSIClient`, and `./tasks`' `createSyncShipsTask`) — nothing calls any RSI-adjacent service unless a site enables it, and there is no official RSI API (handle checks scrape RSI's own public citizen page; ship data comes from a third-party community API). **SC bundle** — `createSCPlatform(config)` composes org (SC terminology preset + rank presets) + the extensions above + the 12 SC collections + permission registration into one call, and bundles `@wabbit/tome-blocks-sc-pack`, `@wabbit/tome-blocks-signal-theme`, and `@wabbit/tome-cop` as real dependencies. `createSCLayer(config)` is the SC-collections-only convenience function for manual composition. Mints two new platform permissions (`MANAGE_FLEET`, `MANAGE_LOCATIONS`); consumes three of org's existing keys (`MANAGE_STRUCTURE`, `MANAGE_LOGISTICS`, `MANAGE_TEMPLATES`). See the package README for the full quickstart, advanced manual-composition path, the `tome-accounts` co-installation warning, and the documented v1 access-control posture.

  • Initial release — the Star Citizen wrapper over Tome. **SC data layer** — 12 collections extending `@wabbit/tome-org`: fleet management (`createShipCollection`, `createFleetCollection`, `createFleetLogCollection`, `createAssetAvailabilityCollection`, `createResourceRequestCollection`), SC operations (`createLocationCollection`, `createGameplayActivityCollection`, `createOperationTemplateCollection`, `createForceTemplateCollection`), and military coordination (`createSquadronCollection`, `createTaskForceCollection`, `createTaskForceAssignmentCollection`). Post-hoc extension functions (`withRSIFields`, `withFleetFields`, `withSquadronMembership`, `withSquadronMemberCountSync`, `withSCRankPresets`) wrap org's Member/Event/Membership/Rank collections without modifying `@wabbit/tome-org` itself. Hook-based, opt-in RSI integration (`validateRSIHandle`, `syncMemberOrg`, `createSyncShipDatabase`, the tree-shakeable `./rsi` `RSIClient`, and `./tasks`' `createSyncShipsTask`) — nothing calls any RSI-adjacent service unless a site enables it, and there is no official RSI API (handle checks scrape RSI's own public citizen page; ship data comes from a third-party community API). **SC bundle** — `createSCPlatform(config)` composes org (SC terminology preset + rank presets) + the extensions above + the 12 SC collections + permission registration into one call, and bundles `@wabbit/tome-blocks-sc-pack`, `@wabbit/tome-blocks-signal-theme`, and `@wabbit/tome-cop` as real dependencies. `createSCLayer(config)` is the SC-collections-only convenience function for manual composition. Mints two new platform permissions (`MANAGE_FLEET`, `MANAGE_LOCATIONS`); consumes three of org's existing keys (`MANAGE_STRUCTURE`, `MANAGE_LOGISTICS`, `MANAGE_TEMPLATES`). See the package README for the full quickstart, advanced manual-composition path, the `tome-accounts` co-installation warning, and the documented v1 access-control posture.