a9703520a7
Files changed: - CHANGES.md - VERSION - docs/ownership-and-templates.md - tools/CONTRACT.md - tools/chemenu/commands/dist_cmd.py
116 lines
7.8 KiB
Markdown
116 lines
7.8 KiB
Markdown
# Ownership and Templates
|
|
|
|
Chemenu ships two kinds of files side by side, and at a glance they look the same: both are
|
|
plain markdown, both sit in the repo root or under `kb/`, both get read at session start. But a
|
|
stack upgrade treats them completely differently. Some - [AGENTS.md](../AGENTS.md),
|
|
[kb/CONTRACT.md](../kb/CONTRACT.md), the per-stage contracts - are identical in every instance
|
|
that runs this stack and are the next release's to replace (with one caveat about local edits,
|
|
below). Others - `USER.md`,
|
|
`SOUL.md`, `kb/CONVENTIONS.md`, `ENVIRONMENT.md` - describe one particular instance, and
|
|
overwriting them would silently erase a choice someone made on purpose.
|
|
|
|
## Two different kinds of truth
|
|
|
|
The stack-owned files describe how the tool works. `kb/CONTRACT.md` opens by saying it holds
|
|
what `tools/wikitool` enforces or what follows mechanically from how it operates - see
|
|
[kb/CONTRACT.md](../kb/CONTRACT.md), lines 10-13. That kind of statement doesn't vary by
|
|
instance: the compiler behaves the same way regardless of who is running it, so the sentence
|
|
describing that behavior can be copied byte-for-byte into every checkout without becoming
|
|
wrong anywhere.
|
|
|
|
The instance-owned files describe a choice: which language pages are written in, what tone the
|
|
agent takes, who the operator is, which git remote is authoritative, which MCP servers are
|
|
reachable. None of that follows from the tool's mechanics - two instances of the identical
|
|
stack can answer all of these differently and both be correct. [AGENTS.md § Personalization](../AGENTS.md#personalization)
|
|
frames the split the same way for `kb/CONTRACT.md` versus `kb/CONVENTIONS.md`: "the split is by
|
|
who may change the sentence, not by what it is about." A rule about page structure could in
|
|
principle have been written per-instance too, but then every instance answering "not German" to
|
|
setup would be hand-editing a file the stack also ships, and the next `dist export` merge would
|
|
hand the instance's own file back to it, discarding the customization.
|
|
|
|
## Why silent overwrite is the failure being designed against
|
|
|
|
A stack update is meant to be a routine, low-risk operation: pull the latest release, get
|
|
whatever fixes and features shipped since the last one. That only stays low-risk if the update
|
|
knows which files it's allowed to touch. If `USER.md` or `kb/CONVENTIONS.md` were treated the
|
|
same as `AGENTS.md` - shipped and periodically re-copied - an upgrade would quietly replace a
|
|
description of *this* operator, in *this* language, with whatever placeholder or default the
|
|
stack maintainers wrote. The damage wouldn't be loud: nothing crashes, the files still parse,
|
|
the agent just starts acting on the wrong premises until someone notices the voice or the
|
|
language changed.
|
|
|
|
Keeping the boundary at the file level, rather than trying to merge changes within a shared
|
|
file, means an upgrade never has to guess which lines are "stack" and which are "instance" -
|
|
the file itself already answers that.
|
|
|
|
## Why the boundary is a predicate rather than a list
|
|
|
|
For a while the boundary was written down as a list of paths - once in `dist_cmd.py`, once in
|
|
the merge procedure a private instance was told to run by hand, and once in the check that
|
|
procedure ended with. Three copies of one fact, which is the shape [AGENTS.md](../AGENTS.md)
|
|
invariant 8 exists to forbid, and they drifted exactly as predicted: the hand-run procedure was
|
|
still naming three paths after the collection contracts had moved to the instance's side of the
|
|
line, so it discarded upstream changes to files it had never heard of, while its own final check
|
|
excluded the same three paths and therefore reported success.
|
|
|
|
`chemenu/ownership.py` replaced the lists with one question - is this path, under a content
|
|
stage, the stack's or the instance's? - answered by shape rather than by enumeration:
|
|
`<stage>/CONTRACT.md`, and anything ending `.template`. Both consumers ask it, so `dist export`
|
|
and `wikitool upstream merge` cannot disagree, and a machinery file added under a content stage
|
|
tomorrow is recognised by both without either being edited. The deeper point is not the
|
|
deduplication: a list has to be maintained by whoever remembers it exists, and the failure mode
|
|
when nobody does is silence, because a path the list has never heard of simply looks like
|
|
content.
|
|
|
|
## Why a `.template`, not just an absent file
|
|
|
|
The mechanism for instance-owned content is a `.template` file the distribution ships instead
|
|
of the real one - `USER.md.template`, `SOUL.md.template`, `kb/CONVENTIONS.md.template`,
|
|
`ENVIRONMENT.md.template`. An alternative would have been to ship nothing at all and let a
|
|
brand-new instance start from a blank page. The template exists because a blank page doesn't
|
|
tell [instructions/setup-instance.md](../instructions/setup-instance.md) what shape the answer
|
|
should take, and it gives nothing for a validator to check afterward.
|
|
|
|
A template carries a placeholder value - a sentinel - in the fields that need a real answer.
|
|
Setup interviews the operator and replaces the sentinel with what they actually said. That
|
|
gives `doctor` a mechanical way to tell "personalized" from "not yet": a file that still
|
|
contains the sentinel hasn't been through setup, regardless of whether the file exists. That's
|
|
also why `ENVIRONMENT.md` only warrants a WARN rather than a FAIL when absent - see
|
|
[AGENTS.md § Environment](../AGENTS.md#environment) - while a missing or unfilled
|
|
`USER.md`/`SOUL.md`/`kb/CONVENTIONS.md` is a harder failure: `ENVIRONMENT.md` describes one
|
|
checkout among possibly several and is gitignored for that reason, so its absence is a normal
|
|
state rather than a sign setup was skipped.
|
|
|
|
## The consequence in practice
|
|
|
|
An upgrade sorts every shipped path into three categories, not two - and the third one only
|
|
becomes visible once an upgrade is a command rather than a hand-run copy:
|
|
|
|
- **Verbatim files** - `AGENTS.md`, `kb/CONTRACT.md`, the per-stage contracts, everything under
|
|
`tools/`, `types/` and `instructions/` - are the release's to replace.
|
|
- **`.template`-sourced files** - `USER.md`, `SOUL.md`, `kb/CONVENTIONS.md`, each
|
|
`kb/<name>/COLLECTION.md`, `ENVIRONMENT.md`, the `root: kb` type-specs - are never written by
|
|
an upgrade at all. The distribution ships only the `.template` beside them, so the filled file
|
|
is out of reach by construction rather than by a rule someone has to remember.
|
|
- **Seeded-once files** - `.wikitool-kb.json`, `CHANGES.md`, `kb/log.md`, `raw/.gitkeep` - are
|
|
written into a *new* instance by `dist export` and belong to the instance from then on. They
|
|
are the awkward category: they sit in the release stamp's file list like any other shipped
|
|
file, so an upgrade has to exclude them deliberately (`chemenu.ownership.is_export_stub` and
|
|
`is_upgrade_preserved`). An upgrade that re-seeded them would reset the record of which
|
|
migrations ran, or erase the changelog the instance wrote for itself.
|
|
|
|
The first category carries a caveat that the word "verbatim" hides. It says who *decides* the
|
|
content, not that overwriting is always safe: an instance can still have edited a verbatim file
|
|
- a patched `tools/`, a locally adjusted instruction - and an upgrade assuming otherwise would
|
|
destroy that silently. Avoiding that assumption is the whole reason `dist export` records a
|
|
sha256 per shipped file in `.wikitool-release.json`. `wikitool dist upgrade` compares every
|
|
candidate path against the digest recorded when it was installed, overwrites only what still
|
|
matches, and refuses rather than overwrite what does not.
|
|
|
|
So the practical rule is narrower than "overwrite the verbatim files, leave the rest alone":
|
|
overwrite the verbatim files *this instance has not touched*, never write the other two
|
|
categories, and make a locally changed file a decision someone takes deliberately instead of
|
|
one an upgrade takes for them. The template-sourced files were filled in once, by a person, for
|
|
a reason, and nothing about a newer release of the stack's mechanics gives it standing to
|
|
override that.
|