From 926e0710179dd48edfa82b3f33174da2e52b76c4 Mon Sep 17 00:00:00 2001 From: AndyD9 Date: Wed, 12 Aug 2026 13:22:45 +0200 Subject: [PATCH 1/2] docs: adopt POSEIDON and bootstrap the repository Turn the freshly installed kit into this repository's kit, and repair the half-finished install that preceded it. Repair: - restore 25 tracked documents deleted from the worktree by install.sh - archive the superseded set (roadmap, stack, security, quality, support, templates, tickets T0001-T0005) under docs/archive/ with git mv - keep the four accepted ADRs live in docs/decisions/ - remove the stale duplicate of the kit left at the repository root Bootstrap, from an audit of the code and the owner's answers: - ARCHITECTURE.md: seven boundaries, each row citing the file it was read from - RULES.md: one section per boundary; invariants written as refusals, seeded from scripts/security-check.ps1 and real incidents only - VALIDATION.md: commands copied from .github/workflows, honest `manual` rows where no check exists - SETUP.md, PRODUCT.md, AGENTS.md, CURRENT_STATE.md - KNOWN_ISSUES.md: KI-001..KI-015 carried over, KI-016..KI-019 found in the audit - LEARNINGS.md: LC-2026-001 and LC-2026-002 ADR-0005 (proposed) supersedes ADR-0002: harden the existing application instead of rewriting it. Needs the owner's acceptance. CURRENT_STATE.md records no shipped capability. The only executed evidence is the frontend suite: 4 files, 11 tests, passing. That is deliberate, not an omission. checks: agent mode - python3 is absent, so poseidon/checks/check.py runs nowhere including CI. Caps and placeholders were verified by hand. Co-Authored-By: Claude Opus 5 --- .claude/commands/bootstrap.md | 6 + .claude/commands/check.md | 6 + .claude/commands/close.md | 6 + .claude/commands/handoff.md | 6 + .claude/commands/locate.md | 6 + .claude/commands/milestone.md | 6 + .claude/commands/poseidon.md | 6 + .claude/commands/review.md | 6 + .claude/commands/unit-new.md | 6 + .claude/commands/unit-ready.md | 6 + .claude/commands/verify.md | 6 + .claude/skills/poseidon/SKILL.md | 9 + AGENTS.md | 230 ++--- CLAUDE.md | 3 + docs/ARCHITECTURE.md | 285 +++--- docs/CURRENT_STATE.md | 309 ++----- docs/KNOWN_ISSUES.md | 89 +- docs/LEARNINGS.md | 105 +++ docs/PRODUCT.md | 195 ++-- docs/RULES.md | 129 +++ docs/SETUP.md | 100 +++ docs/VALIDATION.md | 101 +++ docs/WORKFLOW.md | 174 ++-- docs/{ => archive}/QUALITY.md | 0 docs/{ => archive}/README.md | 0 docs/{ => archive}/ROADMAP.md | 0 docs/{ => archive}/SECURITY.md | 0 docs/{ => archive}/STACK.md | 0 docs/{ => archive}/SUPPORT.md | 0 docs/{ => archive}/templates/ADR.md | 0 docs/{ => archive}/templates/PHASE_REVIEW.md | 0 docs/{ => archive}/templates/TICKET.md | 0 docs/{ => archive}/tickets/README.md | 0 .../tickets/T0001-baseline-reproductible.md | 0 ...002-modele-produit-solo-ou-collaboratif.md | 0 .../tickets/T0003-strategie-de-refonte.md | 0 .../T0004-matrice-support-windows-msfs.md | 0 .../tickets/T0005-selection-stack-cible.md | 0 ...0005-durcissement-au-lieu-de-reecriture.md | 80 ++ docs/decisions/README.md | 27 +- docs/features/.gitkeep | 0 docs/validation-baselines.json | 8 + poseidon/POSEIDON.md | 148 +++ poseidon/README.md | 277 ++++++ poseidon/VERSION | 1 + poseidon/checks/check.py | 847 ++++++++++++++++++ poseidon/checks/limits.json | 144 +++ poseidon/commands/bootstrap.md | 161 ++++ poseidon/commands/check.md | 42 + poseidon/commands/close.md | 70 ++ poseidon/commands/handoff.md | 37 + poseidon/commands/install.md | 89 ++ poseidon/commands/locate.md | 30 + poseidon/commands/milestone.md | 74 ++ poseidon/commands/review.md | 52 ++ poseidon/commands/unit-new.md | 60 ++ poseidon/commands/unit-ready.md | 36 + poseidon/commands/verify.md | 55 ++ poseidon/references/checks-by-hand.md | 70 ++ poseidon/references/delegation.md | 41 + poseidon/references/install.md | 63 ++ poseidon/references/recovery.md | 48 + poseidon/references/review.md | 45 + poseidon/skeletons/AGENTS.md | 113 +++ poseidon/skeletons/docs/ARCHITECTURE.md | 77 ++ poseidon/skeletons/docs/CURRENT_STATE.md | 59 ++ poseidon/skeletons/docs/KNOWN_ISSUES.md | 44 + poseidon/skeletons/docs/LEARNINGS.md | 70 ++ poseidon/skeletons/docs/PRODUCT.md | 71 ++ poseidon/skeletons/docs/RULES.md | 117 +++ poseidon/skeletons/docs/SETUP.md | 76 ++ poseidon/skeletons/docs/VALIDATION.md | 80 ++ poseidon/skeletons/docs/WORKFLOW.md | 115 +++ poseidon/templates/ADR.md | 41 + poseidon/templates/AGENTS-zone.md | 49 + poseidon/templates/EXAMPLE-FEATURE.md | 164 ++++ poseidon/templates/FEATURE.md | 103 +++ 77 files changed, 4761 insertions(+), 718 deletions(-) create mode 100644 .claude/commands/bootstrap.md create mode 100644 .claude/commands/check.md create mode 100644 .claude/commands/close.md create mode 100644 .claude/commands/handoff.md create mode 100644 .claude/commands/locate.md create mode 100644 .claude/commands/milestone.md create mode 100644 .claude/commands/poseidon.md create mode 100644 .claude/commands/review.md create mode 100644 .claude/commands/unit-new.md create mode 100644 .claude/commands/unit-ready.md create mode 100644 .claude/commands/verify.md create mode 100644 .claude/skills/poseidon/SKILL.md create mode 100644 CLAUDE.md create mode 100644 docs/LEARNINGS.md create mode 100644 docs/RULES.md create mode 100644 docs/SETUP.md create mode 100644 docs/VALIDATION.md rename docs/{ => archive}/QUALITY.md (100%) rename docs/{ => archive}/README.md (100%) rename docs/{ => archive}/ROADMAP.md (100%) rename docs/{ => archive}/SECURITY.md (100%) rename docs/{ => archive}/STACK.md (100%) rename docs/{ => archive}/SUPPORT.md (100%) rename docs/{ => archive}/templates/ADR.md (100%) rename docs/{ => archive}/templates/PHASE_REVIEW.md (100%) rename docs/{ => archive}/templates/TICKET.md (100%) rename docs/{ => archive}/tickets/README.md (100%) rename docs/{ => archive}/tickets/T0001-baseline-reproductible.md (100%) rename docs/{ => archive}/tickets/T0002-modele-produit-solo-ou-collaboratif.md (100%) rename docs/{ => archive}/tickets/T0003-strategie-de-refonte.md (100%) rename docs/{ => archive}/tickets/T0004-matrice-support-windows-msfs.md (100%) rename docs/{ => archive}/tickets/T0005-selection-stack-cible.md (100%) create mode 100644 docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md create mode 100644 docs/features/.gitkeep create mode 100644 docs/validation-baselines.json create mode 100644 poseidon/POSEIDON.md create mode 100644 poseidon/README.md create mode 100644 poseidon/VERSION create mode 100644 poseidon/checks/check.py create mode 100644 poseidon/checks/limits.json create mode 100644 poseidon/commands/bootstrap.md create mode 100644 poseidon/commands/check.md create mode 100644 poseidon/commands/close.md create mode 100644 poseidon/commands/handoff.md create mode 100644 poseidon/commands/install.md create mode 100644 poseidon/commands/locate.md create mode 100644 poseidon/commands/milestone.md create mode 100644 poseidon/commands/review.md create mode 100644 poseidon/commands/unit-new.md create mode 100644 poseidon/commands/unit-ready.md create mode 100644 poseidon/commands/verify.md create mode 100644 poseidon/references/checks-by-hand.md create mode 100644 poseidon/references/delegation.md create mode 100644 poseidon/references/install.md create mode 100644 poseidon/references/recovery.md create mode 100644 poseidon/references/review.md create mode 100644 poseidon/skeletons/AGENTS.md create mode 100644 poseidon/skeletons/docs/ARCHITECTURE.md create mode 100644 poseidon/skeletons/docs/CURRENT_STATE.md create mode 100644 poseidon/skeletons/docs/KNOWN_ISSUES.md create mode 100644 poseidon/skeletons/docs/LEARNINGS.md create mode 100644 poseidon/skeletons/docs/PRODUCT.md create mode 100644 poseidon/skeletons/docs/RULES.md create mode 100644 poseidon/skeletons/docs/SETUP.md create mode 100644 poseidon/skeletons/docs/VALIDATION.md create mode 100644 poseidon/skeletons/docs/WORKFLOW.md create mode 100644 poseidon/templates/ADR.md create mode 100644 poseidon/templates/AGENTS-zone.md create mode 100644 poseidon/templates/EXAMPLE-FEATURE.md create mode 100644 poseidon/templates/FEATURE.md diff --git a/.claude/commands/bootstrap.md b/.claude/commands/bootstrap.md new file mode 100644 index 0000000..ca282f2 --- /dev/null +++ b/.claude/commands/bootstrap.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /bootstrap — see poseidon/commands/bootstrap.md +--- + +Read `poseidon/commands/bootstrap.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/check.md b/.claude/commands/check.md new file mode 100644 index 0000000..13f6814 --- /dev/null +++ b/.claude/commands/check.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /check — see poseidon/commands/check.md +--- + +Read `poseidon/commands/check.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/close.md b/.claude/commands/close.md new file mode 100644 index 0000000..3853829 --- /dev/null +++ b/.claude/commands/close.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /close — see poseidon/commands/close.md +--- + +Read `poseidon/commands/close.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/handoff.md b/.claude/commands/handoff.md new file mode 100644 index 0000000..f2812ce --- /dev/null +++ b/.claude/commands/handoff.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /handoff — see poseidon/commands/handoff.md +--- + +Read `poseidon/commands/handoff.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/locate.md b/.claude/commands/locate.md new file mode 100644 index 0000000..06df235 --- /dev/null +++ b/.claude/commands/locate.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /locate — see poseidon/commands/locate.md +--- + +Read `poseidon/commands/locate.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/milestone.md b/.claude/commands/milestone.md new file mode 100644 index 0000000..f2b929f --- /dev/null +++ b/.claude/commands/milestone.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /milestone — see poseidon/commands/milestone.md +--- + +Read `poseidon/commands/milestone.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/poseidon.md b/.claude/commands/poseidon.md new file mode 100644 index 0000000..350fa59 --- /dev/null +++ b/.claude/commands/poseidon.md @@ -0,0 +1,6 @@ +--- +description: Launch POSEIDON — locate the current state and route to the one action that applies. +--- + +Read `poseidon/POSEIDON.md` and follow it exactly. If the file is missing, say +so and stop; do not improvise a method in its place. diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 0000000..f8285b7 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /review — see poseidon/commands/review.md +--- + +Read `poseidon/commands/review.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/unit-new.md b/.claude/commands/unit-new.md new file mode 100644 index 0000000..d83fcac --- /dev/null +++ b/.claude/commands/unit-new.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /unit-new — see poseidon/commands/unit-new.md +--- + +Read `poseidon/commands/unit-new.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/unit-ready.md b/.claude/commands/unit-ready.md new file mode 100644 index 0000000..aee9bc3 --- /dev/null +++ b/.claude/commands/unit-ready.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /unit-ready — see poseidon/commands/unit-ready.md +--- + +Read `poseidon/commands/unit-ready.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/commands/verify.md b/.claude/commands/verify.md new file mode 100644 index 0000000..e676545 --- /dev/null +++ b/.claude/commands/verify.md @@ -0,0 +1,6 @@ +--- +description: POSEIDON /verify — see poseidon/commands/verify.md +--- + +Read `poseidon/commands/verify.md` and follow it exactly, with +$ARGUMENTS as its arguments. If the file is missing, say so and stop. diff --git a/.claude/skills/poseidon/SKILL.md b/.claude/skills/poseidon/SKILL.md new file mode 100644 index 0000000..bcd645b --- /dev/null +++ b/.claude/skills/poseidon/SKILL.md @@ -0,0 +1,9 @@ +--- +name: poseidon +description: POSEIDON — the engineering method for this repository. Use when the user says "lance poseidon", "launch poseidon", "poseidon", "start poseidon", or asks what to work on, where things stand, or what to do next. Also use at the start of any session that will change code in this repo. +--- + +Read `poseidon/POSEIDON.md` at the repository root and follow it exactly. + +If that file does not exist, say so and stop. Do not improvise a method in its +place — an invented process that looks like POSEIDON is worse than none. diff --git a/AGENTS.md b/AGENTS.md index 40d9a38..2faa384 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,117 +1,137 @@ -# AGENTS.md — Loi du dépôt Thrustline - -Ce fichier s'applique à tout le dépôt. Toute instruction plus locale doit être -compatible avec ces règles. - -## Mission - -Thrustline est une application Windows distribuable de gestion de compagnie -aérienne virtuelle pour Microsoft Flight Simulator. La refonte vise en priorité : - -1. stabilité et récupération après erreur ; -2. sécurité d'un client distribué et modifiable ; -3. intégrité de l'économie et des données ; -4. compatibilité MSFS/SimConnect ; -5. maintenabilité, testabilité et mises à jour sûres. - -## Sources de vérité - -Lire avant tout travail : - -1. `docs/CURRENT_STATE.md` — ce qui existe réellement ; -2. `docs/ROADMAP.md` — ordre des phases ; -3. le ticket concerné dans `docs/tickets/` ; -4. les documents spécialisés indiqués par le ticket. - -Références permanentes : - -- `docs/PRODUCT.md` — périmètre et règles produit ; -- `docs/ARCHITECTURE.md` — architecture cible et frontières ; -- `docs/SECURITY.md` — règles de sécurité ; -- `docs/QUALITY.md` — stratégie de tests et critères de qualité ; -- `docs/WORKFLOW.md` — cycle complet d'un ticket. - -Le code et les migrations appliquées priment sur une documentation périmée. -Signaler et corriger les divergences dans le même ticket si elles sont directement -liées au changement. - -## Règles de travail - -- Implémenter un seul ticket à la fois. -- Ne pas anticiper les tickets futurs. -- Ne pas refactorer un système sans rapport. -- Respecter strictement `Allowed areas` et `Do not touch`. -- Préserver les modifications utilisateur non liées. -- Ne pas changer l'architecture sans ADR accepté. -- Éviter toute dépendance nouvelle si une solution simple existe déjà. -- Arrêter et demander une décision si une ambiguïté change le produit, la - sécurité, les données ou l'architecture. -- Un problème découvert hors périmètre devient un follow-up dans - `docs/KNOWN_ISSUES.md`, pas une modification opportuniste. - -## Frontières techniques - -- `app/` : Tauri v2, React, TypeScript, Vite, UI et orchestration cliente. -- `sim-bridge/` : .NET 8, SimConnect, télémétrie locale, REST/SignalR. -- `supabase/` : Auth, PostgreSQL, RLS, Realtime, RPC et Edge Functions. -- `legacy/` : lecture seule jusqu'à son archivage explicite. - -Le desktop, le sidecar et MSFS sont des clients non fiables. Le serveur est -autoritaire pour l'argent, la propriété, la réputation, la progression et les -transitions sensibles. Aucun secret backend ne doit être livré au client. - -## Qualité d'implémentation - -- TypeScript strict, C# nullable et Rust sans avertissement introduit. -- Pages React minces ; règles métier et accès données hors des composants. -- Mutations sensibles via commande serveur transactionnelle et idempotente. -- Migrations Supabase append-only. -- Contrats partagés versionnés et consommateurs mis à jour ensemble. -- Erreurs actionnables pour l'utilisateur, détails techniques dans des logs - redigés. -- Aucun secret, JWT, donnée personnelle ou header d'authentification dans Git ou - les logs. +# AGENTS.md — Thrustline + +Short on purpose. It carries the invariants; detail lives in the documents it +cites, opened only when the task concerns them. + +Thrustline is a distributable Windows 11 desktop application for managing a +virtual airline in Microsoft Flight Simulator 2024, for a solo owner-operator. +Priorities, in order: stability and recovery after error; security of a +distributed, modifiable client; economy and data integrity; MSFS and SimConnect +compatibility; maintainability. Never sacrifice a higher priority to speed up a +lower one. Given two equally correct options, the one serving the higher +priority wins, even if it costs more. + +## Start here — every session + +Say "lance poseidon", or run `/locate`. It tells you which state you are in and +which single command applies. Do not begin work without it: that is how a +session ends up touching four unrelated things. + +## What to read, and when + +1. this file; +2. `/AGENTS.md` for the zone being worked, if it exists; +3. `docs/CURRENT_STATE.md` — the proven state, held under 200 lines; +4. the unit: in full at its first milestone; afterwards the header, the open + milestone, `Allowed areas`, `Do not touch`. + +That is all. `RULES.md` is read **one section** at a time, for the boundary the +milestone touches. `VALIDATION.md` is read **one row** at a time. Never open a +document "for context". + +Precedence when sources disagree: code, migrations and lockfiles on the branch > +active unit and accepted decisions > `CURRENT_STATE.md` > specialised docs > +roadmap and README. A branch or an unmerged Pull Request is never a delivered +capability — always distinguish local, pushed, in review, merged. On a +contradiction, stop, quote both passages, and have the stale source fixed. + +On demand under `docs/`: `RULES.md` (invariants by boundary), `VALIDATION.md` +(commands), `ARCHITECTURE.md` (where trust changes), `WORKFLOW.md` (the cycle), +`SETUP.md` (getting it running), `PRODUCT.md` (what it is and is not), +`KNOWN_ISSUES.md`, `LEARNINGS.md`, `decisions/` (accepted ADRs). +`docs/archive/` is historical: read it for context, never as law. + +## Non-negotiable invariants + +- **Authority.** The server is authoritative for money, ownership, reputation, + progression and every sensitive transition. The desktop app, the sidecar and + MSFS are untrusted clients. Never accept an identity, a role, an amount, an + outcome or the current time from a client. Never ship a backend secret to a + client, and never return a privileged Supabase credential to the sidecar. +- **Data.** Migrations are append-only; never edit or delete an applied one. + Every sensitive mutation is transactional and idempotent. No schema change + merges without proving isolation for owner A, owner B and anonymous. +- **Secrets.** Never commit or log a secret, token, JWT, authentication header, + personal datum, or environment file. Never execute a remote script. +- **Boundaries.** Validate every input at every boundary before any side effect: + data and persistence, authoritative service, bridge local API, SimConnect and + MSFS, third-party egress, desktop shell, distribution. +- **Code.** TypeScript strict, C# nullable, no new Rust warning. Business rules + and data access stay out of React components. Shared contracts are versioned + and their consumers updated in the same change. Exact versions and committed + lockfiles. Never add a dependency where the pinned toolchain suffices. +- **Decisions.** No architecture or trust-boundary change without a written ADR + accepted by `PacoRCS`; an accepted ADR changes only via one that supersedes + it. +- **Versions.** Semver, currently resetting to `0.0.1-alpha`. Never an opaque + version, a reused number, or a moved tag. The single source among the four + manifests is **not yet decided** — see `PRODUCT.md`, open questions. +- **Legacy.** `legacy/` is read-only until explicitly archived. + +This file stays under two pages: a new global rule **replaces or condenses** an +existing one. It never stacks. + +## Scope + +One unit = one user capability = one branch = one worktree. At most **two** units +`In progress`. Cycle in `docs/WORKFLOW.md`. + +- Implement the unit only; respect `Allowed areas` and `Do not touch`. Do not + anticipate future units, and do not refactor an unrelated system. +- Record any out-of-scope discovery in `docs/KNOWN_ISSUES.md` with evidence and + severity, **without fixing it**. +- Never include, overwrite or publish a pre-existing user change unrelated to + the unit. +- **Decide alone by default.** Facing an ambiguity, choose the safest option — + then, among equally safe ones, the fastest — implement it, and record the + assumption in the report. `PacoRCS` is asked only when no option satisfies the + invariants, when the act is irreversible or leaves the repo, or when it is a + product, security, data or architecture call no evidence settles. A question + that can wait for the report does not interrupt the work. +- The unit file's `Status` field is authoritative; its index is generated from + that field and is never hand-edited. ## Validation -Exécuter les contrôles proportionnés au ticket, puis consigner les résultats : - -```powershell -# Frontend -Set-Location app -npm test -npm run build +`docs/VALIDATION.md` gives the commands per area touched: targeted tests first, +then boundary checks. Never report a check you did not run as passed: `passed`, +`not run`, `blocked by environment`, and `failed` are four distinct outcomes, +and a tool exiting `0` without discovering any test proves nothing — `dotnet +test` does exactly that in this repository. -# Sidecar -Set-Location ..\sim-bridge -dotnet build --configuration Release -dotnet test --configuration Release +SQL changes require a local or staging isolation test between two users. +SimConnect changes require a replayed trace or a documented manual MSFS test. -# Tauri -Set-Location ..\app\src-tauri -cargo check --locked +`python3` is absent here, so POSEIDON's checks run in **agent mode**: every +report must say `checks: agent mode — not deterministic, no CI gate`. -# Invariants du dépôt -Set-Location ..\.. -.\scripts\security-check.ps1 -``` +Three failed attempts at the same fix: revert to the last green commit, record +the failure with its evidence, halt. A fourth attempt is forbidden. -Les changements SQL exigent des tests locaux/staging d'isolation entre deux -utilisateurs. Les changements SimConnect exigent un replay de trace ou un test -manuel MSFS documenté. Ne jamais annoncer comme réussi un contrôle non exécuté. +## Git -## Fin de ticket +- Read the current branch immediately before every commit — a shared worktree's + `HEAD` may have moved. Never invent a branch, a base, or a CI result: read + them. +- Stage by explicit path list; never `git add .` or `git add -A`; re-read + `git diff --cached` and run `git diff --cached --check` before committing. +- Conventional Commits. Branch, commit, push and open or update the Pull Request + autonomously — no intermediate confirmation is needed for those. +- Never force-push, never modify a protected branch directly, never bypass a + protection. The Pull Request stays draft until validations and dependencies + are ready. +- Git for Windows and PowerShell for this repository, never Git under WSL on + `/mnt/c`. Never ask for a GitHub password; recommend `gh auth login`. +- **Final review and merge belong exclusively to `PacoRCS`**, and require his + explicit confirmation. -Un ticket n'est `Done` que si : +## Ending a task -- ses critères d'acceptation sont satisfaits ; -- les tests automatisés pertinents passent ; -- sa vérification manuelle a été effectuée ou clairement déléguée ; -- les risques et limites sont consignés ; -- `docs/CURRENT_STATE.md` est mis à jour si l'état réel change ; -- le ticket contient son Completion Report ; -- aucun changement hors périmètre n'est inclus. +The report gives: status, files changed, commands and real results, risks and +limits, branch, commit, Pull Request. -Le rapport final doit donner : résumé, fichiers modifiés, commandes exécutées, -résultats, vérification manuelle, risques, follow-ups et documentation mise à jour. +**Every report ends with `Next`** — the next concrete action, who owns it, what +unblocks it — **then a resume prompt** copyable into a fresh session, giving +pointers and state, never document contents. Format in `/handoff`. +A report without both is not finished. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..25b705c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +@AGENTS.md + +When the user says "lance poseidon", "launch poseidon", or "/poseidon", read `poseidon/POSEIDON.md` and follow it. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4493b1a..2defee5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,177 +1,108 @@ -# Architecture cible - -Statut : cible de refonte ; toute divergence durable nécessite un ADR. - -## Architecture logique - -```text -MSFS - │ SimConnect (données non fiables) - ▼ -Bridge .NET 10 LTS - │ événements de vol versionnés - │ canal local authentifié - ▼ -Tauri 2.11 / Rust stable épinglé - │ capacités natives minimales - ▼ -React 19 / TypeScript 6 - │ lectures RLS + commandes HTTPS - ▼ -Supabase / PostgreSQL 17 - ├─ Auth - ├─ PostgreSQL + RLS - ├─ RPC/Edge Functions transactionnelles - ├─ Realtime - └─ jobs serveur contrôlés -``` - -## Frontières de confiance - -- Le frontend, Tauri, le bridge, MSFS et tous les fichiers importés sont publics - et modifiables par l'utilisateur. -- Supabase valide identité, appartenance, permissions et transitions. -- Le serveur recalcule argent, prix, récompenses, usure et progression. -- La télémétrie est un signal borné et analysé, pas une preuve absolue. - -## Responsabilités - -### React - -- Présentation, formulaires, cache de vues et états UX. -- Aucun calcul client utilisé comme autorité économique. -- Pages minces ; logique dans modules de fonctionnalité testables. -- Une couche `queries` pour les lectures et `commands` pour les mutations. - -### Tauri/Rust - -- Cycle de vie de l'application et du bridge. -- Capacités natives en allowlist. -- Validation native des URL externes. -- Update signée, diagnostics et stockage local approprié. - -### Bridge .NET - -- Connexion/reconnexion SimConnect. -- Client hors processus self-contained Windows x64, limité à la session locale. -- SDK/API SimConnect officiel MSFS 2024 isolé derrière `ISimConnectAdapter` ; - aucun type SDK dans le domaine et aucune dépendance du domaine au wrapper. -- Normalisation, validation et agrégation de la télémétrie. -- Machine à états de vol déterministe et rejouable. -- Rapport de vol versionné. -- Aucun secret privilégié ni écriture économique directe. - -### Supabase - -- Identité et autorisation. -- Ownership MVP limité côté serveur à un propriétaire par compagnie et au plus - une compagnie par utilisateur. -- Identité de compagnie distincte de l'identité utilisateur, sans droit - collaboratif implicite. -- Commandes sensibles transactionnelles et idempotentes. -- Grand livre immuable et projections. -- RLS défensive sur chaque table exposée. -- Audit des changements sensibles. -- Traitements passifs côté serveur, indépendants d'un client ouvert. -- PostgreSQL 17 fourni par la plateforme ; migrations et types générés depuis - le schéma neuf. -- RPC SQL préférée pour les commandes purement transactionnelles ; Edge - Functions réservées à l'orchestration réseau ou aux secrets serveur. - -## Stratégie de construction - -Conformément à `ADR-0002`, l'architecture cible sera construite par réécriture -totale dans un nouveau dépôt et un historique Git neuf. Le dépôt actuel reste une -référence en lecture seule pour les comportements, l'UX et la documentation -jusqu'à validation de la parité du golden path, puis il est archivé. - -Il n'existe aucune coexistence en production, aucune double lecture/écriture et -aucune compatibilité avec l'ancien schéma. Le nouveau dépôt utilise des branches -courtes, une intégration fréquente et une CI obligatoire. Le premier vertical -slice critique est le moteur de vol SimConnect rejouable jusqu'au rapport -versionné. - -## Organisation cible - -```text -app/src/ - app/ # composition, routes, providers - features/ - dispatch/ - api/ - components/ - hooks/ - model/ - tests/ - shared/ # UI, contrats, utilitaires sans métier - -sim-bridge/ - Api/ - Application/ - Domain/ - Infrastructure/ - SimConnect/ - Contracts/ - -supabase/ - functions/ - migrations/ - tests/ - -docs/ - decisions/ - tickets/ - templates/ -``` - -Cette organisation appartient au nouveau dépôt. Aucun code n'y est déplacé -depuis le dépôt actuel sans ticket de caractérisation et preuve de qualité, -sécurité, provenance et licence. La réécriture reste pilotée par gates et tests -de caractérisation malgré l'absence de migration de code. - -## Données - -- UUID pour les identités distribuées. -- Horodatages UTC ; temps simulé explicitement distinct du temps réel. -- Grand livre financier append-only. -- Version ou jeton de concurrence sur ressources sensibles. -- `operation_id` unique pour chaque commande rejouable. -- Suppression logique pour données auditables ; rétention documentée. -- Types Supabase générés et dérive détectée en CI. - -## Contrats - -- Payloads REST, SignalR et RPC versionnés. -- Validation aux deux extrémités. -- Changements compatibles ou migration coordonnée. -- Événements de domaine sans types SimConnect natifs. - -## Résilience - -- Timeout et annulation sur toute I/O. -- Retry uniquement pour opérations idempotentes, avec backoff et jitter. -- File locale/outbox pour les commandes critiques interrompues. -- États visibles : local, pending, synchronized, rejected. -- Aucun `last write wins` pour argent, propriété ou affectations. - -## Décisions encore ouvertes - -- HTTP localhost renforcé ou named pipe Windows pour le bridge. -- Cache SQLite local et données autorisées hors ligne. -- Stratégie d'anti-triche proportionnée. -- Fournisseur de crash reporting et consentement. - -## Décisions acceptées - -- `ADR-0001` : MVP solo connecté, un propriétaire unique et au plus une - compagnie par utilisateur ; modèle préparé pour une collaboration ultérieure, - sans membre ni rôle collaboratif dans le MVP. -- `ADR-0002` : réécriture totale isolée dans un nouveau dépôt ; ancien dépôt - conservé comme référence en lecture seule, nouveau schéma sans migration des - données de développement et une seule bascule publique après parité. -- `ADR-0003` : Windows 11 x64 maintenu et MSFS 2024 stable uniquement ; Store et - Steam sont validés séparément, sans support Windows 10, ARM64, MSFS 2020, - Insider ou Preview. WebView2 Evergreen et bridge .NET self-contained. -- `ADR-0004` : Node 24/pnpm 11/React 19/Tauri 2.11, bridge .NET 10 LTS avec SDK - SimConnect officiel abstrait et backend Supabase/PostgreSQL 17. Les versions - exactes et la politique mensuelle sont dans `docs/STACK.md`. +# Architecture + +The shape of the system, and **where trust changes**. + +Cap: **150 lines**. Read the `Boundaries` table when opening a unit; read the +rest only when the open milestone changes what it describes. + +Every row carries the file it was read from. A row with no evidence is a belief. + +--- + +## Boundaries + +**This table is the spine of the repository.** Each row here is a section of +`docs/RULES.md` and a row of `docs/VALIDATION.md`. + +| Boundary | Trust changes how | Enforced in | Read from | +| --- | --- | --- | --- | +| Data and persistence | an authenticated identity becomes durable rows filtered by RLS | `supabase/migrations/` | `20260721190000_security_hardening.sql` | +| Authoritative service | a client-reported flight becomes an irreversible ledger write | `supabase/functions/`, SQL RPCs | `supabase/functions/complete-flight/index.ts` | +| Bridge local API | a loopback HTTP caller becomes the holder of a live user JWT | `sim-bridge/Program.cs` | `sim-bridge/Program.cs:116` | +| SimConnect and MSFS | simulator telemetry becomes domain flight events | `sim-bridge/SimConnect/` | `sim-bridge/SimConnect/ISimClient.cs` | +| Third-party egress | an outside response becomes a domain object | `app/src/lib/simbrief.ts`, `sim-bridge/Program.cs` | `app/src/lib/simbrief.ts:3`, `sim-bridge/Program.cs:188` | +| Desktop shell | web content becomes operating-system reach | `app/src-tauri/` | `app/src-tauri/capabilities/default.json` | +| Distribution | source becomes a signed artifact on a user machine | `scripts/build-sidecar.ps1` | `.github/workflows/ci.yml:66` | + +The React client is not a boundary. It is the untrusted side of four of them. + +## Components + +| Component | Responsibility | Depends on | Read from | +| --- | --- | --- | --- | +| `app/` | UI, client orchestration, session handling | Supabase, bridge, SimBrief | `app/package.json` | +| `app/src-tauri/` | desktop shell, WebView2 host, sidecar launch | `app/`, sidecar | `app/src-tauri/Cargo.toml` | +| `sim-bridge/` | SimConnect telemetry, flight detection, local REST/SignalR | MSFS, Supabase | `sim-bridge/Thrustline.Bridge.csproj` | +| `supabase/` | schema, RLS, RPCs, edge function | — | `supabase/config.toml` | +| `legacy/` | read-only Electron + WPF reference | — | `legacy/package.json` | + +## What is authoritative over what + +**The server is authoritative for money, ownership, reputation, progression and +every sensitive transition.** The desktop app, the sidecar and MSFS are all +untrusted clients — a modifiable client on a user's machine can send anything. + +Never trusted from a client: money amounts, ownership claims, role claims, the +current time, flight outcomes, or any identity supplied in a request body. +Caller identity is derived from the JWT alone. `scripts/security-check.ps1` +enforces two consequences of this mechanically: no `p_now` in `app/src` (a +client-controlled server clock) and no service-role credential in the sidecar. + +The sidecar is trusted with *less* than the app: it holds a user access token in +memory (`sim-bridge/Session/SessionStore.cs`) but is never given a privileged +key. + +## Data flow of the main path + +1. User authenticates against Supabase Auth — crosses **Data and persistence**. +2. App posts `accessToken` + `supabaseUrl` + `anonKey` to `POST /session` on + `127.0.0.1:5055`; the bridge verifies the token against `/auth/v1/user` + before storing a session — crosses **Bridge local API**. +3. Bridge subscribes to MSFS via `NativeSimConnectClient` and detects phases — + crosses **SimConnect and MSFS**. +4. Telemetry is pushed to the app over SignalR `/hubs/sim` — crosses + **Bridge local API**. +5. Flight completion is submitted to the `complete-flight` edge function, which + computes and writes the ledger — crosses **Authoritative service**. +6. Rows are read back under RLS — crosses **Data and persistence**. + +## State that outlives a request + +| Store | Written by | Read by | If lost | +| --- | --- | --- | --- | +| Supabase PostgreSQL | edge function, RPCs | app under RLS | company, fleet, finances and history are gone; no local copy exists | +| Bridge session (in memory) | `POST /session` | bridge services | user re-authenticates; no data loss | +| WebView2 local storage | `@supabase/supabase-js` (`persistSession: true`) | app | user signs in again | +| Build artifacts | `scripts/build-sidecar.ps1` | installer | rebuild from source | + +## Deliberate architectural constraints + +A unit may not quietly undo these. Changing one requires a superseding ADR. + +- Solo ownership: one account owns at most one company and is its only human; + `owner` is the only role — ADR-0001. +- Windows 11 x64 + MSFS 2024 only; no Windows 10, ARM64, MSFS 2020, or + Xbox/Cloud — ADR-0003. +- Tauri + WebView2, not Electron; Supabase/PostgreSQL, not Firestore; + SimConnect behind an internal replayable abstraction — ADR-0004. +- `legacy/` is read-only until explicitly archived — `AGENTS.md`. + +## Known structural debt + +Where the real structure disagrees with the documents above. + +- **ADR-0002 is contradicted by reality and by the owner's current decision.** + It mandates a full rewrite in a new repository with fresh history; this is the + original repository with `legacy/` carved out in place by `fc3316d`, and the + owner has since chosen to harden the existing code rather than rewrite it. + Superseded by ADR-0005 (proposed) — `KI-016`. +- **ADR-0004 pins a stack the repository does not have**: .NET 10, pnpm 11, + TypeScript 6, React 19, Tauri 2.11, `rust-toolchain.toml`. Actual: `net8.0`, + npm, TypeScript 5.9, React 18.3, no `rust-toolchain.toml` — `KI-017`. +- **No .NET test project exists**, so `dotnet test` discovers nothing and exits + `0` — `KI-004`. +- **No automated RLS isolation suite** covering owner A / owner B / anonymous — + `KI-002`. +- The bridge weather routes use a static `new HttpClient()` + (`sim-bridge/Program.cs:178`) while `IHttpClientFactory` is registered at + line 57 and used correctly at line 119 — `KI-018`. diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md index 9b4705c..0f7953a 100644 --- a/docs/CURRENT_STATE.md +++ b/docs/CURRENT_STATE.md @@ -1,213 +1,102 @@ -# État actuel du dépôt - -Dernière revue documentaire : 24 juillet 2026 (ticket T0003). -Statut : baseline locale vérifiée ; validations externes encore requises. - -## Produit - -La version existante est une alpha active de gestion de compagnie aérienne -virtuelle. Le routeur et la navigation exposent les parcours suivants : - -- authentification et création initiale d'une compagnie ; -- tableau de bord, compagnie, flotte et marchés d'avions ; -- routes, dispatch/SimBrief, vols et suivi de vol en direct ; -- horaires et opérations passives ; -- équipage, finances, paramètres, EFB et succès. - -Le code contient aussi des services de maintenance, expérience passager, ACARS, -météo, événements de jeu et calculs d'atterrissage. T0001 prouve leur présence -dans le dépôt, pas leur fonctionnement de bout en bout. Il n'existe pas encore de -version publique stable. - -## Stack active et versions observées - -Baseline exécutée sous Windows le 24 juillet 2026 : - -- Node.js `24.14.1` et npm `11.11.0` ; -- SDK .NET `10.0.201`, projet bridge ciblant `net8.0` ; -- `rustc 1.94.1` et `cargo 1.94.1` ; -- Supabase CLI non installée/non trouvée ; -- Tauri v2 / Rust, React 18 / TypeScript / Vite / Tailwind ; -- ASP.NET Core et SimConnect.NET pour le bridge ; -- REST et SignalR sur loopback entre UI et bridge ; -- Supabase Auth/PostgreSQL/RLS/Realtime/Edge Functions. - -Le moteur Node déclaré par `app/package.json` est `>=24.18.0 <25`. La machine de -baseline est donc en dessous de la version minimale, même si les tests et le -build frontend réussissent. Les workflows CI utilisent Node `24.18.0`, .NET 8.x -et Rust stable. - -## Inventaire reproductible - -- Lockfiles : `app/package-lock.json` et `app/src-tauri/Cargo.lock`. -- Scripts npm : `dev`, `build`, `test`, `test:watch`, `test:coverage`, `preview`, - `tauri`, `tauri:dev`, `tauri:build`, `sidecar:build`. -- Scripts dépôt : `scripts/build-sidecar.ps1` et - `scripts/security-check.ps1`. -- Workflows : `.github/workflows/ci.yml` et - `.github/workflows/security.yml`. -- Migrations Supabase append-only constatées : 25. -- Dépendances directes : 11 npm runtime, 12 npm développement, 2 NuGet, - 6 Cargo runtime et 1 Cargo build. -- Variables/configurations relevées par nom seulement : - `VITE_SUPABASE_URL`, `VITE_SUPABASE_ANON_KEY`, `VITE_SIM_BRIDGE_URL`, - `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` et - `THRUSTLINE_BRIDGE_TOKEN`. - -## Procédure vérifiée depuis un clone propre - -Installer Windows, Node `24.18.x`, npm, le SDK .NET 8, Rust stable et les -prérequis Tauri v2, puis exécuter depuis la racine : - -```powershell -Set-Location app -npm ci -npm test -npm run build - -Set-Location ..\sim-bridge -dotnet restore -dotnet build --configuration Release -dotnet test --configuration Release - -Set-Location ..\app\src-tauri -cargo check --locked +# Current state + +**The proven present, never the intent.** A capability enters here only once +merged into the main branch, with the evidence that establishes it. The future +belongs to the roadmap, defects to `KNOWN_ISSUES.md`. + +Cap: **200 lines**, enforced by a check. Update at the close of any unit that +changes the proven reality. An entry that is no longer true is **replaced**, not +appended. + +--- + +## Current milestone: hardening the working application + +The application already does what it is meant to do; the owner's instruction is +to keep every feature and improve the code underneath it — best practices, +smaller and more readable files, reliability, security, and dependencies that +are not years behind. Reached when the code has been hardened boundary by +boundary without a single user-visible feature being lost. `PacoRCS` decides +that it is reached. + +This replaces the full-rewrite direction of ADR-0002. That reversal needs +ADR-0005 before any unit relies on it. + +## Capabilities shipped in `main` + +`` + +**Nothing user-facing is currently proven, and that is the honest state, not an +oversight.** The owner says the application works; the repository cannot yet +demonstrate it. The only automated coverage is 11 tests across four pure-logic +files (`boarding`, `geo`, `scheduleGenerator`, `units`) — no UI test, no +integration test, no bridge test, no RLS test, no end-to-end path. + +| Capability | Evidence | Since | +| --- | --- | --- | +| Frontend logic suite passes | `cd app; npm test` → 4 files, 11 tests, run 2026-08-12 | 2026-08-12 | + +Filling this table is the first real question the owner still owes the +repository. Until then, no unit may assume a working baseline it has not itself +established. + +## Units in progress + +None. POSEIDON was bootstrapped on 2026-08-12; no unit has been opened yet. + +| Unit | Open milestone | Branch | Blocked by | +| --- | --- | --- | --- | +| — | — | — | — | + +## Missing for the current milestone + +1. **ADR-0005** superseding ADR-0002 — the rewrite decision is reversed but not + recorded. Everything below inherits from it. +2. **The capability inventory** — which features are real, and how each is + demonstrated. Without it "keep every feature" has no referent. +3. **A version reset** to `0.0.1-alpha` across the four manifests, and a + decision on which one is the single source. +4. **A dependency upgrade pass** — 19 direct dependencies are outdated, 8 by a + major version. +5. **A .NET test project**, so `dotnet test` stops exiting `0` on nothing. +6. **An RLS isolation suite** for owner A / owner B / anonymous. +7. **python3**, to take the method checks out of agent mode. + +## Outside the milestone, tracked elsewhere + +Nineteen entries in `docs/KNOWN_ISSUES.md`, including the three contradictions +between the accepted ADRs and reality (`KI-016`, `KI-017`) and the absent +release pipeline (`KI-003`). Historical tickets T0001–T0005 and the superseded +documentation set are in `docs/archive/`; they are context, never law. + +## Steering + +- **Keep the application, harden the code — `PacoRCS`, 2026-08-12.** No rewrite. + Every existing feature is preserved; the work is best practices, smaller + files, reliability and security. Reverses ADR-0002; pending ADR-0005. +- **Reliability and security win arbitrations — `PacoRCS`, 2026-08-12.** Where + one option is more reliable and more secure, take it, including for runtime + and dependency versions. +- **Semver from `0.0.1-alpha` — `PacoRCS`, 2026-08-12.** The four manifests + currently read `0.2.0` independently; the single source is undecided. +- **POSEIDON adopted, prior docs archived — `PacoRCS`, 2026-08-12.** The four + ADRs stay live in `docs/decisions/`; tickets, roadmap and the superseded + document set moved to `docs/archive/` with history preserved. +- **`checks: agent mode` — bootstrap, 2026-08-12.** `python3` is absent, so + `poseidon/checks/check.py` runs nowhere, including CI. Method-document caps, + boundary-to-section correspondence and placeholder detection are unverified. + Every report must say so. +- **A new repository is under consideration — `PacoRCS`, 2026-08-12.** Possibly + a fresh start seeded from this codebase. Undecided; `docs/`, `AGENTS.md` and + `poseidon/` travel intact if it happens. + +## Reproduce and verify -Set-Location ..\.. -powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\security-check.ps1 +``` +cd app +npm test ``` -Configurer uniquement après restauration les variables nécessaires dans un -fichier local non versionné. L'exécution de l'application et les validations -cloud nécessitent une instance Supabase configurée. Le suivi réel nécessite -MSFS/SimConnect. Le packaging signé nécessite un certificat approprié. - -## Validation exécutée le 24 juillet 2026 - -- `npm ci` : réussi depuis le lockfile ; avertissement `EBADENGINE` à cause de - Node `24.14.1` ; audit npm signalant 2 vulnérabilités modérées. -- `npm test` : 4 fichiers, 11 tests réussis. -- `npm run build` : TypeScript/Vite réussi. -- `dotnet restore` : réussi. -- `dotnet build --configuration Release` : réussi, 0 avertissement, 0 erreur. -- `dotnet test --configuration Release` : code 0, mais aucun projet de tests ni - test exécuté. -- `cargo check --locked` : réussi ; avertissement d'environnement sur la - canonicalisation de `C:\Users\andyd`. -- `scripts/security-check.ps1` : invariants réussis lorsque PowerShell est lancé - avec `-ExecutionPolicy Bypass`. - -Les premiers essais de `npm ci`, `dotnet restore` et `dotnet test` ont échoué -par refus d'accès du bac à sable aux caches/configurations utilisateur, puis ont -réussi avec cet accès autorisé. Le premier lancement direct du script de sécurité -a été bloqué par la politique PowerShell locale, puis a réussi avec le bypass -explicite. Ces échecs sont classés comme environnement, pas comme défauts du code. - -## Contrôles non exécutables dans cette baseline - -- Connexion réelle à MSFS/SimConnect, replay de trace et parcours de vol : MSFS - absent et aucun replay automatisé fourni. -- Démarrage/reset Supabase local, application des migrations et tests RLS entre - deux utilisateurs : Supabase CLI absente et aucun environnement de test fourni. -- Déploiement de l'Edge Function et validation cloud : projet/identifiants - Supabase absents. -- Build installable signé, installation, mise à jour et rollback : aucun - certificat de signature ni pipeline de release complet fourni. -- Validation manuelle complète de l'interface : services externes requis absents. - -## Sécurité déjà présente - -- Bridge lié à localhost. -- Jeton aléatoire d'instance entre Tauri et bridge. -- CSP Tauri et capacités limitées. -- Clôture de vol privilégiée déplacée vers une Edge Function/RPC. -- Migrations de durcissement RLS/grants. -- CI avec audits de dépendances, scan de secrets et invariants. - -## Dette et risques structurants - -- Aucun projet de tests .NET dédié constaté. -- Aucun test RLS automatisé livré constaté. -- Le build Tauri complet dépend du sidecar généré dans `externalBin`. -- L'environnement local Node ne satisfait pas le moteur déclaré. -- Les README divergent (`Node 24.18 LTS` contre `Node 20+`) et utilisent - `npm install` au lieu de la restauration déterministe `npm ci`. -- Deux vulnérabilités npm modérées sont signalées ; aucune mise à jour n'a été - faite dans T0001. -- Pages React volumineuses et mélange UI/orchestration/données. -- Mutations métier directes depuis le client et séquences multi-écritures non - atomiques. -- Pas de pipeline complet de release signée/updater. -- Versions répétées dans plusieurs manifestes. -- Politique de suppression/récupération du propriétaire et durée de rétention à - définir avant toute suppression irréversible. - -## Travail local à préserver - -La modification utilisateur de `app/src-tauri/Cargo.toml` est restée intacte -pendant T0001. Son empreinte SHA-256 avant et après validation est -`94BF4E46145BC363B70BD3EEE5BBA578EF198718EBDADB7264C260FEAFAE2AFE`. -Les autres modifications utilisateur préexistantes n'ont pas été intégrées au -ticket. - -## Décision produit - -`ADR-0001` retient un MVP solo connecté préparé pour une collaboration -ultérieure : au plus une compagnie par utilisateur, un propriétaire humain -unique, aucun membre ni rôle collaboratif dans le MVP. La collaboration probable -après le MVP exigera une nouvelle ADR. - -## Décision de refonte - -`ADR-0002` retient une réécriture totale isolée dans un nouveau dépôt avec un -historique Git neuf. Le dépôt actuel devient une référence en lecture seule pour -l'UX, les comportements et la documentation jusqu'à parité du golden path, puis -il sera archivé. Le nouveau produit utilise un schéma Supabase neuf : les données -actuelles sont uniquement des données de développement et ne seront pas migrées. -Il n'y aura ni coexistence en production, ni double écriture, ni ancien client -connecté au nouveau backend. Une seule bascule publique est prévue après les -gates de caractérisation, SimConnect, parité, restauration et distribution. - -## Décision de support - -`ADR-0003` retient Windows 11 x64, sur une version publique encore maintenue par -Microsoft, et MSFS 2024 stable uniquement. Microsoft Store/Xbox App et Steam sont -deux combinaisons distinctes qui restent `Unsupported — validation requise` -jusqu'à une fiche de test réelle par canal. Windows 10, ARM64, MSFS 2020, -Windows Insider et les builds MSFS Beta/Preview sont `Unsupported`. - -Une machine Ryzen 7 5800X, 32 Go et RX 6070 XT est disponible comme profil -recommandé de validation. Elle ne prouve pas le minimum matériel. Aucun test -MSFS réel n'a été exécuté dans T0004. - -## Décision de stack cible - -`ADR-0004` retient Node 24 LTS avec pnpm 11, React 19/Vite 8, Tauri 2.11 sur -WebView2 Evergreen, Rust stable épinglé, un bridge .NET 10 LTS self-contained -`win-x64`, le SDK SimConnect officiel derrière une abstraction interne, et -Supabase/PostgreSQL 17. Firebase, Electron et le wrapper `SimConnect.NET` ne sont -pas les fondations de la refonte. - -Cette décision est documentaire : aucune version n'a été installée et aucun -manifest, lockfile, workflow ou code applicatif n'a été modifié. Les performances -et la compatibilité self-contained SimConnect restent à prouver par les tickets -d'adoption. - -## Prochain ticket recommandé - -`T0006 — Épingler les runtimes et créer la source de versions`. Il doit créer -dans le nouveau dépôt les pins Node/pnpm/Rust/.NET/PowerShell, le bootstrap et -les contrôles de dérive, sans introduire encore le shell ou le frontend. Les -budgets du profil minimum/recommandé sont renumérotés `T0015`. - -## Mise à jour de ce fichier - -Après chaque ticket terminé, modifier uniquement : - -- capacités réellement disponibles ; -- structure ou dépendances actives ; -- validation réellement exécutée ; -- dette ajoutée/résolue ; -- prochain ticket recommandé. - -Ne pas y copier l'historique Git ni les projets futurs. +Expect `4 passed (4)` files and `11 passed (11)` tests. That is the only claim +on this page that has been executed. Everything else in `docs/VALIDATION.md` is +`not run` or `blocked by environment` as of 2026-08-12. diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index db7bb83..e3725ef 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -1,39 +1,66 @@ -# Problèmes connus et follow-ups +# Known issues and follow-ups -Ce registre contient uniquement les découvertes réelles hors périmètre d'un -ticket. La roadmap contient le travail planifié. +Register of **real discoveries outside the scope** of a unit of work. Planned +work belongs to the roadmap; accepted invariants to `RULES.md`; tooling traps to +`LEARNINGS.md`. + +A discovery enters at the moment it is made, with its evidence, and is **not +fixed in passing**. That is the rule protecting a unit's scope: without the +register, every discovery becomes either scope drift or an omission. ## Format -| ID | Sévérité | Zone | Résumé | Preuve | Ticket cible | Statut | -| --- | --- | --- | --- | --- | --- | --- | +- **Severity**: `Critical`, `High`, `Medium`, `Low`. Closed list. +- **Status**: `Open`, `Accepted`, `Scheduled`, `Resolved`, `Invalid`. Closed list. +- **Evidence**: what establishes the defect — command, output, file and line. + Mandatory to reach `Resolved`. A resolved entry with no evidence is a false + success, not a success. +- **Target**: the unit that will handle it, or the roadmap phase. + +A `Critical` or `High` found when opening a unit may block it. The coordinator +says so explicitly rather than ignoring it. + +## What does not belong here + +- A future capability — that is roadmap. +- An accepted, bounded risk — that is an exception in `RULES.md`, with a deadline. +- Evidence missing for lack of an environment — that is a report limit, not a + defect. +- An intuition with no reproducible evidence. + +A candidate is not a known issue by default. It joins this register only if it +describes a **real** defect, out of scope, worth tracking. + +## Remediation + +Fixing an entry goes through a separate unit, whose acceptance includes a +relevant negative test and an explicit residual risk. Closing an entry without a +negative test is preparing to reopen it. -Sévérité : `Critical`, `High`, `Medium`, `Low`. -Statut : `Open`, `Accepted`, `Scheduled`, `Resolved`, `Invalid`. +## Entries -## Entrées initiales +KI-001 to KI-015 were carried over from the pre-POSEIDON register on +2026-08-12; their wording is condensed, their evidence unchanged. KI-016 to +KI-019 were found during the bootstrap audit. -| ID | Sévérité | Zone | Résumé | Preuve | Ticket cible | Statut | +| ID | Severity | Area | Summary | Evidence | Target | Status | | --- | --- | --- | --- | --- | --- | --- | -| KI-001 | High | Data | Certaines mutations métier sont encore effectuées directement par le client. | Audit initial | Phase 2 | Open | -| KI-002 | High | Quality | Absence de suite automatisée RLS A/B/anonyme. | Audit initial | Phase 1–2 | Open | -| KI-003 | High | Release | Pas de pipeline complet d'artefacts/updater signés. | Audit initial | Phase 6 | Open | -| KI-004 | Medium | Bridge | Pas de projet de tests .NET/replay SimConnect constaté. | Audit initial | Phase 3 | Open | -| KI-005 | Medium | Frontend | Plusieurs pages mélangent UI, règles et accès aux données. | Audit initial | Phase 4 | Open | -| KI-006 | Medium | Docs | Documentation historique partiellement désynchronisée. | Audit initial | T0001 | Open | -| KI-007 | Medium | Product | Mode solo ou VA collaborative non tranché. | ADR-0001 : MVP solo préparé pour collaboration ultérieure | T0002 | Resolved | -| KI-008 | High | Rebuild | Une réécriture totale peut omettre des comportements actuels non caractérisés. | ADR-0002 : couverture automatisée faible face au périmètre existant | Caractérisation du golden path | Open | -| KI-009 | High | Bridge | Aucun corpus de traces SimConnect rejouables n'est fourni pour prouver la parité du moteur de vol. | T0001 et ADR-0002 | Premier vertical slice SimConnect | Open | -| KI-010 | High | Release | Après création de données réelles dans le nouveau schéma, aucun retour vers l'ancien produit ne sera possible. | ADR-0002 : nouveau schéma sans compatibilité descendante | Phase 6 | Accepted | -| KI-011 | High | Support | Aucune preuve réelle distincte ne valide encore MSFS 2024 Microsoft Store/Xbox App et Steam ; une seule machine de test est disponible. | T0004 et proposition ADR-0003 | Validation plateformes / premier vertical slice SimConnect | Open | -| KI-012 | Medium | Support | Le profil matériel minimum Thrustline n'est pas mesuré ; la machine Ryzen 7 5800X, 32 Go, RX 6070 XT ne prouve que le profil recommandé cible. | Réponses Andy et ADR-0003 | T0015 | Open | -| KI-013 | Medium | Desktop | Le gain réel de Tauri/WebView2 face à un shell .NET natif n'est pas mesuré sur le profil cible. | ADR-0004 : choix fondé sur l'architecture et l'absence de runtime Chromium embarqué | T0007 puis T0015 | Open | -| KI-014 | Medium | Backend | La stack Supabase locale n'est pas strictement identique au cloud et PostgreSQL 17 doit être confirmé sur chaque projet dev/staging/prod. | Documentation officielle Supabase consultée dans T0005 | T0012 | Open | -| KI-015 | High | Bridge | Le SDK managed SimConnect officiel documente .NET Framework et dépend de binaires/installation SDK ; sa publication self-contained .NET 10 reste à prouver. | Documentation MSFS 2024 SimConnect consultée dans T0005 | T0011 | Open | - -## Règles - -- Ajouter une preuve reproductible. -- Lier à un ticket lorsqu'il devient planifié. -- Ne pas résoudre discrètement un problème hors scope. -- Retirer une entrée uniquement si son historique reste traçable dans un ticket. +| KI-001 | High | Data | Some business mutations are still performed directly by the client. | Initial audit | hardening | Open | +| KI-002 | High | Quality | No automated RLS suite for owner A / owner B / anonymous. | Initial audit | hardening | Open | +| KI-003 | High | Distribution | No complete signed-artifact and updater pipeline. | Initial audit | distribution | Open | +| KI-004 | Medium | Bridge | No .NET test project: `cd sim-bridge; dotnet test` discovers 0 tests and exits `0`. | No `.csproj` references xunit/NUnit/MSTest, verified 2026-08-12 | hardening | Open | +| KI-005 | Medium | Client | Several React pages mix UI, business rules and data access. | Initial audit | hardening | Open | +| KI-006 | Medium | Docs | Historical documentation partially out of sync. | Initial audit | bootstrap | Resolved | +| KI-007 | Medium | Product | Solo vs collaborative VA undecided. | ADR-0001: solo MVP prepared for later collaboration | T0002 | Resolved | +| KI-008 | High | Rebuild | A total rewrite could drop uncharacterised current behaviour. | ADR-0002: weak automated coverage against the existing scope | ADR-0005 | Open | +| KI-009 | High | Bridge | No replayable SimConnect trace corpus exists to prove flight-engine parity. | T0001 and ADR-0002 | SimConnect slice | Open | +| KI-010 | High | Data | Once real data exists in the new schema, no return to the old product is possible. | ADR-0002: no backward compatibility | distribution | Accepted | +| KI-011 | High | Support | MSFS 2024 Store and Steam channels each need their own proof; only one test machine is available. | T0004 and ADR-0003 | platform validation | Open | +| KI-012 | Medium | Support | The minimum hardware profile is not measured; the available machine only proves the recommended profile. | ADR-0003 | later | Open | +| KI-013 | Medium | Desktop | Tauri/WebView2's real gain over a native .NET shell is unmeasured on the target profile. | ADR-0004 | later | Open | +| KI-014 | Medium | Data | Local Supabase is not identical to cloud; PostgreSQL 17 must be confirmed per project. | Supabase docs consulted in T0005 | later | Open | +| KI-015 | High | Bridge | The official managed SimConnect SDK documents .NET Framework and depends on an SDK install; self-contained publication is unproven. | MSFS 2024 SimConnect docs consulted in T0005 | SimConnect slice | Open | +| KI-016 | High | Architecture | ADR-0002 mandates a rewrite in a new repository with fresh history; this is the original repository with `legacy/` carved out in place, and the owner has reversed the rewrite decision. | `git log --diff-filter=A -- legacy` → `fc3316d`; owner, 2026-08-12 | ADR-0005 | Open | +| KI-017 | High | Supply chain | ADR-0004 pins .NET 10, pnpm 11, TypeScript 6, React 19, Tauri 2.11 and a `rust-toolchain.toml`. Actual: `net8.0`, npm, TS 5.9.3, React 18.3.1, no `rust-toolchain.toml`. .NET 8 support ends November 2026. | `Thrustline.Bridge.csproj`; `npm outdated` 2026-08-12; `ls rust-toolchain.toml` → absent | ADR-0005 then upgrade unit | Open | +| KI-018 | Medium | Bridge | The weather routes build a static `new HttpClient()` although `IHttpClientFactory` is registered and used correctly elsewhere — no DNS refresh, risk of socket exhaustion. | `sim-bridge/Program.cs:178` vs `:57` and `:119` | hardening | Open | +| KI-019 | Medium | Data | No database reset or seed command exists: recreating a schema is entirely manual, so a failed run is not diagnosable. | No Supabase CLI task, npm script or seed in the repo, verified 2026-08-12 | hardening | Open | diff --git a/docs/LEARNINGS.md b/docs/LEARNINGS.md new file mode 100644 index 0000000..bdb93d2 --- /dev/null +++ b/docs/LEARNINGS.md @@ -0,0 +1,105 @@ +# Operational learnings + +A repository's learning is about **observable facts**, never an agent's implicit +memory. This register exists so a difficulty paid once is not paid twice, and so +an instruction does not outlive its cause. + +## States + +| State | Definition | +| --- | --- | +| `Observed` | Seen once, in a single context. | +| `Reproduced` | Reconstructed in a second independent context, or deterministically. | +| `Codified` | Written at its canonical destination. | +| `Enforced` | Applied by an automated check. This is the goal. | +| `Stale` | No longer applies — tool, version, or context changed. Kept, never erased. | + +## Evidence and promotion + +- **Capture** a first occurrence immediately: symptom, context, the wrong + conclusion to avoid, the non-destructive diagnostics run. +- **Promotion** requires two independent occurrences or one deterministic + reproduction. One reproducible occurrence suffices where there is high risk of + a security failure, data loss, or a false success — after explicit review. +- A counter-example beats a third occurrence: what bounds a rule is worth more + than what repeats it. +- A promotion never justifies exceeding a unit's `Allowed areas`. + +## Canonical destination + +Choose the **narrowest** that works. Writing higher than necessary is the most +common way documentation becomes unreadable. + +| Nature | Destination | +| --- | --- | +| One-off fact about a unit | its Completion Report | +| Real defect out of scope | `KNOWN_ISSUES.md` | +| Recurring tooling trap | `RULES.md`, "defects already encountered" | +| Invariant of a boundary | `RULES.md`, that boundary's section | +| Command or execution order | `VALIDATION.md` | +| Constraint of a code zone | `/AGENTS.md` | +| Global project invariant | `AGENTS.md` — replacing an existing rule | + +As soon as a deterministic check is possible, open a bounded unit for the script +or test **instead of** keeping a manual instruction. A manual instruction is a +debt, not an asset. + +## Candidate template + +### LC-YYYY-NNN — + +- **State**: `Observed` +- **Symptom**: <what was observed, literally> +- **Context**: <tool, version, system, command> +- **Wrong conclusion to avoid**: <what we believed that was false> +- **Diagnostics run**: <non-destructive commands and their output> +- **Occurrences**: <unit, date>; <unit, date> +- **Proposed destination**: <per the table above> +- **Revalidate on**: <date or condition — version or tool change> + +## Register + +| ID | Title | State | Destination | Revalidate | +| --- | --- | --- | --- | --- | +| LC-2026-001 | `dotnet test` exits `0` having discovered nothing | Codified | `RULES.md` L1 | when a .NET test project exists (`KI-004`) | +| LC-2026-002 | An install script extracted at the repo root deleted the documentation set | Codified | `RULES.md` L2 | never — the script is gone | + +### LC-2026-001 — `dotnet test` reports success on an empty discovery + +- **State**: `Codified` +- **Symptom**: `cd sim-bridge; dotnet test` exits `0`. Read as a passing suite. +- **Context**: `Thrustline.Bridge.csproj`, .NET 8, 2026-08-12. +- **Wrong conclusion to avoid**: that the bridge has passing tests. It has none: + no `.csproj` in the repository references xunit, NUnit or MSTest. +- **Diagnostics run**: `grep -rl 'xunit\|NUnit\|MSTest' --include=*.csproj .` + → no match outside `legacy/`. +- **Occurrences**: bootstrap, 2026-08-12. Deterministic — one occurrence + suffices, since the failure mode is a false success. +- **Revalidate on**: creation of a .NET test project, which turns this into a + real baseline in `docs/validation-baselines.json`. + +### LC-2026-002 — An unreviewed install script deleted the documentation set + +- **State**: `Codified` +- **Symptom**: after running `install.sh` at the repository root, 25 tracked + documents were deleted from the worktree — the roadmap, four ADRs, five + tickets and three templates — and a stale duplicate of the method kit was left + at the root as `checks/ commands/ examples/ repo/ skill/`. +- **Context**: POSEIDON 1.0.0 install, 2026-08-12. +- **Wrong conclusion to avoid**: that the spilled directories were duplicates of + `poseidon/` and safe to delete without checking. They were an **older** + packaging: their `locate.md` referenced a `method` skill, and their + `check.py` was 3.8 KB smaller. +- **Diagnostics run**: `git status --porcelain=v1`; `diff -rq checks poseidon/checks`; + `unzip -l docs/poseidon-dist/poseidon.zip`. All deletions were uncommitted and + recovered with `git restore --source=HEAD`. +- **Occurrences**: bootstrap, 2026-08-12. Deterministic, and the risk is data + loss — one occurrence suffices. +- **Revalidate on**: never. Extract an archive to a scratch directory and copy + in deliberately; never run an unreviewed installer inside the worktree. + +## Revalidation and retirement + +At the deadline, or on a version change: confirm, replace, or mark `Stale` — +without erasing history. An `Enforced` entry whose check has been removed drops +back to `Codified`: that is a regression of guarantee, and it must be visible. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 837b9c8..90b7577 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -1,97 +1,98 @@ -# Vision produit — Thrustline Rebuild - -Statut : Draft à valider avant la phase 1. - -## Vision - -Thrustline transforme les vols MSFS en gestion cohérente d'une compagnie -aérienne virtuelle : planifier, exploiter, suivre, analyser et développer une -compagnie sans perdre les données ni pouvoir falsifier facilement son économie. - -## Principes produit - -- Windows-first et MSFS-first. -- Fonctionnel même lorsque MSFS est fermé pour la partie gestion. -- Dégradation claire en cas de panne Supabase, SimBrief, météo ou SimConnect. -- Aucune perte silencieuse d'un vol ou d'une transaction. -- Les calculs importants sont explicables à l'utilisateur. -- Les mises à jour sont signées, récupérables et non destructrices. -- Vie privée par défaut ; télémétrie facultative et transparente. - -## Utilisateur principal - -Un passionné de simulation qui gère sa propre compagnie et effectue lui-même des -vols dans MSFS. - -## Modèle produit retenu - -Le MVP est **solo connecté, préparé pour une collaboration ultérieure**, -conformément à `ADR-0001`. - -- Un utilisateur possède au plus une compagnie. -- Une compagnie a exactement un propriétaire humain. -- Le propriétaire est le seul à voir et gérer les vols, finances, flotte et - horaires de sa compagnie. -- Aucun membre, rôle supplémentaire, invitation, partage ou transfert de - propriété n'est livré dans le MVP. -- L'identité de la compagnie reste distincte de celle du propriétaire afin de ne - pas bloquer une évolution future. - -La collaboration est probable après le MVP, mais elle nécessitera une nouvelle -ADR. Ne pas ajouter partiellement du multi-utilisateur. - -## Parcours essentiels - -1. Installer, lancer et créer/se connecter à un compte. -2. Créer une compagnie sans état partiel. -3. Acheter ou louer un avion. -4. Créer un dispatch et préparer le vol avec SimBrief. -5. Connecter MSFS et suivre les phases du vol. -6. Reprendre après une déconnexion ou un crash. -7. Finaliser le vol une seule fois. -8. Voir les impacts financiers, réputation et maintenance. -9. Planifier des opérations passives sans incohérence temporelle. -10. Mettre à jour ou désinstaller sans perdre les données. - -## MVP de la refonte - -- Authentification et compagnie. -- Flotte, maintenance et propriété. -- Routes, dispatch et SimBrief. -- SimConnect, phases de vol, ACARS résumé et rapport. -- Grand livre financier autoritaire. -- Réputation et progression minimales. -- Sauvegarde cloud, reprise et diagnostics. -- Installateur et mises à jour signés. - -## Hors MVP - -- Réseau social, marketplace communautaire et mods. -- Membres de compagnie, rôles collaboratifs, invitations et transfert de - propriété. -- Vols, finances, flotte ou horaires partagés entre plusieurs humains. -- Application mobile. -- Tableau de bord web public. -- Multi-VA complexe. -- Simulation économique exhaustive. -- Anti-triche présenté comme inviolable. - -## Contraintes de distribution - -- Windows 11 x64 uniquement, version publique encore maintenue par Microsoft. -- MSFS 2024 stable uniquement ; Microsoft Store/Xbox App et Steam sont deux - combinaisons distinctes à prouver avant toute déclaration `Supported`. -- Windows 10, ARM64, MSFS 2020, Windows Insider et MSFS Beta/Preview sont exclus - conformément à `ADR-0003`. -- Aucun privilège administrateur permanent. -- Données utilisateur séparées des fichiers d'installation. -- Politique de confidentialité, support, sécurité et licence avant bêta publique. - -## Mesures de réussite - -- Sessions sans crash ≥ objectif défini avant bêta. -- Zéro double clôture de vol. -- Zéro variation financière sans écriture de grand livre. -- Reprise après coupure testée sur chaque parcours essentiel. -- Temps de démarrage et consommation mémoire budgétés. -- Mise à jour N-1 → N et rollback validés sur VM propre. +# Product + +What this system is, for whom, and what it refuses to be. + +Cap: **120 lines**. Read when the open milestone changes what is described here — +and once, in full, at the first session of anyone new to the repo. + +**Nothing in this file can be derived from the code.** Every line was asked and +answered by `PacoRCS`, on 2026-08-12 unless another date is given, or carries the +ADR in which he answered it earlier. + +--- + +## One sentence + +Thrustline is a distributable Windows 11 desktop application for managing a +virtual airline in Microsoft Flight Simulator 2024, for a solo owner-operator. + +## Who uses it + +| User | What they are trying to do | What failure costs them | +| --- | --- | --- | +| Company owner | run one virtual airline — fleet, schedules, dispatch, finances — and fly its flights in MSFS 2024 | **data and money.** The ledger, fleet and flight history exist only server-side; there is no local copy and no export. A corrupted balance or a lost flight report is unrecoverable for them | + +The single owner is the only human on a company (ADR-0001). Their failure costs +data, so **every boundary they touch is `maximum evidence`** in `VALIDATION.md`. +That is the whole reason the depth column reads the way it does. + +## Priorities, in order + +1. **Stability and recovery after error** — a simulator session is long and + cannot be replayed. A crash that loses a flight in progress costs the user + hours they cannot get back, which no later correctness makes up for. +2. **Security of a distributed, modifiable client** — the app, the sidecar and + MSFS all run on the user's machine and can all be edited. Anything the client + is trusted with is effectively public. +3. **Economy and data integrity** — money, ownership and progression are the + product. A wrong balance is worse than a missing feature, and unlike a crash + it can go unnoticed for weeks. +4. **MSFS and SimConnect compatibility** — the simulator is a moving target the + project does not control; breaking with it makes the product useless even + when every test passes. +5. **Maintainability.** + +Given two equally correct options, the one serving the higher priority wins, +even if it costs more. Where an option is more reliable *and* more secure than +the alternative, take it — that is the owner's standing instruction, and it is +what settles version and dependency arbitrations. + +## What it is not + +Each line prevents a class of feature request and a class of accidental +architecture. + +- **Not a multi-pilot virtual airline.** No members, roles beyond `owner`, + invitations, membership requests, exclusion, suspension, or ownership + transfer. No user belongs to more than one company — ADR-0001. +- **Not a shared or real-time collaborative workspace.** No second human may + prepare or fly for a company — ADR-0001. +- **Not cross-platform.** Windows 11 x64 only: no Windows 10, no ARM64, no + Windows Server, no Wine or Proton — ADR-0003. +- **Not compatible with older or console simulators.** MSFS 2024 only: no MSFS + 2020, no Xbox, Cloud or PlayStation — SimConnect is not reachable there — + ADR-0003. +- **Not an Electron application.** No embedded Chromium or Node runtime — + ADR-0004. +- **Not a simulator modification.** It never patches MSFS, and never bypasses an + ACL, DRM or session boundary — ADR-0003. + +## Constraints that are not technical + +None recorded. Asked 2026-08-12: no regulation, contract, deadline or platform +policy constrains the work. Revisit before any public distribution — store +policy and code-signing obligations arrive with it. + +## What "working" looks like + +The owner can complete the full path, unattended, without losing state: +sign in → own one company → acquire and view a fleet → create a dispatch and +prepare the flight → connect to MSFS and have phases tracked deterministically → +survive a disconnection or crash mid-flight → get a report and close the flight +exactly once → see an authoritative ledger entry that matches it. + +Signals it has stopped working: a flight that cannot be closed, a balance that +disagrees with its flight history, a session that loses telemetry silently +rather than reporting the loss, or an update that resets user data. + +## Open product questions + +A question listed here is a legitimate reason for a unit to sit in `Draft`. + +| Question | Blocks | Asked on | +| --- | --- | --- | +| Which capabilities are genuinely shipped and merged, and by what evidence? Nothing user-facing is currently proven. | `CURRENT_STATE.md`, every unit that assumes a working baseline | 2026-08-12 | +| Does the hardening happen in this repository, or in a new one seeded from this codebase? The owner is undecided. | repository strategy, ADR-0005 | 2026-08-12 | +| Which of the four manifests is the single source of the version number? | the version reset to `0.0.1-alpha` | 2026-08-12 | +| What is the account-deletion and data-retention policy? No irreversible deletion may be implemented before it exists. | any account-deletion unit | ADR-0001, 2026-07-24 | +| Which MSFS 2024 channel — Microsoft Store or Steam — gets validated first? Only one machine is available, and a proof on one channel does not carry to the other. | `KI-011`, the SimConnect slice | ADR-0003, 2026-07-26 | diff --git a/docs/RULES.md b/docs/RULES.md new file mode 100644 index 0000000..0b8fa7e --- /dev/null +++ b/docs/RULES.md @@ -0,0 +1,129 @@ +# Invariants by boundary + +Canonical source of accepted invariants. Read **the section for the boundary you +touch**, never the whole file. + +Cap: **300 lines**. A new invariant replaces or condenses another. + +## Section index — jump straight to yours + +| You are touching | Section | +| --- | --- | +| schema, migrations, RLS | [Data and persistence](#data-and-persistence) | +| RPCs, edge functions, ledger | [Authoritative service](#authoritative-service) | +| `sim-bridge/Program.cs`, session, SignalR | [Bridge local API](#bridge-local-api) | +| `sim-bridge/SimConnect/`, telemetry | [SimConnect and MSFS](#simconnect-and-msfs) | +| SimBrief, weather, map tiles | [Third-party egress](#third-party-egress) | +| `app/src-tauri/`, CSP, capabilities | [Desktop shell](#desktop-shell) | +| packaging, installer, versions | [Distribution](#distribution) | +| dependencies, lockfiles, CI config | [CI and supply chain](#ci-and-supply-chain) | +| telemetry, personal data, logs | [Privacy](#privacy) | +| tooling that already bit us once | [Known traps](#rules-from-defects-already-encountered) | + +## How to write a line + +One line per invariant. Four columns: stable `#` never reused; the invariant as +a **refusal**; the `Check` that enforces it (`manual` is a debt, not a pass); and +`Proven by` as a **citation**, not a narrative. + +An invariant with no automated check that could have one is unfinished: open a +bounded unit for the script or test rather than keeping a manual instruction. + +An agent never creates an exception alone. Every exception is approved by +`PacoRCS`, bounded to one unit and one surface, and carries a residual risk and +a deadline. An exception with no deadline is an abandoned rule. + +--- + +## Data and persistence + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| D1 | Migrations are append-only; never edit or delete an applied migration. | `manual` | `AGENTS.md`, ADR-0002 | +| D2 | An account owns at most one company and is its only human; `owner` is the only role. | `manual` | ADR-0001 | +| D3 | Never expose a row across owners: every table reachable by a client carries an RLS policy keyed on the JWT identity. | `manual` | `20260721190000_security_hardening.sql` | +| D4 | Never merge a schema change without proving isolation for owner A, owner B and anonymous. | `manual` — no suite exists (`KI-002`) | ADR-0001 | +| D5 | Never widen a `SECURITY DEFINER` function without re-reading its search_path and its caller check. | `manual` | `20260721201000_require_auth_passive_rpc.sql` | + +## Authoritative service + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| S1 | Caller identity comes from the JWT, never from the request body. | `manual` | ADR-0001 | +| S2 | Never accept money, ownership, reputation, progression or a flight outcome as a client-supplied value; the server computes it. | `manual` | `AGENTS.md` | +| S3 | Never let the client supply the server clock. | `./scripts/security-check.ps1` | `security-check.ps1` (`p_now`) | +| S4 | Never return a privileged Supabase credential to the sidecar or the app. | `./scripts/security-check.ps1` | `security-check.ps1` (`ServiceRoleKey`) | +| S5 | Every sensitive mutation is transactional and idempotent; a retry never double-writes the ledger. | `manual` | `AGENTS.md`, ADR-0001 | + +## Bridge local API + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| B1 | Kestrel binds loopback only; never bind `0.0.0.0` or a routable interface. | `manual` | `sim-bridge/Program.cs:23` | +| B2 | Never allow any origin: CORS stays an explicit allow-list. | `./scripts/security-check.ps1` | `security-check.ps1` (`AllowAnyOrigin`) | +| B3 | Never store a session from an unverified token: `POST /session` verifies against `/auth/v1/user` first. | `manual` | `sim-bridge/Program.cs:116` | +| B4 | Never accept an arbitrary `supabaseUrl`: HTTPS and `.supabase.co`, or loopback. | `manual` | `sim-bridge/Program.cs:125` | +| B5 | Validate every input before any side effect, including route parameters. | `manual` | `sim-bridge/Program.cs:184` | + +## SimConnect and MSFS + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| M1 | The domain talks to `ISimClient`, never to a SimConnect type directly. | `manual` | ADR-0004, `sim-bridge/SimConnect/ISimClient.cs` | +| M2 | Never treat simulator telemetry as trusted: a modified sim can report anything. | `manual` | `AGENTS.md` | +| M3 | Never merge a SimConnect change without a replayed trace or a documented manual MSFS test. | `manual` — no replay corpus exists (`KI-009`) | `AGENTS.md`, ADR-0002 | +| M4 | Never require a modification to the simulator, or bypass an ACL, DRM or session boundary. | `manual` | ADR-0003 | + +## Third-party egress + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| T1 | Never add an outbound host without adding it to the Tauri CSP in the same change. | `manual` | `app/src-tauri/tauri.conf.json:26` | +| T2 | Never trust a third-party response shape: parse defensively, fail closed. | `manual` | `app/src/lib/simbrief.ts:75` | +| T3 | Never send a Thrustline credential, JWT or personal datum to a third party. | `manual` | `AGENTS.md` | +| T4 | Reuse pooled clients: never construct a long-lived static `HttpClient` where `IHttpClientFactory` is registered. | `manual` | `KI-018` | + +## Desktop shell + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| W1 | Never set the Tauri CSP to `null`. | `./scripts/security-check.ps1` | `security-check.ps1` | +| W2 | Never grant the WebView process-execution permissions (`shell:allow-execute`, `shell:allow-spawn`). | `./scripts/security-check.ps1` | `security-check.ps1` | +| W3 | Capabilities stay minimal: adding one is a reviewed change, never incidental. | `manual` | `app/src-tauri/capabilities/default.json` | +| W4 | No business rule and no authority decision lives in a React component. | `manual` | `AGENTS.md`, `KI-005` | + +## Distribution + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| P1 | Versions are semver from a single source; never publish an opaque version, reuse a number, or move a tag. | `manual` | bootstrap 2026-08-12 | +| P2 | The bridge ships self-contained `win-x64`; never require a .NET SDK or runtime on the user machine. | `./scripts/build-sidecar.ps1` | ADR-0003, ADR-0004 | +| P3 | Never publish an artifact without verifiable provenance and checksums. | `manual` — no release pipeline exists (`KI-003`) | ADR-0004 | + +## CI and supply chain + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| I1 | Exact versions and committed lockfiles; CI restores frozen (`npm ci`, never `npm install`). | `.github/workflows/security.yml` | ADR-0004 | +| I2 | Never merge with a known high-severity vulnerability in a production dependency. | `npm audit --omit=dev --audit-level=high` | `security.yml:41` | +| I3 | Never let a direct dependency fall a major version behind without a recorded decision — 19 are currently outdated, 8 by a major. | `npm outdated` | owner, 2026-08-12 | +| I4 | Never add a dependency where the pinned toolchain already suffices. | `manual` | `AGENTS.md` | +| I5 | Never commit a secret, token, personal datum or environment file. Never execute a remote script. | `gitleaks` | `security.yml:56` | + +## Privacy + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| V1 | Never log a secret, JWT, authentication header or personal datum; technical detail goes to redacted logs. | `manual` | `AGENTS.md` | +| V2 | Errors shown to the user are actionable and carry no internal detail. | `manual` | `AGENTS.md` | + +## Rules from defects already encountered + +A tooling trap, promoted from `LEARNINGS.md` after two independent occurrences +or one deterministic reproduction. A line here is **removed** as soon as a check +makes the trap impossible. + +| # | Trap | What to do instead | Promoted from | +| --- | --- | --- | --- | +| L1 | `dotnet test` exits `0` having discovered no tests, and reads as a pass. | Check the discovered count against `docs/validation-baselines.json` before concluding. No .NET test project exists yet (`KI-004`). | LC-2026-001 | +| L2 | An installer extracted at the repository root deleted the whole documentation set and left a stale duplicate of the method kit. | Never run an unreviewed install script in the worktree; extract to a scratch directory and copy in deliberately. | LC-2026-002 | diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..5378e4b --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,100 @@ +# Setup + +How to get from a fresh clone to a running system, and back to a clean one. + +Cap: **100 lines**. Read when a command in `VALIDATION.md` needs a prerequisite +you do not have. Every command is copied from a file in the repository, and the +source is named. + +## Prerequisites + +| Tool | Version | Pinned by | Why exactly this version | +| --- | --- | --- | --- | +| Windows | 11 x64 | ADR-0003 | the bridge, the Tauri build and MSFS are Windows-only | +| Node | 24.18.0 | `.node-version`, `.nvmrc`, `app/package.json` engines `>=24.18.0 <25`, `ci.yml:22` | pinned identically in the repo and in CI | +| npm | bundled with Node 24 | `app/package-lock.json` | ADR-0004 specifies pnpm 11; **not adopted** (`KI-017`) | +| .NET SDK | 8.0.x | `Thrustline.Bridge.csproj` (`net8.0`), `security.yml:25` | ADR-0004 targets .NET 10 LTS; **not adopted**, and .NET 8 support ends November 2026 (`KI-017`) | +| Rust | stable | `dtolnay/rust-toolchain@stable` in both workflows | no `rust-toolchain.toml` exists despite ADR-0004 requiring one (`KI-017`) | +| WebView2 | Evergreen | supplied by Windows 11 | ADR-0004: not shipped with the app | +| MSFS 2024 | current public stable | ADR-0003 | only needed for SimConnect work | +| Supabase CLI + Docker | not recorded | — | `no reason recorded` — needed for any local schema work; nothing in the repo pins a version | +| python3 | any | — | **absent on this machine**; POSEIDON checks cannot run (see `VALIDATION.md`) | + +`no reason recorded` is a legitimate entry. An invented reason is not. + +## From clone to running + +``` +cd app +npm ci +copy .env.example .env +``` + +Then fill `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` in `app/.env`. + +``` +npm run dev +``` + +For the full desktop app, the sidecar must be built first — `npm run tauri:dev` +will not produce it: + +``` +.\scripts\build-sidecar.ps1 +cd app +npm run tauri:dev +``` + +Source: `app/package.json` scripts, `app/.env.example`, `scripts/build-sidecar.ps1`. + +## Configuration + +| Variable | Required | Where the local value comes from | Read from | +| --- | --- | --- | --- | +| `VITE_SUPABASE_URL` | yes | the Supabase project — ask `PacoRCS` | `app/.env.example` | +| `VITE_SUPABASE_ANON_KEY` | yes | the Supabase project — ask `PacoRCS` | `app/.env.example` | +| `VITE_SIM_BRIDGE_URL` | no | defaults to `http://127.0.0.1:5055` | `app/.env.example` | + +The anon key is designed to ship in the bundle; RLS is what protects the data. +That is not a licence to relax D3. + +**No secret, token, personal datum or environment file is ever committed or +logged.** `.env`, `.env.local` and `.env.*.local` are git-ignored +(`.gitignore:5-7`) and `gitleaks` runs on every pull request. + +## Back to a clean state + +``` +cd app; rmdir /s /q node_modules dist; npm ci +cd ..\sim-bridge; dotnet clean; rmdir /s /q bin obj publish +cd ..\app\src-tauri; cargo clean +``` + +There is **no database reset command** — no Supabase CLI task, no npm script, no +seed. Recreating a schema is entirely manual, so a failed run is not +diagnosable. That is real debt (`KI-019`). + +## Verify the setup worked + +``` +cd app +npm test +``` + +Expect `4 passed (4)` files and `11 passed (11)` tests — the baseline in +`docs/validation-baselines.json`. A run discovering fewer is a failure whatever +its exit code. + +## What cannot be run locally + +MSFS 2024 SimConnect behaviour, cross-owner RLS isolation, and signed-installer +distribution. See the table at the end of `docs/VALIDATION.md` for who runs +each, on which environment, and what the local substitute does not prove. + +## Traps + +- `npm run tauri:dev` starts without the sidecar and the app looks alive while + every sim feature is dead. Run `.\scripts\build-sidecar.ps1` first. +- `dotnet test` exits `0` in `sim-bridge/` having discovered nothing. It is not + a passing test run (`KI-004`). +- Git for Windows only. Do not drive this repository from WSL over `/mnt/c`. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md new file mode 100644 index 0000000..74213b2 --- /dev/null +++ b/docs/VALIDATION.md @@ -0,0 +1,101 @@ +# Validation + +This file answers one question: **I touched this — what do I run?** + +Cap: **130 lines**. Read the **one row** for the area you touched. + +Every command is copied from `.github/workflows/ci.yml`, `security.yml` or +`app/package.json`. Run them from the repository root unless a row says +otherwise. + +## What to run + +Order: targeted tests for the zone first, then the boundary checks. + +| You touched | Commands, in order | Prerequisite | Depth | +| --- | --- | --- | --- | +| Data and persistence | `manual` — no automated RLS suite exists (`KI-002`); prove owner A / owner B / anonymous by hand on a disposable project | Supabase CLI + Docker | maximum | +| Authoritative service | `manual` — no edge-function tests exist; exercise `complete-flight` against a disposable project | Supabase CLI + Docker | maximum | +| Bridge local API | `cd sim-bridge; dotnet build --configuration Release` -> `manual` request-by-request check | .NET 8 SDK | maximum | +| SimConnect and MSFS | `cd sim-bridge; dotnet build --configuration Release` -> `manual` MSFS 2024 session, documented | MSFS 2024 on Windows 11 | maximum | +| Third-party egress | `cd app; npm test` -> `npm run build` -> `manual` offline and error-path check | — | maximum | +| Desktop shell | `cd app/src-tauri; cargo check` -> `cd app; npm run build` -> `.\scripts\security-check.ps1` | Rust stable | maximum | +| Distribution | `.\scripts\build-sidecar.ps1` -> `cd app; npm run tauri:build -- --no-bundle` | Rust + .NET 8 SDK | maximum | +| Client / UI | `cd app; npm test` -> `npm run build` | — | fast + real journey | +| Dependencies | `cd app; npm audit --omit=dev --audit-level=high` -> `npm outdated` -> `cd sim-bridge; dotnet list package --vulnerable --include-transitive` | network | maximum | +| CI config | push the branch and read the run; never assume a workflow edit is inert | — | maximum | +| Method documents | `python3 poseidon/checks/check.py --all` — **unavailable, see below** | python3 | fast | + +`npm run build` is `tsc -b && vite build`: it is the type check as well as the +build. There is no separate lint step in this repository. + +## Checks mode — agent mode + +`python3` is **not installed on this machine**. `poseidon/checks/check.py` +cannot run here or in CI, so the method documents have no deterministic gate: +their caps, the boundary-to-section correspondence, placeholder detection and +the 35 negative mutations are all unverified. + +Until python3 is installed, every report must state +`checks: agent mode — not deterministic, no CI gate`, and the substitute is +`poseidon/references/checks-by-hand.md`. Reading a file is not equivalent to a +check that fails a build. + +## Two speeds + +- **Maximum evidence** for anything touching money, data, server authority, + security or the simulator: full checks, real runtime, a dedicated manual + verification. +- **Fast** for client composition: types, tests, build — and the manual + verification is the real journey through the application. + +The **boundary touched** decides, never the size of the diff. + +## Four outcomes, never interchangeable + +`passed`, `failed`, `not run`, `blocked by environment`. + +A tool exiting `0` without discovering any test proves nothing: check the +**number of items discovered** against the baseline before concluding. This is +not hypothetical here — `dotnet test` discovers zero tests in this repository +and exits `0` (`KI-004`, `L1`). + +## Cross-cutting checks + +Run before any Pull Request, whatever the area touched. + +``` +.\scripts\security-check.ps1 +``` + +CI additionally runs `gitleaks` and `rustsec/audit-check` on every pull request +(`.github/workflows/security.yml`). Neither has a local equivalent here. + +## Baselines + +Guardrail counters live in `docs/validation-baselines.json`, one entry per +suite. A run discovering less than its baseline is `failed`, whatever its exit +code. + +Measured on 2026-08-12 by running the suite, not by counting files: + +| Suite | Command | Discovered | Result | +| --- | --- | --- | --- | +| frontend | `cd app; npm test` | 11 tests in 4 files | passed | +| bridge | `cd sim-bridge; dotnet test` | **0 — no test project exists** | proves nothing | + +A baseline rises only in the unit that adds the corresponding tests, through +`python3 poseidon/checks/check.py --record-baseline <suite>=<n> --raised-by <ID>`, +which refuses any value below the current one. That command is unavailable in +agent mode; the current entry was written by hand and carries no such guard. + +## Checks blocked by the environment + +| Check | Why unavailable | Who runs it, where | What the substitute does not prove | +| --- | --- | --- | --- | +| `check.py --all` | python3 absent | anyone, once python3 is installed | reading the documents does not enforce caps or fail a build | +| MSFS golden path | MSFS 2024 required | `PacoRCS`, Windows 11 + MSFS 2024 | a build proves nothing about SimConnect behaviour | +| RLS isolation | no suite, no disposable project wired | `PacoRCS`, Supabase project | a passing frontend suite says nothing about cross-owner leakage | +| Signed installer | no release pipeline (`KI-003`) | `PacoRCS`, Windows 11 VM | `--no-bundle` does not prove installation or upgrade | + +Absence is stated explicitly, never inferred from silence. diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index b5c7f06..899d7e0 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -1,121 +1,115 @@ -# Workflow de refonte +# Workflow -## Rôles +The **procedure** for each phase lives in the slash commands. This file defines +what the phases operate on: roles, states, the unit, and the size limits. Read +the section the router names, not the file. -- **Andy — Product Owner** : tranche la vision, le périmètre et les compromis. -- **Agent planificateur** : analyse le dépôt, propose ADR, roadmap et tickets. -- **Codex implémenteur** : exécute un ticket borné et fournit les preuves. -- **Reviewer** : cherche régressions, failles, dérive architecturale et tests - manquants avant validation. +Cap: **165 lines**. -Un même outil peut tenir plusieurs rôles, mais pas dans la même étape sans -effectuer une revue adversariale explicite. +## Roles -## États d'un ticket +- **`<Owner>`** — settles vision, scope, trade-offs, and merges. The only role + never held by an agent. +- **Coordinator** — carries one unit end to end: plan, code, evidence. +- **Reviewer** — hunts regressions, flaws, drift, and missing tests. -`Draft → Ready → In progress → Review → Verify → Done` +One agent may hold the last two roles, never on the same milestone. -États alternatifs : `Blocked`, `Rejected`, `Superseded`. +## States -## 1. Préparer la phase +Two independent fields, two closed lists. A check enforces both. -1. Définir le résultat utilisateur. -2. Écrire/valider les ADR structurantes. -3. Identifier dépendances et risques. -4. Découper en vertical slices. -5. Créer seulement les 3–8 prochains tickets détaillés ; garder le reste au - niveau roadmap pour éviter un plan périmé de 50 tickets. +- **Unit** `Status:` — `Draft` → `Ready` → `In progress` → `Done`. + Alternates: `Blocked`, `Rejected`, `Superseded`. (`Review` and `Verify` are + legal but describe a whole unit awaiting `<Owner>`, not a milestone.) +- **Milestone** `Status:` — `Draft` → `In progress` → `Review` → `Verify` → + `Verified` → `Done`. Alternate: `Blocked`. -## 2. Rendre un ticket Ready +`Verified` and `Done` are distinct on purpose: `/verify` writes the first, +`/close` writes the second, and the gap is what makes a close survive a session +ending between them. -Un ticket Ready contient : +Which command a given pair routes to is defined **only** in POSEIDON's router +table, `poseidon/POSEIDON.md`. It is not repeated here. -- objectif unique ; -- dépendances satisfaites ; -- zones autorisées et interdites ; -- exigences et non-objectifs ; -- critères d'acceptation observables ; -- tests attendus ; -- vérification manuelle de 5–10 minutes ; -- revue sécurité si nécessaire. +## The unit of work -Le reviewer challenge le ticket avant tout code : trop large, ambigu, mauvaise -frontière ou absence de preuve = retour en Draft. +The unit of tracking, of branching, and of integration is the **user +capability**: one file in `docs/features/`, one branch, one Pull Request. -## 3. Implémenter +A capability is a complete vertical slice. Data schema, authoritative service, +authenticated boundary, validation on a real runtime, and client composition +live together in it, split into ordered **milestones**: one commit per milestone, +one review per milestone, one manual verification per milestone. -1. Créer une branche `type/TXXXX-slug`. -2. Lire `AGENTS.md`, `CURRENT_STATE.md`, le ticket et les docs liées. -3. Vérifier l'état Git et préserver les changements existants. -4. Inspecter le code réel. -5. Implémenter seulement le ticket. -6. Exécuter d'abord les tests ciblés, puis les gates applicables. -7. Ne pas corriger les découvertes hors scope ; les consigner. +Work that is not a user capability — governance, a fix, tooling, a check — uses +**the same file with a single milestone**. There is no second format to maintain. -Types de branche : `foundation`, `feature`, `fix`, `security`, `refactor`, `docs`. +Why the vertical slice: splitting a capability into four to six technical units +produces as many branches, bases, and index lines to keep consistent — for user +value that only exists at the last one. The cost is paid in reconciliation units +that ship nothing. -## 4. Revoir +## 1. Prepare -La revue vérifie dans cet ordre : +1. Define the user outcome. +2. Write or confirm the structuring decisions. +3. Identify dependencies and risks. +4. Split into capabilities, each a complete vertical slice. +5. Order milestones from most-constraining to most-dependent: data and authority + before boundary, boundary before client composition. +6. Detail only the **next one to three** capabilities; keep the rest at roadmap + level, so the plan does not go stale. -1. sécurité et perte de données ; -2. conformité aux critères ; -3. régressions et compatibilité ; -4. architecture et dette créée ; -5. tests et observabilité ; -6. lisibilité et performance. +Before launching a wave: map the shared paths, ask for every `<Owner>` decision +in **one batch**, assign disjoint paths and the integration order, reserve the +verification environments, and confirm no approved Pull Request is waiting only +on propagation. -Une revue ne doit pas demander une réécriture esthétique sans bénéfice mesurable. +Daily priority order: a Pull Request ready to integrate, a blocking CI, a +critical-path dependency, then a new unit. Code volume does not measure progress; +only a proven capability present on the main branch reduces what is left. -## 5. Vérifier +## 2. Ready -Effectuer la checklist manuelle du ticket. Si elle échoue, le ticket revient en -`In progress`. Si elle est impossible localement, indiquer précisément qui doit -la faire et sur quel environnement ; le ticket reste `Verify`. +A `Ready` capability has: one user goal; dependencies genuinely satisfied; the +union of `Allowed areas` and `Do not touch`; ordered milestones, each with its +observable result, primary boundary, and validations; acceptance criteria +observable over the whole capability; and one 5–10 minute manual verification per +milestone. -## 6. Clore +The reviewer challenges the unit **before any code**. Double goal, unobservable +milestone, wrong boundary, or missing evidence sends it back to `Draft`. A +milestone that cannot be committed alone is not a milestone: it belongs to the +previous one. -1. Remplir le Completion Report dans le ticket. -2. Actualiser `CURRENT_STATE.md` si l'état réel a changé. -3. Ajouter les problèmes différés dans `KNOWN_ISSUES.md`. -4. Mettre à jour une ADR seulement par une nouvelle ADR qui la remplace. -5. Fusionner après checks et revue. -6. Choisir le prochain ticket Ready. +## 3 to 6. Implement, review, verify, close -## Limites de taille +Procedure in `/milestone`, `/review`, `/verify`, `/close`. Not restated here. -Un bon ticket : +One principle governs all four and is stated only here: **a capability is +reviewed milestone by milestone, on the pushed diff, by an agent that did not +write it, before the next milestone opens.** This is the counterpart of the +vertical slice. Without it a sensitive-data change and a read-only panel go +through the same review, and the first loses the attention it deserves. A single +review at the end of a capability is non-conforming. -- vise un résultat ; -- modifie idéalement une frontière principale ; -- produit un diff révisable ; -- se vérifie manuellement en 5–10 minutes ; -- peut être abandonné sans invalider plusieurs jours de travail. +## Size limits -Si le ticket combine migration, nouveau protocole, gros écran et pipeline release, -le diviser. +A good capability targets a single outcome, statable without an "and", carries +two to five milestones, and can be abandoned without invalidating more than a few +days of work. -## Commande de démarrage recommandée +A good milestone targets an observable result, changes **one** primary boundary +and not three, produces a diff reviewable in one pass, is manually verifiable in +5–10 minutes, and **is committable alone** even if the capability is incomplete. -```text -Implémente uniquement le ticket TXXXX. +The milestone carries the size limit, not the capability. A milestone combining a +data change, a new protocol, and a large screen must be split. A capability +needing more than five milestones, or two goals, must be split. -Lis AGENTS.md, docs/CURRENT_STATE.md et le ticket complet. -Respecte Allowed areas et Do not touch. -Avant de coder, signale toute contradiction bloquante. -Après le changement, exécute les validations du ticket et remplis son -Completion Report avec preuves, risques et follow-ups. -``` - -## Rétrospective - -À la fin de chaque phase : - -- quelles règles ont évité une erreur ? -- quels tickets étaient trop grands ou ambigus ? -- quels contrôles manquaient ? -- quels documents ont dérivé ? -- quelle automatisation répétée mérite un script ou une skill ? - -Modifier le workflow uniquement sur la base d'un problème répété. +## Side loops +Learning: `docs/LEARNINGS.md`. Debt and out-of-scope discoveries: +`docs/KNOWN_ISSUES.md`. Accepted invariants: `docs/RULES.md`. None of these is +restated here — that is the single-source rule. diff --git a/docs/QUALITY.md b/docs/archive/QUALITY.md similarity index 100% rename from docs/QUALITY.md rename to docs/archive/QUALITY.md diff --git a/docs/README.md b/docs/archive/README.md similarity index 100% rename from docs/README.md rename to docs/archive/README.md diff --git a/docs/ROADMAP.md b/docs/archive/ROADMAP.md similarity index 100% rename from docs/ROADMAP.md rename to docs/archive/ROADMAP.md diff --git a/docs/SECURITY.md b/docs/archive/SECURITY.md similarity index 100% rename from docs/SECURITY.md rename to docs/archive/SECURITY.md diff --git a/docs/STACK.md b/docs/archive/STACK.md similarity index 100% rename from docs/STACK.md rename to docs/archive/STACK.md diff --git a/docs/SUPPORT.md b/docs/archive/SUPPORT.md similarity index 100% rename from docs/SUPPORT.md rename to docs/archive/SUPPORT.md diff --git a/docs/templates/ADR.md b/docs/archive/templates/ADR.md similarity index 100% rename from docs/templates/ADR.md rename to docs/archive/templates/ADR.md diff --git a/docs/templates/PHASE_REVIEW.md b/docs/archive/templates/PHASE_REVIEW.md similarity index 100% rename from docs/templates/PHASE_REVIEW.md rename to docs/archive/templates/PHASE_REVIEW.md diff --git a/docs/templates/TICKET.md b/docs/archive/templates/TICKET.md similarity index 100% rename from docs/templates/TICKET.md rename to docs/archive/templates/TICKET.md diff --git a/docs/tickets/README.md b/docs/archive/tickets/README.md similarity index 100% rename from docs/tickets/README.md rename to docs/archive/tickets/README.md diff --git a/docs/tickets/T0001-baseline-reproductible.md b/docs/archive/tickets/T0001-baseline-reproductible.md similarity index 100% rename from docs/tickets/T0001-baseline-reproductible.md rename to docs/archive/tickets/T0001-baseline-reproductible.md diff --git a/docs/tickets/T0002-modele-produit-solo-ou-collaboratif.md b/docs/archive/tickets/T0002-modele-produit-solo-ou-collaboratif.md similarity index 100% rename from docs/tickets/T0002-modele-produit-solo-ou-collaboratif.md rename to docs/archive/tickets/T0002-modele-produit-solo-ou-collaboratif.md diff --git a/docs/tickets/T0003-strategie-de-refonte.md b/docs/archive/tickets/T0003-strategie-de-refonte.md similarity index 100% rename from docs/tickets/T0003-strategie-de-refonte.md rename to docs/archive/tickets/T0003-strategie-de-refonte.md diff --git a/docs/tickets/T0004-matrice-support-windows-msfs.md b/docs/archive/tickets/T0004-matrice-support-windows-msfs.md similarity index 100% rename from docs/tickets/T0004-matrice-support-windows-msfs.md rename to docs/archive/tickets/T0004-matrice-support-windows-msfs.md diff --git a/docs/tickets/T0005-selection-stack-cible.md b/docs/archive/tickets/T0005-selection-stack-cible.md similarity index 100% rename from docs/tickets/T0005-selection-stack-cible.md rename to docs/archive/tickets/T0005-selection-stack-cible.md diff --git a/docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md b/docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md new file mode 100644 index 0000000..abebd13 --- /dev/null +++ b/docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md @@ -0,0 +1,80 @@ +# ADR-0005 — Harden the existing application instead of rewriting it + +Date: 2026-08-12 +Status: Proposed — awaiting acceptance by PacoRCS +Decider: PacoRCS +Supersedes: ADR-0002 + +## Context + +ADR-0002 (accepted 2026-07) decided a complete rewrite in a **new repository +with fresh Git history**, treating the current repository as a read-only +reference, building a new Supabase project and a new schema, and discarding +current data as disposable. + +Two things are true now that were not then. + +**The rewrite was never executed as written.** This is still the original +repository: 171 commits, first commit `205e422`. The Electron and WPF code was +moved into `legacy/` by `fc3316d` and the Tauri app was scaffolded in place by +`9feb47c`. What happened was directory isolation inside one repository, not a +new repository with fresh history. + +**The owner's assessment of the existing application has changed.** On +2026-08-12 he states that the current version works, that every feature must be +kept, and that the work he wants is correcting the code: best practices, smaller +and more readable files, better reliability, and added security. + +A rewrite discards working behaviour that nobody has characterised — the risk +already recorded as `KI-008`. With the owner reporting the application as +working, that risk no longer buys anything. + +## Options + +| Option | What it gives | What it costs | +| --- | --- | --- | +| A — Execute ADR-0002 as written: rewrite in a new repository | A clean schema and history; no legacy compromise | Discards working, uncharacterised behaviour (`KI-008`); months before parity; contradicts the owner's stated goal | +| B — Harden the existing codebase in place | Keeps every working feature; each improvement is independently verifiable; risk is bounded per unit | Carries existing structural debt (`KI-001`, `KI-005`); no clean-slate moment | +| C — Harden the codebase, then move it to a fresh repository | Same as B, plus a clean history at the end | The move itself is work and risk; a premature move loses the history that explains the code | + +## Decision + +**Thrustline is hardened in place, not rewritten.** Every existing feature is +preserved; the work is code quality, file size and readability, reliability, and +security, applied boundary by boundary as bounded units. + +Where two options are otherwise equivalent, the more reliable and more secure +one is taken — including for runtime and dependency versions. Outdated +dependencies are treated as a security concern, not a cosmetic one. + +Whether the hardened codebase later moves to a fresh repository (option C) is +**deliberately left open** and does not block any unit. `docs/`, `AGENTS.md` and +`poseidon/` travel intact if it happens. + +## Consequences + +- **Makes possible.** Incremental, individually verifiable improvement with no + parity gap and no cutover. `legacy/` stays read-only reference material. + ADR-0001 (solo ownership), ADR-0003 (Windows 11 + MSFS 2024) and the product + scope are unaffected. +- **Makes impossible or expensive.** No clean-slate schema. Existing structural + debt is inherited and must be paid down unit by unit rather than skipped. + Migrations stay append-only against the current schema — the freedom ADR-0002 + granted to start over is withdrawn. +- **ADR-0004 needs re-examination, not repeal.** Its stack pins were chosen for + a greenfield build. Under hardening they become an **upgrade path**, and each + step needs its own unit and evidence: .NET 8 → 10 (support ends November + 2026), React 18 → 19, TypeScript 5.9 → 7, npm → pnpm, and adding the missing + `rust-toolchain.toml`. Whether every pin is still the right target is a + separate decision. Tracked as `KI-017`. +- **What must change now.** `ARCHITECTURE.md` records ADR-0002 as superseded; + `CURRENT_STATE.md` names the hardening milestone; `KI-008` and `KI-016` close + when this ADR is accepted. No new invariant in `RULES.md` follows directly + from this decision. + +## Revalidation + +Re-examine if a hardening unit shows a boundary cannot be made safe without a +structural change larger than the boundary itself — that is the evidence that +would reopen the rewrite question. Otherwise this holds until the product scope +changes. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index df81104..18a1d53 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -9,14 +9,25 @@ agents réouvrent sans cesse une question ou suivent une règle devenue obsolèt - Numéro séquentiel, titre stable. - Une ADR acceptée n'est pas réécrite pour changer l'histoire. - Une nouvelle ADR peut en remplacer une ancienne. -- Les détails d'implémentation ordinaires restent dans les tickets. +- Les détails d'implémentation ordinaires restent dans les unités + (`docs/features/`), pas ici. +- Modèle : `poseidon/templates/ADR.md`. -## ADR prioritaires +## ADR acceptées -1. Modèle solo connecté ou VA collaborative. -2. Stratégie de refonte incrémentale. -3. Canal local Tauri ↔ bridge. -4. Stockage/cache hors ligne. -5. Autorité et modèle anti-triche. -6. Stratégie de distribution et mise à jour. +| ADR | Sujet | Statut | +| --- | --- | --- | +| ADR-0001 | Modèle produit solo | Accepted | +| ADR-0002 | Stratégie de refonte | Remplacée par ADR-0005 (proposée) | +| ADR-0003 | Matrice de support Windows/MSFS | Accepted | +| ADR-0004 | Stack cible et politique de versions | Accepted, à réexaminer (`KI-017`) | +| ADR-0005 | Durcissement au lieu de réécriture | Proposed | + +## ADR encore à écrire + +1. Canal local Tauri ↔ bridge. +2. Stockage/cache hors ligne. +3. Autorité et modèle anti-triche. +4. Stratégie de distribution et mise à jour. +5. Politique de suppression de compte et de rétention (ADR-0001). diff --git a/docs/features/.gitkeep b/docs/features/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/validation-baselines.json b/docs/validation-baselines.json new file mode 100644 index 0000000..8043ee0 --- /dev/null +++ b/docs/validation-baselines.json @@ -0,0 +1,8 @@ +{ + "frontend": { + "discovered": 11, + "raised_by": "bootstrap-2026-08-12", + "command": "cd app; npm test", + "note": "4 files, 11 tests, measured by running the suite on 2026-08-12" + } +} diff --git a/poseidon/POSEIDON.md b/poseidon/POSEIDON.md new file mode 100644 index 0000000..0a934ad --- /dev/null +++ b/poseidon/POSEIDON.md @@ -0,0 +1,148 @@ +# POSEIDON — the router + +You are running POSEIDON. Announce it once, with its version: + +``` +POSEIDON <version from poseidon/VERSION> — locating. +``` + +You are never "working on the repo". At any moment you are in **exactly one +state of exactly one unit of work**. Everything below exists to keep that true. + +Skipping Step 0 is the failure POSEIDON prevents. Do not skip it. + +## Prerequisite + +POSEIDON lives in `poseidon/` at the root of the working directory. If that +folder is absent, say exactly this and stop — do not improvise a method: + +> POSEIDON is not installed here. Extract the archive at the repo root, then say +> "lance poseidon" again. + +If `poseidon/` is present but `AGENTS.md` or `docs/` is missing, the state is +`NOT_INSTALLED` → run `/install`. **You install it yourself, with your own file +tools.** There is no script to run and nothing to build. + +Two layers, and they never mix: + +- `poseidon/` — **the method.** Replaceable as one block; upgrading is replacing + the folder. Never edit anything in it to work around a rule: a wrong rule is a + unit of work, with a negative mutation proving the fix. +- `docs/`, `AGENTS.md` — **your project.** Written by `/bootstrap`, owned by the + repo, never overwritten by an upgrade. + +## Tools + +POSEIDON is markdown you read and files you write. It needs no runtime, no build +step, and no shell script. Two exceptions, and they are not equivalent: + +- **`git` is required.** The method is about getting proven work onto a branch + and into a Pull Request. Without git there is nothing to route. +- **`python3` is optional**, and buys determinism. `poseidon/checks/check.py` is + stdlib-only, runs in CI where no agent exists, and self-tests against 35 + negative mutations. Nothing you do by reading can replace that. + +Probe once per session, cheaply, and carry the answer: + +- `python3 --version` succeeds → **script mode**. +- otherwise → **agent mode**: follow `poseidon/references/checks-by-hand.md`, + and every report from then on states + `checks: agent mode — not deterministic, no CI gate`. + +Never let agent mode be silent. A method whose checks nobody can run in CI is a +convention, and saying so is the difference between the two. + +## Step 0 — Locate (mandatory, every session) + +Run `/locate`, or do exactly what it does: + +1. `git status --porcelain=v1 -b` +2. Read `AGENTS.md`, then `docs/CURRENT_STATE.md`. +3. Find the active unit — branch `feature/<id>-*` first; else the single row of + the `Units in progress` table; else none. +4. If a unit exists, read its **header** (the `Status:` line), `Allowed areas`, + `Do not touch`, and the **first milestone whose `Status:` is not `Done`** — + that is the open milestone, and there is never more than one. + +Announce one line, then stop reading. + +## Step 1 — Route + +Two fields decide everything: the unit's `Status:` and the open milestone's +`Status:`. Read them literally; do not infer state from what the code looks like. + +Before the table, in order: + +- `AGENTS.md` or `docs/` missing → `NOT_INSTALLED` → `/install`. +- `AGENTS.md` still contains `<PROJECT>` → `BOOTSTRAP` → `/bootstrap`. POSEIDON + is installed but never adapted to this repository; every document you would + open is still a template. Do not start a unit in a repository whose invariants + are placeholders. + +A slash command is convenience. If `/milestone` is not registered, read +`poseidon/commands/milestone.md` and follow it — same for every command named +below. + +| Unit `Status:` | Open milestone `Status:` | State | Run | +| --- | --- | --- | --- | +| *no unit* | — | `IDLE` | `/unit-new` | +| `Draft` | any | `DRAFT` | `/unit-ready` | +| `Ready` | any | `READY` | `/milestone` — it first sets the unit to `In progress` | +| `In progress` | `Draft` or `In progress` | `IMPLEMENT` | `/milestone` | +| `In progress` | `Review` | `REVIEW` | `/review` | +| `In progress` | `Verify` | `VERIFY` | `/verify` | +| `In progress` | `Verified` | `CLOSE` | `/close` | +| `In progress` | *none — all `Done`* | `CLOSE_UNIT` | `/close` | +| any | `Blocked` | `BLOCKED` | report and halt | +| `Blocked`, `Rejected`, `Superseded` | any | `BLOCKED` | report and halt | +| `Done` | — | not active | `/unit-new`, or nothing | + +The two fields cannot both be ambiguous: exactly one row matches. If none +matches, the file is malformed — report `state=CONFLICT`, quote the two `Status:` +lines, and halt. Never guess a state. + +`/verify` writes `Verified`, not `Done`. `/close` writes `Done`. That is what +makes the close survive a session ending between the two. + +## Reading budget + +A document is opened because a step above named it, never "for context". + +- `docs/RULES.md` — **one section**, for the boundary this milestone touches. +- `docs/VALIDATION.md` — **one row**, for the area you touched. +- `docs/WORKFLOW.md` — roles, states, unit definition, size limits. Procedure is + in the commands; do not look for it here. +- `docs/ARCHITECTURE.md` — the `Boundaries` table when opening a unit; the rest + only when the milestone changes what it describes. +- `docs/PRODUCT.md`, `docs/SETUP.md`, `docs/decisions/` — only when the open + milestone changes what they describe, or when a prerequisite is missing. + +Precedence when sources disagree, and the contradiction protocol: `AGENTS.md`. + +## Stop conditions + +Halt and report — do not improvise around any of these: + +- **Three strikes.** Three failed attempts at the same fix: revert, record, + halt. Protocol in `poseidon/references/recovery.md`. +- **Out of scope.** A real defect outside `Allowed areas` goes to + `docs/KNOWN_ISSUES.md` with its evidence — and is **not fixed**. +- **Reserved decision.** No option satisfies the invariants; or the act is + irreversible or leaves the repo; or it is a product call no evidence settles. +- **Merge.** Final review and merge belong exclusively to `<Owner>`. + +Everything else you **decide alone**: safest option first, fastest among equally +safe ones, then record the assumption in the report. + +## Ending any response + +Run `/handoff`, or produce what it specifies: the report, `Next`, and a resume +prompt. A report without all three is not finished. + +## References + +`poseidon/references/review.md` — the adversarial review order and what `<base>` means. +`poseidon/references/delegation.md` — when a sub-agent is allowed, and its contract. +`poseidon/references/recovery.md` — three strikes, and the four validation outcomes. +`poseidon/references/install.md` — installing POSEIDON. +`poseidon/references/checks-by-hand.md` — the checks when `python3` is absent. diff --git a/poseidon/README.md b/poseidon/README.md new file mode 100644 index 0000000..e25b794 --- /dev/null +++ b/poseidon/README.md @@ -0,0 +1,277 @@ +# POSEIDON — agent-driven development method + +Extract at the root of a repository — empty or already large — then say +**`lance poseidon`** in Claude Code. That is the whole installation. + +Two layers, and they never mix: + +- **`poseidon/` is the method.** Replaceable as one block; upgrading is + replacing the folder. Nothing in it is project-specific. +- **`docs/` and `AGENTS.md` are your project.** Written once by `/bootstrap`, + owned by the repo, never touched by an upgrade. + +Extraction places `poseidon/` and two files under `.claude/` — nothing else, so +it cannot touch your `README.md`, your commands, or anything you already have. +Everything after that, the agent does with its own file tools: there is no +script, no build step, and nothing to install. + +This README is the only file written for a **human**. Everything else is written +for an **agent**, in the imperative, and is kept as short as the rule allows. +That separation is the point: an agent reading three paragraphs of justification +has to infer the action, and a fraction of the time it infers the wrong one. + +## The principle, in one sentence + +> **Documentation is indexed by durable boundary — never by ticket. A ticket's +> identifier is a citation, never a title.** + +A system's boundaries are bounded and knowable: database, authenticated API, +client, side processes, CI, distribution. Its tickets are not. Documentation +indexed by ticket grows without limit and nobody ever dares remove a line, +because a section titled `## Evidence T0032` looks like the evidence itself. + +It is not. The evidence lives in the ticket's Completion Report — already +written, already versioned, already dated. Rules documents **cite** it. That one +change divides the volume by four and loses nothing. + +## The second principle: one state, one command + +Everything above governs what is *written*. This governs what an agent *does*. + +At any moment the agent is in exactly one state of exactly one unit of work, and +that state is **two literal fields**: the unit's `Status:` and the open +milestone's `Status:`. `poseidon/POSEIDON.md` reads those two fields at +the start of every session and routes to the single command that applies. +Nothing is inferred from what the code looks like, and exactly one router row +ever matches — if none does, the agent halts instead of guessing. + +That is what stops a session from touching four unrelated things. A set of +excellent principles, with no trigger and no router, will not. + +One detail carries more weight than its size suggests: `/verify` writes +`Verified`, and only `/close` writes `Done`. If the two were merged, a session +ending between verification and closing would leave the milestone `Done` — and +the next session's router would skip straight past the Completion Report, the +state update, and the registries, silently, forever. + +## Target tree + +Every file carries a line cap enforced by an automated check, and a moment at +which it is read. An agent never opens a document "for context". + +| In the target repo | Cap | When it is read | +| --- | --- | --- | +| `AGENTS.md` | 115 | always | +| `<zone>/AGENTS.md` | 50 | if the task touches the zone | +| `docs/CURRENT_STATE.md` | 200 | always | +| `docs/WORKFLOW.md` | 165 | at a unit's first milestone | +| `docs/RULES.md` | 300 | **one section**, per boundary touched | +| `docs/VALIDATION.md` | 130 | **one row**, before validating | +| `docs/KNOWN_ISSUES.md` | — | opening and closing a unit | +| `docs/LEARNINGS.md` | — | when a learning is a candidate | +| `docs/features/README.md` | **generated** | never in full | +| `poseidon/templates/FEATURE.md` | 105 | when creating a unit | +| `poseidon/templates/ADR.md` | 45 | for a structuring decision | +| `docs/ARCHITECTURE.md` | 150 | the `Boundaries` table, opening a unit | +| `docs/PRODUCT.md` | 120 | when the milestone changes what it describes | +| `docs/SETUP.md` | 100 | when a prerequisite is missing | +| `poseidon/` | — | routed, every session | +| `.claude/commands/*.md` | — | one per phase, on invocation | +| `poseidon/checks/check.py` | — | in CI, blocking | + +**One unit format.** A user capability, a fix, a tool, or a check all use the +same file; what distinguishes them is the number of milestones, not the +template. A second format would mean a second index, a second set of rules, and +one more question at every creation. + +`docs/decisions/` holds ADRs; it has no cap, because a superseded decision is +kept rather than condensed. + +## What POSEIDON refuses + +These choices are the method. Dropping them brings the complexity back within +weeks. + +- **No document is titled by a ticket identifier.** Not `## Evidence T0032`, not + `## Boundary T0023`. A check enforces this. +- **No evidence is written twice.** The Completion Report is the trace; rules + cite it in a `Proven by` column. +- **No index is maintained by hand** where a script can produce it from the + files themselves. +- **No global rule stacks.** A new rule in `AGENTS.md` replaces or condenses + another. The cap is the mechanism. +- **No method document exceeds its cap.** An overrun is not a warning — it is a + check failure that blocks integration. +- **No fourth attempt.** Three failed attempts at the same fix: revert, record, + halt. + +## Scaling to a large codebase + +Three mechanisms, and only three. + +1. **Nested `AGENTS.md`.** Agents read the file nearest the code. A task in + `<zone>/` loads ~110 + ~40 lines, never the whole map. This is the only + mechanism that does not degrade when the repo triples: you add zones, not + lines to the root file. +2. **`Allowed areas` / `Do not touch` on every unit.** The agent never needs to + understand the whole system, only its boundary. That is what makes a + several-hundred-thousand-line codebase tractable by a two-day unit. +3. **Indexing by boundary.** `RULES.md` grows with the number of boundaries, not + with history. + +## Install + +``` +tar xzf poseidon.tar.gz -C /path/to/repo +``` + +Then say **`lance poseidon`**. The router sees `AGENTS.md` and `docs/` missing, +reports `NOT_INSTALLED`, and runs `/install` — the agent writes the project layer +itself, merging an existing `AGENTS.md` with judgement rather than refusing or +clobbering, identically on every OS. + +Then **`/bootstrap`**, once. It audits the codebase for everything the code can prove (boundaries, the real +commands in the task runner and CI, pinned versions), asks in **one batch** for +everything it cannot (who this is for, the priority order, which incidents have +actually happened, who merges), and then writes `ARCHITECTURE.md`, `RULES.md`, +`VALIDATION.md`, `SETUP.md`, `PRODUCT.md`, `AGENTS.md`, and `CURRENT_STATE.md` — +in that order, because each one feeds the next. The boundary table it produces +becomes the section list of `RULES.md` and the row list of `VALIDATION.md`; a +check enforces that correspondence from then on. + +Its one rule is **derive, or ask — never invent.** A codebase proves what it +does; it cannot prove who it is for or which trade-off wins. An answer guessed +during bootstrap is read as established fact by every session afterwards, and +nothing ever re-checks it. Unanswered questions are written as +`<unanswered — asked YYYY-MM-DD>` and keep surfacing, which is correct. + +Then wire `python3 poseidon/checks/check.py --all` into CI as blocking, and run +`/locate`. It will tell you where you are. + +## Adopting into a repository that already exists + +Two things break on day one, and both are handled. + +**Existing `AGENTS.md` / `CLAUDE.md`.** `/install` never overwrites; it reports +what it left in place and merges it into POSEIDON's structure, keeping every +existing rule. This matters more than it looks: until `AGENTS.md` carries POSEIDON's content, the +router never sees `<PROJECT>`, never routes to `/bootstrap`, and POSEIDON does +nothing at all — silently. `/install` will not finish with that unresolved. + +**Existing `TODO`/`FIXME` debt.** A real codebase has hundreds. Failing all of +them on day one turns CI red and someone switches the checks off, which costs +more than the debt did. So `/install` runs `poseidon/checks/check.py --adopt`, which records +how many unregistered markers each file already carries. The check then blocks +**new** debt without demanding the old be cleared: + +``` +web/legacy.py: 13 unregistered markers, 1 more than the 12 grandfathered +``` + +It is a ratchet, not an exemption: it only ever tightens. Re-run `--adopt` after +a cleanup unit to lower the counts. A marker citing a `KI-`/`LC-` id that does +not exist is never grandfathered — that is a wrong reference, not debt. + +The same idea applies to test baselines: record what each suite **actually +discovers today**, not what it should. A baseline taken from an already-broken +suite locks in the breakage. + +Expect `/bootstrap`'s `CURRENT_STATE.md` step to be uncomfortable. Writing down +only what is merged and evidenced almost always reveals capabilities everyone +believed were shipped. That discomfort is the point of adopting it. + +## The checks — the one optional dependency + +POSEIDON is markdown you read and files you write. It needs no runtime and no +build step. Two exceptions, and they are not equivalent: + +- **`git` is required.** The method exists to get proven work onto a branch and + into a Pull Request. Without git there is nothing to route. +- **`python3` is optional, and buys determinism.** + +`poseidon/checks/check.py` is stdlib-only. Nine groups: line caps, tracking +coherence, `CURRENT_STATE.md` drift, the generated unit index, the ban on +identifiers in headings, well-formed registers, orphan +`TODO`/`FIXME`/`HACK`/`XXX` markers, validation baselines, and the boundary +correspondence — every boundary in `ARCHITECTURE.md` must have its section in +`RULES.md`, or the rules have silently stopped covering part of the system. + +Two rules govern all of them: + +- **Fail closed.** An unreadable entry, a missing file, or an unknown argument + makes a check fail, never pass. +- **Every check carries negative mutations.** `--self-test` builds a fixture + tree, applies 35 mutations, and asserts each is caught **by the check it was + written for** — not incidentally by a neighbour. A check with no negative + mutation may have stopped checking anything while staying green, and that is + invisible. + +The self-test earned its keep during development. Requiring each mutation to be +caught by its *own* group immediately exposed a milestone parser counting +`### J1` headings inside the Completion Report as extra milestones — a bug a +laxer assertion had reported as passing. + +### Why not have the agent do it + +Because three things do not survive the substitution, and the method is built on +all three: + +1. **CI has no agent.** The checks are supposed to *block integration*. Read by + an agent, they become advice, and advice does not stop a merge. +2. **You cannot self-test by introspection.** `--self-test` proves the checks + still catch what they claim to. Nothing an agent does about its own + reliability is equivalent. +3. **Reproducibility.** A check that sometimes fires is worse than no check: it + teaches people to re-run until green. + +Cost matters too, in the other direction: `check.py --all` is roughly zero +tokens and under a second. Verifying nine groups by reading is thousands of +tokens at every close. + +POSEIDON runs without it — `poseidon/references/checks-by-hand.md` is the agent +procedure — but then every report must state +`checks: agent mode — not deterministic, no CI gate`. Never silently. + +## Deliberate variants + +| Default | Alternative | What the alternative costs | +| --- | --- | --- | +| Cite the Completion Report | Archive narratives in `docs/archive/` | A safety net, at the price of files nobody re-reads | +| Generated unit index | Hand-maintained index | The ability to annotate a line, at the price of a whole class of drift and the reconciliation tickets it produces | +| Nested `AGENTS.md` | One root file | Simpler at first, untenable as soon as the repo has several active zones | +| A skill + commands | Docs alone | Nothing to install, at the price of the agent having to guess when to read what — which is the failure this version exists to fix | +| `check.py` for the checks | Agent reads and judges | No CI gate, no self-test, not reproducible — see below | + +## POSEIDON keeps its own caps + +Every file shipped here is within the cap it imposes. That claim is not a table +someone maintains by hand — this README refuses hand-maintained indexes above, +and a stale line-count table would be exactly one. It is checked: + +``` +python3 poseidon/checks/check.py --caps +``` + +`RULES.md`, `VALIDATION.md`, and `CURRENT_STATE.md` are skeletons: they fill up +and climb toward their caps. The rest are complete as they stand. + +## What changed from the previous version + +The content was not the problem; the absence of a trigger was. + +| Change | Why | +| --- | --- | +| Added `poseidon/POSEIDON.md` | An entry point that locates the state and routes. Previously nothing told the agent when to read `WORKFLOW.md`. | +| Added 11 slash commands | One per phase. A phase described in prose is a phase the agent improvises. | +| Rationale moved into this README | ~60% of the old volume was justification addressed to a human. Agent-facing files are now imperative. | +| `CHECKS.md` → working `poseidon/checks/check.py` | POSEIDON itself said the method drifts within weeks without the checks. A specification is not a check. | +| Added `examples/F0001.md`, filled | Agents reproduce a worked example far more reliably than they apply a written rule. | +| Added the three-strike protocol | The old kit had no failure-recovery rule, so a stuck agent kept trying. | +| Added a section index to `RULES.md` | "Read one section" is only actionable if the agent can find the section without reading the file. | +| One language throughout | Field names were half French, half English; an agent hesitates over the name of a field it must write. | +| Split `Verified` from `Done` | So a session ending mid-close cannot skip the close forever. | +| `WORKFLOW.md` §3–6 cut to a pointer | They restated the commands. Under POSEIDON's own reading budget those lines were unreachable, and they were spending the 165-line cap. | +| Added `/bootstrap`, `PRODUCT.md`, `ARCHITECTURE.md`, `SETUP.md` | A freshly installed kit was a repo full of `<placeholders>` with nothing telling the agent to fill them — so the first session improvised, which is the exact failure POSEIDON exists to stop. | +| Added the boundary correspondence check | `ARCHITECTURE.md` naming a boundary that `RULES.md` does not cover means the rules stopped covering part of the system, silently. | +| Deleted `install.sh`, added `/install` | A shell script cannot merge an existing `AGENTS.md` with judgement, and fails outright on Windows without a POSIX shell. The agent can do both. | +| `python3` demoted to optional | POSEIDON now runs with no runtime at all — but agent mode is labelled `not deterministic, no CI gate` in every report, never silently. | diff --git a/poseidon/VERSION b/poseidon/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/poseidon/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/poseidon/checks/check.py b/poseidon/checks/check.py new file mode 100644 index 0000000..b6bd897 --- /dev/null +++ b/poseidon/checks/check.py @@ -0,0 +1,847 @@ +#!/usr/bin/env python3 +"""Automated checks for the method kit. + +Fail closed: an unreadable entry, a missing file, or an unknown argument makes a +check FAIL, never pass. Every check carries negative mutations, exercised by +`--self-test` — a check with no negative mutation may have stopped checking +anything while staying green, and that is invisible. + +Usage: + python3 check.py --all + python3 check.py --caps --tracking + python3 check.py --fix-index + python3 check.py --record-baseline handlers=11 --raised-by F0001 + python3 check.py --self-test + python3 check.py --all --root /path/to/repo --strict-placeholders +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import tempfile +from pathlib import Path + +UNIT_ID = re.compile(r"\bF\d{4}\b") +MILESTONE_ID = re.compile(r"^###\s+(J\d+)\b", re.MULTILINE) +STATUS_LINE = re.compile(r"^Status:\s*(.*?)\s*$", re.MULTILINE) +ANY_ID = re.compile(r"\b(F\d{4}|KI-\d+|LC-\d{4}-\d+)\b") +REF_ID = re.compile(r"\b(KI-\d+|LC-\d{4}-\d+)\b") +PLACEHOLDER = re.compile(r"<[^<>\n]{2,80}>") +SKELETON_CELL = re.compile(r"^<[^>]*>$") +UNESCAPED_PIPE = re.compile(r"(?<!\\)\|") +GENERATED_BANNER = "<!-- generated by poseidon/checks/check.py — do not edit -->" + + +class Failures(list): + def add(self, check: str, path, message: str) -> None: + self.append(f"[{check}] {path}: {message}") + + +# --------------------------------------------------------------------------- # +# helpers +# --------------------------------------------------------------------------- # + +def load_limits(root: Path) -> dict: + here = Path(__file__).resolve().parent + for candidate in (here / "limits.json", root / "poseidon/checks/limits.json"): + if candidate.is_file(): + return json.loads(candidate.read_text(encoding="utf-8")) + raise SystemExit("FAIL [config] limits.json not found — failing closed") + + +def read(path: Path) -> str: + # Normalise line endings: a multiline check must run against what the CI + # runner will actually see, not against the local checkout's endings. + return path.read_text(encoding="utf-8", errors="strict").replace("\r\n", "\n") + + +def section_body(text: str, title: str) -> tuple[str, int]: + """Body of a `## <title>` section, and its offset. ('', -1) if absent.""" + m = re.search(rf"^##\s+{re.escape(title)}\s*$", text, re.MULTILINE) + if not m: + return "", -1 + rest = text[m.end():] + nxt = re.search(r"^##\s+", rest, re.MULTILINE) + return (rest[: nxt.start()] if nxt else rest), m.start() + + +def table_rows(text: str, section: str | None = None) -> list[list[str]]: + """Data rows of markdown tables: header, separator, and skeleton rows dropped. + + A skeleton row is one where any cell is a bare `<placeholder>` — the shipped + template rows. Validating those as real entries would make `<Medium>` a + legal severity; `--strict-placeholders` is what flags them instead. + """ + body = text if section is None else section_body(text, section)[0] + rows: list[list[str]] = [] + for line in body.split("\n"): + line = line.strip() + if not line.startswith("|") or not line.endswith("|"): + continue + cells = [c.strip().replace(r"\|", "|") + for c in UNESCAPED_PIPE.split(line.strip("|"))] + if all(set(c) <= set("- :") and c for c in cells): + continue + rows.append(cells) + if not rows: + return [] + return [r for r in rows[1:] if not any(SKELETON_CELL.match(c) for c in r)] + + +def unit_files(root: Path) -> list[Path]: + d = root / "docs/features" + if not d.is_dir(): + return [] + return sorted(p for p in d.glob("*.md") if p.name != "README.md") + + +def parse_unit(path: Path, text: str) -> dict: + # Milestones are parsed from the `## Milestones` section only. The + # Completion Report reuses `### J1` headings; counting those as milestones + # is a real bug this scoping prevents. + body, offset = section_body(text, "Milestones") + if offset == -1: + first = MILESTONE_ID.search(text) + body = text[first.start():] if first else "" + offset = first.start() if first else len(text) + head = text[:offset] + milestones = [] + marks = list(MILESTONE_ID.finditer(body)) + for i, m in enumerate(marks): + end = marks[i + 1].start() if i + 1 < len(marks) else len(body) + block = body[m.start():end] + milestones.append({"id": m.group(1), "statuses": STATUS_LINE.findall(block)}) + dep_body, dep_off = section_body(text, "Dependencies") + branch = re.search(r"^Branch:\s*`?([^`\n]+?)`?\s*$", head, re.MULTILINE) + return { + "id": path.stem, + "status": (STATUS_LINE.findall(head) or [None])[0], + "head_statuses": STATUS_LINE.findall(head), + "milestones": milestones, + "dependencies": set(UNIT_ID.findall(dep_body)) if dep_off != -1 else set(), + "branch": branch.group(1) if branch else "", + } + + +def milestone_status(ms: dict) -> str | None: + return ms["statuses"][0] if len(ms["statuses"]) == 1 else None + + +def open_milestone(unit: dict) -> str: + for ms in unit["milestones"]: + if milestone_status(ms) != "Done": + return ms["id"] + return "—" + + +def walk_files(root: Path, limits: dict): + ignored = set(limits["ignored_dirs"]) + for dirpath, dirnames, filenames in os.walk(root): + rel_dir = Path(dirpath).relative_to(root).as_posix().lstrip("./") + dirnames[:] = [ + d for d in dirnames + if d not in ignored and f"{rel_dir}/{d}".lstrip("/") not in ignored + ] + for name in filenames: + yield Path(dirpath) / name + + +# --------------------------------------------------------------------------- # +# checks +# --------------------------------------------------------------------------- # + +def check_caps(root: Path, limits: dict, f: Failures) -> None: + for rel, cap in limits["line_caps"].items(): + p = root / rel + if not p.is_file(): + f.add("caps", rel, "declared in limits.json but missing — failing closed") + continue + n = len(read(p).rstrip("\n").split("\n")) + if n > cap: + f.add("caps", rel, f"{n} lines > cap {cap}. Condense; never raise the cap.") + zone_cap = limits["zone_agents_cap"] + # Walk, not glob: a vendored AGENTS.md under node_modules must not block CI + # with an error the agent is forbidden from fixing. + for p in walk_files(root, limits): + if p.name != "AGENTS.md" or p.parent == root: + continue + n = len(read(p).rstrip("\n").split("\n")) + if n > zone_cap: + f.add("caps", p.relative_to(root), f"{n} lines > zone cap {zone_cap}") + + +def check_tracking(root: Path, limits: dict, f: Failures) -> None: + units = unit_files(root) + known = {p.stem for p in units} + for p in units: + rel = p.relative_to(root) + u = parse_unit(p, read(p)) + + if len(u["head_statuses"]) != 1: + f.add("tracking", rel, + f"expected exactly one unit-level Status, found {len(u['head_statuses'])}") + elif u["status"] not in limits["unit_states"]: + f.add("tracking", rel, f"invalid unit Status {u['status']!r}") + + if not u["milestones"]: + f.add("tracking", rel, "no milestone — a unit has at least one (J1)") + for i, ms in enumerate(u["milestones"], start=1): + if ms["id"] != f"J{i}": + f.add("tracking", rel, + f"milestone sequence broken: expected J{i}, found {ms['id']}") + if len(ms["statuses"]) != 1: + f.add("tracking", rel, + f"{ms['id']}: expected exactly one Status, found {len(ms['statuses'])}") + elif ms["statuses"][0] not in limits["milestone_states"]: + f.add("tracking", rel, + f"{ms['id']}: invalid Status {ms['statuses'][0]!r}") + + # Milestones close in order: once one is not Done, none after it may be. + seen_open = None + for ms in u["milestones"]: + if milestone_status(ms) != "Done": + seen_open = seen_open or ms["id"] + elif seen_open: + f.add("tracking", rel, + f"{ms['id']} is Done while {seen_open} is not — " + "milestones close in order") + + all_done = bool(u["milestones"]) and all( + milestone_status(ms) == "Done" for ms in u["milestones"]) + if u["status"] == "Ready" and all_done: + f.add("tracking", rel, "Status: Ready but every milestone is Done") + if u["status"] == "Done" and not all_done: + f.add("tracking", rel, + "Status: Done but a milestone is not Done — a unit closes only " + "when every milestone has closed") + if u["status"] in limits["unit_states_active"] and all_done: + f.add("tracking", rel, + f"Status: {u['status']} but every milestone is Done — close the unit") + + for dep in u["dependencies"] - {u["id"]}: + if dep not in known: + f.add("tracking", rel, f"phantom dependency {dep} — no such unit file") + + +def check_current_state(root: Path, limits: dict, f: Failures) -> None: + """The `Units in progress` table is hand-written and is what the router reads + to find the active unit when the branch does not name it. Unchecked, it drifts.""" + p = root / "docs/CURRENT_STATE.md" + if not p.is_file(): + f.add("current-state", "docs/CURRENT_STATE.md", "missing — failing closed") + return + rows = table_rows(read(p), "Units in progress") + cap = limits["max_units_in_progress"] + if len(rows) > cap: + f.add("current-state", "docs/CURRENT_STATE.md", + f"{len(rows)} units in progress > cap {cap}") + known = {q.stem: parse_unit(q, read(q)) for q in unit_files(root)} + for row in rows: + if len(row) != 4: + f.add("current-state", "docs/CURRENT_STATE.md", + f"row has {len(row)} columns, expected 4: {row}") + continue + uid = re.sub(r"[`\[\]()]", "", row[0]).split(".md")[0].strip() + if uid not in known: + f.add("current-state", "docs/CURRENT_STATE.md", + f"lists {uid}, which has no unit file") + continue + u = known[uid] + if u["status"] not in limits["unit_states_active"]: + f.add("current-state", "docs/CURRENT_STATE.md", + f"{uid} listed in progress but its Status is {u['status']!r}") + listed = row[1].strip().split()[0] if row[1].strip() else "" + actual = open_milestone(u) + if listed and listed != actual: + f.add("current-state", "docs/CURRENT_STATE.md", + f"{uid}: open milestone listed as {listed}, actually {actual}") + + +def render_index(root: Path) -> str: + lines = ["# Units index", "", GENERATED_BANNER, "", + "| Unit | Status | Open milestone | Branch |", + "| --- | --- | --- | --- |"] + for p in unit_files(root): + u = parse_unit(p, read(p)) + lines.append( + f"| [{u['id']}]({p.name}) | {u['status'] or '?'} " + f"| {open_milestone(u)} | `{u['branch']}` |" + ) + return "\n".join(lines) + "\n" + + +def check_index(root: Path, limits: dict, f: Failures, fix: bool = False) -> None: + d = root / "docs/features" + if not d.is_dir(): + if fix: + return + f.add("index", "docs/features/", "missing — the units directory is required") + return + target = d / "README.md" + expected = render_index(root) + if fix: + target.write_text(expected, encoding="utf-8") + return + if not target.is_file(): + f.add("index", "docs/features/README.md", "missing — run --fix-index") + return + if read(target) != expected: + f.add("index", "docs/features/README.md", + "out of date. Run --fix-index; never hand-edit a generated index.") + + +def check_headings(root: Path, limits: dict, f: Failures) -> None: + for rel in limits["no_unit_id_in_headings"]: + p = root / rel + if not p.is_file(): + f.add("headings", rel, "missing — failing closed") + continue + for line_no, line in enumerate(read(p).split("\n"), start=1): + if line.startswith("#") and ANY_ID.search(line): + f.add("headings", f"{rel}:{line_no}", + "identifier in a heading. Move it to the " + "'Proven by' / 'Promoted from' column and drop the narrative.") + + +def check_registries(root: Path, limits: dict, f: Failures) -> None: + ki = root / "docs/KNOWN_ISSUES.md" + if not ki.is_file(): + f.add("registries", "docs/KNOWN_ISSUES.md", "missing — failing closed") + else: + seen: set[str] = set() + for row in table_rows(read(ki), "Entries"): + if len(row) != 7: + f.add("registries", "docs/KNOWN_ISSUES.md", + f"row has {len(row)} columns, expected 7: {row}") + continue + rid, sev, _area, _summary, evidence, _target, status = row + if rid in seen: + f.add("registries", "docs/KNOWN_ISSUES.md", f"duplicate id {rid}") + seen.add(rid) + if sev not in limits["issue_severities"]: + f.add("registries", "docs/KNOWN_ISSUES.md", f"{rid}: invalid severity {sev!r}") + if status not in limits["issue_statuses"]: + f.add("registries", "docs/KNOWN_ISSUES.md", f"{rid}: invalid status {status!r}") + if status == "Resolved" and not evidence.strip(): + f.add("registries", "docs/KNOWN_ISSUES.md", + f"{rid}: Resolved with empty evidence — a false success, not a success") + + lr = root / "docs/LEARNINGS.md" + if not lr.is_file(): + f.add("registries", "docs/LEARNINGS.md", "missing — failing closed") + else: + seen = set() + for row in table_rows(read(lr), "Register"): + if len(row) != 5: + f.add("registries", "docs/LEARNINGS.md", + f"row has {len(row)} columns, expected 5: {row}") + continue + lid, _title, state, _dest, revalidate = row + if lid in seen: + f.add("registries", "docs/LEARNINGS.md", f"duplicate id {lid}") + seen.add(lid) + if state not in limits["learning_states"]: + f.add("registries", "docs/LEARNINGS.md", f"{lid}: invalid state {state!r}") + if state in limits["learning_states_needing_revalidation"] \ + and not revalidate.strip(): + f.add("registries", "docs/LEARNINGS.md", + f"{lid}: promoted state with no revalidation date") + + +def registry_ids(root: Path) -> set[str]: + ids: set[str] = set() + for rel in ("docs/KNOWN_ISSUES.md", "docs/LEARNINGS.md"): + p = root / rel + if p.is_file(): + ids |= set(REF_ID.findall(read(p))) + return ids + + +def scan_markers(root: Path, limits: dict) -> tuple[dict[str, list[int]], list[tuple[str, int, str]], list[str]]: + """Returns (unregistered markers by file, bad references, unreadable files). + + `poseidon/` is excluded by ignored_dirs: the method is not the project, and + its own fixture strings contain marker words.""" + known = registry_ids(root) + exts = set(limits["code_extensions"]) + words = "|".join(limits["marker_words"]) + marker = re.compile(rf"(?<![A-Za-z0-9_])({words})(?![A-Za-z0-9_])") + unregistered: dict[str, list[int]] = {} + bad: list[tuple[str, int, str]] = [] + unreadable: list[str] = [] + for p in walk_files(root, limits): + if p.suffix not in exts: + continue + rel = p.relative_to(root).as_posix() + try: + text = read(p) + except (UnicodeDecodeError, OSError): + unreadable.append(rel) + continue + for line_no, line in enumerate(text.split("\n"), start=1): + if not marker.search(line): + continue + ids = REF_ID.findall(line) + if not ids: + unregistered.setdefault(rel, []).append(line_no) + for i in ids: + if i not in known: # exact id, not substring + bad.append((rel, line_no, i)) + return unregistered, bad, unreadable + + +def load_adoption(root: Path, limits: dict) -> dict[str, int]: + """Markers grandfathered when the kit was adopted into an existing repo. + + Without this, day one on a real codebase is hundreds of failures, CI goes + red, and the checks get switched off — which costs more than the debt did. + The ratchet freezes existing debt and blocks new debt; it never permits more. + """ + p = root / limits["adoption_file"] + if not p.is_file(): + return {} + try: + return dict(json.loads(read(p)).get("markers", {})) + except (json.JSONDecodeError, TypeError, ValueError): + return {} # malformed → grandfather nothing, i.e. fail closed + + +def check_markers(root: Path, limits: dict, f: Failures) -> None: + unregistered, bad, unreadable = scan_markers(root, limits) + grandfathered = load_adoption(root, limits) + for rel in unreadable: + f.add("markers", rel, "unreadable — failing closed") + for rel, lines in sorted(unregistered.items()): + allowed = grandfathered.get(rel, 0) + if len(lines) <= allowed: + continue + shown = ", ".join(str(n) for n in lines[:5]) + ("…" if len(lines) > 5 else "") + if allowed: + f.add("markers", rel, + f"{len(lines)} unregistered markers, {len(lines) - allowed} more than " + f"the {allowed} grandfathered at adoption (lines {shown}). Register the " + "new ones in KNOWN_ISSUES.md or remove them.") + else: + for n in lines: + f.add("markers", f"{rel}:{n}", + "marker with no registry entry. Open a KNOWN_ISSUES.md entry " + "or delete the marker.") + for rel, n, i in bad: # a wrong reference is never grandfathered + f.add("markers", f"{rel}:{n}", f"marker cites unknown entry {i}") + + +def check_baselines(root: Path, limits: dict, f: Failures) -> None: + """Guardrail counters live in data, not prose, so a run that discovers less + than its baseline fails on its own instead of depending on a paragraph.""" + rel = limits["baselines_file"] + p = root / rel + if not p.is_file(): + f.add("baselines", rel, + "missing — create it (install.sh does) or drop the reference " + "from docs/VALIDATION.md") + return + try: + data = json.loads(read(p)) + except json.JSONDecodeError as exc: + f.add("baselines", rel, f"invalid JSON ({exc}) — failing closed") + return + if not isinstance(data, dict): + f.add("baselines", rel, "top level must be an object of suite -> entry") + return + for name, entry in data.items(): + if name.startswith("_"): + continue + if not isinstance(entry, dict): + f.add("baselines", rel, f"{name}: entry must be an object") + continue + n = entry.get("discovered") + if not isinstance(n, int) or isinstance(n, bool) or n < 0: + f.add("baselines", rel, f"{name}: 'discovered' must be an integer >= 0") + if not UNIT_ID.fullmatch(str(entry.get("raised_by", ""))): + f.add("baselines", rel, + f"{name}: 'raised_by' must name the unit that added the tests " + "(a baseline rises only there)") + + +def check_boundaries(root: Path, limits: dict, f: Failures) -> None: + """Every boundary declared in ARCHITECTURE.md is a section of RULES.md. + + This is the correspondence that makes documentation indexed by boundary + rather than by history: if the two drift, RULES.md silently stops covering + part of the system. + """ + arch = root / "docs/ARCHITECTURE.md" + rules = root / "docs/RULES.md" + for p in (arch, rules): + if not p.is_file(): + f.add("boundaries", p.relative_to(root), "missing — failing closed") + return + declared = [row[0] for row in table_rows(read(arch), "Boundaries") if row] + sections = {m.strip().lower() for m in + re.findall(r"^##\s+(.+?)\s*$", read(rules), re.MULTILINE)} + for name in declared: + clean = re.sub(r"[`*]", "", name).strip() + if clean.lower() not in sections: + f.add("boundaries", "docs/RULES.md", + f"no '## {clean}' section, but ARCHITECTURE.md declares it a " + "boundary. Add the section, or drop the boundary.") + + +def check_placeholders(root: Path, limits: dict, f: Failures) -> None: + exempt = tuple(limits["placeholder_exempt"]) + for rel in limits["placeholder_targets"]: + if rel.startswith(exempt): + continue + p = root / rel + if not p.is_file(): + continue + for line_no, line in enumerate(read(p).split("\n"), start=1): + for hit in PLACEHOLDER.findall(line): + # `<e.g. ...>` is illustration inside a skeleton row; the + # surrounding `<command>` / `<unit>` cells are not, and those + # are exactly what an agent misreads as a real rule. + if hit.lower().startswith("<e.g."): + continue + f.add("placeholders", f"{rel}:{line_no}", + f"unfilled placeholder {hit} — an agent reads it as a real rule") + + +CHECKS = { + "caps": check_caps, + "tracking": check_tracking, + "current-state": check_current_state, + "index": check_index, + "headings": check_headings, + "registries": check_registries, + "markers": check_markers, + "baselines": check_baselines, + "boundaries": check_boundaries, +} + + +# --------------------------------------------------------------------------- # +# self-test: every check must be provably still checking something +# --------------------------------------------------------------------------- # + +F0001 = ( + "# F0001 — a capability\n\nStatus: In progress\n" + "Branch: `feature/f0001-slug`\n\n" + "## Dependencies\n\n- none\n\n" + "## Milestones\n\n### J1 — first\n\nStatus: Done\n\n" + "### J2 — second\n\nStatus: Draft\n\n" + # The Completion Report reuses `### J1`; it must not be read as a milestone. + "## Completion Report\n\n### J1\n\n- result obtained: yes\n\n" + "### J2\n\n- result obtained:\n\n### Summary\n\n- risks:\n" +) + +FIXTURE = { + "AGENTS.md": "# AGENTS.md\n\nInvariants.\n", + "docs/CURRENT_STATE.md": "# Current state\n\n## Units in progress\n\n" + "| Unit | Open milestone | Branch | Blocked by |\n" + "| --- | --- | --- | --- |\n" + "| F0001 | J2 | `feature/f0001-slug` | nothing |\n", + "docs/WORKFLOW.md": "# Workflow\n\nCycle.\n", + "docs/RULES.md": "# Invariants by boundary\n\n## Data and persistence\n\n" + "| # | Invariant | Check | Proven by |\n| --- | --- | --- | --- |\n" + "| D1 | no destructive migration | `make m` | F0001 |\n\n" + "## Client\n\n| # | Invariant | Check | Proven by |\n" + "| --- | --- | --- | --- |\n" + "| C1 | no authority decision on the client | `make c` | F0001 |\n", + "docs/ARCHITECTURE.md": "# Architecture\n\n## Boundaries\n\n" + "| Boundary | Trust changes how | Enforced in | Read from |\n" + "| --- | --- | --- | --- |\n" + "| Data and persistence | input becomes state | `db/` | `db/schema.sql` |\n" + "| Client | truth becomes display | `web/` | `web/main.ts` |\n", + "docs/PRODUCT.md": "# Product\n\nOne sentence.\n", + "docs/SETUP.md": "# Setup\n\nCommands.\n", + "docs/VALIDATION.md": "# Validation\n\n## What to run\n\nrows.\n", + "poseidon/templates/FEATURE.md": "# FXXXX\n\nStatus: Draft\n", + "poseidon/templates/ADR.md": "# ADR-NNNN\n", + "docs/KNOWN_ISSUES.md": "# Known issues\n\n## Entries\n\n" + "| ID | Severity | Area | Summary | Evidence | Target | Status |\n" + "| --- | --- | --- | --- | --- | --- | --- |\n" + "| KI-001 | Medium | api | slow | `a \\| b` -> 502 | F0001 | Open |\n" + "| KI-120 | Low | api | noise | `x` -> y | F0001 | Open |\n", + "docs/LEARNINGS.md": "# Learnings\n\n## Register\n\n" + "| ID | Title | State | Destination | Revalidate |\n" + "| --- | --- | --- | --- | --- |\n" + "| LC-2026-001 | crlf trap | Observed | — | 2026-12-01 |\n", + "docs/features/F0001.md": F0001, + "docs/validation-baselines.json": + '{\n "handlers": {"discovered": 11, "raised_by": "F0001"}\n}\n', + "src/app.py": "def main():\n return 1 # see KI-120\n# TODO: legacy debt\n", + "docs/adoption.json": '{\n "markers": {"src/app.py": 1}\n}\n', + "node_modules/pkg/AGENTS.md": "x\n" * 80, +} + +MUTATIONS = [ + ("caps: file one line over its cap", + lambda r: (r / "docs/RULES.md").write_text("x\n" * 301, encoding="utf-8")), + ("caps: declared file deleted", + lambda r: (r / "docs/WORKFLOW.md").unlink()), + ("caps: real zone file over the zone cap", + lambda r: _write(r / "web/AGENTS.md", "x\n" * 60)), + ("tracking: invalid unit status", + lambda r: _sub(r / "docs/features/F0001.md", "Status: In progress", "Status: Almost")), + ("tracking: duplicated unit status", + lambda r: _sub(r / "docs/features/F0001.md", "Status: In progress", + "Status: In progress\nStatus: Draft")), + ("tracking: unit status missing", + lambda r: _sub(r / "docs/features/F0001.md", "Status: In progress\n", "")), + ("tracking: milestone missing from sequence", + lambda r: _sub(r / "docs/features/F0001.md", "### J2 — second", "### J3 — second")), + ("tracking: milestone status missing", + lambda r: _sub(r / "docs/features/F0001.md", + "### J2 — second\n\nStatus: Draft", "### J2 — second\n")), + ("tracking: invalid milestone status", + lambda r: _sub(r / "docs/features/F0001.md", "Status: Draft", "Status: Ready")), + ("tracking: milestone closed out of order", + lambda r: (_sub(r / "docs/features/F0001.md", + "### J1 — first\n\nStatus: Done", "### J1 — first\n\nStatus: Draft"), + _sub(r / "docs/features/F0001.md", + "### J2 — second\n\nStatus: Draft", "### J2 — second\n\nStatus: Done"))), + ("tracking: phantom dependency", + lambda r: _sub(r / "docs/features/F0001.md", "- none", "- F0099")), + ("tracking: unit Done while a milestone is not", + lambda r: _sub(r / "docs/features/F0001.md", "Status: In progress", "Status: Done")), + ("tracking: unit left active with every milestone Done", + lambda r: _sub(r / "docs/features/F0001.md", + "### J2 — second\n\nStatus: Draft", "### J2 — second\n\nStatus: Done")), + ("current-state: lists a unit that does not exist", + lambda r: _sub(r / "docs/CURRENT_STATE.md", "| F0001 |", "| F0404 |")), + ("current-state: open milestone drifted from the unit file", + lambda r: _sub(r / "docs/CURRENT_STATE.md", "| J2 |", "| J1 |")), + ("current-state: more units in progress than the cap", + lambda r: _append(r / "docs/CURRENT_STATE.md", + "| F0002 | J1 | `b` | nothing |\n| F0003 | J1 | `c` | nothing |\n")), + ("index: stale by one line", + lambda r: _sub(r / "docs/features/README.md", "| J2 |", "| J1 |")), + ("index: unit absent from index", + lambda r: _write(r / "docs/features/F0002.md", + "# F0002 — other\n\nStatus: Draft\nBranch: `feature/f0002-x`\n\n" + "## Milestones\n\n### J1 — only\n\nStatus: Draft\n")), + ("headings: unit id promoted to a heading in RULES.md", + lambda r: _append(r / "docs/RULES.md", "\n## Evidence F0001\n\nnarrative.\n")), + ("headings: unit id promoted to a heading in CURRENT_STATE.md", + lambda r: _append(r / "docs/CURRENT_STATE.md", "\n## Boundary F0023\n\nnarrative.\n")), + ("registries: invented severity", + lambda r: _sub(r / "docs/KNOWN_ISSUES.md", "| Medium |", "| Catastrophic |")), + ("registries: duplicate issue id", + lambda r: _append(r / "docs/KNOWN_ISSUES.md", + "| KI-001 | Low | api | dup | `x` | F0001 | Open |\n")), + ("registries: Resolved with empty evidence", + lambda r: _sub(r / "docs/KNOWN_ISSUES.md", + r"| `a \| b` -> 502 | F0001 | Open |", "| | F0001 | Resolved |")), + ("registries: promoted learning with no revalidation date", + lambda r: _sub(r / "docs/LEARNINGS.md", + "| Observed | — | 2026-12-01 |", "| Enforced | RULES.md | |")), + ("markers: new marker beyond the grandfathered count", + lambda r: _append(r / "src/app.py", "# TODO: unbounded retry\n")), + ("markers: orphan marker in a file with no grandfathered debt", + lambda r: _write(r / "src/new.py", "x = 1 # TODO: added today\n")), + ("markers: adoption file corrupted grandfathers nothing", + lambda r: (r / "docs/adoption.json").write_text("{oops", encoding="utf-8")), + ("markers: marker trailing valid code on the same line", + lambda r: _append(r / "src/app.py", "x = 1 # FIXME later\n")), + ("markers: marker citing an id that is a prefix of a real one", + lambda r: _append(r / "src/app.py", "# TODO(KI-12): not a real entry\n")), + ("baselines: file deleted", + lambda r: (r / "docs/validation-baselines.json").unlink()), + ("baselines: count replaced by prose", + lambda r: _sub(r / "docs/validation-baselines.json", '"discovered": 11', + '"discovered": "eleven"')), + ("baselines: raised_by not naming a unit", + lambda r: _sub(r / "docs/validation-baselines.json", '"raised_by": "F0001"', + '"raised_by": "maintenance"')), + ("boundaries: declared boundary with no RULES.md section", + lambda r: _append(r / "docs/ARCHITECTURE.md", + "| Authenticated boundary | anonymous becomes identified " + "| `api/` | `api/auth.py` |\n")), + ("boundaries: RULES.md section renamed out from under the boundary", + lambda r: _sub(r / "docs/RULES.md", "## Client", "## Frontend")), + ("boundaries: ARCHITECTURE.md deleted", + lambda r: (r / "docs/ARCHITECTURE.md").unlink()), +] + + +def _write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _sub(path: Path, old: str, new: str, count: int = 1) -> None: + text = path.read_text(encoding="utf-8") + if old not in text: + raise AssertionError(f"self-test fixture drift: {old!r} not in {path}") + path.write_text(text.replace(old, new, count), encoding="utf-8") + + +def _append(path: Path, extra: str) -> None: + with path.open("a", encoding="utf-8") as fh: + fh.write(extra) + + +def _build_fixture(root: Path) -> None: + for rel, content in FIXTURE.items(): + _write(root / rel, content) + check_index(root, load_limits(root), Failures(), fix=True) + + +def run_self_test() -> int: + import shutil + problems = [] + with tempfile.TemporaryDirectory() as tmp: + base = Path(tmp) / "base" + base.mkdir() + _build_fixture(base) + limits = load_limits(base) + + f = Failures() + for fn in CHECKS.values(): + fn(base, limits, f) + if f: + problems.append("clean fixture must pass, but reported:\n " + + "\n ".join(f)) + + for i, (label, mutate) in enumerate(MUTATIONS): + work = Path(tmp) / f"m{i}" + shutil.copytree(base, work) + mutate(work) + f = Failures() + for fn in CHECKS.values(): + fn(work, limits, f) + if not f: + problems.append(f"mutation NOT caught: {label}") + continue + # A mutation caught only by an unrelated check proves nothing about + # the check it was written for. + expected = label.split(":", 1)[0] + if not any(line.startswith(f"[{expected}]") for line in f): + problems.append( + f"mutation caught by the WRONG check: {label}\n" + f" expected [{expected}], got: {f[0]}") + + if problems: + print("SELF-TEST FAILED", file=sys.stderr) + for p in problems: + print(" " + p, file=sys.stderr) + return 1 + print(f"self-test ok — clean fixture passes, " + f"{len(MUTATIONS)} mutations each caught by their own check") + return 0 + + +# --------------------------------------------------------------------------- # + +def record_baseline(root: Path, limits: dict, spec: str, raised_by: str) -> int: + if "=" not in spec or not UNIT_ID.fullmatch(raised_by or ""): + print("FAIL --record-baseline needs NAME=COUNT and --raised-by FNNNN", + file=sys.stderr) + return 2 + name, _, count = spec.partition("=") + if not count.isdigit(): + print(f"FAIL COUNT must be a non-negative integer, got {count!r}", file=sys.stderr) + return 2 + p = root / limits["baselines_file"] + data = json.loads(read(p)) if p.is_file() else {} + prev = data.get(name, {}).get("discovered") + if isinstance(prev, int) and int(count) < prev: + print(f"FAIL {name}: {count} < existing baseline {prev}. A baseline never " + "drops — something stopped running.", file=sys.stderr) + return 1 + data[name] = {"discovered": int(count), "raised_by": raised_by} + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"baseline {name} = {count} (raised by {raised_by})") + return 0 + + +def adopt(root: Path, limits: dict) -> int: + """Freeze the marker debt an existing repository already carries.""" + unregistered, bad, _ = scan_markers(root, limits) + counts = {rel: len(lines) for rel, lines in sorted(unregistered.items())} + p = root / limits["adoption_file"] + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps( + {"_why": "Marker debt present when the kit was adopted. This ratchet only " + "tightens: re-run --adopt after a cleanup unit to lower it. Never " + "raise a count by hand — that is how the check stops checking.", + "markers": counts}, indent=2) + "\n", encoding="utf-8") + total = sum(counts.values()) + print(f"adopted: {total} existing marker(s) across {len(counts)} file(s) frozen in " + f"{limits['adoption_file']}") + if bad: + print(f" note: {len(bad)} marker(s) cite a KI-/LC- id that does not exist. " + "Those are never grandfathered — fix or remove them.") + for rel, n in list(counts.items())[:5]: + print(f" {n:4} {rel}") + if len(counts) > 5: + print(f" …and {len(counts) - 5} more file(s)") + return 0 + + +def main(argv: list[str]) -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--root", default=".", help="repository root") + ap.add_argument("--all", action="store_true") + for name in CHECKS: + ap.add_argument(f"--{name}", action="store_true") + ap.add_argument("--strict-placeholders", action="store_true", + help="fail on unfilled <...> placeholders (run after install)") + ap.add_argument("--fix-index", action="store_true", + help="regenerate docs/features/README.md") + ap.add_argument("--adopt", action="store_true", + help="freeze the marker debt an existing repo already carries") + ap.add_argument("--record-baseline", metavar="NAME=COUNT") + ap.add_argument("--raised-by", metavar="FNNNN", default="") + ap.add_argument("--self-test", action="store_true", + help="mutate a fixture tree and assert every mutation is caught") + try: + args = ap.parse_args(argv) + except SystemExit: + return 2 # unknown argument: fail closed + + if args.self_test: + return run_self_test() + + root = Path(args.root).resolve() + if not root.is_dir(): + print(f"FAIL [config] root {root} is not a directory", file=sys.stderr) + return 2 + limits = load_limits(root) + + if args.adopt: + return adopt(root, limits) + if args.record_baseline: + return record_baseline(root, limits, args.record_baseline, args.raised_by) + if args.fix_index: + check_index(root, limits, Failures(), fix=True) + print("regenerated docs/features/README.md") + return 0 + + selected = [n for n in CHECKS if getattr(args, n.replace("-", "_"))] + if args.all or not selected: + selected = list(CHECKS) + + f = Failures() + for name in selected: + CHECKS[name](root, limits, f) + if args.strict_placeholders: + check_placeholders(root, limits, f) + + if f: + print(f"FAILED — {len(f)} finding(s)", file=sys.stderr) + for line in f: + print(" " + line, file=sys.stderr) + return 1 + print(f"ok — {len(selected)} check group(s) passed on {root}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/poseidon/checks/limits.json b/poseidon/checks/limits.json new file mode 100644 index 0000000..1cf9a45 --- /dev/null +++ b/poseidon/checks/limits.json @@ -0,0 +1,144 @@ +{ + "line_caps": { + "AGENTS.md": 115, + "docs/CURRENT_STATE.md": 200, + "docs/WORKFLOW.md": 165, + "docs/RULES.md": 300, + "docs/VALIDATION.md": 130, + "poseidon/templates/FEATURE.md": 105, + "poseidon/templates/ADR.md": 45, + "docs/PRODUCT.md": 120, + "docs/ARCHITECTURE.md": 150, + "docs/SETUP.md": 100 + }, + "zone_agents_cap": 50, + "unit_states": [ + "Draft", + "Ready", + "In progress", + "Review", + "Verify", + "Done", + "Blocked", + "Rejected", + "Superseded" + ], + "unit_states_active": [ + "In progress", + "Review", + "Verify" + ], + "unit_states_closed": [ + "Done", + "Rejected", + "Superseded" + ], + "milestone_states": [ + "Draft", + "In progress", + "Review", + "Verify", + "Verified", + "Done", + "Blocked" + ], + "max_units_in_progress": 2, + "issue_severities": [ + "Critical", + "High", + "Medium", + "Low" + ], + "issue_statuses": [ + "Open", + "Accepted", + "Scheduled", + "Resolved", + "Invalid" + ], + "learning_states": [ + "Observed", + "Reproduced", + "Codified", + "Enforced", + "Stale" + ], + "learning_states_needing_revalidation": [ + "Codified", + "Enforced" + ], + "no_unit_id_in_headings": [ + "AGENTS.md", + "docs/RULES.md", + "docs/VALIDATION.md", + "docs/CURRENT_STATE.md", + "docs/WORKFLOW.md", + "docs/ARCHITECTURE.md", + "docs/PRODUCT.md", + "docs/SETUP.md" + ], + "baselines_file": "docs/validation-baselines.json", + "code_extensions": [ + ".py", + ".js", + ".jsx", + ".ts", + ".tsx", + ".go", + ".rs", + ".java", + ".kt", + ".rb", + ".php", + ".c", + ".h", + ".cc", + ".cpp", + ".hpp", + ".cs", + ".swift", + ".sh", + ".sql", + ".yml", + ".yaml", + ".toml" + ], + "marker_words": [ + "TODO", + "FIXME", + "HACK", + "XXX" + ], + "ignored_dirs": [ + ".git", + "node_modules", + "vendor", + "dist", + "build", + "target", + ".venv", + "venv", + "__pycache__", + ".next", + ".cache", + "docs/archive", + "poseidon", + ".claude" + ], + "placeholder_targets": [ + "AGENTS.md", + "docs/CURRENT_STATE.md", + "docs/WORKFLOW.md", + "docs/RULES.md", + "docs/VALIDATION.md", + "docs/KNOWN_ISSUES.md", + "docs/LEARNINGS.md", + "docs/PRODUCT.md", + "docs/ARCHITECTURE.md", + "docs/SETUP.md" + ], + "placeholder_exempt": [ + "poseidon/" + ], + "adoption_file": "docs/adoption.json" +} diff --git a/poseidon/commands/bootstrap.md b/poseidon/commands/bootstrap.md new file mode 100644 index 0000000..bcb18ff --- /dev/null +++ b/poseidon/commands/bootstrap.md @@ -0,0 +1,161 @@ +--- +description: Fill the kit's placeholders for this repository — by auditing the codebase for what it can prove, and asking for what it cannot. +argument-hint: [--audit-only] to report findings without writing +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +--- + +Turn a freshly installed kit into this repository's kit. Run once. Until it has +run, every other command is reading `<placeholders>` as if they were rules. + +**The one rule of this command: derive, or ask. Never invent.** +A codebase proves what it does. It cannot prove who it is for, which trade-off +wins, or why a version is pinned. Writing a plausible answer where you should +have asked is the most expensive thing you can do here — it will be read as +established fact by every session that follows, and nothing will ever re-check +it. + +## 0. Merge what the installer refused to overwrite + +`/install` never clobbers; it reports what it left in place. If any of those are +still unmerged, handle them **first** — until `AGENTS.md` carries POSEIDON's +content, the router never sees `<PROJECT>` and POSEIDON is inert. + +For each: read both versions and merge **into** POSEIDON's structure. The repo's +existing rules are real knowledge; POSEIDON's structure is what makes them +findable. Keep every existing rule, place it under POSEIDON's headings, delete +nothing without saying so. + +Show the user what you kept, moved, and dropped before writing. + +## 1. Detect the mode + +``` +git log --oneline | wc -l +ls +``` + +- **Greenfield** — no source files, or only scaffolding. Almost everything is + asked. Say so, and go to step 3. +- **Existing** — real code and real history. Most of `ARCHITECTURE.md` and + `SETUP.md` is derivable. Do step 2 first. + +Announce which mode, and on what evidence. + +## 2. Audit — existing codebases only + +Read, in this order, and note the file each finding came from: + +1. **Manifests and lockfiles** — `package.json`, `pyproject.toml`, `go.mod`, + `Cargo.toml`, `pom.xml`, `Gemfile`, `composer.json`. Gives: runtime, + versions, scripts, test runner, entry points. +2. **Task runner and CI** — `Makefile`, `justfile`, `Taskfile`, `.github/workflows/`, + `.gitlab-ci.yml`. **This is where the real commands live.** `SETUP.md` and + `VALIDATION.md` are copied from here, not composed. +3. **Top-level structure** — directory names and their sizes. Where is the bulk + of the code? +4. **Boundaries** — the whole point. Look for: schema or migration directories; + anything reading a token, a session, or a header; anything serving HTTP or + RPC; anything rendering or running on a client; anything publishing an + artifact. A boundary is a place where **trust changes**, not a folder. +5. **Tests** — where they live, how they are run, how many are discovered. +6. **Configuration** — committed example env files, config schemas. Never open + a real secret file; if you find one committed, stop and report it as a + `Critical` entry in `KNOWN_ISSUES.md`. + +Then report the audit before writing anything: the boundaries found, the real +commands found, and — most importantly — **what you could not determine**. That +last list becomes half your questions. + +## 3. Ask — one batch, then wait + +Everything that follows is unanswerable from code. Ask all of it in **a single +message**, numbered, with your best guess offered where you have one so the +answer can be "yes" — never as a default you proceed on. + +Always ask: + +1. One sentence: what is this, for whom, on which platform? +2. The four priorities, in order — the tiebreaker for every later arbitration. +3. What is this deliberately **not**? (Each answer prevents a class of feature + request.) +4. Non-technical constraints: regulation, contract, deadline, platform policy. +5. Which user's failure costs data, money, or trust? (This sets review depth.) +6. Who is `<Owner>` — who reviews and merges? +7. The version scheme, and its single source. + +Ask, in existing repos, only what the audit left open: + +8. Confirm or correct the boundary list. (Show yours; ask what is missing or + wrong. This is the highest-value question in the batch — it becomes the + section list of `RULES.md` and the row list of `VALIDATION.md`.) +9. Which incidents have actually happened? Each one becomes a line in + `RULES.md`. Do **not** invent preventive rules; they are never applied, and + they spend the cap that keeps the file readable. +10. Which capabilities are genuinely **shipped and merged**, with what evidence? +11. Anything in the code that looks wrong but is deliberate. + +If an answer does not come, write `<unanswered — asked YYYY-MM-DD>` in that +exact form and continue. Do not substitute a guess. `--strict-placeholders` will +keep surfacing it, which is the correct outcome. + +## 4. Write, in this order + +The order matters: each file feeds the next. + +1. **`docs/ARCHITECTURE.md`** — the `Boundaries` table first. Every row names + the file it was read from. +2. **`docs/RULES.md`** — one `##` section per boundary, named **identically**. + A check enforces the correspondence. Fill lines only from real incidents + (question 9); leave a boundary's table empty rather than inventing rows. +3. **`docs/VALIDATION.md`** — one row per boundary, commands copied from the + task runner and CI. Where no command exists, write `manual` — that is a + visible debt, not a gap to paper over. +4. **`docs/SETUP.md`** — from the manifests and CI, with sources named. +5. **`docs/PRODUCT.md`** — from the answers only. +6. **`AGENTS.md`** — replace every placeholder. Invariants are written as + **refusals**, each verifiable by a check or by reading a diff. Derive them + from the boundaries and the incidents, not from good intentions. +7. **`docs/CURRENT_STATE.md`** — the **proven** present. A capability enters + only if merged and evidenced (question 10). On an existing repo this step + almost always reveals capabilities believed shipped that are not. Say so + plainly; it is the most useful output of this whole command. + +Propose — do not create — `<zone>/AGENTS.md` for the 3–6 most-worked zones, and +name the evidence for each (commit frequency, size, boundary crossed). Wait for +confirmation: a zone that does not qualify is 50 lines every session pays for. + +## 5. Existing repositories — seed the ratchets + +Run once, before wiring CI: + +``` +python3 poseidon/checks/check.py --adopt # freeze existing TODO/FIXME debt +python3 poseidon/checks/check.py --record-baseline <suite>=<n> --raised-by <ID> +``` + +No `python3`? Say so, record `checks: agent mode` under Steering in +`CURRENT_STATE.md`, and follow `poseidon/references/checks-by-hand.md`. Without +`--adopt` you cannot tell new marker debt from old — state that limitation +rather than reporting a clean scan. + +`--adopt` records how much marker debt each file already carries, so the checks +block **new** debt without demanding the old debt be cleared first. Report the +totals to the user — that number is a real measurement of the repo, and it is +often the first time anyone has seen it. Re-run `--adopt` after a cleanup unit; +it only ever tightens. + +For each test suite, record the count it **actually discovers today**. A baseline +recorded from a suite that was already broken locks in the breakage — run each +suite and read the real number first. + +## 6. Verify + +``` +python3 poseidon/checks/check.py --all --strict-placeholders +``` + +Must exit `0`, except for entries you deliberately left as +`<unanswered — asked …>`. List those explicitly in the report; each one is a +real question waiting on `<Owner>`, not a formatting artifact. + +Then `Next: /unit-new` — the repository is ready for its first unit. diff --git a/poseidon/commands/check.md b/poseidon/commands/check.md new file mode 100644 index 0000000..37646b8 --- /dev/null +++ b/poseidon/commands/check.md @@ -0,0 +1,42 @@ +--- +description: Run the method's automated checks and explain any failure in terms of the rule it protects. +argument-hint: [--all | --caps | --tracking | --registries | --strict-placeholders] +allowed-tools: Bash(python:*), Bash(python3:*), Read, Edit, Glob, Grep +--- + +Run `python3 poseidon/checks/check.py $ARGUMENTS` (default `--all`). + +If `python3` is unavailable, do **not** silently skip: switch to agent mode, +follow `poseidon/references/checks-by-hand.md`, and label the result +`checks: agent mode — not deterministic, no CI gate`. + +For each failure, report three things: the check that fired, the exact file and +line, and **the rule it protects**. A failure is never a warning to acknowledge — +it blocks integration. + +## Fixing — the only legitimate direction + +| Failure | The fix | +| --- | --- | +| Line cap exceeded | Remove or condense content. **Never raise the cap.** A new global rule replaces or condenses an existing one. | +| Index out of date | `check.py --fix-index`. Never hand-edit the generated index. | +| Unit ID in a heading | Move the identifier into the `Proven by` / `Promoted from` column and delete the narrative. | +| Status invalid, duplicated, or missing | One `Status` field per unit and per milestone, from the closed list. | +| Milestone sequence broken | Milestones are numbered `J1`, `J2`, … with no gaps. | +| Phantom dependency | The cited unit does not exist — fix the reference, do not delete the dependency. | +| Milestone closed out of order | A later milestone is `Done` while an earlier one is not. Milestones close in sequence; reopen the later one. | +| Unit `Done` with a milestone open | The unit closes only when every milestone has. | +| Unit active with every milestone `Done` | Run `/close`; the unit is finished and nobody said so. | +| `CURRENT_STATE.md` drifted | The `Units in progress` table disagrees with the unit files. The **unit file wins** — fix the table. | +| Boundary with no `RULES.md` section | `ARCHITECTURE.md` declares a boundary that `RULES.md` does not cover. Add the section (empty is fine), or drop the boundary — it was never one. | +| Registry entry malformed | Closed lists for severity and status; evidence is mandatory on anything `Resolved`. | +| Orphan `TODO`/`FIXME`/`HACK`/`XXX` | Open a `KNOWN_ISSUES.md` entry, or delete the marker. | +| Baselines file malformed or missing | Counters live in data, not prose. Fix the shape; `--record-baseline NAME=N --raised-by FNNNN` writes it. Never lower a baseline — a baseline rises only in the unit that adds the tests. | + +If a check itself is wrong, that is a unit of work — with a negative mutation +proving the fix. Never disable a check to get green. + +Also run `python3 poseidon/checks/check.py --self-test` after touching the checks +themselves. It mutates a fixture tree and asserts each mutation is caught. A +check with no negative mutation may have stopped checking anything while staying +green. diff --git a/poseidon/commands/close.md b/poseidon/commands/close.md new file mode 100644 index 0000000..d6c5b45 --- /dev/null +++ b/poseidon/commands/close.md @@ -0,0 +1,70 @@ +--- +description: Close a milestone — or the whole unit — and hand off to the Owner. +argument-hint: <unit id> [milestone] +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +--- + +Close $ARGUMENTS. + +The code for this milestone is already committed and pushed by `/milestone`. +`/close` commits **documentation only**. If you find yourself changing code here, +you are in the wrong command. + +## 1. Completion Report + +Complete the block for this milestone — `/milestone` and `/review` and `/verify` +each filled part of it. Confirm every line is present: result obtained, files +changed, commands and **real** results, `commit:`, manual verification, review +findings addressed. + +This block is the project's evidence. Rules documents **cite** it; they never +restate it. If you are about to paste a narrative into `RULES.md`, stop — write +a citation in the `Proven by` column instead. + +## 2. State + +- `docs/CURRENT_STATE.md` — only if the **proven** reality changed. A capability + enters only when merged into the main branch, with its evidence. An entry that + is no longer true is **replaced**, not appended. Never list a commit, a branch, + or an unmerged Pull Request as a delivered capability. +- `docs/KNOWN_ISSUES.md` — deferred findings and debt created, each with + evidence, severity, and a target unit. +- `docs/LEARNINGS.md` — capture a first occurrence immediately. Promote only on + two independent occurrences or one deterministic reproduction. Route to the + **narrowest** destination that works; writing higher than necessary is the + most common way documentation becomes unreadable. Where a deterministic check + is possible, open a bounded unit for it instead of keeping a manual + instruction — a manual instruction is a debt, not an asset. + +## 3. Checks + +``` +python3 poseidon/checks/check.py --all +``` + +Must exit `0`. If it does not, fix the cause; never widen a cap to pass. + +No `python3`? Agent mode — `poseidon/references/checks-by-hand.md` — and the +report says so. Never write `checks passed` for checks you could not run. + +## 4. Set the states — in this order + +1. Milestone `Status: Verified` → `Done`. +2. Commit the documentation changes (explicit path list, re-read + `git diff --cached`), push, and update the Pull Request. +3. **Milestones remain** → the next one is now the open milestone; update the + `Units in progress` row. `Next: /milestone`. +4. **That was the last milestone** → fill the unit `Summary`, set the **unit** + `Status: Done`, remove its row from `Units in progress`, add the capability + to `Capabilities shipped` only once `<Owner>` has merged — not before — and + hand the Pull Request over. + +A unit left at `In progress` with every milestone `Done` fails the tracking +check. So does a unit set to `Done` with a milestone still open. Both are caught +by `check.py`; run it again after this step. + +**`<Owner>` alone reviews and merges. Never merge.** + +## 5. Hand off + +End with `Next` and a resume prompt — run `/handoff`. diff --git a/poseidon/commands/handoff.md b/poseidon/commands/handoff.md new file mode 100644 index 0000000..40e4cbd --- /dev/null +++ b/poseidon/commands/handoff.md @@ -0,0 +1,37 @@ +--- +description: End the session — produce the report, the next action, and a resume prompt for a fresh session. +allowed-tools: Read, Bash(git status:*), Bash(git log:*), Bash(git branch:*), Glob +--- + +Close out this session. Do not start new work. + +## Report + +- **Status** — the state, per the router. +- **Files changed** — from `git status`, not from memory. +- **Commands and real results** — `passed` / `failed` / `not run` / + `blocked by environment`. Never report a check you did not run. +- **Checks mode** — `script` or `agent mode — not deterministic, no CI gate`. +- **Risks and limits** — including anything a substitute validation does not + prove. +- **Branch, commit, Pull Request** — real values, read from Git. Never invented. + +## Next + +One line: the next concrete action, who owns it, and what unblocks it. + +## Resume prompt + +Emit this verbatim, filled. Pointers and state only — never document contents: + +``` +Resume <PROJECT>. Read AGENTS.md, docs/CURRENT_STATE.md, then +<unit file> (header + milestone <N> + Allowed areas / Do not touch). +Branch <name>, worktree <path>, last commit <sha>, PR <url or none>. +Done: <one line>. Remaining: <the next concrete action>. +Validations passed: <command -> result>. To replay: <commands>. +Traps: <what cost time, or what can break again>. +Decide alone per AGENTS.md; do not merge. +``` + +A report without both `Next` and a resume prompt is not finished. diff --git a/poseidon/commands/install.md b/poseidon/commands/install.md new file mode 100644 index 0000000..5a948e7 --- /dev/null +++ b/poseidon/commands/install.md @@ -0,0 +1,89 @@ +--- +description: Install POSEIDON into this repository — done by you, with your own tools. No shell script, no build step. +argument-hint: [--force] to overwrite files that already exist +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +--- + +Install POSEIDON here. **You do this yourself, with Read/Write/Glob/Grep.** There +is no installer to run: a shell script cannot merge an existing `AGENTS.md` with +judgement, and it fails outright on Windows without a POSIX shell. You can do +both. + +Run this only when `AGENTS.md` or `docs/` is missing. If they exist and still +contain `<PROJECT>`, the install is done — run `/bootstrap` instead. + +## 1. Report the ground you are standing on + +``` +poseidon/VERSION +``` + +State the version, the OS, and whether `python3 --version` succeeds. That last +one decides the checks mode for every session after this — see step 5. + +## 2. Copy the project's own files + +Never overwrite. For each, if the destination exists, **leave it** and note it +for step 4. + +| From | To | +| --- | --- | +| `poseidon/skeletons/AGENTS.md` | `AGENTS.md` | +| `poseidon/skeletons/docs/*.md` | `docs/` | + +Create empty: `docs/features/`, `docs/decisions/`. +Create `docs/validation-baselines.json` containing `{}`. + +## 3. Wire the trigger + +`.claude/skills/poseidon/SKILL.md` and `.claude/commands/poseidon.md` shipped in +the archive — confirm both exist; that is what makes "lance poseidon" fire. + +Then, for ergonomics only, create a stub in `.claude/commands/` for each file in +`poseidon/commands/`: + +``` +--- +description: POSEIDON /<name> — see poseidon/commands/<name>.md +--- + +Read `poseidon/commands/<name>.md` and follow it exactly, with $ARGUMENTS as its +arguments. If the file is missing, say so and stop. +``` + +**Check for a name collision first.** If `.claude/commands/review.md` already +exists and is not a POSEIDON stub, do not touch it — name yours +`poseidon-review.md` and say so. These stubs are convenience, not mechanism: the +router works by reading `poseidon/commands/<name>.md` directly. + +`CLAUDE.md`: if absent, write `@AGENTS.md` and the trigger line. If present and +it does not mention `poseidon/POSEIDON.md`, append both — never rewrite it. + +## 4. Merge, do not clobber + +For every file you left in place at step 2, read both versions and merge **into** +POSEIDON's structure: keep every existing rule, place it under POSEIDON's +headings, delete nothing without saying so. Show the user what you kept, moved, +and dropped before writing. + +`AGENTS.md` matters most. Until it carries POSEIDON's content the router never +sees `<PROJECT>`, never routes to `/bootstrap`, and POSEIDON does nothing at all +— silently. Do not finish this command with that unresolved. + +## 5. Freeze what the repo already carries + +If the repo has history (`git log -1` succeeds), it has `TODO`/`FIXME` debt. +Failing all of it on day one turns CI red and someone switches the checks off, +which costs more than the debt did. + +- **python3 available** → `python3 poseidon/checks/check.py --adopt`, then + `--fix-index`, then `--all`. Report the totals. +- **python3 absent** → say so plainly, and record in `docs/CURRENT_STATE.md` + under Steering: `checks: agent mode — not deterministic, no CI gate`. Then + follow `poseidon/references/checks-by-hand.md`. Do not pretend the gap is not + there; a check nobody can run in CI is not a check. + +## 6. Report + +Version, files written, files left for merge, checks mode, and what is still +`<placeholder>`. Then `Next: /bootstrap`. diff --git a/poseidon/commands/locate.md b/poseidon/commands/locate.md new file mode 100644 index 0000000..cc6a4a9 --- /dev/null +++ b/poseidon/commands/locate.md @@ -0,0 +1,30 @@ +--- +description: Determine the current state of the repository and route to the one action that applies. +allowed-tools: Bash(git status:*), Bash(git branch:*), Read, Glob, Grep +--- + +Run Step 0 of `poseidon/POSEIDON.md`. Read nothing beyond what it names. + +1. `git status --porcelain=v1 -b` +2. Read `AGENTS.md`, then `docs/CURRENT_STATE.md`. +3. Identify the active unit: + - branch matches `feature/<id>-*` → that unit; + - else the `Units in progress` table holds exactly one row → that unit; + - else the table holds two rows → report `state=AMBIGUOUS`, list both, and + ask which one. Do not pick. + - else → no unit. +4. If a unit exists, read its header `Status:`, `Allowed areas`, `Do not touch`, + and the first milestone whose `Status:` is not `Done`. + +Output exactly this, and nothing more: + +``` +state=<STATE> unit=<ID|none> milestone=<Jn|none> unit-status=<S> milestone-status=<S> branch=<name> dirty=<yes|no> +→ next: /<command> +``` + +Then stop. Do not start the routed command's work in the same turn unless asked. + +If the branch, `CURRENT_STATE.md`, and the unit file disagree, or if no router +row matches the two `Status:` values, report `state=CONFLICT`, quote the three +sources, and stop. diff --git a/poseidon/commands/milestone.md b/poseidon/commands/milestone.md new file mode 100644 index 0000000..bc7c6e4 --- /dev/null +++ b/poseidon/commands/milestone.md @@ -0,0 +1,74 @@ +--- +description: Open or resume the current milestone — implement it, validate it, commit it, push it. +argument-hint: [unit id] — defaults to the active unit +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +--- + +Work the **open milestone** — the first one whose `Status` is not `Done` — of +unit $ARGUMENTS (default: the active unit). One milestone per invocation. Never +open the next one here. + +## 1. Load, and only this + +- the unit header, the open milestone block, `Allowed areas`, `Do not touch`; +- `<zone>/AGENTS.md` for each zone the milestone touches; +- the **one section** of `docs/RULES.md` for this milestone's primary boundary; +- the **one row** of `docs/VALIDATION.md` for the area you are about to touch. + +Announce the boundary and the paths you are allowed to write. That list is +binding for the rest of this command. + +## 2. Branch and state + +Report the current branch **immediately before touching anything** — a shared +worktree may have moved. Preserve existing uncommitted work; never discard it. +Create the unit branch, or join it if it already exists. Never recreate an +active branch. + +If the unit `Status:` is `Ready`, set it to `In progress` now, and add its row to +the `Units in progress` table of `docs/CURRENT_STATE.md`. The router keys off +that field; leaving it at `Ready` makes the next session re-enter this command. +Set the open milestone `Status:` to `In progress`. + +## 3. Implement + +Read the real code before writing any. Implement this milestone only. + +A real defect found outside `Allowed areas` goes into `docs/KNOWN_ISSUES.md` +with its evidence and severity, and is **not fixed**. This is the rule that +protects the scope; breaking it is how a two-day unit becomes a two-week one. + +On repeated failure, apply `poseidon/references/recovery.md` — three strikes, then revert +and halt. + +## 4. Validate + +Targeted tests for the zone first, then the boundary checks, in the order +`VALIDATION.md` gives. Record each command with its **real output**. Use +`passed` / `failed` / `not run` / `blocked by environment` — never a substitute. +A suite exiting `0` with no tests discovered is `failed`, not `passed`. + +## 5. Commit and push + +- Stage by **explicit path list**. Never `git add .`, never `git add -A`. +- Read `git diff --cached` before committing. Files modified outside the unit are + listed separately in the report and left unstaged. +- Commit, push, open or update the Pull Request as **draft** while validations or + dependencies are pending. +- Never force-push. Never bypass a protection. Never invent a branch, a base, or + a CI result. **Never merge.** + +## 6. Record + +Fill this milestone's Completion Report block: result obtained, files changed, +commands and real results, and — required — `commit:` with the **pushed SHA**. +`/review` computes the diff under review from the previous milestone's `commit:`; +without it the reviewer has to guess what to look at. + +Leave `manual verification:` and `review findings addressed:` empty — `/verify` +and `/review` fill them. + +Set the milestone `Status: Review`. Do not touch the unit `Status:`; it stays +`In progress` until `/close`. + +Then `Next: /review <ID> <Jn>`. diff --git a/poseidon/commands/review.md b/poseidon/commands/review.md new file mode 100644 index 0000000..1e10e2a --- /dev/null +++ b/poseidon/commands/review.md @@ -0,0 +1,52 @@ +--- +description: Adversarially review the pushed diff of one milestone, before the next one opens. +argument-hint: <unit id> <milestone, e.g. J2> +allowed-tools: Read, Glob, Grep, Bash(git diff:*), Bash(git log:*), Bash(git show:*), Task +--- + +Review milestone $ARGUMENTS on its **pushed diff**, following +`poseidon/references/review.md`. + +You did not write this diff. If you did write it in this same session, say so and +delegate the review to a sub-agent with the brief in `poseidon/references/delegation.md`. + +## Inputs — nothing else + +`<base>` is the previous milestone's `commit:` from the Completion Report. For +`J1` it is `git merge-base origin/<main branch> HEAD`. Never review against +`HEAD~1`; a milestone may be several commits. + +``` +git log --oneline <base>..HEAD +git diff <base>..HEAD -- <the milestone's paths> +``` + +Plus: the unit header, the milestone block, `Allowed areas`, `Do not touch`, and +the `RULES.md` section for the milestone's primary boundary. The milestone's +`Risk` and `Security-sensitive` values set the depth — a sensitive-data change +and a read-only panel do not earn the same attention. + +## Order — report every band, but band 1 is blocking on its own + +1. security and data loss +2. conformance to the stated result, and **any path touched outside + `Allowed areas`** — always blocking +3. regressions and compatibility for every changed contract +4. architecture and debt created +5. tests and observability — would a deliberately broken behaviour fail a test? + is the discovered-test count at or above baseline? +6. readability and performance + +Never request an aesthetic rewrite with no measurable benefit. + +## Verdict — exactly one + +- `pass` +- `pass with follow-ups` — each follow-up written into `docs/KNOWN_ISSUES.md` + with evidence, severity, and target, before you say this +- `return to In progress` — set the **milestone** `Status: In progress` (the unit + stays `In progress` throughout) and name the fixes + +Record the findings you addressed in the milestone's Completion Report. + +On `pass`, set the milestone `Status: Verify`. Then `Next: /verify <ID> <Jn>`. diff --git a/poseidon/commands/unit-new.md b/poseidon/commands/unit-new.md new file mode 100644 index 0000000..c754e0e --- /dev/null +++ b/poseidon/commands/unit-new.md @@ -0,0 +1,60 @@ +--- +description: Create a new unit of work — one user capability, one file, one branch, one Pull Request. +argument-hint: <one sentence describing the user-visible outcome> +allowed-tools: Read, Write, Edit, Glob, Grep, Bash(git status:*), Bash(git branch:*), Bash(ls:*) +--- + +Create one unit of work for: **$ARGUMENTS** + +Refuse and stop if there are already **two** units `In progress`. Say which ones. + +## 1. Check it is one unit + +A unit is one user capability, stated without an "and". If the goal needs an +"and" between two distinct capabilities, that is two units — say so and ask which +one to open. Governance, a fix, tooling, or a check use the **same file with a +single milestone**; there is no second format. + +## 2. Allocate + +- Next free ID by scanning `docs/features/` — `F0001`, `F0002`, … +- Slug from the goal. Branch name `feature/<id>-<slug>`. Do **not** create the + branch yet; `/milestone` does that. + +## 3. Write `docs/features/<ID>.md` + +Copy `poseidon/templates/FEATURE.md`. Read `poseidon/templates/EXAMPLE-FEATURE.md` once +for the shape of a well-formed unit — copy its shape, never its content. + +If this unit changes the architecture or a trust boundary, it needs a written +decision **first**: `poseidon/templates/ADR.md` → `docs/decisions/ADR-NNNN-<slug>.md`, +and list it under `Dependencies`. A unit that assumes an unwritten decision +cannot reach `Ready`. + +Fill, in this order: + +1. `Goal` — one observable user outcome. +2. `Context` — what is **actually present** on the main branch. Verify by + reading the code, not `CURRENT_STATE.md`. +3. `Dependencies` — name any missing `<Owner>` decision, hardware, or human + verification explicitly. That is what keeps a unit out of `Ready`. +4. `Allowed areas` / `Do not touch` — the union across all milestones. Derive + these from the code you just read; a guess here is the most expensive error + in the file. +5. `Milestones` — 2 to 5, ordered most-constraining first: data and authority + before boundary, boundary before client composition. One milestone = one + commit = one primary boundary = one review. Each carries its observable + result, boundary, validations, review focus, and a 5–10 minute manual check. +6. `Acceptance criteria` — observable over the whole capability. + +Leave `Status: Draft`. Leave the Completion Report empty. + +## 4. Register + +Add one row to the `Units in progress` table of `docs/CURRENT_STATE.md`. +Do **not** hand-edit any generated index. + +## 5. Report + +State the ID, the goal in one line, the milestone count, and the single riskiest +assumption you made. Then `Next: /unit-ready <ID>`. diff --git a/poseidon/commands/unit-ready.md b/poseidon/commands/unit-ready.md new file mode 100644 index 0000000..b89c9ca --- /dev/null +++ b/poseidon/commands/unit-ready.md @@ -0,0 +1,36 @@ +--- +description: Adversarially challenge a Draft unit and promote it to Ready — before any code is written. +argument-hint: <unit id, e.g. F0001> +allowed-tools: Read, Edit, Glob, Grep, Bash(git log:*), Bash(ls:*) +--- + +Challenge unit **$ARGUMENTS** before any code exists. Act as the reviewer, not +as the author: your job is to find the reason this unit is not ready. + +Read the unit file in full. Read `docs/WORKFLOW.md` §2. Read nothing else yet. + +## Send back to Draft if any of these holds + +- The `Goal` contains an "and" joining two distinct capabilities. +- A dependency is listed but **not actually satisfied** — verify each one in the + code or in `CURRENT_STATE.md`; do not take the file's word for it. +- A milestone has no observable result, or its result is a code change rather + than a behaviour ("refactor X" is not a result). +- **A milestone cannot be committed on its own.** Then it is not a milestone; it + belongs to the previous one. This is the most common defect — check every one. +- A milestone touches more than one primary boundary. +- A milestone has no exact validation command, or no 5–10 minute manual check. +- `Allowed areas` does not cover the paths the milestones obviously need, or + overlaps `Do not touch`. +- More than 5 milestones, or two goals → split into two units. +- `Security-sensitive: Yes` anywhere, and the `Review` section is empty. + +For each finding: quote the offending line, state the observable consequence, +and give the correction. Do not rewrite the unit silently. + +## If nothing holds + +Set `Status: Ready`. Confirm in one line what a reader would be able to do once +this unit is `Done`, and name the milestone that carries the most risk. + +Then `Next: /milestone <ID>`. diff --git a/poseidon/commands/verify.md b/poseidon/commands/verify.md new file mode 100644 index 0000000..ab56991 --- /dev/null +++ b/poseidon/commands/verify.md @@ -0,0 +1,55 @@ +--- +description: Run the milestone's manual verification and replay its validations after the last change. +argument-hint: <unit id> <milestone, e.g. J2> +allowed-tools: Read, Edit, Bash, Glob, Grep +--- + +Verify milestone $ARGUMENTS. + +## 1. Replay + +Re-run the milestone's validations **after the last modification**, in the order +`docs/VALIDATION.md` gives for the touched area. A result obtained before the +last change is not evidence. A result obtained inside a sub-task is not evidence. + +Record each as `passed` / `failed` / `not run` / `blocked by environment`, with +the exact command and its real output. + +Compare each suite's **discovered count** against `docs/validation-baselines.json` +before concluding. Below baseline is `failed`, whatever the exit code — something +stopped running. If this milestone adds tests, raise the baseline here and only +here: `python3 poseidon/checks/check.py --record-baseline <suite>=<n> --raised-by <ID>`. +The tool refuses a value below the existing one. + +## 2. Manual verification + +Run the milestone's 5–10 minute manual check as written. Report what you +actually observed, not what the check expects to happen. + +- It fails → set the **milestone** `Status: In progress` and name the exact + failing step. The unit `Status:` stays `In progress` throughout. +- It cannot be run here → the milestone **stays at `Verify`**. Name the check, why it + is unavailable, who must run it and on which environment, what was done + instead, and what that substitute **does not prove**. + +A local proof never replaces a real device, a remote scheduler, or a real user. + +## 3. Depth + +`Maximum evidence` for anything touching money, data, server authority, or +security: full checks, negative mutations, real runtime, dedicated manual check. +`Fast` for client composition: types, tests, build — and the manual check is the +real journey through the application, not a separate checklist. + +The **boundary touched** decides the depth, never the size of the diff. + +## 4. Hand to close + +Write what you observed into the milestone's `manual verification:` line. + +On success set the milestone `Status: Verified` — **not `Done`**. `Done` is +written by `/close`, after the report, the registries, and the state file are +updated. That gap is deliberate: if the session ends here, the router still +routes the next one to `/close` instead of skipping it forever. + +Then `Next: /close <ID> <Jn>`. diff --git a/poseidon/references/checks-by-hand.md b/poseidon/references/checks-by-hand.md new file mode 100644 index 0000000..c551f79 --- /dev/null +++ b/poseidon/references/checks-by-hand.md @@ -0,0 +1,70 @@ +# Checks without python3 — agent mode + +`poseidon/checks/check.py` is stdlib-only Python. When `python3` is unavailable, +POSEIDON still runs, but the checks degrade — and the degradation must be +**stated, never hidden**. Every report in agent mode carries: + +``` +checks: agent mode — not deterministic, no CI gate +``` + +## What you lose, precisely + +Say this out loud once, at install, and do not let it be forgotten: + +1. **No CI gate.** CI has no agent. Nothing blocks a merge that violates the + method. The checks become advice. +2. **No negative mutations.** `--self-test` proves the checks still catch what + they claim to. Reading files proves nothing about your own reliability, and + you cannot self-test by introspection. +3. **Not reproducible.** Two runs may disagree. A check that sometimes fires is + worse than no check, because it teaches people to re-run until green. + +This is why the script exists. If `python3` can be made available — it ships with +macOS and every Linux, and Claude Code's Bash tool can install it — do that +instead of reading this file. `git` is not optional for this method; `python3` is +the one dependency worth removing, and this is the price. + +## What you can verify by reading, and how + +Do these at `/close`, in this order. Report each as `passed` / `failed` / +`not run` — never guess. + +**Tracking coherence.** Open the unit file. Exactly one `Status:` before +`## Milestones`. Exactly one per `### Jn` block. Milestones numbered `J1`, +`J2`, … with no gap. No milestone `Done` after one that is not. Unit `Done` only +if every milestone is. Every `F####` under `## Dependencies` has a file in +`docs/features/`. **This is the most valuable one, and the one you do best** — +it is reading and comparing, not counting. + +**Generated index.** Rebuild `docs/features/README.md` from the unit files and +compare to what is there. Same columns, same order (sorted by filename). If they +differ, rewrite it — never hand-patch a row. + +**Identifiers in headings.** Grep for `^#.*\b(F[0-9]{4}|KI-[0-9]+|LC-)` in +`AGENTS.md` and `docs/*.md`. Any hit is a failure: move the identifier into the +`Proven by` / `Promoted from` column and delete the narrative. + +**Registers.** In `docs/KNOWN_ISSUES.md`: severities in +`Critical|High|Medium|Low`, statuses in `Open|Accepted|Scheduled|Resolved|Invalid`, +no duplicate id, and **no `Resolved` row with an empty Evidence cell**. In +`docs/LEARNINGS.md`: states in the closed list, a revalidation date on anything +`Codified` or `Enforced`. + +**Boundaries.** Every row of the `Boundaries` table in `docs/ARCHITECTURE.md` +has a `## <same name>` section in `docs/RULES.md`. + +**Line caps.** `wc -l` if you have a shell; otherwise read the file and count. +Caps are in `poseidon/checks/limits.json`. Treat a result within 5 lines of a cap +as **unverified**, not as passed — say so. + +**Markers.** Grep `TODO|FIXME|HACK|XXX` across source files. Compare against +`docs/adoption.json` if it exists. Without it you cannot tell new debt from old, +so report the raw count and say that is what you are reporting. + +## The rule that does not degrade + +Never report `passed` for something you did not actually check. In agent mode +`not run` is a frequent, legitimate, and honest answer. A green report you +produced by pattern-matching is exactly the false success the whole method is +built to prevent. diff --git a/poseidon/references/delegation.md b/poseidon/references/delegation.md new file mode 100644 index 0000000..416c6c1 --- /dev/null +++ b/poseidon/references/delegation.md @@ -0,0 +1,41 @@ +# Delegating to a sub-agent + +## Allowed only when + +At least two sub-tasks each produce a **useful result independently**. In +practice: inspecting different zones, read-only research, adversarial review, or +disjoint paths behind a stable contract. + +## Sequential otherwise — no exceptions + +- dependent sub-tasks; +- the same file, or the same persistent state; +- changing a contract together with its callers; +- any Git operation. + +## The brief + +A delegation that omits any line below will drift. Include all of them: + +- the expected result, and the precise question being answered; +- the sources to read — named files, not "the codebase"; +- allowed paths, and forbidden paths; +- either `read-only`, or the exhaustive list of paths it may write; +- the validations expected, and the shape of the report back; +- stop conditions: contradiction, unmet dependency, secret encountered, + unexpected modification, reserved decision. + +In a shared worktree, `read-only` is the default and a sub-agent never switches +the branch. + +## On return + +The coordinator, in this order: + +1. separates findings, assumptions, and modifications actually present on disk; +2. rejects any change outside the assigned paths — reverts it, does not adopt it; +3. runs the review on the **combined diff**, not on each report; +4. replays the validations after the last modification. + +A command that succeeded inside a sub-task is not evidence about the integrated +result. Only validations replayed after the final change count. diff --git a/poseidon/references/install.md b/poseidon/references/install.md new file mode 100644 index 0000000..097b8e4 --- /dev/null +++ b/poseidon/references/install.md @@ -0,0 +1,63 @@ +# Installing POSEIDON + +No script, no build step, no package manager. Extract, then ask. + +## 1. Extract at the repo root + +``` +tar xzf poseidon.tar.gz -C /path/to/repo +``` + +That places two things: + +``` +poseidon/ the method — replaceable as one block +.claude/skills/poseidon/SKILL.md the trigger +.claude/commands/poseidon.md /poseidon +``` + +Extraction merges into an existing `.claude/`; it touches only these two names. +On Windows, any archive tool does the same job — nothing here needs a shell. + +## 2. Say "lance poseidon" + +The router sees `AGENTS.md` and `docs/` missing, reports `NOT_INSTALLED`, and +runs `/install`. **The agent installs POSEIDON with its own file tools**, which +is strictly better than a script could: it merges an existing `AGENTS.md` with +judgement instead of refusing or clobbering, and it works the same on every OS. + +`/install` writes the project's own layer: + +``` +AGENTS.md docs/*.md docs/features/ +CLAUDE.md docs/decisions/ docs/validation-baselines.json +.claude/commands/* stubs, only where no name collides +``` + +Anything that already exists is kept and merged, never overwritten. + +## 3. `/bootstrap` + +Audits the codebase for what it can prove, asks in one batch for what it cannot, +writes `ARCHITECTURE.md` → `RULES.md` → `VALIDATION.md` → `SETUP.md` → +`PRODUCT.md` → `AGENTS.md` → `CURRENT_STATE.md`, in that order, because each +feeds the next. One rule: **derive, or ask — never invent.** + +## 4. The checks + +`python3` is the one optional dependency, and the one worth having: + +``` +python3 poseidon/checks/check.py --self-test # the checks still check something +python3 poseidon/checks/check.py --all # the repo conforms +``` + +Put `--all` in CI, blocking. Without `python3` POSEIDON still runs — see +`checks-by-hand.md` — but there is no CI gate and no determinism, and every +report has to say so. + +## Upgrading + +Replace `poseidon/`. Say "lance poseidon" and ask it to refresh the command +stubs. `docs/` and `AGENTS.md` are never touched — that is the whole point of +the two-layer split. `poseidon/VERSION` says what you are on. diff --git a/poseidon/references/recovery.md b/poseidon/references/recovery.md new file mode 100644 index 0000000..f33bfd3 --- /dev/null +++ b/poseidon/references/recovery.md @@ -0,0 +1,48 @@ +# Recovery + +## The three-strike protocol + +An *attempt* is one change made with the intent of fixing the same failing +symptom, followed by one replay of the validation that exposed it. + +- **Strike 1 fails** → state the hypothesis you are now testing, in one line, + before changing anything. If you cannot state it, you are guessing; go to the + diagnostic step below instead of editing. +- **Strike 2 fails** → stop editing. Run non-destructive diagnostics only: read + the actual error, read the code path, print the real values. Then one last + attempt, targeted. +- **Strike 3 fails** → **revert to the last green commit** + (`git restore --source=<sha> -- <paths>`, explicit paths, never a global + reset), record the failure in `docs/KNOWN_ISSUES.md` with the exact command, + its real output, and the file and line, then report and halt. + +A fourth attempt is forbidden. Leaving the tree broken is also forbidden: the +repository is green or it is reverted. + +## Never do this while failing + +- Weaken or delete a test to make a suite pass. +- Add a retry, a sleep, or a broad `catch` to hide a failure. +- Widen `Allowed areas` to reach the real cause. Record it and stop. +- Report `passed` for a check you did not run to completion. + +## The four validation outcomes + +They never substitute for one another in a report: + +| Outcome | Means | +| --- | --- | +| `passed` | ran to completion, discovered ≥ baseline items, exited 0 | +| `failed` | ran, and did not pass | +| `not run` | you did not run it | +| `blocked by environment` | it cannot run here — name who runs it and where | + +A tool exiting `0` after discovering **no tests** proves nothing. Compare the +discovered count against the baseline before concluding anything. + +## When a manual verification cannot be done locally + +The unit stays in `Verify`. The report names: the exact check, why it is +unavailable here, who must run it and on which environment, what was done +instead, and **what that substitute does not prove**. Absence is stated, never +inferred from silence. diff --git a/poseidon/references/review.md b/poseidon/references/review.md new file mode 100644 index 0000000..525d53f --- /dev/null +++ b/poseidon/references/review.md @@ -0,0 +1,45 @@ +# Adversarial review + +A capability is reviewed **milestone by milestone, on the pushed diff of that +milestone**, by an agent that did not write it, before the next milestone opens. +A single review at the end of the capability is non-conforming. + +## Inputs + +- the pushed diff of this milestone only: `git diff <base>..HEAD -- <paths>`, + where `<base>` is the previous milestone's `commit:` line in the Completion + Report, or `git merge-base origin/<main branch> HEAD` for `J1`; +- the unit header, the milestone block, `Allowed areas`, `Do not touch`; +- the `RULES.md` section for the milestone's primary boundary; +- the milestone's `Risk` and `Security-sensitive` values — they set the depth. + +Nothing else. A reviewer who reads the whole repo reviews nothing. + +## Order — report every band; a band 1 finding blocks on its own + +1. **Security and data loss.** Authority derived from the caller's token, not the + request body. No secret reachable by a client. Sensitive mutations atomic and + idempotent. Destructive migration or irreversible write. +2. **Conformance.** Does the diff produce the milestone's stated observable + result, and only that? Any path touched outside `Allowed areas` is blocking. +3. **Regressions and compatibility.** Callers of every changed contract. Schema + and wire compatibility. Isolation between distinct rights holders. +4. **Architecture and debt created.** Business rules on the wrong side of the + boundary. Debt that is not recorded. +5. **Tests and observability.** Does a deliberately broken behaviour fail a test? + If not, the test proves nothing. Check discovered-test count against baseline. +6. **Readability and performance.** + +## Output + +Findings as a list, each with: band, file and line, the observable failure, and +what would make it pass. Then a verdict: `pass`, `pass with follow-ups` +(follow-ups recorded in `KNOWN_ISSUES.md`), or `return to In progress`. + +## Forbidden + +- Requesting an aesthetic rewrite with no measurable benefit. +- Reviewing intent instead of the diff. +- Approving on the basis of a validation you did not see replayed after the last + change. A command that passed in a sub-task proves nothing about the merged + result. diff --git a/poseidon/skeletons/AGENTS.md b/poseidon/skeletons/AGENTS.md new file mode 100644 index 0000000..6e94844 --- /dev/null +++ b/poseidon/skeletons/AGENTS.md @@ -0,0 +1,113 @@ +# AGENTS.md — <PROJECT> + +Short on purpose. It carries the invariants; detail lives in the documents it +cites, opened only when the task concerns them. + +<PROJECT> is <one sentence: what it is, for whom, on which platform>. +Priorities, in order: <priority 1>; <priority 2>; <priority 3>; <priority 4>; +maintainability. Never sacrifice a higher priority to speed up a lower one. Given +two equally correct options, the one serving the higher priority wins, even if it +costs more. + +## Start here — every session + +Say "lance poseidon", or run `/locate`. It tells you which state you are in +and which single command applies. Do not begin work without it: that is how a +session ends up touching four unrelated things. + +## What to read, and when + +1. this file; +2. `<zone>/AGENTS.md` for the zone being worked, if it exists; +3. `docs/CURRENT_STATE.md` — the proven state, held under 200 lines; +4. the unit: in full at its first milestone; afterwards the header, the open + milestone, `Allowed areas`, `Do not touch`. + +That is all. `RULES.md` is read **one section** at a time, for the boundary the +milestone touches. `VALIDATION.md` is read **one row** at a time. Never open a +document "for context". + +Precedence when sources disagree: code, migrations and lockfiles on the branch > +active unit and accepted decisions > `CURRENT_STATE.md` > specialised docs > +roadmap and README. A branch or an unmerged Pull Request is never a delivered +capability — always distinguish local, pushed, in review, merged. On a +contradiction, stop, quote both passages, and have the stale source fixed. + +On demand under `docs/`: `RULES.md` (invariants by boundary), `VALIDATION.md` +(commands), `ARCHITECTURE.md` (where trust changes), `WORKFLOW.md` (the cycle), +`SETUP.md` (getting it running), `PRODUCT.md` (what it is and is not), +`KNOWN_ISSUES.md`, `LEARNINGS.md`. + +If this file still says `<PROJECT>`, the kit was installed but never adapted: +run `/bootstrap` before anything else. + +## Non-negotiable invariants + +Replace with the project's own. An invariant is written as a **refusal**, not as +an intention, and must be verifiable by a check or by reading a diff. + +- **Authority.** <Who is authoritative over sensitive data; which components are + untrusted clients; what never leaves the server.> +- **Data.** <Schema evolution rule; transactions and idempotence; the isolation + proof required on every change.> +- **Secrets.** Never commit or log a secret, token, personal datum, or + environment file. Never execute a remote script. +- **Boundaries.** Validate every input at every boundary: <list them>. +- **Code.** <Strict typing, warnings forbidden, thin layers, exact versions and + lockfiles, no new dependency where the pinned toolchain suffices.> +- **Decisions.** No architecture or trust-boundary change without a written + decision; an accepted decision changes only via a decision that supersedes it. +- **Versions.** <Scheme, single source.> Never an opaque version, a reused + number, or a moved tag. + +This file stays under two pages: a new global rule **replaces or condenses** an +existing one. It never stacks. + +## Scope + +One unit = one user capability = one branch = one worktree. At most **two** units +`In progress`. Cycle in `docs/WORKFLOW.md`. + +- Implement the unit only; respect `Allowed areas` and `Do not touch`. +- Record any out-of-scope discovery in `docs/KNOWN_ISSUES.md` with evidence and + severity, **without fixing it**. +- **Decide alone by default.** Facing an ambiguity, choose the safest option — + then, among equally safe ones, the fastest — implement it, and record the + assumption in the report. `<Owner>` is asked only when no option satisfies the + invariants, when the act is irreversible or leaves the repo, or when it is a + product call no evidence settles. A question that can wait for the report does + not interrupt the work. +- The unit file's `Status` field is authoritative; its index is generated from + that field and is never hand-edited. + +## Validation + +`docs/VALIDATION.md` gives the commands per area touched: targeted tests first, +then boundary checks. Never report a check you did not run as passed: `passed`, +`not run`, `blocked by environment`, and `failed` are four distinct outcomes, and +a tool exiting `0` without discovering any test proves nothing. + +Three failed attempts at the same fix: revert to the last green commit, record +the failure with its evidence, halt. A fourth attempt is forbidden. + +## Git + +- Read the current branch immediately before every commit — a shared worktree's + `HEAD` may have moved. +- Stage by explicit path list; never `git add .` or `git add -A`; re-read + `git diff --cached` before committing. +- Never force-push, never bypass a protection. Pull Request stays draft until + validations and dependencies are ready. Never invent a branch, a base, or a CI + result. +- **Final review and merge belong exclusively to `<Owner>`.** + +## Ending a task + +The report gives: status, files changed, commands and real results, risks and +limits, branch, commit, Pull Request. + +**Every report ends with `Next`** — the next concrete action, who owns it, what +unblocks it — **then a resume prompt** copyable into a fresh session, giving +pointers and state, never document contents. Format in `/handoff`. + +A report without both is not finished. diff --git a/poseidon/skeletons/docs/ARCHITECTURE.md b/poseidon/skeletons/docs/ARCHITECTURE.md new file mode 100644 index 0000000..5501056 --- /dev/null +++ b/poseidon/skeletons/docs/ARCHITECTURE.md @@ -0,0 +1,77 @@ +# Architecture + +The shape of the system, and **where trust changes**. + +Cap: **150 lines**. Read the `Boundaries` table when opening a unit; read the +rest only when the open milestone changes what it describes. + +Unlike `PRODUCT.md`, almost everything here **is** derivable from the code, and +therefore must be: every row carries the file it was read from. A row with no +evidence is a belief, and beliefs are what this kit exists to remove. + +--- + +## Boundaries + +**This table is the spine of the repository.** Each row here is a section of +`docs/RULES.md` and a row of `docs/VALIDATION.md`. A boundary is a place where +trust changes — not a folder, not a layer, not a team. + +A check enforces the correspondence: a boundary listed here with no matching +section in `RULES.md` fails. + +| Boundary | Trust changes how | Enforced in | Read from | +| --- | --- | --- | --- | +| <Data and persistence> | <untrusted input becomes durable state> | <path> | <file that proves it> | +| <Authoritative service> | <a request becomes an authorised decision> | <path> | <file> | +| <Authenticated boundary> | <an anonymous caller becomes an identified one> | <path> | <file> | +| <Client> | <server truth becomes untrusted display> | <path> | <file> | + +If two rows are enforced in the same place, they are one boundary. If one row is +enforced in four places, it is not yet a boundary — it is a convention, and it +will be broken. + +## Components + +| Component | Responsibility | Depends on | Read from | +| --- | --- | --- | --- | +| <name> | <one line> | <components, not libraries> | <manifest or entry point> | + +## What is authoritative over what + +<The single most important paragraph for an agent. Which component decides, and +which components merely display or cache. Name what must never be trusted from +the client, and what the server never derives from a request body.> + +## Data flow of the main path + +<Three to eight steps, the request that matters most, from entry to durable +state and back. Name the boundary crossed at each step, using the names in the +table above — not new ones.> + +1. <step> — crosses <boundary> +2. <step> + +## State that outlives a request + +<Database, cache, queue, filesystem, third-party store. For each: who writes it, +who may read it, and what happens when it is lost. The last column is the one +people skip and then regret.> + +| Store | Written by | Read by | If lost | +| --- | --- | --- | --- | +| <name> | <component> | <components> | <consequence> | + +## Deliberate architectural constraints + +<Decisions already made that a unit may not quietly undo, each pointing at its +record in `docs/decisions/`. Changing one of these requires a superseding ADR, +never a pull request that happens to include it.> + +- <constraint> — ADR-NNNN + +## Known structural debt + +<Where the real structure disagrees with this document, with the entry in +`KNOWN_ISSUES.md` that tracks it. Writing the aspiration here instead of the +reality is how an architecture document becomes fiction.> diff --git a/poseidon/skeletons/docs/CURRENT_STATE.md b/poseidon/skeletons/docs/CURRENT_STATE.md new file mode 100644 index 0000000..d75e7ac --- /dev/null +++ b/poseidon/skeletons/docs/CURRENT_STATE.md @@ -0,0 +1,59 @@ +# Current state + +**The proven present, never the intent.** A capability enters here only once +merged into the main branch, with the evidence that establishes it. The future +belongs to the roadmap, defects to `KNOWN_ISSUES.md`. + +Cap: **200 lines**, enforced by a check. This is the file every agent reads on +every task: each line here is paid for at every session. + +Update at the close of any unit that changes the proven reality. An entry that is +no longer true is **replaced**, not appended. As the file approaches its cap, +older material moves to `docs/archive/`. + +--- + +## Current milestone: <name> + +<Three to six lines: what is targeted, the observable definition of "reached", +and who decides that it is.> + +## Capabilities shipped in `<main branch>` + +One line per genuinely usable capability, with its evidence. No commits, no +branches, no unmerged Pull Requests. + +| Capability | Evidence | Since | +| --- | --- | --- | +| <what a user can do> | <command, test, or verification that establishes it> | <date> | + +## Units in progress + +At most two. One line each, with the open milestone and what blocks it. + +| Unit | Open milestone | Branch | Blocked by | +| --- | --- | --- | --- | +| <F0001> | <J2 — authenticated boundary> | <feature/f0001-slug> | <nothing / decision / environment> | + +## Missing for the current milestone + +Short, ordered list. Each line is either an existing unit, or a gap that must +become one. + +## Outside the milestone, tracked elsewhere + +<What is known, real, and deliberately deferred. Point at `KNOWN_ISSUES.md` or +the roadmap rather than describing it here.> + +## Steering + +Steering decisions, **dated**, with their author. They live here — not in +`AGENTS.md`, which carries only timeless invariants. A stale decision is removed, +not stacked. + +- **<Decision> — <author>, <date>.** <What it concretely changes.> + +## Reproduce and verify + +<The two or three commands that let anyone confirm the state described here is +real. Detail in `docs/VALIDATION.md`.> diff --git a/poseidon/skeletons/docs/KNOWN_ISSUES.md b/poseidon/skeletons/docs/KNOWN_ISSUES.md new file mode 100644 index 0000000..e138e15 --- /dev/null +++ b/poseidon/skeletons/docs/KNOWN_ISSUES.md @@ -0,0 +1,44 @@ +# Known issues and follow-ups + +Register of **real discoveries outside the scope** of a unit of work. Planned +work belongs to the roadmap; accepted invariants to `RULES.md`; tooling traps to +`LEARNINGS.md`. + +A discovery enters at the moment it is made, with its evidence, and is **not +fixed in passing**. That is the rule protecting a unit's scope: without the +register, every discovery becomes either scope drift or an omission. + +## Format + +- **Severity**: `Critical`, `High`, `Medium`, `Low`. Closed list. +- **Status**: `Open`, `Accepted`, `Scheduled`, `Resolved`, `Invalid`. Closed list. +- **Evidence**: what establishes the defect — command, output, file and line. + Mandatory to reach `Resolved`. A resolved entry with no evidence is a false + success, not a success. +- **Target**: the unit that will handle it, or the roadmap phase. + +A `Critical` or `High` found when opening a unit may block it. The coordinator +says so explicitly rather than ignoring it. + +## What does not belong here + +- A future capability — that is roadmap. +- An accepted, bounded risk — that is an exception in `RULES.md`, with a deadline. +- Evidence missing for lack of an environment — that is a report limit, not a + defect. +- An intuition with no reproducible evidence. + +A candidate is not a known issue by default. It joins this register only if it +describes a **real** defect, out of scope, worth tracking. + +## Remediation + +Fixing an entry goes through a separate unit, whose acceptance includes a +relevant negative test and an explicit residual risk. Closing an entry without a +negative test is preparing to reopen it. + +## Entries + +| ID | Severity | Area | Summary | Evidence | Target | Status | +| --- | --- | --- | --- | --- | --- | --- | +| KI-001 | <Medium> | <area> | <one line> | <command -> output> | <unit> | Open | diff --git a/poseidon/skeletons/docs/LEARNINGS.md b/poseidon/skeletons/docs/LEARNINGS.md new file mode 100644 index 0000000..19225bf --- /dev/null +++ b/poseidon/skeletons/docs/LEARNINGS.md @@ -0,0 +1,70 @@ +# Operational learnings + +A repository's learning is about **observable facts**, never an agent's implicit +memory. This register exists so a difficulty paid once is not paid twice, and so +an instruction does not outlive its cause. + +## States + +| State | Definition | +| --- | --- | +| `Observed` | Seen once, in a single context. | +| `Reproduced` | Reconstructed in a second independent context, or deterministically. | +| `Codified` | Written at its canonical destination. | +| `Enforced` | Applied by an automated check. This is the goal. | +| `Stale` | No longer applies — tool, version, or context changed. Kept, never erased. | + +## Evidence and promotion + +- **Capture** a first occurrence immediately: symptom, context, the wrong + conclusion to avoid, the non-destructive diagnostics run. +- **Promotion** requires two independent occurrences or one deterministic + reproduction. One reproducible occurrence suffices where there is high risk of + a security failure, data loss, or a false success — after explicit review. +- A counter-example beats a third occurrence: what bounds a rule is worth more + than what repeats it. +- A promotion never justifies exceeding a unit's `Allowed areas`. + +## Canonical destination + +Choose the **narrowest** that works. Writing higher than necessary is the most +common way documentation becomes unreadable. + +| Nature | Destination | +| --- | --- | +| One-off fact about a unit | its Completion Report | +| Real defect out of scope | `KNOWN_ISSUES.md` | +| Recurring tooling trap | `RULES.md`, "defects already encountered" | +| Invariant of a boundary | `RULES.md`, that boundary's section | +| Command or execution order | `VALIDATION.md` | +| Constraint of a code zone | `<zone>/AGENTS.md` | +| Global project invariant | `AGENTS.md` — replacing an existing rule | + +As soon as a deterministic check is possible, open a bounded unit for the script +or test **instead of** keeping a manual instruction. A manual instruction is a +debt, not an asset. + +## Candidate template + +### LC-YYYY-NNN — <title> + +- **State**: `Observed` +- **Symptom**: <what was observed, literally> +- **Context**: <tool, version, system, command> +- **Wrong conclusion to avoid**: <what we believed that was false> +- **Diagnostics run**: <non-destructive commands and their output> +- **Occurrences**: <unit, date>; <unit, date> +- **Proposed destination**: <per the table above> +- **Revalidate on**: <date or condition — version or tool change> + +## Register + +| ID | Title | State | Destination | Revalidate | +| --- | --- | --- | --- | --- | +| LC-YYYY-001 | <title> | Observed | — | <date> | + +## Revalidation and retirement + +At the deadline, or on a version change: confirm, replace, or mark `Stale` — +without erasing history. An `Enforced` entry whose check has been removed drops +back to `Codified`: that is a regression of guarantee, and it must be visible. diff --git a/poseidon/skeletons/docs/PRODUCT.md b/poseidon/skeletons/docs/PRODUCT.md new file mode 100644 index 0000000..55b00f4 --- /dev/null +++ b/poseidon/skeletons/docs/PRODUCT.md @@ -0,0 +1,71 @@ +# Product + +What this system is, for whom, and what it refuses to be. + +Cap: **120 lines**. Read when the open milestone changes what is described here — +and once, in full, at the first session of anyone new to the repo. + +**Nothing in this file can be derived from the code.** A codebase can tell you +what it does; it cannot tell you who it is for, what it is allowed to become, or +which trade-off wins. Every line here was **asked** and answered by `<Owner>`, or +it is a guess wearing the costume of a fact. `/bootstrap` asks; it never infers. + +--- + +## One sentence + +<What it is, for whom, on which platform. If this needs two sentences, the +product is two products or the sentence is not finished.> + +## Who uses it + +| User | What they are trying to do | What failure costs them | +| --- | --- | --- | +| <role> | <the job they hired this for> | <time, money, trust, data> | + +The third column decides review depth downstream. A user whose failure costs +data or money makes every boundary they touch `maximum evidence` in +`VALIDATION.md`. + +## Priorities, in order + +<The same ordered list as `AGENTS.md`. It lives there for the agent; here is +where the reasoning for the order is written.> + +1. <priority 1> — <why it outranks the next one> +2. <priority 2> — <…> +3. <priority 3> +4. <priority 4> +5. maintainability + +Given two equally correct options, the one serving the higher priority wins, +even if it costs more. This ordering is the single most-used line of the whole +kit: it settles arbitrations that would otherwise become questions. + +## What it is not + +<The neighbouring products this is deliberately not. Each line prevents a class +of feature request and a class of accidental architecture.> + +- <not a …> +- <not a …> + +## Constraints that are not technical + +<Regulation, contract, deadline, platform policy, an agreement with a customer. +The things that make an otherwise correct solution unacceptable — and that no +amount of reading the code will reveal.> + +## What "working" looks like + +<The observable signals that the product is doing its job. Not metrics to +optimise — signals that would tell you it stopped working.> + +## Open product questions + +<Decisions `<Owner>` has not made yet, and which units they block. A question +listed here is a legitimate reason for a unit to sit in `Draft`.> + +| Question | Blocks | Asked on | +| --- | --- | --- | +| <question> | <unit or roadmap phase> | <date> | diff --git a/poseidon/skeletons/docs/RULES.md b/poseidon/skeletons/docs/RULES.md new file mode 100644 index 0000000..2859aa9 --- /dev/null +++ b/poseidon/skeletons/docs/RULES.md @@ -0,0 +1,117 @@ +# Invariants by boundary + +Canonical source of accepted invariants. Read **the section for the boundary you +touch**, never the whole file. + +Cap: **300 lines**. A new invariant replaces or condenses another. That is the +mechanism that keeps this file readable. + +## Section index — jump straight to yours + +| You are touching | Section | +| --- | --- | +| schema, migrations, persistence | [Data](#data-and-persistence) | +| authoritative server logic | [Authoritative service](#authoritative-service) | +| HTTP / RPC handlers, auth | [Authenticated boundary](#authenticated-boundary) | +| UI, client state, rendering | [Client](#client) | +| CI config, dependencies, lockfiles | [CI and supply chain](#ci-and-supply-chain) | +| packaging, publishing, releases | [Distribution](#distribution) | +| telemetry, personal data | [Privacy](#privacy) | +| tooling that already bit us once | [Known traps](#rules-from-defects-already-encountered) | + +## How to write a line + +One line per invariant, never a paragraph. Four columns: + +| Column | Content | +| --- | --- | +| `#` | Stable identifier, prefixed by boundary: `D1`, `S3`, `C2`. Never reused. | +| Invariant | A **refusal**, negative or imperative. One line. | +| Check | The command or test that enforces it. `manual` if none — that is a debt. | +| Proven by | The unit that established it. **A citation, not a title.** | + +Forbidden in this file: a heading containing a unit identifier; the narrative of +what a unit proved; any restatement of a Completion Report. Evidence lives in the +unit; here it is cited. A check enforces this. + +An invariant with no automated check that could have one is unfinished: open a +bounded unit for the script or test rather than keeping a manual instruction. + +## Lifecycle of an invariant + +A new or modified invariant states the asset protected, the trust boundary, the +abuse refused, the expected check, and — if it depends on a tool or a version — +its revalidation condition. + +An agent never creates an exception alone. Every exception is approved by +`<Owner>`, bounded to one unit and one surface, carries a residual risk and a +deadline. An exception with no deadline is an abandoned rule. + +--- + +The sections below are a skeleton. Replace the boundaries with the real system's +— they are read off the architecture, not off the history. A boundary is a place +where trust changes. + +## Data and persistence + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| D1 | <e.g. migrations are append-only; no destructive redefinition> | `<command>` | <unit> | +| D2 | <e.g. every sensitive command is transactional and idempotent> | `<command>` | <unit> | +| D3 | <e.g. every schema change proves isolation between distinct rights holders and anonymous> | `<command>` | <unit> | + +## Authoritative service + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| S1 | <e.g. caller identity is derived from the token, never taken from the request body> | `<command>` | <unit> | +| S2 | <e.g. no server secret is readable by a client> | `<command>` | <unit> | + +## Authenticated boundary + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| F1 | <e.g. request body bounded to <N> KiB> | `<command>` | <unit> | +| F2 | <e.g. response allow-listed, versioned, not cached> | `<command>` | <unit> | +| F3 | <e.g. every input validated before any side effect> | `<command>` | <unit> | + +## Client + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| C1 | <e.g. no business rule and no authority decision on the client> | `<command>` | <unit> | +| C2 | <e.g. content security policy per channel, no remote source> | `<command>` | <unit> | + +## CI and supply chain + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| I1 | <e.g. exact versions and lockfiles from the repo, nothing floating> | `<command>` | <unit> | +| I2 | <e.g. no secret exposed to a job triggered from an external contribution> | `<command>` | <unit> | + +## Distribution + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| P1 | <e.g. no artifact published without verifiable provenance> | `<command>` | <unit> | + +## Privacy + +| # | Invariant | Check | Proven by | +| --- | --- | --- | --- | +| V1 | <e.g. collection off by default; explicit consent for diagnostics> | `<command>` | <unit> | +| V2 | <e.g. no real data until <condition> is lifted> | `<command>` | <unit> | + +## Rules from defects already encountered + +A special section: it describes not a system boundary but a tooling trap, +promoted from `LEARNINGS.md` after two independent occurrences or one +deterministic reproduction. Same format, same cap. + +| # | Trap | What to do instead | Promoted from | +| --- | --- | --- | --- | +| L1 | <observable symptom> | <the behaviour that avoids it> | <LC-YYYY-NNN> | + +A line here is **removed** as soon as a check makes the trap impossible. This is +the only place in the file whose goal is to shrink. diff --git a/poseidon/skeletons/docs/SETUP.md b/poseidon/skeletons/docs/SETUP.md new file mode 100644 index 0000000..261c7cc --- /dev/null +++ b/poseidon/skeletons/docs/SETUP.md @@ -0,0 +1,76 @@ +# Setup + +How to get from a fresh clone to a running system, and back to a clean one. + +Cap: **100 lines**. Read when a command in `VALIDATION.md` needs a prerequisite +you do not have. + +Every command here is **copied from a file in the repo** — a manifest, a +task-runner file, a CI job — never from memory and never invented. The source is +named. A command that appears only here, in no file the toolchain reads, will be +wrong within a month and nobody will notice until it blocks someone. + +--- + +## Prerequisites + +| Tool | Version | Pinned by | Why exactly this version | +| --- | --- | --- | --- | +| <runtime> | <exact> | <lockfile, tool-version file, CI image> | <or `no reason recorded` — which is itself worth knowing> | + +`no reason recorded` is a legitimate entry. An invented reason is not. + +## From clone to running + +``` +<install dependencies> +<create local configuration from the committed example> +<start dependencies — database, queue, container runtime> +<run migrations> +<start the system> +``` + +Source: `<the file each command was read from>`. + +## Configuration + +| Variable | Required | Where the local value comes from | Read from | +| --- | --- | --- | --- | +| <NAME> | yes/no | <committed example file, secret manager, `<Owner>`> | <file> | + +**No secret, token, personal datum, or environment file is ever committed or +logged.** If a value cannot be obtained without `<Owner>`, say so here rather +than leaving someone to guess — and never place a real value in this file. + +## Back to a clean state + +``` +<stop everything> +<reset the database> +<clear caches and build artifacts> +``` + +The reset path matters more than the setup path: it is what makes a failed run +diagnosable instead of permanent. If there is no reset command, that is debt — +record it in `KNOWN_ISSUES.md`. + +## Verify the setup worked + +``` +<the smallest command that proves the system is actually up> +``` + +Not "it started with no error". A command whose output would differ if the +system were broken. + +## What cannot be run locally + +<Hardware, a third-party service, a remote scheduler, a real device, a real +user. For each: who can run it, on which environment, and what a local +substitute does **not** prove. `VALIDATION.md` refers here rather than +restating it.> + +## Traps + +<What has cost someone an hour here. One line each, with the observable symptom. +Delete a line as soon as a check or a script makes the trap impossible.> diff --git a/poseidon/skeletons/docs/VALIDATION.md b/poseidon/skeletons/docs/VALIDATION.md new file mode 100644 index 0000000..8549616 --- /dev/null +++ b/poseidon/skeletons/docs/VALIDATION.md @@ -0,0 +1,80 @@ +# Validation + +This file answers one question: **I touched this — what do I run?** + +No narrative, no evidence, no description of what a check covers. Those live in +the Completion Report of the unit that wrote it. + +Cap: **130 lines**. Read the **one row** for the area you touched. + +## What to run + +Order: targeted tests for the zone first, then the boundary checks. + +| You touched | Commands, in order | Prerequisite | Depth | +| --- | --- | --- | --- | +| <schema / migrations> | `<static check>` -> `<start>` -> `<reset>` -> `<tests>` -> `<types>` | <container runtime> | maximum | +| <authoritative service> | `<static check>` -> `<unit tests>` | — | maximum | +| <authenticated boundary> | `<handler tests>` -> `<real-runtime validation>` | <local runtime> | maximum | +| <client / UI> | `<type check>` -> `<tests>` -> `<build>` | — | fast + real journey | +| <side process> | `<build>` -> `<tests>` | — | per boundary | +| <CI config> | `<CI check>` | — | maximum | +| <dependencies> | `<audit>` -> `<licence report>` | network | maximum | +| <method documents> | `python3 poseidon/checks/check.py --all` | — | fast | + +## Two speeds + +Not every validation is worth the same, and pretending otherwise makes the UI pay +the price of the database. + +- **Maximum evidence** for anything touching money, data, server authority, or + security: full checks, negative mutations, real runtime, a dedicated manual + verification. +- **Fast** for client composition: types, tests, build — and the manual + verification is the real journey through the application, not a separate + checklist. + +The **boundary touched** decides, never the size of the diff. + +## Four outcomes, never interchangeable + +`passed`, `failed`, `not run`, `blocked by environment`. + +A tool exiting `0` without discovering any test proves nothing: check the +**number of items discovered** against the baseline before concluding. + +## Cross-cutting checks + +Run before any Pull Request, whatever the area touched. + +``` +python3 poseidon/checks/check.py --all +<authority invariant check> +``` + +## Baselines + +Guardrail counters — tests discovered, assertions, performance budgets — are +**not prose**. They live in `docs/validation-baselines.json`, one entry per suite: + +```json +{ "handlers": { "discovered": 11, "raised_by": "F0001" } } +``` + +A run discovering less than its baseline is `failed`, whatever its exit code — +instead of depending on a paragraph someone must remember to re-read. A baseline +rises only in the unit that adds the corresponding tests, and only through +`python3 poseidon/checks/check.py --record-baseline <suite>=<n> --raised-by <ID>`, +which refuses any value below the current one. + +## Checks blocked by the environment + +When a check requires an absent environment — hardware, operating system, third +party service, a physical person — the report names: + +1. the exact check, and why it is unavailable; +2. who must run it, and on which environment; +3. what was done instead, and what that substitute **does not prove**. + +A local proof never replaces a remote scheduler, a real device, or a real user. +Absence is stated explicitly, never inferred from silence. diff --git a/poseidon/skeletons/docs/WORKFLOW.md b/poseidon/skeletons/docs/WORKFLOW.md new file mode 100644 index 0000000..899d7e0 --- /dev/null +++ b/poseidon/skeletons/docs/WORKFLOW.md @@ -0,0 +1,115 @@ +# Workflow + +The **procedure** for each phase lives in the slash commands. This file defines +what the phases operate on: roles, states, the unit, and the size limits. Read +the section the router names, not the file. + +Cap: **165 lines**. + +## Roles + +- **`<Owner>`** — settles vision, scope, trade-offs, and merges. The only role + never held by an agent. +- **Coordinator** — carries one unit end to end: plan, code, evidence. +- **Reviewer** — hunts regressions, flaws, drift, and missing tests. + +One agent may hold the last two roles, never on the same milestone. + +## States + +Two independent fields, two closed lists. A check enforces both. + +- **Unit** `Status:` — `Draft` → `Ready` → `In progress` → `Done`. + Alternates: `Blocked`, `Rejected`, `Superseded`. (`Review` and `Verify` are + legal but describe a whole unit awaiting `<Owner>`, not a milestone.) +- **Milestone** `Status:` — `Draft` → `In progress` → `Review` → `Verify` → + `Verified` → `Done`. Alternate: `Blocked`. + +`Verified` and `Done` are distinct on purpose: `/verify` writes the first, +`/close` writes the second, and the gap is what makes a close survive a session +ending between them. + +Which command a given pair routes to is defined **only** in POSEIDON's router +table, `poseidon/POSEIDON.md`. It is not repeated here. + +## The unit of work + +The unit of tracking, of branching, and of integration is the **user +capability**: one file in `docs/features/`, one branch, one Pull Request. + +A capability is a complete vertical slice. Data schema, authoritative service, +authenticated boundary, validation on a real runtime, and client composition +live together in it, split into ordered **milestones**: one commit per milestone, +one review per milestone, one manual verification per milestone. + +Work that is not a user capability — governance, a fix, tooling, a check — uses +**the same file with a single milestone**. There is no second format to maintain. + +Why the vertical slice: splitting a capability into four to six technical units +produces as many branches, bases, and index lines to keep consistent — for user +value that only exists at the last one. The cost is paid in reconciliation units +that ship nothing. + +## 1. Prepare + +1. Define the user outcome. +2. Write or confirm the structuring decisions. +3. Identify dependencies and risks. +4. Split into capabilities, each a complete vertical slice. +5. Order milestones from most-constraining to most-dependent: data and authority + before boundary, boundary before client composition. +6. Detail only the **next one to three** capabilities; keep the rest at roadmap + level, so the plan does not go stale. + +Before launching a wave: map the shared paths, ask for every `<Owner>` decision +in **one batch**, assign disjoint paths and the integration order, reserve the +verification environments, and confirm no approved Pull Request is waiting only +on propagation. + +Daily priority order: a Pull Request ready to integrate, a blocking CI, a +critical-path dependency, then a new unit. Code volume does not measure progress; +only a proven capability present on the main branch reduces what is left. + +## 2. Ready + +A `Ready` capability has: one user goal; dependencies genuinely satisfied; the +union of `Allowed areas` and `Do not touch`; ordered milestones, each with its +observable result, primary boundary, and validations; acceptance criteria +observable over the whole capability; and one 5–10 minute manual verification per +milestone. + +The reviewer challenges the unit **before any code**. Double goal, unobservable +milestone, wrong boundary, or missing evidence sends it back to `Draft`. A +milestone that cannot be committed alone is not a milestone: it belongs to the +previous one. + +## 3 to 6. Implement, review, verify, close + +Procedure in `/milestone`, `/review`, `/verify`, `/close`. Not restated here. + +One principle governs all four and is stated only here: **a capability is +reviewed milestone by milestone, on the pushed diff, by an agent that did not +write it, before the next milestone opens.** This is the counterpart of the +vertical slice. Without it a sensitive-data change and a read-only panel go +through the same review, and the first loses the attention it deserves. A single +review at the end of a capability is non-conforming. + +## Size limits + +A good capability targets a single outcome, statable without an "and", carries +two to five milestones, and can be abandoned without invalidating more than a few +days of work. + +A good milestone targets an observable result, changes **one** primary boundary +and not three, produces a diff reviewable in one pass, is manually verifiable in +5–10 minutes, and **is committable alone** even if the capability is incomplete. + +The milestone carries the size limit, not the capability. A milestone combining a +data change, a new protocol, and a large screen must be split. A capability +needing more than five milestones, or two goals, must be split. + +## Side loops + +Learning: `docs/LEARNINGS.md`. Debt and out-of-scope discoveries: +`docs/KNOWN_ISSUES.md`. Accepted invariants: `docs/RULES.md`. None of these is +restated here — that is the single-source rule. diff --git a/poseidon/templates/ADR.md b/poseidon/templates/ADR.md new file mode 100644 index 0000000..c22b47a --- /dev/null +++ b/poseidon/templates/ADR.md @@ -0,0 +1,41 @@ +# ADR-NNNN — <Decision, phrased as a decision> + +Date: <YYYY-MM-DD> +Status: Proposed / Accepted / Superseded by ADR-NNNN +Decider: <Owner> + +A structuring decision is written **before** the code it authorises. An accepted +ADR is never modified: it is replaced by an ADR that supersedes it and explains +what changed in the world. + +## Context + +What forces a decision now: a constraint, an incident, a deadline, a +disagreement. What is true at the moment of deciding, with its evidence. + +## Options + +| Option | What it gives | What it costs | +| --- | --- | --- | +| <A> | | | +| <B> | | | + +An ADR with a single option has not decided: it has documented a reflex. + +## Decision + +<What is chosen, in one to three sentences, affirmatively.> + +## Consequences + +- What this makes possible. +- What this makes impossible or expensive — the part that gets forgotten and + then regretted. +- What must change now: invariants in `RULES.md`, checks in `VALIDATION.md`, + code zones affected. + +## Revalidation + +<Condition or date at which this decision must be re-examined: a change of +version, of scale, of external constraint. "Never" is a valid answer, provided +it is written down.> diff --git a/poseidon/templates/AGENTS-zone.md b/poseidon/templates/AGENTS-zone.md new file mode 100644 index 0000000..28c9f2b --- /dev/null +++ b/poseidon/templates/AGENTS-zone.md @@ -0,0 +1,49 @@ +# AGENTS.md — zone `<path/to/zone>` + +Place at the root of the zone, not in `docs/`. Agents read the file nearest the +code they touch. This file **completes** the root `AGENTS.md`; it never repeats +it and never contradicts it. + +Cap: **50 lines**, enforced by a check. Beyond that, either the zone must be +split, or a rule actually belongs in `docs/RULES.md`. + +Create a zone only when all three hold: it is worked regularly, it has rules that +apply to it alone, and an agent ignoring them produces a real defect. Three to +six zones are enough for a large repository. + +--- + +## What this zone does + +<Two lines. Its role, and the trust boundary it lives in — authoritative server +side, untrusted client side, isolated process.> + +## Never do this here + +<The refusals specific to this zone — the ones a competent but new agent would +break. Three to six lines.> + +- <e.g. no business rule in a page component; it lives in <layer>> +- <e.g. no direct network call; go through <adapter>> +- <e.g. no filesystem access outside <directory>> + +## Local conventions + +<Naming, file splitting, test locations, generators to use. Only what cannot be +inferred by reading two neighbouring files.> + +## Validate this zone + +``` +<type-check command> +<targeted test command> +<build command> +``` + +Boundary checks are in `docs/VALIDATION.md`. + +## Known traps + +<What has already cost time here. One trap per line, with its observable +symptom. Delete a line as soon as a check makes the trap impossible — that is +the point.> diff --git a/poseidon/templates/EXAMPLE-FEATURE.md b/poseidon/templates/EXAMPLE-FEATURE.md new file mode 100644 index 0000000..999af46 --- /dev/null +++ b/poseidon/templates/EXAMPLE-FEATURE.md @@ -0,0 +1,164 @@ +# F0001 — A signed-in user exports their own data as a CSV file + +Status: In progress +Branch: `feature/f0001-self-serve-export` +Risk: High +Security-sensitive: Yes + +> Worked example. Copy the *shape*, not the content. It exists because agents +> reproduce a filled example far more reliably than they apply a written rule. +> Note what it does **not** contain: no narrative in the rules documents, no +> milestone that cannot be committed alone, no validation reported without its +> real output. + +## Goal + +A signed-in user requests an export and receives a CSV containing their own +records, and only their own. + +## Context + +On `main` today: `users` and `records` tables exist; the authenticated boundary +exists at `api/handlers/`; there is no export path of any kind, and no +background job runner. Verified by reading `api/handlers/__init__.py` and +`db/migrations/` at `a4f21c9`, not from `CURRENT_STATE.md`. + +## Dependencies + +- ADR-0007 (accepted) — exports are generated synchronously until p95 exceeds + 2 s; no job runner is introduced by this unit. +- None outstanding. No `<Owner>` decision, hardware, or human verification is + missing — this unit could enter `Ready`. + +## Allowed areas + +- `db/migrations/` +- `api/exports/` +- `api/handlers/exports.py` +- `web/src/features/export/` +- `tests/exports/` + +## Do not touch + +- `api/auth/` — identity derivation is out of scope; use the existing helper. +- `db/migrations/0001_*` … `0018_*` — append-only. +- `web/src/features/billing/` — shares the download component; changing it is + its own unit. + +## Non-goals + +- Export formats other than CSV — F0004. +- Scheduled or emailed exports — needs a job runner, blocked by ADR-0007. +- Admin export of another user's data — a different trust boundary entirely. + +## Milestones + +### J1 — the export row exists and is isolated per user + +Status: Done +Risk: High +Security-sensitive: Yes + +- result: an `exports` row can be created and read back, and a query issued as + user B cannot see user A's row. +- boundary: data and persistence +- validations: `make migrate-check` -> `make db-reset` -> `pytest tests/exports/test_isolation.py` -> `mypy db api` +- review: the isolation predicate — is it enforced in the schema or only in the + query the tests happen to use? +- manual check: reset the database, insert two users, query as each, confirm + each sees one row. ~6 min. + +### J2 — the authenticated endpoint returns the caller's CSV + +Status: Review +Risk: High +Security-sensitive: Yes + +- result: `GET /api/exports/me.csv` returns only the caller's records; an + unauthenticated call returns 401 and no body. +- boundary: authenticated boundary +- validations: `pytest tests/exports/test_handler.py` -> `make run-local` + `scripts/smoke-export.sh` +- review: is the identity taken from the token, or anywhere from the request? +- manual check: call the endpoint with user A's token, then with user B's, then + with none. ~8 min. + +### J3 — the user can trigger the download from the UI + +Status: Draft +Risk: Low +Security-sensitive: No + +- result: an "Export my data" button downloads the file with a stable filename. +- boundary: client +- validations: `npm run typecheck` -> `npm test -- export` -> `npm run build` +- review: no business rule and no authority decision added on the client. +- manual check: click the button as a real signed-in user, open the file. ~5 min. + +## Acceptance criteria + +- [ ] A signed-in user downloads a CSV of their own records. +- [ ] A request without a valid token returns 401 with no body. +- [ ] A user with zero records receives a valid CSV with headers only. +- [ ] `docs/CURRENT_STATE.md` lists the capability with its evidence. + +## Review + +J1 and J2 are `Security-sensitive: Yes`. + +- asset protected, boundary, abuse refused: users' own records; authenticated + boundary; reading another user's export. +- validation, authorisation, atomicity, idempotence, logs: identity derived from + the token only; export generation is read-only, so idempotent; the export id + is logged, never the row contents. +- debt created, manual check to automate: the cross-user smoke test is manual at + J2 — F0011 turns it into a check. +- residual risk or approved exception: synchronous generation blocks a worker on + very large accounts. Accepted per ADR-0007, revisit when p95 > 2 s. + +## Rollback + +Drop the endpoint and the UI at any point; `0019_add_exports` is additive and +can be left in place. Rolling back after J3 loses nothing — no data is written +by the export path. + +## Completion Report + +### J1 + +- result obtained: `exports` table added; isolation enforced by a row-level + predicate on `owner_id`, not by the query. +- files changed: `db/migrations/0019_add_exports.sql`, `api/exports/model.py`, + `tests/exports/test_isolation.py` +- commands and real results: + - `make migrate-check` -> passed + - `make db-reset` -> passed + - `pytest tests/exports/test_isolation.py` -> passed, 7 tests discovered + (baseline 6, raised in this unit because it adds the tests) + - `mypy db api` -> passed, 0 errors +- commit: `7c1d4e0` (pushed) — `/review` used `git merge-base origin/main HEAD` + as its base for J1; J2 uses `7c1d4e0`. +- manual verification: passed — two users inserted, each query returned exactly + its own row; the cross-user query returned 0 rows, not an error. +- review findings addressed: reviewer noted isolation was initially enforced in + the query only. Moved into the schema predicate; the test that passed before + the fix now fails when the predicate is removed. + +### J2 + +- result obtained: <in review> +- files changed: `api/handlers/exports.py`, `tests/exports/test_handler.py` +- commands and real results: + - `pytest tests/exports/test_handler.py` -> passed, 11 tests discovered + - `scripts/smoke-export.sh` -> **blocked by environment** — needs the staging + token issuer, unavailable locally. `<Owner>` to run it on staging. Local + handler tests do **not** prove token issuance or the 401 path end to end. +- commit: `b93a115` (pushed) +- manual verification: pending +- review findings addressed: pending + +### Summary + +- risks and limits: to fill at close. +- follow-ups recorded: KI-014 (large-account timeout), F0011 (automate the + cross-user smoke test). +- documentation updated: to fill at close. diff --git a/poseidon/templates/FEATURE.md b/poseidon/templates/FEATURE.md new file mode 100644 index 0000000..56f8692 --- /dev/null +++ b/poseidon/templates/FEATURE.md @@ -0,0 +1,103 @@ +# FXXXX — <Capability, stated from the user's point of view> + +Status: Draft +Branch: `feature/fxxxx-slug` +Risk: Low +Security-sensitive: No + +`Risk` and `Security-sensitive` are the **defaults for the milestones**; a +milestone that redeclares them wins. They set review depth, judged where the risk +exists — a sensitive-data change and a read-only panel do not earn the same +attention. Work with no user capability — governance, a fix, tooling — uses this +same file with **a single milestone**. + +## Goal + +One observable user outcome. If the goal needs an "and" between two distinct +capabilities, that is two units. + +## Context + +Why this capability exists, what is **actually present** on the main branch, and +the useful links. + +## Dependencies + +- <Units, decisions, or prerequisites required.> +- Name explicitly any missing `<Owner>` decision, hardware, or human + verification: that is what keeps the unit out of `Ready`. + +## Allowed areas + +- <Union of the paths writable across all milestones.> + +## Do not touch + +- <Explicitly excluded areas.> + +## Non-goals + +- <Neighbouring capabilities deliberately excluded, and the unit that will carry + them.> + +## Milestones + +Ordered. One milestone = one commit = one primary boundary = one review. The +first milestone that is not `Done` is the only open one. + +### J1 — <milestone result> + +Status: Draft +Risk: Low +Security-sensitive: No + +- result: <observable behaviour once this milestone is committed> +- boundary: <the primary boundary touched> +- validations: <exact commands that prove this milestone> +- review: <what the adversarial review must look for first> +- manual check: <5 to 10 minutes> + +### J2 — <milestone result> + +Same structure. Two to five milestones; beyond that, these are two units. + +## Acceptance criteria + +- [ ] <Criterion observable over the whole capability.> +- [ ] <Relevant error or limit covered.> +- [ ] <Documentation synchronised if needed.> + +## Review + +Fill only if a milestone carries `Security-sensitive: Yes`, or if the unit +creates debt. Name the milestones concerned. + +- asset protected, boundary, abuse refused: +- validation, authorisation, atomicity, idempotence, logs: +- debt created, manual check to automate: +- residual risk or approved exception, with its deadline: + +## Rollback + +How to abandon the capability without data loss, and up to which milestone a +partial rollback stays coherent. + +## Completion Report + +**This is where the project's evidence lives.** Rules documents cite it; they do +not copy it. One block per milestone, filled at the moment it is committed. + +### J1 + +- result obtained: +- files changed: +- commands and real results: +- commit: <the pushed SHA — `/review` uses the previous milestone's as its base> +- manual verification: +- review findings addressed: + +### Summary + +- risks and limits: +- follow-ups recorded: +- documentation updated: From c7367e2e06bfb5d4120f9ba4cec754e1b0b39dd0 Mon Sep 17 00:00:00 2001 From: AndyD9 <andy.dodakal@gmail.> Date: Wed, 12 Aug 2026 13:29:21 +0200 Subject: [PATCH 2/2] docs: accept ADR-0005 and propagate the supersession PacoRCS accepted ADR-0005 on 2026-08-12. Hardening the existing application in place replaces the full-rewrite direction of ADR-0002. - ADR-0005: Proposed -> Accepted - ADR-0002: Accepted -> Superseded by ADR-0005 - decisions/README.md: status table updated - ARCHITECTURE.md: hardening becomes a deliberate constraint; the ADR-0002 contradiction leaves Known structural debt - CURRENT_STATE.md: milestone and steering reworded; ADR-0005 drops off the Missing list - KNOWN_ISSUES.md: KI-008 and KI-016 Resolved, each citing ADR-0005 - PRODUCT.md: the fresh-repository question stays open but now blocks nothing, per ADR-0005 The stack drift (KI-017) is untouched and still open: ADR-0004's pins are now an upgrade path, and each step needs its own unit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- docs/ARCHITECTURE.md | 8 +++--- docs/CURRENT_STATE.md | 27 +++++++++---------- docs/KNOWN_ISSUES.md | 4 +-- docs/PRODUCT.md | 2 +- .../ADR-0002-strategie-de-refonte.md | 4 +-- ...0005-durcissement-au-lieu-de-reecriture.md | 2 +- docs/decisions/README.md | 4 +-- 7 files changed, 24 insertions(+), 27 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2defee5..f2c901d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -79,6 +79,9 @@ key. A unit may not quietly undo these. Changing one requires a superseding ADR. +- Harden in place, never rewrite: every existing feature is preserved, and + improvement happens boundary by boundary as bounded units — ADR-0005, + superseding ADR-0002. - Solo ownership: one account owns at most one company and is its only human; `owner` is the only role — ADR-0001. - Windows 11 x64 + MSFS 2024 only; no Windows 10, ARM64, MSFS 2020, or @@ -91,11 +94,6 @@ A unit may not quietly undo these. Changing one requires a superseding ADR. Where the real structure disagrees with the documents above. -- **ADR-0002 is contradicted by reality and by the owner's current decision.** - It mandates a full rewrite in a new repository with fresh history; this is the - original repository with `legacy/` carved out in place by `fc3316d`, and the - owner has since chosen to harden the existing code rather than rewrite it. - Superseded by ADR-0005 (proposed) — `KI-016`. - **ADR-0004 pins a stack the repository does not have**: .NET 10, pnpm 11, TypeScript 6, React 19, Tauri 2.11, `rust-toolchain.toml`. Actual: `net8.0`, npm, TypeScript 5.9, React 18.3, no `rust-toolchain.toml` — `KI-017`. diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md index 0f7953a..80af6b2 100644 --- a/docs/CURRENT_STATE.md +++ b/docs/CURRENT_STATE.md @@ -19,8 +19,8 @@ are not years behind. Reached when the code has been hardened boundary by boundary without a single user-visible feature being lost. `PacoRCS` decides that it is reached. -This replaces the full-rewrite direction of ADR-0002. That reversal needs -ADR-0005 before any unit relies on it. +This replaces the full-rewrite direction of ADR-0002, superseded by ADR-0005 +(accepted 2026-08-12). ## Capabilities shipped in `main` @@ -50,30 +50,29 @@ None. POSEIDON was bootstrapped on 2026-08-12; no unit has been opened yet. ## Missing for the current milestone -1. **ADR-0005** superseding ADR-0002 — the rewrite decision is reversed but not - recorded. Everything below inherits from it. -2. **The capability inventory** — which features are real, and how each is +1. **The capability inventory** — which features are real, and how each is demonstrated. Without it "keep every feature" has no referent. -3. **A version reset** to `0.0.1-alpha` across the four manifests, and a +2. **A version reset** to `0.0.1-alpha` across the four manifests, and a decision on which one is the single source. -4. **A dependency upgrade pass** — 19 direct dependencies are outdated, 8 by a +3. **A dependency upgrade pass** — 19 direct dependencies are outdated, 8 by a major version. -5. **A .NET test project**, so `dotnet test` stops exiting `0` on nothing. -6. **An RLS isolation suite** for owner A / owner B / anonymous. -7. **python3**, to take the method checks out of agent mode. +4. **A .NET test project**, so `dotnet test` stops exiting `0` on nothing. +5. **An RLS isolation suite** for owner A / owner B / anonymous. +6. **python3**, to take the method checks out of agent mode. ## Outside the milestone, tracked elsewhere -Nineteen entries in `docs/KNOWN_ISSUES.md`, including the three contradictions -between the accepted ADRs and reality (`KI-016`, `KI-017`) and the absent -release pipeline (`KI-003`). Historical tickets T0001–T0005 and the superseded +Nineteen entries in `docs/KNOWN_ISSUES.md`. `KI-008` and `KI-016` closed with +ADR-0005; the stack drift (`KI-017`) and the absent release pipeline (`KI-003`) +remain open. Historical tickets T0001–T0005 and the superseded documentation set are in `docs/archive/`; they are context, never law. ## Steering - **Keep the application, harden the code — `PacoRCS`, 2026-08-12.** No rewrite. Every existing feature is preserved; the work is best practices, smaller - files, reliability and security. Reverses ADR-0002; pending ADR-0005. + files, reliability and security. Recorded as ADR-0005, accepted 2026-08-12, + superseding ADR-0002. - **Reliability and security win arbitrations — `PacoRCS`, 2026-08-12.** Where one option is more reliable and more secure, take it, including for runtime and dependency versions. diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index e3725ef..d3efe9d 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -52,7 +52,7 @@ KI-019 were found during the bootstrap audit. | KI-005 | Medium | Client | Several React pages mix UI, business rules and data access. | Initial audit | hardening | Open | | KI-006 | Medium | Docs | Historical documentation partially out of sync. | Initial audit | bootstrap | Resolved | | KI-007 | Medium | Product | Solo vs collaborative VA undecided. | ADR-0001: solo MVP prepared for later collaboration | T0002 | Resolved | -| KI-008 | High | Rebuild | A total rewrite could drop uncharacterised current behaviour. | ADR-0002: weak automated coverage against the existing scope | ADR-0005 | Open | +| KI-008 | High | Rebuild | A total rewrite could drop uncharacterised current behaviour. | ADR-0002: weak automated coverage against the existing scope. Closed by ADR-0005 (accepted 2026-08-12): no rewrite happens, so nothing is dropped. | ADR-0005 | Resolved | | KI-009 | High | Bridge | No replayable SimConnect trace corpus exists to prove flight-engine parity. | T0001 and ADR-0002 | SimConnect slice | Open | | KI-010 | High | Data | Once real data exists in the new schema, no return to the old product is possible. | ADR-0002: no backward compatibility | distribution | Accepted | | KI-011 | High | Support | MSFS 2024 Store and Steam channels each need their own proof; only one test machine is available. | T0004 and ADR-0003 | platform validation | Open | @@ -60,7 +60,7 @@ KI-019 were found during the bootstrap audit. | KI-013 | Medium | Desktop | Tauri/WebView2's real gain over a native .NET shell is unmeasured on the target profile. | ADR-0004 | later | Open | | KI-014 | Medium | Data | Local Supabase is not identical to cloud; PostgreSQL 17 must be confirmed per project. | Supabase docs consulted in T0005 | later | Open | | KI-015 | High | Bridge | The official managed SimConnect SDK documents .NET Framework and depends on an SDK install; self-contained publication is unproven. | MSFS 2024 SimConnect docs consulted in T0005 | SimConnect slice | Open | -| KI-016 | High | Architecture | ADR-0002 mandates a rewrite in a new repository with fresh history; this is the original repository with `legacy/` carved out in place, and the owner has reversed the rewrite decision. | `git log --diff-filter=A -- legacy` → `fc3316d`; owner, 2026-08-12 | ADR-0005 | Open | +| KI-016 | High | Architecture | ADR-0002 mandated a rewrite in a new repository with fresh history; this is the original repository with `legacy/` carved out in place. | `git log --diff-filter=A -- legacy` → `fc3316d`. Closed by ADR-0005 (accepted 2026-08-12), which supersedes ADR-0002 and matches the decision to reality. | ADR-0005 | Resolved | | KI-017 | High | Supply chain | ADR-0004 pins .NET 10, pnpm 11, TypeScript 6, React 19, Tauri 2.11 and a `rust-toolchain.toml`. Actual: `net8.0`, npm, TS 5.9.3, React 18.3.1, no `rust-toolchain.toml`. .NET 8 support ends November 2026. | `Thrustline.Bridge.csproj`; `npm outdated` 2026-08-12; `ls rust-toolchain.toml` → absent | ADR-0005 then upgrade unit | Open | | KI-018 | Medium | Bridge | The weather routes build a static `new HttpClient()` although `IHttpClientFactory` is registered and used correctly elsewhere — no DNS refresh, risk of socket exhaustion. | `sim-bridge/Program.cs:178` vs `:57` and `:119` | hardening | Open | | KI-019 | Medium | Data | No database reset or seed command exists: recreating a schema is entirely manual, so a failed run is not diagnosable. | No Supabase CLI task, npm script or seed in the repo, verified 2026-08-12 | hardening | Open | diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 90b7577..7ac85dd 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -92,7 +92,7 @@ A question listed here is a legitimate reason for a unit to sit in `Draft`. | Question | Blocks | Asked on | | --- | --- | --- | | Which capabilities are genuinely shipped and merged, and by what evidence? Nothing user-facing is currently proven. | `CURRENT_STATE.md`, every unit that assumes a working baseline | 2026-08-12 | -| Does the hardening happen in this repository, or in a new one seeded from this codebase? The owner is undecided. | repository strategy, ADR-0005 | 2026-08-12 | +| Does the hardened codebase later move to a fresh repository? ADR-0005 leaves this deliberately open; it blocks no unit. | nothing — answer before any distribution unit | 2026-08-12 | | Which of the four manifests is the single source of the version number? | the version reset to `0.0.1-alpha` | 2026-08-12 | | What is the account-deletion and data-retention policy? No irreversible deletion may be implemented before it exists. | any account-deletion unit | ADR-0001, 2026-07-24 | | Which MSFS 2024 channel — Microsoft Store or Steam — gets validated first? Only one machine is available, and a proof on one channel does not carry to the other. | `KI-011`, the SimConnect slice | ADR-0003, 2026-07-26 | diff --git a/docs/decisions/ADR-0002-strategie-de-refonte.md b/docs/decisions/ADR-0002-strategie-de-refonte.md index 5b6e560..318088e 100644 --- a/docs/decisions/ADR-0002-strategie-de-refonte.md +++ b/docs/decisions/ADR-0002-strategie-de-refonte.md @@ -1,10 +1,10 @@ # ADR-0002 — Réécriture totale isolée -Status: Accepted +Status: Superseded by ADR-0005 Date: 2026-07-24 Deciders: Andy (Product Owner) Supersedes: — -Superseded by: — +Superseded by: ADR-0005 (2026-08-12) ## Context diff --git a/docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md b/docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md index abebd13..6dba05f 100644 --- a/docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md +++ b/docs/decisions/ADR-0005-durcissement-au-lieu-de-reecriture.md @@ -1,7 +1,7 @@ # ADR-0005 — Harden the existing application instead of rewriting it Date: 2026-08-12 -Status: Proposed — awaiting acceptance by PacoRCS +Status: Accepted — PacoRCS, 2026-08-12 Decider: PacoRCS Supersedes: ADR-0002 diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 18a1d53..b1ea4db 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -18,10 +18,10 @@ agents réouvrent sans cesse une question ou suivent une règle devenue obsolèt | ADR | Sujet | Statut | | --- | --- | --- | | ADR-0001 | Modèle produit solo | Accepted | -| ADR-0002 | Stratégie de refonte | Remplacée par ADR-0005 (proposée) | +| ADR-0002 | Stratégie de refonte | Superseded by ADR-0005 | | ADR-0003 | Matrice de support Windows/MSFS | Accepted | | ADR-0004 | Stack cible et politique de versions | Accepted, à réexaminer (`KI-017`) | -| ADR-0005 | Durcissement au lieu de réécriture | Proposed | +| ADR-0005 | Durcissement au lieu de réécriture | Accepted | ## ADR encore à écrire