Merge pull request 'feat(fleet): fiscal profile + compliance calendar + ADC register (T11 data) — ⚠️ operator gate: Accept adc-001…005' (#71) from arcodange/fiscal-profile into main

This commit was merged in pull request #71.
This commit is contained in:
2026-07-19 09:22:56 +02:00
16 changed files with 1531 additions and 3 deletions
+1 -1
View File
@@ -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 | | `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 | | `.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) | | `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 ## Operating rules for agents
+1 -1
View File
@@ -47,7 +47,7 @@ fleet/
│ ├── prompt.md │ ├── prompt.md
│ └── scripts/ │ └── scripts/
├── golden/ # per-atom golden sets — land with erp#39 ├── 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) └── harness/ # multi-runtime harness layer: verifier tests + builder bench (harness/README.md)
``` ```
+113 -1
View File
@@ -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-<slug>.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 <leaf>
§ <anchor>`, 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 (~223267 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.
+45
View File
@@ -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" }
}
}
}
}
}
+198
View File
@@ -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 <leaf> § <anchor>" 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."
@@ -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).
@@ -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`.
@@ -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).
@@ -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.
@@ -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`.
@@ -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`.
@@ -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.
+47
View File
@@ -0,0 +1,47 @@
---
id: adc-NNN
title: "<short decision 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 — <title>
> 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.
+59
View File
@@ -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" }
}
}
}
}
}
+158
View File
@@ -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)"
+435
View File
@@ -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())