Skip to content

feat(data): insert-time default-factory components (creation-time identity seam) - #200

Merged
krisnye merged 3 commits into
mainfrom
krisnye/guid
Sep 16, 2026
Merged

krisnye merged 3 commits into
mainfrom
krisnye/guid

Conversation

@krisnye

@krisnye krisnye commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a general, guid-agnostic ECS capability: a component schema may name a defaultFactory that mints the component's value at insert time when the row omits it. Such a component becomes optional at insert — omit it and the archetype mints a fresh value; supply it (the replication-inbound / load path) and the supplied value wins.

This is the enforcement seam FFP/Squirrel needs for FFP-108211 (deterministic sibling order via a creation-time GUID tiebreak), without teaching the core ECS anything about GUIDs. guid stays entirely consumer-side: Squirrel declares guid: { …, defaultFactory: "guid" } and registers { guid: () => mintGuid() } once. The ECS only knows "a component may name a default factory."

Why this shape (vs. type-requiring a guid on document-quadrant inserts)

  • Nothing to forget. The mint lives in the archetype's codegen'd insert — the single bottleneck the reverted "creation-time identity layer" lacked. There are no ~30 insert sites to update; existing sites compile unchanged and new ones "just work."
  • Inbound-adopt & load are native. row supplies value → use it; absent → mint is Squirrel's adopt-inbound-on-miss, moved to creation time. fromData already repopulates non-insert columns, so load supplies the persisted guid and the factory never runs — no re-mint, no escape hatch needed.
  • v4 vs uuidv7 dissolves. The factory body is consumer-owned; keep uuidv7. Zero API impact.
  • Pure-JSON schema preserved. The schema carries a serializable name, resolved by a registry — mirroring the existing interpolators precedent. The factory (a function) lives in CreateStoreOptions.defaultFactories, never in the schema.
  • Fail-fast guard. A component naming a factory the registry doesn't provide throws at archetype construction (a boundary throw, per errors-as-data) — not a per-insert hot-path throw.

What's threaded

DFK (default-factory keys, derived from component schemas via DefaultFactoryKeys<CS>) flows so the omission is compile-checked end to end:

Schema.defaultFactorycreateArchetype (codegen) → CoreStoreTransactionDeclarations + Plugin.ToStore.

Inside a transaction/system body, t.archetypes.X.insert({ … }) may omit default-factory components while every other component stays required (@ts-expect-error-verified).

Deliberately NOT threaded

The read-only Database interface and Database.FromPlugin extends-chain are left untouched. Actions take a read-only Database (they dispatch transactions, never insert), so inserts only ever flow through the mutable transaction/system store — which is threaded. Keeping DFK off the Database generic list avoids amplifying the plugin-composition instantiation cost.

Type-perf (plugin extends-chain, scripts/typeperf/measure.mjs extends)

The added cost is a fixed per-plugin DefaultFactoryKeys<CS> pass that does not scale with extends depth — the quadratic composition coefficient is unchanged:

depth Instantiations before after Δ inst/depth² before → after
2 162,300 190,974 +17.7% 40,575 → 47,744
8 232,782 263,004 +13.0% 3,637 → 4,109
16 547,846 581,140 +6.1% 2,140 → 2,270
32 3,064,950 3,107,844 +1.4% 2,993 → 3,035

The absolute Δ is ~constant (+29k → +43k) across all depths; the marginal per-link is flat.

Consumer usage

const guid = { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4, defaultFactory: "guid" } as const satisfies Schema;

const db = Database.create(plugin, { defaultFactories: { guid: () => Guid.create() } });

// In a transaction body:
addNode(t, args: { name: string }) {
  return t.archetypes.Node.insert(args);   // guid omitted → minted at creation
}
adoptNode(t, args: { name: string; guid: Guid }) {
  return t.archetypes.Node.insert(args);   // guid supplied → adopted (inbound / load)
}

Testing

  • create-archetype.test.ts — mint-on-omit (fresh per insert), supply-overrides, and the generic (non-identifier-name) insert path.
  • archetype.ts compile-time tests — EntityInsertValues<C, DK> optionality.
  • default-factory.test.ts — end-to-end through the full plugin chain (Database.create → transaction → insert): mint, adopt, init-error, and the compile-time optionality of Plugin.ToStore.
  • Full data suite: 3521 passing. Full monorepo typecheck (caches cleared): clean.

Follow-ups for the consumer (Squirrel-side, not in this PR)

  • A verifying test enumerating document-quadrant archetypes asserting each includes guid (closes the "new document archetype forgot to declare guid" residual gap — per explicit-over-magical).
  • A test asserting the replication apply/inbound path always supplies the guid, so the factory is never reached for a replicated entity (guards the one real failure mode: a dropped inbound guid would silently mint a fresh one and re-introduce divergence).

🤖 Generated with Claude Code

krisnye and others added 3 commits September 15, 2026 20:52
A component schema may name a zero-arg `defaultFactory` (pure-JSON name,
resolved by a store-level registry). Such a component is OPTIONAL at insert:
omit it and the archetype mints a fresh value; supply it (replication
inbound / load) and the supplied value wins. Threads a default-factory-keys
type param (DFK) through Core and Store so the omission is type-checked.
Naming a factory the registry lacks throws at archetype construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Transaction bodies and system stores now type default-factory components as
optional at insert (DFK flows via TransactionDeclarations + Plugin.ToStore),
so consumer insert sites can omit them and the archetype mints them.
createDatabase accepts a defaultFactories registry. The read-only Database
interface and FromPlugin extends-chain are deliberately untouched to avoid
amplifying the plugin-composition instantiation cost. E2E + type tests added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krisnye
krisnye merged commit ecc12fd into main Sep 16, 2026
3 checks passed
@krisnye
krisnye deleted the krisnye/guid branch September 16, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant