Files
chemenu/tools/chemenu/ownership.py
T
torben a6d07f97c4
CI / verify (push) Successful in 5m19s
CI / pwsh (push) Successful in 1m55s
Release / release (push) Successful in 36s
feat!: installation only from a release, into an empty folder; upstream merge/verify and private-instance.md removed, dist adopt, shell-neutral instructions (#153)
Files changed:
- .gitea/workflows/ci.yml
- .gitea/workflows/release.yml
- AGENTS.md
- CHANGES.md
- DEVELOPMENT.md
- EVALS.md
- INSTALL.md
- README.md
- VERSION
- docs/ownership-and-templates.md
- instructions/CONTRACT.md
- instructions/bootstrap.md
- instructions/dev/dev-setup.md
- instructions/dev/stack-dev/SKILL.md
- instructions/gates.md
- instructions/ingest-large-tree.md
- instructions/kb-profiles.md
- instructions/mcp-read-server.md
- instructions/migrations/3.0.0-authoring-conventions.md
- instructions/preflight.md
- instructions/private-instance.md
- instructions/session-setup.md
- instructions/setup-instance.md
- instructions/upgrade-instance.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/upstream_cmd.py
- tools/chemenu/commands/work_cmd.py
- tools/chemenu/config.py
- tools/chemenu/ownership.py
- tools/chemenu/tests/test_cli.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_instructions_shell.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/tests/test_run_budget.py
- tools/chemenu/tests/test_upstream_cmd.py
- tools/chemenu/toc.py
- tools/preflight.ps1
- tools/preflight.sh
2026-10-01 22:12:09 +02:00

86 lines
4.1 KiB
Python

"""The ownership boundary for a path under a content stage: does it belong to
the *stack* (ships with every distribution, and a release replaces it) or to
the *instance* (never ships filled, and no release touches it)?
One predicate, so every caller answers the same question about the same paths
instead of keeping its own literal list that can drift out of sync - see
AGENTS.md invariant 8, and Gitea #30 for the incident that made the drift
concrete (the private-instance merge procedure, since removed with
`upstream merge` in Gitea #153, hardcoded a three-path list that `dist_cmd.py`
had already outgrown).
"""
from __future__ import annotations
# The stages whose content belongs to *this instance*, not the stack. Mirrors
# the sentence .gitignore already makes about raw/, kb/ and work/ being the
# repo's content, plus reports/ - only reports/CONTRACT.md is tracked there,
# the rest is gitignored, so restoring it is a no-op today. It stays in the
# set anyway: a set that is "almost" this one is the beginning of the same
# drift this module exists to end.
CONTENT_STAGES = ("kb", "raw", "work", "reports")
# Bare filenames `dist export` overwrites with a fresh stub rather than
# shipping the stack's own copy. Not stack-owned: they are the instance's own
# log/placeholder, which `dist export` writes brand-new rather than copying.
EXPORT_STUB_NAMES = ("log.md", ".gitkeep")
# The single machinery filename directly under a content stage's own root.
_STAGE_CONTRACT_NAME = "CONTRACT.md"
def is_stack_owned(relative: str) -> bool:
"""Whether `relative` - a path under a content stage, e.g. "kb/CONTRACT.md"
or "kb/entities/COLLECTION.md.template" - is machinery: it ships with
every distribution, and a release replaces it.
True for exactly two shapes:
- `<stage>/CONTRACT.md`, directly under a content stage's own root. Not
recursive: `kb/<collection>/COLLECTION.md` sits one level deeper and is
instance-owned (see kb/CONTRACT.md's collection-ownership split).
- Any path under a content stage ending in `.template` - by construction
the stack's own copy of something the instance adopts by renaming
(`kb/CONVENTIONS.md.template` and every `kb/<name>/COLLECTION.md.template`
today; a future stack-owned template under a content stage falls under
this rule automatically, with no code change here).
False for everything else under a content stage, `EXPORT_STUB_NAMES`
included - those are handled separately by whichever caller cares about
them.
"""
parts = relative.split("/")
if len(parts) < 2 or parts[0] not in CONTENT_STAGES:
return False
if relative.endswith(".template"):
return True
return len(parts) == 2 and parts[1] == _STAGE_CONTRACT_NAME
def is_export_stub(name: str) -> bool:
"""Whether `name` (a bare filename, not a path) is one `dist export`
overwrites with a fresh stub of its own rather than shipping verbatim."""
return name in EXPORT_STUB_NAMES
# Root-relative paths `dist export` seeds once, from a template it owns, and
# which the instance owns exclusively from that point on. `dist upgrade`
# (Gitea #7) must never overwrite them, even though they sit in the release
# stamp's `files` block like any other planned file - the same shape as
# `EXPORT_STUB_NAMES` above, but keyed by full path rather than bare filename,
# since nothing else at the repo root gets this treatment and a bare-filename
# match would be too broad here.
#
# `.wikitool-kb.json` is `migrate done`'s state file: an upgrade that resets it
# declares a content shape nobody actually produced. `CHANGES.md` is the
# instance's own changelog, not the stack's - `dist export` seeds it from a
# blank template (`dist_templates/CHANGES.md`) the same way it seeds
# `kb/log.md`, and overwriting it on upgrade would erase every entry the
# instance ever wrote for itself.
UPGRADE_PRESERVED_PATHS = (".wikitool-kb.json", "CHANGES.md")
def is_upgrade_preserved(relative: str) -> bool:
"""Whether `relative` (a plan-relative path from the repo root, e.g.
"CHANGES.md") is one `dist upgrade` must never write."""
return relative in UPGRADE_PRESERVED_PATHS