Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions spec/issue-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
Port the *added-fact* documentation rule from

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — this commit (3268278, "Materialize signed-off spec for #9") introduces a new top-level spec/ directory with no precedent in the repo (git log main -- spec/ is empty) and no .gitignore entry, which sits outside the ticket's "No other file changes."

It is loop mechanism rather than anything the builder chose, and it is isolated in its own commit, so it is trivially droppable. Worth a one-time decision: keep spec/ as a checked-in convention for this repo, or gitignore it. Also note the file records a premise that is already stale ("The template ships no Sphinx docs/ tree") — if it stays in the repo it will read as current documentation.

~Written by Claude, run via the agentic engineering loop

[XomniaAI/agentic-engineering-loop#434](https://github.com/XomniaAI/agentic-engineering-loop/pull/434)
into the template's `AGENTS.md`, so generated projects tell their agents to keep
docs, docstrings, and comments short.

## Why

The template already enforces docstring **presence** — ruff `D` with the numpy
convention in `template/pyproject.toml.jinja` — but nothing bounds their
**length**. Agents working in a freshly generated project therefore produce
`Parameters`/`Returns` blocks that restate the annotations, step-by-step
narrations of the implementation, and docstrings that only grow on every edit.
Upstream #434 fixed exactly this gap in the loop's own `AGENTS.md` with a single
rule: past the summary line, a line must carry a fact the code cannot.

`template/AGENTS.md` is the natural home — it is the file `template/CLAUDE.md`
includes (`@AGENTS.md`), and it currently holds only *Package Management* and
*Git Conventions*.

## Scope

Two files, both under `template/`:

1. `template/AGENTS.md` — append the new section (see verbatim text below).
2. `tests/test_template.py` — in `test_template_renders`, add coverage that the
agent-instruction files actually land in a generated project:

```python
assert (project_path / "AGENTS.md").is_file()
assert (project_path / "CLAUDE.md").is_file()
```

No other file changes.

## Exact text to add

Append at the **end of `template/AGENTS.md`**, after the *Git Conventions*
section, separated by one blank line. Heading is Title Case to match the two
existing headings (upstream uses sentence case; the template does not). Prose
wrapped at 80 columns. `template/AGENTS.md` is a static file (no `.jinja`
suffix), so it must contain **no Jinja expressions** — do not reference
`{{ project_slug }}` or any other variable.

```markdown
## Documentation and Comment Style

Brevity is not tidiness — it is what gets the text read. A docstring, comment,
or page nobody reads still has to be maintained, so it is worse than none.

Docstring *presence* is enforced by ruff's `D` rules (numpy convention) in
`pyproject.toml`. This section bounds their *length*.

The unit of judgement is the *added fact*: after the summary line, every line
must tell the reader something they cannot get from the name, the signature,
the type hints, or the code itself.

Earns more than a summary line:

- a contract the signature doesn't show — preconditions, invariants, what it
raises
- units, bounds, or formats a type can't carry (`timeout: float` — seconds or
milliseconds?)
- caller-visible behaviour that would surprise — mutation, ordering,
idempotency, blocking, retries, cost (a network round-trip, an LLM call)
- a pointer to the *why* when it isn't obvious (link the issue or ADR, don't
restate it)

Does not:

- restating the signature, or a numpy `Parameters`/`Returns` section where the
name and type already say it
- narrating the implementation step by step — that's the code
- history ("previously…", "refactored to…") — that's git
- examples for a function whose use is obvious from its name

The same rule governs prose: the README covers what a reader needs to install,
run, and maintain the project; implementation detail belongs in the code.
Prefer a list, table, or short code example over paragraphs. When editing an
existing docstring, comment, or page, it must not get longer unless behaviour
was added.
```

## Deliberately not ported from #434

- **The `docs/source/contributing/code_style.md` half.** The template ships no
Sphinx `docs/` tree and no contributing guide, so that counterpart has no
target here. `AGENTS.md` is the single source.
- **Loop-specific wording.** "the cost of spawning a Claude run" becomes the
generic "a network round-trip, an LLM call"; "`docs/` covers behaviour needed
to operate or maintain the loop" becomes the README sentence above.
- **The port-inheritance rewording** — specific to the upstream codebase (also
dropped by #434 itself relative to its own upstream).

## Out of scope

- Adding a root-level `AGENTS.md` for the `copier_python` repo itself.
- Adding a `docs/` tree or `CONTRIBUTING.md` to the template (that belongs in
its own issue if wanted).
- Changing the ruff `D` selection or its ignore list in
`template/pyproject.toml.jinja`.
- Retro-fitting the rule onto the template's existing docstrings in
`template/src/{{ project_slug }}/*.jinja` — they are already one-liners and
comply.

## Acceptance criteria

- [ ] `template/AGENTS.md` ends with the *Documentation and Comment Style*
section, matching the text above; the existing *Package Management* and
*Git Conventions* sections are unchanged.
- [ ] No Jinja delimiters (`{{`, `{%`) introduced in `template/AGENTS.md`.
- [ ] `tests/test_template.py::test_template_renders` asserts both `AGENTS.md`
and `CLAUDE.md` exist in the generated project.
- [ ] `uv run pytest tests` passes (all tests, including the rendered
project's `make lint`, `make test`, and pre-commit run).
- [ ] Commit message uses the `docs:` prefix and carries no `Co-Authored-By` or
other AI attribution trailer, per the template's own *Git Conventions*.

## Verification

```bash
cd /workspace/copier_python
uv run pytest tests -q
```

The suite generates a project into a temp dir, runs `uv sync`, then `make lint`,
`make test`, and `pre-commit run --all-files` inside it — so it needs network
access and takes a few minutes. Nothing in the added text is executable, so a
green suite plus a read of the rendered `AGENTS.md` is the full check.

## Risk

Low. Documentation-only text in a static template file, plus two assertions.
Worst case is wording taste, reviewable in the diff.
37 changes: 37 additions & 0 deletions template/AGENTS.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,40 @@ those blocks once you start modelling your own domain.
{%- else %}
- `make lint` runs ruff and the `ty` type checker.
{%- endif %}

## Documentation and Comment Style

Brevity is not tidiness — it is what gets the text read. A docstring, comment,
or page nobody reads still has to be maintained, so it is worse than none.

Docstring *presence* is enforced by ruff's `D` rules (numpy convention) in
`pyproject.toml`. This section bounds their *length*.

The unit of judgement is the *added fact*: after the summary line, every line
must tell the reader something they cannot get from the name, the signature,
the type hints, or the code itself.

Earns more than a summary line:

- a contract the signature doesn't show — preconditions, invariants, what it
raises
- units, bounds, or formats a type can't carry (`timeout: float` — seconds or
milliseconds?)
- caller-visible behaviour that would surprise — mutation, ordering,
idempotency, blocking, retries, cost (a network round-trip, an LLM call)
- a pointer to the *why* when it isn't obvious (link the issue or ADR, don't
restate it)

Does not:

- restating the signature, or a numpy `Parameters`/`Returns` section where the
name and type already say it
- narrating the implementation step by step — that's the code
- history ("previously…", "refactored to…") — that's git
- examples for a function whose use is obvious from its name

The same rule governs prose: the README covers what a reader needs to install,
run, and maintain the project; implementation detail belongs in the code.
Prefer a list, table, or short code example over paragraphs. When editing an
existing docstring, comment, or page, it must not get longer unless behaviour
was added.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor — the ported prose paragraph contradicts the docs tree the template now ships.

The spec justified dropping #434's docs/source/contributing/code_style.md half on the grounds that "the template ships no Sphinx docs/ tree and no contributing guide." That is no longer true on main: template/docs/source/contributing/ ships 10 pages, including a 184-line documentation_guide.md. The PR body flags the rationale as outdated; this is the concrete consequence.

Two specific collisions, both in a file that is always in an agent's context (template/CLAUDE.md is just @AGENTS.md):

  1. "implementation detail belongs in the code" vs documentation_guide.md, which routes How it is built to architecture/implementation/ pages covering "the internal types and how they collaborate" with file:line references.
  2. "a page … must not get longer unless behaviour was added" vs the scaffold's own stubs — docs/source/contributing/code_style.md says "This page is a scaffold stub. Author it from the repository." Authoring a stub makes a page much longer with no behaviour change.

Failure scenario: an agent in a generated project is asked to document a new subsystem, reads AGENTS.md, and concludes prose lives in the README while implementation detail stays in the code — so it never authors the architecture/implementation/ page the shipped docs guide requires, and declines to expand the stubs. The shipped Sphinx scaffold stays a scaffold.

Not filed as gating: the spec mandated this text verbatim and put the docs half explicitly out of scope, so a fixer would be overriding a signed-off spec. The call is whether to (a) ship as-is, (b) scope the paragraph to "the README and docs/" and carve out unauthored scaffold pages, or (c) open the follow-up issue for #434's code_style.md half and reconcile there. My preference is (c) with a one-clause tweak here — replacing "implementation detail belongs in the code" with a pointer to docs/source/contributing/documentation_guide.md would remove the contradiction without touching the added-fact rule.

~Written by Claude, run via the agentic engineering loop

4 changes: 4 additions & 0 deletions tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ def test_template_renders(project):
assert (project.path / "src" / "test_project" / "__init__.py").is_file()
assert (project.path / "src" / "test_project" / "main.py").is_file()
assert (project.path / "tests" / "test_test_project.py").is_file()
assert (project.path / "AGENTS.md").is_file()
assert (project.path / "CLAUDE.md").is_file()
# AGENTS.md is rendered from a .jinja source, so placeholders must resolve.
assert "{{" not in (project.path / "AGENTS.md").read_text(encoding="utf-8")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — the guard covers {{ but not {%, and the acceptance criterion names both delimiters. A stray {%- if … %} that Jinja never consumes (e.g. mis-typed as {% -if … %}) would render literally and slip past this assertion.

Low value in practice — a genuinely malformed tag usually fails rendering outright, and this matches the existing test_docs_scaffold_renders pattern, so consistency is a fair reason to leave it. If you want it tight: assert not re.search(r"\{\{|\{%", text) (re is already imported at line 3).

~Written by Claude, run via the agentic engineering loop


domain = project.path / "src" / "test_project" / "domain"
example_test = project.path / "tests" / "test_example_notes.py"
Expand Down
Loading