Skip to content

Add named world truth groups - #19

Open
scottbenton wants to merge 3 commits into
mainfrom
agent/add-world-truth-groups
Open

Add named world truth groups#19
scottbenton wants to merge 3 commits into
mainfrom
agent/add-world-truth-groups

Conversation

@scottbenton

@scottbenton scottbenton commented Jul 11, 2026

Copy link
Copy Markdown
Member

This MR adds a new, optional, top level worlds object to rulesets and expansions. The goal of worlds is twofold -

  1. Make multi-setting packages possible, and encourage consumers to respect this. We technically have this already with Starforged & Sundered Isles (where a playset of the two includes two worlds), but there's no guidance from datasworn on how to handle this. Worlds makes it explicit that consumers should be able to handle more than one setting in a playset, and should give players choice over which one they use.
  2. Give each setting a name. This way "The Ironlands" vs "The Forge" is baked into the json files.

The new top level worlds package allows for multiple worlds, and each world contains a name and a list of truth ids. This keeps truths backwards compatible, because nothing changes in that definition, while allowing progressive enhancement for apps that choose to utilize this new feature.

Once packages & consumers update, I'd encourage storing the key of this world to determine which world a player is using. This world key can then be used to edit the experience in worlds - for example, determining whether or not to show "Sectors" if "The Forge" is our active world. This also allows for combining things like Ironsworn and Starforged - we can have both rulesets active, and have the user tell us which truths & world they would like to use.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Published experimental packages for b9bd274.

npm i @datasworn-community/core@0.2.9-experimental.19.b9bd274b7db5
npm i @datasworn-community/core@pr-19
npm i @datasworn-community/build-tools@0.2.9-experimental.19.b9bd274b7db5
npm i @datasworn-community/build-tools@pr-19

The pr-19 alias remains after this PR closes. Use a versioned install command above for reproducibility.

@github-actions

Copy link
Copy Markdown
Contributor

Release policy needs a label

Schema-sensitive changes require exactly one of release:minor-schema, release:major-schema, release:patch, release:none; found none

This PR touches schema-sensitive paths, so CI needs exactly one release intent label:

  • release:minor-schema for additive schema changes
  • release:major-schema for breaking schema changes
  • release:patch for tooling, codegen, refactors, or generated-history changes that should publish without changing the schema line
  • release:none for schema-sensitive-only changes that should not publish

Current release labels: none

Schema-sensitive files:

  • packages/build-tools/schema-source/schema/RulesPackages.ts
  • packages/build-tools/schema-source/schema/Truths.ts
  • packages/core/json/datasworn-source.schema.json
  • packages/core/json/datasworn.schema.json
  • packages/core/src/Datasworn.ts
  • packages/core/src/DataswornSource.ts
  • packages/core/src/DataswornVersion.ts

If this should ship a new core/build-tools package without changing the active schema line, use release:patch.

@scottbenton scottbenton added release:minor-schema Publish a new additive Datasworn schema line release_experimental Publish PR canary packages and removed release_experimental Publish PR canary packages labels Jul 11, 2026
@scottbenton
scottbenton marked this pull request as ready for review July 11, 2026 00:56
Comment thread packages/core/json/datasworn.schema.json
Comment thread packages/core/json/datasworn.schema.json
Comment thread packages/core/json/datasworn.schema.json
@scottbenton
scottbenton force-pushed the agent/add-world-truth-groups branch 2 times, most recently from 47586f5 to 0c16edf Compare July 11, 2026 14:35
@sgreg

sgreg commented Jul 12, 2026

Copy link
Copy Markdown

Since I had a few comments, this looks good to me, but I think someone with more history/experience and stronger opinions on schema changes should still take a look and give approval then.

@scottbenton

Copy link
Copy Markdown
Member Author

Thank you for the comments!

@tbsvttr

tbsvttr commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Coming at this from the perspective of a downstream fork operator (tbsvttr/datasworn) — the design reads well and the test coverage is more thorough than most schema PRs get (7 focused cases, including duplicate rejection, empty-array rejection, cross-package resolution, and unresolved-reference rejection).

