feat(events): a dry run carries a module's advice (MODULE_API 1.16.0)
An action answering verify: true with { ok: true, detail: { warnings } } has
each line shown as a warning finding (code `advice`) that never fails the
dry run. Strings only, at most ten per step, each cut at 500 characters;
anything else under warnings is ignored, and a refused step carries none.
Rust PLAN_REDESIGNS §11.4, D391: a raid base's spot is checked against the
world as it stands now (a player on it, a rock beside it), which may change
before the run, so the editor shows it as advice. One optional reading of an
existing member, so minor; Module-uo's ^1.10.0 still resolves and says no
warnings. STEAM_SIGNIN.md's claim moves to 1.17.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -11,6 +11,9 @@
|
||||
// that the two files can drift, so a test asserts they agree
|
||||
// (client/test/moduleRegistry.test.js) rather than trusting a bump to remember
|
||||
// both.
|
||||
// 1.16.0 — a dry run's `detail.warnings` (Rust PLAN_REDESIGNS §11.4, D391): a
|
||||
// module's advice, shown by core's own event editor as warning findings.
|
||||
// Server-side; this file bumps for the reason at the top.
|
||||
// 1.15.0 — a failed step's `retryAfterMs` (Rust PLAN_REDESIGNS §14.3.13): when
|
||||
// to try again. Server-side; the run console's retry line now says when. This
|
||||
// file bumps for the reason at the top.
|
||||
@@ -91,4 +94,4 @@
|
||||
// but the two halves state ONE version: a module declares a single coreApi range
|
||||
// and is served one chunk, so a client that claimed 1.0.0 while the server
|
||||
// answered 1.1.0 would be two answers to one question.
|
||||
export const MODULE_API_VERSION = '1.15.0'
|
||||
export const MODULE_API_VERSION = '1.16.0'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_comment": "Generated event-trigger inventory - the authoritative freeze of CORE's engagement contract (docs/website/ENGAGEMENT.md 4.3). Regenerate with `npm run engagement:manifest` in website/server. A renamed variable, a changed type or a widened ceiling breaks stored templates and rules, so the diff here is the review signal. A module ships its own copy in its bundle; this file never contains one.",
|
||||
"moduleApiVersion": "1.15.0",
|
||||
"moduleApiVersion": "1.16.0",
|
||||
"triggers": [
|
||||
{
|
||||
"id": "event.phase.changed",
|
||||
|
||||
@@ -36,6 +36,26 @@ const authorize = require('./authorize')
|
||||
const settingsDb = require('../model/events/eventActionSettings.db')
|
||||
const registries = require('../modules/registries')
|
||||
|
||||
/** The most advice lines one step's dry run carries, and the longest one. */
|
||||
const ADVICE_MAX = 10
|
||||
const ADVICE_CHARS = 500
|
||||
|
||||
/**
|
||||
* A dry run's `detail.warnings`, as the lines a dry run shows: strings only,
|
||||
* trimmed, the empty dropped, at most ten, each cut at 500 characters. Anything
|
||||
* else a module put there is ignored rather than refused — advice that cannot be
|
||||
* read is not a reason to fail a plan that would run.
|
||||
*/
|
||||
function adviceOf(detail) {
|
||||
if (!detail || typeof detail !== 'object' || !Array.isArray(detail.warnings)) return []
|
||||
return detail.warnings
|
||||
.filter((w) => typeof w === 'string')
|
||||
.map((w) => w.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, ADVICE_MAX)
|
||||
.map((w) => (w.length > ADVICE_CHARS ? `${w.slice(0, ADVICE_CHARS - 1)}…` : w))
|
||||
}
|
||||
|
||||
/**
|
||||
* Dry-run a spec.
|
||||
*
|
||||
@@ -108,13 +128,21 @@ async function verifySpec(spec, { user = null, scope = '' } = {}) {
|
||||
)
|
||||
if (result.outcome === 'retry' || result.outcome === 'terminal') {
|
||||
findings.push({ ...where, level: 'error', code: 'refused', message: result.error })
|
||||
} else if (result.actionVersionDrift) {
|
||||
findings.push({
|
||||
...where,
|
||||
level: 'warning',
|
||||
code: 'version-drift',
|
||||
message: `authored against version ${result.actionVersionDrift.authored}; ${step.actionId} is now version ${result.actionVersionDrift.registered}`,
|
||||
})
|
||||
} else {
|
||||
if (result.actionVersionDrift) {
|
||||
findings.push({
|
||||
...where,
|
||||
level: 'warning',
|
||||
code: 'version-drift',
|
||||
message: `authored against version ${result.actionVersionDrift.authored}; ${step.actionId} is now version ${result.actionVersionDrift.registered}`,
|
||||
})
|
||||
}
|
||||
// MODULE_API 1.16.0: a module's ADVICE on a step that would run — the
|
||||
// world as it stands now, which may change before the run does (Rust
|
||||
// PLAN_REDESIGNS §11.4, D391). A warning, so it never fails the dry run.
|
||||
for (const message of adviceOf(result.detail)) {
|
||||
findings.push({ ...where, level: 'warning', code: 'advice', message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,4 +178,4 @@ async function verifySpec(spec, { user = null, scope = '' } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifySpec }
|
||||
module.exports = { verifySpec, adviceOf }
|
||||
|
||||
@@ -9,6 +9,15 @@
|
||||
// Deliberately separate from PROTOCOL_VERSION (which versions the shard wire and
|
||||
// has nothing to say about a website module) and from any module's own version.
|
||||
|
||||
// 1.16.0 — a dry run carries a module's advice: an action that answers
|
||||
// `verify: true` with `{ ok: true, detail: { warnings: [...] } }` has each line
|
||||
// shown as a WARNING finding (code `advice`) that does not fail the dry run
|
||||
// (Rust PLAN_REDESIGNS §11.4, D391: a raid base's spot is checked against the
|
||||
// world as it stands now, which may change before the run). Strings only, at most
|
||||
// ten, each cut at 500 characters. One optional reading of an existing member, so
|
||||
// minor. Module-uo declares `^1.10.0` and says no warnings: its dry runs read
|
||||
// exactly as they did. STEAM_SIGNIN.md's claim moves to the next minor again.
|
||||
//
|
||||
// 1.15.0 — a failed step may say when to try again: `{ ok: false, retry: true,
|
||||
// retryAfterMs }` (Rust PLAN_REDESIGNS §14.3.13, D371; docs/website/EVENTS.md §F).
|
||||
// Clamped to one minute–thirty minutes, never refused; absent, the flat
|
||||
@@ -207,6 +216,6 @@
|
||||
// an admin action a module performs belongs in core's one audit log, the
|
||||
// extension slot needs the user its prefix names, and §2.7 forbids a module
|
||||
// reading core's `APP_BASE_URL` for itself. Additions only, so minor.
|
||||
const MODULE_API_VERSION = '1.15.0'
|
||||
const MODULE_API_VERSION = '1.16.0'
|
||||
|
||||
module.exports = { MODULE_API_VERSION }
|
||||
|
||||
@@ -112,7 +112,7 @@ test('the version a module declares against satisfies ^1.10.0', () => {
|
||||
// 1.10.0 loading — Module-uo declares exactly `^1.10.0` — and this is the
|
||||
// assertion that says it does. The number itself is pinned in version.js.
|
||||
assert.ok(semver.satisfies(MODULE_API_VERSION, '^1.10.0'), MODULE_API_VERSION)
|
||||
assert.equal(MODULE_API_VERSION, '1.15.0')
|
||||
assert.equal(MODULE_API_VERSION, '1.16.0')
|
||||
})
|
||||
|
||||
test('a module registers actions, budgets, leases and option sources', () => {
|
||||
|
||||
@@ -240,6 +240,44 @@ test('a step authored against an older action version is a WARNING, not an error
|
||||
assert.match(report.findings[0].message, /authored against version 1/)
|
||||
})
|
||||
|
||||
test('a module’s advice is a WARNING finding per line, and the dry run still passes (MODULE_API 1.16.0)', async () => {
|
||||
// Rust PLAN_REDESIGNS §11.4, D391: a raid base's spot is checked against the
|
||||
// world as it stands now. A player standing on it may walk off before the run,
|
||||
// so the check is advice, never a reason the plan fails.
|
||||
register([
|
||||
recorder('test.spawn', {
|
||||
answer: { ok: true, detail: { warnings: ['the spot is refused now: a player is within 50 m', ' ', 'at the spot now: Rock is too large', 7] } },
|
||||
}),
|
||||
])
|
||||
const report = await verifySpec(spec([step('test.spawn')]), { user: ADMIN })
|
||||
assert.equal(report.ok, true, 'advice must not fail the dry run')
|
||||
assert.deepEqual(
|
||||
report.findings.map((f) => [f.level, f.code, f.message, f.actionId]),
|
||||
[
|
||||
['warning', 'advice', 'the spot is refused now: a player is within 50 m', 'test.spawn'],
|
||||
['warning', 'advice', 'at the spot now: Rock is too large', 'test.spawn'],
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
test('advice is bounded: at most ten lines, each cut at 500 characters, and anything but strings ignored', async () => {
|
||||
const { adviceOf } = require('../src/events/verify')
|
||||
assert.deepEqual(adviceOf(null), [])
|
||||
assert.deepEqual(adviceOf({ warnings: 'one line' }), [])
|
||||
assert.deepEqual(adviceOf({ note: 'no warnings here' }), [])
|
||||
assert.equal(adviceOf({ warnings: Array.from({ length: 30 }, (_, i) => `line ${i}`) }).length, 10)
|
||||
const long = adviceOf({ warnings: ['x'.repeat(900)] })[0]
|
||||
assert.equal(long.length, 500)
|
||||
assert.ok(long.endsWith('…'))
|
||||
})
|
||||
|
||||
test('a refused step carries no advice: the refusal is the finding', async () => {
|
||||
register([recorder('test.spawn', { answer: { ok: false, retry: false, error: 'no such base', detail: { warnings: ['ignored'] } } })])
|
||||
const report = await verifySpec(spec([step('test.spawn')]), { user: ADMIN })
|
||||
assert.equal(report.ok, false)
|
||||
assert.deepEqual(report.findings.map((f) => f.code), ['refused'])
|
||||
})
|
||||
|
||||
test('a finding says which phase and which step it is about', async () => {
|
||||
// A twelve-step definition needs the finding anchored, or the author is left
|
||||
// reading the message and counting rows.
|
||||
|
||||
Reference in New Issue
Block a user