diff --git a/.playwright-mcp/page-2026-08-26T23-58-30-111Z.yml b/.playwright-mcp/page-2026-08-26T23-58-30-111Z.yml new file mode 100644 index 0000000..6d40b71 --- /dev/null +++ b/.playwright-mcp/page-2026-08-26T23-58-30-111Z.yml @@ -0,0 +1,342 @@ +- generic [active] [ref=e1]: + - link "Skip to content" [ref=e2] [cursor=pointer]: + - /url: "#main" + - generic [ref=e4]: + - paragraph [ref=e9]: ﴿وَقُلْ رَبِّ زِدْنِي عِلْمًا ١١٤﴾ + - paragraph [ref=e13]: "[Tā-Hā 114]" + - banner [ref=e14]: + - generic [ref=e15]: + - checkbox "Menu" [ref=e16] + - link "keel" [ref=e17] [cursor=pointer]: + - /url: /en/ + - navigation "Main navigation" [ref=e21]: + - link "Features" [ref=e22] [cursor=pointer]: + - /url: /en/features/ + - link "Install" [ref=e23] [cursor=pointer]: + - /url: /en/install/ + - link "Docs" [ref=e24] [cursor=pointer]: + - /url: /en/docs/ + - link "News" [ref=e25] [cursor=pointer]: + - /url: /en/news/ + - link "Changelog" [ref=e26] [cursor=pointer]: + - /url: /en/changelog/ + - link "Community" [ref=e27] [cursor=pointer]: + - /url: /en/community/ + - link "Compliance" [ref=e28] [cursor=pointer]: + - /url: /en/compliance/ + - link "Compare" [ref=e29] [cursor=pointer]: + - /url: /en/compare/ + - link "About" [ref=e30] [cursor=pointer]: + - /url: /en/about/ + - generic [ref=e31]: + - generic [ref=e32]: + - paragraph [ref=e33]: + - text: EN + - generic [ref=e37]: ▾ + - list "Language": + - listitem: + - link "العربية": + - /url: /ar/ + - listitem: + - link "Français": + - /url: /fr/ + - button "Toggle light and dark theme" [ref=e38] [cursor=pointer] + - link "View on GitHub" [ref=e41] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel + - main [ref=e44]: + - generic [ref=e46]: + - navigation "Docs navigation" [ref=e47]: + - link [ref=e48] [cursor=pointer]: + - /url: /en/install/ + - generic [ref=e52]: + - paragraph [ref=e53]: Get started guides + - list [ref=e57]: + - listitem [ref=e58]: + - link "1 · Install and first run" [ref=e59] [cursor=pointer]: + - /url: /en/guides/first-run/ + - listitem [ref=e60]: + - link "2 · Your first simulation" [ref=e61] [cursor=pointer]: + - /url: /en/guides/first-simulation/ + - listitem [ref=e62]: + - link "3 · The paper profile" [ref=e63] [cursor=pointer]: + - /url: /en/guides/paper-profile/ + - listitem [ref=e64]: + - link "4 · Attestations before anything is live" [ref=e65] [cursor=pointer]: + - /url: /en/guides/compliance-attest/ + - generic [ref=e66]: + - paragraph [ref=e67]: Deep explainers + - list [ref=e72]: + - listitem [ref=e73]: + - link "How Shariah crypto screening actually works" [ref=e74] [cursor=pointer]: + - /url: /en/guides/how-shariah-crypto-screening-works/ + - listitem [ref=e75]: + - link "Qabd (constructive possession) in spot crypto, explained" [ref=e76] [cursor=pointer]: + - /url: /en/guides/qabd-constructive-possession-spot-crypto/ + - listitem [ref=e77]: + - link "Attestation is not a fatwa" [ref=e78] [cursor=pointer]: + - /url: /en/guides/attestation-is-not-a-fatwa/ + - generic [ref=e79]: + - paragraph [ref=e80]: Guides + - list [ref=e85]: + - listitem [ref=e86]: + - link "Operator runbook" [ref=e87] [cursor=pointer]: + - /url: /en/docs/operator-runbook/ + - listitem [ref=e88]: + - link "Go-live runbook" [ref=e89] [cursor=pointer]: + - /url: /en/docs/go-live-runbook/ + - generic [ref=e90]: + - paragraph [ref=e91]: Reference + - list [ref=e95]: + - listitem [ref=e96]: + - link "Glossary" [ref=e97] [cursor=pointer]: + - /url: /en/docs/glossary/ + - listitem [ref=e98]: + - link "The fiqh basis" [ref=e99] [cursor=pointer]: + - /url: /en/docs/fiqh-basis/ + - generic [ref=e100]: + - paragraph [ref=e101]: Research & experiments + - list [ref=e105]: + - listitem [ref=e106]: + - 'link "Experiment: the honest result, restated" [ref=e107] [cursor=pointer]': + - /url: /en/docs/experiment-honest-result-restated/ + - listitem [ref=e108]: + - 'link "Experiment: the hourly backtest" [ref=e109] [cursor=pointer]': + - /url: /en/docs/experiment-hourly-backtest/ + - listitem [ref=e110]: + - 'link "Research: speculation risk sources (Lahlou)" [ref=e111] [cursor=pointer]': + - /url: /en/docs/research-lahlou-speculation-risk/ + - listitem [ref=e112]: + - 'link "Research: money management & fiqh sources" [ref=e113] [cursor=pointer]': + - /url: /en/docs/research-money-management/ + - article [ref=e114]: + - navigation "Breadcrumb" [ref=e115]: + - link "Docs" [ref=e116] [cursor=pointer]: + - /url: /en/docs/ + - generic [ref=e117]: › + - generic [ref=e118]: Reference + - generic [ref=e119]: › + - generic [ref=e120]: Glossary + - heading "Glossary" [level=1] [ref=e121] + - paragraph [ref=e122]: + - text: Fetched at build time from CodeGateSoftware/keel@v0.11.2. If a document moves, the build fails — this site never renders stale docs. Last fetched 2026-08-26. + - link "View source on GitHub" [ref=e123] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/v0.11.2/docs/glossary.md + - text: . + - generic [ref=e124]: + - paragraph [ref=e125]: "The single source for the console’s vocabulary. Every term a keel screen can show is defined HERE and nowhere else: the TUI’s Help menu renders this file directly (bounded read, cached by mtime — the same reader discipline the Research corpus keeps), and the docs link to it rather than restating a definition that could drift." + - paragraph [ref=e126]: + - text: Two honesty rules this file inherits from + - code [ref=e127]: docs/fiqh-basis.md + - text: ":" + - list [ref=e128]: + - listitem [ref=e129]: + - strong [ref=e130]: Fiqh terms are anchored, never authored. + - text: A fiqh term’s definition is a verbatim passage of + - code [ref=e131]: docs/fiqh-basis.md + - text: ", and its" + - code [ref=e132]: "Source:" + - text: line names the document and the exact section it was quoted from. Where fiqh-basis does not state a term (gharar), the definition says so, rather than papering the gap with a paraphrase that sounds like a ruling — the knowledge-base sources fiqh-basis indexes are the place to read it. + - listitem [ref=e133]: + - strong [ref=e134]: Rule parameters are not defined here. + - text: What + - code [ref=e135]: turtle_breakout + - text: ’s + - code [ref=e136]: entry_lookback + - text: means lives in the rule class that defines it, rendered by introspection through + - code [ref=e137]: keel.commands.rules.describe_params + - text: — the help system LINKS to that source; a second table in this file would drift the day a class changed. The entries below define only the vocabulary shared across screens. + - paragraph [ref=e138]: + - text: Each entry is a + - code [ref=e139]: "## term" + - text: heading, a definition, and a + - code [ref=e140]: "Source:" + - text: line. + - heading "rail" [level=2] [ref=e141] + - paragraph [ref=e142]: One of keel’s numbered hard guards that every order passes through — spend caps, drawdown breakers, the allowlist, settlement-currency and spot-shape checks. Eighteen exist (1-14, 16, 17, 18, 19 — there is no rail 15); each is un-overridable and audit-logged. + - paragraph [ref=e143]: "Source: keel’s own vocabulary — docs/fiqh-basis.md’s rails table (the prudential rails 2-14, 16) plus its prose sections for rails 1, 17, 18 and 19" + - heading "attestation" [level=2] [ref=e144] + - paragraph [ref=e145]: + - text: market facts are computed, Shariah classifications are + - strong [ref=e146]: ATTESTED, never inferred + - text: . Whether a token’s core purpose is a haram sector (§28.4), whether it is asset-backed + - code [ref=e147]: "'ayn" + - text: or a claim + - code [ref=e148]: dayn + - text: (§65.5/§67.2), and whether it pays a riba-like yield are questions of fact-plus-scholarship about the world. No module in this repository derives them from candles, and none pretends to. A human records them, with a source and a name, via + - code [ref=e149]: keel assets attest + - text: . + - paragraph [ref=e150]: "Source: docs/fiqh-basis.md — ”## What is attested versus what is computed”" + - heading "instrument attestation" [level=2] [ref=e151] + - paragraph [ref=e152]: + - text: Only + - code [ref=e153]: spot + - text: is admitted; CFD, future, perpetual, option, and leveraged-token listings are refused, recorded via + - code [ref=e154]: keel assets attest-instrument + - text: . Unattested fails closed. + - paragraph [ref=e155]: + - text: "Source: docs/fiqh-basis.md — ”### The curation screen (" + - code [ref=e156]: keel/compliance/screen.py + - text: )” + - heading "exemption" [level=2] [ref=e157] + - paragraph [ref=e158]: + - text: a documented exception ( + - code [ref=e159]: keel assets exempt + - text: ") may waive only ONE criterion today:" + - code [ref=e160]: history + - paragraph [ref=e161]: + - text: "Source: docs/fiqh-basis.md — ”### The curation screen (" + - code [ref=e162]: keel/compliance/screen.py + - text: )” + - heading "screening" [level=2] [ref=e163] + - paragraph [ref=e164]: An unattested asset is not “probably fine” — it is unknown, and the screen fails closed on unknown. + - paragraph [ref=e165]: "Source: docs/fiqh-basis.md — ”## What is attested versus what is computed”" + - heading "promotion gate" [level=2] [ref=e166] + - paragraph [ref=e167]: + - text: The thresholds a candidate rule must clear on out-of-sample evidence before + - code [ref=e168]: rules promote + - text: "moves it toward live: the lookahead gate first (truncation-diff analysis must find the rule clean of future-reading before anything else is even measured), then the four performance floors — min_trades (a minimum number of trades), min_expectancy, min_rr (a minimum realised reward:risk ratio) and min_win_rate — AND the overfitting gate (G4): a probability of backtest overfitting (PBO) above its bound TOGETHER WITH a steeply negative degradation slope, a conjunction, not a bare PBO bound. Pooling is per parameter SET and covers only the sample-size axis — the same parameter set’s paper evidence may count toward min_trades across products — and the G4/overfitting gate is NOT pooled. A pooled count is not a count of independent observations: the signals herd (about 8 products fire the same UTC day) and the outcomes correlate (ICC 0.212), so n pooled trades carry n / DEFF ~ n / 2.58 EFFECTIVE observations — n = 100 pooled is n_eff ~ 39, an edge of 20 points or more (#427; keel/research/throughput.py, pinned by tests/research/test_throughput.py). The DCA benchmark is not a floor of this gate; a simulate report is where a rule is measured against it." + - code [ref=e169]: keel insights + - text: renders how far a rule sits from the gate. + - paragraph [ref=e170]: "Source: keel’s own vocabulary — keel/strategy/promotion.py and keel/research/cscv.py" + - heading "paper mode" [level=2] [ref=e171] + - paragraph [ref=e172]: "Simulated execution against real prices: paper buys spend a paper cash balance, no order ever reaches a venue, and every fill is synthetic. keel’s paper deployments are separate config+db pairs from live — no figure on one describes the other." + - paragraph [ref=e173]: "Source: keel’s own vocabulary — the wrappers and docs/operator-runbook.md" + - heading "live mode" [level=2] [ref=e174] + - paragraph [ref=e175]: "Real orders at a real venue. Live selection is guarded: choosing the live deployment in the console asks an explicit y/N, and the engine’s confirm mode gates every order behind an interactive confirmation." + - paragraph [ref=e176]: "Source: keel’s own vocabulary — the wrappers and docs/operator-runbook.md" + - heading "kill switch" [level=2] [ref=e177] + - paragraph [ref=e178]: + - text: A stored halt that stops all trading immediately ( + - code [ref=e179]: keel kill + - text: — one command, always allowed, logged). Releasing it ( + - code [ref=e180]: keel resume + - text: ) is deliberately harder than engaging it. + - paragraph [ref=e181]: "Source: keel’s own vocabulary — keel/commands/trading.py" + - heading "autonomy" [level=2] [ref=e182] + - paragraph [ref=e183]: The armed state in which the agent places orders unattended. Arming demands a typed yes at the terminal; disarming only ever reduces capability and stays ungated. + - paragraph [ref=e184]: "Source: keel’s own vocabulary — keel/commands/autonomy.py" + - heading "qabd" [level=2] [ref=e185] + - paragraph [ref=e186]: possession is the ability to dispose, not physical custody + - paragraph [ref=e187]: + - text: "Source: docs/fiqh-basis.md — ”### Rail 17 — withdrawal capability," + - code [ref=e188]: qabd + - text: §65.4” + - heading "riba" [level=2] [ref=e189] + - paragraph [ref=e190]: Coinbase pays USDC rewards on idle balances, that interest is riba, and it accrues with no order placed + - paragraph [ref=e191]: "Source: docs/fiqh-basis.md — ”### Purification (§65.9) and idle-balance rewards (§56.3)”" + - heading "gharar" [level=2] [ref=e192] + - paragraph [ref=e193]: not stated in docs/fiqh-basis.md — the knowledge-base sources it indexes (docs/superpowers/references/trading-knowledge-base/) are the place to read it + - paragraph [ref=e194]: "Source: docs/fiqh-basis.md — not stated there; see ”## How to read the citations”" + - heading "maysir" [level=2] [ref=e195] + - paragraph [ref=e196]: + - text: The commoner English transliteration of + - emphasis [ref=e197]: maisir + - text: — see the maisir entry. + - paragraph [ref=e198]: "Source: keel’s own vocabulary — a spelling pointer, not a definition" + - heading "maisir" [level=2] [ref=e199] + - paragraph [ref=e200]: + - text: what makes speculation + - emphasis [ref=e201]: maisir + - text: is non-ownership, non-delivery, difference-settlement + - paragraph [ref=e202]: "Source: docs/fiqh-basis.md — ”### Rails 18/19 — settlement currency and spot-instrument shape”" + - heading "purification" [level=2] [ref=e203] + - paragraph [ref=e204]: interest/reward credits are segregated from realised P&L and the equity base, reported as owed to charity, never recognised as profit + - paragraph [ref=e205]: "Source: docs/fiqh-basis.md — ”### Purification (§65.9) and idle-balance rewards (§56.3)”" + - heading "session-bound venue" [level=2] [ref=e206] + - paragraph [ref=e207]: + - text: "A market with opening hours (equities): it closes for nights, weekends and holidays, and its adapter declares" + - code [ref=e208]: session_bound + - text: so the engine consults the venue’s own clock before trading. Crypto venues are the 24/7 contrast — always open, no clock to consult. + - paragraph [ref=e209]: + - text: "Source: keel’s own vocabulary — the adapters’" + - code [ref=e210]: session_bound + - text: capability declaration + - heading "market clock" [level=2] [ref=e211] + - paragraph [ref=e212]: + - text: The venue’s OWN clock read ( + - code [ref=e213]: /v2/clock + - text: on Alpaca; a constant OPEN on 24/7 venues) — never a locally maintained calendar, which would drift from the venue on holidays and half-days. + - paragraph [ref=e214]: + - text: "Source: keel’s own vocabulary — the broker port’s" + - code [ref=e215]: market_clock() + - heading "trust window" [level=2] [ref=e216] + - paragraph [ref=e217]: + - text: The interval a recorded market-clock state vouches for. Outside it the record is stale and every surface renders CLOCK UNAVAILABLE, fail-loud — exactly how + - code [ref=e218]: fetch --check + - text: treats a record that no longer vouches for anything. + - paragraph [ref=e219]: "Source: keel’s own vocabulary — keel.agent’s recorded session state" + - heading "DCA benchmark" [level=2] [ref=e220] + - paragraph [ref=e221]: "The honest alternative every simulated strategy is measured against: dollar-cost-average buying of the same products over the same window. A strategy that cannot beat it has no reason to exist." + - paragraph [ref=e222]: "Source: keel’s own vocabulary — keel/commands/simulate.py" + - heading "granularity" [level=2] [ref=e223] + - paragraph [ref=e224]: + - text: "The candle bar size a rule runs on (one hour, one day, …). It is a rule PARAMETER: each kind’s choices, default and meaning render from the rule class itself through" + - code [ref=e225]: keel commands.rules.describe_params + - text: — this glossary does not restate them. + - paragraph [ref=e226]: "Source: keel’s own vocabulary — the rule classes via describe_params" + - heading "trials ledger" [level=2] [ref=e227] + - paragraph [ref=e228]: + - text: The hash-chained record of every backtest trial, with its M/N decision accountings; + - code [ref=e229]: keel trials verify + - text: walks the chain and reports any break. + - paragraph [ref=e230]: "Source: keel’s own vocabulary — keel/research/ledger.py" + - paragraph [ref=e231]: + - link "View source on GitHub — docs/glossary.md" [ref=e232] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/v0.11.2/docs/glossary.md + - navigation "Document navigation" [ref=e233]: + - link "← Previous Go-live runbook" [ref=e234] [cursor=pointer]: + - /url: /en/docs/go-live-runbook/ + - generic [ref=e235]: ← Previous + - generic [ref=e236]: Go-live runbook + - link "Next → The fiqh basis" [ref=e237] [cursor=pointer]: + - /url: /en/docs/fiqh-basis/ + - generic [ref=e238]: Next → + - generic [ref=e239]: The fiqh basis + - contentinfo [ref=e240]: + - generic [ref=e241]: + - generic [ref=e242]: + - generic [ref=e243]: + - heading "Standing disclaimers" [level=2] [ref=e244] + - paragraph [ref=e245]: keel is a personal tool — not financial advice, and not a fatwa or religious advice. Consult a qualified financial advisor and a knowledgeable scholar before trading. You are solely responsible for your trading decisions and for your own attestations. + - generic [ref=e246]: + - heading "Legal" [level=2] [ref=e247] + - paragraph [ref=e248]: Alpaca, Coinbase and Robinhood are trademarks of their respective owners. keel is not affiliated with, endorsed by, or sponsored by any of them. Venue names appear solely to identify what the code talks to. + - paragraph [ref=e249]: Not affiliated with the Kubernetes project also named Keel (keel.sh). + - paragraph [ref=e250]: "Engine: Apache-2.0 (CodeGateSoftware/keel). This site's code: MIT. Site content: © CodeGate Software — quoting with attribution is welcome." + - generic [ref=e251]: + - heading "The project" [level=2] [ref=e252] + - list [ref=e253]: + - listitem [ref=e254]: + - link "View on GitHub" [ref=e255] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel + - listitem [ref=e256]: + - link "See releases on GitHub" [ref=e257] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/releases + - listitem [ref=e258]: + - link "Discuss on GitHub" [ref=e259] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/discussions + - listitem [ref=e260]: + - link "The honest result, stated first" [ref=e261] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md + - listitem [ref=e262]: + - link "This site's source" [ref=e263] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keeltrading.com + - generic [ref=e264]: + - heading "Editions" [level=2] [ref=e265] + - list [ref=e266]: + - listitem [ref=e267]: + - link "English" [ref=e268] [cursor=pointer]: + - /url: /en/ + - listitem [ref=e269]: + - link "العربية" [ref=e270] [cursor=pointer]: + - /url: /ar/ + - listitem [ref=e271]: + - link "Français" [ref=e272] [cursor=pointer]: + - /url: /fr/ + - listitem [ref=e273]: Español (later) + - generic [ref=e274]: + - generic [ref=e275]: © 2026 CodeGate Software + - generic [ref=e276]: + - text: The honest result, stated first — + - link "the experiment record" [ref=e277] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-26T23-58-40-711Z.yml b/.playwright-mcp/page-2026-08-26T23-58-40-711Z.yml new file mode 100644 index 0000000..d18a363 --- /dev/null +++ b/.playwright-mcp/page-2026-08-26T23-58-40-711Z.yml @@ -0,0 +1,347 @@ +- generic [active] [ref=f1e1]: + - link "Skip to content" [ref=f1e2] [cursor=pointer]: + - /url: "#main" + - generic [ref=f1e4]: + - paragraph [ref=f1e9]: ﴿وَقُلْ رَبِّ زِدْنِي عِلْمًا ١١٤﴾ + - paragraph [ref=f1e13]: "[Tā-Hā 114]" + - banner [ref=f1e14]: + - generic [ref=f1e15]: + - checkbox "Menu" [ref=f1e16] + - link "keel" [ref=f1e17] [cursor=pointer]: + - /url: /en/ + - navigation "Main navigation" [ref=f1e21]: + - link "Features" [ref=f1e22] [cursor=pointer]: + - /url: /en/features/ + - link "Install" [ref=f1e23] [cursor=pointer]: + - /url: /en/install/ + - link "Docs" [ref=f1e24] [cursor=pointer]: + - /url: /en/docs/ + - link "News" [ref=f1e25] [cursor=pointer]: + - /url: /en/news/ + - link "Changelog" [ref=f1e26] [cursor=pointer]: + - /url: /en/changelog/ + - link "Community" [ref=f1e27] [cursor=pointer]: + - /url: /en/community/ + - link "Compliance" [ref=f1e28] [cursor=pointer]: + - /url: /en/compliance/ + - link "Compare" [ref=f1e29] [cursor=pointer]: + - /url: /en/compare/ + - link "About" [ref=f1e30] [cursor=pointer]: + - /url: /en/about/ + - generic [ref=f1e31]: + - generic [ref=f1e32]: + - paragraph [ref=f1e33]: + - text: EN + - generic [ref=f1e37]: ▾ + - list "Language": + - listitem: + - link "العربية": + - /url: /ar/ + - listitem: + - link "Français": + - /url: /fr/ + - button "Toggle light and dark theme" [ref=f1e38] [cursor=pointer] + - link "View on GitHub" [ref=f1e41] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel + - main [ref=f1e44]: + - generic [ref=f1e46]: + - navigation "Docs navigation" [ref=f1e47]: + - link [ref=f1e48] [cursor=pointer]: + - /url: /en/install/ + - generic [ref=f1e52]: + - paragraph [ref=f1e53]: Get started guides + - list [ref=f1e57]: + - listitem [ref=f1e58]: + - link "1 · Install and first run" [ref=f1e59] [cursor=pointer]: + - /url: /en/guides/first-run/ + - listitem [ref=f1e60]: + - link "2 · Your first simulation" [ref=f1e61] [cursor=pointer]: + - /url: /en/guides/first-simulation/ + - listitem [ref=f1e62]: + - link "3 · The paper profile" [ref=f1e63] [cursor=pointer]: + - /url: /en/guides/paper-profile/ + - listitem [ref=f1e64]: + - link "4 · Attestations before anything is live" [ref=f1e65] [cursor=pointer]: + - /url: /en/guides/compliance-attest/ + - generic [ref=f1e66]: + - paragraph [ref=f1e67]: Deep explainers + - list [ref=f1e72]: + - listitem [ref=f1e73]: + - link "How Shariah crypto screening actually works" [ref=f1e74] [cursor=pointer]: + - /url: /en/guides/how-shariah-crypto-screening-works/ + - listitem [ref=f1e75]: + - link "Qabd (constructive possession) in spot crypto, explained" [ref=f1e76] [cursor=pointer]: + - /url: /en/guides/qabd-constructive-possession-spot-crypto/ + - listitem [ref=f1e77]: + - link "Attestation is not a fatwa" [ref=f1e78] [cursor=pointer]: + - /url: /en/guides/attestation-is-not-a-fatwa/ + - generic [ref=f1e79]: + - paragraph [ref=f1e80]: Guides + - list [ref=f1e85]: + - listitem [ref=f1e86]: + - link "Operator runbook" [ref=f1e87] [cursor=pointer]: + - /url: /en/docs/operator-runbook/ + - listitem [ref=f1e88]: + - link "Go-live runbook" [ref=f1e89] [cursor=pointer]: + - /url: /en/docs/go-live-runbook/ + - generic [ref=f1e90]: + - paragraph [ref=f1e91]: Reference + - list [ref=f1e95]: + - listitem [ref=f1e96]: + - link "Glossary" [ref=f1e97] [cursor=pointer]: + - /url: /en/docs/glossary/ + - listitem [ref=f1e98]: + - link "The fiqh basis" [ref=f1e99] [cursor=pointer]: + - /url: /en/docs/fiqh-basis/ + - generic [ref=f1e100]: + - paragraph [ref=f1e101]: Research & experiments + - list [ref=f1e105]: + - listitem [ref=f1e106]: + - 'link "Experiment: the honest result, restated" [ref=f1e107] [cursor=pointer]': + - /url: /en/docs/experiment-honest-result-restated/ + - listitem [ref=f1e108]: + - 'link "Experiment: the hourly backtest" [ref=f1e109] [cursor=pointer]': + - /url: /en/docs/experiment-hourly-backtest/ + - listitem [ref=f1e110]: + - 'link "Research: speculation risk sources (Lahlou)" [ref=f1e111] [cursor=pointer]': + - /url: /en/docs/research-lahlou-speculation-risk/ + - listitem [ref=f1e112]: + - 'link "Research: money management & fiqh sources" [ref=f1e113] [cursor=pointer]': + - /url: /en/docs/research-money-management/ + - article [ref=f1e114]: + - navigation "Breadcrumb" [ref=f1e115]: + - link "Docs" [ref=f1e116] [cursor=pointer]: + - /url: /en/docs/ + - generic [ref=f1e117]: › + - generic [ref=f1e118]: Reference + - generic [ref=f1e119]: › + - generic [ref=f1e120]: Glossary + - heading "Glossary" [level=1] [ref=f1e121] + - note [ref=f1e122]: + - strong [ref=f1e123]: These pages describe a different keel version + - text: You're running keel 0.10.0. These pages describe 0.11.2. + - link "See what changed →" [ref=f1e124] [cursor=pointer]: + - /url: /en/changelog/ + - paragraph [ref=f1e125]: + - text: Fetched at build time from CodeGateSoftware/keel@v0.11.2. If a document moves, the build fails — this site never renders stale docs. Last fetched 2026-08-26. + - link "View source on GitHub" [ref=f1e126] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/v0.11.2/docs/glossary.md + - text: . + - generic [ref=f1e127]: + - paragraph [ref=f1e128]: "The single source for the console’s vocabulary. Every term a keel screen can show is defined HERE and nowhere else: the TUI’s Help menu renders this file directly (bounded read, cached by mtime — the same reader discipline the Research corpus keeps), and the docs link to it rather than restating a definition that could drift." + - paragraph [ref=f1e129]: + - text: Two honesty rules this file inherits from + - code [ref=f1e130]: docs/fiqh-basis.md + - text: ":" + - list [ref=f1e131]: + - listitem [ref=f1e132]: + - strong [ref=f1e133]: Fiqh terms are anchored, never authored. + - text: A fiqh term’s definition is a verbatim passage of + - code [ref=f1e134]: docs/fiqh-basis.md + - text: ", and its" + - code [ref=f1e135]: "Source:" + - text: line names the document and the exact section it was quoted from. Where fiqh-basis does not state a term (gharar), the definition says so, rather than papering the gap with a paraphrase that sounds like a ruling — the knowledge-base sources fiqh-basis indexes are the place to read it. + - listitem [ref=f1e136]: + - strong [ref=f1e137]: Rule parameters are not defined here. + - text: What + - code [ref=f1e138]: turtle_breakout + - text: ’s + - code [ref=f1e139]: entry_lookback + - text: means lives in the rule class that defines it, rendered by introspection through + - code [ref=f1e140]: keel.commands.rules.describe_params + - text: — the help system LINKS to that source; a second table in this file would drift the day a class changed. The entries below define only the vocabulary shared across screens. + - paragraph [ref=f1e141]: + - text: Each entry is a + - code [ref=f1e142]: "## term" + - text: heading, a definition, and a + - code [ref=f1e143]: "Source:" + - text: line. + - heading "rail" [level=2] [ref=f1e144] + - paragraph [ref=f1e145]: One of keel’s numbered hard guards that every order passes through — spend caps, drawdown breakers, the allowlist, settlement-currency and spot-shape checks. Eighteen exist (1-14, 16, 17, 18, 19 — there is no rail 15); each is un-overridable and audit-logged. + - paragraph [ref=f1e146]: "Source: keel’s own vocabulary — docs/fiqh-basis.md’s rails table (the prudential rails 2-14, 16) plus its prose sections for rails 1, 17, 18 and 19" + - heading "attestation" [level=2] [ref=f1e147] + - paragraph [ref=f1e148]: + - text: market facts are computed, Shariah classifications are + - strong [ref=f1e149]: ATTESTED, never inferred + - text: . Whether a token’s core purpose is a haram sector (§28.4), whether it is asset-backed + - code [ref=f1e150]: "'ayn" + - text: or a claim + - code [ref=f1e151]: dayn + - text: (§65.5/§67.2), and whether it pays a riba-like yield are questions of fact-plus-scholarship about the world. No module in this repository derives them from candles, and none pretends to. A human records them, with a source and a name, via + - code [ref=f1e152]: keel assets attest + - text: . + - paragraph [ref=f1e153]: "Source: docs/fiqh-basis.md — ”## What is attested versus what is computed”" + - heading "instrument attestation" [level=2] [ref=f1e154] + - paragraph [ref=f1e155]: + - text: Only + - code [ref=f1e156]: spot + - text: is admitted; CFD, future, perpetual, option, and leveraged-token listings are refused, recorded via + - code [ref=f1e157]: keel assets attest-instrument + - text: . Unattested fails closed. + - paragraph [ref=f1e158]: + - text: "Source: docs/fiqh-basis.md — ”### The curation screen (" + - code [ref=f1e159]: keel/compliance/screen.py + - text: )” + - heading "exemption" [level=2] [ref=f1e160] + - paragraph [ref=f1e161]: + - text: a documented exception ( + - code [ref=f1e162]: keel assets exempt + - text: ") may waive only ONE criterion today:" + - code [ref=f1e163]: history + - paragraph [ref=f1e164]: + - text: "Source: docs/fiqh-basis.md — ”### The curation screen (" + - code [ref=f1e165]: keel/compliance/screen.py + - text: )” + - heading "screening" [level=2] [ref=f1e166] + - paragraph [ref=f1e167]: An unattested asset is not “probably fine” — it is unknown, and the screen fails closed on unknown. + - paragraph [ref=f1e168]: "Source: docs/fiqh-basis.md — ”## What is attested versus what is computed”" + - heading "promotion gate" [level=2] [ref=f1e169] + - paragraph [ref=f1e170]: + - text: The thresholds a candidate rule must clear on out-of-sample evidence before + - code [ref=f1e171]: rules promote + - text: "moves it toward live: the lookahead gate first (truncation-diff analysis must find the rule clean of future-reading before anything else is even measured), then the four performance floors — min_trades (a minimum number of trades), min_expectancy, min_rr (a minimum realised reward:risk ratio) and min_win_rate — AND the overfitting gate (G4): a probability of backtest overfitting (PBO) above its bound TOGETHER WITH a steeply negative degradation slope, a conjunction, not a bare PBO bound. Pooling is per parameter SET and covers only the sample-size axis — the same parameter set’s paper evidence may count toward min_trades across products — and the G4/overfitting gate is NOT pooled. A pooled count is not a count of independent observations: the signals herd (about 8 products fire the same UTC day) and the outcomes correlate (ICC 0.212), so n pooled trades carry n / DEFF ~ n / 2.58 EFFECTIVE observations — n = 100 pooled is n_eff ~ 39, an edge of 20 points or more (#427; keel/research/throughput.py, pinned by tests/research/test_throughput.py). The DCA benchmark is not a floor of this gate; a simulate report is where a rule is measured against it." + - code [ref=f1e172]: keel insights + - text: renders how far a rule sits from the gate. + - paragraph [ref=f1e173]: "Source: keel’s own vocabulary — keel/strategy/promotion.py and keel/research/cscv.py" + - heading "paper mode" [level=2] [ref=f1e174] + - paragraph [ref=f1e175]: "Simulated execution against real prices: paper buys spend a paper cash balance, no order ever reaches a venue, and every fill is synthetic. keel’s paper deployments are separate config+db pairs from live — no figure on one describes the other." + - paragraph [ref=f1e176]: "Source: keel’s own vocabulary — the wrappers and docs/operator-runbook.md" + - heading "live mode" [level=2] [ref=f1e177] + - paragraph [ref=f1e178]: "Real orders at a real venue. Live selection is guarded: choosing the live deployment in the console asks an explicit y/N, and the engine’s confirm mode gates every order behind an interactive confirmation." + - paragraph [ref=f1e179]: "Source: keel’s own vocabulary — the wrappers and docs/operator-runbook.md" + - heading "kill switch" [level=2] [ref=f1e180] + - paragraph [ref=f1e181]: + - text: A stored halt that stops all trading immediately ( + - code [ref=f1e182]: keel kill + - text: — one command, always allowed, logged). Releasing it ( + - code [ref=f1e183]: keel resume + - text: ) is deliberately harder than engaging it. + - paragraph [ref=f1e184]: "Source: keel’s own vocabulary — keel/commands/trading.py" + - heading "autonomy" [level=2] [ref=f1e185] + - paragraph [ref=f1e186]: The armed state in which the agent places orders unattended. Arming demands a typed yes at the terminal; disarming only ever reduces capability and stays ungated. + - paragraph [ref=f1e187]: "Source: keel’s own vocabulary — keel/commands/autonomy.py" + - heading "qabd" [level=2] [ref=f1e188] + - paragraph [ref=f1e189]: possession is the ability to dispose, not physical custody + - paragraph [ref=f1e190]: + - text: "Source: docs/fiqh-basis.md — ”### Rail 17 — withdrawal capability," + - code [ref=f1e191]: qabd + - text: §65.4” + - heading "riba" [level=2] [ref=f1e192] + - paragraph [ref=f1e193]: Coinbase pays USDC rewards on idle balances, that interest is riba, and it accrues with no order placed + - paragraph [ref=f1e194]: "Source: docs/fiqh-basis.md — ”### Purification (§65.9) and idle-balance rewards (§56.3)”" + - heading "gharar" [level=2] [ref=f1e195] + - paragraph [ref=f1e196]: not stated in docs/fiqh-basis.md — the knowledge-base sources it indexes (docs/superpowers/references/trading-knowledge-base/) are the place to read it + - paragraph [ref=f1e197]: "Source: docs/fiqh-basis.md — not stated there; see ”## How to read the citations”" + - heading "maysir" [level=2] [ref=f1e198] + - paragraph [ref=f1e199]: + - text: The commoner English transliteration of + - emphasis [ref=f1e200]: maisir + - text: — see the maisir entry. + - paragraph [ref=f1e201]: "Source: keel’s own vocabulary — a spelling pointer, not a definition" + - heading "maisir" [level=2] [ref=f1e202] + - paragraph [ref=f1e203]: + - text: what makes speculation + - emphasis [ref=f1e204]: maisir + - text: is non-ownership, non-delivery, difference-settlement + - paragraph [ref=f1e205]: "Source: docs/fiqh-basis.md — ”### Rails 18/19 — settlement currency and spot-instrument shape”" + - heading "purification" [level=2] [ref=f1e206] + - paragraph [ref=f1e207]: interest/reward credits are segregated from realised P&L and the equity base, reported as owed to charity, never recognised as profit + - paragraph [ref=f1e208]: "Source: docs/fiqh-basis.md — ”### Purification (§65.9) and idle-balance rewards (§56.3)”" + - heading "session-bound venue" [level=2] [ref=f1e209] + - paragraph [ref=f1e210]: + - text: "A market with opening hours (equities): it closes for nights, weekends and holidays, and its adapter declares" + - code [ref=f1e211]: session_bound + - text: so the engine consults the venue’s own clock before trading. Crypto venues are the 24/7 contrast — always open, no clock to consult. + - paragraph [ref=f1e212]: + - text: "Source: keel’s own vocabulary — the adapters’" + - code [ref=f1e213]: session_bound + - text: capability declaration + - heading "market clock" [level=2] [ref=f1e214] + - paragraph [ref=f1e215]: + - text: The venue’s OWN clock read ( + - code [ref=f1e216]: /v2/clock + - text: on Alpaca; a constant OPEN on 24/7 venues) — never a locally maintained calendar, which would drift from the venue on holidays and half-days. + - paragraph [ref=f1e217]: + - text: "Source: keel’s own vocabulary — the broker port’s" + - code [ref=f1e218]: market_clock() + - heading "trust window" [level=2] [ref=f1e219] + - paragraph [ref=f1e220]: + - text: The interval a recorded market-clock state vouches for. Outside it the record is stale and every surface renders CLOCK UNAVAILABLE, fail-loud — exactly how + - code [ref=f1e221]: fetch --check + - text: treats a record that no longer vouches for anything. + - paragraph [ref=f1e222]: "Source: keel’s own vocabulary — keel.agent’s recorded session state" + - heading "DCA benchmark" [level=2] [ref=f1e223] + - paragraph [ref=f1e224]: "The honest alternative every simulated strategy is measured against: dollar-cost-average buying of the same products over the same window. A strategy that cannot beat it has no reason to exist." + - paragraph [ref=f1e225]: "Source: keel’s own vocabulary — keel/commands/simulate.py" + - heading "granularity" [level=2] [ref=f1e226] + - paragraph [ref=f1e227]: + - text: "The candle bar size a rule runs on (one hour, one day, …). It is a rule PARAMETER: each kind’s choices, default and meaning render from the rule class itself through" + - code [ref=f1e228]: keel commands.rules.describe_params + - text: — this glossary does not restate them. + - paragraph [ref=f1e229]: "Source: keel’s own vocabulary — the rule classes via describe_params" + - heading "trials ledger" [level=2] [ref=f1e230] + - paragraph [ref=f1e231]: + - text: The hash-chained record of every backtest trial, with its M/N decision accountings; + - code [ref=f1e232]: keel trials verify + - text: walks the chain and reports any break. + - paragraph [ref=f1e233]: "Source: keel’s own vocabulary — keel/research/ledger.py" + - paragraph [ref=f1e234]: + - link "View source on GitHub — docs/glossary.md" [ref=f1e235] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/v0.11.2/docs/glossary.md + - navigation "Document navigation" [ref=f1e236]: + - link "← Previous Go-live runbook" [ref=f1e237] [cursor=pointer]: + - /url: /en/docs/go-live-runbook/ + - generic [ref=f1e238]: ← Previous + - generic [ref=f1e239]: Go-live runbook + - link "Next → The fiqh basis" [ref=f1e240] [cursor=pointer]: + - /url: /en/docs/fiqh-basis/ + - generic [ref=f1e241]: Next → + - generic [ref=f1e242]: The fiqh basis + - contentinfo [ref=f1e243]: + - generic [ref=f1e244]: + - generic [ref=f1e245]: + - generic [ref=f1e246]: + - heading "Standing disclaimers" [level=2] [ref=f1e247] + - paragraph [ref=f1e248]: keel is a personal tool — not financial advice, and not a fatwa or religious advice. Consult a qualified financial advisor and a knowledgeable scholar before trading. You are solely responsible for your trading decisions and for your own attestations. + - generic [ref=f1e249]: + - heading "Legal" [level=2] [ref=f1e250] + - paragraph [ref=f1e251]: Alpaca, Coinbase and Robinhood are trademarks of their respective owners. keel is not affiliated with, endorsed by, or sponsored by any of them. Venue names appear solely to identify what the code talks to. + - paragraph [ref=f1e252]: Not affiliated with the Kubernetes project also named Keel (keel.sh). + - paragraph [ref=f1e253]: "Engine: Apache-2.0 (CodeGateSoftware/keel). This site's code: MIT. Site content: © CodeGate Software — quoting with attribution is welcome." + - generic [ref=f1e254]: + - heading "The project" [level=2] [ref=f1e255] + - list [ref=f1e256]: + - listitem [ref=f1e257]: + - link "View on GitHub" [ref=f1e258] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel + - listitem [ref=f1e259]: + - link "See releases on GitHub" [ref=f1e260] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/releases + - listitem [ref=f1e261]: + - link "Discuss on GitHub" [ref=f1e262] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/discussions + - listitem [ref=f1e263]: + - link "The honest result, stated first" [ref=f1e264] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md + - listitem [ref=f1e265]: + - link "This site's source" [ref=f1e266] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keeltrading.com + - generic [ref=f1e267]: + - heading "Editions" [level=2] [ref=f1e268] + - list [ref=f1e269]: + - listitem [ref=f1e270]: + - link "English" [ref=f1e271] [cursor=pointer]: + - /url: /en/ + - listitem [ref=f1e272]: + - link "العربية" [ref=f1e273] [cursor=pointer]: + - /url: /ar/ + - listitem [ref=f1e274]: + - link "Français" [ref=f1e275] [cursor=pointer]: + - /url: /fr/ + - listitem [ref=f1e276]: Español (later) + - generic [ref=f1e277]: + - generic [ref=f1e278]: © 2026 CodeGate Software + - generic [ref=f1e279]: + - text: The honest result, stated first — + - link "the experiment record" [ref=f1e280] [cursor=pointer]: + - /url: https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md \ No newline at end of file diff --git a/README.md b/README.md index dc67d37..4d7953a 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,10 @@ registrar completes (PRD §11). SEO strategy and roadmap: - **Static-first Astro** (zero client JavaScript), deployed to Cloudflare Pages from `main`; every PR gets a preview. - **Docs pipeline (FR-4)** — [engine-docs.manifest.json](engine-docs.manifest.json) - pins the engine documents; `npm run build` fetches them at build time and - **fails loudly** if one moves. Documents are never hand-copied. + pins the engine documents at keel's latest published release tag, resolved at + build time (#85), so the site describes what an operator runs; `npm run build` + fetches them and **fails loudly** if one moves. Documents are never + hand-copied. - **News (FR-5)** — the Announcements category of GitHub Discussions, read via the public REST endpoint (no token, no Worker needed — see the note in docs/DEPLOYMENT.md), refreshed by an hourly rebuild workflow. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index f46a2ef..1059620 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -101,9 +101,21 @@ not the pull request. - **A build that cannot fetch a pinned engine doc fails on purpose** (FR-4). Fix by updating `engine-docs.manifest.json` to the document's new path — never by hand-copying content into `src/content/engine-docs/` (gitignored). -- **Release / Discussions fetch failures degrade, not fail**: the build keeps - the last-known `data/*.json`; the Install and News pages show their GitHub - fallbacks. `data/` contents are build artifacts and never committed. +- **The docs ref is a release tag, not a branch** (#85). `npm run fetch` runs + `fetch-release.mjs` first, so `fetch-engine-docs.mjs` reads the tag from + `data/release.json` without a second call to the releases API. If no tag can + be resolved at all, the docs fetch exits non-zero: there is deliberately no + fallback to `main`, which is the skew the pin removes. The last-known-tag + fallback in `release-tag.mjs` is a local convenience only — `data/` is + gitignored and no workflow caches it, so in CI the releases API is the sole + source and a failure stops the build. +- **Release / Discussions fetch failures degrade, not fail** *for page content*: + the build keeps the last-known `data/*.json`; the Install and News pages show + their GitHub fallbacks. `data/` contents are build artifacts and never + committed. One exception since #85 — the **tag** the release fetch resolves is + load-bearing for the docs pin above, so a build that cannot resolve one stops + rather than degrading. The live site is unaffected: Cloudflare Pages keeps + serving the last successful deployment. - **Adding French (FR dry-run, Success criterion 8)**: copy the thin wrappers into `src/pages/fr/`, add `fr` dictionaries under `src/i18n/pages/`, and extend `locales` in `src/i18n/config.ts` plus the sitemap i18n map in diff --git a/docs/LAUNCH-CHECKLIST.md b/docs/LAUNCH-CHECKLIST.md index f7687ea..e96c55a 100644 --- a/docs/LAUNCH-CHECKLIST.md +++ b/docs/LAUNCH-CHECKLIST.md @@ -74,9 +74,10 @@ Verified 2026-08-22: ## 5. Docs pages fail the build when an engine doc moves — GREEN `scripts/fetch-engine-docs.mjs` fetches every document pinned in -`engine-docs.manifest.json`; on a 404 or network error it records the failure and -ends with `process.exit(1)` ("The build stops here by design (FR-4): never render -stale or missing docs."). CI surfaces this legibly: `.github/workflows/ci.yml` step +`engine-docs.manifest.json`, at keel's latest published release tag — resolved at +build time since #85, never at `main`. On a 404 or network error it records the +failure and ends with `process.exit(1)` ("The build stops here by design (FR-4): +never render stale or missing docs."). CI surfaces this legibly: `.github/workflows/ci.yml` step **"Fetch engine docs, release, discussions"** (`npm run fetch`) runs before type check and build; `deploy.yml` chains the same fetch inside `npm run build`, so a moved engine doc red-Xes Checks and blocks the deploy instead of publishing a @@ -93,8 +94,12 @@ stale page. EN install **100/100**, FR compare **100/100**. Both runs sit comfortably above the ≥95/≥95 budget. The site ships no client-side -JavaScript framework or bundle by design — two small inline scripts only: the -light/dark choice and the code-block copy button. +JavaScript framework or bundle by design — three behaviors only: the light/dark +choice, the code-block copy button, and the docs version notice (#85). Counted as +tags that is four blocks on an English engine document page (the light/dark +choice is a bootstrap plus a toggle) and three everywhere else. +The third one ships on English engine document pages only, and does nothing at +all unless keel sends a `?v=` that differs from the tag the docs were built from. ## 7. Honesty review green in EN and AR before launch — GREEN (reviewed EN + AR + FR) diff --git a/engine-docs.manifest.json b/engine-docs.manifest.json index 6cba518..57b4355 100644 --- a/engine-docs.manifest.json +++ b/engine-docs.manifest.json @@ -1,6 +1,7 @@ { "repo": "CodeGateSoftware/keel", - "ref": "main", + "ref": "latest-release", + "refNote": "Sentinel, read by scripts/fetch-engine-docs.mjs (#85): resolve to the latest published release tag at build time, so the site describes what an operator actually runs. A literal branch, tag or SHA here is used verbatim instead. There is no fallback to main: if no tag resolves, the build fails.", "docs": [ { "path": "docs/glossary.md", diff --git a/package.json b/package.json index d5a95c3..8e08449 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "0.1.0", "private": true, "scripts": { - "fetch": "node scripts/fetch-engine-docs.mjs && node scripts/fetch-release.mjs && node scripts/fetch-discussions.mjs", + "fetch": "node scripts/fetch-release.mjs && node scripts/fetch-engine-docs.mjs && node scripts/fetch-discussions.mjs", "dev": "npm run fetch && astro dev", "build": "npm run fetch && astro build", "preview": "astro preview", diff --git a/scripts/fetch-engine-docs.mjs b/scripts/fetch-engine-docs.mjs index 39b77c9..cc45bd9 100644 --- a/scripts/fetch-engine-docs.mjs +++ b/scripts/fetch-engine-docs.mjs @@ -11,16 +11,53 @@ * * Relative markdown links inside fetched documents are rewritten to absolute * GitHub blob URLs so they resolve from this origin; document text is untouched. + * + * #85 — the ref is the latest published release tag, not `main`. An operator + * runs a release, so the documents this site renders must be the documents that + * release shipped. The manifest keeps the policy in its `ref` field as the + * sentinel "latest-release"; a literal branch, tag or SHA is still honored. */ import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; +import { LATEST_RELEASE, resolveLatestReleaseTag } from "./lib/release-tag.mjs"; const root = dirname(dirname(fileURLToPath(import.meta.url))); const manifest = JSON.parse( readFileSync(join(root, "engine-docs.manifest.json"), "utf8"), ); +/** + * The ref every fetch, rewritten link and source URL below points at. When the + * release tag cannot be resolved the build stops: falling back to `main` would + * silently restore the skew this pin exists to remove. + */ +/** + * Everything below reaches the build log by way of the releases API — the tag + * itself, and the `source` note, which quotes a network error verbatim. The tag + * is already validated in release-tag.mjs; this strips control characters and + * caps the length so no remote string can forge a line in the build log, which + * is read to decide whether a deploy is trustworthy. + */ +const forLog = (value) => + String(value).replace(/[\u0000-\u001F\u007F]/g, " ").slice(0, 200); + +const docsRef = await (async () => { + if (manifest.ref !== LATEST_RELEASE) return manifest.ref; + const resolved = await resolveLatestReleaseTag(root, manifest.repo); + if (!resolved.tag) { + console.error( + `\nFAIL: could not resolve the latest release tag for ${manifest.repo} — ${forLog(resolved.source)}.`, + ); + console.error( + "The docs pipeline pins to a published release and never falls back to main (#85).", + ); + process.exit(1); + } + console.log(` docs ref: ${forLog(resolved.tag)} (from ${forLog(resolved.source)})`); + return resolved.tag; +})(); + const DOCS_DIR = join(root, "src/content/engine-docs"); const META_FILE = join(root, "data/docs-meta.json"); mkdirSync(DOCS_DIR, { recursive: true }); @@ -88,14 +125,14 @@ const failures = []; const fetchedAt = new Date().toISOString(); const meta = { repo: manifest.repo, - ref: manifest.ref, + ref: docsRef, fetchedAt, sections: manifest.sections ?? [], docs: [], }; for (const doc of manifest.docs) { - const url = `https://raw.githubusercontent.com/${manifest.repo}/${manifest.ref}/${doc.path}`; + const url = `https://raw.githubusercontent.com/${manifest.repo}/${docsRef}/${doc.path}`; let response; try { response = await fetch(url, { headers: { "user-agent": "keeltrading.com-docs-fetch" } }); @@ -106,7 +143,7 @@ for (const doc of manifest.docs) { if (!response.ok) { failures.push( `${doc.path}: HTTP ${response.status} at ${url}\n` + - ` The document may have moved in ${manifest.repo}@${manifest.ref}. ` + + ` The document may have moved in ${manifest.repo}@${docsRef}. ` + `Update engine-docs.manifest.json to the new path — do not hand-copy the doc.`, ); continue; @@ -116,7 +153,7 @@ for (const doc of manifest.docs) { // the page's single H1 (one-h1-per-page). Heading IDs are unaffected. markdown = markdown.replace(/^#\s+.+\n/, ""); markdown = demoteTopLevelHeadings(markdown); - markdown = rewriteRelativeLinks(markdown, doc.path, manifest.repo, manifest.ref); + markdown = rewriteRelativeLinks(markdown, doc.path, manifest.repo, docsRef); writeFileSync(join(DOCS_DIR, `${doc.slug}.md`), markdown); meta.docs.push({ slug: doc.slug, @@ -126,7 +163,7 @@ for (const doc of manifest.docs) { ar: doc.ar, fr: doc.fr ?? doc.en, section: doc.section ?? "reference", - sourceUrl: `https://github.com/${manifest.repo}/blob/${manifest.ref}/${doc.path}`, + sourceUrl: `https://github.com/${manifest.repo}/blob/${docsRef}/${doc.path}`, }); console.log(` fetched ${doc.path} -> engine-docs/${doc.slug}.md`); } diff --git a/scripts/lib/release-tag.mjs b/scripts/lib/release-tag.mjs new file mode 100644 index 0000000..c2fe5e1 --- /dev/null +++ b/scripts/lib/release-tag.mjs @@ -0,0 +1,104 @@ +/** + * Resolve the keel release tag this build describes (#85). + * + * Why a tag and not `main`: an operator runs a published release, so the site + * must describe a published release. The install page already learned this the + * hard way — it described a script served from `main`, and `main` moved the + * Python floor from 3.11 to 3.14 underneath the copy (PR #88). Pinning the docs + * pipeline to the released tag is the same correction, applied upstream of the + * prose: FR-9 says this site describes what shipped. + * + * Resolution order, cheapest first: + * + * 1. data/release.json, when fetch-release.mjs wrote it recently. `npm run + * fetch` runs that script first, so the normal build path costs zero extra + * calls to the releases API. + * 2. The releases API — one call, for a standalone run of the docs fetch. + * 3. The last-known tag in data/release.json, when the API is unreachable. + * That is still a real release tag, and the ref it resolves to is recorded + * in data/docs-meta.json and printed on every doc page, so a reader can + * see exactly which version the page describes. + * + * This tier is a local convenience, not CI resilience: `data/` is + * gitignored and no workflow caches it, so on the machine that actually + * ships the site the file never pre-exists. In CI the behaviour is binary + * — the releases API answers, or the build stops. + * + * There is deliberately no fallback to `main`. A silent fallback would + * reintroduce the skew this whole change exists to remove. When no tag can be + * resolved at all, the caller fails the build. + */ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** Manifest sentinel: "whatever the latest published release is at build time". */ +export const LATEST_RELEASE = "latest-release"; + +/** How recent data/release.json must be to be reused without an API call. */ +const FRESH_MS = 60 * 60 * 1000; + +/** + * A git tag this pipeline is willing to trust. The resolved tag is interpolated + * into raw.githubusercontent URLs, GitHub blob URLs and build logs, so it is + * validated at the boundary rather than anywhere downstream: whatever the + * releases API hands back, only a plain version tag gets past this line. + * + * Deliberately narrow. keel tags releases `v0.11.2`; anything that does not + * look like that is treated as unresolved, and the caller fails the build. + */ +const TAG_PATTERN = /^v?[0-9]+(\.[0-9]+){0,3}(-[0-9A-Za-z.-]{1,32})?$/; + +/** @param {unknown} tag @returns {string|null} the tag, or null if untrusted. */ +function trustedTag(tag) { + return typeof tag === "string" && TAG_PATTERN.test(tag) ? tag : null; +} + +function readReleaseFile(root) { + const file = join(root, "data/release.json"); + if (!existsSync(file)) return null; + try { + return JSON.parse(readFileSync(file, "utf8")); + } catch { + return null; + } +} + +/** + * @param {string} root project root + * @param {string} repo owner/name, e.g. CodeGateSoftware/keel + * @returns {Promise<{tag: string|null, source: string}>} tag is null when + * nothing could be resolved; source explains where the tag came from. + */ +export async function resolveLatestReleaseTag(root, repo) { + const cached = readReleaseFile(root); + const cachedAge = cached?.fetchedAt ? Date.now() - Date.parse(cached.fetchedAt) : Number.NaN; + const cachedTag = trustedTag(cached?.tag); + if (cachedTag && cachedAge >= 0 && cachedAge < FRESH_MS) { + return { tag: cachedTag, source: "data/release.json, written this build" }; + } + + const headers = { + accept: "application/vnd.github+json", + "user-agent": "keeltrading.com-docs-fetch", + }; + if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + + try { + const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { + headers, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const release = await response.json(); + const apiTag = trustedTag(release?.tag_name); + if (!apiTag) throw new Error("the releases API returned no usable tag_name"); + return { tag: apiTag, source: "the GitHub releases API" }; + } catch (error) { + if (cachedTag) { + return { + tag: cachedTag, + source: `data/release.json, last known — the releases API is unreachable (${error.message})`, + }; + } + return { tag: null, source: `the releases API is unreachable (${error.message})` }; + } +} diff --git a/src/components/DocsVersionNotice.astro b/src/components/DocsVersionNotice.astro new file mode 100644 index 0000000..2459f6f --- /dev/null +++ b/src/components/DocsVersionNotice.astro @@ -0,0 +1,97 @@ +--- +import { localePath } from "../i18n/config"; +import { t } from "../i18n/ui"; + +/** + * #85 — the version-skew notice on an engine document page. + * + * keel's browser UI deep-links here carrying the version it is running: + * /en/docs/glossary/?v=0.11.2#qabd. The docs pipeline pins to the latest + * published release tag, so most readers match and see nothing. When they do + * not match, this says so plainly instead of letting the reader assume the page + * describes their build. + * + * The site is `output: "static"`, so no query parameter exists at build time: + * the comparison happens in the browser. The markup ships hidden and complete, + * and the inline script below only fills one text node and unhides it — the + * same "labels as data attributes, behaviour in one small inline script" + * pattern as the code-block copy button. Because the element is in the document + * from the first parse and is unhidden during parsing, a `#anchor` in the URL + * still lands where it should; the script never reads or writes location.hash. + * + * Anything unexpected in `?v=` renders nothing at all: an unrecognized value is + * not evidence of skew. + * + * English only, by design and by routing: engine documents are published in + * English and `[slug].astro` exists only under /en/ (the ar and fr editions + * ship a docs index, not per-document pages). The Arabic and French strings + * exist in ui.ts so the chrome stays complete if that ever changes. + */ +interface Props { + /** The ref the page was built from — data/docs-meta.json's `ref`. */ + builtRef: string; +} + +const { builtRef } = Astro.props; +const chrome = t("en"); + +/** + * Only a version tag is comparable. A branch or SHA has nothing to compare. + * + * Anchored at both ends deliberately. Unanchored, a literal SHA in the + * manifest's `ref` — the documented escape hatch — would render a notice + * reading "these pages describe 1d1799e" when the SHA happens to start with a + * digit, and nothing when it starts with a letter. + */ +const comparable = /^v?\d+(\.\d+)*$/.test(builtRef); + +// Rendered once with placeholders; the script substitutes both versions, so the +// script itself stays free of any locale's wording. +const template = chrome.docs.versionSkew("{running}", "{built}"); +--- + +{ + comparable && ( +
+ ) +} + +{ + comparable && ( + + ) +} diff --git a/src/components/docs/nav.ts b/src/components/docs/nav.ts index b725777..7238e0c 100644 --- a/src/components/docs/nav.ts +++ b/src/components/docs/nav.ts @@ -48,9 +48,13 @@ interface MetaFile { docs: DocMeta[]; } +/** Only reachable if data/docs-meta.json is missing — a real build cannot get + * here, because the fetch script writes that file or exits non-zero. The ref + * is left empty rather than naming a branch: since #85 the site describes a + * release tag, and a hard-coded "main" here would be a claim, not a default. */ const FALLBACK_META: MetaFile = { repo: "CodeGateSoftware/keel", - ref: "main", + ref: "", fetchedAt: "", sections: [], docs: [], diff --git a/src/components/pages/DocsViewPage.astro b/src/components/pages/DocsViewPage.astro index 757f003..59c1726 100644 --- a/src/components/pages/DocsViewPage.astro +++ b/src/components/pages/DocsViewPage.astro @@ -1,6 +1,7 @@ --- import { render, type CollectionEntry } from "astro:content"; import Base from "../../layouts/Base.astro"; +import DocsVersionNotice from "../DocsVersionNotice.astro"; import DocsSidebar from "../docs/DocsSidebar.astro"; import DocsToc from "../docs/DocsToc.astro"; import { loadDocsMeta, neighbors } from "../docs/nav"; @@ -115,6 +116,8 @@ if (glossaryTerms.length > 0) {