/** * 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` ); }