Ein fokussiertes Hardening-Release.
Keine neuen Funktionen — nur Korrekturen für die seit 0.8.7 gemeldeten Ecken und Kanten.
Jede Änderung hier ist entweder eine von Benutzern gefundene Regression, ein CI-Workflow-Fehler oder ein Zuverlässigkeitsproblem zur Build-Zeit.

🐛 Fehlerbehebungen

Docker: Volume-Mount-Berechtigungen korrigiert

docmd init und andere dateischreibende Befehle schlugen mit “Permission denied” fehl, wenn /docs ein host-gemountetes Volume war, das einem anderen uid als dem im Container integrierten docmd-Benutzer (uid 1001) gehörte.
Das Image liefert jetzt su-exec mit, und der Entrypoint erkennt die uid:gid des gemounteten Verzeichnisses und führt sich vor jedem Befehl als diese Identität neu aus — dasselbe Muster, das die offiziellen postgres- und redis-Images verwenden.

# All dies funktioniert auf einem Host-Volume ohne -u-Flags
docker run -v $(pwd):/docs ghcr.io/docmd-io/docmd:latest init
docker run -v $(pwd):/docs -p 3000:3000 ghcr.io/docmd-io/docmd:latest

Der cp -a (Archiv)-Modus, der Warnungen zu “Operation not permitted” bei der Eigentumsübertragung auslöste, ist verschwunden.
Der -u $(id -u):$(id -g)-Workaround ist nicht mehr erforderlich und wurde aus allen READMEs und dem Docker-Deployment-Leitfaden entfernt.

Docker: init blockiert in CI nicht mehr

Die interaktive Eingabeaufforderung “Do you want to override these files?” hing in Nicht-TTY-Umgebungen (CI, docker run ohne -it, weitergeleitete Eingabe).
Die CLI erkennt jetzt den nicht-interaktiven Modus und überspringt die Eingabeaufforderung automatisch — bestehende Dateien werden standardmäßig beibehalten (sicher), oder -y/--yes akzeptiert, um das Überschreiben zu wählen.
Eine neue --force-Flag funktioniert ebenfalls in jeder Umgebung für explizites Überschreiben.

# Überschreiben in CI / Docker / weitergeleiteter Eingabe erzwingen
docker run --rm -v $(pwd):/docs ghcr.io/docmd:latest init --force

# Oder -y verwenden
docker run --rm -v $(pwd):/docs ghcr.io/docmd:latest init -y

docmd add über npx: falsches “npm not found” unter Windows

npx @docmd/core add <plugin> gab unter Windows PowerShell “The package manager ‘npm’ was not found on your system PATH” aus, obwohl npm --version in derselben Shell einwandfrei funktionierte.
Ursache: Wenn @docmd/core über npx gestartet wird, erbt der gestartete Kindprozess unter Windows einen beschnittenen PATH.
Der Installer löst npm/pnpm/yarn/bun jetzt relativ zu process.execPath (dem eigenen Pfad von Node) auf, was unabhängig davon funktioniert, wie @docmd/core gestartet wurde.

Die Fehlermeldung erkennt jetzt auch den spezifischen Fall (Node erreichbar + Binärdatei fehlt) und schlägt vor, das Plugin direkt über den Paketmanager des Hosts zu installieren.

Template-Registry-Lookup für @docmd/template-*

Das Setzen von theme.template auf ein offizielles Template wie summer oder @docmd/template-summer gab aus:

⬢ Plugin "template-summer" not found in official registry - manual installation required

Ursache: Der Auto-Install-Registry-Lookup entfernte @docmd/plugin- aus dem Paketnamen, um den Registry-Schlüssel abzuleiten, entfernte aber nicht @docmd/template-.
Daher wurde @docmd/template-summer zum Schlüssel template-summer statt summer aufgelöst, der in der Registry nicht existiert.
Beide Präfixe werden jetzt korrekt entfernt.
Benutzerdefinierte/Drittanbieter-Templates sind nicht betroffen.

Behandlung von Peer-Abhängigkeiten bei semantischer Suche

Wenn plugins.search.semantic = true gesetzt war und die ML-Peer-Abhängigkeiten (@huggingface/transformers, onnxruntime-node) nicht vorhanden waren, fiel der Build stillschweigend auf Stichwortsuche zurück, mit einem verwirrenden Fehler tief in docmd-search:

⬢ Semantic indexing failed: Embedding failed: Failed to load model "Xenova/all-MiniLM-L6-v2": Cannot find package '@huggingface/transformers'

Zwei Verbesserungen:

  1. Klare Vorabprüfung — das Plugin überprüft jetzt, ob die Peer-Abhängigkeiten nach der automatischen Installation auflösbar sind, und gibt eine umsetzbare Fehlermeldung aus, die das fehlende Paket benennt, anstatt den Fehler aus docmd-search heraus durchschlagen zu lassen.
  2. Korrektur des veralteten Modul-Caches — nach der automatischen Installation von docmd-search während des Builds konnte der Modul-Cache des aktuellen Node-Prozesses die neuen Pakete nicht sehen.
    Die Indizierung läuft jetzt in einem Kindprozess, der mit einer sauberen Modulauflösung startet.
    npm install Auto-Installation übergibt außerdem --foreground-scripts, damit das onnxruntime-node Postinstall (native Binärdatei-Download) tatsächlich in CI-Umgebungen mit aktivierter npm-Sicherheitsfunktion allow-scripts ausgeführt wird.