Non-blocking notes to consider before landing the 0.3.0 bump:

  1. Nothing prevents two worlds sharing the same display name. Dictionary keys keep them distinct programmatically, but two worlds both labeled "The Forge" would validate while creating a real UX bug for anyone picking from a list. Might warrant either a build-time uniqueness check or a doc note that the key/name should be kept in sync.

  2. The "combining rulesets" motivation isn't reflected in the schema itself. The PR description flags the powerful use case — active Ironsworn + Starforged, user selects world — but there's no description note on the worlds field that hints at that intent. If two rulesets both define a world under the same dictionary key, is the resolution defined? A sentence on the collection describing the multi-package composition story would go a long way for downstream implementers.

  3. Consider marking worlds as releaseStage: 'experimental'. The sibling factions field on Truth carries that marker for a similar "may see significant changes" reason (@scottbenton's _id-versus-key call is defensible now but might reverse if per-world content targeting shows up later). Softens future churn.

  4. worlds is opt-in — a truth doesn't have to belong to any world. Probably intentional (backwards compat with existing rulesets), but a one-liner clarifying that worlds is a curation layer on top of truths, not a partition, would avoid confused readers.

The _id-vs-key decision looks right for the current shape (nothing references worlds outward). Nice work.

@scottbenton
scottbenton requested a review from zkat July 16, 2026 00:07
@scottbenton

Copy link
Copy Markdown
Member Author

I don't think I want to prevent display name collisions. Its not a concern for the vast majority of players, and something consuming apps can resolve fairly easily by including the ruleset name the world came from in the picker. If two worlds within the same ruleset have the same display name, that's on the author to fix.

Good call on the world key. This is kinda making me rethink giving these an ID, so that a playset with package1/earth and package2/earth could use the ID to differentiate them world:package1/earth.

I don't think they need the experimental release stage. This is all kinda experimental yet, but I think this shape is solid enough that I'm not concerned.

Worlds should absolutely be opt-in. I should add some docs here as well, but the idea is that if there is no world attached to the schema, but there are truths, consuming apps should treat those truths as if they are one singular world.

@tbsvttr

tbsvttr commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Thanks for engaging on all four — good calls across the board.

Agree on 1, 3, 4. The name-uniqueness enforcement was overweighting a rare edge case; apps really can resolve it in the picker. The experimental marker's yours to skip since you feel the shape's stable. And the "no world, existing truths → treat as one singular world" fallback is exactly the right default — worth explicitly capturing that in a _comment on the worlds field alongside the docs, so consumers who never read the docs still see the intent.

On #2 — adding _id. Genuinely interesting shift. The disambiguation win (world:starforged/the_forge vs world:some_expansion/the_forge) is real and matches the convention every other primary node in Datasworn already follows. One less exception for consumers to remember.

Trade-off worth naming before you commit though: adding the ID quietly signals to future contributors that worlds are a first-class citizen — the kind of thing content can reference outward. Right now nothing points at a world from a move or oracle or asset. If someone shows up in six months proposing "let me tag a move with world_scope: world:starforged/the_forge" so it only fires in that world, the ID is what enables it — and now you're negotiating whether per-world content targeting is a thing Datasworn does. Not necessarily bad, but a bigger design commitment than the current "worlds = named subset of existing truths".

Cheapest way to keep the win without opening that door: add the ID, keep the world node's shape minimal (_id + name + truths[]), and don't yet extend any other node type to accept a world reference. That way disambiguation works today and per-world content targeting stays a deliberate future decision rather than an accidental drift.

Either direction (with or without ID) is defensible — just wanted the shape shift to be a conscious choice.

@scottbenton

Copy link
Copy Markdown
Member Author

Thinking on this again, I think keeping the ID off to start with might be the better move, because it allows storing the reference in a backwards compatible way. For example, if I have Starforged and Sundered Isles active, without worlds, I might just store the package key starforged as the key that lets me know which set of truths we are using. So now, to use a specific world, I could add the world key to that package key {packageKey: starforged, worldKey:forge} to choose a specific world, or just keep the package key to use all the truths from a given world.

@tbsvttr

tbsvttr commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Agreed, and I think this lands in the right place — the composite key is a better fit than the ID for a reason worth making explicit.

The disambiguation win isn't lost. The thing that made you reconsider in the first place was package1/earth vs package2/earth. {packageKey, worldKey} already resolves that, because the package key is part of the reference. So dropping _id costs nothing there.

The fallback becomes one code path, which is the real payoff. You noted earlier that a package with truths but no worlds should be treated as one implicit world. With the composite key, an absent worldKey means exactly that same thing — "all truths from this package". So a consumer writes one lookup that covers all three cases: a pre-worlds package, a post-worlds package where the player picked "all", and a post-worlds package where they picked a specific world. An ID-based reference would force a type switch between "package key" and world:foo/bar to express those.

It also matches existing schema precedent. StatRule and ConditionMeterRule are dictionary-keyed plain objects with no _id, addressed by bare key scoped to their package. The _id-bearing nodes — moves, oracles, assets, truths — are the ones content references by ID. Nothing in content points at a world, so the ID-less shape puts World in the right family rather than making it an exception.

And it's the reversible direction. Adding _id later is additive (minor bump); removing it later is breaking (major). Starting without keeps the option cheap.

One thing worth folding into the docs you mentioned: with the ID gone, the reference shape is a consumer convention that the schema no longer expresses. If it stays undocumented, every app invents its own encoding and they won't interop — which matters as soon as two tools try to read the same saved character. Suggest documenting the three rules as one model, since they're the same rule seen from different angles:

  • a world reference is {packageKey, worldKey}
  • absent worldKey → all truths from that package
  • package with truths but no worlds → a single implicit world

The ID decision itself needs no change to the branch — World is already ID-less there, so that part is a confirmation rather than a revision.


Edit: I went through the rest of the PR to see whether it's ready to approve. The design and tests look good to me — on a trial merge all 23 tests pass, worlds being Type.Optional keeps existing packages valid, and the six new cases cover the ones that matter (duplicate truths, empty set, missing name, cross-package resolution, unresolved reference). Two things came up though.

1. The generated schema goes stale on merge — worth a regen before landing.

The branch is 14 commits behind main, and in the meantime main picked up the TypeBox 0.34.41 → 0.34.52 bump (#25), which reorders required above properties throughout the generated schema. The new World block here was generated under 0.34.41, so it carries the old ordering while the rest of main's schema carries the new one.

It merges cleanly textually and CI is green here — the branch is self-consistent on its own — so this only shows up once the two meet. I ran a local trial merge of main + this branch and then bun run check:schema-generation:

error: Generated artifacts are stale: packages/core/json/datasworn.schema.json,
packages/core/json/datasworn-source.schema.json. Run "bun run generate" and commit the result.

bun run generate on the merged tree produces exactly this, and nothing else:

 "title": "World",
 "type": "object",
+"required": ["name", "truths"],
 "properties": { … }
-"required": ["name", "truths"]

So merging as-is would leave main red on the drift gate until someone regenerates. Fix is just: merge/rebase main in, bun run generate, commit the result.

2. The 0.3.0 bump needs a rollout order.

This moves DATASWORN_SCHEMA_VERSION to 0.3.0, and both content repos currently pin @datasworn-community/core at 0.2.9. That's the core-vs-content version skew you flagged yourself in the schema-explorer thread (.github#11) — so it'd help to have the intended order written down somewhere before this lands, since a few things key off it:

  • when official-content / community-content move to 0.3.0
  • whether the viewer's single-schema-line policy means they have to move together or can lag
  • when python-bindings regenerates (it just landed on 0.2.0, so it'll need another pass)

Neither is a design objection — happy to approve once the regen is in, and glad to do the python-bindings regen against 0.3.0 whenever you want it.

@scottbenton
scottbenton force-pushed the agent/add-world-truth-groups branch from 0c16edf to b9bd274 Compare August 1, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release_experimental Publish PR canary packages release:minor-schema Publish a new additive Datasworn schema line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants