Files
chemenu/raw/notes/Conversation Transcript - Write-Once Frontmatter Fields and touch --set Session 2026-08-31.md
torben 18ae28f918
CI / verify (push) Failing after 32s
Release / release (push) Successful in 38s
Chemenu 2.1.0 - deterministischer Wissenskompiler
Chemenu kompiliert Rohnotizen zu einem verlinkten, quellengebundenen Wiki:
raw/ -> types/ + tools/ -> kb/ -> reports/. Was mechanisch ist, macht
tools/wikitool; was Urteil braucht, macht ein Agent unter Contracts, deren
Grenzen in Code durchgesetzt sind statt im Prompt.

Dieser Commit ist der Startpunkt der oeffentlichen Historie. Die vorherige
Entwicklung fand in einer privaten Instanz statt und ist nicht Teil dieses
Repositorys; ihre Erzaehlung steht vollstaendig in CHANGES.md, das mit 44
Eintraegen von 0.1.0 bis 2.1.0 erhalten geblieben ist.

Der mitgelieferte Korpus ist ein Testbett und eine Demo: 170 Seiten ueber den
Stack selbst - Gates, Lint, Versionierung, Suche, das Wiki-Muster. Er
dokumentiert das Werkzeug mit den eigenen Mitteln des Werkzeugs.

Lizenz: AGPL-3.0 fuer den Stack (tools/, types/), CC-BY-4.0 fuer die Inhalte.
Die Grenze zwischen beiden ist der Dateiplan, den dist export berechnet -
siehe NOTICE.
2026-09-01 16:26:14 +02:00

162 lines
7.9 KiB
Markdown

