Exporte als konfigurierte Ziele: abgeleitete, schreibgeschützte Lesefassung von kb/ je Zielordner (z. B. OneDrive/SharePoint für Microsoft 365) #214

Closed
opened 2026-10-12 16:29:09 +00:00 by torben · 7 comments
Owner

Abgespalten aus der Diskussion zu #186 (2026-10-12). Stand: Erledigt (2026-10-13). Gebaut und veröffentlicht als 10.0.0-beta.13 (Commit 99945c4, CI-Lauf 683 grün). Abschluss-Nachzug als 10.0.0-beta.14 (Commit d92800f, CI-Lauf 686 grün): instructions/publish-cycle.md sagt, was eine WARN oder ein ERROR des Exports nach publish bedeutet, und docs/explanation/architecture.md nennt den Seitenexport. Ausgeliefert mit Release 10.0.0. F1 ist durch die Prüfung von Hand am 2026-10-13 beantwortet (siehe „Nachgang“), das How-to ist nachgezogen (Commit f2f251f). Aus derselben Prüfung sind drei Folgepakete entstanden: #216, #217, #218.

Anlass

In der entstehenden beruflichen Instanz sollen Microsoft-365-Werkzeuge die KB lesen können. Laut einer Recherche des Betreibers (nicht belegt, keine Primärquelle im Repo) zieht Copilot Autopilot, vormals „Microsoft Scout“, seinen Kontext über Work IQ aus Inhalten in Microsoft 365, also auch aus OneDrive/SharePoint. Eine öffentliche API oder MCP-Schnittstelle ist für Autopilot nicht dokumentiert. Für solche Abnehmer ist ein Export in M365-Speicher der realistische Weg. Der Live-Weg über unseren MCP-Server ist ein eigenes Paket. Ein Export ist außerdem der Ansatzpunkt für Konverter in andere Formate.

