All checks were successful
PR checks / checks (pull_request) Successful in 1m5s
Builds `/app/` and `/beta/`, the SQLite signup store, the rate limiting and the export CLI of PLAN.md §8, and adds this repository's first test suite. Four decisions of record, D26–D29 (§8, "How phase 5 built the app and the beta"): - D26 — the screenshot slot ships empty, reserved for phase 9. §10 promised `/app/` "the 14 existing screenshots"; they are a July trusted-device smoke test against an unseeded dev instance, captured before the theming work, and five of the fourteen are two-factor prompts. Shipping them would break D4. Phase 9 already builds the rig, so it gains an emulator pass. - D27 — the public demo is the tester target. `ConnectScreen.kt` gates the whole app on a validated deployment address, so a tester needs somewhere to point it. The beta therefore waits on the demo VM, and the page says so. - D28 — `/beta` handles its own POST; there is no `/api/beta-signup`. An endpoint cannot report a validation error without JavaScript. §6's diagram is amended. - D29 — the APK and the beta get equal billing, and the APK link is off: `androidApk.serviceable` is false because the published v0.5.0 build does not work. The panel stays and states that plainly rather than being removed. Three mechanisms the plan did not anticipate: - `liveBrand()` — a server-rendered page never passes through the boot rewrite, so `/beta` reads the mounted brand.json itself. Pasting the Play opt-in URL in takes effect on the next request rather than the next restart. - `checkLinks.mjs` derives on-demand routes from `prerender = false` in the source. A PLANNED_ROUTES entry would have been wrong: its reverse check fires when a route has been built, and an on-demand route never produces a file, so the entry could never rot out. - `npm test` — the five existing checks all read built output, and none of this logic appears there. A honeypot can stop working and leave the build identical. Also: `checkFacts.mjs` gains the APK assets and `minSdk`, and learns that RFC 2606 reserved domains are not contact addresses; the D13 rule is otherwise unchanged. Verified end to end against the built server: every outcome renders with no JavaScript, cross-origin POSTs are refused, a mounted opt-in URL appears without a restart, and the export CLI round-trips. Co-Authored-By: Claude <noreply@anthropic.com>
210 lines
10 KiB
JavaScript
210 lines
10 KiB
JavaScript
/**
|
|
* notBuilt.mjs — the deliberate absences of PLAN.md §2, as data (D22).
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* WHY THIS IS A LIST AND NOT A PARAGRAPH
|
|
* ---------------------------------------------------------------------------------------
|
|
* §2 calls its absent-features list "as load-bearing as the rest", and the homepage already
|
|
* promises a reader they will find it on both `/features/` and `/integrations/`. Two pages
|
|
* each writing their own version of "what we did not build" is how the inconvenient half
|
|
* quietly stops being mentioned on one of them — the same failure `capabilities.mjs` exists
|
|
* to prevent, pointed the other way.
|
|
*
|
|
* So: one list, tagged with the pages that show it. `/modules/` reads it too, because the
|
|
* three absences a module author most needs to know about are all here.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* THE RULE FOR ADDING ONE
|
|
* ---------------------------------------------------------------------------------------
|
|
* An entry belongs here when a reasonable reader would assume the thing exists. That is a
|
|
* higher bar than "we have not built it" — the site is not an inventory of everything
|
|
* absent from it — and a lower bar than "someone asked for it". Matrix is here because the
|
|
* original brief for this site listed it as a feature; the installer's missing platforms
|
|
* are here because every other tool in the world ships a macOS build.
|
|
*
|
|
* Each entry says what it is, and then why not. The "why not" is the point: an absence with
|
|
* a reason reads as a decision, and an absence without one reads as a gap. Where the
|
|
* reasoning was written down somewhere in the open, the entry links to it on a BRANCH path
|
|
* — `scripts/checkLinks.mjs` fails a commit permalink, because a permalink is a fact frozen
|
|
* at a sha while the document keeps moving.
|
|
*
|
|
* `resolvedBy` is not decoration. D8 gives the Integration Kit's draft status a defined
|
|
* removal condition, and stating the exit condition on the others too is what stops this
|
|
* file becoming a list of permanent apologies.
|
|
*/
|
|
|
|
const GITEA = 'https://gitea.whitlocktech.com/RunicGateway';
|
|
|
|
/**
|
|
* `scope` — which pages render the entry.
|
|
*
|
|
* `features` /features/, under the capability groups
|
|
* `integrations` /integrations/, under the integrations that do exist
|
|
* `modules` /modules/, where a module author is deciding whether to start
|
|
*
|
|
* Typed rather than inferred, for the same reason `capabilities.mjs` is: `link` is present
|
|
* on four entries out of six, and an inferred union makes `entry.link` unreadable on the
|
|
* page that renders all of them.
|
|
*
|
|
* @typedef {object} Absence
|
|
* @property {string} id
|
|
* @property {string[]} scope
|
|
* @property {string} title
|
|
* @property {string} body
|
|
* @property {string} resolvedBy What would make this entry go away. Never optional.
|
|
* @property {{ href: string, label: string }} [link]
|
|
*
|
|
* @type {Absence[]}
|
|
*/
|
|
export const notBuilt = [
|
|
{
|
|
id: 'matrix',
|
|
scope: ['integrations'],
|
|
title: 'Matrix',
|
|
body:
|
|
'Researched properly and then declined. Matrix has no channel-with-overwrites, no ' +
|
|
'role object, no voice channel of its own — voice is an RTC session needing a media ' +
|
|
'server the homeserver does not ship — and no way to register a slash command. Of ' +
|
|
'the five things a shared chat interface would have to name, an honest Matrix ' +
|
|
'implementation could provide two. What came out of that work was a capability ' +
|
|
'contract rather than an integration.',
|
|
resolvedBy:
|
|
'Nothing planned. If the protocol grows the missing four, the contract is already ' +
|
|
'the shape a second platform would plug into.',
|
|
link: { href: `${GITEA}/docs/src/branch/main/website/TEAMS.md`, label: 'The research, in full' },
|
|
},
|
|
{
|
|
id: 'multi-module',
|
|
scope: ['features', 'integrations', 'modules'],
|
|
title: 'More than one game module at a time',
|
|
body:
|
|
'One active module per deployment. The database columns that would scope data to a ' +
|
|
'module exist and are populated, so the door is not nailed shut, but nothing ' +
|
|
'exercises them and no interface offers it. A community running two games runs two ' +
|
|
'deployments.',
|
|
resolvedBy:
|
|
'Someone needing it. The schema was shaped to keep it possible, which is a different ' +
|
|
'thing from planning it.',
|
|
},
|
|
{
|
|
id: 'second-module',
|
|
scope: ['integrations', 'modules'],
|
|
title: 'A second game module',
|
|
body:
|
|
'There is exactly one, and it is Ultima Online. A paper dry-run for a Rust module ' +
|
|
'exists and is deliberately unimplemented — it was written to test whether the ' +
|
|
'module contract generalises, not to ship. Until a second one exists, "any game" is ' +
|
|
'an argument about a shape rather than a demonstration.',
|
|
resolvedBy: 'The first module built for a game that is not Ultima Online.',
|
|
link: { href: `${GITEA}/docs/src/branch/main/modules/rust-dryrun.md`, label: 'The dry-run' },
|
|
},
|
|
{
|
|
id: 'integration-kit-draft',
|
|
scope: ['integrations', 'modules'],
|
|
title: 'A finished Integration Kit',
|
|
body:
|
|
'The kit that teaches you to put a different game on this platform describes itself ' +
|
|
'as a draft, and it is right to. It has four chapters, a working template and a CI ' +
|
|
'job that builds that template against a pinned core — but nobody outside this ' +
|
|
'project has yet followed it to a working module, which is the only test of a set of ' +
|
|
'instructions that counts.',
|
|
resolvedBy:
|
|
'Someone outside this project building a working module for a new game by following ' +
|
|
"it alone. That is the kit's own stated condition, not one invented here.",
|
|
link: { href: `${GITEA}/Integration-kit/src/branch/main/README.md`, label: 'The kit' },
|
|
},
|
|
{
|
|
id: 'installer-platforms',
|
|
scope: ['features'],
|
|
title: 'A macOS or Windows-on-ARM installer',
|
|
body:
|
|
'Linux and Windows, on x86-64, plus Linux on arm64. The missing builds are missing ' +
|
|
'on purpose: the installer runs on the machine the game server lives on, because the ' +
|
|
'game and the bridge have to share a host, and no game server anybody runs is on ' +
|
|
'either of those platforms.',
|
|
resolvedBy: 'A game server that runs there.',
|
|
link: { href: `${GITEA}/docs/src/branch/main/installer/INSTALL.md`, label: 'The operator guide' },
|
|
},
|
|
{
|
|
id: 'public-demo',
|
|
// Phase 5 added `app` and `beta`, and that is not tidying. D27 makes the demo the
|
|
// deployment a beta tester connects to, so on those two pages this stopped being a
|
|
// thing the site lacks and became the thing the beta is waiting for. An absence that
|
|
// blocks a call to action has to be on the page carrying that call to action.
|
|
scope: ['features', 'app', 'beta'],
|
|
title: 'A public demo you can click through',
|
|
body:
|
|
'Planned and out of scope today: a virtual machine running the whole stack including ' +
|
|
'a game server, with settings locked down and an hourly reset. Until it exists this ' +
|
|
'site does not link to one, and there is no screenshot here of something that is not ' +
|
|
'running somewhere.',
|
|
resolvedBy:
|
|
'The machine being stood up. The site is already built to gain it by way of one line ' +
|
|
'in a configuration file, rather than a rebuild.',
|
|
},
|
|
|
|
/* ---------------------------------------------------------------------------------------
|
|
THE ANDROID CLIENT (phase 5)
|
|
|
|
These are about the app rather than the platform, and they live here rather than in a
|
|
second list on `/app/` for the reason this file exists at all: two lists of absences
|
|
drift, and the one that drifts is always the one nobody is looking at. The `scope` tag
|
|
is what keeps them off the pages they would be noise on.
|
|
--------------------------------------------------------------------------------------- */
|
|
{
|
|
id: 'ios-app',
|
|
scope: ['app'],
|
|
title: 'An iOS app',
|
|
body:
|
|
'Android only. There is no iOS build, no cross-platform layer waiting to grow one, ' +
|
|
'and no work in progress — the app is native Kotlin and Compose, so a second ' +
|
|
'platform would be a second app rather than another build target.',
|
|
resolvedBy: 'Nothing planned. A deployment is a website first, and that works on any phone.',
|
|
},
|
|
{
|
|
id: 'play-listing',
|
|
scope: ['app', 'beta'],
|
|
title: 'A listing on Google Play',
|
|
body:
|
|
'The app is not published. A developer account exists; the closed test is the next ' +
|
|
'step, and production access cannot even be requested until a run of testers has ' +
|
|
'been opted in continuously — which is what the beta is for, and why the beta is not ' +
|
|
'a formality.',
|
|
resolvedBy: 'The closed test running its course, and then a production review.',
|
|
link: { href: '/beta/', label: 'The closed beta' },
|
|
},
|
|
{
|
|
id: 'app-offline',
|
|
scope: ['app'],
|
|
title: 'Reading anything offline',
|
|
body:
|
|
'Every screen is a live read against the deployment. Nothing is cached for offline ' +
|
|
'use, so the app with no signal is an app with no content.',
|
|
resolvedBy: 'Somebody asking for it. Nobody has.',
|
|
},
|
|
];
|
|
|
|
/** The entries a given page renders, in file order. */
|
|
export function notBuiltFor(scope) {
|
|
return notBuilt.filter((entry) => entry.scope.includes(scope));
|
|
}
|
|
|
|
/**
|
|
* Fails the build when a scope renders nothing.
|
|
*
|
|
* The homepage tells a reader in as many words that the absences are listed "on features
|
|
* and integrations". A tag typo, or an entry removed without checking who was showing it,
|
|
* turns that sentence into a promise the site does not keep — and an empty section is the
|
|
* one defect that looks deliberate, because a page with nothing under a heading reads as a
|
|
* page with nothing to admit.
|
|
*/
|
|
export function assertScopeNonEmpty(scope) {
|
|
if (notBuiltFor(scope).length) return;
|
|
|
|
throw new Error(
|
|
`src/data/notBuilt.mjs has no entry tagged "${scope}", but a page is rendering that scope.\n` +
|
|
`\nThe homepage promises this list appears on /features/ and /integrations/ (§2, D22).\n` +
|
|
`Tag an entry with "${scope}", or take the section off the page that asks for it.\n`
|
|
);
|
|
}
|