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
42 changes: 40 additions & 2 deletions luria/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,28 @@ class SchemeInfo:
blurb: str
dir: str
output: str
# "index" (a table and per-value pages under `output`) or "document"
# (the bodies assembled into the one page `output`).
render: str = "index"


@dataclass(frozen=True)
class OutputInfo:
"""A chain or a view, as a template names it: its name, its title where
it has one, and the page it renders into, relative to the project root."""
name: str
title: str
output: str


@dataclass(frozen=True)
class PathsInfo:
"""The pages and directories luria renders at fixed, configured places,
relative to the project root."""
docs: str
reports: str
record: str # the record description
configuration: str # the configuration reference


@dataclass(frozen=True)
Expand Down Expand Up @@ -213,9 +235,24 @@ def vocabulary(self, name: str) -> list[Term]:
def schemes(self) -> list[SchemeInfo]:
return [SchemeInfo(s.prefix, str(s.title or ""),
str(s.blurb or "").strip(), self.cfg.rel(s.dir),
self.cfg.rel(s.output) if s.output else "")
self.cfg.rel(s.output) if s.output else "",
s.render)
for s in self.cfg.schemes.values()]

def chains(self) -> list[OutputInfo]:
return [OutputInfo(c.name, c.title, self.cfg.rel(c.output))
for c in self.cfg.chains.values()]

def views(self) -> list[OutputInfo]:
return [OutputInfo(v.name, v.name, self.cfg.rel(v.output))
for v in self.cfg.views.values()]

def paths(self) -> PathsInfo:
return PathsInfo(self.cfg.rel(self.cfg.docs),
self.cfg.rel(self.cfg.reports),
self.cfg.rel(self.cfg.record_doc),
self.cfg.rel(self.cfg.config_doc))

def journals(self) -> list[JournalInfo]:
return [JournalInfo(j.name, str(j.title or ""),
str(j.blurb or "").strip(), self.cfg.rel(j.dir),
Expand Down Expand Up @@ -307,7 +344,8 @@ def render(view) -> str:
env.globals.update(doc=record.doc, docs=record.docs,
vocabulary=record.vocabulary, schemes=record.schemes,
match=record.match,
journals=record.journals)
journals=record.journals, chains=record.chains,
views=record.views, paths=record.paths)
try:
text = env.get_template(_name(view)).render()
except (jinja2.TemplateError, _Refused, TypeError, ValueError,
Expand Down
9 changes: 9 additions & 0 deletions record/changelog.d/generated-targets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Added

- More of the configuration reaches a view template: each scheme's
`render` kind on `schemes()`, `chains()` and `views()` with their
outputs, and `paths()` for the status reports, the record description
and the configuration reference. This repository's `CLAUDE.md` uses
them to derive its "never edit a generated file" list in the template.
A test holds that every file `luria index` writes falls under a path
the bootloader names.
26 changes: 26 additions & 0 deletions record/devlog.d/2026/10/16/045651.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: 'CLAUDE.md derives its list of generated files'
created: '2026-10-16T04:56:51'
tags: []
---

`CLAUDE.md` named the generated files by hand, in a bullet saying never to
edit them. The list is now derived in its template, from loops over
`schemes()`, `journals()`, `chains()` and `views()`, plus `paths()` for
the pages luria renders at fixed places. A first version put the list in
`Config.generated()` and had `is_generated` ask it, so the check and the
page would share one list. It was dropped in review: the labels and the
loops are presentation and belong in the template, and `is_generated` was
right as it stood.

The cost is a second derivation of "what luria writes", in the template,
so the guard is a property and not a list ([DP-003](../../record/principles.d/DP-003.md)). Every file
`luria index` renders for this record has to fall under a path the
rendered bootloader names. I fired it once ([DP-006](../../record/principles.d/DP-006.md)): with the journals
loop deleted from the template, it failed and named
`docs/devlog/2026-10.md`. A new kind of generated output fails it until
the template learns to say it.

Trap: `adr_index.outputs()` includes nested records' views ([ADR-078](../../record/decisions.d/ADR-078.md)),
which this record's bootloader does not name. The test asks
`outputs(nested=False)`.
24 changes: 20 additions & 4 deletions record/views.d/CLAUDE.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,26 @@ Orient first:
anything the next person would otherwise rediscover, a decision document
when a real alternative was rejected. Filed with context loaded it costs
a paragraph; re-derived cold it costs a session.
- **Never edit a generated file.** Anything stamped `GENERATED` — the
decision index, tag pages, `docs/design-principles.md`, journal books,
reports, `docs/record.md`, `docs/configuration.md`, the README badge
region — is rebuilt by `luria index`. Edit the sources, rerun it.
- **Never edit a generated file.** Anything stamped `GENERATED` is rebuilt
by `luria index`; edit the sources and rerun it. The README's generated
regions are rebuilt too, and so is everything below, derived from this
record's configuration:
{% for s in schemes() if s.output %}
- the {{ s.title or s.prefix }} {{ "index and its per-value pages" if s.render == "index" else "document" }}: `{{ s.output }}`
{% endfor %}
{% for j in journals() %}
- the {{ j.title }} books: `{{ j.output }}`
{% endfor %}
{% for c in chains() %}
- the {{ c.title }} chain page: `{{ c.output }}`
{% endfor %}
{% for v in views() %}
- the `{{ v.name }}` view: `{{ v.output }}`
{% endfor %}
{% set p = paths() %}
- the status reports: `{{ p.reports }}`
- the record description: `{{ p.record }}`
- the configuration reference: `{{ p.configuration }}`
- **Never hand-write a link target for a code.** Write the bare code and
run `luria link --fix`; record prose renders into views in other
directories, so only the fixer knows the frame a target must resolve
Expand Down
45 changes: 45 additions & 0 deletions tests/test_generated_targets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# tests/test_generated_targets.py
"""`CLAUDE.md`'s "never edit a generated file" list is derived in its
template (ADR-130), from what the record's config says it renders. The
template is then a second derivation of what `luria index` writes, beside
the generator itself, so this guards the property and not the list (DP-003):
every file the index writes lies at or under a path the bootloader names.
"""

from __future__ import annotations

import re

from luria import adr_index, config, views


def named_paths() -> list[str]:
text = views.render(config.current().views["bootloader"])
section = text.split("**Never edit a generated file.**", 1)[1]
section = section.split("\n- **", 1)[0]
return re.findall(r"^ - .*: `([^`]+)`$", section, re.MULTILINE)


def test_the_bootloader_names_every_file_the_index_writes():
cfg = config.current()
named = [cfg.root / p for p in named_paths()]
missing = [cfg.rel(out) for out in adr_index.outputs(nested=False)
if not any(out == n or n in out.parents for n in named)]
assert missing == []


def test_it_names_each_kind_of_view():
named = named_paths()
for path in ("docs/decisions", "docs/design-principles.md",
"docs/devlog", "CLAUDE.md", "docs/reports",
"docs/record.md", "docs/configuration.md"):
assert path in named


def test_paths_and_scheme_render_kinds_reach_a_template():
view = config.current().views["bootloader"]
record = views._Record(view.output)
kinds = {s.prefix: s.render for s in record.schemes()}
assert kinds == {"ADR": "index", "DP": "document"}
assert record.paths().record == "docs/record.md"
assert [v.output for v in record.views()] == ["CLAUDE.md"]
Loading