npm-publish: packages/templates/* jetzt im Veröffentlichungs-Loop

Die npm-publish.yml-Workflow durchlief packages/*, packages/legacy/*, packages/plugins/*, packages/engines/*, aber nicht packages/templates/*.
Templates wie @docmd/template-summer und @docmd/template-rain wurden nicht veröffentlicht.
Jetzt enthalten.

docmd init führt standardmäßig keinen Netzwerkaufruf mehr durch

Zuvor holte docmd init bei jedem Lauf SKILL.md von raw.githubusercontent.com/docmd-io/docmd-skills.
Der abgerufene Inhalt überschrieb den gebündelten Standardwert, wenn der Abruf erfolgreich war.
Zwei Änderungen:

  • Standard ist jetzt nur lokal — der gebündelte SKILL.md-Inhalt wird mit dem Paket ausgeliefert und ohne Netzwerkaufruf geschrieben.
  • Opt-in über --with-skill-Flag oder DOCMD_FETCH_REMOTE_SKILL=1-Umgebungsvariable, um die neueste SKILL.md aus dem docmd-skills-Repo abzurufen (CI-Umgebungen und Offline-Arbeitsplätze können den gebündelten Standard weiterhin verwenden).
docmd init --with-skill    # neueste SKILL.md aus docmd-skills abrufen
DOCMD_FETCH_REMOTE_SKILL=1 docmd init   # dasselbe, über Umgebungsvariable

docker-publish: nicht mehr bei jedem Push auf main ausgelöst

Der docker-publish.yml-Workflow hatte einen push: branches: [main]-Trigger, der bewirkte, dass er bei jedem Commit auf main lief, nicht nur bei Release-Ereignissen. Entfernt — wird jetzt nur noch bei v*-Tag-Pushes, veröffentlichten Releases und manuellem Dispatch ausgelöst.

Dockerfile: pnpm install in CI zuverlässig gemacht

Die Produktionsstufe verwendete corepack prepare pnpm@10.33.2 --activate (mit expliziter Version), aber die Builder-Stufe corepack prepare --activate (ohne Version) schlug in GitHub Actions CI fehl, da sie eine package.json im aktuellen Arbeitsverzeichnis erforderte.
Beide Stufen verwenden jetzt direkt npm install -g pnpm@10.33.2 — einfacher, keine Corepack-Auflösungsschritte, funktioniert identisch in CI und lokal.
Bei einem sauberen Build weigerte sich pnpm außerdem, das vorhandene node_modules-Verzeichnis in der Nicht-TTY-Docker-Umgebung ohne interaktive Bestätigung zu entfernen.
Der Install-Befehl übergibt jetzt --config.confirmModulesPurge=false, das ist pnms zielgerichtete Flag für diesen Fall.
Der breitere ENV CI=true-Weg wurde erwogen und abgelehnt, da er in jedes nachfolgende RUN durchsickert.

UI-Übersetzungsschlüssel werden im veröffentlichten Paket als literaler Text gerendert

builtWith, copyContext, copiedToClipboard und andere template-übersetzte Strings wurden im Build-Output als literaler Schlüsseltext gerendert, wenn das veröffentlichte @docmd/ui-Paket verwendet wurde.
Das Monorepo funktionierte einwandfrei, da packages/ui/translations/ neben dist/ existierte, aber das files-Array des veröffentlichten Pakets enthielt das translations/-Verzeichnis nicht, sodass pnpm pack es aus dem Tarball entfernte.
translations wurde zum files-Array hinzugefügt; loadTranslations gibt jetzt auch eine klare Warnung aus, wenn das Verzeichnis fehlt, damit zukünftige Regressionen nicht als stillschweigende Literalschlüssel-Ausgabe auftauchen.

@docmd/template-* Registry-Schlüssel-Fehlabgleich

Das Setzen von theme.template auf summer oder @docmd/template-summer gab “Plugin ‘template-summer’ not found in official registry — manual installation required” aus, obwohl das Paket installiert war.
Ursache: Der Auto-Install-Registry-Lookup entfernte @docmd/plugin- aus dem Paketnamen, um den Registry-Schlüssel abzuleiten, entfernte aber nicht @docmd/template-.
Daher wurde @docmd/template-summer zum Schlüssel template-summer statt summer aufgelöst.
Beide Präfixe werden jetzt korrekt entfernt.
Benutzerdefinierte/Drittanbieter-Templates sind nicht betroffen — sie umgehen den Registry-Lookup vollständig.

Das Klicken auf einen Navigationsseitenleisten-Unterlink in einem --offline-Build schaltete nur den Aufklappzustand des übergeordneten Elements um, anstatt die Zielseite zu öffnen.
Ursache: Das data-spa-enabled-Flag blieb für Offline-Builds auf true, sodass der SPA-Klick-Handler gebunden wurde und Klicks mit preventDefault() konsumierte.
Aber file:// hat kein fetch für den SPA-Pfad-Abruf, sodass die Navigation nie stattfand.
An zwei Stellen behoben:

  • data-spa-enabled ist jetzt false, wenn isOfflineMode true ist, sodass der SPA-Handler nie bindet und der Browser eine normale Ganzseiten-Navigation durchführt.
  • Eine Safety-Net-Prüfung im Haupt-SPA-Klick-Handler bricht für das file://-Protokoll ab, sodass der Klick nicht konsumiert wird, selbst wenn das Flag falsch ist.
docmd build --offline   # SPA jetzt automatisch deaktiviert
# Workaround auf 0.8.7: über lokalen Server öffnen, oder layout.spa: false setzen

--offline-Modus: umfassende file://-Robustheitsprüfung

Während der Behebung von #164 wurde jede andere Stelle auditiert, an der das clientseitige JS localStorage, fetch, XMLHttpRequest berührt oder sich auf SPA/Netzwerk verlässt.
Alle localStorage-Schreibvorgänge (Seitenleisten-Klappzustand, Theme-Umschaltung, Sprachpräferenz, Cookie-Zustimmung) sind jetzt in try/catch gekapselt, sodass file://-Builds in Chromium-basierten Browsern, in denen der lokale Speicher blockiert ist, keine Fehler auslösen.
Die HEAD fetch-Prüfung des Versionswechslers wird unter file:// übersprungen (Browser blockieren sie sowieso) und geht direkt zur direkten Navigation über.
XHR im noStyle-i18n-Loader ist mit onerror-Behandlung umschlossen.
Der URL-Builder hat für den Offline-Modus bereits /index.html angehängt (unverändert).
Ergebnis: Das Öffnen einer gebauten Site über file:// funktioniert jetzt End-to-End ohne JS-Fehler, ohne defekte Navigation und ohne stillschweigenden Funktionsverlust.

📚 Dokumentationsaktualisierungen

Die folgenden Dokumente wurden aktualisiert, um die oben genannten Korrekturen widerzuspiegeln:

  • Deployment → Docker — Image-Details-Tabelle aktualisiert, um das Root + su-exec-Modell widerzuspiegeln, und der Abschnitt “Custom working directory and file ownership” empfiehlt nicht mehr den -u $(id -u):$(id -g)-Workaround (nicht mehr nötig).
  • docker/DOCKER.md — Abschnitt “Permission Issues” umgeschrieben.
  • Alle sechs READMEs (en, de, es, fr, ja, zh) — Entfernung des -u $(id -u):$(id -g)-Hinweises.

Die Docker-Dokumentation wird in einem Follow-up erweitert, um explizit init --force, die -y-Flag und das Nicht-TTY-Verhalten abzudecken.

🔒 Sicherheitsgrundlage (Phase 0 — intern)

Diese Version legt die gemeinsamen Sicherheitsprimitive, auf denen die übrige Sicherheitsarbeit in 0.8.8 aufbaut. Diese Änderungen sind nicht-breaking und rein additiv — noch keine öffentliche API-Änderung.

Neuer safePath-Heimatort und UserPath-Brand

safePath(root, relativePath) — der von DEVELOPMENT-BENCHMARK.md S1 benötigte Path-Traversal-Schutz — ist von @docmd/api zu @docmd/utils umgezogen, damit er überall ohne Abhängigkeit vom API-Paket verfügbar ist.
@docmd/api re-exportiert es weiterhin zur Rückwärtskompatibilität.

Daneben: ein neuer TypeScript-Branded-Type UserPath. Jeder über asUserPath() umgewandelte String wird als benutzersteuerbare Eingabe markiert; nachgelagerter Code wird erwartet, ihn vor jedem fs.*-Aufruf durch safePath() aufzulösen.
Der Brand ist zur Laufzeit ein No-Op (null Kosten) und ein Typsystem-Signal.

Zentralisierte Escape-Helfer

Vier Helfer werden jetzt von @docmd/utils ausgeliefert:

  • escHtml — HTML-Text und Inline-Interpolation
  • attrEsc — semantischer Alias für HTML-Attributwerte
  • jsonInjectJSON.stringify-Wrapper für <script>-Injektion
  • scriptLiteralJSON.stringify-Wrapper für Inline-JS-String-Kontexte

Jeder Escape-Helfer wird mit XSS-Canary-Payloads unit-getestet (19 Assertions). Ein kanonischer Pfad bedeutet, dass zukünftige Mitwirkende .replace(/&/g, '&amp;')-Muster nicht neu erfinden müssen.

Zwei neue interne ESLint-Regeln

  • docmd/no-unsafe-fs-read — markiert fs.readFile, fs.writeFile, fs.unlinkSync usw., bei denen das Pfadargument nicht durch safePath() aufgelöst wurde. Erkennt potenzielle CWE-22-Verstöße zur Lint-Zeit. Derzeit warn-Schweregrad; wird nach Phase 1 auf error hochgestuft.
  • docmd/require-verify-client — markiert jedes new WebSocketServer({...}) ohne verifyClient-Callback. Sofort auf error gesetzt, damit die zwei vorhandenen Dev-Server-WebSocket-Instanzen (der Dev-Server und der Workspace-Dev-Server) in CI fehlschlagen, bis sie Origin-Checks erhalten. Phase 1 PR 1.D fügt die verifyClient-Callbacks hinzu.

Der anfängliche Lint-Durchlauf zeigt 141 Path-Traversal-Warnungen und 2 verify-Client-Fehler im gesamten Monorepo — diese werden zum Phase 1-Cleanup-Backlog.

Neuer security.html-Konfigurationsschlüssel (Standard auf escape umgestellt)

Der Markdown-Parser respektiert jetzt einen security.html-Konfigurationsschlüssel mit drei Werten:

Wert Verhalten Anwendungsfall
'escape' (Standard in 0.8.8) Raw-HTML in Markdown wird HTML-escaped und als Text angezeigt Standard — sicher für benutzergenerierte Inhalte
'allow' Raw-HTML wird an die gerenderte Ausgabe durchgereicht Dokumentations-Sites, die absichtlich Raw-HTML wie <details>-Blöcke verwenden
'strip' Raw-HTML wird vollständig aus der gerenderten Ausgabe entfernt Gesperrte Inhalte (Unternehmens-Wikis, regulierte Branchen)

Verhaltensänderung: Der Pre-0.8.8-Standard war effektiv 'allow'. Wenn Ihre Dokumentation auf Raw-HTLL angewiesen ist (z. B. <details>, <summary>, eingebettete <iframe>), fügen Sie security: { html: 'allow' } zu Ihrer Konfiguration hinzu, um das alte Verhalten beizubehalten. Container-Blöcke (::: callout, ::: tabs, ::: steps, ::: cards usw.) sind nicht betroffen — sie werden von benutzerdefinierten Regeln geparst, nicht als Raw-HTML.

End-to-End-Abdeckung befindet sich in scripts/brute-test-security.js (22 Assertions über alle drei Richtlinien plus Fallback-Verhalten).

Path-Traversal-Härtung (private Offenlegung)

Drei interne Datei-Zugriffspfade wurden gegen Path-Traversal gehärtet. Keine öffentliche API-Änderung. Siehe den Security Advisory für betroffene Versionen.

Die Korrekturen landen in packages/core/src/commands/mcp.ts, packages/api/src/hooks.ts und packages/plugins/openapi/src/index.ts. Alle drei gehen durch den zentralisierten safePath-Helfer aus @docmd/utils, sodass jeder zukünftige Codepfad durch dasselbe Primitiv abgesichert ist.

End-to-End-Abdeckung in scripts/brute-test-security.js-Szenarien S7 bis S10 (15 Assertions einschließlich einer absichtlichen Canary-Datei außerhalb des Projektstamms und eines bösartigen Local-Path-Plugin-Fixtures).

Dev-Server-Härtung (private Offenlegung)

Die WebSocket-Handler des Dev-Servers und der init-Befehl wurden gehärtet.
Der Dev-Server bindet jetzt standardmäßig an 127.0.0.1 (verwenden Sie DOCMD_HOST=0.0.0.0, um im LAN verfügbar zu machen), und beide WebSocketServer-Instanzen validieren den Origin-Header gegen eine Loopback-Allowlist, bevor sie den Handshake akzeptieren.
Der init-Befehl führt bei jedem Lauf keinen Netzwerkaufruf mehr durch — der gebündelte SKILL.md-Inhalt wird lokal geschrieben; Benutzer installieren den vollständigen Agenten-Skill-Set über npx docmd-skills [dir].

End-to-End-Abdeckung in scripts/brute-test-security.js-Szenarien S11 und S12 (16 Assertions einschließlich eines Snapshots abgefangener fetch-Aufrufe, der die Zero-Network-Eigenschaft belegt).

HTML-Escape-Härtung (private Offenlegung)

In Meta-Tags und Plugin-Head/Body-Injektionspunkte interpolierte benutzersteuerbare Werte werden jetzt über die zentralisierten attrEsc / sanitizeHeadInjection-Helfer aus @docmd/utils escaped.
Die og/twitter Meta-Tags, Link-href-Schemata und plugin-generiertes Head-HTML durchlaufen alle denselben kanonischen Escape-Pfad.
Container-Blöcke (::: callout, ::: tabs usw.) waren bereits durch Phase 0s Standard-html: 'escape'-Markdown-it-Richtlinie geschützt und benötigen keine weitere Änderung.

End-to-End-Abdeckung in scripts/brute-test-security.js-Szenarien S13 bis S15 (13 Assertions einschließlich eines projektinternen Plugins, dessen generateMetaTags <script> und javascript:-Payloads zurückgibt).

Plugin-Eingabevalidierung (private Offenlegung)

Das Analytics-Plugin validiert jetzt measurementId (muss G-XXXXXXXX entsprechen) und trackingId (muss UA-XXXXXXX-Y entsprechen) vor der Injektion des Google Analytics-Skripts.
Ungültige IDs werden mit einer Konsolenwarnung übersprungen, statt roh interpoliert zu werden.
Das PWA-Plugin validiert themeColor jetzt anhand einer CSS-Farbregex (#hex, rgb(), hsl() oder eine CSS-Namensfarbe) und fällt bei ungültiger Eingabe auf den Standard #1e293b zurück.
Beide Validatoren verwenden scriptLiteral() / attrEsc() aus @docmd/utils für Defense-in-Depth.

End-to-End-Abdeckung in scripts/brute-test-security.js-Szenarien S16 und S17 (15 Assertions: ungültige GA4 / UA / themeColor-Payloads, gültiger Fall, Standard-Fallback-Pfad).

MCP und gebündelte SKILL.md sind jetzt docmd-skills-aware

docmd-skills wird als eigenständiges npm-Paket veröffentlicht (https://www.npmjs.com/package/docmd-skills). Die gebündelte SKILL.md, die von docmd init geschrieben wird, und der MCP get_skill-Fallback verweisen Agenten jetzt auf den einzeiligen Installationsbefehl:

npx docmd-skills ~/.claude/skills   # oder ~/.cursor/skills, ./.skills usw.

Der vollständige Agenten-Skill-Set (docmd-skills, docmd-dev, docmd-writer) wird mit diesem einen Befehl installiert. Die gebündelte SKILL.md ist ausreichend für den MCP get_skill-Konsum; npx docmd-skills ist der Pfad zum umfangreichen, mehrteiligen Skill-Set für den interaktiven Agenten-Einsatz.

🛡️ Sicherheitshärtung (Phase 1 — 13 CVEs geschlossen)

Die Battle-Test-0.8.6-Audit fand 13 CVE-Klasse-Probleme in Core + Plugins.
Alle sind in 0.8.8 behoben. CI-Pipelines, die auf docmd <cmd>-Exit-Codes abhängen, ließen vor diesen Korrekturen stillschweigend defekte Builds durch.

Highlights

  • Cross-Site WebSocket Hijacking (CSWSH) — N-S1: Der Dev-Server-WebSocket hatte keinen verifyClient-Callback, sodass jeder Ursprung eine Verbindung öffnen und threads:add-thread-Dateischreibvorgänge auslösen konnte.
    Die neue verifyClient-Fabrik (packages/core/src/utils/ws-origin-guard.ts) erlaubt Loopback standardmäßig und jeden expliziten zusätzlichen Host.
    Beide WebSocket-Aufrufstellen in dev.ts und workspace.ts lehnen jetzt nicht-allowlistete Ursprünge mit Exit-Code 1 ab.

  • MCP read_doc Path-Traversal — T-S1 / S-3: path.resolve(cwd, route) in packages/core/src/commands/mcp.ts akzeptierte absolute Pfade wie /etc/passwd und ../-Escapes.
    Ersetzt durch safePath(cwd, asUserPath(route)), das bei Traversal einen Fehler auslöst.
    Das safePath-Primitiv lebt in @docmd/utils und ist die einzige Quelle der Wahrheit für alle Dateiauflösung-Aufrufe.

  • Plugin-RCE über lokalen Unterordner — T-S8: require.resolve(name, { paths: resolvePaths }) in packages/api/src/hooks.ts suchte in übergeordneten Verzeichnissen und lud beliebige npm-Pakete aus Geschwisterordnern.
    Local-Path-Plugins werden jetzt über safePath(projectRoot, asUserPath(name)) aufgelöst, das auf den Projektstamm beschränkt ist.

  • Plugin-Head-XSS — T-S7: Ein Plugin, das <script>alert(1)</script> aus generateMetaTags zurückgibt, injizierte Live-Skript in jede Seite.
    Das neue sanitizeHeadInjection() in @docmd/utils entfernt <script> / <style> und neutralisiert javascript: / vbscript: URIs.
    In generator.ts für injectHead / injectBody verdrahtet.

  • OpenAPI / Analytics / PWA / Threads XSS — T-S3 / S-4 / S-5 / S-7: Alle vier Plugins validieren jetzt ihre nicht vertrauenswürdigen Konfigurationseingaben (GA4 measurementId, UA trackingId, PWA themeColor) und escapen über attrEsc() / scriptLiteral() aus @docmd/utils, bevor sie in das HTML interpoliert werden.

  • Init macht einen Netzwerkaufruf zu GitHub — T-S5: entfernt.
    docmd-skills ist jetzt ein eigenständiges npm-Paket — die gebündelte SKILL.md ist der Standard; nur über die alte --with-skill-Flag (aus Rückwärtskompatibilität erhalten) kann der Netzwerkaufruf opt-in aktiviert werden.

  • Dev-Server bindet standardmäßig an 0.0.0.0 — T-S6: Standard auf 127.0.0.1 umgestellt.
    Die DOCMD_HOST-Umgebungsvariable ermöglicht LAN-Bindung.
    Die TUI warnt beim Binden an eine Nicht-Loopback-Adresse.

Neue Helfer in @docmd/utils

Funktion Zweck
escHtml(s) HTML-Escape für Text und <script>-Blöcke
attrEsc(s) HTML-Escape für Attributwerte
jsonInject(s) Sicher in JSON-Kontext einfügen
scriptLiteral(s) Sicher in JS-String-Literal einfügen
safePath(root, userPath) Path-Traversal-sichere Dateiauflösung
asUserPath(s) Gebrandeter UserPath-Typ
normalisePath(s) POSIX-Pfad-Normalisierung
sanitizeHeadInjection(html) Entfernt <script>, <style>, neutralisiert javascript: / vbscript:

Test-Abdeckung

scripts/brute-test-security.js88 Assertions über 13 Szenarien (S-1, S-3, S-4, S-5, S-6, S-7, S-9, S-10, S-12, S-13, S-15, S-16, S-17), die jeden dokumentierten CVE-Pfad abdecken. Alle 13 Probleme sind auf der CLI-Oberfläche regressionsgetestet — ein CI-Lauf erkennt jede Regression automatisch.

📦 Container-Parser (Phase 2 — 5 Fehler behoben)

Die 0.8.6-Audit fand fünf stille Container-Parser-Fehler (F1–F5), die falsche Ausgaben ohne Fehler und ohne Exit-Code-Änderung erzeugten.
Der Parser ist jetzt deterministisch, klammerbewusst und einrückungsbewusst.

Was behoben wurde

  • F1 — verschachtelte Grids: ::: grids + N× ::: grid + ein ::: pro Karte gab früher den gesamten Block als Rohtext <p>::: grids<br>::: grid<br>...<br>:::</p> aus. Der Normalisierer fügt explizite :::-Schließungen hinzu, damit die grids-Regel passt.
  • F2 — Selbstschließende Tag-Beschädigung: ::: tag + verwaistes ::: beschädigte den Tiefenzähler für jeden folgenden Container. Selbstschließende Namen (button, tag, embed) werden respektiert; das verwaiste ::: wird mit einer [normaliser] WARNING entfernt.
  • F3 — Nicht übereinstimmende Schließungstypen: ::: callout ... ::: card ... ::: verwurzelte den Parser stillschweigend neu. Das Callout wird bei EOF automatisch mit einem [normaliser] ERROR geschlossen.
  • F4 — Dreifache Schließung löscht Inhalt: Nackte :::-Zeilen sickerten als <p>:::</p>-Absätze in die Seite ein. Verwaisungen werden mit Warnungen entfernt.
  • F5 — 5-stufig verschachtelte Callouts kollabieren: Verschachtelte Callouts renderten nur die äußerste Ebene. Der Normalisierer fügt implizite Schließungen für die inneren Ebenen ein, damit jede callout-Regel rekursiv passt.

Implementierung

Neue packages/parser/src/utils/container-normaliser.ts — reine TypeScript-Portierung des alten robust-parser-shim/ (146 Zeilen).
Lineares Scannen, einrückungsbewusster Stack, keine modulweiten veränderlichen Zustände.
Verdrahtet in packages/parser/src/markdown-processor.ts in SOWOHL processContent ALS AUCH processContentAsync VOR den onBeforeParse-Hooks der Benutzer-Plugins und VOR dem Render von markdown-it.
Läuft innerhalb von createDepthTrackingContainer VOR smartDedent erneut, damit der innere rekursive Render balancierte Eingaben sieht.

Determinismus-Vertrag

Der Normalisierer ist eine reine Funktion seiner Eingabe — kein Date.now, kein Math.random, kein modulweiter veränderlicher Zustand.
Drei Schichten schützen den Vertrag:

  1. Deklarativ — der DETERMINISM AUDIT-Block im Datei-Header dokumentiert die vier nicht-deterministischen Primitive, die das Modul NICHT verwendet.
  2. Empirischpackages/parser/test/container-normaliser.test.js hat 60 Assertions, einschließlich 100-facher nebenläufiger Parsing UND eines echten node:worker_threads-Cross-Worker-Parsings.
  3. RegressionssicherverifyDeterminismAtBoot() in packages/core/src/engine/worker-parser.ts parst bei der Worker-Initialisierung eine bekannte Eingabe und stellt sicher, dass die Ausgabe mit einem eingefrorenen Snapshot übereinstimmt. Jeder zukünftige Nicht-Determinismus lässt den Worker beim Boot abstürzen.

🔧 CLI-Verträge (Phase 3 — 7 Fehler behoben)

Fünf dokumentierte CLI-Fehlerpfade beendeten sich stillschweigend mit 0; zwei validate-Pfade gaben falsche Ergebnisse zurück.
Alle sieben sind behoben.

Was behoben wurde

Befehl Fehlerfall Pre-Fix-Exit Post-Fix-Exit
docmd build Unbekanntes Plugin in Konfig 0 1
docmd migrate Keine --docusaurus/etc.-Flag 0 1
docmd migrate --help (es ist eine Hilfeausgabe) 0 0 (unverändert)
docmd remove <nonexistent> Plugin nicht in Registry 0 1
docmd validate --json Defekte Links 0 1
docmd add <plugin> auf TS/MJS/CJS Konfigurationsdatei unangetastet 0 1 + behoben
docmd remove <plugin> Plugin-Eintrag verbleibt in Konfig 0 1 + behoben
docmd build (Workspace) Roher Stack-Trace, Exit 0 0 1 + saubere TUI
docmd init Beispiel Lehrte F2-Trap-Muster OK bereinigt
docmd validate (nachgestellter Schrägstrich) Falsch-Positiv auf /page/ 1 0

Implementierung

  • Neue packages/core/src/utils/exit.ts exportiert exitCodeFor(err), exitWithError(err, opts?), failWith(message, opts?) — die einzige Quelle der Wahrheit für den Exit-Code-Vertrag.
  • Neue getPluginLoadErrors() in @docmd/api verfolgt Plugin-LOAD-Fehler getrennt von Runtime-Hook-Fehlern. build.ts prüft sie nach loadPlugins() und wirft, was der vorhandene catch-Block in Exit 1 umwandelt.
  • Neue packages/plugins/installer/src/config-editor.ts — ein klammerbalancierter Scanner, der alle fünf unterstützten Konfigurationsformate (docmd.config.{json,js,mjs,cjs,ts}) einheitlich behandelt. Der alte regex-basierte Injektor passte nur auf module.exports = {...} und war für export default defineConfig({...}) stillschweigend no-op. resolveConfigPath() prüfte nur .json/.js und hätte stillschweigend eine neue Datei neben der .ts eines Projekts erstellt.

Test-Abdeckung

scripts/brute-test.js22 neue Assertions über drei Phase 3-Abschnitte (Test 26: Exit-Codes, Test 27: Plugin-Add/Remove, Test 29: Validate + Workspace + Init). Alle unter tests/cli-contracts/.

🤖 OKF-Plugin (0.8.8 — neues Core-Plugin)

Ein neues Core-Plugin erzeugt ein Open Knowledge Format-Bundle für den KI-Agenten-Konsum.
Das Bundle liegt unter site/okf/ und enthält ein typisiertes Manifest (okf.yaml), einen interaktiven Graph-Viewer und eine Markdown-Datei pro Seite.

Was Sie out-of-the-box erhalten

site/okf/
├── okf.yaml              ← typisiertes Manifest (Bundle-Zusammenfassung)
├── index.md              ← Karpathy-Stil-Katalog, nach Typ gruppiert
├── graph.html            ← interaktiver Force-Directed-Viewer
├── graph.json            ← Graph-Daten (Knoten + Kanten)
├── graph.js              ← Viewer-Laufzeit (Vanilla, keine CDN-Abhängigkeiten)
├── graph.css             ← Viewer-Styles (Theme-fähig)
├── concepts/<slug>.md    ← eine Markdown-Datei pro Seite
└── _meta/
    ├── bundle.json       ← JSON-Spiegel von okf.yaml
    └── lint-report.txt   ← während der Erzeugung erzeugte Warnungen

Standardverhalten (0.8.8)

  • Standardmäßig aktiviert — OKF ist ein Core-Plugin wie llms und seo. Der Build lädt es automatisch und erzeugt das Bundle, auch wenn der Benutzer plugins.okf nicht zu seiner Konfiguration hinzugefügt hat. Es werden drei Opt-out-Pfade unterstützt (false, enabled: false oder Capability-Filter).
  • Standardmäßig einsprachig — das Bundle enthält Seiten nur in der Standard-Locale. Die Dateien der Standard-Locale liegen im Bundle-Stammverzeichnis (kein en/-Unterverzeichnis).
  • Typ-Inferenz — Seiten unter /api/, /guides/ usw. werden automatisch klassifiziert; alles andere fällt auf concept zurück.

Multi-Locale Opt-in

{
  "plugins": {
    "okf":  { "localeStrategy": "folders" },
    "llms": { "i18n": true }
  }
}

Wenn beide Flags gesetzt sind, bleiben die Dateien der Standard-Locale im Bundle-Stammverzeichnis (sodass bestehende Konsumenten nicht beeinträchtigt werden) und Nicht-Standard-Locales erhalten ein <locale>/-Unterverzeichnis (OKF) oder .<locale>-Suffix (LLMS):

site/okf/okf.yaml         ← Standard-Locale
site/okf/concepts/...md   ← Standard-Locale-Konzepte
site/okf/ja/okf.yaml     ← Japanisch
site/okf/ja/concepts/...md

site/llms.txt             ← Standard-Locale (en)
site/llms.ja.txt         ← Japanisch

Test-Abdeckung

packages/plugins/okf/tests/index.test.ts (8 Tests) + packages/plugins/okf/tests/brute.test.ts (15 Tests) einschließlich 12 real-fs-Szenarien. 23/23 bestehen. Neuer Test übt den Standard-Aktiviert-Vertrag (ein Projekt ohne plugins.okf-Eintrag erhält trotzdem ein generiertes Bundle).

🧪 Test-Infrastruktur (kategorisiert)

Die Brute-Tests waren früher 1000+ Zeilen in zwei scripts/-Dateien mit doppelten Test-Nummern (Phase 3 PR 3.A verwendete 26, 27, 29 erneut). Sie sind jetzt unter tests/ kategorisiert:

tests/
├── shared.js                                ← Fixture-Helfer
├── runner.js                                ← Orchestrator mit TUI-Abschnitten
├── cli-contracts/
│   ├── exit-codes.test.js                   ← F6, M-12
│   ├── plugin-add-remove.test.js            ← F7, M-3
│   └── validate-workspace.test.js           ← M-1, F8, F9
└── (pro-Paket-Tests verbleiben unter packages/*/test/)

