fix(core): the last fixes' core half (W1 W2 W5 W19)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 49s
PR Checks / client-build (pull_request) Successful in 1m4s
PR Checks / server-tests (pull_request) Successful in 6m6s

Stage 4 of docs/modules/rust/PLAN_LAST_FIXES.md, with the org lead's
2026-10-22 decisions.

W19: one module per site at boot, as at install. The first module
directory by name holds the site, whether or not it then loads; every
other is recorded startup_failed at the manifest stage, before a line of
its code is required, with a reason naming the module that holds the
site. Module-uo and Module-Rust side by side used to fail the second
with a slot collision. Loader, lifecycle, schema and event-contract
tests that loaded several fixture modules now load each case alone.

W1: a reconcile files a resource the game no longer has, and that is
past its `until`, as expired rather than orphaned, with the same log
line expired() writes. No MODULE_API bump: `until` and reconcile's
answer are unchanged.

W5: a dry run that passes on the draft is kept with the fingerprint of
the validated spec (three nullable columns on event_definitions).
Publish stamps the new version verified, with that dry run's reviewer
and time, when the spec it freezes has the same fingerprint, and writes
version.verified on the occurrences it re-pins. Verify answers
draftRecorded and publish answers verified; OpenAPI regenerated.

W2: the run console says "Phase p1: steps done, waiting to advance" and
"Why this phase has not advanced", not "has not started".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-22 00:38:34 -05:00
parent 9a1654fc54
commit f40dd765c3
18 changed files with 632 additions and 210 deletions

View File

@@ -29,6 +29,8 @@
// DST gap shifts it, it is never rejected), so a schedule that validates here
// computes there.
const crypto = require('crypto')
const registries = require('../modules/registries')
// For `priceOf` and `undeclaredDimensions` only — the save-time half of §F's
// fail-closed budget rule (Phase 7). Nothing here reaches the database:
@@ -730,6 +732,31 @@ function publishable(spec) {
return dormant.length ? { ok: false, dormant: [...new Set(dormant)] } : { ok: true, dormant: [] }
}
/** JSON with every object's keys sorted, so equal specs serialise equally. */
function canonical(value) {
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`
if (value && typeof value === 'object') {
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort()
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`
}
return JSON.stringify(value) ?? 'null'
}
/**
* A spec's fingerprint: the sha256 of its canonical JSON (W5, Rust
* PLAN_LAST_FIXES).
*
* What lets a dry run that passed on a draft count for the version published
* from it. The draft is fingerprinted when the dry run passes and the version
* when it is published, both from `validate`'s output, so an edit in between is
* a different fingerprint and nothing carries over. Key order is not part of it:
* a spec that round-trips through the database with its keys reordered is the
* same plan.
*/
function fingerprint(spec) {
return crypto.createHash('sha256').update(canonical(spec)).digest('hex')
}
/** An empty, valid spec — what a newly created draft carries. */
const emptySpec = () => ({
schedule: { kind: 'manual' },
@@ -742,6 +769,7 @@ module.exports = {
// 1.13.0): eventDefinitions.model `buildFromTemplate`.
checkParams,
publishable,
fingerprint,
actionIdsIn,
emptySpec,
defaultOnFailure,