Version: 0.1.0.dev0
Date: 2026-08-04
Python: ≥ 3.14
Repository: https://github.com/litnimax/odoo2angee-engine
Status: first vertical slices implemented; M13 engine-side complete, Paseo v3 UI pending
This document describes the current system as implemented: ownership, architecture, data flow, commands, artifacts, trust boundaries, and how universal vs pilot-specific the pieces are. It is a portable overview for humans and agents; detailed contracts live in the linked docs and schemas.
odoo2angee-engine is a migration-evidence engine for planning
Odoo → Angee migrations.
It:
- scans local Odoo addons without starting Odoo, importing addon Python, accessing a database, or writing into Odoo/Angee source trees;
- preserves source facts as an immutable intermediate representation (IR);
- prepares deterministic work items for external agents (Paseo);
- validates untrusted agent submissions and publishes normalized reports;
- optionally captures a bounded runtime oracle for
sale.orderdraft behavior; - generates one Angee model from an explicit accepted decision.
It does not:
- own IDE UI, Git, worktrees, terminals, or agent execution (Paseo owns those);
- run an LLM inside the engine;
- turn Angee into a migration control plane;
- silently drop unsupported source constructs;
- widen generation beyond the one-model primitive-field slice.
| Concern | Owner |
|---|---|
| UI, agents, Git, worktrees, terminals, files, diff | Paseo |
| Odoo inventory, IR, migration units, decisions, validation, reports | odoo2angee-engine |
| Odoo source semantics | Odoo sources + engine static parsers |
| Target framework/runtime | Angee (read as capability catalog; never a control plane) |
| Target application examples | arpee-angee (reference only) |
| Runtime characterization of sale draft | Engine capture-sale-oracle only |
Rule: reference repositories (Odoo, Angee, arpee, Paseo) are read-only. Portable artifacts never embed absolute host paths, timestamps, mtimes, or random IDs.
Dependencies point inward:
CLI → application use cases → domain IR
↑ ↑
source adapters validation / storage / reports
| Package | Role |
|---|---|
odoo2angee.cli |
command dispatch, human/JSON/events presentation |
odoo2angee.doctor |
read-only environment checks |
odoo2angee.inventory |
filesystem, manifest, Python, XML, CSV, JS adapters |
odoo2angee.ir |
immutable Odoo IR + accepted Angee target IR |
odoo2angee.analysis |
work-item prep, untrusted ingest, migration units |
odoo2angee.target_capabilities |
pinned Angee capability catalog loader |
odoo2angee.oracle |
explicit Docker sale characterization |
odoo2angee.generation |
one-model validation, render, ownership, drift |
odoo2angee.project_summary |
read-only target state summary |
odoo2angee.events / progress / cancellation |
Paseo subprocess contract |
odoo2angee.validation / storage |
invariants, canonical JSON, atomic writes |
Domain code must not import Odoo, Django, Angee, Paseo, Git, or terminal UI.
Odoo source files
→ static inventory adapters (ast / safe XML / CSV / JS inventory)
→ immutable Odoo IR
→ structural validation
→ 13 canonical inventory JSON + shared publication marker
immutable Odoo IR / published inventory
→ addon-analysis work item v2
→ external Paseo agent (structured output)
→ untrusted submission validation
→ normalized analysis + findings reports
published inventory (marker-complete)
→ model-behavior migration unit (roots → same-addon closure)
→ migration-analysis work item v3
(+ target capability catalog
+ optional sale oracle digest
+ clean Angee Git HEAD)
→ external Paseo agent
→ untrusted ingest
→ analysis-only report (no decision, no code gen)
explicit accepted decision (.odoo2angee/decisions/)
→ Angee one-model target IR
→ deterministic owned target files + generation report
capture-sale-oracle (only Docker path)
→ clean revision-pinned Odoo worktree
→ packaged fixture × 2 fresh DBs
→ byte-identical canonical observations
→ .odoo2angee/oracles/odoo.sale.order-draft.v1.json
- Never import or execute an Odoo addon, its manifest, or its Python modules.
- Parse manifests and Python with
ast; parse XML with external entity/network access disabled. - Never write below the scanned addon directory.
- Preserve unsupported or dynamic constructs as diagnostics/findings — never silently drop source facts.
- Canonical artifacts are deterministic and path-portable.
- Keep source facts separate from target mappings and user decisions.
odoo2angee.ir.odoois source-only: no Angee/Django types, no target choices.
All commands support human output (default), --json, or
--events-jsonl --invocation-id <id>.
| Command | Purpose | Writes | Docker |
|---|---|---|---|
doctor |
check Python/Node/Git, Odoo/Angee roots, isolation | no | no |
scan-addon <addon> |
static inventory → 13 JSON | target inventory | no |
project-summary |
summarize published artifacts | no | no |
prepare-addon-analysis <addon> |
publish model-only v2 work item | work-items | no |
ingest-addon-analysis <addon> <file> |
validate draft → analysis reports | reports | no |
prepare-migration-analysis sale |
publish v3 migration work item | migration-analysis | no |
ingest-migration-analysis sale <file> |
validate draft → v3 report | migration-analysis | no |
capture-sale-oracle |
runtime sale draft oracle (2×) | oracles | yes |
generate-angee-model <decision.json> |
one-model Angee generation | target addon | no |
--check on ingest/generate compares expected bytes without writing (where
supported).
uv run odoo2angee doctor
uv run odoo2angee scan-addon sale
uv run odoo2angee capture-sale-oracle --json # optional, once per revision
uv run odoo2angee prepare-migration-analysis sale --json
# Paseo agent → submission JSON
uv run odoo2angee ingest-migration-analysis sale agent-output.json --json
# later, after human-accepted decision:
uv run odoo2angee generate-angee-model path/to/decision.jsoninventory/<addon>/
addon.json
models.json
fields.json
inheritance.json
security.json
views.json
actions.json
menus.json
data.json
cron.json
controllers.json
frontend.json
findings.json
schema_version = 1- addon-relative paths only
- shared SHA-256 publication marker over marker-neutral payloads (required by M13 snapshot loader; optional in legacy inventory schema)
- identical content is not rewritten on rescan
work-items/<addon>/...
submissions/<addon>/*.json # Paseo-written, untrusted
reports/<addon>/analysis.json
reports/<addon>/findings.json
v2 work item is self-contained: 13 inventory paths, trusted SourceRef
catalog, per-model analysis identity, instructions, exact draft-07 output
schema. Engine owns canonical IDs; agent uses submission-local keys only.
migration-analysis/
work-items/<addon>/...
submissions/<addon>/*.json
reports/<addon>/...
Report is explicitly:
accepted_decision: falseimplementation_authorized: falsecode_generation: false
project-summary exposes migration_analyses with states:
prepared | published | stale | incomplete | invalid.
oracles/odoo.sale.order-draft.v1.json
decisions/<addon>/*.json # human-accepted model slice
# generated files live in the target Angee addon with ownership marker
Public aggregate: odoo2angee.ir.OdooAddon (from scan_addon(...).ir).
Frozen entities include:
OdooModel, OdooField, OdooInheritance, OdooConstraint,
OdooSecurityGroup, OdooAccessRule, OdooRecordRule, OdooView,
OdooAction, OdooMenu, OdooDataRecord, OdooScheduledAction,
OdooController, OdooFrontendAsset.
Mapping boundary: IR holds only Odoo evidence. Future mappings consume it as immutable input; they must not reach back into parser-specific dicts.
Policy: odoo.model-behavior-closure.v1
Schema: schemas/odoo-migration-unit-v1.schema.json
build_model_behavior_migration_unit(snapshot, roots)
build_sale_order_migration_unit(snapshot) # roots = sale.order + sale.order.line- all same-addon fragments for selected root models
- their fields and constraints
- framework callbacks:
depends,depends_context,onchange,constrains,ondelete,autovacuum - lifecycle methods:
create,write,unlink - recursively reachable direct
self.method()calls - method semantics (extractor revision pinned)
- follow relations (
relation_expansion: false) - scan dependency-addon closure
- include views/security/menus as unit members
- authorize writes or target choices
Related-recordset calls, super, env/context/privilege/persistence, inheritance parents outside selection, and relation targets become typed external requirements or obligations.
Unit identity binds: full artifact-set digests, semantics schema/extractor revision, selected semantics digest, policy, and unit content.
The engine never runs an agent. Pattern:
- Prepare — trusted context → work item + exact output schema
- Paseo — structured generation; agent is read-only; one JSON draft
- Ingest — rebuild evidence, fail-closed membership checks, own IDs, atomic publish
- every current model →
ready_for_design | blocked | deferred - rejects decisions, mappings, extra top-level fields
- v1 submissions still accepted for compatibility
- binds complete inventory publication, canonical unit, packaged capability catalog + clean Angee HEAD, optional oracle digest or explicit absence
- agent classifies every obligation, external requirement, and available oracle
stage as:
supportedpartially_supportedcapability_missingmanual_design_required
- work-item schema
$idembeds context digest (no schema substitution) - ingest returns
report_sha256(content digest) separately from semanticreport_id
Configured target and host-owned ancestors are trusted configuration. All
.odoo2angee descendants are treated as hostile:
- descriptor-relative traversal
O_NOFOLLOW/O_DIRECTORY- bounded reads,
fstaton opened leaves - v3 single-file publisher: temp file + fsync + final evidence check + rename through directory descriptors
JSON size caps: 16 MiB (v1/v2 inputs), 32 MiB (v3).
ID: odoo.sale.order-draft.v1
Kind: sale-behavior-observation
Command: capture-sale-oracle
Only runtime/Docker boundary in the product.
Static inventory knows structure and method AST facts. It cannot faithfully
execute Odoo ORM, compute graphs, view-driven Form onchange ordering, or
draft-vs-persist behavior. The oracle is executable evidence for the
bounded sale.order / sale.order.line aggregate.
draft.defaultsdraft.partner-selecteddraft.line-alpha-added/draft.line-beta-addeddraft.line-alpha-updated(qty/price/discount)draft.tax-removed/draft.tax-restoreddraft.line-beta-removeddraft.before-savepersisted.after-reloadrollback.before/rollback.after
Observations use stable fixture keys (not DB ids). Money/qty are canonicalized decimals. Two runs on fresh DBs must be byte-identical.
- requires
oracle.source_revision(full 40-char Git SHA) - clean shared worktree for Odoo core + sale at that revision
- read-only source mounts, disposable network/tmpfs DB
- revalidate clean revision after each run and before commit
- atomic publish; unchanged bytes not rewritten
- cooperative cancellation; labeled Docker cleanup
- mutable image tags are not future provenance (prefer digests)
project-summary lists published oracles without starting Docker.
Analysis binds oracle by digest or records absence; source and oracle revisions
are independent claims (same_source_revision may be unproven).
Draft “no DB writes” is proven for sale.order / sale.order.line row
counts, not absolute database non-mutation. Prefer that wording in consumers.
- Packaged canonical JSON (
catalogs/angee-target-capabilities-v1.json) - Schema:
target-capability-catalog-v1.schema.json - Curated from a named Angee Git revision
- Granular status, repository-relative evidence, constraints, notes
- Loader is fail-closed; never imports Angee or probes runtime
- Handwritten app workarounds do not promote a capability to
supported
Example families: same-row derived values, cross-row dependency graphs, server draft/prefill calculation, invariant-preserving writes, editable relation drafts, dynamic form modifiers, company-scoped records.
Input: accepted decision under .odoo2angee/decisions/ conforming to
angee-model-slice-v1.schema.json.
Produces (bounded slice):
- exactly one new Odoo-shaped Angee model
- explicit primitive field subset
- one admin-only
permissions.zed - one GraphQL resource
- one standard
ResourceListform/list - ownership marker + generation report (omitted source fields named)
Does not:
- convert Odoo record rules, XML views, related fields
- close addon dependency graphs
- infer target semantics from the scanner alone
Target project must already exist and compose the addon in settings.yaml.
--check compares expected bytes without writing.
Pilot gates after generation (in the target project):
uv run manage.py angee build
uv run manage.py makemigrations sale
uv run manage.py makemigrations --check --dry-run sale
uv run manage.py schema
uv run manage.py schema --check
uv run manage.py test pilot.sale.testsFull text: docs/paseo-contract.md
Schema: schemas/paseo-events-v1.schema.json
- Engine is a short-lived subprocess; no stdin
- Paseo serializes writers to one
angee.target - Output modes: human |
--json|--events-jsonl --invocation-id - Event envelope v1:
invocation.started→ phases/progress/artifacts → exactly one terminalcompleted | failed | cancelled - SIGINT/SIGTERM → exit 130/143 with graceful cancel where checkpoints allow
- stderr: human one-liners with stable prefixes; behavior from exit codes + stdout
- Forward compatibility: ignore unknown fields/types/phases
odoo2angee.yaml (project-local source of truth):
odoo:
core: /path/to/odoo
addons:
- /path/to/addons
angee:
framework: /path/to/angee-django
target: /path/to/pilot-target
reference:
arpee: /path/to/arpee-angee
runtime:
workspaces: /path/to/workspaces
oracle:
source_revision: <40-char-sha>
# docker_binary: docker
# odoo_image: odoo:19.0
# postgres_image: postgres:17Paseo should always pass --config /absolute/path.
Honest readiness for “extract any model”:
| Layer | Generic? | Notes |
|---|---|---|
| Scanner + inventory | Yes | any addon with __manifest__.py |
| Odoo IR | Yes | all inventoried entities |
| Method semantics | Yes | any method |
| Addon analysis v2 | Yes | model-only status per addon |
| Migration unit builder | Yes | build_model_behavior_migration_unit(snapshot, roots) |
| Migration unit policy | Partial | same-addon, no relations — intentional v1 |
| Migration analysis v3 surface | No | CLI/schemas gated to sale + fixed roots |
| Oracle | No | one sale draft scenario |
| Capability catalog | Yes (IDs) | content driven by current Angee audit |
| Generation | Partial | renderer generic; slice = 1 model + primitives |
| Multi-addon dependency closure | No | deferred |
| Security / XML view auto-port | No | by design |
Interpretation: the engine is a generic evidence platform with a sale.order aggregate pilot end-to-end. Source facts for any model are available now; full migration-analysis productization and runtime oracles for arbitrary models are not.
prepare-migration-analysis <addon> --roots m1,m2(drop sale hardcode; oracle optional)- Oracle scenario registry (sale draft = first registered scenario)
- Multi-addon /
_inheritpolicy v2 (opt-in) - Relation expansion policy (allowlisted / depth-limited)
- Paseo durable v3 agent/review workflow
- Generation scope expansion only after accepted evidence — not before
| Capability | Status |
|---|---|
| Read-only addon inventory scanner | implemented |
| Immutable Odoo IR | implemented |
| Method semantics extractor | implemented |
| Publication marker / snapshot loader | implemented |
| Model-behavior migration unit foundation | implemented |
| Pinned Angee capability catalog | implemented |
| Paseo events/cancellation contract | implemented |
| Addon analysis handoff v2 (+ v1 compat) | implemented |
| Sale oracle capture | implemented |
| Migration analysis handoff v3 (engine) | implemented |
| Paseo agent/review UI for v3 | pending |
| One-model Angee generation | implemented |
| Multi-model / multi-addon generation | not in scope yet |
| Implicit Docker anywhere else | forbidden |
uv run python -m unittest discover -s tests -v
uv run python -m compileall -q src tests
uv run ruff check src tests
uv run odoo2angee doctor
uv run odoo2angee scan-addon contacts
uv buildTests must continue to prove: non-execution of addon code, path containment, deterministic output, diagnostic completeness, source non-mutation, and black-box Paseo contract behavior.
| Path | Topic |
|---|---|
README.md |
user-facing commands and guarantees |
AGENTS.md |
contributor rules and local path snapshot |
docs/architecture.md |
layers, trust, data flow |
docs/implementation-plan.md |
milestones and first vertical slice |
docs/odoo-ir.md |
source IR contract |
docs/paseo-contract.md |
subprocess/events/artifacts/exit codes |
docs/m13-sale-behavior-evidence.md |
sale unit + oracle + capabilities |
docs/system-overview.md |
this document |
schemas/*.schema.json |
versioned portable contracts |
catalogs/angee-target-capabilities-v1.json |
pinned target capabilities |
skills/migrate-odoo-addon-analysis |
packaged agent skill |
odoo2angee-engine statically inventories Odoo addons into deterministic
artifacts and an immutable IR; builds bounded source-behavior migration units;
hands trusted, schema-exact analysis work to Paseo-owned agents; validates
untrusted drafts without running LLMs; optionally characterizes sale.order
draft runtime behavior via an explicit double-run Docker oracle; and generates
only an explicit one-model Angee slice from human-accepted decisions—keeping
source facts, target capabilities, agent prose, and code generation in strictly
separated trust domains.