From 279d022afdffe26204729069b8eb4bf90ddfdfab Mon Sep 17 00:00:00 2001 From: Elton Turing Date: Fri, 4 Sep 2026 22:14:59 +0200 Subject: [PATCH] Nimm die Quellen-Checkouts aus der Ablage heraus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Andreas 04.09.: "nimm den info-checkout aus silverbullet raus". Die Quellen liegen jetzt unter /quellen/ neben der Ablage — die Oberfläche sieht sie nicht mehr. Der Index führt sie unter dem virtuellen Pfad /doku//…, read löst ihn auf die Datei im Checkout auf (mit Wurzelprüfung). Kein Zurückschreiben mehr: geändert wird im Quell-Repo, gelesen über die gebaute MkDocs-Seite. Co-Authored-By: Claude Fable 5.1 --- docs/quellen.md | 32 +++++++++++++++----------------- image/km/config.py | 22 +++++++++++++++++++--- image/km/gitrepo.py | 13 ++++++------- image/km/index.py | 9 +++++---- image/km/runner.py | 18 ++++++++++-------- image/km/server.py | 5 +++-- 6 files changed, 58 insertions(+), 41 deletions(-) diff --git a/docs/quellen.md b/docs/quellen.md index 068175d..8fb1f3a 100644 --- a/docs/quellen.md +++ b/docs/quellen.md @@ -7,21 +7,20 @@ in `gitrepo.py` (`sources_of`) und `runner.py`. ## Bauform -Jede Quelle ist ein eigener Git-Checkout **in** der Ablage: +Jede Quelle ist ein eigener Git-Checkout **neben** der Ablage — außerhalb +von SilverBullet (Andreas, 04.09.: „nimm den info-checkout aus silverbullet +raus"): ``` -//doku// ← z. B. familie/doku/info/ = Clone von ahmann/info +/quellen// ← z. B. /srv/km/data/quellen/info = Clone von ahmann/info ``` -- SilverBullet sieht die Seiten wie alle anderen; der Index nimmt sie mit - (`typ: doku`, `projekt: `, `classification` aus dem Frontmatter). -- Die Ablage ignoriert `*/doku/` in ihrer `.gitignore`: die Wahrheit dieser - Seiten liegt im Quell-Repo, nicht ein zweites Mal in `ahmann/km`. -- **Je Takt:** Änderungen aus der Oberfläche werden im Checkout committet - („km: Änderungen aus der Oberfläche", Autor `KM_GIT_NAME`) und **in das - Quell-Repo gepusht**, danach `git pull --rebase`. Ein Commit aus - SilverBullet landet also Minuten später auf `master` von `ahmann/info` — - ohne PR und ohne Umlaut-Riegel. Das ist gewollt (Auto-Commit und Push). +- Im Index erscheinen die Seiten unter dem virtuellen Pfad + `/doku//` (`typ: doku`, `projekt: `, + `classification` aus dem Frontmatter); `read` löst ihn auf die Datei im + Checkout auf. +- **Je Takt:** `git pull --rebase`, danach der statische Bau (unten). Es + wird nichts zurückgeschrieben — geändert wird im Quell-Repo. ## Konfiguration @@ -37,14 +36,13 @@ KM_SOURCES=info=https://git.home/ahmann/info.git@master Hat die Quelle ein `mkdocs.yml`, baut km sie nach jedem Pull mit Änderung mit `python -m mkdocs build` nach `/site/` — die gewohnte MkDocs-Darstellung, read-only, ohne laufenden Dienst. Caddy liefert sie unter -`/doku//` aus (deploy/README.md). SilverBullet bleibt für die Ablage -selbst; für die Doku ist die gebaute Seite der Leseweg. +`/doku//` aus (deploy/README.md). SilverBullet zeigt nur die Ablage +selbst; für die Doku ist die gebaute Seite der einzige Leseweg. ## Was Andreas wissen sollte - **Alles ist drin**, auch `corp`- und `lan`-Seiten — bis die Quellen - getrennt werden, ist die Familien-Oberfläche damit nicht nur Familie. -- **Admonitions** (`!!! warning`) aus MkDocs zeigt SilverBullet als Rohtext - (Test vom 04.09., spine/core#8). + getrennt werden, zeigt die gebaute Seite der Familie damit nicht nur Familie. - Der Checkout enthält das ganze Repo (`ai/`, `deployment/`, …), nicht nur - `docs/`; indexiert werden nur `*.md`. + `docs/`; indexiert werden alle `*.md`, gebaut wird nur, was `mkdocs.yml` + kennt. diff --git a/image/km/config.py b/image/km/config.py index 6c911f9..2c66afb 100644 --- a/image/km/config.py +++ b/image/km/config.py @@ -55,9 +55,11 @@ class Config: gitea_password: str | None gitea_orgs: list[str] = field(default_factory=list) gitea_repos: list[str] = field(default_factory=list) - # weitere Quellen als eigene Git-Checkouts unter /doku/, - # komplett und ungefiltert (Andreas 04.09.: "vorerst alles komplett rein, - # sobald hive.corp steht, trennen wir die Quellen"). + # weitere Quellen als eigene Git-Checkouts unter /quellen/ + # (ausserhalb der Ablage, nicht in SilverBullet), im Index unter dem + # virtuellen Pfad /doku//…; komplett und ungefiltert (Andreas + # 04.09.: "vorerst alles komplett rein, sobald hive.corp steht, trennen + # wir die Quellen"). # "info=https://git.home/ahmann/info.git@master" sources: list[tuple[str, str, str]] = field(default_factory=list) interval: int = 300 @@ -75,6 +77,20 @@ class Config: def state(self) -> Path: return self.ablage.parent / "km-ingest-state.json" + def source_path(self, name: str) -> Path: + return self.ablage.parent / "quellen" / name + + def source_prefix(self, name: str) -> str: + return f"{self.namespace}/doku/{name}/" + + def resolve(self, rel: str) -> Path: + """Index-Pfad -> Datei: Ablage, oder Quelle hinter ihrem virtuellen Praefix.""" + for name, _, _ in self.sources: + pre = self.source_prefix(name) + if rel.startswith(pre): + return self.source_path(name) / rel[len(pre):] + return self.ablage / rel + @classmethod def from_env(cls) -> "Config": e = lambda k, d="": _clean(os.environ.get(k)) or d # noqa: E731 diff --git a/image/km/gitrepo.py b/image/km/gitrepo.py index a5c69a8..81fcbeb 100644 --- a/image/km/gitrepo.py +++ b/image/km/gitrepo.py @@ -89,11 +89,9 @@ class Ablage: # Wahrheit; die Quellen haben ihr eigenes Remote und werden hier ignoriert. gi = a / ".gitignore" existing = gi.read_text() if gi.exists() else "" - # */doku/ sind die Quellen-Checkouts -- deren Wahrheit liegt im - # jeweiligen Remote (z. B. ahmann/info), nicht ein zweites Mal hier. # _plug/ bleibt drin: die Plug-Dateien (TreeView) gehoeren zur Ablage, # sonst fehlt nach einem frischen Checkout die Seitenleiste. - for line in (".silverbullet*", "*/doku/"): + for line in (".silverbullet*",): if line not in existing.splitlines(): existing += line + "\n" gi.write_text(existing) @@ -141,8 +139,9 @@ class Ablage: def sources_of(cfg: Config) -> list[Ablage]: - """Die Quellen als Checkouts unter /doku/ in der Ablage: die - Oberflaeche sieht sie, km zieht sie je Takt und liefert Aenderungen aus - der Oberflaeche in ihr eigenes Remote ein.""" - return [Ablage(cfg, cfg.ablage / cfg.namespace / "doku" / name, url, branch, name=name) + """Die Quellen als Checkouts NEBEN der Ablage (/quellen/): + SilverBullet sieht sie nicht (Andreas 04.09.: "nimm den info-checkout aus + silverbullet raus"); km zieht sie je Takt, baut die statische Seite und + indexiert sie unter dem virtuellen Pfad /doku//…""" + return [Ablage(cfg, cfg.source_path(name), url, branch, name=name) for name, url, branch in cfg.sources] diff --git a/image/km/index.py b/image/km/index.py index 2dabad1..30262e4 100755 --- a/image/km/index.py +++ b/image/km/index.py @@ -224,8 +224,8 @@ def sha256(data: bytes) -> str: def index_file(con: sqlite3.Connection, ablage: Path, f: Path, default_ns: str, - max_chars: int, overlap: int, default_cls: str = "lan") -> bool: - rel = f.relative_to(ablage).as_posix() + max_chars: int, overlap: int, default_cls: str = "lan", rel: str | None = None) -> bool: + rel = rel or f.relative_to(ablage).as_posix() raw = f.read_bytes() digest = sha256(raw) row = con.execute("SELECT sha256 FROM docs WHERE path=?", (rel,)).fetchone() @@ -254,8 +254,9 @@ def index_file(con: sqlite3.Connection, ablage: Path, f: Path, default_ns: str, return True -def remove_missing(con: sqlite3.Connection, ablage: Path) -> int: - gone = [r[0] for r in con.execute("SELECT path FROM docs") if not (ablage / r[0]).exists()] +def remove_missing(con: sqlite3.Connection, ablage: Path, resolve=None) -> int: + resolve = resolve or (lambda rel: ablage / rel) + gone = [r[0] for r in con.execute("SELECT path FROM docs") if not resolve(r[0]).exists()] with con: for rel in gone: ids = [r[0] for r in con.execute("SELECT id FROM chunks WHERE path=?", (rel,))] diff --git a/image/km/runner.py b/image/km/runner.py index e4a801c..72c05cb 100644 --- a/image/km/runner.py +++ b/image/km/runner.py @@ -41,13 +41,8 @@ class Runner: # Quellen: erst Aenderungen aus der Oberflaeche in ihr Remote, # dann ziehen -- die Wahrheit einer info-Seite bleibt in info. for src in self.sources: - if src.commit_all("km: Änderungen aus der Oberfläche"): - src.push() - result[f"commit_{src.name}"] = src.head() before = src.head() src.pull() - if src.last_push_error: - result[f"push_fehler_{src.name}"] = src.last_push_error built = self._build_site(src, changed=src.head() != before) if built: result[f"site_{src.name}"] = built @@ -115,9 +110,16 @@ class Runner: c = self.cfg con = km_index.connect(c.db) try: - files = sorted(p for p in c.ablage.rglob("*.md") if not any(s.startswith(".") for s in p.parts)) - changed = sum(km_index.index_file(con, c.ablage, f, c.namespace, 600, 60, c.classification) for f in files) - gone = km_index.remove_missing(con, c.ablage) + def md(root): + return sorted(p for p in root.rglob("*.md") + if not any(s.startswith(".") for s in p.relative_to(root).parts)) + files = [(f, None) for f in md(c.ablage)] + for src in self.sources: + pre = c.source_prefix(src.name) + files += [(f, pre + f.relative_to(src.path).as_posix()) for f in md(src.path)] + changed = sum(km_index.index_file(con, c.ablage, f, c.namespace, 600, 60, c.classification, rel) + for f, rel in files) + gone = km_index.remove_missing(con, c.ablage, c.resolve) out = {"dateien": len(files), "geaendert": changed, "entfernt": gone} if c.embed and changed: out["eingebettet"] = km_index.embed_pending( diff --git a/image/km/server.py b/image/km/server.py index 23597c8..97f5232 100644 --- a/image/km/server.py +++ b/image/km/server.py @@ -112,8 +112,9 @@ class Api: def read(self, rel: str) -> dict: if not rel or rel.startswith("/") or ".." in rel.split("/"): raise ValueError("path ungueltig") - f = (self.cfg.ablage / rel).resolve() - if not str(f).startswith(str(self.cfg.ablage.resolve())) or not f.is_file(): + f = self.cfg.resolve(rel).resolve() + roots = [self.cfg.ablage.resolve(), *[self.cfg.source_path(n).resolve() for n, _, _ in self.cfg.sources]] + if not any(str(f).startswith(str(r)) for r in roots) or not f.is_file(): raise FileNotFoundError(rel) text = f.read_text(encoding="utf-8") meta, body = km_index.parse_frontmatter(text)