# Conversation Transcript - Write-Once Frontmatter Fields and touch --set Session
> Source: Claude Code session (`claude-opus-5`), llm-wiki-test1 workspace
> Collected: 2026-08-31
> Participant: Torben
> Fidelity: **faithful summary transcript, not a verbatim log.** Torben's three design
> decisions are recorded verbatim as the options he was shown and the ones he chose; command
> output quoted below is real; the assistant's reasoning is condensed.
> No second-hand material. No credentials appeared in the session.
> One of two transcripts cut from this stretch of the session; the other covers the
> Mass-Update Gate's counting rule and the measured iteration-budget calibration.
Covers Gitea issue #14, implemented as `touch --set/--add/--remove` in stack `1.4.0`
(`dbe2f73`). Issue #14 closed, #16 opened for the part deliberately left out.
---
## Turn 1 - "Führe 14 aus und lege mir die nötigen Entscheidungen jeweils mit Hintergrund vor"
The assistant read `touch.py` before proposing anything, and found the groundwork already in
place: `validate_fields()` validates **per field** rather than per document, which is exactly
what a `--set` needs. Its docstring explains why - whole-document validation would refuse to
bump `modified:` on a page that is invalid for some unrelated reason, "which is exactly the page
most in need of maintenance".
Three decisions were put to Torben, each with the trade-off spelled out.
### Decision 1 - which fields may `--set` write?
| Option | Consequence |
|---|---|
| **Denylist** (chosen) | Everything the schema declares, minus a short reasoned block list. A field added to a type-spec works immediately. |
| Allowlist | A maintained set of writable fields. Safer against surprises, but every new schema field needs a code change. |
| Denylist minus the already-flagged fields | Also blocks `summary`/`provenance`/`confidence-base`, which have their own options. Prevents two ways to do one thing, at the cost of a surprising refusal. |
**Torben chose the denylist.** The argument that carried it: an allowlist is a second copy of
the schema, and the copy is the one that drifts - AGENTS.md invariant 8 applied to a constant.
Blocked, each naming the command that owns the field instead:
- `type:` - changes the page's schema *and* its directory; that is `page-lifecycle.md`.
- `confidence:` - derived from `confidence_base` by decay, not authored.
- `related:`/`sources:`/`entities:`/`concepts:` - `xref` maintains the reverse direction and
the body bullets; a bare frontmatter write would leave the other half stale.
### Decision 2 - what does `--set tags=a,b` mean for a list that already has values?
| Option | Consequence |
|---|---|
| Replace only | One rule. Adding a single tag means naming the whole list. |
| **Replace plus `--add`/`--remove`** (chosen) | Three options instead of one, but a single element can be changed without reading the list first. Leaves open what `--remove` does with an absent element. |
**Torben chose replace plus `--add`/`--remove`.**
The open sub-question - `--remove` on an element that is not there - the assistant decided
itself and said so: **it succeeds and reports it.** Idempotent like `xref remove`, because a
repair command that refuses to run twice is one nobody dares script; but never silent, because a
silent no-op looks exactly like a successful removal, and that is how a mistyped element name
hides.
### Decision 3 - does this pass also deliver `raw rename`?
| Option | Consequence |
|---|---|
| **`touch --set` only** (chosen) | Stays `size/S`. The file move becomes possible in two steps (`git mv`, then `touch --set raw_files=…`). The remaining gap gets its own issue. |
| Both in one release | Closes #14 fully and removes the intermediate state, at `size/M`: backward search over all `raw_files:` references, behaviour with multiple owners, contract lines for two commands. |
**Torben chose the narrower pass.** `raw rename` became issue #16 (`prio/2`, `size/S`).
---
## Turn 2 - implementation
### What moved
`_coerce_set_value`, `_parse_set_fields` and `_check_raw_files_exist` left `new_page.py` for
`commands/_util.py` and lost their leading underscores. Two commands, one implementation -
otherwise `touch --set` would have inherited the comma bug from #12 on day one. A test covers
exactly that: an existing raw file whose name contains a comma, referenced through `\,`, read
back as a single path.
`raw_files:` written by `touch` gets the same filesystem existence check `new` performs. It is
I/O, not a data shape, so no schema can express it.
### Two refusals, deliberately worded differently
A blocked field is a routing problem, so the message names the command that owns it. An unknown
field is a typo or the wrong page type, so the message lists what the page actually has - the
value is learning that `tags` was meant:
```
$ tools/wikitool touch --page "Diff-Reviewable Agent Edits" --set "sources=Source - X"
ERROR `sources` cannot be set with --set: page-reference field - use `wikitool xref add` / `xref remove`
$ tools/wikitool touch --page "Diff-Reviewable Agent Edits" --set "tag=x"
ERROR Type types/concept.md declares no field 'tag'.
Settable fields for this page: concept_type, confidence_base, created,
modified, provenance, summary, tags
```
### A test-harness trap, fixed once
The existing `test_touch.py` called the Typer callback directly with a full argument list, so
three new options broke seven call sites with `TypeError: 'OptionInfo' object is not iterable`.
A callback invoked directly receives `OptionInfo` objects for whatever the caller omits - the
same hazard `dist_cmd.py` avoids by keeping its logic beside the wrapper. The tests now go
through a `_touch(**overrides)` helper that supplies every option, so the next option costs one
line rather than seven.
---
## Turn 3 - the repair that proves it
`kb/concepts/Diff-Reviewable Agent Edits.md` had been created hours earlier by an ingest whose
`--set tags=` value carried a trailing comma; everything after the separator was lost. That
subagent correctly worked through all three ways out and rejected each: `touch` could not set
`tags:`, hand-editing frontmatter comes too close to invariant 1, and `rm` + `new` would have
broken the `concepts:` reference the source page already held. The page kept `[agent-workflow]`
permanently, because of a comma.
Against the real corpus:
```
$ tools/wikitool touch --page "Diff-Reviewable Agent Edits" --add "tags=context-engineering,tooling"
tags: added 'context-engineering', 'tooling'
OK Touched kb/concepts/Diff-Reviewable Agent Edits.md
$ tools/wikitool touch --page "Diff-Reviewable Agent Edits" --add "tags=context-engineering" --no-date
OK 'Diff-Reviewable Agent Edits' already up to date; nothing to change.
$ tools/wikitool touch --page "Diff-Reviewable Agent Edits" --remove "tags=vertippt" --no-date
tags: not present, nothing removed: 'vertippt'
OK 'Diff-Reviewable Agent Edits' already up to date; nothing to change.
```
The page now carries `[agent-workflow, context-engineering, tooling]`.
---
## The defect this closed, stated once
A field `new` wrote once - `tags:`, `raw_files:`, `source_url:` - was afterwards unreachable.
`touch` did not know it; hand-editing frontmatter is what the tool exists to prevent; deleting
and recreating the page breaks every reference already pointing at it. And `new` is **not
idempotent**, so the window to get the value right was exactly one command wide.
The evidence that this was a rate rather than an accident: three failures across three
consecutive ingests on the same day, at two different fields, by three different agents. One of
them was a trailing comma.
---
## Outcome
| Artifact | Result |
|---|---|
| Stack version | `1.4.0` (MINOR - new capability, backwards compatible) |
| Commit | `dbe2f73` - 9 files |
| Tests | 674 passing, in the normal and the hardened environment |
| Gitea | #14 closed with the three decisions recorded; #16 opened (`prio/2`, `size/S`) |
| Corpus | The unrepairable page repaired |