[ADD] script todo : /todo_generate_code, effort high et règles du dépôt
Les règles à appliquer avant de coder sont éparpillées entre les configs, les hooks et les modules maison, et la documentation en contredit plusieurs : autopep8 est recommandé alors que le script sort en 1, aucun script oca-* n'est installé, la racine n'a ni .pre-commit-config.yaml ni .pylintrc, et flake8 comme pylint-odoo ne vivent que dans le venv Odoo, que rien ne lance. Le gabarit énonce ce que l'outillage impose, à effort high, l'épinglage ultracode restant celui de l'utilisateur. Vérifié : 146 règles relevées, 142 confirmées contre leur citation, 4 retirées. Deux gardes lient chaque entrée du menu à un gabarit présent qui déclare le bon nom. --- EN --- The rules to apply before coding are scattered across the configs, the hooks and the in-house modules, and the documentation contradicts several of them: autopep8 is recommended although the script exits 1, no oca-* script is installed, the root carries neither .pre-commit-config.yaml nor .pylintrc, and flake8 as well as pylint-odoo live only in the Odoo venv, which nothing invokes. The template states what the tooling enforces, at high effort, the ultracode pin remaining the user's own. Checked: 146 rules surveyed, 142 confirmed against their citation, 4 dropped. Two guards tie each menu entry to a template that exists and declares the right name. Assisted-by: Claude Opus 5
This commit is contained in:
parent
4f85a4f225
commit
2435f0f6d3
4 changed files with 317 additions and 0 deletions
228
conf/template_claude_commands_todo_generate_code.md
Normal file
228
conf/template_claude_commands_todo_generate_code.md
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
---
|
||||
name: todo_generate_code
|
||||
description: "Write code in ERPLibre by the rules the repository actually enforces: Odoo/OCA module conventions first, then the real format and verify toolchain."
|
||||
disable-model-invocation: true
|
||||
effort: high
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Edit
|
||||
- Write
|
||||
- Grep
|
||||
- Glob
|
||||
- Bash(make format:*)
|
||||
- Bash(make test_unit:*)
|
||||
- Bash(make test_unit_file:*)
|
||||
- Bash(./script/maintenance/:*)
|
||||
- Bash(python3:*)
|
||||
- Bash(git status:*)
|
||||
- Bash(git diff:*)
|
||||
- Bash(ls:*)
|
||||
- Bash(cat:*)
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
- Odoo series in this checkout: !`cat .odoo-version`
|
||||
- Available venvs: !`ls -d .venv.* 2>/dev/null`
|
||||
- Branch and pending work: !`git status --porcelain`
|
||||
- Addons trees: !`ls -d odoo*/addons/*/ 2>/dev/null | head -12`
|
||||
|
||||
## Task
|
||||
|
||||
Write code in this repository by the rules it ENFORCES, which are not always
|
||||
the rules it documents. Establish them first, then write. The sections below
|
||||
were read off the configuration files and the in-house modules, not off the
|
||||
prose; where a document disagrees with a tool, the tool wins and the
|
||||
disagreement is named.
|
||||
|
||||
### The effort tier
|
||||
|
||||
The `effort: high` above applies to this invocation — "comprehensive
|
||||
implementation with extensive testing", the tier for code that has to survive
|
||||
review.
|
||||
|
||||
A session can be pinned above it. `/effort ultracode` sets xhigh AND turns on
|
||||
dynamic workflow orchestration, and a system-reminder then asks for a workflow
|
||||
on every substantive task. That standing opt-in does not apply here: writing
|
||||
one module correctly is one agent's job, and fanning it out multiplies both the
|
||||
token cost and the ways the pieces disagree.
|
||||
|
||||
So when a reminder says ultracode is on, say in one line that this command
|
||||
works at high, and ask the user to type `/effort high` — that one command sets
|
||||
the tier and clears the ultracode flag in the same move. A command's
|
||||
frontmatter cannot release a session pin; only the user's own `/effort` can,
|
||||
from an interactive terminal. Work solo either way.
|
||||
|
||||
### 1. Before writing a line
|
||||
|
||||
**Find the tree.** New modules go under `odoo<VERSION>/addons/<Org>_<repo>/<module>/`
|
||||
— the version-prefixed tree is the real addons root, and the generator
|
||||
rewrites any `addons/` it is handed into `odoo<VERSION>/addons/`
|
||||
(`script/code_generator/new_project.py:144-146`). The version is in
|
||||
`.odoo-version`; the venv carries BOTH versions in its name, so find it with
|
||||
`ls -d .venv.odoo*` rather than composing it from memory.
|
||||
|
||||
**Read the neighbours.** Two or three in-house modules under
|
||||
`odoo<VERSION>/addons/ERPLibre_erplibre_addons/` show the conventions in force
|
||||
better than any list. Copy their shape.
|
||||
|
||||
**Bootstrap rather than hand-roll.** `script/code_generator/new_project.py -d
|
||||
<git root dir> -m <module name>` creates a module; `create_from_existing_module.py`
|
||||
clones one. A module cloned from an existing one INHERITS its comments and
|
||||
docstrings — reread them before committing, a client or database name travels
|
||||
that way on its own.
|
||||
|
||||
**Two rules bind every line you write**, and no tool checks either:
|
||||
- Nothing identifying outside `private/` — no customer or third-party
|
||||
organisation, no real database, host or VM name, no IP, e-mail or path
|
||||
carrying an account name. Generalise to the class of situation instead.
|
||||
- A comment says how the CODE works, in the present. A sentence whose subject
|
||||
is an incident, a machine, a date or a person belongs in `tasks/`, which is
|
||||
not versioned.
|
||||
|
||||
**Never edit a generated file.** `FICHIER.md` and `FICHIER.fr.md` come from
|
||||
`FICHIER.base.md` through mmg; an edit is lost at the next `make doc_markdown`.
|
||||
|
||||
### 2. Odoo module conventions
|
||||
|
||||
`__manifest__.py` carries at minimum `name`, `version`, `author`, `license`,
|
||||
`category`, `summary`, `depends`, `data`, `installable`. `version` is
|
||||
`<series>.1.0.0` — `18.0.1.0.0` on an 18.0 checkout — and `license` is
|
||||
`AGPL-3`, which every in-house module uses.
|
||||
|
||||
The `data` list is ordered `security/`, then `data/`, then `wizards/`, then
|
||||
`views/`, with `views/menu.xml` last.
|
||||
|
||||
Non-Odoo Python requirements go in `external_dependencies: {"python": [...]}`,
|
||||
and the import is guarded in the model with `try/except ImportError` logging at
|
||||
debug level — a missing optional dependency must not break the registry.
|
||||
|
||||
Layout: `models/`, `views/`, `security/`, `wizards/`, `data/`, `controllers/`,
|
||||
`i18n/`, `report/`, `tests/`, `static/description/`. Ship
|
||||
`static/description/icon.png`.
|
||||
|
||||
Naming, one for one:
|
||||
- `models/<model_name_with_dots_as_underscores>.py`, one model per file —
|
||||
model `devops.workspace` lives in `models/devops_workspace.py`.
|
||||
- `views/<same_name>.xml` for a Model; a TransientModel's Python AND its XML
|
||||
both live in `wizards/`.
|
||||
- `ir.ui.view` ids: `<model_underscored>_view_<type>` (`_view_form`,
|
||||
`_view_tree`, `_view_search`, `_view_kanban`).
|
||||
- `ir.actions.act_window` ids: `<model_underscored>_<action>_action_window`;
|
||||
server actions: `<model_underscored>_<name>_server_action`.
|
||||
- `res.groups` go in `security/<module_name>.xml` — named after the module, not
|
||||
`security.xml` — and that file is listed BEFORE `ir.model.access.csv`.
|
||||
|
||||
`security/ir.model.access.csv` carries exactly the header
|
||||
`id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink`.
|
||||
Split the rows by privilege level — a read-only row for the broad group, a
|
||||
full-CRUD row for the administrative one — rather than one blanket row.
|
||||
|
||||
Every Python file opens with the AGPL licence comment; hand-written models and
|
||||
hooks also carry the shebang and copyright lines. A package `__init__.py`
|
||||
starts with `# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)`,
|
||||
a blank line, then imports. The root `__init__.py` imports subpackages on one
|
||||
line (`from . import models, wizards`) and a hook by name
|
||||
(`from .hooks import post_init_hook`); hooks live in a top-level `hooks.py` and
|
||||
are declared under the matching manifest key.
|
||||
|
||||
Models declare `_name`, `_description` and, when mixins are used, `_inherit`;
|
||||
the class name is the CamelCase of the model. Odoo symbols come in one grouped
|
||||
import — `from odoo import _, api, exceptions, fields, models`.
|
||||
|
||||
Translations go in `i18n/` as `<module>.pot` plus one `.po` per locale.
|
||||
|
||||
### 3. Format — the toolchain that exists
|
||||
|
||||
`make format` before committing. It formats only what git reports as
|
||||
modified, added, renamed or untracked, dispatching each file by extension —
|
||||
so it is cheap and safe to run repeatedly. `make format_all` sweeps whole
|
||||
areas instead.
|
||||
|
||||
What it runs underneath, and what to match when writing by hand:
|
||||
|
||||
| Kind | Tool and settings |
|
||||
|------|-------------------|
|
||||
| Python | `isort --profile black -l 79`, then `black -l 79 --preview -t py37` |
|
||||
| XML | prettier + `@prettier/plugin-xml`, tab-width 4, print-width 120 |
|
||||
| XML under `data/` | same, print-width 999999999 — long data strings stay on one line |
|
||||
| js, css, scss, html | prettier, tab-width 4, print-width 120, no bracket spacing |
|
||||
| Shell | `shfmt -i 2 -ci -w` |
|
||||
|
||||
black and isort run from `.venv.erplibre`, prettier from the repo-pinned
|
||||
`./node_modules/.bin` — a globally installed prettier is a different version
|
||||
and reformats differently. Nothing under `not_supported_files/` is formatted.
|
||||
|
||||
**What NOT to run**, each verified absent from this checkout rather than
|
||||
assumed:
|
||||
- `./script/maintenance/autopep8.sh` — `oca-autopep8` is not installed; the
|
||||
script exits 1. `doc/DEVELOPMENT.base.md` still recommends it; the document
|
||||
is stale.
|
||||
- Any `oca-*` console script (`oca-gen-addon-readme`, `oca-towncrier`, …) —
|
||||
`script/OCA_maintainer-tools` is checked out but never pip-installed, and its
|
||||
install block in `install_locally_dev.sh` is commented out. Run one as a
|
||||
module from that directory if you truly need it.
|
||||
- `pre-commit run` at the repository root — there is no root
|
||||
`.pre-commit-config.yaml`. ERPLibre's own hooks are hand-written Python,
|
||||
installed once per clone with
|
||||
`git config core.hooksPath script/git/hooks`.
|
||||
|
||||
**A vendored OCA addon** under `odoo<VERSION>/addons/OCA_*` carries its own
|
||||
`.pre-commit-config.yaml`: run THAT from its directory instead of the ERPLibre
|
||||
format scripts, so the patch matches what upstream will accept.
|
||||
|
||||
### 4. Lint
|
||||
|
||||
Neither linter runs on its own — no target, no hook, no CI invokes them, so
|
||||
running them is a deliberate act:
|
||||
|
||||
- flake8 lives in the Odoo venv, not `.venv.erplibre`. The root `.flake8`
|
||||
applies: max-line-length 80, max-complexity 16, `select = C,E,F,W,B,B9`,
|
||||
ignoring E203, E501 and W503 for black compatibility.
|
||||
- pylint-odoo also lives in the Odoo venv:
|
||||
`.venv.odoo<...>/bin/pylint --load-plugins=pylint_odoo --rcfile=<file> <path>`.
|
||||
The `--rcfile` is not optional — the repository root ships no `.pylintrc`,
|
||||
and the ones found under vendored trees belong to those projects.
|
||||
|
||||
### 5. Verify
|
||||
|
||||
Verification is entirely local: `.github/` holds no workflow, so nothing
|
||||
catches a mistake after the fact.
|
||||
|
||||
**Repository scripts** — `make test_unit` is the fast gate: no PostgreSQL, no
|
||||
Odoo, no VM, a few seconds. While iterating on one file, `make test_unit_file
|
||||
F=test/test_<name>.py`.
|
||||
|
||||
A new file in `test/` must declare at least one `test_*` function and end with
|
||||
`if __name__ == "__main__": unittest.main()` as the LAST top-level statement —
|
||||
the suite has its own test that enforces both, since the runner selects files
|
||||
by the glob `test/test_*.py`. A test that creates a real machine, installs a
|
||||
system or runs for hours goes in `long_test/` instead, and undoes itself with
|
||||
`--detruire`.
|
||||
|
||||
**An Odoo module** — drop the database, then run the module's tests:
|
||||
|
||||
```bash
|
||||
./odoo_bin.sh db --drop --database test_<module>
|
||||
./test.sh -d test_<module> --db-filter test_<module> -i <module>
|
||||
```
|
||||
|
||||
`./test.sh` is `./run.sh` with `ODOO_MODE_TEST=true --workers 0`, which adds
|
||||
`--test-enable --no-http --stop-after-init`. For coverage, bracket that with
|
||||
`./.venv.erplibre/bin/coverage erase` before and a combine/report after, and
|
||||
set `ODOO_MODE_COVERAGE=true` — coverage is switched on through the
|
||||
environment, never a CLI flag. `make open_test_coverage` opens the report.
|
||||
|
||||
To run ONE test file inside a module, install the module first, then pass
|
||||
`--test-file=`.
|
||||
|
||||
### 6. Then, and only then, commit
|
||||
|
||||
`make format`, the tests above, then `/commit` — which resolves the model,
|
||||
writes the `[TYPE] portée : sujet` subject under 72 characters, the bilingual
|
||||
body under ten lines per language, and the `Assisted-by:` trailer. The
|
||||
`commit-msg` hook REFUSES a message that breaks the mechanical part of that;
|
||||
the `pre-commit` hook only reports comments worth rereading and never blocks.
|
||||
|
||||
Stage by naming files. `git add -A` sweeps in `private/` and `tasks/`, which
|
||||
are untracked on purpose.
|
||||
|
|
@ -3068,6 +3068,12 @@ class TODO(
|
|||
" command"
|
||||
)
|
||||
},
|
||||
{
|
||||
"prompt_description": t(
|
||||
"Todo Generate Code - Code by the OCA rules at high"
|
||||
" effort"
|
||||
)
|
||||
},
|
||||
{"prompt_description": t("Show installed custom commands")},
|
||||
]
|
||||
help_info = self.fill_help_info(choices)
|
||||
|
|
@ -3101,6 +3107,11 @@ class TODO(
|
|||
"template_claude_commands_todo_add_command.md",
|
||||
)
|
||||
elif status == "4":
|
||||
self._setup_claude_command(
|
||||
"todo_generate_code",
|
||||
"template_claude_commands_todo_generate_code.md",
|
||||
)
|
||||
elif status == "5":
|
||||
self._list_claude_commands()
|
||||
else:
|
||||
print(t("Command not found !"))
|
||||
|
|
@ -3232,6 +3243,9 @@ class TODO(
|
|||
"template_claude_commands_git_prepare_merge.md"
|
||||
),
|
||||
"todo_add_command": "template_claude_commands_todo_add_command.md",
|
||||
"todo_generate_code": (
|
||||
"template_claude_commands_todo_generate_code.md"
|
||||
),
|
||||
"todo_plan_max": "template_claude_commands_todo_plan_max.md",
|
||||
}
|
||||
for nom, gabarit in sorted(gabarits.items()):
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,12 @@ TRANSLATIONS = {
|
|||
),
|
||||
"en": "Todo Add Command + Plan Max - Plan and add a todo.py command",
|
||||
},
|
||||
"Todo Generate Code - Code by the OCA rules at high effort": {
|
||||
"fr": (
|
||||
"Todo Generate Code - Coder selon les règles OCA, effort élevé"
|
||||
),
|
||||
"en": "Todo Generate Code - Code by the OCA rules at high effort",
|
||||
},
|
||||
"Enter your full name: ": {
|
||||
"fr": "Entrez votre nom complet : ",
|
||||
"en": "Enter your full name: ",
|
||||
|
|
|
|||
|
|
@ -485,6 +485,75 @@ class TestSetupClaudeCommit(unittest.TestCase):
|
|||
mock_makedirs.assert_called_once()
|
||||
|
||||
|
||||
class TestClaudeCommandTemplates(unittest.TestCase):
|
||||
"""Chaque commande proposée par le menu doit avoir son gabarit.
|
||||
|
||||
Un nom de gabarit fautif ne se voit qu'à l'exécution, au moment où le
|
||||
déploiement échoue devant l'utilisateur : rien ne relie le littéral passé
|
||||
à `_setup_claude_command` au fichier de `conf/`.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _deployed_templates():
|
||||
"""Les gabarits nommés dans les appels à `_setup_claude_command`."""
|
||||
import ast
|
||||
|
||||
source = Path("script/todo/todo.py").read_text(encoding="utf-8")
|
||||
found = []
|
||||
for node in ast.walk(ast.parse(source)):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
attr = getattr(node.func, "attr", None)
|
||||
if attr != "_setup_claude_command":
|
||||
continue
|
||||
# (nom_de_commande, nom_de_gabarit) : les deux sont des littéraux,
|
||||
# sans quoi le test ne peut rien affirmer.
|
||||
args = [
|
||||
a.value
|
||||
for a in node.args
|
||||
if isinstance(a, ast.Constant) and isinstance(a.value, str)
|
||||
]
|
||||
if len(args) >= 2:
|
||||
found.append(args[1])
|
||||
return found
|
||||
|
||||
def test_every_menu_template_exists(self):
|
||||
templates = self._deployed_templates()
|
||||
self.assertGreaterEqual(len(templates), 4, templates)
|
||||
for name in templates:
|
||||
with self.subTest(template=name):
|
||||
self.assertTrue(
|
||||
os.path.isfile(os.path.join("conf", name)),
|
||||
f"conf/{name} est nommé par le menu et n'existe pas",
|
||||
)
|
||||
|
||||
def test_every_template_declares_its_own_name(self):
|
||||
"""Le `name:` du frontmatter donne le nom de la commande `/…` ; un
|
||||
gabarit qui en déclare un autre déploie un fichier dont le contenu
|
||||
parle d'une commande différente."""
|
||||
source = Path("script/todo/todo.py").read_text(encoding="utf-8")
|
||||
import ast
|
||||
|
||||
pairs = []
|
||||
for node in ast.walk(ast.parse(source)):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if getattr(node.func, "attr", None) != "_setup_claude_command":
|
||||
continue
|
||||
args = [
|
||||
a.value
|
||||
for a in node.args
|
||||
if isinstance(a, ast.Constant) and isinstance(a.value, str)
|
||||
]
|
||||
if len(args) >= 2:
|
||||
pairs.append((args[0], args[1]))
|
||||
self.assertTrue(pairs)
|
||||
for command, template in pairs:
|
||||
with self.subTest(command=command):
|
||||
text = Path("conf", template).read_text(encoding="utf-8")
|
||||
self.assertIn(f"name: {command}\n", text)
|
||||
|
||||
|
||||
class TestClaudePlugins(unittest.TestCase):
|
||||
"""Le menu des plugins Claude Code.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue