diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a979d95..83f199c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # #91 — fails when a link into the engine repo names a branch instead of the + # ref the build resolved. Its own step so the failure is legible in Checks + # rather than buried in astro's output, same reasoning as the fetch step. + - name: Engine links are pinned, not on a branch + run: npm run check:engine-refs + - name: Type check run: npm run check diff --git a/package.json b/package.json index 8e08449..8f30e3c 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "dev": "npm run fetch && astro dev", "build": "npm run fetch && astro build", "preview": "astro preview", - "check": "astro check", + "check": "node scripts/check-engine-refs.mjs && astro check", "check:ai-crawlers": "node scripts/check-ai-crawlers.mjs", - "og": "node scripts/generate-og.mjs" + "og": "node scripts/generate-og.mjs", + "check:engine-refs": "node scripts/check-engine-refs.mjs" }, "dependencies": { "@astrojs/rss": "^4.0.19", diff --git a/scripts/check-engine-refs.mjs b/scripts/check-engine-refs.mjs new file mode 100644 index 0000000..bf1407d --- /dev/null +++ b/scripts/check-engine-refs.mjs @@ -0,0 +1,82 @@ +/** + * Fails when a link into the keel repository names a branch instead of the ref + * this build resolved (#91). + * + * #85 pinned the docs pipeline to the published release tag; #91 did the same + * for hand-written links, via `src/lib/engine-url.ts`. Neither is self-keeping: + * the next person to paste a GitHub URL into a component or a copy file + * reintroduces the skew, and nothing would notice. + * + * This is that guard, and it exists because a review pointed out the PR + * claiming it already did. `grep blob/main` was the sentence; it was never + * wired to anything, and it would not have caught two forms this repo already + * contained — `tree/main/packages` and `raw.githubusercontent.../keel/main/...`. + * So the pattern here matches any engine URL carrying a branch name, in any of + * the shapes GitHub serves. + * + * ## The two allowlisted exceptions, and why each is correct + * + * `scripts/install.sh` — deliberately NOT pinnable. The file does not exist at + * v0.11.2 (`git cat-file -e v0.11.2:scripts/install.sh` fails); it lives on the + * default branch only. Pinning it would turn the install page's primary command + * into a 404. The install page says so in its own copy. + * + * `src/content/` — fetched release-note prose, gitignored, written by + * scripts/fetch-release.mjs. Those links are quotes from releases as published; + * rewriting them would edit what a release said. + */ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join, relative } from "node:path"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const SRC = join(root, "src"); + +/** Any keel URL whose ref position holds a branch-shaped name rather than a tag. */ +const BRANCHY = /CodeGateSoftware\/keel\/(?:blob|tree|raw)\/(main|master|HEAD)\//g; +/** raw.githubusercontent.com puts the ref straight after the repo, with no verb. */ +const RAW = /raw\.githubusercontent\.com\/CodeGateSoftware\/keel\/(main|master|HEAD)\//g; + +const ALLOW = [ + { path: "src/components/pages/InstallPage.astro", why: "scripts/install.sh exists only on the default branch (#91)" }, + { path: "src/content/", why: "fetched release-note quotes — the site must not rewrite what a release said" }, +]; + +const allowed = (rel) => ALLOW.some((a) => rel === a.path || rel.startsWith(a.path)); + +function* walk(dir) { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) yield* walk(full); + else yield full; + } +} + +const findings = []; +for (const file of walk(SRC)) { + const rel = relative(root, file); + if (allowed(rel)) continue; + const text = readFileSync(file, "utf8"); + for (const re of [BRANCHY, RAW]) { + re.lastIndex = 0; + let m; + while ((m = re.exec(text)) !== null) { + const line = text.slice(0, m.index).split("\n").length; + findings.push(`${rel}:${line} — engine link pinned to '${m[1]}': ${m[0]}`); + } + } +} + +if (findings.length > 0) { + console.error(`\nFAIL: ${findings.length} engine link(s) name a branch instead of the resolved ref:\n`); + for (const f of findings) console.error(` - ${f}`); + console.error( + "\nBuild the URL with `engineSourceUrl` from src/lib/engine-url.ts, which pins to the ref\n" + + "in data/docs-meta.json. A branch link invites a reader to verify a release against code\n" + + "that is not in it (#91). If the target genuinely only exists on the default branch, add it\n" + + "to ALLOW in this file with the reason.\n", + ); + process.exit(1); +} + +console.log(`ok: no engine link names a branch (${ALLOW.length} documented exception(s))`); diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 89892af..3d1cea0 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -7,6 +7,7 @@ import { ENGINE_DISCUSSIONS_URL, } from "../i18n/config"; import { home } from "../i18n/pages/home"; +import { engineBlobUrl } from "../lib/engine-url"; interface Props { locale: Locale; @@ -16,8 +17,9 @@ const { locale } = Astro.props; const chrome = t(locale); const c = home[locale]; -const honestExperiment = - "https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md"; +const honestExperiment = engineBlobUrl( + "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", +); const editionLinks = [ { label: "English", locale: "en" as Locale }, diff --git a/src/components/pages/ComparePage.astro b/src/components/pages/ComparePage.astro index 6b969f2..849c519 100644 --- a/src/components/pages/ComparePage.astro +++ b/src/components/pages/ComparePage.astro @@ -4,6 +4,7 @@ import HonestBox from "../HonestBox.astro"; import { compare, compareColumns, compareRows } from "../../i18n/pages/compare"; import { home } from "../../i18n/pages/home"; import { alternatesFor, localePath, type Locale } from "../../i18n/config"; +import { engineBlobUrl } from "../../lib/engine-url"; interface Props { locale: Locale; @@ -23,7 +24,9 @@ const honestLinks = [ : locale === "fr" ? "Le compte rendu de l'expérience" : "The experiment record", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + href: engineBlobUrl( + "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + ), }, ]; --- diff --git a/src/components/pages/CompliancePage.astro b/src/components/pages/CompliancePage.astro index 490df4b..eefc613 100644 --- a/src/components/pages/CompliancePage.astro +++ b/src/components/pages/CompliancePage.astro @@ -3,6 +3,7 @@ import Base from "../../layouts/Base.astro"; import HonestBox from "../HonestBox.astro"; import { compliance } from "../../i18n/pages/compliance"; import { alternatesFor, localePath, type Locale } from "../../i18n/config"; +import { engineBlobUrl, engineSourceUrl } from "../../lib/engine-url"; import { home } from "../../i18n/pages/home"; interface Props { @@ -21,7 +22,9 @@ const honestLinks = [ : locale === "fr" ? "Le compte rendu de l'expérience" : "The experiment record", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + href: engineBlobUrl( + "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + ), }, { label: @@ -86,7 +89,7 @@ const honestLinks = [ c.links.items.map((link) => (
  • - + {link.label}

    diff --git a/src/components/pages/FeaturesPage.astro b/src/components/pages/FeaturesPage.astro index 1308353..100fc84 100644 --- a/src/components/pages/FeaturesPage.astro +++ b/src/components/pages/FeaturesPage.astro @@ -3,6 +3,7 @@ import Base from "../../layouts/Base.astro"; import ForOperators from "../ForOperators.astro"; import { features } from "../../i18n/pages/features"; import { alternatesFor, localePath, type Locale } from "../../i18n/config"; +import { engineSourceUrl } from "../../lib/engine-url"; interface Props { locale: Locale; @@ -40,7 +41,7 @@ const c = features[locale]; )}

    - {c.verifyNote}{locale === "fr" ? "\u00A0:" : ":"} {feature.verify.label} + {c.verifyNote}{locale === "fr" ? "\u00A0:" : ":"} {feature.verify.label}

    )) diff --git a/src/components/pages/HomePage.astro b/src/components/pages/HomePage.astro index b8215ff..26d724f 100644 --- a/src/components/pages/HomePage.astro +++ b/src/components/pages/HomePage.astro @@ -11,6 +11,7 @@ import { ENGINE_URL, } from "../../i18n/config"; import { t } from "../../i18n/ui"; +import { engineBlobUrl } from "../../lib/engine-url"; interface Props { locale: Locale; @@ -67,7 +68,9 @@ const nextItems = c.next.items.map((item, index) => ({ ...item, href: nextTarget const honestLinks = [ { label: c.honest.experimentLabel, - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + href: engineBlobUrl( + "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + ), }, { label: c.honest.announcementLabel, href: "https://github.com/CodeGateSoftware/keel/discussions/304" }, ]; diff --git a/src/components/pages/InstallPage.astro b/src/components/pages/InstallPage.astro index c517b06..772fc10 100644 --- a/src/components/pages/InstallPage.astro +++ b/src/components/pages/InstallPage.astro @@ -3,6 +3,7 @@ import Base from "../../layouts/Base.astro"; import ForOperators from "../ForOperators.astro"; import CodeBlock from "../CodeBlock.astro"; import { readDataFile } from "../docs/nav"; +import { engineBlobUrl } from "../../lib/engine-url"; import { install } from "../../i18n/pages/install"; import { alternatesFor, formatDate, localePath, type Locale } from "../../i18n/config"; import { t } from "../../i18n/ui"; @@ -252,7 +253,7 @@ const fromSourceCommands = [

    - + {c.unsigned.more} →

    diff --git a/src/i18n/pages/compliance.ts b/src/i18n/pages/compliance.ts index 71fb8eb..7a1a34d 100644 --- a/src/i18n/pages/compliance.ts +++ b/src/i18n/pages/compliance.ts @@ -1,4 +1,5 @@ import type { LocalizedPage } from "../config"; +import type { EngineSource } from "../../lib/engine-url"; /** * Compliance (FR-2): the Shariah methodology in plain terms — what keel @@ -15,7 +16,9 @@ export interface ComplianceContent { doesNot: { title: string; items: string[] }; framing: { title: string; body: string[] }; attest: { title: string; body: string[]; command: string; commandNote: string }; - links: { title: string; items: { label: string; note: string; href: string }[] }; + /** Primary sources in the engine repo. Copy carries the repo-relative path; + * CompliancePage builds the URL at the ref this build resolved (#91). */ + links: { title: string; items: (EngineSource & { label: string; note: string })[] }; translatedFromRev?: string; } @@ -96,22 +99,23 @@ export const compliance: LocalizedPage = { { label: "The fiqh basis (docs/fiqh-basis.md)", note: "The Shariah reasoning keel encodes, ruling by ruling, each with its in-repo source.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/fiqh-basis.md", + path: "docs/fiqh-basis.md", }, { label: "Scholarly review status", note: "What a scholarly review would cover, what it would and would not signify — and the standing status: not reviewed.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/fiqh-basis.md#scholarly-review-status", + path: "docs/fiqh-basis.md", + hash: "#scholarly-review-status", }, { label: "The honest result — experiment record", note: "No shipped rule family is net-positive at the taker fee actually paid; every number stated.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + path: "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", }, { label: "The glossary (docs/glossary.md)", note: "The single source for the vocabulary — fiqh terms are anchored, never authored.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/glossary.md", + path: "docs/glossary.md", }, ], }, @@ -194,22 +198,23 @@ export const compliance: LocalizedPage = { { label: "الأساس الفقهي (docs/fiqh-basis.md)", note: "الاستدلال الشرعي الذي يُشفّره كيل، حكمًا حكمًا، مع مصدر كلِّ حكمٍ داخل المستودع.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/fiqh-basis.md", + path: "docs/fiqh-basis.md", }, { label: "حالة المراجعة العلمية", note: "ما ستغطّيه المراجعة العلمية، وما تعنيه وما لا تعنيه — والحالة القائمة: غير مُراجَع.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/fiqh-basis.md#scholarly-review-status", + path: "docs/fiqh-basis.md", + hash: "#scholarly-review-status", }, { label: "النتيجة الصادقة — سجلّ التجربة", note: "لا تحقّق أيُّ عائلةٍ من القواعد المُصدَّرة ربحًا صافيًا عند رسوم الآخذ الفعلية؛ وكلُّ الأرقام مذكورة.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + path: "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", }, { label: "المسرد (docs/glossary.md)", note: "المصدر الوحيد للمصطلحات — والمصطلحات الفقهية مُسنَدةٌ إلى مصادرها، لا مؤلَّفة.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/glossary.md", + path: "docs/glossary.md", }, ], }, @@ -292,22 +297,23 @@ export const compliance: LocalizedPage = { { label: "La base fiqh (docs/fiqh-basis.md)", note: "Le raisonnement Shariah que keel encode, règle par règle, avec sa source dans le dépôt.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/fiqh-basis.md", + path: "docs/fiqh-basis.md", }, { label: "Le statut de l'examen par des savants", note: "Ce que couvrirait un examen par des savants, ce qu'il signifierait ou non — et le statut actuel : non examinée.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/fiqh-basis.md#scholarly-review-status", + path: "docs/fiqh-basis.md", + hash: "#scholarly-review-status", }, { label: "Le résultat honnête — le compte rendu de l'expérience", note: "Aucune famille de règles livrée ne dégage un résultat net positif aux frais de preneur réellement payés ; tous les chiffres sont donnés.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", + path: "docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md", }, { label: "Le glossaire (docs/glossary.md)", note: "L'unique source du vocabulaire — les termes du fiqh y sont ancrés, jamais inventés.", - href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/glossary.md", + path: "docs/glossary.md", }, ], }, diff --git a/src/i18n/pages/features.ts b/src/i18n/pages/features.ts index 0346978..d15a61d 100644 --- a/src/i18n/pages/features.ts +++ b/src/i18n/pages/features.ts @@ -1,4 +1,5 @@ import type { LocalizedPage } from "../config"; +import type { EngineSource } from "../../lib/engine-url"; /** * Features (FR-2): mapped 1:1 to real engine capabilities. Every section ends @@ -9,7 +10,10 @@ export interface Feature { title: string; body: string; points?: string[]; - verify: { label: string; href: string }; + /** The "verify in the repo" pointer. Copy carries the repo-relative path, + * never a URL: FeaturesPage builds the href at the ref this build resolved, + * so the reader lands on the code the release they run actually contains (#91). */ + verify: EngineSource & { label: string }; } export interface FeaturesContent { @@ -36,7 +40,7 @@ export const features: LocalizedPage = { { title: "Attested asset screening — fails closed", body: "Admission to the allowlist is split by what is knowable. Market facts are computed. The Shariah questions are not: whether a token's core purpose is a haram sector, whether it is asset-backed ('ayn) or a claim on a debtor (dayn), and whether it pays a riba-like yield. Those are attested through keel assets attest, never inferred. An absent attestation is a rejection, not a default pass.", - verify: { label: "compliance/screen.py", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/compliance/screen.py" }, + verify: { label: "compliance/screen.py", path: "keel/compliance/screen.py" }, }, { title: "The rails — eighteen checks no order can skip", @@ -51,32 +55,32 @@ export const features: LocalizedPage = { "A maximum-spread entry gate that refuses live BUYs at a spread of 50 basis points or wider, and refuses outright if the order book cannot be read", "A rail veto names the rail that fired and the command that clears it", ], - verify: { label: "execution/guards.py", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/execution/guards.py" }, + verify: { label: "execution/guards.py", path: "keel/execution/guards.py" }, }, { title: "Strategy gates — candidate → paper → live", body: "A rule must walk three stages before it can touch live money. Promotion clears a two-part gate: performance floors, and an overfitting check (PBO/CSCV). The 100-trade sample floor can be met by the rule's own backtest, or pooled across products in paper. Pooling requires at least five products contributing ten trades each, because a pool of correlated samples overstates its own power.", - verify: { label: "agent.py — RULE_REGISTRY", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/agent.py" }, + verify: { label: "agent.py — RULE_REGISTRY", path: "keel/agent.py" }, }, { title: "Honest measurement, against DCA", body: "keel simulate replays the real rules over fetched history, compares against a simple DCA benchmark, and writes a GO-LIVE / TRAIN-MORE report naming every gate and its numbers. The backtester prices per-product slippage scaled from each asset's real liquidity, from 5 to 50 basis points, so results cannot be flattered by thin order books. On the default rules it will very likely tell you TRAIN-MORE. That is the engine working, not broken.", - verify: { label: "the experiment record", href: "https://github.com/CodeGateSoftware/keel/tree/main/docs/experiments" }, + verify: { label: "the experiment record", path: "docs/experiments", kind: "tree" }, }, { title: "Three deployment profiles that share nothing", body: "Daily paper, live, and an hourly evidence profile (paper-hourly) — each with its own database and config. The hourly profile exists because the daily clock measures only 2.15 signals per asset-year, which puts a 100-trade review 31 to 84 years away. The same rules on ONE_HOUR bars fire 49.4 signals per asset-year, about 940 entry signals a year once pooled. That moves a forward-evidence review to weeks instead of decades. The hourly profile is measured net negative too: it exists to collect admissible forward evidence, not profit.", - verify: { label: "operator runbook", href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/operator-runbook.md" }, + verify: { label: "operator runbook", path: "docs/operator-runbook.md" }, }, { title: "A broker port, not a broker lock-in", body: "Adapters implement one contract — the keel-broker-api port — and register under the keel.brokers entry point. Coinbase Advanced Trade is the reference adapter; Robinhood ships as an optional, deliberately unwired venue; an Alpaca adapter joined in v0.10.0. A deliberately divergent fake venue keeps the port honest: the conformance suite, about 3,000 tests, runs against both.", - verify: { label: "packages/", href: "https://github.com/CodeGateSoftware/keel/tree/main/packages" }, + verify: { label: "packages/", path: "packages", kind: "tree" }, }, { title: "Confirm by default; autonomy changes who is asked", body: "keel previews each order and asks you at the terminal. Running headless, with no one to ask, it declines. keel autonomy on changes who is asked, never what is allowed. To stop trading, keel kill — the kill-switch fails closed.", - verify: { label: "the README, 'How keel works'", href: "https://github.com/CodeGateSoftware/keel#how-keel-works" }, + verify: { label: "the README, 'How keel works'", path: "README.md", hash: "#how-keel-works" }, }, ], inert: { @@ -102,7 +106,7 @@ export const features: LocalizedPage = { { title: "فرزُ أصولٍ موثَّق — يرفض عند الفشل", body: "القبولُ في قائمة الأصول المسموح بها مقسومٌ بحسب ما يمكن معرفتُه. فوقائعُ السوق تُحسَب؛ أمّا التصنيفات الشرعية — هل الغرض الأساسي للرمز قطاعٌ محرَّم (§28.4)، وهل هو عينٌ ('ayn) مدعومةٌ بأصلٍ أم دَينٌ (dayn)‏ (§65.5/§67.2)، وهل يوزّع عائدًا شبيهًا بالربا — فتُوثَّق ولا تُستنبَط، عبر الأمر keel assets attest. وغيابُ التوثيق رفضٌ، لا قبولٌ افتراضي.", - verify: { label: "compliance/screen.py", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/compliance/screen.py" }, + verify: { label: "compliance/screen.py", path: "keel/compliance/screen.py" }, }, { title: "سكك الأمان — ثمانية عشر فحصًا لا يتجاوزها أيُّ أمر", @@ -117,32 +121,32 @@ export const features: LocalizedPage = { "بوابةُ دخولٍ بحدٍّ أقصى لفارق السعر: ترفض الشراء الحيّ عند 50 نقطة أساسٍ أو أكثر، وترفض كذلك عند تعذُّر قراءة دفتر الأوامر", "رفضُ السكة يسمّي نفسه ويسمّي الأمرَ الذي يرفعه", ], - verify: { label: "execution/guards.py", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/execution/guards.py" }, + verify: { label: "execution/guards.py", path: "keel/execution/guards.py" }, }, { title: "بوابات الاستراتيجية — مرشَّحة ← تجريبية ← حيّة", body: "على القاعدة أن تجتاز ثلاث مراحل قبل أن تلمس مالًا حقيقيًّا. ولا تُرقَّى إلا باجتياز بوابةٍ من شقّين: حدودٌ دنيا للأداء، وفحصٌ للإفراط في المُلاءمة (PBO/CSCV). ويجوز بلوغُ الحدّ الأدنى البالغ مائة صفقة بالاختبار الرجعي للقاعدة نفسها، أو بتجميع صفقات الوسائط نفسها عبر منتجاتٍ متعدّدةٍ في التداول التجريبي — بشرط أن يُسهم خمسةُ منتجاتٍ على الأقل بعشر صفقاتٍ لكلٍّ منها، لأن تجميع عيّناتٍ مترابطةٍ يُبالغ في تقدير قوّتها الإحصائية.", - verify: { label: "agent.py — RULE_REGISTRY", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/agent.py" }, + verify: { label: "agent.py — RULE_REGISTRY", path: "keel/agent.py" }, }, { title: "قياسٌ صادق، مقابل الشراء الدوري المنتظم", body: "يعيد الأمر keel simulate تشغيلَ القواعد الحقيقية على التاريخ المجلوب، ويقارنها بمؤشّرٍ مرجعيٍّ بسيطٍ هو الشراء الدوري المنتظم (DCA)، ويكتب تقرير GO-LIVE أو TRAIN-MORE مسمّيًا كلَّ بوابةٍ وأرقامَها. ويُسعّر المحرّك الرجعي الانزلاقَ لكلِّ منتجٍ على قدر سيولته الفعلية (5–50 نقطة أساس)، فلا يمكن تجميلُ النتائج بدفاترِ أوامرَ ضعيفةِ السيولة. وعلى القواعد الافتراضية سيقول لك على الأرجح TRAIN-MORE — وهذا دليلُ عمل المحرّك لا دليلُ عطبه.", - verify: { label: "سجلّ التجارب", href: "https://github.com/CodeGateSoftware/keel/tree/main/docs/experiments" }, + verify: { label: "سجلّ التجارب", path: "docs/experiments", kind: "tree" }, }, { title: "ثلاثة أنماط نشرٍ لا يتقاسم أيٌّ منها شيئًا", body: "نمطٌ يوميٌّ تجريبي، ونمطٌ حيّ، ونمطٌ ساعيٌّ لجمع الأدلة (paper-hourly) — لكلٍّ منها قاعدةُ بياناته وإعداداته. ووُجد النمطُ الساعي لأن المؤقّت اليومي يقيس 2.15 إشارةً لكل أصلٍ في السنة (أي إنّ مراجعة المائة صفقة تبعد ما بين 31 و84 سنة)، بينما تُطلق القواعد نفسها على شموع الساعة 49.4 إشارة — أي نحو 940 إشارة دخولٍ سنويًّا بعد التجميع، فتصير مراجعةُ الأدلة الأمامية على بُعد أسابيع بدل عقود. وهو مقيسٌ بخسارةٍ صافيةٍ أيضًا: فقد وُجد لجمع أدلةٍ أماميةٍ مقبولة، لا للربح.", - verify: { label: "كتاب تشغيل المشغّل", href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/operator-runbook.md" }, + verify: { label: "كتاب تشغيل المشغّل", path: "docs/operator-runbook.md" }, }, { title: "منفذُ وسطاءٍ، لا ارتهانٌ لوسيط", body: "تُنفّذ المحوّلات عقدًا واحدًا — منفذ keel-broker-api — وتُسجَّل تحت نقطة الدخول keel.brokers. ومحوّل Coinbase Advanced Trade هو المحوّل المرجعي؛ ويُسلَّم Robinhood منصّةً اختياريةً غيرَ موصولةٍ عمدًا؛ وانضمّ محوّل Alpaca في الإصدار v0.10.0. وثمّة منصّةٌ وهميةٌ متعمَّدةُ الاختلاف تُبقي المنفذ أمينًا: إذ تعمل حزمةُ اختبارات المطابقة (نحو 3,000 اختبار) على الاثنتين معًا.", - verify: { label: "packages/", href: "https://github.com/CodeGateSoftware/keel/tree/main/packages" }, + verify: { label: "packages/", path: "packages", kind: "tree" }, }, { title: "التأكيدُ هو الأصل؛ والاستقلاليةُ تغيّر مَن يُسأل", body: "يعاين كيل كلَّ أمرٍ ويسأل عند الطرفية؛ وإن كان يعمل بلا واجهةٍ تفاعلية رفض الأمر. والأمر keel autonomy on يغيّر مَن يُسأل، لا ما يُسمح به. ولإيقاف التداول: keel kill — ومفتاحُ الإيقاف يرفض عند الفشل.", - verify: { label: "الـREADME، «كيف يعمل كيل»", href: "https://github.com/CodeGateSoftware/keel#how-keel-works" }, + verify: { label: "الـREADME، «كيف يعمل كيل»", path: "README.md", hash: "#how-keel-works" }, }, ], inert: { @@ -168,7 +172,7 @@ export const features: LocalizedPage = { { title: "Filtrage des actifs par attestation — blocage par défaut", body: "L'admission dans la liste blanche est découpée selon ce qu'il est possible de savoir. Les faits de marché se calculent. Les classifications Shariah — l'activité principale du token relève-t-elle d'un secteur interdit (§28.4), s'agit-il d'un 'ayn adossé à un actif ou d'une créance dayn (§65.5/§67.2), le token verse-t-il un rendement assimilable au riba — sont attestées, jamais déduites, au moyen de keel assets attest. Une attestation manquante vaut refus, pas acceptation par défaut.", - verify: { label: "compliance/screen.py", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/compliance/screen.py" }, + verify: { label: "compliance/screen.py", path: "keel/compliance/screen.py" }, }, { title: "Les garde-fous (rails) — dix-huit contrôles qu'aucun ordre ne contourne", @@ -183,32 +187,32 @@ export const features: LocalizedPage = { "Un plafond d'écart (spread) à l'entrée, qui refuse tout achat réel à partir de 50 points de base et bloque d'office si le carnet est illisible", "Tout veto d'un garde-fou se nomme et indique la commande qui le lève", ], - verify: { label: "execution/guards.py", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/execution/guards.py" }, + verify: { label: "execution/guards.py", path: "keel/execution/guards.py" }, }, { title: "Verrous de stratégie — candidate → papier → réel", body: "Une règle doit franchir trois étapes avant de toucher de l'argent réel. La promotion passe par un verrou en deux volets : des seuils de performance et un contrôle de surapprentissage (PBO/CSCV). Le seuil de 100 transactions peut être atteint par le backtest de la règle, ou en mutualisant le même jeu de paramètres sur d'autres produits en papier — à condition qu'au moins cinq produits y contribuent pour dix transactions chacun, car un ensemble d'échantillons corrélés surestime sa propre puissance.", - verify: { label: "agent.py — RULE_REGISTRY", href: "https://github.com/CodeGateSoftware/keel/blob/main/keel/agent.py" }, + verify: { label: "agent.py — RULE_REGISTRY", path: "keel/agent.py" }, }, { title: "Mesure honnête, face au DCA", body: "keel simulate rejoue les vraies règles sur l'historique récupéré, les compare à la référence DCA et rédige un rapport GO-LIVE / TRAIN-MORE qui nomme chaque verrou et ses chiffres. Le backtesteur applique à chaque produit un glissement calibré sur sa liquidité réelle (5 à 50 points de base), afin qu'aucun résultat ne puisse être flatté par un carnet d'ordres peu liquide. Sur les règles par défaut, il vous répondra très probablement TRAIN-MORE : c'est le moteur qui fonctionne, pas une panne.", - verify: { label: "le registre des expériences", href: "https://github.com/CodeGateSoftware/keel/tree/main/docs/experiments" }, + verify: { label: "le registre des expériences", path: "docs/experiments", kind: "tree" }, }, { title: "Trois profils de déploiement qui ne partagent rien", body: "Papier quotidien, réel, et un profil horaire de collecte de preuves (paper-hourly) — chacun avec sa propre base de données et sa propre configuration. Le profil horaire existe parce que l'horloge quotidienne ne mesure que 2,15 signaux par actif et par an : à ce rythme, il faudrait de 31 à 84 ans pour réunir les 100 transactions d'une revue. Les mêmes règles sur des bougies ONE_HOUR en déclenchent 49,4 — environ 940 signaux d'entrée par an une fois mutualisés — ce qui ramène cette revue à quelques semaines au lieu de quelques décennies. Lui aussi est mesuré perdant : il existe pour collecter des preuves recevables, pas du profit.", - verify: { label: "le runbook opérateur", href: "https://github.com/CodeGateSoftware/keel/blob/main/docs/operator-runbook.md" }, + verify: { label: "le runbook opérateur", path: "docs/operator-runbook.md" }, }, { title: "Un port courtier, pas un enfermement propriétaire", body: "Les adaptateurs mettent en œuvre un seul contrat — le port keel-broker-api — et se déclarent sous le point d'entrée keel.brokers. Coinbase Advanced Trade est l'adaptateur de référence ; Robinhood est livré comme plateforme optionnelle, délibérément non raccordée ; un adaptateur Alpaca s'y est ajouté en v0.10.0. Une plateforme factice, volontairement divergente, maintient le port honnête : la suite de conformité (~3 000 tests) s'exécute sur les deux.", - verify: { label: "packages/", href: "https://github.com/CodeGateSoftware/keel/tree/main/packages" }, + verify: { label: "packages/", path: "packages", kind: "tree" }, }, { title: "Confirmation par défaut ; l'autonomie change l'interlocuteur, pas la règle", body: "keel prévisualise chaque ordre et demande confirmation dans le terminal ; sans opérateur, il refuse. keel autonomy on change qui l'on interroge, jamais ce qui est permis. Pour tout arrêter : keel kill — le coupe-circuit se ferme en cas de défaillance.", - verify: { label: "le README, « How keel works »", href: "https://github.com/CodeGateSoftware/keel#how-keel-works" }, + verify: { label: "le README, « How keel works »", path: "README.md", hash: "#how-keel-works" }, }, ], inert: { diff --git a/src/lib/engine-url.ts b/src/lib/engine-url.ts new file mode 100644 index 0000000..e69217a --- /dev/null +++ b/src/lib/engine-url.ts @@ -0,0 +1,73 @@ +/** + * Engine-repository links, built from the ref this build actually resolved. + * + * #85 pinned the docs pipeline to the latest published release tag; #91 is the + * same skew in hand-written links. The ref lives in exactly one place — + * data/docs-meta.json, written by scripts/fetch-engine-docs.mjs — and every + * link into the engine repo is constructed from it here, so a hand-written + * href cannot drift from the release the reader is running. + * + * `scripts/check-engine-refs.mjs` keeps it that way — run by `npm run check` and + * as its own CI step. It fails on any engine URL naming a branch, in every shape + * GitHub serves (`blob/`, `tree/`, `raw/`, and raw.githubusercontent.com), with + * two documented exceptions: `scripts/install.sh`, which exists only on the + * default branch, and `src/content/`, which is fetched release-note prose whose + * links are quotes the site must not rewrite. + */ +import { loadDocsMeta } from "../components/docs/nav"; + +/** A heading anchor. Fragments survive pinning — #scholarly-review-status must + * still land on that section, not at the top of the file. */ +export type EngineHash = `#${string}`; + +/** A pointer into the engine repository, as copy files carry it: a repo-relative + * path, never a URL. The component builds the URL at the resolved ref. */ +export interface EngineSource { + /** Repo-relative, e.g. "keel/compliance/screen.py" or "docs/glossary.md". */ + path: string; + /** "tree" for a directory listing; "blob" (the default) for a single file. */ + kind?: "blob" | "tree"; + /** Optional heading anchor, appended verbatim. */ + hash?: EngineHash; +} + +/** + * Build an absolute GitHub URL at the ref this build resolved. + * + * Throws — deliberately, and loudly — when no ref was resolved. nav.ts's + * FALLBACK_META sets `ref: ""` rather than naming a branch, because a + * hard-coded "main" would be a claim rather than a default. Falling back here + * would restore exactly the skew this removes: a "Verify in the repository" + * link that shows the reader code their release does not contain. Astro + * surfaces the throw as a build failure, which is the intended behaviour and + * matches FR-4 — the build stops rather than publishing a false pointer. + */ +export function engineSourceUrl(source: EngineSource): string { + const meta = loadDocsMeta(); + // Trimmed, not just checked for truthiness: `fetch-engine-docs.mjs` returns a + // hand-set `engine-docs.manifest.json` ref VERBATIM (it short-circuits before + // release-tag.mjs's TAG_PATTERN), so a stray space would otherwise pass the + // guard below and ship `blob/ /keel/...` on a green build. + const repo = (meta.repo ?? "").trim(); + const ref = (meta.ref ?? "").trim(); + const path = source.path.replace(/^\/+/, ""); + + if (!path) { + throw new Error("engine link: an empty path cannot be pinned to a ref."); + } + if (!ref || !repo) { + throw new Error( + `engine link: data/docs-meta.json resolved no ${!ref ? "ref" : "repo"}, so "${path}" ` + + "cannot be pinned. Run `npm run fetch` — scripts/fetch-engine-docs.mjs writes it. " + + 'There is deliberately no fallback to "main": an unpinned link would invite a reader ' + + "to verify a release against code that is not in it.", + ); + } + + return `https://github.com/${repo}/${source.kind ?? "blob"}/${ref}/${path}${source.hash ?? ""}`; +} + +/** Convenience for the common case: a single file, optionally at an anchor. */ +export function engineBlobUrl(path: string, hash?: EngineHash): string { + return engineSourceUrl({ path, hash }); +}