From a482bb18c46099b0c30e771f8debcf3682a60025 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sat, 18 Jul 2026 23:45:00 +0200 Subject: [PATCH] feat(fleet): fiscal profile + compliance calendar + ADC register (erp#54, T11 data) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fleet/profile/ goes from stub to the machine-readable business-rules surface the fleet reads (PRD agent-catalog document surface + compliance ADC framework): - fiscal.yaml — entity, VAT position, 8 rules (regime reel simplifie until 2026-12-31 -> quarterly CA3 from 2027-01-01 per LF 2025 art. 38; KM export autoliquidation 259-1 CGI box E2; FR 20% deductible; intra-EU reverse charge; FX 766/666; SaaS expensed; CCA 455 lane). Every rule carries effective_from/effective_until AND decision: adc-NNN; every date cites its PRD anchor as an inline comment (verified against factory origin/main). - calendar.yaml — 15 entries: acomptes TVA (2026-07 month-window, 2026-12-15), last CA12 FY-2026 (2027-05-04), CA3 quarterly windows, CFE (December), AG comptes annuels (2027-06-30), e-invoicing milestones (2026-09-01 reception, 2027-09-01 emission/e-reporting), URSSAF echeancier with the in-file NOTE that a real direct debit exists since May 2026 (erp#57 revisit of the payroll-dormant assumption), KM deferred due dates + renewal stub. - JSON Schemas for both + scripts/validate.py (stdlib-only: strict YAML-subset parser, JSON-Schema-subset checker, rule->ADC resolution, calendar checks). - decisions/ — ADC register: template + adc-001..005 Accepted formalizations (autoliquidation KM, FX->766/666, SaaS expensed, reel simplifie until abolition, CCA personal-card lane) + adc-006/007 Proposed stubs (retainer currency -> erp#53; capital path -> erp#51). Agents draft, the operator Accepts — never the reverse; immutable once merged, supersede never edit. - Mutation policy in-file: PRs only (T12 proposes, human merges). - Same-change: profile README stub -> real doc; fleet/README.md layout line and AGENTS.md fleet row updated (profile no longer a stub). Validation: PASS — 8 rules, 15 entries, 7 ADCs, 0 errors, 7 warnings (the warnings list exactly what awaits operator verification). Human gate left open on purpose: operator sanity-read of the calendar + Acceptance of adc-001..005. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh --- AGENTS.md | 2 +- fleet/README.md | 2 +- fleet/profile/README.md | 114 ++++- fleet/profile/calendar.schema.json | 45 ++ fleet/profile/calendar.yaml | 198 ++++++++ .../adc-001-km-autoliquidation-259-1.md | 75 +++ .../decisions/adc-002-fx-deltas-766-666.md | 70 +++ .../adc-003-saas-subscriptions-expensed.md | 62 +++ .../adc-004-reel-simplifie-until-abolition.md | 73 +++ .../adc-005-cca-personal-card-lane.md | 67 +++ .../decisions/adc-006-km-retainer-currency.md | 66 +++ .../decisions/adc-007-capital-deposit-path.md | 61 +++ fleet/profile/decisions/adc-template.md | 47 ++ fleet/profile/fiscal.schema.json | 59 +++ fleet/profile/fiscal.yaml | 158 +++++++ fleet/profile/scripts/validate.py | 435 ++++++++++++++++++ 16 files changed, 1531 insertions(+), 3 deletions(-) create mode 100644 fleet/profile/calendar.schema.json create mode 100644 fleet/profile/calendar.yaml create mode 100644 fleet/profile/decisions/adc-001-km-autoliquidation-259-1.md create mode 100644 fleet/profile/decisions/adc-002-fx-deltas-766-666.md create mode 100644 fleet/profile/decisions/adc-003-saas-subscriptions-expensed.md create mode 100644 fleet/profile/decisions/adc-004-reel-simplifie-until-abolition.md create mode 100644 fleet/profile/decisions/adc-005-cca-personal-card-lane.md create mode 100644 fleet/profile/decisions/adc-006-km-retainer-currency.md create mode 100644 fleet/profile/decisions/adc-007-capital-deposit-path.md create mode 100644 fleet/profile/decisions/adc-template.md create mode 100644 fleet/profile/fiscal.schema.json create mode 100644 fleet/profile/fiscal.yaml create mode 100644 fleet/profile/scripts/validate.py diff --git a/AGENTS.md b/AGENTS.md index aaf0924..7f48676 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ The [AI back-office PRD](https://gitea.arcodange.lab/arcodange-org/factory/src/b | `bin/arcodange` | operational CLI — read prod (invoices, payments, TVA, bank, templates, snapshot), sandbox writes, gated promote | | `.claude/skills/` | the skill catalog; each `SKILL.md` frontmatter carries its **Use when… / SKIP for…** triggers — read them before reinventing | | `test/` | Playwright provisioning POCs (sandbox write agent `ai_agent_sandbox`, rights in `provisionSandbox.ts` WRITE_IDS) | -| `fleet/` | the AI-agent fleet — atom registry + `atom.yaml` schema ([fleet/README.md](fleet/README.md)), class skeletons ([fleet/classes/](fleet/classes/)), worked example `invoice-extract`; golden sets + fiscal profile are stubs ([erp#39](https://gitea.arcodange.lab/arcodange-org/erp/issues/39), [erp#54](https://gitea.arcodange.lab/arcodange-org/erp/issues/54)) | +| `fleet/` | the AI-agent fleet — atom registry + `atom.yaml` schema ([fleet/README.md](fleet/README.md)), class skeletons ([fleet/classes/](fleet/classes/)), worked example `invoice-extract`; fiscal profile + compliance calendar + ADC register ([fleet/profile/](fleet/profile/README.md)); golden sets are stubs ([erp#39](https://gitea.arcodange.lab/arcodange-org/erp/issues/39)) | ## Operating rules for agents diff --git a/fleet/README.md b/fleet/README.md index 0346a97..e9039a0 100644 --- a/fleet/README.md +++ b/fleet/README.md @@ -47,7 +47,7 @@ fleet/ │ ├── prompt.md │ └── scripts/ ├── golden/ # per-atom golden sets — land with erp#39 -├── profile/ # fiscal.yaml + calendar.yaml + ADC register — land with erp#54 +├── profile/ # fiscal.yaml + calendar.yaml + ADC register + validator (profile/README.md) └── harness/ # multi-runtime harness layer: verifier tests + builder bench (harness/README.md) ``` diff --git a/fleet/profile/README.md b/fleet/profile/README.md index 047b51c..a05fe8d 100644 --- a/fleet/profile/README.md +++ b/fleet/profile/README.md @@ -1 +1,113 @@ -The machine-readable fiscal profile (`fiscal.yaml` + `calendar.yaml` + the ADC register, every rule dated `effective_from`) lands with [erp#54](https://gitea.arcodange.lab/arcodange-org/erp/issues/54), per the PRD [document surface](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/agent-catalog.md#the-document-surface-agents-read). +# fleet/profile/ — fiscal profile, compliance calendar, ADC register + +The machine-readable **business-rules surface** the fleet reads (T10 TVA +preparation, T03 VAT ventilation, T11 calendar & reminders): prompts stay thin +and rule-free ([fleet/README.md](../README.md)); the rules live here, dated and +justified. Shipped by [erp#54](https://gitea.arcodange.lab/arcodange-org/erp/issues/54) +per the PRD [document surface](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/agent-catalog.md#the-document-surface-agents-read) +and [ADC framework](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc). + +## Layout + +| File | Role | +| --- | --- | +| [`fiscal.yaml`](fiscal.yaml) | fiscal profile: entity, VAT position, **rules** — regime, VAT treatments, bookkeeping methods | +| [`fiscal.schema.json`](fiscal.schema.json) | JSON Schema for `fiscal.yaml` | +| [`calendar.yaml`](calendar.yaml) | compliance calendar: dated obligations the T11 reminder loop ([erp#60](https://gitea.arcodange.lab/arcodange-org/erp/issues/60)) reads | +| [`calendar.schema.json`](calendar.schema.json) | JSON Schema for `calendar.yaml` | +| [`scripts/validate.py`](scripts/validate.py) | stdlib-only validator: schema + referential integrity (rule → ADC) | +| [`decisions/`](decisions/) | the **ADC register** (*accounting decision records*): `adc-template.md` + `adc-NNN-.md` | + +## The rule contract + +Every `fiscal.yaml` rule carries: + +- **`effective_from` / `effective_until`** — dated validity (`null` until = in + force until superseded); +- **`decision: adc-NNN`** — the record in `decisions/` that justifies it. + +So the chain **écriture → règle → ADC → base légale** resolves in four hops, +identically every time (PRD compliance § ADC rule 2, challenge C6). Dates in +both YAML files cite their source anchor as an inline comment (`PRD `, verified against factory `origin/main`). + +## The ADC register (`decisions/`) + +MADR-lite + accounting fields — see [`adc-template.md`](decisions/adc-template.md). +Frontmatter is machine-parsed (id, status, decided, effective dates, +supersedes/superseded_by); the body carries Context (*fait générateur*), +Decision, **Base légale & doctrine**, Alternatives rejected, Consequences, +QA & validation, References. + +Status flow: **Proposed → Accepted → Superseded-by-NNN**, with four rules +(PRD compliance § ADC): + +1. **Immutable once Accepted** (merged on main) — supersede, never edit + (permanence des méthodes, PCG art. 121-5). +2. **Every fiscal.yaml rule cites its ADC** — enforced by `scripts/validate.py`, + which also refuses a rule citing a non-Accepted record. +3. **The annexe writes itself at close** from the records Accepted and in + force during the exercice. +4. **Proposed ADCs are the expert-comptable agenda. Agents draft, the + operator Accepts — never the reverse.** + +Seed state: **adc-001…005 are Accepted as formalizations** of decisions the +operator already made in practice (their sanity read is the erp#54 human +gate — pre-merge amendments are draft edits, not supersedes); +**adc-006/adc-007 are Proposed stubs** (retainer currency → +[erp#53](https://gitea.arcodange.lab/arcodange-org/erp/issues/53); capital path → +[erp#51](https://gitea.arcodange.lab/arcodange-org/erp/issues/51)). + +## Validation + +```sh +python3 fleet/profile/scripts/validate.py +``` + +Stdlib-only (no PyYAML, no jsonschema — runs anywhere, CI-able). It checks: +both YAML files against their schemas; every rule's `decision` resolves to +exactly one existing, **Accepted** ADC; unique ids; `effective_from <= +effective_until`; calendar entries carry exactly one of `due`|`recurrence` +(unless `pending-definition`/`conditional`); ADC frontmatter/section hygiene. +Exit 0 = pass; warnings list what awaits operator verification +(month-precision dates, estimated entries). + +To stay parseable, both YAML files are written in a **strict subset**: block +maps/lists, single-line scalars, one-line `[flow]` lists of scalars, +comments — no anchors, no multi-line scalars, no nested flow. + +Calendar date precision: `YYYY-MM-DD` = exact; `YYYY-MM` = month window — the +source gives no day; verify on the authority's notice, **never invent one**. + +## Mutation policy + +Changes arrive **as PRs only** — the T12 regulatory watch *proposes* a diff +(what changed, source links, effective dates); a **human reviews and merges** +(PRD task-inventory § T11 guardrail: "the calendar file is reviewed like +code"; § T12: "the watch proposes, the human adopts"). Calendar entries +sourced from mail content are human-confirmed (mail is untrusted input). **No +agent edits these files directly.** A rule change moves its ADC in the same +PR (supersede, never edit), and a schema change moves the validator with it — +the repo-wide same-change freshness rule. + +## Facts not corroborated by the PRD + +Flagged here honestly (and inline where used) — the PRD wins on conflict; +these await operator/expert verification: + +- **SIREN 999657455** — from erp#54 + agent memory `arcodange-tva-fiscal`; + no PRD leaf states it. +- **Exact incorporation day** — PRD says "mid-January 2026" only; rules use a + conservative `2026-01-01` exercice-start bound. +- **July 2026 acompte exact day** — PRD gives the month only (T10 trigger). +- **CFE exact day + creation-year exoneration / initial 1447-C declaration** — + PRD says "December" only; the rest is to verify with the SIE. +- **CA3 July/October/January windows** — only the April window is PRD-anchored + (roadmap, 2027-04-20); the others come from the erp#54 issue text. +- **TVA credit amount (~223–267 EUR)** — ERP reads (skill + memory); the PRD + only says "in TVA credit — verify, don't assume". +- **URSSAF échéancier cadence/nature** — one known debit (2026-05-22, + 493.00 EUR, erp#57); the PRD's "payroll dormant" assumption is under revisit + in [erp#57](https://gitea.arcodange.lab/arcodange-org/erp/issues/57). +- **BOI-BIC-CHG-20-30-10 tolerance cite** (adc-003) and **CCA formalities** + (adc-005) — general doctrine, queued for the expert-comptable. diff --git a/fleet/profile/calendar.schema.json b/fleet/profile/calendar.schema.json new file mode 100644 index 0000000..62c93de --- /dev/null +++ b/fleet/profile/calendar.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "fleet/profile/calendar.schema.json", + "title": "Arcodange compliance calendar (fleet/profile/calendar.yaml)", + "description": "Dated obligations the T11 reminder loop (erp#60) reads. due 'YYYY-MM-DD' = exact date; 'YYYY-MM' = month window (source gives no day - verify on the authority's notice, never invent). Exactly one of due|recurrence per entry unless status is pending-definition/conditional (checked by scripts/validate.py, which implements the JSON-Schema subset {type, properties, required, additionalProperties, items, enum, pattern}).", + "type": "object", + "additionalProperties": false, + "required": ["version", "defaults", "entries"], + "properties": { + "version": { "type": "integer", "enum": [1] }, + "defaults": { + "type": "object", + "additionalProperties": false, + "required": ["reminders_days_before"], + "properties": { + "reminders_days_before": { "type": "array", "items": { "type": "integer" } }, + "reminders_repeat_until_acknowledged": { "type": "boolean" } + } + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "title", "category", "source"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, + "title": { "type": "string" }, + "category": { "type": "string", "enum": ["fiscal", "legal", "regulatory", "social", "receivable", "contract"] }, + "nature": { "type": "string", "enum": ["regulatory-hard", "fiscal-hard", "legal-hard", "engineering", "informational"] }, + "status": { "type": "string", "enum": ["confirmed", "estimated", "conditional", "pending-definition"] }, + "due": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}(-[0-9]{2})?$" }, + "recurrence": { "type": "string", "enum": ["yearly", "quarterly", "monthly", "unknown"] }, + "months": { "type": "array", "items": { "type": "integer" } }, + "effective_from": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "effective_until": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "authority": { "type": "string" }, + "related_task": { "type": "string", "pattern": "^T[0-9]{2}$" }, + "source": { "type": "string" }, + "notes": { "type": "string" } + } + } + } + } +} diff --git a/fleet/profile/calendar.yaml b/fleet/profile/calendar.yaml new file mode 100644 index 0000000..b8e543e --- /dev/null +++ b/fleet/profile/calendar.yaml @@ -0,0 +1,198 @@ +# ============================================================================ +# fleet/profile/calendar.yaml — Arcodange compliance calendar (machine-readable, T11) +# ============================================================================ +# The dated obligations the T11 reminder loop (erp#60) reads; maintained in git +# and "reviewed like code" (PRD task-inventory § T11 guardrail). +# +# PRD root: https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office +# All "PRD § " comments below were verified against the factory +# repo at origin/main on 2026-07-18. +# +# MUTATION POLICY — same as fiscal.yaml: changes arrive as PRs ONLY (T12 +# regulatory watch proposes, a human merges); calendar entries proposed from +# mail content are confirmed by a human (PRD task-inventory § T11 step 4: mail +# is untrusted input). No agent edits this file directly. +# +# DATE PRECISION — due "YYYY-MM-DD" = exact date; "YYYY-MM" = month window: +# the source gives no day — verify on the authority's own notice (avis +# d'acompte, espace pro), NEVER invent one. +# +# Schema: calendar.schema.json — validate with: python3 scripts/validate.py +# ============================================================================ + +version: 1 + +defaults: + reminders_days_before: [30, 7, 1] # PRD task-inventory § T11 step 2 — "reminders at D-30/D-7/D-1 via Telegram"; the reminder LOOP itself is erp#60, not this file + reminders_repeat_until_acknowledged: true # PRD task-inventory § T11 guardrail — "reminders repeat until acknowledged - silence is never treated as done" + +entries: + + # --- TVA, réel simplifié era (until 2026-12-31) --------------------------- + + - id: "tva-acompte-2026-07" + title: "Acompte de TVA de juillet 2026 (réel simplifié)" + category: "fiscal" + nature: "fiscal-hard" + status: "estimated" + due: "2026-07" # PRD task-inventory.md § T10 trigger — "acompte July 2026"; month only — the exact day is on the avis d'acompte (NOT in the PRD) + authority: "DGFiP" + related_task: "T10" + source: "PRD task-inventory.md § T10 trigger" + notes: "Expected approx 0 EUR while in TVA credit - verify on impots.gouv.fr, never assume (PRD T10). First year: no prior CA12 to base an acompte on. A DGFiP mail of 2026-06-25 announced this acompte (agent memory arcodange-tva-fiscal - not a PRD leaf)." + + - id: "tva-acompte-2026-12" + title: "Acompte de TVA de décembre 2026 (réel simplifié)" + category: "fiscal" + nature: "fiscal-hard" + status: "confirmed" + due: "2026-12-15" # PRD roadmap.md § Milestones — "2026-12-15 | Acompte TVA de décembre (≈ 0 € expected — verify) | fiscal — hard" + authority: "DGFiP" + related_task: "T10" + source: "PRD roadmap.md § Milestones + README.md § regulatory table (2026-12 row)" + notes: "POC-4a dry-run scheduled 2026-11-30 → 2026-12-11 (PRD roadmap Gantt, e2) - the first euro-parity proof. Expected approx 0 EUR while in credit - verify, never assume." + + - id: "tva-ca12-fy2026" + title: "CA12 FY-2026 (form 3517) - the LAST CA12, recovers the TVA credit" + category: "fiscal" + nature: "fiscal-hard" + status: "confirmed" + due: "2027-05-04" # PRD roadmap.md § Milestones — "2027-05-04 | CA12 FY-2026 filed (TVA credit recovery) | fiscal — hard (early-May window)" + authority: "DGFiP" + related_task: "T10" + source: "PRD roadmap.md § Milestones + task-inventory.md § T10 trigger ('CA12 for FY 2026 ~May 2027')" + notes: "Last CA12 ever: the regime is abolished 2027-01-01 (fiscal.yaml vat-regime-ca3-quarterly, adc-004). Recovery point for the cumulated TVA credit, including pre-creation deductible VAT. Prep window 2027-04-19 → 2027-05-03 (PRD roadmap Gantt, e5)." + + # --- TVA, CA3 era (from 2027-01-01) --------------------------------------- + + - id: "regime-simplifie-abolition" + title: "Régime réel simplifié abolished → quarterly CA3 regime starts" + category: "regulatory" + nature: "regulatory-hard" + status: "confirmed" + due: "2027-01-01" # PRD roadmap.md § Milestones — "2027-01-01 | Régime simplifié abolished → quarterly CA3 | regulatory — hard" + authority: "DGFiP" + related_task: "T10" + source: "PRD roadmap.md § Milestones; LF 2025 art. 38 per task-inventory.md § T10 trigger" + notes: "Regime switch, not a filing. From here fiscal.yaml rule vat-regime-ca3-quarterly is in force (decision adc-004)." + + - id: "ca3-2027-q1" + title: "First real CA3 (2027-Q1) filing" + category: "fiscal" + nature: "fiscal-hard" + status: "confirmed" + due: "2027-04-20" # PRD roadmap.md § Milestones — "2027-04-20 | First real CA3 (2027-Q1) filed | fiscal — hard (April window)" + authority: "DGFiP" + related_task: "T10" + source: "PRD roadmap.md § Milestones + Gantt (e4: prepare 2027-04-01 → 2027-04-16)" + notes: "Preceded by POC-4b Q1 simulation + expert-comptable checkpoint 2027-01-11 → 2027-02-26 (PRD roadmap Gantt, e3): the expert checkpoint sits BEFORE the first real quarterly filing." + + - id: "ca3-quarterly" + title: "CA3 quarterly filing windows (April / July / October / January)" + category: "fiscal" + nature: "fiscal-hard" + status: "estimated" + recurrence: "quarterly" + months: [4, 7, 10, 1] # windows per erp#54 deliverable text; only the April window is PRD-anchored (roadmap § Milestones, 2027-04-20) + effective_from: "2027-01-01" # first covered quarter; first filing lands in the April 2027 window + effective_until: null + authority: "DGFiP" + related_task: "T10" + source: "erp#54 deliverable ('CA3 quarterly windows April/July/Oct/Jan'); April window corroborated by PRD roadmap.md § Milestones" + notes: "The July/October/January windows and each quarter's exact deadline are NOT corroborated by a PRD leaf - verify each on the espace pro before relying on a day." + + # --- Other fiscal --------------------------------------------------------- + + - id: "cfe-annual" + title: "CFE (cotisation foncière des entreprises) - December" + category: "fiscal" + nature: "fiscal-hard" + status: "estimated" + recurrence: "yearly" + months: [12] # PRD task-inventory.md § T11 step 1 — "CFE (cotisation foncière des entreprises, December)"; no day in the PRD + authority: "DGFiP" + related_task: "T10" + source: "PRD task-inventory.md § T11 step 1" + notes: "Exact day not in the PRD (statutorily mid-December - verify on the avis). NOT corroborated by the PRD and to verify with the SIE: whether the creation-year (2026) is exonerated and whether an initial declaration (1447-C) is due before 2026-12-31." + + - id: "is-installments" + title: "IS installments (once profitable)" + category: "fiscal" + status: "conditional" + recurrence: "unknown" + authority: "DGFiP" + related_task: "T10" + source: "PRD task-inventory.md § T11 step 1 — 'IS installments (once profitable)'" + notes: "Dormant until the first profitable exercice; dates get defined (via PR) when the condition triggers." + + # --- Legal ---------------------------------------------------------------- + + - id: "ag-comptes-annuels-fy2026" + title: "AG - approval of the FY-2026 annual accounts" + category: "legal" + nature: "legal-hard" + status: "confirmed" + due: "2027-06-30" # PRD roadmap.md § Milestones — "2027-06-30 | AG — FY-2026 accounts approved | legal — hard" + authority: "Greffe / AG" + source: "PRD roadmap.md § Milestones + task-inventory.md § T11 step 1 ('within 6 months of FY close → June 2027 for FY 2026')" + notes: "The annexe's règles et méthodes section is generated from the ADC register - records Accepted and in force during the exercice (PRD compliance.md § ADC, rule 3)." + + # --- E-invoicing milestones ----------------------------------------------- + + - id: "einvoicing-reception" + title: "E-invoice reception mandatory (all companies)" + category: "regulatory" + nature: "regulatory-hard" + status: "confirmed" + due: "2026-09-01" # PRD roadmap.md § Milestones — "2026-09-01 | E-invoice reception mandatory | regulatory — hard" + authority: "DGFiP" + source: "PRD roadmap.md § Milestones + README.md § regulatory table" + notes: "PA (plateforme agréée) leaning Qonto - decision D4, ADR due 2026-08-14 with a fallback window to 2026-08-28 (PRD roadmap Gantt, P2). Reception must be wired and verified BEFORE this date." + + - id: "einvoicing-emission-ereporting" + title: "E-invoice emission + e-reporting mandatory (PME)" + category: "regulatory" + nature: "regulatory-hard" + status: "confirmed" + due: "2027-09-01" # PRD roadmap.md § Milestones — "2027-09-01 | E-reporting + emission mandatory (PME) | regulatory — hard" + authority: "DGFiP" + source: "PRD roadmap.md § Milestones + README.md § regulatory table" + notes: "Covers e-reporting of the KM export invoices (PRD README § three dated regulatory obligations; task-inventory § T05 step 5). P6 pipeline work starts 2027-05-03 (roadmap Gantt, f1)." + + # --- Social (URSSAF) ------------------------------------------------------ + + - id: "urssaf-echeancier" + title: "URSSAF échéancier (cotisations - nature to clarify)" + category: "social" + status: "pending-definition" + recurrence: "unknown" + authority: "URSSAF" + source: "erp#54 deliverable + erp#57 (2026-05-22 direct debit evidence)" + notes: "NOTE - a real URSSAF direct debit exists since May 2026 (2026-05-22, 493.00 EUR, target account 6451 - erp#57): the PRD's 'URSSAF/DSN payroll declarations (dormant until first salary)' assumption (task-inventory § T11 step 1) needs revisiting. [HUMAN] clarification of what these cotisations are (president? CFP?) is tracked in erp#57. Do not fire payroll/DSN reminders until clarified; do not invent a cadence." + + # --- Contract-driven (KissMetrics) ---------------------------------------- + + - id: "km-fac005-due" + title: "KM invoice FAC005 due (deferred schedule)" + category: "receivable" + status: "confirmed" + due: "2026-08-23" # PRD task-inventory.md § Backlog, 'Contract lifecycle' — "FAC005/FAC007 validated, due 2026-08-23 & 2026-09-23" + related_task: "T06" + source: "PRD task-inventory.md § Backlog — deferred schedule 'now IN the ERP'; rides this calendar per the same bullet ('#54/#60')" + notes: "Receivable watch input for T06 dunning - cross-check against the contracted deferred cycle, not naive due dates." + + - id: "km-fac007-due" + title: "KM invoice FAC007 due (deferred schedule)" + category: "receivable" + status: "confirmed" + due: "2026-09-23" # PRD task-inventory.md § Backlog, 'Contract lifecycle' — "FAC005/FAC007 validated, due 2026-08-23 & 2026-09-23" + related_task: "T06" + source: "PRD task-inventory.md § Backlog — deferred schedule 'now IN the ERP'; rides this calendar per the same bullet ('#54/#60')" + notes: "Receivable watch input for T06 dunning - cross-check against the contracted deferred cycle, not naive due dates." + + - id: "km-contract-renewal-4pct-window" + title: "KM contract renewal + 4 %-window obligation (dates TBD)" + category: "contract" + status: "pending-definition" + source: "PRD task-inventory.md § Backlog, 'Contract lifecycle' — 'renewal reminders + the 4 %-window obligation ride the compliance calendar (#54/#60)'" + notes: "Exact dates and terms land with the contract objects (erp#65 phase 2 / erp#67). Do not invent dates - this stub only reserves the slot the PRD points at this calendar." diff --git a/fleet/profile/decisions/adc-001-km-autoliquidation-259-1.md b/fleet/profile/decisions/adc-001-km-autoliquidation-259-1.md new file mode 100644 index 0000000..eab6398 --- /dev/null +++ b/fleet/profile/decisions/adc-001-km-autoliquidation-259-1.md @@ -0,0 +1,75 @@ +--- +id: adc-001 +title: "KM client invoices under autoliquidation - art. 259-1° CGI (no French VAT, box E2)" +status: Accepted +decided: 2026-07-18 +effective_from: 2026-01-01 +effective_until: null +supersedes: null +superseded_by: null +--- + +# adc-001 — KM client invoices under autoliquidation — art. 259-1° CGI + +> **Accepted as formalization** of practice in force "since the first invoice" +> (PRD [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc)). +> The operator's sanity read is the erp#54 human gate; amendments before the PR +> merges are edits to a draft — immutability starts at merge on main. + +## Context + +KissMetrics (US) is the sole client: B2B services exported outside the EU. The +first client invoice forced the VAT position — charge French VAT, or treat the +supply as outside French VAT scope. + +## Decision + +B2B services supplied to a non-EU taxable customer are outside French VAT +scope (place of supply = where the customer is established). Therefore: +TVA collectée = 0 on every KM invoice; each invoice carries the autoliquidation +mention referencing art. 259-1° CGI; the basis reports in box E2 of the +CA3/CA12. Applied to 100 % of client revenue today. + +## Base légale & doctrine + +- CGI art. 259-1° — place of supply of B2B services is where the *preneur + assujetti* is established. +- CA3 box E2 mapping per the T10 mode opératoire (PRD task-inventory § T10 + step 1: "box E2 export — today 100 % of client revenue is box E2 + autoliquidation Art. 259-1°, collectée = 0"). +- Invoice mention checked structurally by the `dolibarr-invoice-audit` skill + ("TVA 259-1° CGI"). +- Expert-comptable confirmation pending — on the PRD compliance.md § Questions + list (PAF documentation expectations for the export invoices). + +## Alternatives rejected + +- **Charging French VAT** — wrong place-of-supply analysis for extra-EU B2B + services; would create undue TVA collectée. +- **Franchise en base** — would renounce the right to deduct while the company + runs a structural VAT credit (all revenue exported, FR expenses carrying + 20 % VAT). + +## Consequences + +- Structural TVA credit while KM is the sole client (collectée 0 vs déductible + on FR supplier invoices) — surfaced in `fiscal.yaml` `vat_position`. +- Downstream rule: `fiscal.yaml` `client-export-autoliquidation`. +- E-invoicing: these export invoices stay OUT of domestic e-invoicing scope + but ENTER e-reporting on 2027-09-01 (PRD compliance.md § Obligations, PAF + row; roadmap § Milestones) — the PAF stays load-bearing for them. +- Annexe: the VAT method is disclosed from this record at close (ADC rule 3). + +## QA & validation + +- `dolibarr-tva-reconciliation` classifies every KM invoice in bucket E2 + (autoliquidation, collectée = 0) — in production. +- `dolibarr-invoice-audit` verifies the invoice mention per invoice. +- Expert-comptable review: pending (PRD compliance.md § Questions). + +## References + +- PRD compliance.md § ADC seed table (adc-001) + § Obligations (PAF row). +- PRD task-inventory.md § T05 step 2, § T10 step 1. +- `fiscal.yaml` rule `client-export-autoliquidation`. +- erp#54 (this register's seeding issue). diff --git a/fleet/profile/decisions/adc-002-fx-deltas-766-666.md b/fleet/profile/decisions/adc-002-fx-deltas-766-666.md new file mode 100644 index 0000000..c8054f4 --- /dev/null +++ b/fleet/profile/decisions/adc-002-fx-deltas-766-666.md @@ -0,0 +1,70 @@ +--- +id: adc-002 +title: "FX deltas on customer wires booked to 766 / 666, invoices never adjusted" +status: Accepted +decided: 2026-07-18 +effective_from: 2026-01-01 +effective_until: null +supersedes: null +superseded_by: null +--- + +# adc-002 — FX deltas on customer wires → 766 / 666 + +> **Accepted as formalization** — the method was applied in the 2026-07-11 +> books regularization (PRD [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc)). +> Effective from the exercice start (ADC rule 1: effective dates sit on +> exercice boundaries where possible). Operator sanity read = erp#54 human gate. + +## Context + +KissMetrics pays in USD via Wise; the EUR amounts settled differ from the EUR +invoice totals. Observed deltas: +1.08 EUR on FAC004 and +50.05 EUR on FAC006 +(erp#57). The 2026-07-11 regularization forced the choice of where the delta +lives. + +## Decision + +The customer payment is recorded at the invoice's **remaining EUR amount** +(full-remaining), so the invoice closes exactly. The difference against the +EUR actually received is booked **separately**: gain → account 766 (gains de +change), loss → account 666 (pertes de change). A validated invoice is never +adjusted for FX. + +## Base légale & doctrine + +- PCG chart — account 666 *pertes de change* / account 766 *gains de change* + (charges et produits financiers). +- Inaltérabilité of validated entries (Code de commerce L.123-22; PRD + compliance.md § The ledger grammar): corrections are new documents, never + edits — the separate 766/666 entry is the append-shaped correction. + +## Alternatives rejected + +- **Adjusting the invoice amount to the settled EUR** — violates the ledger + grammar (a validated document is immutable) and inaltérabilité. +- **USD multicurrency invoicing** — open question, not rejected: it is the + subject of adc-006 (erp#53). If Accepted, adc-006 would supersede or narrow + this record's scope for future invoices. + +## Consequences + +- Accounts touched: 766, 766-side produits / 666-side charges financiers. +- Downstream rule: `fiscal.yaml` `fx-deltas-766-666`. +- Annexe: FX-treatment method disclosed from this record at close. +- Interaction: pending adc-006 (retainer currency) may change where FX lives + for future invoices; until then this method applies unchanged. + +## QA & validation + +- Applied in the 2026-07-11 regularization; rehearsed on the sandbox (replay + pack preserved under + `.claude/skills/dolibarr-sandbox-write/replay-packs/2026-07-11-books-regularization/`). +- The two écart entries themselves are [HUMAN] UI work tracked in erp#57. +- Expert-comptable review: pending (rides the compliance.md § Questions list). + +## References + +- PRD compliance.md § ADC seed table (adc-002) + § The ledger grammar. +- erp#57 (écarts +1.08 FAC004 / +50.05 FAC006), erp#53 / adc-006. +- `fiscal.yaml` rule `fx-deltas-766-666`. diff --git a/fleet/profile/decisions/adc-003-saas-subscriptions-expensed.md b/fleet/profile/decisions/adc-003-saas-subscriptions-expensed.md new file mode 100644 index 0000000..60a673a --- /dev/null +++ b/fleet/profile/decisions/adc-003-saas-subscriptions-expensed.md @@ -0,0 +1,62 @@ +--- +id: adc-003 +title: "AI/SaaS subscriptions expensed (6xx), never immobilized at current unit costs" +status: Accepted +decided: 2026-07-18 +effective_from: 2026-01-01 +effective_until: null +supersedes: null +superseded_by: null +--- + +# adc-003 — AI/SaaS subscriptions expensed (6xx), never immobilized + +> **Accepted as formalization** of what the PRD seed table calls "implicit +> today" (PRD [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc)). +> Operator sanity read = erp#54 human gate. + +## Context + +Arcodange's supplier side is dominated by recurring AI/SaaS subscriptions +(Anthropic, Cloudflare, OVH, etc.) at small monthly unit costs. Each entry +raised the same implicit question: expense (6xx) or immobilize (20x/205)? + +## Decision + +AI/SaaS subscription costs are **expensed to class-6 accounts in the period +invoiced**, never immobilized at current unit costs. Review clause: if a +perpetual licence or a single subscription materially exceeds the +small-equipment tolerance, a **superseding ADC is drafted before the method +changes** (permanence des méthodes). + +## Base légale & doctrine + +- PCG art. 211-1 — an asset requires control and expected future economic + benefits; a monthly subscription confers neither beyond the period billed. +- BOI-BIC-CHG-20-30-10 — the 500 EUR HT small-equipment tolerance. *General + doctrine cite, not sourced from a PRD leaf — expert-comptable confirmation + requested.* +- Subscriptions are services (no transferred right of ownership) → charges. + +## Alternatives rejected + +- **Immobilizing (205 concessions et droits similaires) + amortization** — + disproportionate tracking for small monthly amounts and no durable right is + acquired; rejected at current unit costs. + +## Consequences + +- Accounts touched: class-6 service/subscription charge accounts. +- Downstream rule: `fiscal.yaml` `saas-subscriptions-expensed`. +- Annexe: immobilization policy disclosed from this record at close. + +## QA & validation + +- Consistent with every supplier entry recorded to date (ERP state). +- Expert-comptable review: pending — specifically the BOI tolerance cite above. + +## References + +- PRD compliance.md § ADC seed table (adc-003). +- `fiscal.yaml` rule `saas-subscriptions-expensed`. +- erp#54 (register seeding). diff --git a/fleet/profile/decisions/adc-004-reel-simplifie-until-abolition.md b/fleet/profile/decisions/adc-004-reel-simplifie-until-abolition.md new file mode 100644 index 0000000..c3c011a --- /dev/null +++ b/fleet/profile/decisions/adc-004-reel-simplifie-until-abolition.md @@ -0,0 +1,73 @@ +--- +id: adc-004 +title: "Stay régime réel simplifié until the legal abolition; quarterly CA3 from 2027-01-01" +status: Accepted +decided: 2026-07-18 +effective_from: 2026-01-01 +effective_until: null +supersedes: null +superseded_by: null +--- + +# adc-004 — Stay réel simplifié until abolition; quarterly CA3 from 2027-01-01 + +> **Accepted as formalization** — "Accepted in practice" per the PRD +> [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc). +> Operator sanity read = erp#54 human gate. + +## Context + +Arcodange holds the régime réel simplifié de TVA (CA12 + acomptes) for its +first exercice (FY-2026). LF 2025 art. 38 abolishes that regime on 2027-01-01 +(PRD task-inventory § T10 trigger), which forced the question: opt out early +to the réel normal, or ride the regime to its legal end? + +## Decision + +Stay under the **régime réel simplifié** (annual CA12 form 3517 + July and +December acomptes) until the abolition — **no early opt-out**. From +**2027-01-01**, file **quarterly CA3** (turnover under 1 M EUR; monthly above +that threshold). A last CA12 covering FY-2026 is still filed in 2027 +(~2027-05-04 per the PRD roadmap), recovering the accumulated TVA credit. + +## Base légale & doctrine + +- CGI art. 302 septies A — régime simplifié d'imposition. +- LF 2025 art. 38 — abolition effective 2027-01-01 (PRD task-inventory § T10). +- CGI art. 287 — CA3 filing; the 1 M EUR monthly threshold noted in PRD + task-inventory § T12 step 1. +- Regime holder status confirmed by a DGFiP mail of 2026-06-25 (agent memory + `arcodange-tva-fiscal`) — *evidence outside the PRD; keep the mail*. + +## Alternatives rejected + +- **Early opt-out to réel normal (monthly/quarterly CA3 from 2026)** — adds + declarations for zero benefit while the company sits in a structural TVA + credit, and the switch is imposed on 2027-01-01 anyway. + +## Consequences + +- Downstream rules (`fiscal.yaml`): `vat-regime-reel-simplifie`, + `vat-regime-ca3-quarterly`, **and the standard-law VAT ventilation operated + under this regime** — `supplier-fr-vat-deductible` (CGI art. 271) and + `supplier-intra-eu-reverse-charge` (CGI art. 283-2) cite this record as + their operating decision: they are law application, not method choices, and + this record is where that ventilation is documented. +- Calendar entries: `tva-acompte-2026-07`, `tva-acompte-2026-12`, + `tva-ca12-fy2026`, `regime-simplifie-abolition`, `ca3-2027-q1`, + `ca3-quarterly`. +- Annexe: VAT regime + change of regime (imposed by law) disclosed at close. + +## QA & validation + +- POC-4a: December 2026 acompte dry-run (2026-11-30 → 2026-12-11, PRD roadmap + Gantt e2) — first euro-parity proof. +- POC-4b: 2027-Q1 CA3 simulation + expert-comptable checkpoint BEFORE the + first real quarterly filing (PRD roadmap Gantt e3). + +## References + +- PRD roadmap.md § Milestones (2027-01-01, 2027-04-20, 2027-05-04). +- PRD task-inventory.md § T10 trigger + § T11 step 1 + § T12 step 1. +- PRD compliance.md § ADC seed table (adc-004). +- `fiscal.yaml` rules listed under Consequences; erp#54. diff --git a/fleet/profile/decisions/adc-005-cca-personal-card-lane.md b/fleet/profile/decisions/adc-005-cca-personal-card-lane.md new file mode 100644 index 0000000..caa72d1 --- /dev/null +++ b/fleet/profile/decisions/adc-005-cca-personal-card-lane.md @@ -0,0 +1,67 @@ +--- +id: adc-005 +title: "Personal-card business expenses through the compte courant d'associé (455 lane)" +status: Accepted +decided: 2026-07-18 +effective_from: 2026-01-01 +effective_until: null +supersedes: null +superseded_by: null +--- + +# adc-005 — Personal-card business expenses through the CCA (455 lane) + +> **Accepted as formalization** — "practiced; formalized with erp#57" per the +> PRD [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc). +> Operator sanity read = erp#54 human gate. + +## Context + +Some business expenses are paid with the operator's personal card instead of +the Qonto/Wise business accounts — e.g. the Anthropic May/June receipts +(#2344-8391, #2997-4837) with no matching Qonto debit (erp#57). The books need +those expenses without any money moving through an agent. + +## Decision + +A business expense paid personally is recorded as a **normal supplier +invoice**, and its payment is booked against the **compte courant d'associé** +(account 455), via the Dolibarr bank account "G.RADUREAU CCA" +(`fk_account=3`). Reimbursement of the CCA balance is a **separate human +decision**, never automatic. + +## Base légale & doctrine + +- PCG account 455 — *associés, comptes courants*. +- CCA formalities (convention de compte courant, interest terms if any): + *not sourced from a PRD leaf — expert-comptable confirmation requested.* + +## Alternatives rejected + +- **Notes de frais module** — heavier machinery than a one-person SAS needs + today; can supersede this lane later if volume justifies it. +- **Leaving personal-card expenses out of the ERP** — breaks completeness and + the piste d'audit fiable (PRD compliance.md § Obligations, PAF row). + +## Consequences + +- Accounts touched: 455 (payment side) + the expense's class-6 account. +- The CCA bank account (`fk_account=3`) is **invisible via the bank APIs** — a + permanent, flagged manual lane in bank reconciliation (PRD task-inventory + § T07 guardrail), never silently ignored. +- Downstream rule: `fiscal.yaml` `personal-card-cca-lane`. +- Annexe: related-party (compte courant) balance disclosed at close. + +## QA & validation + +- Practiced in the current books; remaining entries (Anthropic clarification: + which payment rail) are [HUMAN] work tracked in erp#57. +- Expense-report/CSV visibility for the personal lane is an explicit PRD + backlog item (task-inventory § Backlog). + +## References + +- PRD compliance.md § ADC seed table (adc-005). +- PRD task-inventory.md § T07 guardrail + § Backlog (expense reports). +- erp#57 (bucket C — non-invoice entries). +- `fiscal.yaml` rule `personal-card-cca-lane`. diff --git a/fleet/profile/decisions/adc-006-km-retainer-currency.md b/fleet/profile/decisions/adc-006-km-retainer-currency.md new file mode 100644 index 0000000..da3f464 --- /dev/null +++ b/fleet/profile/decisions/adc-006-km-retainer-currency.md @@ -0,0 +1,66 @@ +--- +id: adc-006 +title: "KM retainer currency & composition (USD multicurrency vs EUR-at-settlement; line structure)" +status: Proposed +decided: null +effective_from: null +effective_until: null +supersedes: null +superseded_by: null +--- + +# adc-006 — KM retainer currency & composition (Proposed) + +> **Proposed — operator input needed.** This stub reserves the decision slot +> per the PRD [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc) +> ("blocks erp#53 wiring; operator input needed"). Agents may enrich the +> draft with sources and alternatives; **only the operator Accepts**. + +## Context + +The recurring template id=1 carries 5100 while the actual May/June drafts were +2×2145.92 + 2×2575.11 EUR; the 2026-07-11 regularization surfaced a probable +**USD-denominated retainer** ($5,500/mo = $2,500 net-30 + $3,000 deferred ++3 months — erp#53; contract WIP `test/km_contract.json` in the trunk). FX +deltas have already been booked twice under adc-002. The invoicing method must +be stable before T05 automation wires it (permanence des méthodes — not +improvised per month). + +## Decision + +**To arbitrate — no decision recorded.** Open questions (erp#53): + +1. USD multicurrency invoices vs EUR-at-settlement invoicing? +2. One invoice with two lines vs two invoices per month (net-30 part / + deferred part)? +3. Écarts de change policy interaction with adc-002 (would a USD-multicurrency + choice narrow or supersede it for future invoices)? + +## Base légale & doctrine + +To gather with the full draft: CGI art. 289 (invoice currency rules), PCG FX +treatment for multicurrency receivables, Dolibarr multicurrency module +implications. Nothing asserted here yet. + +## Alternatives rejected + +None yet — the alternatives above are open; each needs its why-it-loses +written before acceptance. + +## Consequences + +- **Blocks erp#53** (T05 monthly client invoice at A2): wiring waits for this + record to be Accepted. +- May supersede or narrow adc-002's scope for future invoices. +- Annexe: revenue-recognition/currency method disclosure once Accepted. + +## QA & validation + +Planned per erp#53: dry-run parity (first agent-fired invoice == its manual +twin), then one real month through the gate — after acceptance. + +## References + +- erp#53 (T05 client invoice; decision D3) · erp#54 (register seeding). +- PRD compliance.md § ADC seed table (adc-006). +- adc-002 (current FX method) · trunk WIP `test/km_contract.json`. diff --git a/fleet/profile/decisions/adc-007-capital-deposit-path.md b/fleet/profile/decisions/adc-007-capital-deposit-path.md new file mode 100644 index 0000000..99cba15 --- /dev/null +++ b/fleet/profile/decisions/adc-007-capital-deposit-path.md @@ -0,0 +1,61 @@ +--- +id: adc-007 +title: "Capital-deposit entry path (1013; module compta vs paiement divers)" +status: Proposed +decided: null +effective_from: null +effective_until: null +supersedes: null +superseded_by: null +--- + +# adc-007 — Capital-deposit entry path (Proposed) + +> **Proposed — awaits erp#51 findings** per the PRD +> [compliance › ADC seed table](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc). +> Agents may enrich the draft; **only the operator Accepts**. + +## Context + +The apport en capital of **1,000 EUR** (2026-01-21, notaire FOUREZ → compte +1013) is still unrecorded in the ERP (erp#57, bucket C). The clean entry path +depends on whether Dolibarr's double-entry accounting module (Comptabilité +expert) is enabled and chart-mapped — exactly what the erp#51 sandbox +verifications establish. + +## Decision + +**To arbitrate after erp#51 findings — no decision recorded.** Candidate +paths: + +1. Direct journal entry to 1013 via the accounting module (if enabled/mapped). +2. *Paiement divers* mapped to 1013 (bank-side entry) if the module path is + not available in time for FY-2026 close. + +## Base légale & doctrine + +- PCG account 1013 — *capital souscrit, appelé, versé*. +- Supporting documents: statuts + certificat du dépositaire (notaire FOUREZ). +- Module-side constraints: PRD compliance.md § Dolibarr verifications + (sandbox first). + +## Alternatives rejected + +None yet — the candidate paths above are open pending erp#51's finding on the +accounting-module state. + +## Consequences + +- FY-2026 balance-sheet completeness (capital line) — prerequisite for the AG + approving the FY-2026 accounts (calendar `ag-comptes-annuels-fy2026`). +- Blocks part of erp#57 (bucket C non-invoice entries). + +## QA & validation + +Planned: rehearse the chosen path on a fresh iso-prod sandbox checkpoint +first (erp#51 discipline), then the [HUMAN] prod entry per erp#57. + +## References + +- erp#51 (Dolibarr verifications) · erp#57 (bucket C) · erp#54 (seeding). +- PRD compliance.md § ADC seed table (adc-007) + § Dolibarr verifications. diff --git a/fleet/profile/decisions/adc-template.md b/fleet/profile/decisions/adc-template.md new file mode 100644 index 0000000..048e1f2 --- /dev/null +++ b/fleet/profile/decisions/adc-template.md @@ -0,0 +1,47 @@ +--- +id: adc-NNN +title: "" +status: Proposed # Proposed -> Accepted -> Superseded-by-NNN +decided: null # date the operator Accepted (a human act - agents draft, never Accept) +effective_from: null # aligned with the fiscal.yaml rules this record justifies; prefer an exercice boundary +effective_until: null # null = in force until superseded +supersedes: null # adc-NNN this record replaces, if any +superseded_by: null # set on THIS record when a successor is Accepted (the only edit allowed after acceptance) +--- + +# adc-NNN — + +> Format: MADR-lite + accounting fields, per the PRD +> [compliance › ADC framework](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/compliance.md#accounting-decision-records-adc). +> Status semantics: **agents draft, the operator Accepts — never the reverse.** +> Once Accepted (merged on main): **immutable — supersede, never edit** +> (permanence des méthodes, PCG art. 121-5). A method change is a new ADC with +> an explicit impact note and an effective date, ideally an exercice boundary. + +## Context + +The *fait générateur* — the transaction that forced a choice. + +## Decision + +The method chosen, stated so an agent can apply it mechanically. + +## Base légale & doctrine + +PCG / CGI / BOFiP / expert-comptable advice — every claim with its source. + +## Alternatives rejected + +Each alternative, with why it lost. + +## Consequences + +Accounts touched · annexe impact · downstream rules (fiscal.yaml rule ids). + +## QA & validation + +Sandbox rehearsal, euro-parity, expert review status. + +## References + +Example écritures, issues, PRD anchors. diff --git a/fleet/profile/fiscal.schema.json b/fleet/profile/fiscal.schema.json new file mode 100644 index 0000000..5a67459 --- /dev/null +++ b/fleet/profile/fiscal.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "fleet/profile/fiscal.schema.json", + "title": "Arcodange fiscal profile (fleet/profile/fiscal.yaml)", + "description": "Machine-readable fiscal profile per PRD agent-catalog 'The document surface agents read': schema'd YAML; every rule carries effective_from/effective_until AND decision: adc-NNN. The bundled validator (scripts/validate.py, stdlib-only) implements the subset {type, properties, required, additionalProperties, items, enum, pattern} used here.", + "type": "object", + "additionalProperties": false, + "required": ["version", "entity", "vat_position", "rules"], + "properties": { + "version": { "type": "integer", "enum": [1] }, + "entity": { + "type": "object", + "additionalProperties": false, + "required": ["name", "legal_form", "siren", "incorporated", "fiscal_year_end", "vat_registered", "b2b_only"], + "properties": { + "name": { "type": "string" }, + "legal_form": { "type": "string" }, + "siren": { "type": "string", "pattern": "^[0-9]{9}$" }, + "incorporated": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}(-[0-9]{2})?$" }, + "fiscal_year_end": { "type": "string", "pattern": "^[0-9]{2}-[0-9]{2}$" }, + "vat_registered": { "type": "boolean" }, + "b2b_only": { "type": "boolean" } + } + }, + "vat_position": { + "type": "object", + "description": "Informational ERP-read position, NOT a rule (no decision field): verify before any filing, never assume (PRD task-inventory T10).", + "additionalProperties": false, + "required": ["stance", "as_of", "note"], + "properties": { + "stance": { "type": "string", "enum": ["credit", "payable", "nil"] }, + "net_credit_eur_approx_min": { "type": "number" }, + "net_credit_eur_approx_max": { "type": "number" }, + "as_of": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "note": { "type": "string" } + } + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "title", "category", "statement", "effective_from", "effective_until", "decision", "legal_basis", "source"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, + "title": { "type": "string" }, + "category": { "type": "string", "enum": ["vat-regime", "vat-treatment", "bookkeeping"] }, + "statement": { "type": "string" }, + "effective_from": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "effective_until": { "type": ["string", "null"], "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "decision": { "type": "string", "pattern": "^adc-[0-9]{3}$" }, + "legal_basis": { "type": "array", "items": { "type": "string" } }, + "source": { "type": "string" }, + "notes": { "type": "string" } + } + } + } + } +} diff --git a/fleet/profile/fiscal.yaml b/fleet/profile/fiscal.yaml new file mode 100644 index 0000000..c13a343 --- /dev/null +++ b/fleet/profile/fiscal.yaml @@ -0,0 +1,158 @@ +# ============================================================================ +# fleet/profile/fiscal.yaml — Arcodange fiscal profile (machine-readable, T11) +# ============================================================================ +# What the fiscal atoms read (T10 TVA preparation, T03 VAT ventilation, T11 +# calendar) instead of carrying business rules in prompts — per the PRD +# agent-catalog § "The document surface agents read". +# +# PRD root: https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office +# All "PRD <leaf> § <anchor>" comments below were verified against the factory +# repo at origin/main on 2026-07-18. +# +# MUTATION POLICY — changes arrive as PRs ONLY. The T12 regulatory watch +# *proposes* a diff (source links + effective dates, PRD task-inventory § T12); +# a human reviews and merges (PRD task-inventory § T11 guardrail: "the calendar +# file is reviewed like code"). No agent edits this file directly. A rule +# change moves its ADC too — supersede, never edit (PRD compliance.md § ADC, +# rule 1: permanence des méthodes made structural). +# +# CONTRACT — every rule carries: +# effective_from / effective_until (null = in force until superseded) +# decision: adc-NNN (its justification record, decisions/) +# so the chain écriture → règle → ADC → base légale resolves in four hops +# (PRD compliance.md § ADC, rule 2). +# +# Schema: fiscal.schema.json — validate with: python3 scripts/validate.py +# (stdlib-only; this file is written in the strict YAML subset the validator +# parses: block maps/lists, single-line scalars, [flow] lists of scalars, +# comments — no anchors, no multi-line scalars; see ../profile README). +# ============================================================================ + +version: 1 + +entity: + name: "Arcodange" + legal_form: "SAS" # PRD README.md § The problem — "one-person SAS (software consulting)" + siren: "999657455" # erp#54 + agent memory arcodange-tva-fiscal — NOT corroborated by a PRD leaf (see README § Facts not corroborated by the PRD) + incorporated: "2026-01" # PRD compliance.md § Questions for the expert-comptable — "mid-January 2026 incorporation"; exact day not in the PRD (month precision on purpose) + fiscal_year_end: "12-31" # PRD compliance.md § Questions for the expert-comptable — "close 2026-12-31" + vat_registered: true # holder of the régime réel simplifié (rules below) + b2b_only: true # PRD compliance.md § Obligations, NF525 row — "Arcodange is B2B-only" + +# Informational ERP-read position, NOT a rule (hence no decision field). +# PRD README.md § regulatory table (2026-12 row) + task-inventory § T10 trigger: +# expected ≈ 0 € to pay while in credit — "verify, don't assume". +vat_position: + stance: "credit" # TVA collectée = 0 (100 % of revenue in box E2) vs déductible on FR supplier invoices + net_credit_eur_approx_min: 223.22 # dolibarr-tva-summary skill figure (all-time at skill authoring) — ERP read, not a PRD figure + net_credit_eur_approx_max: 267.12 # agent memory arcodange-tva-fiscal (prod `arcodange tva summary`, 2026-06-30) — ERP read, not a PRD figure + as_of: "2026-06-30" + note: "Approximation from ERP reads. Verify on the espace pro impots.gouv.fr before any filing - never assume (PRD task-inventory T10)." + +rules: + + # --- VAT regime ----------------------------------------------------------- + + - id: "vat-regime-reel-simplifie" + title: "Régime réel simplifié (CA12 + acomptes) for FY-2026" + category: "vat-regime" + statement: "Arcodange declares VAT under the régime réel simplifié: one annual CA12 (form 3517) plus two acomptes (July and December). No monthly or quarterly CA3 under this regime. Stay on it until its legal abolition - no early opt-out to the réel normal." + effective_from: "2026-01-01" # conservative exercice-start bound; incorporation is "mid-January 2026" (PRD compliance.md § Questions) — exact registration day NOT in the PRD + effective_until: "2026-12-31" # PRD task-inventory.md § T11 step 1 — "réel simplifié until 2026-12-31" + decision: "adc-004" + legal_basis: + - "CGI art. 302 septies A (régime simplifié d'imposition)" + - "LF 2025 art. 38 - abolition effective 2027-01-01 (PRD task-inventory § T10 trigger)" + source: "PRD task-inventory.md § T11 step 1 + § T10 trigger; PRD README.md § three dated regulatory obligations" + notes: "The regime was confirmed by a DGFiP mail of 2026-06-25 (agent memory arcodange-tva-fiscal) - not a PRD leaf; keep the mail as evidence." + + - id: "vat-regime-ca3-quarterly" + title: "Quarterly CA3 from the régime simplifié abolition" + category: "vat-regime" + statement: "From 2027-01-01 the régime réel simplifié is abolished: Arcodange files quarterly CA3 declarations (turnover under 1,000,000 EUR; the CA3 becomes monthly above that threshold). A last CA12 covering FY-2026 is still filed in 2027." + effective_from: "2027-01-01" # PRD roadmap.md § Milestones — "2027-01-01 | Régime simplifié abolished → quarterly CA3 | regulatory — hard" + effective_until: null # in force until superseded (e.g. crossing the 1 M€ threshold → monthly) + decision: "adc-004" + legal_basis: + - "LF 2025 art. 38 (PRD task-inventory § T10 trigger)" + - "CGI art. 287 (CA3 filing)" + - "1 M€ monthly threshold noted in PRD task-inventory § T12 step 1" + source: "PRD roadmap.md § Milestones + task-inventory.md § T10 trigger" + + # --- VAT treatment: clients ---------------------------------------------- + + - id: "client-export-autoliquidation" + title: "Client invoices to KissMetrics (extra-EU export): autoliquidation art. 259-1° CGI" + category: "vat-treatment" + statement: "B2B services to KissMetrics (US, extra-EU) are outside French VAT scope: the place of supply is where the customer is established. TVA collectée = 0 on every KM invoice; each invoice carries the autoliquidation mention referencing art. 259-1° CGI; the basis reports in box E2 of the CA3/CA12. Today this covers 100 % of client revenue." + effective_from: "2026-01-01" # practiced "since the first invoice" (PRD compliance.md § ADC seed table, adc-001); conservative exercice-start bound — the first invoice's exact date is not in the PRD + effective_until: null + decision: "adc-001" + legal_basis: + - "CGI art. 259-1° (place of supply of B2B services = the customer)" + - "CA3 box E2 mapping per PRD task-inventory § T10 step 1" + source: "PRD compliance.md § ADC seed table (adc-001) + task-inventory.md § T05 step 2 and § T10 step 1" + notes: "From 2027-09-01 these export invoices enter e-reporting scope (PRD roadmap § Milestones); they stay OUT of domestic e-invoicing scope, so the PAF remains load-bearing for them (PRD compliance.md § Obligations, PAF row)." + + # --- VAT treatment: suppliers -------------------------------------------- + + - id: "supplier-fr-vat-deductible" + title: "FR supplier invoices: VAT déductible at the invoiced rate (20 % standard)" + category: "vat-treatment" + statement: "Domestic French supplier invoices carry deductible French VAT at the invoiced rate - 20 % standard in practice. A rate outside the whitelist {0, 2.1, 5.5, 10, 20} without an explicit reverse-charge mention is quarantined, never defaulted." + effective_from: "2026-01-01" # standard-law ventilation applied since the first exercice (conservative exercice-start bound; see adc-004) + effective_until: null + decision: "adc-004" # standard law operated under the regime decision — recorded as a downstream rule in adc-004 § Consequences + legal_basis: + - "CGI art. 271 (droit à déduction)" + - "Rate whitelist per PRD task-inventory § T02 step 5" + source: "PRD task-inventory.md § T03 step 2 - 'correct VAT treatment per the fiscal profile: FR 20 % déductible, intra-EU reverse charge, etc.'" + + - id: "supplier-intra-eu-reverse-charge" + title: "Intra-EU B2B supplier invoices: reverse charge (autoliquidation)" + category: "vat-treatment" + statement: "Intra-EU B2B supplier invoices (e.g. Wise, BE) arrive without VAT: Arcodange self-assesses French VAT - declared as collectée AND déductible, net zero for a fully deductible business. Déductible-side CA3 mapping: lines 17+24." + effective_from: "2026-01-01" # same conservative exercice-start bound as supplier-fr-vat-deductible + effective_until: null + decision: "adc-004" # downstream rule of the regime decision, see adc-004 § Consequences + legal_basis: + - "CGI art. 283-2 (autoliquidation des prestations intracommunautaires)" + - "CA3 lines 17+24 mapping per the dolibarr-tva-deductible skill - ERP-side convention, not a PRD leaf" + source: "PRD task-inventory.md § T03 step 2 (intra-EU reverse charge) + § T10 step 1" + + # --- Bookkeeping methods -------------------------------------------------- + + - id: "fx-deltas-766-666" + title: "FX deltas on customer wires → 766 / 666" + category: "bookkeeping" + statement: "Customer payments are recorded at the invoice's remaining EUR amount; any difference against the EUR actually received is booked separately as gains de change (account 766) or pertes de change (account 666). A validated invoice is never adjusted for FX - corrections stay append-only per the ledger grammar." + effective_from: "2026-01-01" # method applies from exercice start; formalized in the 2026-07-11 books regularization (PRD compliance.md § ADC seed table, adc-002) + effective_until: null + decision: "adc-002" + legal_basis: + - "PCG chart accounts 666 (pertes de change) / 766 (gains de change)" + - "PRD compliance.md § The ledger grammar (append-only corrections)" + source: "PRD compliance.md § ADC seed table (adc-002); worked examples in erp#57 (+1.08 EUR FAC004, +50.05 EUR FAC006)" + + - id: "saas-subscriptions-expensed" + title: "AI/SaaS subscriptions expensed (6xx), never immobilized" + category: "bookkeeping" + statement: "AI/SaaS subscription costs are expensed to class-6 accounts in the period invoiced, never immobilized at current unit costs. If a perpetual licence or a single subscription ever materially exceeds the small-equipment tolerance, a superseding ADC is drafted before changing method." + effective_from: "2026-01-01" # implicit practice since the first exercice (PRD compliance.md § ADC seed table, adc-003: "implicit today") + effective_until: null + decision: "adc-003" + legal_basis: + - "PCG art. 211-1 (asset definition: control + future economic benefits)" + - "BOI-BIC-CHG-20-30-10 (500 EUR HT small-equipment tolerance) - general doctrine, not a PRD leaf; expert-comptable confirmation requested in adc-003" + source: "PRD compliance.md § ADC seed table (adc-003)" + + - id: "personal-card-cca-lane" + title: "Personal-card business expenses through the compte courant d'associé (455)" + category: "bookkeeping" + statement: "Business expenses paid with the operator's personal card are recorded as normal supplier invoices whose payment is booked against the compte courant d'associé (account 455; Dolibarr bank account G.RADUREAU CCA, fk_account=3). That account is invisible to the bank APIs - a permanent, flagged manual lane, never silently ignored. Reimbursement is a separate human decision." + effective_from: "2026-01-01" # practiced since the first exercice; formalized with erp#57 (PRD compliance.md § ADC seed table, adc-005) + effective_until: null + decision: "adc-005" + legal_basis: + - "PCG account 455 (associés - comptes courants)" + source: "PRD compliance.md § ADC seed table (adc-005) + task-inventory.md § T07 guardrail (fk_account=3 manual lane)" diff --git a/fleet/profile/scripts/validate.py b/fleet/profile/scripts/validate.py new file mode 100644 index 0000000..c779259 --- /dev/null +++ b/fleet/profile/scripts/validate.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +"""Validate the fleet/profile document surface (erp#54). + +Checks, in order: + 1. fiscal.yaml parses (strict YAML subset) and validates against fiscal.schema.json + 2. calendar.yaml parses and validates against calendar.schema.json + 3. Referential integrity: + - every fiscal rule's `decision: adc-NNN` resolves to exactly one + decisions/adc-NNN-*.md whose frontmatter status is Accepted + (agents draft, the operator Accepts - a rule may only rely on an + Accepted decision) + - rule/entry ids unique; effective_from <= effective_until + - calendar entries: exactly one of due|recurrence, unless status is + pending-definition/conditional; recurrence months are 1..12 + 4. ADC register hygiene: filename <-> frontmatter id match, valid status + (Proposed | Accepted | Superseded-by-NNN), Accepted records carry + decided + effective_from, Proposed records carry decided: null, + required body sections present, no duplicate adc numbers. + +Stdlib only, by design (PRD agent-catalog "document surface" + erp#54): the +profile YAML is written in a strict subset - block maps, block sequences, +single-line scalars, one-line [flow] lists of scalars, comments; no anchors, +no multi-line scalars, no nested flow - and the schemas use the JSON-Schema +subset {type, properties, required, additionalProperties, items, enum, +pattern}. + +Usage: python3 fleet/profile/scripts/validate.py +Exit 0 = all green (warnings allowed). Exit 1 = at least one error. +""" + +import json +import re +import sys +from pathlib import Path + +PROFILE = Path(__file__).resolve().parent.parent +DECISIONS = PROFILE / "decisions" + +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +ADC_FILE_RE = re.compile(r"^adc-(\d{3})-[a-z0-9-]+\.md$") +ADC_STATUS_RE = re.compile(r"^(Proposed|Accepted|Superseded-by-\d{3})$") +ADC_REQUIRED_SECTIONS = [ + "## Context", + "## Decision", + "## Base légale & doctrine", + "## Alternatives rejected", + "## Consequences", + "## QA & validation", + "## References", +] + + +# -------------------------------------------------------------------------- +# Strict YAML-subset parser +# -------------------------------------------------------------------------- + +class YamlSubsetError(Exception): + def __init__(self, msg, line=None): + super().__init__(f"line {line}: {msg}" if line else msg) + + +def _strip_comment(raw, line): + """Return the value token of a raw value string, comment stripped.""" + s = raw.strip() + if not s or s.startswith("#"): + return "" + if s[0] in "\"'": + q = s[0] + end = s.find(q, 1) + if end == -1: + raise YamlSubsetError("unterminated quoted scalar", line) + rest = s[end + 1:].strip() + if rest and not rest.startswith("#"): + raise YamlSubsetError(f"trailing content after quoted scalar: {rest!r}", line) + return s[: end + 1] + idx = s.find(" #") + if idx != -1: + s = s[:idx] + return s.strip() + + +def _scalar(tok, line): + if tok == "" or tok in ("null", "~"): + return None + if tok == "true": + return True + if tok == "false": + return False + if len(tok) >= 2 and tok[0] in "\"'" and tok[-1] == tok[0]: + return tok[1:-1] + if tok.startswith("["): + if not tok.endswith("]"): + raise YamlSubsetError("unterminated flow list", line) + inner = tok[1:-1].strip() + if not inner: + return [] + return [_scalar(p.strip(), line) for p in inner.split(",")] + if re.fullmatch(r"-?\d+", tok): + return int(tok) + if re.fullmatch(r"-?\d+\.\d+", tok): + return float(tok) + return tok # plain string (ISO dates stay strings) + + +def parse_yaml(text, name="<yaml>"): + items = [] + for n, raw in enumerate(text.splitlines(), 1): + if not raw.strip() or raw.strip().startswith("#"): + continue + if "\t" in raw: + raise YamlSubsetError(f"{name}: tab character (use spaces)", n) + indent = len(raw) - len(raw.lstrip(" ")) + items.append((indent, raw.strip(), n)) + if not items: + return {} + value, nxt = _parse_block(items, 0, items[0][0]) + if nxt != len(items): + raise YamlSubsetError(f"{name}: trailing content", items[nxt][2]) + return value + + +def _parse_block(items, i, indent): + _, content, line = items[i] + if items[i][0] != indent: + raise YamlSubsetError("unexpected indent", line) + if content == "-" or content.startswith("- "): + return _parse_seq(items, i, indent) + return _parse_map(items, i, indent) + + +def _parse_map(items, i, indent): + result = {} + while i < len(items): + ind, content, line = items[i] + if ind < indent: + break + if ind > indent: + raise YamlSubsetError("unexpected deeper indent", line) + if content == "-" or content.startswith("- "): + raise YamlSubsetError("sequence item at mapping level", line) + m = re.match(r"^([A-Za-z_][A-Za-z0-9_-]*):(?:\s+(.*))?$", content) + if not m: + raise YamlSubsetError(f"not a 'key: value' line: {content!r}", line) + key, rest = m.group(1), m.group(2) + if key in result: + raise YamlSubsetError(f"duplicate key {key!r}", line) + tok = _strip_comment(rest, line) if rest else "" + if tok: + result[key] = _scalar(tok, line) + i += 1 + else: + i += 1 + if i < len(items) and items[i][0] > indent: + value, i = _parse_block(items, i, items[i][0]) + result[key] = value + else: + result[key] = None + return result, i + + +def _parse_seq(items, i, indent): + result = [] + while i < len(items): + ind, content, line = items[i] + if ind < indent: + break + if ind > indent: + raise YamlSubsetError("unexpected deeper indent in sequence", line) + if not (content == "-" or content.startswith("- ")): + break + rest = content[1:].strip() + if not rest or rest.startswith("#"): + i += 1 + if i < len(items) and items[i][0] > indent: + value, i = _parse_block(items, i, items[i][0]) + result.append(value) + else: + result.append(None) + elif re.match(r"^[A-Za-z_][A-Za-z0-9_-]*:(\s|$)", rest): + # inline first key of a mapping item: reparse at indent+2 + sub = [(indent + 2, rest, line)] + i += 1 + while i < len(items) and items[i][0] > indent: + s_ind, s_content, s_line = items[i] + if s_ind < indent + 2: + raise YamlSubsetError("bad indent inside sequence item", s_line) + sub.append((s_ind, s_content, s_line)) + i += 1 + value, used = _parse_map(sub, 0, indent + 2) + if used != len(sub): + raise YamlSubsetError("trailing content in sequence item", sub[used][2]) + result.append(value) + else: + tok = _strip_comment(rest, line) + result.append(_scalar(tok, line)) + i += 1 + return result, i + + +# -------------------------------------------------------------------------- +# JSON-Schema subset checker +# -------------------------------------------------------------------------- + +def _is_type(v, t): + return { + "object": lambda: isinstance(v, dict), + "array": lambda: isinstance(v, list), + "string": lambda: isinstance(v, str), + "integer": lambda: isinstance(v, int) and not isinstance(v, bool), + "number": lambda: isinstance(v, (int, float)) and not isinstance(v, bool), + "boolean": lambda: isinstance(v, bool), + "null": lambda: v is None, + }[t]() + + +def schema_check(value, schema, path, errors): + types = schema.get("type") + if types: + if isinstance(types, str): + types = [types] + if not any(_is_type(value, t) for t in types): + errors.append(f"{path}: expected {'/'.join(types)}, got {type(value).__name__}") + return + if value is None: + return + if "enum" in schema and value not in schema["enum"]: + errors.append(f"{path}: {value!r} not in {schema['enum']}") + if isinstance(value, str) and "pattern" in schema: + if not re.search(schema["pattern"], value): + errors.append(f"{path}: {value!r} does not match {schema['pattern']!r}") + if isinstance(value, dict): + props = schema.get("properties", {}) + for req in schema.get("required", []): + if req not in value: + errors.append(f"{path}: missing required key '{req}'") + if schema.get("additionalProperties") is False: + for k in value: + if k not in props: + errors.append(f"{path}: unexpected key '{k}'") + for k, v in value.items(): + if k in props: + schema_check(v, props[k], f"{path}.{k}", errors) + if isinstance(value, list) and "items" in schema: + for idx, item in enumerate(value): + schema_check(item, schema["items"], f"{path}[{idx}]", errors) + + +# -------------------------------------------------------------------------- +# ADC register +# -------------------------------------------------------------------------- + +def parse_frontmatter(path): + text = path.read_text(encoding="utf-8") + if not text.startswith("---\n"): + raise YamlSubsetError(f"{path.name}: missing frontmatter") + end = text.find("\n---", 4) + if end == -1: + raise YamlSubsetError(f"{path.name}: unterminated frontmatter") + return parse_yaml(text[4:end], path.name), text[end + 4:] + + +def load_adc_register(errors): + """Return {'adc-NNN': {'file': name, 'status': str, ...}}.""" + register = {} + if not DECISIONS.is_dir(): + errors.append(f"decisions/ directory missing at {DECISIONS}") + return register + for path in sorted(DECISIONS.glob("*.md")): + if path.name == "adc-template.md": + continue + m = ADC_FILE_RE.match(path.name) + if not m: + errors.append(f"decisions/{path.name}: name must match adc-NNN-<slug>.md") + continue + adc_id = f"adc-{m.group(1)}" + if adc_id in register: + errors.append(f"decisions/{path.name}: duplicate id {adc_id} " + f"(also {register[adc_id]['file']})") + continue + try: + fm, body = parse_frontmatter(path) + except YamlSubsetError as e: + errors.append(f"decisions/{path.name}: {e}") + continue + if fm.get("id") != adc_id: + errors.append(f"decisions/{path.name}: frontmatter id {fm.get('id')!r} != {adc_id}") + status = fm.get("status") + if not (isinstance(status, str) and ADC_STATUS_RE.match(status)): + errors.append(f"decisions/{path.name}: invalid status {status!r} " + "(Proposed | Accepted | Superseded-by-NNN)") + if status == "Accepted": + if not (isinstance(fm.get("decided"), str) and DATE_RE.match(fm["decided"])): + errors.append(f"decisions/{path.name}: Accepted requires a 'decided' date") + if not (isinstance(fm.get("effective_from"), str) and DATE_RE.match(fm["effective_from"])): + errors.append(f"decisions/{path.name}: Accepted requires 'effective_from'") + elif status == "Proposed": + if fm.get("decided") is not None: + errors.append(f"decisions/{path.name}: Proposed must keep decided: null " + "(acceptance is a human act)") + for section in ADC_REQUIRED_SECTIONS: + if section not in body: + errors.append(f"decisions/{path.name}: missing section '{section}'") + register[adc_id] = dict(fm, file=path.name) + return register + + +# -------------------------------------------------------------------------- +# Custom (referential) checks +# -------------------------------------------------------------------------- + +def check_dates_ordered(obj, path, errors): + frm, until = obj.get("effective_from"), obj.get("effective_until") + if isinstance(frm, str) and isinstance(until, str) and frm > until: + errors.append(f"{path}: effective_from {frm} > effective_until {until}") + + +def check_fiscal(fiscal, register, errors, warnings, resolution): + seen = set() + for idx, rule in enumerate(fiscal.get("rules") or []): + rid = rule.get("id", f"[{idx}]") + path = f"fiscal.rules.{rid}" + if rid in seen: + errors.append(f"{path}: duplicate rule id") + seen.add(rid) + check_dates_ordered(rule, path, errors) + decision = rule.get("decision") + adc = register.get(decision) + if adc is None: + errors.append(f"{path}: decision {decision!r} does not resolve to any " + f"decisions/adc-NNN-*.md record") + resolution.append((rid, decision, "<missing>", "<missing>")) + continue + resolution.append((rid, decision, adc["file"], adc.get("status"))) + if adc.get("status") != "Accepted": + errors.append(f"{path}: decision {decision} has status " + f"{adc.get('status')!r} - a rule in force may only cite an " + "Accepted decision (agents draft, the operator Accepts)") + + +def check_calendar(calendar, errors, warnings): + seen = set() + for idx, entry in enumerate(calendar.get("entries") or []): + eid = entry.get("id", f"[{idx}]") + path = f"calendar.entries.{eid}" + if eid in seen: + errors.append(f"{path}: duplicate entry id") + seen.add(eid) + check_dates_ordered(entry, path, errors) + status = entry.get("status", "confirmed") + has_due = entry.get("due") is not None + has_rec = entry.get("recurrence") is not None + if has_due and has_rec: + errors.append(f"{path}: carries both due and recurrence") + elif not has_due and not has_rec and status not in ("pending-definition", "conditional"): + errors.append(f"{path}: needs due or recurrence (status {status!r})") + rec = entry.get("recurrence") + months = entry.get("months") + if rec in ("yearly", "quarterly"): + if not months: + errors.append(f"{path}: recurrence {rec!r} requires months") + elif months is not None: + errors.append(f"{path}: months only allowed with yearly/quarterly recurrence") + for mth in months or []: + if not (isinstance(mth, int) and 1 <= mth <= 12): + errors.append(f"{path}: month {mth!r} not in 1..12") + if has_due and re.fullmatch(r"\d{4}-\d{2}", entry["due"]): + warnings.append(f"{path}: month-precision due ({entry['due']}) - the source " + "gives no day; verify on the authority's notice") + if status in ("estimated", "conditional", "pending-definition"): + warnings.append(f"{path}: status {status} - operator verification pending") + + +# -------------------------------------------------------------------------- +# Main +# -------------------------------------------------------------------------- + +def load_and_validate(name, errors): + yaml_path = PROFILE / f"{name}.yaml" + schema_path = PROFILE / f"{name}.schema.json" + try: + data = parse_yaml(yaml_path.read_text(encoding="utf-8"), yaml_path.name) + except (OSError, YamlSubsetError) as e: + errors.append(f"{yaml_path.name}: {e}") + return None + try: + schema = json.loads(schema_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as e: + errors.append(f"{schema_path.name}: {e}") + return None + before = len(errors) + schema_check(data, schema, name, errors) + print(f"== {yaml_path.name} ==") + print(f" parse: OK") + print(f" schema ({schema_path.name}): {'OK' if len(errors) == before else 'FAIL'}") + return data + + +def main(): + errors, warnings, resolution = [], [], [] + + fiscal = load_and_validate("fiscal", errors) + calendar = load_and_validate("calendar", errors) + + print("== decisions/ (ADC register) ==") + register = load_adc_register(errors) + for adc_id in sorted(register): + adc = register[adc_id] + print(f" {adc['file']:<44} [{adc.get('status')}]") + + if fiscal is not None: + check_fiscal(fiscal, register, errors, warnings, resolution) + print("== rule -> ADC resolution ==") + for rid, decision, fname, status in resolution: + print(f" {rid:<32} -> {decision} -> {fname} [{status}]") + if calendar is not None: + check_calendar(calendar, errors, warnings) + + if warnings: + print(f"== warnings ({len(warnings)}) ==") + for w in warnings: + print(f" WARN {w}") + if errors: + print(f"== errors ({len(errors)}) ==") + for e in errors: + print(f" ERROR {e}") + n_rules = len((fiscal or {}).get("rules") or []) + n_entries = len((calendar or {}).get("entries") or []) + verdict = "FAIL" if errors else "PASS" + print(f"== RESULT: {verdict} - {n_rules} rules, {n_entries} calendar entries, " + f"{len(register)} ADC records, {len(errors)} errors, {len(warnings)} warnings ==") + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main())