Der Runner wird von pnpm prep (oder direkt über node tests/runner.js) aufgerufen und führt jede Suite in ihrem eigenen TUI-Abschnitt aus.
Filtern Sie mit --only=exit-codes,container-normaliser, um nur die benannten Suites auszuführen.

Ergebnis

342 Assertions über 9 kategorisierte Dateien, alle grün (zzgl. 89 weiterer pro-Paket-Unit-Tests in 4 Paketen, insgesamt 431 Assertions / 13 Dateien):

Suite Ergebnis Abdeckung
cli-contracts/exit-codes 7 / 7 F6, M-12
cli-contracts/plugin-add-remove 9 / 9 F7, M-3 (TS, MJS, JSON, CJS, JS)
cli-contracts/validate-workspace 6 / 6 M-1, F8, F9
packages/parser/test/container-normaliser 60 / 60 F1–F5 + Determinismus
packages/utils/test/{path,html-escape} 31 / 31 safePath, escHtml usw.
scripts/brute-test-security 88 / 88 Phase 1 CVE-Suite (13 Szenarien)
scripts/brute-test 114 / 114 29 Feature-Integrations-Szenarien
packages/plugins/okf 23 / 23 Unit + 12 real-fs-Szenarien + Graph-Opt-in/Deprecation
packages/plugins/llms 6 / 6 Standard-Locale + i18n-Opt-in (NEU)

