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.
This commit is contained in:
+360
@@ -0,0 +1,360 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user