From 8bf68958466e22f42eee7cc38778d82026ccc070 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sun, 6 Sep 2026 16:24:20 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(web):=20the=20Plans=20page,=20inverted?= =?UTF-8?q?=20=E2=80=94=20a=20transparency=20artifact=20with=20nothing=20t?= =?UTF-8?q?o=20click=20(#706)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A retail broker's Plans & Features page is a tier matrix with "Current plan" and "Upgrade" badges. It is radically clear about pricing and honest AS MARKETING, because every row exists to move a reader one row down. Jesse paywalls even paper trading. keel's constitution runs the other way. Two of its eight numbered lines are "Paper is free and unlimited, forever" and "The free engine is never a demo". So this page takes the clarity and refuses the gating: what runs on this device and why it costs nothing, what a tier would change if one existed, and the named trigger that must fire before any of it does. TRACEABILITY IS THE ACCEPTANCE CRITERION, SO IT IS MECHANICAL "Every claim on the page traces to the evolution plan or the constitution -- no aspirational copy." A page of marketing sentences that merely SOUNDED like the project's documents would pass a human reviewer and rot within a month. So every claim carries the repository-relative path it was quoted from, and the tests open that file and look for the sentence. Verbatim, never paraphrased: a paraphrase is a new claim wearing the authority of an old one. The check runs in BOTH directions, and the second is why it earns its place. A claim nobody can trace fails the build -- and so does a claim the project has since edited out of its own documents, which is the failure that would otherwise go unnoticed: a promise page still making a promise nobody kept. Demonstrated: adding a ninth line to §4 of the evolution plan fails this until the page carries it, because the count comes out of the document rather than a literal. THE WHOLE CONSTITUTION, NOT A SELECTION Eight lines. A page that quoted seven would be choosing which of the project's own commitments to show a reader, on the page whose entire subject is what the project promises. NOTHING IS FOR SALE, AND THE PAGE SAYS SO FROM THE TABLE ADR 0004's answer is "not now" and none of its four triggers has fired, so no tier is `shipped`. That is a FIELD rather than a sentence, because "this does not exist yet" is the most important thing the table says and a sentence can be edited past without anything failing -- and the headline is derived from the field, so the two cannot come to disagree. The direction they would disagree in is a shipped product under a headline saying there is nothing to buy. `available_now` is separate and true of exactly one row: the free tier is what the reader is already running. Four rows presented as four CHOICES would be a paywall matrix with the prices greyed out. THE REFUSALS ARE STRUCTURAL, NOT EDITORIAL There is no `cta`, `url`, `action` or `contact` key on this payload, and nothing beneath it a client could build one from -- asserted over the serialised JSON, so a key added anywhere inside is caught. `render.js` cannot make a button out of a page that never sends it a destination. The view builds no anchor, no button, no form and binds no handler, pinned against the parsed function bodies with string literals KEPT (the earlier lesson in this series: a scan that strips them cannot see `el("a")`). Citations are `` and not links -- `keel serve` is a loopback SQLite reader with no document server behind it, so a link would 404, and a path a reader opens in their own checkout is the honest form. And the tier table has no sort control, on the route or in the view. A tier table ordered by price is a shopping comparison. NO DATABASE The one route here whose subject is the PROJECT rather than the deployment. It reads no repository, no config and no network, so it answers on a machine with nothing set up -- a page explaining that keel is free and runs entirely on the operator's own hardware would be a strange one to gate behind having already installed it. Twelve mutants killed: a constitution line softened into marketing, the constitution quoted incompletely, a price drifting from the published one, a tier marked shipped ahead of the PR that ships it, the page growing an upgrade link, the payload carrying a checkout destination, the refusal list dropped from the page, the table becoming sortable by price, a citation becoming a link, the free tier ceasing to be the one that exists, `for_sale` hard-coded rather than read from the table, and the route starting to require a database. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KZZxmspQXe5qJ9FAsG13s6 --- keel/commands/plans.py | 247 ++++++++++++++++++++++++++++++++ keel/web/api.py | 25 ++++ keel/web/payload.py | 115 +++++++++++++++ keel/web/static/css/keel.css | 22 +++ keel/web/static/index.html | 8 +- keel/web/static/js/main.js | 3 + keel/web/static/js/render.js | 122 ++++++++++++++++ keel/web/staticfiles.py | 1 + tests/commands/test_plans.py | 170 ++++++++++++++++++++++ tests/web/test_api.py | 9 +- tests/web/test_client_assets.py | 61 ++++++++ 11 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 keel/commands/plans.py create mode 100644 tests/commands/test_plans.py diff --git a/keel/commands/plans.py b/keel/commands/plans.py new file mode 100644 index 0000000..215d04d --- /dev/null +++ b/keel/commands/plans.py @@ -0,0 +1,247 @@ +"""The Plans page, inverted -- a transparency artifact, not a paywall matrix (#706). + +Alpaca's Plans & Features page is a tier matrix with "Current plan" and "Upgrade" badges. It is +radically clear about pricing and it is honest *as marketing*: every row exists to move a reader +one row down. Jesse paywalls even paper trading. + +keel's constitution runs the other way -- "Paper is free and unlimited, forever" and "The free +engine is never a demo" are two of its eight numbered lines. So this page takes the clarity and +refuses the gating. It states what runs on this device and why it costs nothing, what a tier would +change if one ever existed, and the named trigger that must fire before any of it does. + +── EVERY CLAIM CARRIES THE DOCUMENT IT CAME FROM, AND A TEST READS THAT DOCUMENT ──────────────── + +The acceptance criterion is traceability, so it is mechanical rather than asserted. A page of +marketing sentences that merely SOUNDED like the project's documents would pass a human reviewer +and rot within a month; the sentences here are quoted verbatim and `tests/commands/test_plans.py` +opens each cited file and looks for them. + +That check runs in both directions, and the second one is why it is worth having. A claim nobody +can trace fails the build -- but so does a claim the project has since edited out of its own +documents, which is the failure that would otherwise go unnoticed: a promise page still making a +promise nobody kept. + +── NOTHING HERE IS FOR SALE, AND THE PAGE SAYS SO RATHER THAN IMPLYING IT ─────────────────────── + +ADR 0004's answer is "not now" and none of its four triggers has fired. No tier below is +`shipped`. There is no price a reader can act on, no address, no link and no instruction -- +because a page that told someone how to pay would have shipped the tier. If a tier ever does +ship, its `shipped` flag is flipped by the PR that ships it, never ahead of it. + +── AND NO DATABASE ────────────────────────────────────────────────────────────────────────────── + +This page describes the project, not the deployment. It reads no repository, no config and no +network, which is why its route can answer on a machine with nothing set up at all. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + +#: The two documents every claim on this page is quoted from. +EVOLUTION_PLAN = "docs/evolution-plan.md" +DECISION_RECORD = "docs/decisions/0004-monetisation-not-now.md" + + +@dataclass(frozen=True) +class Claim: + """One quoted sentence and the file it came from. + + `source` is a repository-relative path rather than a title, so the test that checks this can + OPEN it. A citation naming "the evolution plan" would be a citation nothing could verify -- + worse than none, because it looks checkable. + """ + + text: str + source: str + + +@dataclass(frozen=True) +class Tier: + """One row of the Phase F table, as published. + + `shipped` is `False` on every row and is the only field a future PR should change. It exists + as a field rather than being implied by the page's prose because "this does not exist yet" is + the single most important thing this table says, and a sentence can be edited past without + anything failing. + + `available_now` is separate and true of exactly one row: the free tier is what the reader is + already running. A table that presented four rows as four CHOICES would be a paywall matrix + with the prices greyed out. + """ + + name: str + price: str + buys: str + promise: str + source: str + shipped: bool = False + available_now: bool = False + + +@dataclass(frozen=True) +class Trigger: + """One of ADR 0004's four triggers -- what would have to happen before a tier exists. + + The record says the four are the complete set and that a new trigger is a new decision record + rather than an edit, so this page may not grow a fifth. + """ + + number: int + text: str + source: str = DECISION_RECORD + + +#: The constitution, §4 of the evolution plan, verbatim and entire. +#: +#: ALL EIGHT. A page that quoted seven would be choosing which of the project's own commitments to +#: show a reader, on the page whose whole subject is what the project promises -- and the test +#: counts the numbered lines in the document rather than against a literal, so a ninth line fails +#: the build until this carries it. +CONSTITUTION: tuple[Claim, ...] = ( + Claim("A tool that cannot say no is a flattery tool.", EVOLUTION_PLAN), + Claim("Scores report and gate; they never rank (the Strathern rail).", EVOLUTION_PLAN), + Claim("Fees are priced at what was actually paid.", EVOLUTION_PLAN), + Claim("Paper is free and unlimited, forever.", EVOLUTION_PLAN), + Claim("Every attestation is human-sourced, or refused.", EVOLUTION_PLAN), + Claim("The free engine is never a demo.", EVOLUTION_PLAN), + Claim("The operator's servers never hold venue keys.", EVOLUTION_PLAN), + Claim("Negative results are published.", EVOLUTION_PLAN), +) + +#: Phase F's tier table, as a published intention. Nothing here is buyable. +TIERS: tuple[Tier, ...] = ( + Tier( + name="Free", + price="$0, forever", + buys="The full engine. Never a demo.", + promise="We see nothing", + source=EVOLUTION_PLAN, + available_now=True, + ), + Tier( + name='Pro — "your box, anywhere"', + price="$14/mo · $140/yr prepay", + buys=( + "managed `keel link`, signed+notarized installers (the #438 certs become Pro " + "value), managed updates, Web Push plumbing, priority support" + ), + promise="We can't see your data", + source=EVOLUTION_PLAN, + ), + Tier( + name="Founder — lifetime", + price="$399, first 200, then gone", + buys="Everything in Pro, forever, founder badge", + promise="Grandfathering is a promise, not a promo", + source=EVOLUTION_PLAN, + ), + Tier( + name='Hosted-confirm — "our brain, your keys"', + price="$49/mo · $450/yr (built last)", + buys="Full hosted engine; your device holds the keys and approves every order", + promise="We can see, we can't act", + source=EVOLUTION_PLAN, + ), +) + +#: How ADR 0004 frames what would reopen the question. Quoted because the framing IS the claim: +#: a trigger measured in revenue would make this page a countdown, and one measured in users makes +#: it a condition a reader can check for themselves. +TRIGGER_FRAMING = "Framed in users and demonstrated demand" +TRIGGER_FRAMING_SOURCE = DECISION_RECORD + +#: The four triggers, abbreviated to their firing condition. The record's own table carries the +#: evidence each one needs and who observes it; this page carries what would have to be true. +TRIGGERS: tuple[Trigger, ...] = ( + Trigger(1, "keel reaches an audience shaped like Jesse's pre-2021 one"), + Trigger( + 2, + "The headline finding reverses — a shipped rule family is measured net-positive at the " + "taker fee actually paid", + ), + Trigger( + 3, + "Operators repeatedly and unprompted ask for something that costs real money to run on " + "their behalf", + ), + Trigger( + 4, + "keel's own maintenance load demonstrably exceeds what volunteer, spare-time work can " + "carry", + ), +) + +#: What no tier will ever gate. The load-bearing half of this page, and the half a later +#: contributor would be most tempted to soften -- so each line is quoted from a document that +#: already committed to it rather than written fresh here. +NEVER_PAYWALLED: tuple[Claim, ...] = ( + Claim("Paper is free and unlimited, forever.", EVOLUTION_PLAN), + Claim("Fees are priced at what was actually paid.", EVOLUTION_PLAN), + Claim("Every attestation is human-sourced, or refused.", EVOLUTION_PLAN), + Claim( + "Live order placement (`keel/execution/`) stays inside the Apache-2.0 tree in full, " + "always.", + DECISION_RECORD, + ), + Claim( + "**No engine feature gates for paid tiers** — the free engine is never a demo.", + "docs/architecture.md", + ), + Claim( + "**Affiliate or referral links to any trading venue, broker, or exchange are never " + "added**", + DECISION_RECORD, + ), +) + +#: What the page says about today, in one sentence. Held here rather than in the renderer for the +#: usual reason (Rule 2): it is a judgement about the project's state, and judgements are made in +#: Python. +NOTHING_FOR_SALE = ( + "Nothing on this page is for sale. keel is free, entirely, and no tier below exists — " + "the prices are a published intention, not an offer." +) + + +def every_claim() -> Iterator[Claim]: + """Every traceable claim on the page, for the test that checks each cited file exists.""" + yield from CONSTITUTION + yield from NEVER_PAYWALLED + for tier in TIERS: + yield Claim(tier.promise, tier.source) + for trigger in TRIGGERS: + yield Claim(trigger.text, trigger.source) + yield Claim(TRIGGER_FRAMING, TRIGGER_FRAMING_SOURCE) + + +@dataclass(frozen=True) +class PlansReport: + now_ts: int + constitution: tuple[Claim, ...] + tiers: tuple[Tier, ...] + triggers: tuple[Trigger, ...] + never_paywalled: tuple[Claim, ...] + + @property + def anything_for_sale(self) -> bool: + """Whether any tier has actually shipped. + + Derived from the table rather than from a constant, so flipping one `shipped` flag is all + it takes for the page to stop saying nothing is for sale -- and so that the sentence and + the table can never disagree. + """ + return any(tier.shipped for tier in self.tiers) + + +def gather_plans(*, now_ts: int) -> PlansReport: + """The page. No repository, no config, no network -- it describes the project, not the + deployment, which is why its route answers on a machine with nothing set up.""" + return PlansReport( + now_ts=now_ts, + constitution=CONSTITUTION, + tiers=TIERS, + triggers=TRIGGERS, + never_paywalled=NEVER_PAYWALLED, + ) diff --git a/keel/web/api.py b/keel/web/api.py index cd14abe..69acaac 100644 --- a/keel/web/api.py +++ b/keel/web/api.py @@ -740,6 +740,23 @@ def read_journal(cfg: ServeConfig, query: Query, _state: Any, now_ts: int) -> di ) +def read_plans(_cfg: ServeConfig, _query: Query, _state: Any, now_ts: int) -> dict[str, Any]: + """The Plans page (#706) -- the one route whose subject is the PROJECT, not the deployment. + + No repository, no config, no network. Every string it answers with is quoted from a document + in this repository and carries that document's path, so a reader can check the page against + the source and `tests/commands/test_plans.py` can check it on every build. + + `needs_database=False` for the same reason `/api/config` and `/api/venues` carry it: this + answers identically on a machine with nothing set up, and a page explaining that keel is free + and runs entirely on the operator's own hardware would be a strange one to gate behind having + already installed it. + """ + from keel.commands.plans import gather_plans + + return payload.plans_payload(gather_plans(now_ts=now_ts)) + + def read_rules(cfg: ServeConfig, _query: Query, _state: Any, _now_ts: int) -> dict[str, Any]: repo = open_repo(cfg.db_path) try: @@ -1007,6 +1024,14 @@ class ApiRoute: collection="rules", sortable=("id", "kind", "status", "created_at", "promoted_at", "demoted_at"), ), + # #706. `needs_database=False`, and NO `collection`/`sortable`: there is nothing on this page + # to sort. A tier table ordered by price is a shopping comparison, and the whole point of the + # inversion is that these four rows are not four choices. + "/api/plans": ApiRoute( + html_route="/plans", + read=read_plans, + needs_database=False, + ), "/api/venues": ApiRoute( html_route="/venues", read=read_venues, diff --git a/keel/web/payload.py b/keel/web/payload.py index c82eec7..ee1743c 100644 --- a/keel/web/payload.py +++ b/keel/web/payload.py @@ -140,6 +140,7 @@ # rather than leaving two `JournalReport`s in one namespace to be told apart by context. from keel.commands.journal import JournalReport as DiscretionaryJournal from keel.commands.orders import OrderRow, OrdersReport + from keel.commands.plans import Claim, PlansReport, Tier, Trigger from keel.commands.positions import PositionRow, PositionsReport from keel.commands.research_record import RuleExploration, TrialRow, TrialsReport from keel.commands.slippage import SlippageReport, SlippageRow @@ -1367,6 +1368,120 @@ def _rules_followed_state(value: bool | None) -> str: return NEUTRAL if value else WARN +# -- plans, inverted (#706) ------------------------------------------------------------------------ +# +# THE ONE PAGE IN THIS APPLICATION WHOSE SUBJECT IS THE PROJECT RATHER THAN THE DEPLOYMENT, and +# the only one that reads no repository at all. +# +# Every string below is quoted from a document and carries the file it came from, checked by +# `tests/commands/test_plans.py` against the file itself. Nothing here is composed, summarised or +# softened on the way to the wire -- the whole value of the page is that a reader can check it, +# and a payload that rephrased a promise would be the first place the checking stopped working. + + +def _claim_payload(claim: Claim) -> dict[str, Any]: + """One quoted sentence and its citation. + + `source` is a repository-relative PATH, not a title, because a reader with the repository in + front of them can open it -- and because the test that keeps this page honest opens it too. A + citation naming "the evolution plan" would look checkable and be nothing of the kind. + """ + return {"text": claim.text, "source": claim.source} + + +def _tier_payload(tier: Tier) -> dict[str, Any]: + """One row of the Phase F table, and whether it exists. + + `status` is a `label` and the two readings are NOT good and bad. The free tier is what the + reader is already running (`GOOD` -- it is the one thing on this page that is true today), and + every other row is `UNKNOWN`: not warned about, not promised, simply not a thing yet. `WARN` + would read as a caution about a product, and there is no product to caution anyone about. + + `shipped` and `available_now` are separate fields on the report and collapse to one word here, + because a client rendering two booleans would be deciding what their combination means, which + is a judgement (Rule 2). + """ + return { + "name": tier.name, + "price": tier.price, + "buys": tier.buys, + "promise": tier.promise, + "source": tier.source, + "status": label( + _tier_status(tier), + display=_TIER_STATUS_NOTES[_tier_status(tier)], + state=GOOD if tier.available_now else UNKNOWN, + ), + } + + +#: What each tier's one-word status MEANS. The word is short enough for a table cell; the sentence +#: is what stops a reader taking "planned" for "coming soon", which is the reading a page like this +#: is normally built to encourage. +_TIER_STATUS_NOTES: Mapping[str, str] = { + "running": "this is what you are running now, and it is the whole engine", + "planned": "does not exist — a published intention, gated on a trigger that has not fired", + "shipped": "shipped", +} + + +def _tier_status(tier: Tier) -> str: + """`running` for the free tier, `planned` for the rest -- and `shipped` for whichever row a + future PR flips, which is the only edit this page should ever need.""" + if tier.shipped: + return "shipped" + return "running" if tier.available_now else "planned" + + +def _trigger_payload(trigger: Trigger) -> dict[str, Any]: + return { + "number": str(trigger.number), + "text": trigger.text, + "source": trigger.source, + } + + +def plans_payload(report: PlansReport) -> dict[str, Any]: + """The transparency artifact (#706). + + **The refusals are structural, not editorial.** There is no `cta`, no `url`, no `action` and + no `contact` key on this payload, and there is nothing for a client to build one out of. + `render.js` cannot make a button out of a page that never sends it a destination. + + `for_sale` is a `flag` derived from the tier table rather than a constant, so the sentence and + the table cannot come to disagree: flipping one tier's `shipped` is all it takes for the page + to stop saying nothing is for sale. + """ + from keel.commands.plans import ( + NOTHING_FOR_SALE, + TRIGGER_FRAMING, + TRIGGER_FRAMING_SOURCE, + ) + from keel.commands.plans import ( + Claim as _Claim, + ) + + return { + "as_of": iso(report.now_ts), + "generated_at": moment(report.now_ts), + "for_sale": flag( + report.anything_for_sale, + on="a tier has shipped — this page is out of date", + off=NOTHING_FOR_SALE, + on_state=WARN, + off_state=NEUTRAL, + ), + "constitution": [_claim_payload(claim) for claim in report.constitution], + "tiers": [_tier_payload(tier) for tier in report.tiers], + # The framing rides as a CLAIM like every other sentence here -- it is quoted from ADR + # 0004 and is checkable against it, and a trigger measured in revenue rather than users + # would turn this page into a countdown. + "trigger_framing": _claim_payload(_Claim(TRIGGER_FRAMING, TRIGGER_FRAMING_SOURCE)), + "triggers": [_trigger_payload(trigger) for trigger in report.triggers], + "never_paywalled": [_claim_payload(claim) for claim in report.never_paywalled], + } + + # -- activity ------------------------------------------------------------------------------------ diff --git a/keel/web/static/css/keel.css b/keel/web/static/css/keel.css index 71efd29..8834e45 100644 --- a/keel/web/static/css/keel.css +++ b/keel/web/static/css/keel.css @@ -256,6 +256,28 @@ header .mode-paper { color: var(--muted); } header .mode-confirm, header .mode-live { color: var(--accent); border-color: var(--accent); } +/* THE PLANS PAGE's claim list (#706). + * + * A quoted sentence with its citation beside it, not beneath it: a citation a reader has to go + * looking for is one they take on trust, and taking this page on trust is the one thing it cannot + * afford. `list-style: none` because the numbering belongs to the document being quoted, not to + * this rendering of it -- a page that renumbered the constitution would be paraphrasing it in a + * different way. */ +.claims { + list-style: none; + margin: 0.5rem 0 1rem; + padding: 0; +} +.claims li { + padding: 0.35rem 0; + border-bottom: 1px solid var(--line); +} +.claims li:last-child { border-bottom: none; } +.claims code { + font-size: 0.8em; + color: var(--muted); +} + /* THE SESSION CHIP (#704): profile · mode · equity state, as one group. * * A group and not three loose items, because the three are one answer: "which deployment is this diff --git a/keel/web/static/index.html b/keel/web/static/index.html index 90074c8..c2bd8fb 100644 --- a/keel/web/static/index.html +++ b/keel/web/static/index.html @@ -25,7 +25,7 @@ 2. It is the project's own argument for choosing the web at all (§4 of the spec's Philosophy): the structure of the page is readable in view-source without running anything. - 3. The nav is a fixed list of eight links. Building a fixed list at runtime buys nothing and + 3. The nav is a fixed list of links. Building a fixed list at runtime buys nothing and costs the keyboard path before the module has loaded. ── NO INLINE SCRIPT AND NO INLINE STYLE, AND THAT IS ENFORCED ────────────────────────────── @@ -133,6 +133,12 @@
  • Rules
  • Venues
  • Gates
  • + +
  • Plans
  • Plans