Aktualisierungen — pnpm prep-Pipeline gehärtet

Die Release-Pipeline erhielt drei operative Verbesserungen:

  • Pro-Paket-Unit-Tests nehmen jetzt am Gate teil. pnpm -r run test --if-present läuft nach tests/runner.js, sodass eine Regression in @docmd/parser, @docmd/utils, @docmd/plugin-okf oder @docmd/plugin-mermaid die Release-Pipeline auf die gleiche Weise scheitern lässt wie eine kategorisierte-Läufer-Regression. Keine Paket-Test-Suite kann mehr stillschweigend driften.
  • --verbose / --full-Flag. Standardausgabe bleibt eingeklappt (jeder Testschritt ist eine Zeile), sodass ein sauberer Lauf auf einen Blick lesbar ist. Mit --verbose wird die rohe Testausgabe inline gestreamt — nützlich beim Iterieren eines gerade fehlgeschlagenen Tests, um den Assertion-Text im Kontext zu sehen.
  • Abschließender Summary-Block. Bei einem sauberen Lauf trägt jede Sektion eine Statistik bei (Lint-Zähler, Paket-Gesamtzahlen, Test-Zähler, Docker-Version), die am Ende als ein grüner Block gerendert wird. Bei einem Fehler wird dieselbe Position zu einem roten Issues-Block, der jeden Fehler mit Datei:Zeile-Detail und gedeckeltem Überlauf auflistet. Eine einzige Stelle, um das Urteil zu sehen.

🔌 Plugin-Autoren-Hinweis

Sowohl @docmd/plugin-llms als auch @docmd/plugin-okf sind in 0.8.8 standardmäßig aktivierte Core-Plugins. Der Build lädt sie automatisch. Zum Opt-out verwenden Sie die enabled: false-Form, die bereits für jedes andere Core-Plugin funktioniert:

{
  "plugins": {
    "okf":  { "enabled": false },
    "llms": { "enabled": false }
  }
}

Ausblick — Node 20+ in v0.9.0

Die nächste Minor-Version hebt die Untergrenze auf Node.js 20+ (von 18+), was die Engine-Anforderung, die ausgelieferte Docker-Laufzeit und die CI-Umgebung alle auf Node 22 bringt. Node 18 erreichte im April 2025 das Ende des Lebenszyklus; dies hält docmd auf einer unterstützten Laufzeit mit reichlich Puffer.

Dieses Release (0.8.8) ist nicht betroffen — die >=18-Untergrenze bleibt, bis 0.9.0 ausgeliefert wird.

Aktuell (0.8.x) Nächstes (0.9.0)
Minimum Node 18+ 20+
Docker-Basis 20-alpine 20-alpine
Type Node ^24 ^24
CI-Ziel 24 24

Betroffene Versionen

Alle Korrekturen in diesem Release zielen auf 0.8.7. Es gibt keine Breaking Changes — jede Korrektur ist additiv oder eine reine Fehlerkorrektur.