Befunde aus dem Baum (Stand vor dem Paket)

  • export guidelines war schon ein Export mit fast allen Bausteinen (tools/chemenu/guideline_export.py, tools/chemenu/commands/export_cmd.py):
    • Auswahl über searchs eigene Prädikate (search.filters, „One filter semantics“), mit dem virtuellen Feld collection sowie kind, subtype, title und jedem Frontmatter-Feld. Formen: f=v, f~v, f>=v, f<=v, f>v, f<v, f:* (vorhanden), !f (fehlt). Ein „ungleich“ gibt es nicht. select() lehnt ab: kein Prädikat, unbekanntes Feld, unlesbares Frontmatter irgendwo unter kb/, null Treffer.
    • Sauberkeit: check_clean() lehnt ein kb/ oder types/ mit uncommitteten Änderungen ab. last_commit() nennt den letzten Commit, der eine ausgewählte Seite berührt hat, nicht HEAD. Die Ausgabe ist deterministisch.
    • Rendern: render_page() entfernt den generierten Links-Block und den Fußnotenblock, löst Wikilinks in ihren Anzeigetext auf und entfernt [^cite-id]. Code bleibt dabei byte-gleich (strip_code_spans).
    • Rückweg-Sperre: Jede Datei, deren erste Zeile mit <!-- wikitool:export begann (EXPORT_MARKER, is_export), wurde von raw capture ausgelassen und von raw accept abgelehnt (commands/raw/_common.py, _check_no_export). file_is_export las vor dem Paket nur die ersten rund 23 Bytes; seit 10.0.0-beta.13 erkennt es beide Marker-Formen (Entscheidung 6).
  • Ein Vertraulichkeitsfeld gibt es nicht. Weder kb/CONVENTIONS.md noch ein Type-Spec deklariert eins. !vertraulich in den Beispielen unten ist ein angenommenes Feld, das eine Instanz selbst einführen müsste. Der Export braucht keins, er nimmt jedes Feld, das search kennt.
  • Konfigurationsmuster: config_shapes.INTAKE (#213) und BACKUP (#186) waren die Vorbilder: benannte Einträge mit handler, commands als names_command, Pfade über absolute_path_problem plus „außerhalb der Instanz“ (upload.py). Programme laufen nach fetch_routes.run_fetcher: ohne Shell, stdin geschlossen, Timeout, Ende von stderr bei Fehler.
  • Nach publish lief schon ein Folgeschritt: die Maschinensicherung (git_publish.py § Machine-data backup). Scheitert sie, ist das ein WARN, der Commit bleibt stehen, und backup run wiederholt sie. Eine fehlerhafte backup-Sektion stoppt publish vor dem Commit. Vorbild für Entscheidung 12.
  • Eine ältere Version liest eine unbekannte Settings-Sektion nicht und scheitert nicht daran (settings.unknown, doctor: „not read by this version“). Wichtig für den Versionsteil.

Entscheidungen

  1. Der Arbeitsbaum liegt nie in einem Sync-Ordner (#186). Ein Export ist eine abgeleitete, schreibgeschützte Fassung, die bei jedem Lauf neu erzeugt wird, nie eine Arbeitskopie. Was jemand im Ziel ändert, fließt nicht zurück.
  2. Ziele werden einzeln konfiguriert, jedes mit eigenen Parametern und einem Handler. Weitere Zielarten kommen über die handler-Liste hinzu, ohne Umbau.
  3. Auswahl nur per Positivliste. Ohne ausdrückliche Auswahl wird nichts exportiert. Was die KB in einen Speicher verlässt, den Dritte und deren Werkzeuge lesen, muss benannt sein, nicht nur nicht ausgeschlossen.
  4. Wohin exportiert wird, entscheidet der Betreiber. Jedes Feld der Zieldefinition ist checkout, gate bzw. names a command, config set lehnt es mit Exit 42 ab. Ein Agent kann keinen neuen Abfluss einrichten. Einen Lauf auslösen darf der Agent (export run, und mittelbar über publish, Entscheidung 12), denn er schreibt nur, was der Betreiber freigegeben hat. Das entspricht upload collect.
  5. Auswahl über search-Prädikate (war F3). Kein eigenes Schema aus Collection/Typ/Tag, sondern dieselbe Prädikat-Syntax wie search --field und export guidelines. Eine zweite Filtersemantik wäre eine zweite Stelle, an der „welche Seiten“ anders verstanden wird. Feld-Bedingungen sind damit ohne Zusatzarbeit möglich, etwa !vertraulich für eine Instanz, die ein solches Feld einführt. Form: include ist eine Liste von Auswahlen, jede Auswahl eine Liste von Prädikaten. Innerhalb einer Auswahl müssen alle gelten (UND), exportiert wird, was auf mindestens eine Auswahl passt (ODER).
  6. Der Export-Marker steht im YAML-Frontmatter (war F8, Betreiber 2026-10-12). Jede exportierte Markdown-Datei beginnt mit einem YAML-Frontmatter, dessen erster Schlüssel wikitool_export: "kind=pages instance=<…> commit=<…>" ist (instance wie guideline_export.instance_id(), commit nach Entscheidung 7). Die Datei behält damit echtes YAML, das pandoc und andere Werkzeuge als Metadaten lesen.
    • is_export/file_is_export erkennen beide Formen. (1) Wie bisher: Die erste Zeile beginnt nach optionalem BOM mit <!-- wikitool:export. (2) Neu: Die erste Zeile ist ---, und bis zur schließenden ----Zeile steht eine Zeile, die mit wikitool_export: beginnt. Die Suche ist ein Zeilenscan, kein YAML-Parse, damit auch kaputtes YAML erkannt wird. file_is_export liest dafür höchstens 64 KB. Ein Frontmatter ohne schließendes --- in diesem Bereich gilt als kein Export.
    • raw capture und raw accept übernehmen beide Formen ohne eigene Änderung, weil sie is_export/file_is_export aufrufen. export guidelines schreibt weiter die Kommentarzeile.
    • Entschieden beim Bau: Was ein Export ist, steht allein in guideline_export.py (beide Marker-Konstanten, is_export, file_is_export). page_export.py importiert von dort, ein drittes Modul gibt es nicht.
  7. Ein Lauf bildet einen Commit ab, keinen Arbeitsstand. check_clean() gilt auch hier, commit= ist last_commit() der ausgewählten Seiten.
  8. Dateiablage im Ziel: Jede Seite wird <pfad relativ zu kb/> mit Endung .md, also dieselbe Ordnerstruktur wie kb/. Keine generierten Index-Dateien. Ein Wikilink auf eine exportierte Seite wird ein relativer Markdown-Link (links: relative) oder Klartext (links: text). Ein Wikilink auf eine nicht exportierte Seite wird immer Klartext.
  9. Fußnoten-Zitate [^cite-id] werden in lesbare Quellenangaben aufgelöst: Die Fußnote bleibt, ihr Text ist der Titel der Quellseite (als Link, wenn die Quellseite mit exportiert wird, sonst Klartext). Ein fremder Indexer kann mit der Roh-Syntax nichts anfangen. Der Titel einer nicht exportierten Quellseite verlässt damit die KB. Das gilt als Teil der exportierten Seite, so wie der Text eines Wikilinks.
  10. export guidelines bleibt, wie es ist (war F5). Seine Ziele sind die erfassten Repositories, und es hat ein eigenes Gate. Geteilt wird nur Code. Entschieden beim Bau: page_export.py nutzt Auswahl, check_clean, last_commit, die Rendering-Bausteine und die Export-Erkennung direkt aus guideline_export.py. Die Spiegel-Helfer von backup.py (write_atomic, file_digest, is_under, real_path, is_link, safe_relative, prune_empty) wurden dafür öffentlich, statt sie zu kopieren.
  11. Keine Zielart ohne Dateisystem in diesem Paket (war F6). Eine SharePoint-Bibliothek über Graph o. ä. leistet heute die OneDrive-Synchronisierung. Die handler-Liste bleibt der Erweiterungspunkt.
  12. Auslöser: von Hand und optional nach publish (war F2, Betreiber 2026-10-12). export run [<ziel>...] immer. Dazu je Ziel after_publish (Default false): Nach jedem publish, das committet hat, läuft das Ziel, auch mit --no-push, denn der Export hängt am Commit, nicht am Push. Das funktioniert wie die Maschinensicherung: Ein Fehler ist eine WARN-Zeile mit Grund, der Commit bleibt stehen, Exit 0. Wiederholt wird mit export run, nicht mit einem weiteren publish. Eine fehlerhafte export-Sektion stoppt publish mit Exit 1, bevor etwas committet wird, aber nur, wenn ein Ziel after_publish setzt. Sonst betrifft sie publish nicht.
  13. Kein Gate bei wachsender Menge (war F4, Betreiber 2026-10-12). Die Zieldefinition ist die Freigabe. Neue Seiten, auf die eine Auswahl passt, laufen ohne weiteres Gate mit. Die Ausgabe nennt sie je Ziel als „neu“. Wer etwas nicht im Ziel haben will, schließt es per Prädikat aus. AGENTS.md § Gates bleibt unverändert.
  14. Rückweg-Sperre auch über den Digest (war F7, Betreiber 2026-10-12). Eine DOCX/PDF aus dem Handler command kann keinen Marker tragen. raw accept lehnt deshalb zusätzlich jede Datei ab, deren sha256 im Manifest eines konfigurierten Ziels steht, mit derselben Art Meldung wie _check_no_export, und verschiebt nichts. Ein Ziel, dessen Manifest fehlt oder unlesbar ist, ergibt eine WARN-Zeile, keinen Abbruch. Eine fehlerhafte export-Sektion ergibt ebenfalls eine WARN (die Prüfung entfällt dann), denn raw accept darf an einer Exporteinstellung nicht scheitern. Eine im Ziel bearbeitete und dann eingereichte Datei hat einen anderen Digest und geht durch. Sie gilt als neue Quelle eines Menschen.
  15. doctor kennt nur OK und FAIL für export (entschieden beim Bau). Geplant war zusätzlich eine WARN bei fehlendem Zielordner. Ein fehlender Zielordner ist aber kein Fehler: export run legt ihn an (siehe „Was ein Lauf garantieren muss“), eine WARN dafür wäre bei jedem neu eingerichteten Ziel ein Fehlalarm. doctor meldet daher: Sektion fehlt → OK (absent), Sektion fehlerhaft → FAIL, sonst OK mit jedem Ziel, seinem Handler, seinem Ordner und ob es nach publish läuft.

Konfiguration

Sektion export in .wikitool-local.json, neu als config_shapes.EXPORT in SETTINGS_SECTIONS:

{
  "export": {
    "targets": {
      "m365": {
        "handler": "markdown",
        "path": "C:\\Users\\<user>\\OneDrive - <Firma>\\Chemenu-KB",
        "include": [["collection=entities"], ["collection=concepts", "!vertraulich"]],
        "frontmatter": "summary",
        "links": "relative",
        "after_publish": true
      },
      "m365-docx": {
        "handler": "command",
        "command": "pandoc-docx",
        "path": "C:\\Users\\<user>\\OneDrive - <Firma>\\Chemenu-KB-docx",
        "include": [["collection=concepts"]]
      }
    },
    "commands": {
      "pandoc-docx": ["pwsh", "-NoProfile", "-File", "C:\\tools\\md-to-docx.ps1"]
    }
  }
}
Feld Tier Pflicht Bedeutung
targets checkout, gate ja Zielname ([a-z0-9-]+) → Ziel. Der Name ist das Argument von export run. Mindestens ein Ziel.
targets.<n>.handler checkout, gate ja markdown (eingebaut) oder command (Konverter des Betreibers)
path checkout, gate ja Zielverzeichnis. Absolut, außerhalb der Instanz. Darf weder in einem anderen Ziel, einem intake-Ordner (handler: folder) noch im backup.target liegen oder einen davon enthalten. (Die Regel zum backup.target ist zu streng: #216.)
include checkout, gate ja Liste von Auswahlen (Entscheidung 5). Eine leere Liste oder eine leere Auswahl ist ein Fehler der Sektion. Die Prädikat-Syntax wird beim Lesen der Sektion geprüft (parse_predicate), die Feldnamen beim Lauf (validate_fields). Alle Seiten wählt [["collection:*"]] (#217).
frontmatter checkout, gate nein Was das YAML-Frontmatter außer wikitool_export trägt: minimal (nichts weiter), summary (title, type, summary, modified, soweit vorhanden) oder full (das ganze Frontmatter der Seite). Default summary.
links checkout, gate nein relative oder text. Default relative.
after_publish checkout, gate nein Boolean, Default false. Entscheidung 12.
command checkout, gate ja bei command Name eines Eintrags unter commands
commands.<n> checkout, names a command nein Programm und Argumente als Array

Felder, die nicht zum Handler passen, und ein command-Verweis ohne Eintrag unter commands sind Fehler der Sektion (wie bei intake). frontmatter und links gelten für beide Handler, denn command bekommt die Markdown-Fassung. Ohne Sektion: export run endet mit Exit 1 und nennt die Sektion, doctor meldet OK (absent). Bei einer fehlerhaften Sektion endet export run mit Exit 1 und doctor meldet FAIL. Für publish und raw accept gelten Entscheidung 12 und 14.

Handler command: wikitool rendert erst wie markdown in ein Staging-Verzeichnis und ruft dann das Programm ohne Shell, mit geschlossenem stdin und 300 s Timeout auf. Die letzten zwei Argumente sind das Verzeichnis mit der Markdown-Fassung und ein leeres Ausgabeverzeichnis. Bei Exit 0 wird der Inhalt des Ausgabeverzeichnisses (rekursiv, nur reguläre Dateien, kein Symlink) das, was ins Ziel kommt. Bei jedem anderen Ende bleibt das Ziel unverändert, gemeldet wird das Ende von stderr (bis 2000 Zeichen). Das Staging wird in jedem Fall entfernt. Es liegt außerhalb jedes Ziels und wird nie committet.

Was ein Lauf garantieren muss

export run [<ziel>...], ohne Argument alle Ziele. Jedes Ziel läuft unabhängig. Scheitert eins, laufen die anderen trotzdem, Exit 1, wenn mindestens eins gescheitert ist.

Der Lauf löscht und überschreibt nur, was er selbst geschrieben hat. Im Ziel liegt ein Manifest .wikitool-export.json mit Zielname, Instanz, Commit und jeder geschriebenen Datei mit sha256.

  • Ein Lauf entfernt nur Dateien, die im Manifest stehen und nicht mehr exportiert werden. Dadurch leer gewordene Verzeichnisse entfernt er nur, wenn er sie selbst angelegt hat (im Manifest vermerkt).
  • Dateien, die nicht im Manifest stehen, bleiben byte-gleich.
  • Weicht eine Manifest-Datei im Ziel von ihrem Digest ab, weil jemand sie dort geändert hat, bricht das Ziel mit Fehler ab, bevor es etwas schreibt, und nennt die Datei. Fremde Änderungen werden nie still überschrieben.
  • Fehlt eine Manifest-Datei im Ziel (dort gelöscht), wird sie neu geschrieben, das Ziel scheitert nicht daran.
  • Fehlt das Manifest in einem nicht leeren Zielordner, wird abgelehnt. Es wird nie in einen fremden Ordner hineingespiegelt. Ein fehlender Zielordner wird angelegt.
  • Null ausgewählte Seiten sind ein Fehler des Ziels, bevor etwas gelöscht wird. Ein leerer Export würde sonst das ganze Ziel leeren, dieselbe Überlegung wie in guideline_export.select().
  • Unverändert heißt unberührt: Eine Datei, deren Digest dem Manifest entspricht, wird nicht neu geschrieben, damit der Sync-Client nicht alles erneut hochlädt. Ein Lauf ohne Änderung an der Auswahl schreibt nichts, auch nicht das Manifest. Weil commit= im Marker steht, ändert sich eine Datei nur, wenn ein Commit die ausgewählten Seiten berührt hat. Dann ändern sich alle Dateien des Ziels. Das ist hinzunehmen, denn es ist derselbe Mechanismus wie bei GUIDELINES.md.
  • Geschrieben wird atomar je Datei (temporäre Datei im Ziel, dann os.replace), das Manifest zuletzt.
  • Ausgabe je Ziel: neu (mit Seitentiteln), geändert, entfernt, unverändert (Zahl), gescheitert mit Grund.

Nachgang: F1 und die Prüfung von Hand (2026-10-13)

F1 – Format für Work IQ/Copilot: beantwortet. Frage war, ob Copilot bzw. Work IQ .md-Dateien aus OneDrive/SharePoint indizieren oder ob es DOCX/HTML/PDF braucht. Prüfung von Hand durch den Betreiber auf der Arbeitsinstanz (Release 10.0.0, ein Tenant): Ein markdown-Ziel im OneDrive mit after_publish: true lief nach publish mit, und Microsoft 365 Copilot fand die .md-Dateien und konnte sie nutzen. Das How-to nennt das jetzt und empfiehlt weiter, mit einem kleinen markdown-Ziel zu beginnen; ein command-Ziel bleibt der Weg für einen Tenant, der die Dateien nicht aufnimmt (docs/how-to/export-to-a-synced-folder.md, Commit f2f251f, reine docs/-Prosa ohne Bump). Kein Code.

Weitere Befunde derselben Prüfung, je ein eigenes Paket:

  • Ein Exportziel als Geschwisterordner im backup.target wird abgelehnt, ein intake-Ordner dort nicht – #216.
  • Eine Auswahl „ganze KB“ fehlt dem How-to; [["collection:*"]] funktioniert – #217.
  • Rohdokumente aus raw/ sind nicht Teil des Exports (Entscheidung 1) und werden gewünscht – #218.

Versionsteil

MINOR. Drop-in-Test:

  • Vorwärts: neues Kommando export run, neue Sektion export, Default-Verhalten unverändert. Ohne Sektion ändert sich nichts, auch nicht an publish und raw accept (Entscheidungen 12 und 14 greifen nur mit konfigurierten Zielen). is_export erkennt zusätzlich Dateien mit dem Frontmatter-Schlüssel wikitool_export. Den Schlüssel schrieb vor diesem Paket nichts.
  • Rückwärts: Eine ältere Version liest die unbekannte Sektion export nicht und meldet sie in doctor als „not read by this version“. Die Manifeste liegen außerhalb der Instanz.

Umgesetzt auf dem laufenden Kandidaten 10.0.0 als 10.0.0-beta.13 (--minor, überschreitet nichts neu), der Abschluss-Nachzug als 10.0.0-beta.14 (--patch).

Betroffene Dateien und Flächen

  • Code:
    • tools/chemenu/commands/export_cmd.py (neu: export run, mit cli_contract-Eintrag),
    • neues Modul tools/chemenu/page_export.py für Konfiguration, Auswahl, Rendern und Spiegeln ohne CLI,
    • tools/chemenu/guideline_export.py (is_export, file_is_export, Entscheidung 6),
    • tools/chemenu/backup.py (Spiegel-Helfer öffentlich, Entscheidung 10), tools/chemenu/repo_capture.py (Aufrufer angepasst),
    • tools/chemenu/config_shapes.py (EXPORT, in SETTINGS_SECTIONS),
    • tools/chemenu/commands/doctor.py (Prüfung export, Entscheidung 15),
    • tools/chemenu/commands/git_publish.py (Entscheidung 12, neben dem Sicherungsschritt; der cli_contract-Eintrag von publish nennt den Schritt und seine WARN),
    • tools/chemenu/commands/raw/_common.py (Entscheidung 14, neben _check_no_export; die cli_contract-Einträge von raw accept und raw capture nennen beide Marker-Formen und den Digest),
    • tools/chemenu/cli_contract.py (Gruppe „Exports“), tools/chemenu/reference_doc.py (Region der Sektion).
  • Tests: neu tools/chemenu/tests/test_export_run.py (65 Tests: Lauf-Garantien, Konfiguration, doctor, publish, raw accept, is_export/file_is_export), dazu test_config_shapes.py; die bestehenden Tests in test_export_guidelines.py bleiben grün.
  • Generiert: tools/CONTRACT.md, docs/reference/commands/… (guideline-export.md ist nach exports.md umgezogen), docs/reference/exit-codes.md, die Region der Sektion in docs/reference/configuration.md.
  • Steuerebene: raw/CONTRACT.md (Ablehnung von Exportdateien: Frontmatter-Marker und Digest), instructions/setup-instance.md, im Abschluss instructions/publish-cycle.md (Export-WARN/ERROR nach publish).
  • Docs:
    • neues How-to docs/how-to/export-to-a-synced-folder.md: Einrichtung von Hand, Prädikate, beide Handler, after_publish, was im Ziel nicht geändert werden darf, F1 (im Nachgang beantwortet, f2f251f),
    • Eintrag in docs/how-to/README.md, tools/README.md, Abschnitt „Export to a folder“ in docs/reference/configuration.md,
    • je ein Verweis zur Abgrenzung (Sicherung, Eingang, Export) in docs/how-to/back-up-to-a-synced-folder.md, collect-from-a-synced-folder.md und export-guidelines.md,
    • im Abschluss docs/explanation/architecture.md (Seitenexport als Weg für Leser, die nicht fragen können).
  • CHANGES.md über version bump.

Akzeptanzkriterien

  • Auswahl: Mit include: [["collection=concepts"], ["collection=entities", "!vertraulich"]] (Testkorpus mit einem Feld vertraulich) liegen im Ziel genau die Konzeptseiten und die Entity-Seiten ohne vertraulich, je als <pfad relativ zu kb/>.md.
  • Marker: Jede geschriebene .md-Datei beginnt mit ---, ihr Frontmatter ist gültiges YAML und enthält wikitool_export mit kind=pages, instance= und commit=. Mit frontmatter: minimal ist das der einzige Schlüssel, mit summary kommen genau title, type, summary, modified hinzu, soweit die Seite sie hat.
  • Erkennung: is_export/file_is_export sind wahr für (1) eine Datei mit der Kommentarzeile wie bisher, (2) eine Datei mit wikitool_export: im Frontmatter, auch wenn das übrige YAML kaputt ist. Sie sind falsch für (3) ein Frontmatter ohne den Schlüssel, (4) den Schlüssel erst nach der schließenden ----Zeile, (5) ein Frontmatter ohne schließendes --- innerhalb von 64 KB. Die bestehenden Tests zu GUIDELINES.md bleiben grün.
  • Rückweg Markdown: raw accept lehnt eine exportierte .md-Datei in incoming/ ab, und incoming/ und raw/ sind danach byte-gleich.
  • Digest-Sperre: Eine Datei in incoming/, deren sha256 im Manifest eines konfigurierten Ziels steht (etwa die DOCX aus command), wird von raw accept abgelehnt, und incoming/ und raw/ sind danach byte-gleich. Mit fehlendem Manifest oder fehlerhafter export-Sektion gibt es eine WARN-Zeile, und eine Datei ohne Treffer wird normal angenommen.
  • Rendern: Ein Wikilink auf eine exportierte Seite ist ein relativer Link, der im Ziel auf eine existierende Datei zeigt. Einer auf eine nicht exportierte Seite ist Klartext. Kein [[, kein [^cite- ohne Fußnotendefinition, und Code-Blöcke sind byte-gleich mit der Quelle.
  • Fremdes unberührt: Eine Datei im Ziel, die nicht im Manifest steht, ist nach dem Lauf byte-gleich.
  • Geänderte Datei: Ist eine Manifest-Datei im Ziel verändert, endet das Ziel mit Fehler und nennt sie, und keine Datei im Ziel hat sich geändert (Digest-Vergleich vor/nach).
  • Gelöschte Datei: Fehlt eine Manifest-Datei im Ziel, ist sie nach dem Lauf wieder da, und das Ziel ist nicht gescheitert.
  • Fremder Ordner: Ein nicht leerer Zielordner ohne Manifest wird abgelehnt und bleibt byte-gleich.
  • Leere Auswahl: Passt keine Seite, endet das Ziel mit Fehler, und alle Dateien des vorigen Laufs sind noch da.
  • Entfernen: Fällt eine Seite aus der Auswahl, ist ihre Datei nach dem Lauf weg, und alle anderen Dateien sind byte-gleich.
  • Neu gemeldet: Kommt eine Seite neu in die Auswahl, nennt die Ausgabe ihren Titel als „neu“, und der Lauf endet ohne Rückfrage mit Exit 0.
  • Idempotent: Ein zweiter Lauf ohne Änderung ändert keine mtime im Ziel, auch nicht die des Manifests.
  • Unsauber: Mit uncommitteter Änderung unter kb/ endet der Lauf mit Exit 1, und das Ziel ist unverändert.
  • command: Exit 0 übernimmt die Ausgabedateien und trägt sie mit Digest ins Manifest ein. Exit ≠ 0 und Timeout lassen das Ziel byte-gleich und melden stderr. Das Staging ist danach in jedem Fall weg.
  • after_publish: Nach einem publish, das committet hat (auch mit --no-push), ist ein Ziel mit after_publish: true auf dem neuen Stand und eins ohne unverändert. Scheitert das Ziel, endet publish mit Exit 0 und einer WARN-Zeile, und der Commit existiert. Mit fehlerhafter Sektion und einem after_publish-Ziel endet publish mit Exit 1, ohne Commit. Mit fehlerhafter Sektion ohne ein solches Ziel läuft publish wie bisher. Im Nachgang auch von Hand auf Windows bestätigt.
  • Konfiguration: Ein relativer path, ein path in der Instanz, in einem intake-Ordner oder im backup.target, zwei überlappende Ziele, ein leeres include, eine leere Auswahl, ein syntaktisch falsches Prädikat, ein unbekannter frontmatter- oder links-Wert, ein Feld, das nicht zum Handler passt, und ein command-Verweis ohne Eintrag sind je ein Fehler der Sektion. config set export.… endet mit Exit 42.
  • Mehrere Ziele: Scheitert eins, laufen die anderen, Exit 1. Ein unbekannter Zielname ergibt Exit 1 ohne Wirkung.
  • Doku wie unter „Betroffene Dateien“. Lokal sind pytest (3063 bestanden, 4 übersprungen), docs verify und instructions verify grün, und der CI-Lauf auf dem veröffentlichten Commit ist grün (Lauf 683; Abschluss-Nachzug Lauf 686; Nachgang F1 f2f251f, Lauf 691).

Kette

Kette „OneDrive/Microsoft 365“

Abgespalten aus der Diskussion zu #186 (2026-10-12). Stand: **Erledigt (2026-10-13).** Gebaut und veröffentlicht als 10.0.0-beta.13 (Commit `99945c4`, CI-Lauf 683 grün). Abschluss-Nachzug als 10.0.0-beta.14 (Commit `d92800f`, CI-Lauf 686 grün): `instructions/publish-cycle.md` sagt, was eine `WARN` oder ein `ERROR` des Exports nach `publish` bedeutet, und `docs/explanation/architecture.md` nennt den Seitenexport. Ausgeliefert mit Release 10.0.0. **F1 ist durch die Prüfung von Hand am 2026-10-13 beantwortet** (siehe „Nachgang“), das How-to ist nachgezogen (Commit `f2f251f`). Aus derselben Prüfung sind drei Folgepakete entstanden: #216, #217, #218. ## Anlass In der entstehenden beruflichen Instanz sollen Microsoft-365-Werkzeuge die KB lesen können. Laut einer Recherche des Betreibers (nicht belegt, keine Primärquelle im Repo) zieht Copilot Autopilot, vormals „Microsoft Scout“, seinen Kontext über Work IQ aus Inhalten in Microsoft 365, also auch aus OneDrive/SharePoint. Eine öffentliche API oder MCP-Schnittstelle ist für Autopilot nicht dokumentiert. Für solche Abnehmer ist ein **Export in M365-Speicher** der realistische Weg. Der Live-Weg über unseren MCP-Server ist ein eigenes Paket. Ein Export ist außerdem der Ansatzpunkt für Konverter in andere Formate. ## Befunde aus dem Baum (Stand vor dem Paket) - **`export guidelines` war schon ein Export mit fast allen Bausteinen** (`tools/chemenu/guideline_export.py`, `tools/chemenu/commands/export_cmd.py`): - **Auswahl** über `search`s eigene Prädikate (`search.filters`, „One filter semantics“), mit dem virtuellen Feld `collection` sowie `kind`, `subtype`, `title` und jedem Frontmatter-Feld. Formen: `f=v`, `f~v`, `f>=v`, `f<=v`, `f>v`, `f<v`, `f:*` (vorhanden), `!f` (fehlt). Ein „ungleich“ gibt es nicht. `select()` lehnt ab: kein Prädikat, unbekanntes Feld, unlesbares Frontmatter irgendwo unter `kb/`, null Treffer. - **Sauberkeit:** `check_clean()` lehnt ein `kb/` oder `types/` mit uncommitteten Änderungen ab. `last_commit()` nennt den letzten Commit, der eine ausgewählte Seite berührt hat, nicht `HEAD`. Die Ausgabe ist deterministisch. - **Rendern:** `render_page()` entfernt den generierten Links-Block und den Fußnotenblock, löst Wikilinks in ihren Anzeigetext auf und entfernt `[^cite-id]`. Code bleibt dabei byte-gleich (`strip_code_spans`). - **Rückweg-Sperre:** Jede Datei, deren erste Zeile mit `<!-- wikitool:export` begann (`EXPORT_MARKER`, `is_export`), wurde von `raw capture` ausgelassen und von `raw accept` abgelehnt (`commands/raw/_common.py`, `_check_no_export`). `file_is_export` las vor dem Paket nur die ersten rund 23 Bytes; seit 10.0.0-beta.13 erkennt es beide Marker-Formen (Entscheidung 6). - **Ein Vertraulichkeitsfeld gibt es nicht.** Weder `kb/CONVENTIONS.md` noch ein Type-Spec deklariert eins. `!vertraulich` in den Beispielen unten ist ein angenommenes Feld, das eine Instanz selbst einführen müsste. Der Export braucht keins, er nimmt jedes Feld, das `search` kennt. - **Konfigurationsmuster:** `config_shapes.INTAKE` (#213) und `BACKUP` (#186) waren die Vorbilder: benannte Einträge mit `handler`, `commands` als `names_command`, Pfade über `absolute_path_problem` plus „außerhalb der Instanz“ (`upload.py`). Programme laufen nach `fetch_routes.run_fetcher`: ohne Shell, stdin geschlossen, Timeout, Ende von stderr bei Fehler. - **Nach `publish` lief schon ein Folgeschritt:** die Maschinensicherung (`git_publish.py` § Machine-data backup). Scheitert sie, ist das ein `WARN`, der Commit bleibt stehen, und `backup run` wiederholt sie. Eine fehlerhafte `backup`-Sektion stoppt `publish` vor dem Commit. Vorbild für Entscheidung 12. - **Eine ältere Version liest eine unbekannte Settings-Sektion nicht und scheitert nicht daran** (`settings.unknown`, `doctor`: „not read by this version“). Wichtig für den Versionsteil. ## Entscheidungen 1. **Der Arbeitsbaum liegt nie in einem Sync-Ordner** (#186). Ein Export ist eine **abgeleitete, schreibgeschützte Fassung**, die bei jedem Lauf neu erzeugt wird, nie eine Arbeitskopie. Was jemand im Ziel ändert, fließt nicht zurück. 2. **Ziele werden einzeln konfiguriert**, jedes mit eigenen Parametern und einem Handler. Weitere Zielarten kommen über die `handler`-Liste hinzu, ohne Umbau. 3. **Auswahl nur per Positivliste.** Ohne ausdrückliche Auswahl wird nichts exportiert. Was die KB in einen Speicher verlässt, den Dritte und deren Werkzeuge lesen, muss benannt sein, nicht nur nicht ausgeschlossen. 4. **Wohin exportiert wird, entscheidet der Betreiber.** Jedes Feld der Zieldefinition ist `checkout, gate` bzw. `names a command`, `config set` lehnt es mit Exit 42 ab. Ein Agent kann keinen neuen Abfluss einrichten. **Einen Lauf auslösen darf der Agent** (`export run`, und mittelbar über `publish`, Entscheidung 12), denn er schreibt nur, was der Betreiber freigegeben hat. Das entspricht `upload collect`. 5. **Auswahl über `search`-Prädikate (war F3).** Kein eigenes Schema aus Collection/Typ/Tag, sondern dieselbe Prädikat-Syntax wie `search --field` und `export guidelines`. Eine zweite Filtersemantik wäre eine zweite Stelle, an der „welche Seiten“ anders verstanden wird. Feld-Bedingungen sind damit ohne Zusatzarbeit möglich, etwa `!vertraulich` für eine Instanz, die ein solches Feld einführt. Form: `include` ist eine Liste von Auswahlen, jede Auswahl eine Liste von Prädikaten. Innerhalb einer Auswahl müssen alle gelten (UND), exportiert wird, was auf mindestens eine Auswahl passt (ODER). 6. **Der Export-Marker steht im YAML-Frontmatter (war F8, Betreiber 2026-10-12).** Jede exportierte Markdown-Datei beginnt mit einem YAML-Frontmatter, dessen erster Schlüssel `wikitool_export: "kind=pages instance=<…> commit=<…>"` ist (`instance` wie `guideline_export.instance_id()`, `commit` nach Entscheidung 7). Die Datei behält damit echtes YAML, das pandoc und andere Werkzeuge als Metadaten lesen. - **`is_export`/`file_is_export` erkennen beide Formen.** (1) Wie bisher: Die erste Zeile beginnt nach optionalem BOM mit `<!-- wikitool:export`. (2) Neu: Die erste Zeile ist `---`, und bis zur schließenden `---`-Zeile steht eine Zeile, die mit `wikitool_export:` beginnt. Die Suche ist ein Zeilenscan, kein YAML-Parse, damit auch kaputtes YAML erkannt wird. `file_is_export` liest dafür höchstens 64 KB. Ein Frontmatter ohne schließendes `---` in diesem Bereich gilt als kein Export. - `raw capture` und `raw accept` übernehmen beide Formen ohne eigene Änderung, weil sie `is_export`/`file_is_export` aufrufen. `export guidelines` schreibt weiter die Kommentarzeile. - **Entschieden beim Bau:** Was ein Export ist, steht allein in `guideline_export.py` (beide Marker-Konstanten, `is_export`, `file_is_export`). `page_export.py` importiert von dort, ein drittes Modul gibt es nicht. 7. **Ein Lauf bildet einen Commit ab, keinen Arbeitsstand.** `check_clean()` gilt auch hier, `commit=` ist `last_commit()` der ausgewählten Seiten. 8. **Dateiablage im Ziel:** Jede Seite wird `<pfad relativ zu kb/>` mit Endung `.md`, also dieselbe Ordnerstruktur wie `kb/`. Keine generierten Index-Dateien. Ein Wikilink auf eine exportierte Seite wird ein relativer Markdown-Link (`links: relative`) oder Klartext (`links: text`). Ein Wikilink auf eine nicht exportierte Seite wird immer Klartext. 9. **Fußnoten-Zitate** `[^cite-id]` werden in lesbare Quellenangaben aufgelöst: Die Fußnote bleibt, ihr Text ist der Titel der Quellseite (als Link, wenn die Quellseite mit exportiert wird, sonst Klartext). Ein fremder Indexer kann mit der Roh-Syntax nichts anfangen. Der Titel einer nicht exportierten Quellseite verlässt damit die KB. Das gilt als Teil der exportierten Seite, so wie der Text eines Wikilinks. 10. **`export guidelines` bleibt, wie es ist (war F5).** Seine Ziele sind die erfassten Repositories, und es hat ein eigenes Gate. Geteilt wird nur Code. **Entschieden beim Bau:** `page_export.py` nutzt Auswahl, `check_clean`, `last_commit`, die Rendering-Bausteine und die Export-Erkennung direkt aus `guideline_export.py`. Die Spiegel-Helfer von `backup.py` (`write_atomic`, `file_digest`, `is_under`, `real_path`, `is_link`, `safe_relative`, `prune_empty`) wurden dafür öffentlich, statt sie zu kopieren. 11. **Keine Zielart ohne Dateisystem in diesem Paket (war F6).** Eine SharePoint-Bibliothek über Graph o. ä. leistet heute die OneDrive-Synchronisierung. Die `handler`-Liste bleibt der Erweiterungspunkt. 12. **Auslöser: von Hand und optional nach `publish` (war F2, Betreiber 2026-10-12).** `export run [<ziel>...]` immer. Dazu je Ziel `after_publish` (Default `false`): Nach jedem `publish`, das committet hat, läuft das Ziel, auch mit `--no-push`, denn der Export hängt am Commit, nicht am Push. Das funktioniert wie die Maschinensicherung: Ein Fehler ist eine `WARN`-Zeile mit Grund, der Commit bleibt stehen, Exit 0. Wiederholt wird mit `export run`, nicht mit einem weiteren `publish`. Eine fehlerhafte `export`-Sektion stoppt `publish` mit Exit 1, bevor etwas committet wird, aber nur, wenn ein Ziel `after_publish` setzt. Sonst betrifft sie `publish` nicht. 13. **Kein Gate bei wachsender Menge (war F4, Betreiber 2026-10-12).** Die Zieldefinition ist die Freigabe. Neue Seiten, auf die eine Auswahl passt, laufen ohne weiteres Gate mit. Die Ausgabe nennt sie je Ziel als „neu“. Wer etwas nicht im Ziel haben will, schließt es per Prädikat aus. `AGENTS.md` § Gates bleibt unverändert. 14. **Rückweg-Sperre auch über den Digest (war F7, Betreiber 2026-10-12).** Eine DOCX/PDF aus dem Handler `command` kann keinen Marker tragen. `raw accept` lehnt deshalb zusätzlich jede Datei ab, deren sha256 im Manifest eines konfigurierten Ziels steht, mit derselben Art Meldung wie `_check_no_export`, und verschiebt nichts. Ein Ziel, dessen Manifest fehlt oder unlesbar ist, ergibt eine `WARN`-Zeile, keinen Abbruch. Eine fehlerhafte `export`-Sektion ergibt ebenfalls eine `WARN` (die Prüfung entfällt dann), denn `raw accept` darf an einer Exporteinstellung nicht scheitern. Eine im Ziel bearbeitete und dann eingereichte Datei hat einen anderen Digest und geht durch. Sie gilt als neue Quelle eines Menschen. 15. **`doctor` kennt nur `OK` und `FAIL` für `export` (entschieden beim Bau).** Geplant war zusätzlich eine `WARN` bei fehlendem Zielordner. Ein fehlender Zielordner ist aber kein Fehler: `export run` legt ihn an (siehe „Was ein Lauf garantieren muss“), eine `WARN` dafür wäre bei jedem neu eingerichteten Ziel ein Fehlalarm. `doctor` meldet daher: Sektion fehlt → `OK` (absent), Sektion fehlerhaft → `FAIL`, sonst `OK` mit jedem Ziel, seinem Handler, seinem Ordner und ob es nach `publish` läuft. ## Konfiguration Sektion `export` in `.wikitool-local.json`, neu als `config_shapes.EXPORT` in `SETTINGS_SECTIONS`: ```json { "export": { "targets": { "m365": { "handler": "markdown", "path": "C:\\Users\\<user>\\OneDrive - <Firma>\\Chemenu-KB", "include": [["collection=entities"], ["collection=concepts", "!vertraulich"]], "frontmatter": "summary", "links": "relative", "after_publish": true }, "m365-docx": { "handler": "command", "command": "pandoc-docx", "path": "C:\\Users\\<user>\\OneDrive - <Firma>\\Chemenu-KB-docx", "include": [["collection=concepts"]] } }, "commands": { "pandoc-docx": ["pwsh", "-NoProfile", "-File", "C:\\tools\\md-to-docx.ps1"] } } } ``` | Feld | Tier | Pflicht | Bedeutung | |---|---|---|---| | `targets` | checkout, gate | ja | Zielname (`[a-z0-9-]+`) → Ziel. Der Name ist das Argument von `export run`. Mindestens ein Ziel. | | `targets.<n>.handler` | checkout, gate | ja | `markdown` (eingebaut) oder `command` (Konverter des Betreibers) | | `path` | checkout, gate | ja | Zielverzeichnis. Absolut, außerhalb der Instanz. Darf weder in einem anderen Ziel, einem `intake`-Ordner (`handler: folder`) noch im `backup.target` liegen oder einen davon enthalten. (Die Regel zum `backup.target` ist zu streng: #216.) | | `include` | checkout, gate | ja | Liste von Auswahlen (Entscheidung 5). Eine leere Liste oder eine leere Auswahl ist ein Fehler der Sektion. Die Prädikat-Syntax wird beim Lesen der Sektion geprüft (`parse_predicate`), die Feldnamen beim Lauf (`validate_fields`). Alle Seiten wählt `[["collection:*"]]` (#217). | | `frontmatter` | checkout, gate | nein | Was das YAML-Frontmatter außer `wikitool_export` trägt: `minimal` (nichts weiter), `summary` (`title`, `type`, `summary`, `modified`, soweit vorhanden) oder `full` (das ganze Frontmatter der Seite). Default `summary`. | | `links` | checkout, gate | nein | `relative` oder `text`. Default `relative`. | | `after_publish` | checkout, gate | nein | Boolean, Default `false`. Entscheidung 12. | | `command` | checkout, gate | ja bei `command` | Name eines Eintrags unter `commands` | | `commands.<n>` | checkout, names a command | nein | Programm und Argumente als Array | Felder, die nicht zum Handler passen, und ein `command`-Verweis ohne Eintrag unter `commands` sind Fehler der Sektion (wie bei `intake`). `frontmatter` und `links` gelten für beide Handler, denn `command` bekommt die Markdown-Fassung. Ohne Sektion: `export run` endet mit Exit 1 und nennt die Sektion, `doctor` meldet `OK` (absent). Bei einer fehlerhaften Sektion endet `export run` mit Exit 1 und `doctor` meldet `FAIL`. Für `publish` und `raw accept` gelten Entscheidung 12 und 14. **Handler `command`:** `wikitool` rendert erst wie `markdown` in ein Staging-Verzeichnis und ruft dann das Programm ohne Shell, mit geschlossenem stdin und 300 s Timeout auf. Die letzten zwei Argumente sind das Verzeichnis mit der Markdown-Fassung und ein leeres Ausgabeverzeichnis. Bei Exit 0 wird der Inhalt des Ausgabeverzeichnisses (rekursiv, nur reguläre Dateien, kein Symlink) das, was ins Ziel kommt. Bei jedem anderen Ende bleibt das Ziel unverändert, gemeldet wird das Ende von stderr (bis 2000 Zeichen). Das Staging wird in jedem Fall entfernt. Es liegt außerhalb jedes Ziels und wird nie committet. ## Was ein Lauf garantieren muss `export run [<ziel>...]`, ohne Argument alle Ziele. Jedes Ziel läuft unabhängig. Scheitert eins, laufen die anderen trotzdem, Exit 1, wenn mindestens eins gescheitert ist. **Der Lauf löscht und überschreibt nur, was er selbst geschrieben hat.** Im Ziel liegt ein Manifest `.wikitool-export.json` mit Zielname, Instanz, Commit und jeder geschriebenen Datei mit sha256. - Ein Lauf entfernt nur Dateien, die im Manifest stehen und nicht mehr exportiert werden. Dadurch leer gewordene Verzeichnisse entfernt er nur, wenn er sie selbst angelegt hat (im Manifest vermerkt). - Dateien, die nicht im Manifest stehen, bleiben byte-gleich. - Weicht eine Manifest-Datei im Ziel von ihrem Digest ab, weil jemand sie dort geändert hat, bricht das Ziel mit Fehler ab, **bevor** es etwas schreibt, und nennt die Datei. Fremde Änderungen werden nie still überschrieben. - Fehlt eine Manifest-Datei im Ziel (dort gelöscht), wird sie neu geschrieben, das Ziel scheitert nicht daran. - Fehlt das Manifest in einem nicht leeren Zielordner, wird abgelehnt. Es wird nie in einen fremden Ordner hineingespiegelt. Ein fehlender Zielordner wird angelegt. - **Null ausgewählte Seiten** sind ein Fehler des Ziels, bevor etwas gelöscht wird. Ein leerer Export würde sonst das ganze Ziel leeren, dieselbe Überlegung wie in `guideline_export.select()`. - **Unverändert heißt unberührt:** Eine Datei, deren Digest dem Manifest entspricht, wird nicht neu geschrieben, damit der Sync-Client nicht alles erneut hochlädt. Ein Lauf ohne Änderung an der Auswahl schreibt nichts, auch nicht das Manifest. Weil `commit=` im Marker steht, ändert sich eine Datei nur, wenn ein Commit die ausgewählten Seiten berührt hat. Dann ändern sich alle Dateien des Ziels. Das ist hinzunehmen, denn es ist derselbe Mechanismus wie bei `GUIDELINES.md`. - Geschrieben wird atomar je Datei (temporäre Datei im Ziel, dann `os.replace`), das Manifest zuletzt. - Ausgabe je Ziel: neu (mit Seitentiteln), geändert, entfernt, unverändert (Zahl), gescheitert mit Grund. ## Nachgang: F1 und die Prüfung von Hand (2026-10-13) **F1 – Format für Work IQ/Copilot: beantwortet.** Frage war, ob Copilot bzw. Work IQ `.md`-Dateien aus OneDrive/SharePoint indizieren oder ob es DOCX/HTML/PDF braucht. Prüfung von Hand durch den Betreiber auf der Arbeitsinstanz (Release 10.0.0, ein Tenant): Ein `markdown`-Ziel im OneDrive mit `after_publish: true` lief nach `publish` mit, und **Microsoft 365 Copilot fand die `.md`-Dateien und konnte sie nutzen.** Das How-to nennt das jetzt und empfiehlt weiter, mit einem kleinen `markdown`-Ziel zu beginnen; ein `command`-Ziel bleibt der Weg für einen Tenant, der die Dateien nicht aufnimmt (`docs/how-to/export-to-a-synced-folder.md`, Commit `f2f251f`, reine `docs/`-Prosa ohne Bump). Kein Code. **Weitere Befunde derselben Prüfung, je ein eigenes Paket:** - Ein Exportziel als Geschwisterordner im `backup.target` wird abgelehnt, ein `intake`-Ordner dort nicht – #216. - Eine Auswahl „ganze KB“ fehlt dem How-to; `[["collection:*"]]` funktioniert – #217. - Rohdokumente aus `raw/` sind nicht Teil des Exports (Entscheidung 1) und werden gewünscht – #218. ## Versionsteil **MINOR.** Drop-in-Test: - Vorwärts: neues Kommando `export run`, neue Sektion `export`, Default-Verhalten unverändert. Ohne Sektion ändert sich nichts, auch nicht an `publish` und `raw accept` (Entscheidungen 12 und 14 greifen nur mit konfigurierten Zielen). `is_export` erkennt zusätzlich Dateien mit dem Frontmatter-Schlüssel `wikitool_export`. Den Schlüssel schrieb vor diesem Paket nichts. - Rückwärts: Eine ältere Version liest die unbekannte Sektion `export` nicht und meldet sie in `doctor` als „not read by this version“. Die Manifeste liegen außerhalb der Instanz. Umgesetzt auf dem laufenden Kandidaten 10.0.0 als 10.0.0-beta.13 (`--minor`, überschreitet nichts neu), der Abschluss-Nachzug als 10.0.0-beta.14 (`--patch`). ## Betroffene Dateien und Flächen - **Code:** - `tools/chemenu/commands/export_cmd.py` (neu: `export run`, mit `cli_contract`-Eintrag), - neues Modul `tools/chemenu/page_export.py` für Konfiguration, Auswahl, Rendern und Spiegeln ohne CLI, - `tools/chemenu/guideline_export.py` (`is_export`, `file_is_export`, Entscheidung 6), - `tools/chemenu/backup.py` (Spiegel-Helfer öffentlich, Entscheidung 10), `tools/chemenu/repo_capture.py` (Aufrufer angepasst), - `tools/chemenu/config_shapes.py` (`EXPORT`, in `SETTINGS_SECTIONS`), - `tools/chemenu/commands/doctor.py` (Prüfung `export`, Entscheidung 15), - `tools/chemenu/commands/git_publish.py` (Entscheidung 12, neben dem Sicherungsschritt; der `cli_contract`-Eintrag von `publish` nennt den Schritt und seine `WARN`), - `tools/chemenu/commands/raw/_common.py` (Entscheidung 14, neben `_check_no_export`; die `cli_contract`-Einträge von `raw accept` und `raw capture` nennen beide Marker-Formen und den Digest), - `tools/chemenu/cli_contract.py` (Gruppe „Exports“), `tools/chemenu/reference_doc.py` (Region der Sektion). - **Tests:** neu `tools/chemenu/tests/test_export_run.py` (65 Tests: Lauf-Garantien, Konfiguration, `doctor`, `publish`, `raw accept`, `is_export`/`file_is_export`), dazu `test_config_shapes.py`; die bestehenden Tests in `test_export_guidelines.py` bleiben grün. - **Generiert:** `tools/CONTRACT.md`, `docs/reference/commands/…` (`guideline-export.md` ist nach `exports.md` umgezogen), `docs/reference/exit-codes.md`, die Region der Sektion in `docs/reference/configuration.md`. - **Steuerebene:** `raw/CONTRACT.md` (Ablehnung von Exportdateien: Frontmatter-Marker und Digest), `instructions/setup-instance.md`, im Abschluss `instructions/publish-cycle.md` (Export-`WARN`/`ERROR` nach `publish`). - **Docs:** - neues How-to `docs/how-to/export-to-a-synced-folder.md`: Einrichtung von Hand, Prädikate, beide Handler, `after_publish`, was im Ziel nicht geändert werden darf, F1 (im Nachgang beantwortet, `f2f251f`), - Eintrag in `docs/how-to/README.md`, `tools/README.md`, Abschnitt „Export to a folder“ in `docs/reference/configuration.md`, - je ein Verweis zur Abgrenzung (Sicherung, Eingang, Export) in `docs/how-to/back-up-to-a-synced-folder.md`, `collect-from-a-synced-folder.md` und `export-guidelines.md`, - im Abschluss `docs/explanation/architecture.md` (Seitenexport als Weg für Leser, die nicht fragen können). - `CHANGES.md` über `version bump`. ## Akzeptanzkriterien - [x] **Auswahl:** Mit `include: [["collection=concepts"], ["collection=entities", "!vertraulich"]]` (Testkorpus mit einem Feld `vertraulich`) liegen im Ziel genau die Konzeptseiten und die Entity-Seiten ohne `vertraulich`, je als `<pfad relativ zu kb/>.md`. - [x] **Marker:** Jede geschriebene `.md`-Datei beginnt mit `---`, ihr Frontmatter ist gültiges YAML und enthält `wikitool_export` mit `kind=pages`, `instance=` und `commit=`. Mit `frontmatter: minimal` ist das der einzige Schlüssel, mit `summary` kommen genau `title`, `type`, `summary`, `modified` hinzu, soweit die Seite sie hat. - [x] **Erkennung:** `is_export`/`file_is_export` sind wahr für (1) eine Datei mit der Kommentarzeile wie bisher, (2) eine Datei mit `wikitool_export:` im Frontmatter, auch wenn das übrige YAML kaputt ist. Sie sind falsch für (3) ein Frontmatter ohne den Schlüssel, (4) den Schlüssel erst nach der schließenden `---`-Zeile, (5) ein Frontmatter ohne schließendes `---` innerhalb von 64 KB. Die bestehenden Tests zu `GUIDELINES.md` bleiben grün. - [x] **Rückweg Markdown:** `raw accept` lehnt eine exportierte `.md`-Datei in `incoming/` ab, und `incoming/` und `raw/` sind danach byte-gleich. - [x] **Digest-Sperre:** Eine Datei in `incoming/`, deren sha256 im Manifest eines konfigurierten Ziels steht (etwa die DOCX aus `command`), wird von `raw accept` abgelehnt, und `incoming/` und `raw/` sind danach byte-gleich. Mit fehlendem Manifest oder fehlerhafter `export`-Sektion gibt es eine `WARN`-Zeile, und eine Datei ohne Treffer wird normal angenommen. - [x] **Rendern:** Ein Wikilink auf eine exportierte Seite ist ein relativer Link, der im Ziel auf eine existierende Datei zeigt. Einer auf eine nicht exportierte Seite ist Klartext. Kein `[[`, kein `[^cite-` ohne Fußnotendefinition, und Code-Blöcke sind byte-gleich mit der Quelle. - [x] **Fremdes unberührt:** Eine Datei im Ziel, die nicht im Manifest steht, ist nach dem Lauf byte-gleich. - [x] **Geänderte Datei:** Ist eine Manifest-Datei im Ziel verändert, endet das Ziel mit Fehler und nennt sie, und **keine** Datei im Ziel hat sich geändert (Digest-Vergleich vor/nach). - [x] **Gelöschte Datei:** Fehlt eine Manifest-Datei im Ziel, ist sie nach dem Lauf wieder da, und das Ziel ist nicht gescheitert. - [x] **Fremder Ordner:** Ein nicht leerer Zielordner ohne Manifest wird abgelehnt und bleibt byte-gleich. - [x] **Leere Auswahl:** Passt keine Seite, endet das Ziel mit Fehler, und alle Dateien des vorigen Laufs sind noch da. - [x] **Entfernen:** Fällt eine Seite aus der Auswahl, ist ihre Datei nach dem Lauf weg, und alle anderen Dateien sind byte-gleich. - [x] **Neu gemeldet:** Kommt eine Seite neu in die Auswahl, nennt die Ausgabe ihren Titel als „neu“, und der Lauf endet ohne Rückfrage mit Exit 0. - [x] **Idempotent:** Ein zweiter Lauf ohne Änderung ändert keine mtime im Ziel, auch nicht die des Manifests. - [x] **Unsauber:** Mit uncommitteter Änderung unter `kb/` endet der Lauf mit Exit 1, und das Ziel ist unverändert. - [x] **`command`:** Exit 0 übernimmt die Ausgabedateien und trägt sie mit Digest ins Manifest ein. Exit ≠ 0 und Timeout lassen das Ziel byte-gleich und melden stderr. Das Staging ist danach in jedem Fall weg. - [x] **`after_publish`:** Nach einem `publish`, das committet hat (auch mit `--no-push`), ist ein Ziel mit `after_publish: true` auf dem neuen Stand und eins ohne unverändert. Scheitert das Ziel, endet `publish` mit Exit 0 und einer `WARN`-Zeile, und der Commit existiert. Mit fehlerhafter Sektion und einem `after_publish`-Ziel endet `publish` mit Exit 1, ohne Commit. Mit fehlerhafter Sektion ohne ein solches Ziel läuft `publish` wie bisher. Im Nachgang auch von Hand auf Windows bestätigt. - [x] **Konfiguration:** Ein relativer `path`, ein `path` in der Instanz, in einem `intake`-Ordner oder im `backup.target`, zwei überlappende Ziele, ein leeres `include`, eine leere Auswahl, ein syntaktisch falsches Prädikat, ein unbekannter `frontmatter`- oder `links`-Wert, ein Feld, das nicht zum Handler passt, und ein `command`-Verweis ohne Eintrag sind je ein Fehler der Sektion. `config set export.…` endet mit Exit 42. - [x] **Mehrere Ziele:** Scheitert eins, laufen die anderen, Exit 1. Ein unbekannter Zielname ergibt Exit 1 ohne Wirkung. - [x] **Doku** wie unter „Betroffene Dateien“. Lokal sind `pytest` (3063 bestanden, 4 übersprungen), `docs verify` und `instructions verify` grün, und der CI-Lauf auf dem veröffentlichten Commit ist grün (Lauf 683; Abschluss-Nachzug Lauf 686; Nachgang F1 `f2f251f`, Lauf 691). ## Kette Kette „OneDrive/Microsoft 365“ - Vorgänger: #213 - Nachfolger: keiner - letztes Glied
torben added the prio/plannedsize/Larea/kbkind/decision labels 2026-10-12 16:29:09 +00:00
Author
Owner

Changelog: Body gegen den Baum geprüft. Neu: § Befunde aus dem Baum (export guidelines als Vorbild, Export-Marker und raw accept, Folgeschritt nach publish, unbekannte Sektion bei Downgrade). Entschieden: F3 → search-Prädikate statt Collection/Typ/Tag (E5), F5 → export guidelines bleibt getrennt (E10), F6 → keine Zielart ohne Dateisystem (E11). Neu entschieden: Marker in jeder .md-Datei, deshalb Metadaten nicht als YAML (E6), check_clean (E7), Dateiablage wie kb/ (E8). Garantien ergänzt: leere Auswahl ist Fehler, unverändert heißt unberührt, gelöschte Dateien werden neu geschrieben, Pfad darf kein anderes Ziel, keinen intake-Ordner und kein backup.target überlappen. Manifest heißt .wikitool-export.json. F1 ist jetzt nicht blockierend. Neue offene Frage F7 (Rückweg für DOCX/PDF ohne Marker). Versionsteil MINOR begründet. Akzeptanzkriterien als Entwurf. Offen: F2, F4, F7.

**Changelog:** Body gegen den Baum geprüft. Neu: § Befunde aus dem Baum (`export guidelines` als Vorbild, Export-Marker und `raw accept`, Folgeschritt nach `publish`, unbekannte Sektion bei Downgrade). Entschieden: F3 → `search`-Prädikate statt Collection/Typ/Tag (E5), F5 → `export guidelines` bleibt getrennt (E10), F6 → keine Zielart ohne Dateisystem (E11). Neu entschieden: Marker in jeder `.md`-Datei, deshalb Metadaten nicht als YAML (E6), `check_clean` (E7), Dateiablage wie `kb/` (E8). Garantien ergänzt: leere Auswahl ist Fehler, unverändert heißt unberührt, gelöschte Dateien werden neu geschrieben, Pfad darf kein anderes Ziel, keinen `intake`-Ordner und kein `backup.target` überlappen. Manifest heißt `.wikitool-export.json`. F1 ist jetzt nicht blockierend. Neue offene Frage F7 (Rückweg für DOCX/PDF ohne Marker). Versionsteil MINOR begründet. Akzeptanzkriterien als Entwurf. Offen: F2, F4, F7.
torben added kind/build and removed kind/decision labels 2026-10-12 21:25:00 +00:00
Author
Owner

Changelog: Betreiberentscheidungen vom 2026-10-12 eingetragen. F2 → E12 (after_publish je Ziel, wie die Maschinensicherung), F4 → E13 (kein Gate, neue Seiten werden gemeldet), F7 → E14 (raw accept lehnt Digests aus den Manifesten der Ziele ab), F8 → E6 (Marker als wikitool_export im YAML-Frontmatter, is_export erkennt beide Formen; frontmatter-Werte jetzt minimal|summary|full). Klargestellt: !vertraulich ist ein angenommenes Feld, keins des Schemas. Die Akzeptanzkriterien sind festgezogen, neu sind Erkennung, gelöschte Datei und after_publish. Re-Label: kind/decision → kind/build, weil keine blockierende Frage mehr offen ist. F1 bleibt offen, blockiert aber nicht. size/L bleibt: neues Kommando plus Eingriffe in publish, raw accept und is_export, mit eigenem How-to.

**Changelog:** Betreiberentscheidungen vom 2026-10-12 eingetragen. F2 → E12 (`after_publish` je Ziel, wie die Maschinensicherung), F4 → E13 (kein Gate, neue Seiten werden gemeldet), F7 → E14 (`raw accept` lehnt Digests aus den Manifesten der Ziele ab), F8 → E6 (Marker als `wikitool_export` im YAML-Frontmatter, `is_export` erkennt beide Formen; `frontmatter`-Werte jetzt `minimal|summary|full`). Klargestellt: `!vertraulich` ist ein angenommenes Feld, keins des Schemas. Die Akzeptanzkriterien sind festgezogen, neu sind Erkennung, gelöschte Datei und `after_publish`. **Re-Label:** `kind/decision` → `kind/build`, weil keine blockierende Frage mehr offen ist. F1 bleibt offen, blockiert aber nicht. `size/L` bleibt: neues Kommando plus Eingriffe in `publish`, `raw accept` und `is_export`, mit eigenem How-to.
Author
Owner

Changelog: export run schreibt ausgewählte kb/-Seiten als abgeleitete, schreibgeschützte Fassung in konfigurierte Zielordner (Handler markdown und command, Manifest, after_publish). raw accept lehnt zusätzlich Dateien ab, deren sha256 im Manifest eines Ziels steht, und doctor prüft die neue export-Sektion. Veröffentlicht als 10.0.0-beta.13 (99945c4), CI-Lauf 683 grün. Neues How-to docs/how-to/export-to-a-synced-folder.md; F1 (Indexierung von .md im Tenant) bleibt offen.

**Changelog:** `export run` schreibt ausgewählte `kb/`-Seiten als abgeleitete, schreibgeschützte Fassung in konfigurierte Zielordner (Handler `markdown` und `command`, Manifest, `after_publish`). `raw accept` lehnt zusätzlich Dateien ab, deren sha256 im Manifest eines Ziels steht, und `doctor` prüft die neue `export`-Sektion. Veröffentlicht als 10.0.0-beta.13 (`99945c4`), CI-Lauf 683 grün. Neues How-to `docs/how-to/export-to-a-synced-folder.md`; F1 (Indexierung von `.md` im Tenant) bleibt offen.
Author
Owner

Changelog: Abschluss: instructions/publish-cycle.md sagt jetzt, was eine WARN des Exports nach publish bedeutet (Commit steht, nicht erneut publizieren, export run wiederholt) und was ein ERROR zur export-Sektion bedeutet (nichts veröffentlicht). docs/explanation/architecture.md nennt den Seitenexport neben dem MCP-Server. Veröffentlicht als 10.0.0-beta.14 (d92800f), CI-Lauf 686 grün. Der Body hält die beim Bau getroffenen Entscheidungen fest (Marker-Definition allein in guideline_export.py, öffentliche Spiegel-Helfer aus backup.py, doctor nur OK/FAIL, Entscheidung 15).

**Changelog:** Abschluss: `instructions/publish-cycle.md` sagt jetzt, was eine `WARN` des Exports nach `publish` bedeutet (Commit steht, nicht erneut publizieren, `export run` wiederholt) und was ein `ERROR` zur `export`-Sektion bedeutet (nichts veröffentlicht). `docs/explanation/architecture.md` nennt den Seitenexport neben dem MCP-Server. Veröffentlicht als 10.0.0-beta.14 (`d92800f`), CI-Lauf 686 grün. Der Body hält die beim Bau getroffenen Entscheidungen fest (Marker-Definition allein in `guideline_export.py`, öffentliche Spiegel-Helfer aus `backup.py`, `doctor` nur `OK`/`FAIL`, Entscheidung 15).
Author
Owner

Changelog: F1 ist beantwortet: Microsoft 365 Copilot hat die .md-Dateien eines markdown-Ziels gefunden und genutzt (Prüfung von Hand am 2026-10-13, ein Tenant, Release 10.0.0). „Offene Fragen“ heißt jetzt „Nachgang“. Das How-to ist in f2f251f nachgezogen, reine docs/-Prosa, CI-Lauf 691. after_publish ist auch von Hand bestätigt. Die Folgepakete #216 (Überschneidung mit backup.target), #217 (Auswahl der ganzen KB) und #218 (Rohdokumente im Export) sind im Nachgang, in der Konfigurationstabelle und unter include verlinkt.

**Changelog:** F1 ist beantwortet: Microsoft 365 Copilot hat die `.md`-Dateien eines `markdown`-Ziels gefunden und genutzt (Prüfung von Hand am 2026-10-13, ein Tenant, Release 10.0.0). „Offene Fragen“ heißt jetzt „Nachgang“. Das How-to ist in `f2f251f` nachgezogen, reine `docs/`-Prosa, CI-Lauf 691. `after_publish` ist auch von Hand bestätigt. Die Folgepakete #216 (Überschneidung mit `backup.target`), #217 (Auswahl der ganzen KB) und #218 (Rohdokumente im Export) sind im Nachgang, in der Konfigurationstabelle und unter `include` verlinkt.
Author
Owner

Folgepaket aus der Prüfung von Hand: #221. Der Satz in „Was ein Lauf garantieren muss“, nach dem sich bei jedem Commit an einer ausgewählten Seite alle Dateien des Ziels ändern, hält in der Praxis nicht. Mit einer Auswahl der ganzen KB und after_publish lädt OneDrive nach jedem Ingest die ganze KB neu hoch. Vorschlag in #221: commit= je Seite.

Folgepaket aus der Prüfung von Hand: #221. Der Satz in „Was ein Lauf garantieren muss“, nach dem sich bei jedem Commit an einer ausgewählten Seite alle Dateien des Ziels ändern, hält in der Praxis nicht. Mit einer Auswahl der ganzen KB und `after_publish` lädt OneDrive nach jedem Ingest die ganze KB neu hoch. Vorschlag in #221: `commit=` je Seite.
Author
Owner

Nachtrag (2026-10-15): Entscheidung 7 und der Punkt „Unverändert heißt unberührt“ gelten so nicht mehr. Seit 10.1.0-beta.12 (52078bb, #221) trägt der Marker jeder Datei commit= ihrer eigenen Seite, nicht mehr den Commit des ganzen Ziels. Ein Commit schreibt damit nur die Dateien der Seiten neu, die er berührt hat, dazu Dateien, deren Links sich ändern. Das Manifest führt weiter den neuesten Commit über alle ausgewählten Seiten; export guidelines ist unverändert. Bei einer Auswahl der ganzen KB (#217) hatte der alte Mechanismus nach jedem Ingest alle Dateien neu hochgeladen.

**Nachtrag (2026-10-15):** Entscheidung 7 und der Punkt „Unverändert heißt unberührt“ gelten so nicht mehr. Seit 10.1.0-beta.12 (`52078bb`, #221) trägt der Marker jeder Datei `commit=` **ihrer eigenen Seite**, nicht mehr den Commit des ganzen Ziels. Ein Commit schreibt damit nur die Dateien der Seiten neu, die er berührt hat, dazu Dateien, deren Links sich ändern. Das Manifest führt weiter den neuesten Commit über alle ausgewählten Seiten; `export guidelines` ist unverändert. Bei einer Auswahl der ganzen KB (#217) hatte der alte Mechanismus nach jedem Ingest alle Dateien neu hochgeladen.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#214