Chemenu 2.1.0 - deterministischer Wissenskompiler
CI / verify (push) Failing after 32s
Release / release (push) Successful in 38s

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:
2026-09-01 16:24:34 +02:00
commit 18ae28f918
368 changed files with 50628 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
# CI for the wiki stack.
#
# One job, stopping at the first failure - the stack has no artifact to build
# and nothing to deploy, so the pipeline's whole job is "does the machinery
# still hold together, and does the distribution it produces still work".
#
# Runner: `linux-docker` is one of this Gitea instance's three routing labels
# (alongside `container-builder` and `k3s-deploy`). The job image is named
# explicitly rather than inherited from the runner's label mapping, which is
# not documented anywhere: `debian:trixie-slim` is the base the instance's
# container-build workflows already use, and Trixie's python3 is 3.13, past
# the 3.11 floor `doctor` enforces.
#
# Pinning that image makes `nodejs` this workflow's own responsibility.
# `actions/checkout` is a JavaScript action, and act_runner runs it with `node`
# *inside the job container* - a slim Debian has none, and the job dies with
# exit 127 before any step of ours runs. The shape below (apt `nodejs` first,
# then `checkout@v7`) is the one proven on this instance by
# torben/gitea-mcp@ci-build, workflow `ci-build.yaml`, runs 42-45.
#
# Triggers: content commits are excluded. `publish` touches kb/, raw/ and work/
# and never the stack, so running the suite for them would be pure noise. The
# exclusions are deliberately literal rather than a `!**/CONTRACT.md` negation,
# whose support in Gitea's filter matching is unverified: every pattern here
# names content, so anything unanticipated still triggers CI. The list is
# repeated rather than shared through a YAML anchor for the same reason -
# GitHub's parser rejects anchors outright, and Gitea's is not documented to
# accept them. `kb/CONTRACT.md` is absent on purpose: it is a stack file that
# happens to live under a content directory, and it must keep its CI.
#
# That the filter works is now observed, not assumed (Gitea issue #11): commit
# f916376 published only kb/ and raw/ paths and produced no run at all, while
# the stack commits on either side of it (adfa220, 40adbb7) each produced two.
# Gitea evaluates these patterns the way GitHub does. Do not re-derive this.
name: CI
on:
push:
branches: [main]
paths-ignore:
- 'kb/*/**'
- 'kb/index.md'
- 'kb/log.md'
- 'kb/provenance.md'
- 'raw/*/**'
- 'work/*/**'
- 'reports/*/**'
pull_request:
branches: [main]
paths-ignore:
- 'kb/*/**'
- 'kb/index.md'
- 'kb/log.md'
- 'kb/provenance.md'
- 'raw/*/**'
- 'work/*/**'
- 'reports/*/**'
workflow_dispatch:
jobs:
verify:
runs-on: linux-docker
container:
image: debian:trixie-slim
env:
# Scope the Iteration Budget Gate to this run instead of letting it fall
# back to the parent PID, and keep the trace out of the checkout so the
# working tree stays clean for the ignore-rule checks.
WIKITOOL_SESSION_ID: ci-${{ github.run_id }}
WIKI_TRACE_DIR: /tmp/wikitool-trace
DIST_DIR: /tmp/dist
steps:
- name: System dependencies
# `nodejs` is not for us - it is what act_runner needs to execute the
# JavaScript action in the next step. It has to be installed before the
# checkout, which is why this step comes first.
run: |
set -eu
apt-get update -qq
apt-get install -y --no-install-recommends \
python3 python3-venv git nodejs ripgrep ca-certificates
rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v7
with:
# The version gate diffs against the pushed range's base, so the
# shallow default clone is not enough.
fetch-depth: 0
- name: Tool environment
run: |
set -eu
git config --global --add safe.directory "$GITHUB_WORKSPACE"
python3 -m venv tools/.venv
tools/.venv/bin/pip install --quiet --upgrade pip
tools/.venv/bin/pip install --quiet -r tools/requirements.txt
# pytest-cov is CI-only: tools/requirements.txt describes what an
# *instance* needs at runtime and ships with `dist export`, and an
# instance does not measure this suite. Installed beside pytest for
# the same reason pytest itself is.
tools/.venv/bin/pip install --quiet pytest pytest-cov
- name: Tests
# Not run with WIKI_TRACE=0: two telemetry tests assert that a trace is
# written, and disabling the emitter globally fails them. The suite
# redirects WIKI_TRACE_DIR per test on its own.
#
# One run, not two. This job used to be the only place the suite met a
# machine with no global git configuration, which is how Gitea #8 was
# found - two tests that silently read the developer's `git config
# user.name`. That hole is now closed in the suite itself: the autouse
# `hermetic_environment` fixture gives every test an empty HOME, no
# git configuration and none of the tool's own environment, so this
# container is no longer a special environment worth a second run.
# See instructions/dev/testing-conventions.md.
#
# Coverage is reported, not enforced: there is deliberately no
# `--cov-fail-under` yet (Gitea #10). The threshold gets set in its own
# later commit, with the measured number as its justification - one
# picked before the number is either too low to bite or too high to
# survive the next honest commit, and the second kind gets lowered
# instead of earned. Config: tools/.coveragerc.
run: |
set -eu
cd tools
.venv/bin/python -m pytest -q \
--cov --cov-report=term --cov-report=xml --cov-report=html
- name: Coverage report
# `always()`: a red suite is exactly when the per-module numbers are
# worth reading, and the upload must not disappear with the failure.
# v3, not v4 - v4 is restricted on this Gitea instance; v3 is what is
# proven here (torben/gitea-mcp@ci-build, ci-build.yaml, runs
# 42-45).
if: always()
uses: actions/upload-artifact@v3
with:
name: coverage-${{ github.run_id }}
path: |
tools/coverage.xml
tools/htmlcov/
retention-days: 14
- name: Verify the development tree
run: |
set -eu
tools/wikitool docs verify
tools/wikitool instructions verify
tools/wikitool lint --fail-on-error
- name: Version gate
# A stack change with no version bump cannot be released, because the
# release would carry a change nobody named. Scoped to what
# `dist export` actually ships as behaviour - prose docs and these
# workflows are not in it, and a typo fix should not force a bump.
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BEFORE_SHA: ${{ github.event.before }}
run: |
set -eu
base="${BASE_SHA:-${BEFORE_SHA:-}}"
case "$base" in
""|0000000000000000000000000000000000000000)
echo "No base commit to compare against - skipping the version gate."
exit 0
;;
esac
if ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
echo "Base commit $base is not in this clone - skipping the version gate."
exit 0
fi
changed="$(git diff --name-only "$base" HEAD)"
stack="$(printf '%s\n' "$changed" \
| grep -E '^(tools/|types/|instructions/|AGENTS\.md$|[^/]+/CONTRACT\.md$)' || true)"
if [ -z "$stack" ]; then
echo "No stack paths touched - no version bump required."
exit 0
fi
if printf '%s\n' "$changed" | grep -qx 'VERSION'; then
echo "Stack changed, and VERSION moved to $(cat VERSION)."
exit 0
fi
echo "Stack paths changed without a VERSION bump:"
printf '%s\n' "$stack" | sed 's/^/ /'
echo ""
echo 'Fix: tools/wikitool version bump --patch --title "<what changed>"'
echo 'Then `docs verify` holds VERSION and CHANGES.md together.'
exit 1
- name: Export the distribution
run: tools/wikitool dist export "$DIST_DIR"
- name: The distribution works as a fresh instance
# Replays instructions/setup-instance.md end to end, minus its four
# interactive decision points. What this tests is the release artifact
# as an artifact: the documented path from an unpacked export to a
# verified instance. Running one `instructions verify` against the
# export would only have re-checked the file it just copied.
#
# Personalization is stubbed the same way the identity is: the real
# step interviews the user, so CI substitutes a fixed answer - here,
# the template minus its sentinel line. That is deliberately the
# cheapest thing `doctor`'s personalization check accepts, because
# what is under test is that the export *carries* the templates, not
# what a person would write into them.
run: |
set -eu
cd "$DIST_DIR"
git init -q -b main
git config user.name "CI Instance"
git config user.email "ci@example.invalid"
for personal in USER SOUL; do
grep -v 'wikitool:template-unfilled' "$personal.md.template" > "$personal.md"
done
python3 -m venv tools/.venv
tools/.venv/bin/pip install --quiet -r tools/requirements.txt
tools/wikitool instructions sync
tools/wikitool index rebuild
tools/wikitool sources rebuild-index
tools/wikitool doctor
tools/wikitool docs verify
tools/wikitool instructions verify
tools/wikitool lint --fail-on-error
tools/wikitool version show
# A fresh instance owes no migration: dist export declares its content
# version, so `status` must answer rather than ask for a baseline.
tools/wikitool migrate status
+104
View File
@@ -0,0 +1,104 @@
# Nightly drift check.
#
# CI (`ci.yml`) runs on push and deliberately ignores content paths, because
# `publish` touches kb/, raw/ and work/ on every ingest and running the suite
# for that is noise. That exclusion is observed to work (Gitea #11), which is
# exactly why this file exists: since it landed, `lint --fail-on-error` no
# longer runs when the *corpus* changes. A wiki that drifts into inconsistency
# over a run of publishes would be seen by nobody.
#
# There is also drift that happens with no commit at all. `sources coverage`
# starts reporting the moment a file appears under `raw/` without a source page
# claiming it, and `migrate status` only answers when something asks.
#
# So: the same checks CI runs against the tree, on a clock instead of a push.
# `lint --fail-on-error` is the reason; the rest costs seconds.
#
# Deliberately absent: `migrate verify --from <rev>`. It needs a comparison
# revision that means something, and "yesterday" is not one - the invariant
# diff answers "did *this migration* lose anything", not "did anything change
# since yesterday". In normal operation a changed page is the desired outcome,
# not a finding.
#
# Runner shape: identical to ci.yml, and for the same reason - `nodejs` is what
# act_runner needs to execute the JavaScript `checkout` action *inside* the job
# container, so it is installed before the checkout or the job dies with exit
# 127. Do not re-derive this; see the header of ci.yml.
#
# Failure is meant to be visible without opening the Actions page. That is
# Gitea's own run notification, not something this workflow builds: a job that
# files its own issue needs an Actions token with issue-write and a dedup rule,
# which is more machinery than a red run already carries.
name: Nightly
on:
schedule:
# 03:17 UTC. Gitea evaluates cron in UTC and only for workflows on the
# default branch, so this file has to live on `main` to fire at all - a
# test branch proves nothing about it.
- cron: '17 3 * * *'
workflow_dispatch:
jobs:
drift:
runs-on: linux-docker
container:
image: debian:trixie-slim
env:
# Scope the Iteration Budget Gate to this run instead of letting it fall
# back to the parent PID, and keep the trace out of the checkout so the
# working tree stays clean.
WIKITOOL_SESSION_ID: nightly-${{ github.run_id }}
WIKI_TRACE_DIR: /tmp/wikitool-trace
steps:
- name: System dependencies
run: |
set -eu
apt-get update -qq
apt-get install -y --no-install-recommends \
python3 python3-venv git nodejs ripgrep ca-certificates
rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v7
- name: Tool environment
# More than `ci.yml`'s equivalent, because this job runs `doctor` and
# `ci.yml` does not. `doctor` asks whether a *working instance* is
# correctly configured, and a bare checkout is not one yet - it is the
# fresh clone instructions/bootstrap.md describes. Two of its checks
# answered for the container instead of for the repository on the first
# run (#9): `git-identity` found no `user.name`, and `skills` found
# nothing published, because `.agents/skills/` and `.claude/skills/`
# are generated and deliberately not committed. So the bootstrap runs
# first, and `doctor` then reports on an instance rather than on a
# tarball. `instructions verify` needs the same, for the same reason.
run: |
set -eu
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git config --global user.name "Nightly"
git config --global user.email "nightly@example.invalid"
python3 -m venv tools/.venv
tools/.venv/bin/pip install --quiet --upgrade pip
tools/.venv/bin/pip install --quiet -r tools/requirements.txt
tools/wikitool instructions sync
- name: The instance is still correctly configured
run: tools/wikitool doctor
- name: The stack still describes itself
run: |
set -eu
tools/wikitool docs verify
tools/wikitool instructions verify
- name: The corpus is still structurally sound
# The one check push-driven CI no longer performs on a content commit.
run: tools/wikitool lint --fail-on-error
- name: Every raw file is still claimed, and the content shape declared
run: |
set -eu
tools/wikitool sources coverage
tools/wikitool migrate status
+142
View File
@@ -0,0 +1,142 @@
# Publish a release when VERSION moves on main.
#
# The release artifact is exactly a `dist export` tree, packed with a top-level
# directory: unpack it, run instructions/setup-instance.md, and there is a
# working wiki instance - no checkout of this repo required. CI already proved
# that path works before this workflow ever runs.
#
# The tag is created here, by CI, and never by an agent: AGENTS.md invariant 5
# ("never call raw git commit/push") stays intact because nothing in a session
# has to tag anything.
#
# Auth is `${{ gitea.token }}` - the short-lived per-job token this Gitea
# instance issues - not a 1Password secret. Nothing here reaches outside the
# instance, so the Zero-Trust secret path that the container-build workflows
# use has nothing to carry.
name: Release
on:
push:
branches: [main]
paths:
- VERSION
jobs:
release:
runs-on: linux-docker
container:
image: debian:trixie-slim
permissions:
contents: write
env:
WIKITOOL_SESSION_ID: release-${{ github.run_id }}
WIKI_TRACE_DIR: /tmp/wikitool-trace
BUILD_DIR: /tmp/build
# The address a *reader* uses. `github.server_url` is whatever the runner
# registered against (an internal one here), which is right for the API
# call below and wrong for a URL baked into every distributed instance.
PUBLIC_BASE_URL: https://gitea.nehmer.net
steps:
- name: System dependencies
# `nodejs` is for act_runner, not for us - see the note in ci.yml.
run: |
set -eu
apt-get update -qq
apt-get install -y --no-install-recommends \
python3 python3-venv git nodejs ripgrep ca-certificates curl jq tar gzip
rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v7
- name: Tool environment
run: |
set -eu
git config --global --add safe.directory "$GITHUB_WORKSPACE"
python3 -m venv tools/.venv
tools/.venv/bin/pip install --quiet --upgrade pip
tools/.venv/bin/pip install --quiet -r tools/requirements.txt
- name: Resolve the version and refuse to re-release it
id: version
env:
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TOKEN: ${{ gitea.token }}
run: |
set -eu
version="$(cat VERSION | tr -d '[:space:]')"
tag="v${version}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
status="$(curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${TOKEN}" "${API}/releases/tags/${tag}")"
if [ "$status" = "200" ]; then
echo "Release ${tag} already exists. VERSION was touched without being raised;"
echo 'bump it with `tools/wikitool version bump` instead of re-releasing.'
exit 1
fi
echo "No release ${tag} yet - proceeding."
- name: Release notes from CHANGES.md
# `version notes` fails when the changelog has no entry for this
# version, which is the last place that mistake can still be caught.
run: |
set -eu
tools/wikitool docs verify
tools/wikitool version notes > /tmp/release-notes.md
cat /tmp/release-notes.md
- name: Build the distribution tarball
id: build
env:
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
run: |
set -eu
name="chemenu-stack-${VERSION}"
mkdir -p "$BUILD_DIR"
tools/wikitool dist export "${BUILD_DIR}/${name}" \
--source-repo "${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}" \
--source-commit "${GITHUB_SHA}" \
--release-url "${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}" \
--update-url "${PUBLIC_BASE_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/latest"
tar -czf "${BUILD_DIR}/${name}.tar.gz" -C "$BUILD_DIR" "$name"
( cd "$BUILD_DIR" && sha256sum "${name}.tar.gz" > "${name}.tar.gz.sha256" )
cat "${BUILD_DIR}/${name}.tar.gz.sha256"
echo "name=${name}" >> "$GITHUB_OUTPUT"
- name: Publish the release
env:
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TOKEN: ${{ gitea.token }}
TAG: ${{ steps.version.outputs.tag }}
NAME: ${{ steps.build.outputs.name }}
run: |
set -eu
# Creating the release creates the tag, pinned to this commit.
payload="$(jq -n \
--arg tag "$TAG" \
--arg target "$GITHUB_SHA" \
--arg name "$TAG" \
--rawfile body /tmp/release-notes.md \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body,
draft: false, prerelease: false}')"
release="$(curl -sS -f -X POST "${API}/releases" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload")"
id="$(printf '%s' "$release" | jq -r '.id')"
echo "Created release ${TAG} (id ${id})."
for asset in "${NAME}.tar.gz" "${NAME}.tar.gz.sha256"; do
curl -sS -f -X POST "${API}/releases/${id}/assets?name=${asset}" \
-H "Authorization: token ${TOKEN}" \
-F "attachment=@${BUILD_DIR}/${asset}" > /dev/null
echo "Uploaded ${asset}."
done
echo "Done: ${PUBLIC_BASE_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"