Files
chemenu/raw/notes/Conversation Transcript - Versioning, CI-CD and Content Migration Session 2026-08-30.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

361 lines
20 KiB
Markdown

# Conversation Transcript - Versioning, CI/CD and Content Migration Session
> Source: Claude Code session (`claude-opus-5`), llm-wiki-test1 workspace
> Collected: 2026-08-30 (session spanned 2026-08-29 to 2026-08-30)
> Participant: Torben
> Fidelity: **faithful summary transcript, not a verbatim log.** Reconstructed by the assistant
> at the end of the session. Torben's questions and decisions are rendered closely; the
> assistant's reasoning is condensed. Command outputs quoted below are real. No credentials
> appeared in the session; internal hostnames mentioned are already documented in `kb/`.
> Revised 2026-08-30 with turns 10-13, which happened after the first version was written; the
> model attribution was corrected at the same time (the first version said "Claude Sonnet 5",
> the session log records `claude-opus-5` for all 501 messages).
Covers three pieces of work that turned out to be the same mechanism seen from different ends:
stack versioning with CI and release artifacts (shipped as `0.1.0`, then `1.0.0`), a content
migration strategy built on top of it, and finally getting the pipeline to actually run.
Resulting commits: `2508f7a`, `7d63d61`, `c3034ab`, `db03b08`, `aace3e7`, `401d700`, `b94166b`.
---
## Turn 1 - "Bearbeite den CI/CD-Punkt aus todo.md ... Was denkst du?"
Torben invoked the `stack-dev` skill and asked for the CI/CD item from `TODO.md` to be built
such that semantic versioning comes with it, that the version then drives update detection, and
that release files come out with which a wiki can be set up.
Assistant proposal, after reading `TODO.md`, `tools/CONTRACT.md`, `dist_cmd.py`, `cli.py`,
`docs_verify.py` and `.gitignore`:
- A root `VERSION` file versioning the **stack**, not the content. Key argument: no auto-bump
from commit messages, because `publish --message "ingest: ..."` writes content commits into
the same repository, so a conventional-commit reading would turn every ingest into a release.
- A `wikitool version` command group; `.gitea/workflows/ci.yml` with the four steps from the
TODO plus a version gate and an export smoke test; `.gitea/workflows/release.yml` triggered by
a `VERSION` change, tagging and publishing a `dist export` tarball.
- Update detection via a stamp file written into every export, compared against the origin's
release feed.
- Explicitly *not* shipping CI into a distributed instance, because `runs-on: linux-docker` is a
site-specific runner label.
## Turn 2 - Three design decisions
The assistant asked three questions with recommendations. Torben's answers:
| Question | Decision |
|---|---|
| Network call in `wikitool` vs. strictly offline | **`version check` may make the network call** - own command, no key, timeout, injectable fetch so tests never touch a network |
| Build the upgrade path now, or detection only | **Detection first**, `dist upgrade` later |
| Start at `1.0.0` or `0.1.0` | **`0.1.0`** (against the assistant's recommendation) |
Because `0.x` was chosen, the assistant defined compatibility as "same leftmost non-zero
component" (the rule Cargo's caret ranges use), so that `0.1.x -> 0.2.0` carries the
migration signal that `MAJOR` carries from `1.0.0` on.
## Turn 3 - "Prüfe die Actions gegen den Gitea Runner, die Doku liegt im Wiki"
Sent mid-turn. The assistant ran `wiki-query` against the corpus before writing the workflow
YAML. **The wiki corrected two assumptions:**
1. Authentication is `${{ gitea.token }}`, documented on the `Act Runner` page under
"Gitea-Token" - not `secrets.GITEA_TOKEN`, and not a 1Password secret, since nothing in these
workflows leaves the instance.
2. The label-to-image mapping for `linux-docker` is **the one thing the wiki does not record**,
and it decides whether a `python3` exists at all. The job image was therefore named
explicitly (`container: image: debian:trixie-slim`, the base Scenario B already proves works
with `actions/checkout@v4`).
Also confirmed from the corpus: routing labels are `linux-docker`, `container-builder`,
`k3s-deploy`; `actions/upload-artifact@v3` (v4 support is limited).
A local finding of the same kind: setting `WIKI_TRACE=0` job-wide breaks two telemetry tests
that assert a trace is written, so CI sets `WIKI_TRACE_DIR` outside the workspace instead.
## Turn 4 - Publish, and the Mass-Update Gate
`wikitool publish` exited 42 with a 21-file breakdown, which was reproduced in full for Torben.
He replied "Freigegeben", and the clearance line published `2508f7a`.
## Turn 5 - A correction: the origin repository is private
Attempting to verify whether CI had run, the assistant discovered that its own earlier inference
was wrong. Before the push, `version check` had returned HTTP 404 and this was read as "no
release published yet, so the feed is anonymously readable". It is not:
```
$ curl -s -w "\nHTTP:%{http_code}\n" ".../api/v1/repos/torben/llm-wiki-test1"
{"message":"not found",...} HTTP:404
$ curl -s -w "\nHTTP:%{http_code}\n" ".../api/v1/repos/torben/definitely-not-a-repo-xyz"
{"message":"not found",...} HTTP:404
```
Gitea answers an anonymous caller identically for an invisible repository and a non-existent
one. Consequence: `WIKITOOL_UPDATE_TOKEN` is a requirement rather than an exception while the
repo stays private, and **no conclusion about CI state can be drawn from outside**. Corrected in
`CHANGES.md` and `INSTALL.md`, published as `7d63d61`.
## Turn 6 - "Die Runner laufen nicht" + the next task
Torben: the runners do not run; he will debug it later at his PC and will provide a **Gitea MCP
server** for it. (Dictated "kaufen" for "laufen".) Then the new task:
> "Wie gehen wir denn mit semantischen Änderungen am Content um, die machen wir ja gerade
> mehrfach. Lass uns eine Migrationsstrategie erarbeiten und umsetzen. Wir brauchen erst mal
> einen Plan."
## Turn 7 - Research: the methodology already existed and had been deleted
The assistant recovered the closed `translate-kb-de` workshop from git history
(`git show de0862f:work/translate-kb-de/README.md` and `plan.md`) and found a complete working
methodology that had been thrown away when the workshop closed:
- Units sized by the **iteration budget** (30 calls → ceiling near 24 pages, target ≤ 21), and
batches sized separately by the **Mass-Update Gate** - conflating the two "cost eleven
unnecessary clearances in the first cut of this plan".
- Per unit, **before anything else**: frontmatter, H1, wikilink targets and cite-ids compared
against `HEAD`.
- `lint` read in full every unit, not just the plausibly-affected sections - unit 1's
frontmatter bug surfaced as a schema error on a field nobody had edited.
- Summaries written by the orchestrating session, never pasted from a subagent ("they
embellish: one turned 'measuring application performance and responsiveness' into 'Latenz und
Durchsatz unter Lastbedingungen'").
Two further findings from reading the code:
- `kb_scan.extract_wikilinks()` returns a **set**. That is correct for `lint` (does the
reference resolve?) and wrong for a migration check (did one go missing?). Three of the four
defects the translation found had unchanged link sets and only changed counts.
- `sections.py` already documents a named migration pattern in its docstring: canonical name
plus aliases is "what lets a corpus migrate page by page instead of all at once", and removing
an alias is a breaking change rather than a cleanup.
## Turn 8 - Plan rejected, with three substantive points
Torben rejected the first plan:
> "Lass uns doch mit Version 1.0.0 anfangen, damit wir den 0.* Sonderfall nicht implementieren
> müssen.
> Wo speichern wir denn die aktuelle Version einer KB?
> Wie decken wir ab, dass eine KB ggf. über mehrere Versionen aktualisiert werden muss? Wir
> müssen ja eigentlich sowas wie 'migrate from 1.3.1 to 1.4.0' und dann 'migrate from 1.4.7 to
> 2.0.0' in Sequenz ausführen?
> In diesem Kontext müssen wir keine Beta oder ähnliche Version Keys unterstützen. x.y.z ist die
> maximale Granularität, die wir brauchen."
The second question exposed a real error in the plan: it had **conflated the stack version with
the content version**. An instance can carry machinery 1.4.0 while its content is still in 1.2.0
shape - and that is the state every upgrade passes through. The first plan had written this off
as a "known limitation" instead of solving it.
Revised design:
| Fact | File | Written by | Answers |
|---|---|---|---|
| Stack version | `VERSION` | `version bump` | which machinery is installed |
| Release stamp | `.wikitool-release.json` | `dist export` | where that machinery came from |
| **KB version** | `.wikitool-kb.json` | `migrate done` | what shape the content is in |
Separate files because the two have opposite rules - the stamp is generated and must never be
hand-edited, the KB state is mutable instance state.
On sequencing: `migrate status` builds the interval `(kb_version, VERSION]` from the migration
documents and orders it ascending; 1.3.1 → 2.0.0 runs 1.4.0, then 1.7.0, then 2.0.0. That no
migration targets 1.3.x is not a special case - it is simply not in the interval. `migrate done`
refuses any version that is not the next link, making a skip impossible and an interrupted
multi-step upgrade resumable.
Starting at `1.0.0` also removed a self-contradiction the assistant had shipped in `0.1.0`: the
guidance in `stack-dev/SKILL.md` assigned `--minor` to both "new capability" and "requires
migration", which cannot both be true under `0.x`. The `compat_key` code needed no change - it
is stated uniformly - only the guidance did.
Plan approved on the second attempt.
## Turn 9 - Implementation and verification
Built: `corpus_diff.py`, `kb_state.py`, `migrate_cmd.py` (`list`/`status`/`verify`/`done`/
`baseline`), `instructions/migrate-corpus.md`, the `migrates_to:`/`migration_kind:` schema
fields, boundary enforcement in both `version bump` and `docs verify`, and a `kb-version` check
in `doctor`. Deliberately not built: mechanical runner primitives (a DSL for zero migrations)
and `dist upgrade`.
**Verification against the real corpus, which unit tests cannot replace:**
1. `migrate verify --from 1c3ca39` over 248 pages: zero invariant violations in 2.2 s - but 13
reported "removed pages" that are not pages. The historical side listed every `.md` under
`kb/` while the working-tree side used `iter_kb_pages`, which skips `COLLECTION.md`,
`INDEX.md` and the kb-root meta files. **Two different definitions of "page."** Fixed with a
shared `kb_scan.is_page_path`, and pinned by a regression test.
2. Negative control: one of two `[[Docker]]` occurrences removed from `kb/entities/tools/Act
Runner.md`, leaving the link *set* unchanged.
```
$ tools/wikitool migrate verify --from HEAD --fail-on-error
248 page(s) compared, 0 added, 0 removed, 1 finding(s).
- kb/entities/tools/Act Runner.md: wikilinks - 'Docker' 2->1
$ tools/wikitool lint --fail-on-error # exit 0, all 21 checks silent
```
This is the claim the whole strategy rests on: `lint` reads a single revision, so a reference
that went missing leaves a corpus that is still perfectly consistent.
3. `version bump --major` correctly refused without a migration document, then succeeded with
`--no-migration "no distributed instance exists yet; 1.0.0 is the migration baseline"` - the
new mechanism's first real use.
4. The dev tree itself became the first `migrate baseline` case: it predates `.wikitool-kb.json`,
but its content never lagged its machinery, so `1.0.0`.
5. 630 tests, `docs verify`, `instructions verify`, `lint --fail-on-error`, and a full
`setup-instance.md` replay against a fresh `dist export`.
Published as `c3034ab` after the Mass-Update Gate breakdown (29 counted files) was reproduced
and cleared.
---
## Turn 10 - "Store this conversation in the wiki as raw source in /raw/notes"
The first version of this file. `raw/CONTRACT.md` lists "conversation transcripts" under
`notes/` explicitly, and a sibling already existed (`Conversation Transcript - AGENTS.md Skill
Restructuring Session 2026-08-04.md`), so the format was inherited rather than invented. Labelled
a summary rather than a log, because a verbatim reconstruction would have been fabrication.
Published as `db03b08`.
Immediately afterwards the Gitea MCP server became available, and a read-only diagnosis of the
long-standing "the runners don't run" item produced the opposite of what had been assumed:
```
list_runs -> 6 runs; 46-51 all conclusion: failure
OCI runtime exec failed: exec: "node": executable file not found in $PATH
❌ Failure - Main actions/checkout@v4
exitcode '127': command not found
```
The runners had been picking the workflows up all along. `actions/checkout` is a JavaScript
action that act_runner executes with `node` **inside the job container**, and the pinned
`debian:trixie-slim` has none. The image was pinned precisely as a precaution against the
undocumented `linux-docker` label mapping - the precaution caused the failure.
## Turn 11 - "Check the gitea-mcp repo, branch ci-build"
Torben pointed at two working workflows on another repo and asked for their shape to be picked
up, plus: the wiki workflows must not run on content changes.
`torben/gitea-mcp@ci-build`, `ci-build.yaml`, runs 42-45 green. The whole answer was one
line in an apt list:
```yaml
- name: Install CI Dependencies
run: apt-get install -y --no-install-recommends git nodejs curl unzip ca-certificates build-essential
- name: Checkout Code
uses: actions/checkout@v7
```
`nodejs` installed **before** the checkout, and checkout at `@v7`. Both were adopted in `ci.yml`
and `release.yml`. `runs-on: linux-docker` was kept: runs 46-51 proved it routes and starts the
container, so the label was never the problem.
For the second half, `paths-ignore` on everything `publish` touches. Three choices are worth
recording because they are all *refusals to infer*: `kb/CONTRACT.md` is deliberately not
excluded (it lives under a content directory but belongs to the stack); no `!**/CONTRACT.md`
negation, because Gitea's support for negated filter patterns is undocumented; and the list is
written twice instead of shared through a YAML anchor, because GitHub's parser rejects anchors
outright and Gitea's is not documented to accept them. The patterns fail open - anything
unanticipated still triggers CI. Published as `aace3e7`.
## Turn 12 - The first green run finds a real bug
Run 52 got past checkout and reached `pytest` for the first time ever. Two of 630 tests failed:
```
FAILED test_new_page.py::test_new_source_author_falls_back_to_git_config
FAILED test_provenance.py::test_new_source_with_multiple_raw_files
AssertionError: ERROR No author configured for this instance.
2 failed, 628 passed in 13.85s
```
`config.default_author()` runs `git config user.name` with `cwd=config.ROOT`; the fixture root
is not a repository, so the answer came from the **global git config of whoever ran the suite**.
In the container, as root, there is none. Both tests had been green on every developer machine
for months without ever testing what their names claimed.
Fixed in the tests rather than by giving CI an identity: the first now makes its fixture root a
real repository with a *local* `user.name` and asserts the concrete name, which tests the
fallback more sharply than before; the second sets `WIKI_AUTHOR`, since it is a `raw_files:`
test and authorship was only a precondition. Verified locally with
`GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null pytest` - 630 passed.
That made it `1.0.1`, and because `VERSION` moved, `release.yml` fired on its own. It answered
the last open question of the session without being asked:
```
Created release v1.0.1 (id 54).
Uploaded llm-wiki-stack-1.0.1.tar.gz.
Uploaded llm-wiki-stack-1.0.1.tar.gz.sha256.
```
`${{ gitea.token }}` may create releases and tags and upload assets. No Actions secret with
`write:repository` is needed. Published as `401d700`.
## Turn 13 - "Can we take the CI/CD part out of todo.md?" then: move it to issues
Two cleanups. First `TODO.md`: the entire retrospective went out (64 lines), because the same
history was already in the `0.1.0`, `1.0.0` and `1.0.1` changelog entries and a second copy is
exactly the drifting duplicate the stack avoids everywhere else. Published as `b94166b`.
Then the remaining work moved to Gitea issues #7-#11 - `dist upgrade`, hardening the suite
against silent environment dependencies, the nightly drift check, coverage, and confirming
`paths-ignore` actually matches. `TODO.md` now links to them instead of describing them.
The split was made along a line worth naming: issue #11 is explicitly *an observation, not a
build task* - the proof arrives on its own with the next content-only publish. Filing it as work
would have invited someone to build machinery for a question that answers itself.
---
## Decisions worth carrying forward
- **The version describes the stack; content has its own version.** Conflating them makes the
mid-upgrade state unrepresentable.
- **Compatibility = leftmost non-zero component.** Uniform across `0.x` and `1.x`; from `1.0.0`
it reads as plain semver. No pre-release suffixes - a second ordering rule would have to be
honoured by the release feed, the migration chain and the compatibility check alike.
- **Migrations are `manual: true` instructions** under `instructions/migrations/`, so they ship
with `dist export` without a second export path. Baseline is `1.0.0`; anything older is
re-exported, not migrated.
- **Count, never set**, when asking whether a rewrite dropped something.
- **CI tags, never an agent** - which is what keeps AGENTS.md invariant 5 intact.
- **The wiki corrected the assistant twice** in this session (the Gitea token form, the runner
image), and the assistant's own inference was wrong twice more: anonymous API access, and the
claim that the runners never accepted the workflows. Checking the corpus before writing
infrastructure code paid for itself; inferring past what the corpus actually said did not.
- **A CI run is evidence; a local run is a habit.** Two tests asserted a fallback they never
exercised, and stayed green for months, because every machine that ran them happened to
satisfy the precondition. Nothing short of a foreign environment would have found it.
- **The failure and the fix lived in different places.** The symptom was in CI, the defect was
in the tests. Giving CI a git identity would have made the run green and left the bug.
## What this changed about the runner, in one place
For anyone writing the next workflow against the CI runner - do not re-derive this:
- A pinned `container:` image must `apt-get install nodejs` **as the first step, before
checkout**. act_runner executes JavaScript actions with `node` inside the job container.
- `actions/checkout@v7`, `actions/upload-artifact@v3` (v4 is limited on this instance).
- `debian:trixie-slim` works and carries python3 3.13. Labels `linux-docker` and
`container-builder` both accept a job that names its own image.
- `${{ gitea.token }}` suffices for releases, tags and asset uploads.
- The repo is **private**, and Gitea answers anonymous callers with an identical `404` for an
invisible repo and a non-existent one - so `curl` proves nothing. Read runs through the MCP
server.
## Open at the end of the session
The pipeline runs. `v1.0.1` is published with tarball and `.sha256`. What remains is tracked as
Gitea issues rather than prose: **#7** `dist upgrade`, **#8** hardening the test suite, **#9**
the nightly drift check, **#10** coverage, **#11** confirming `paths-ignore` matches.
One item is genuinely unresolved rather than merely unbuilt: whether Gitea evaluates the
`paths-ignore` patterns as expected. It needs no work, only a look at `list_runs` after the next
content-only publish - and after this session, that is exactly the kind of claim that should not
be assumed.