init
This commit is contained in:
commit
f07ef9653d
851 files changed
+501480
No files matched your search
@@ -0,0 +1,14 @@
|
||||
# Nach .env kopieren und Werte setzen. Die .env ist die EINZIGE Stelle, an der
|
||||
# Modelle stehen — Code enthält keine Defaults und keine Fallbacks (Leitprinzip 2).
|
||||
|
||||
MINIMAX_API_KEY=
|
||||
|
||||
# Rollen-Belegung (Format provider:modell). Start-Belegung: alles MiniMax.
|
||||
ROLLE_SCHNEIDEN=minimax:minimax/MiniMax-M3
|
||||
ROLLE_NACHFASS=minimax:minimax/MiniMax-M3
|
||||
ROLLE_SICHTEN=minimax:minimax/MiniMax-M3
|
||||
ROLLE_JUDGE=minimax:minimax/MiniMax-M3
|
||||
|
||||
# Engine-Tuning (optional, Defaults siehe backend/config.py)
|
||||
# MAX_CONCURRENT_AGENTS=6
|
||||
# RAM_MIN_FREE_PCT=20
|
||||
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
storage/
|
||||
.pytest_cache/
|
||||
@@ -0,0 +1,26 @@
|
||||
.PHONY: test scan rescan status dev build web
|
||||
|
||||
test:
|
||||
cd backend && python3 -m pytest -q
|
||||
|
||||
# Entwicklung: Backend (8100, OHNE --reload — Reload killt laufende Scans) + Vite (5273)
|
||||
dev:
|
||||
cd backend && uvicorn server:app --port 8100 & \
|
||||
cd frontend && npx vite --port 5273
|
||||
|
||||
# Produktion: Frontend bauen, dann nur den Server starten (serviert frontend/dist)
|
||||
build:
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
web:
|
||||
cd backend && uvicorn server:app --port 8100
|
||||
|
||||
# make scan PROJEKT=/pfad/zum/repo
|
||||
scan:
|
||||
python3 -m backend scan $(PROJEKT)
|
||||
|
||||
rescan:
|
||||
python3 -m backend rescan $(PROJEKT)
|
||||
|
||||
status:
|
||||
python3 -m backend status
|
||||
@@ -0,0 +1,331 @@
|
||||
# Planer
|
||||
|
||||
Ein System, mit dem ich alle meine Projekte **verstehe, orchestriere und weiterentwickle** —
|
||||
ohne je die Übersicht zu verlieren, was ein Projekt kann. Wiedereinlesen nach Monaten in
|
||||
Minuten; Änderungen laufen über ein Agenten-Team, das wie ein Entwicklungsunternehmen
|
||||
arbeitet, in dem ich der Geschäftsführer bin.
|
||||
|
||||
Dieses Dokument ist die **Referenz der Planung** (Stand 2026-07-09). Das System ist noch
|
||||
nicht gebaut; wir implementieren schrittweise entlang der Phasen am Ende. Nichts hier ist
|
||||
Code-Realität, bis es umgesetzt und abgehakt ist.
|
||||
|
||||
Referenz-Projekt: `../creator` — dort sind die Kern-Muster bereits validiert
|
||||
(generische Kanban-Engine `kanban.py`, Provider-Layer `agents.py`, Judge-Gates,
|
||||
Resume, Events-Telemetrie, Fake-Agenten für Tests).
|
||||
|
||||
---
|
||||
|
||||
## Leitprinzipien
|
||||
|
||||
1. **Die Wahrheit wohnt im Projekt-Repo.** Artefakte sind Dateien unter `.planer/` im
|
||||
jeweiligen Projekt, versioniert mit dem Code. Code-Änderung + Artefakt-Update = ein
|
||||
Commit (atomarer Sync). Der Planer ist Viewer + Orchestrator, nie Wahrheitsbesitzer;
|
||||
seine DB hält nur wegwerfbaren Lauf-Zustand und Telemetrie.
|
||||
2. **Kein LLM als Default oder Fallback — nirgendwo.** Jeder LLM-Schritt hat eine benannte
|
||||
Rolle; die Zuordnung Rolle → Modell steht ausschließlich in einer zentralen Config, vom
|
||||
Nutzer per UI gesetzt. Unkonfigurierte Rolle oder Provider nicht erreichbar = Lauf
|
||||
pausiert mit sichtbarer Meldung. Niemals stiller Wechsel. (Hintergrund: ein stiller
|
||||
Fallback hat einmal ~40 % des Claude-Abo-Kontingents gekostet.) Mechanisch absichern:
|
||||
Test greppt nach Modell-IDs außerhalb der Config.
|
||||
3. **Provider-Unabhängigkeit.** Eigenes Agentensystem (Creator-Bausteine wiederverwenden),
|
||||
damit jederzeit zwischen Claude (Fable/Opus/Sonnet/Haiku) und MiniMax (M3/M2.7-HS/M2.7)
|
||||
gewechselt werden kann — später auch lokale LLMs. Der Provider-Fallback in
|
||||
`creator/backend/agents.py` („routed provider unavailable → run provider") wird NICHT
|
||||
übernommen.
|
||||
4. **Mechanisch vor LLM.** Alles, was Skripte prüfen können (Anker-Existenz, Tests, Lint,
|
||||
Diff-Umfang), läuft ohne Token und vor jedem LLM-Urteil.
|
||||
5. **Messinvarianz.** Telemetrie und QA messen nur; sie sind nie Teil einer Optimierung.
|
||||
6. **Die UI zeigt, was Handlung braucht — nicht alles, was passiert.** Autonome Agenten
|
||||
bleiben Hintergrundrauschen; nur Entscheidungen, Drift und Ergebnisse drängen sich vor.
|
||||
7. **YAGNI.** Zielbild vollständig planen (dieses Dokument), aber jede Phase baut nur, was
|
||||
sie braucht. Wachstum gesteuert durch Telemetrie und tatsächlichen Gebrauch.
|
||||
8. **Scan-Ausschlussliste, Secrets nie lesen.** Der Scan liest `.env`, `node_modules`,
|
||||
Build-Artefakte etc. grundsätzlich nicht — Fakten zitieren Code, und ein zitierter
|
||||
API-Key stünde sonst im Klartext im Artefakt (Repo!) und in den Agent-Transkripten.
|
||||
|
||||
---
|
||||
|
||||
## Wissensschichten (Dateien in `<projekt>/.planer/`)
|
||||
|
||||
Drei Schichten beantworten drei Fragen:
|
||||
|
||||
### 1. Einheiten — *was der Code tut* (Beleg-Schicht)
|
||||
|
||||
Atomare Codebereiche, MECE über den Code: jede Code-Stelle gehört zu genau einer Einheit.
|
||||
Schnittregel: das Kleinste mit eigener Verantwortung, in einem Satz beschreibbar — mal
|
||||
eine Funktion, mal eine Klasse, mal ein zusammengehöriger Block. Der Scan-Agent schneidet
|
||||
nach dieser Regel (kein Parser); v1 beginnt auf Symbol-Ebene, Kleinkram (Imports, Config)
|
||||
darf anfangs unzugeordnet bleiben.
|
||||
|
||||
Jede Einheit trägt:
|
||||
- **Anker**: Pfad + Symbol + Commit-Hash des letzten Scans (stabile Identität; bei
|
||||
Moves/Renames matcht der Re-Scan nach, statt gelöscht+neu zu melden)
|
||||
- **Ein-Satz-Beschreibung** (die Verantwortung)
|
||||
- **Fakten**: belegte Aussagen über das Verhalten
|
||||
- **Beziehungen/Kanten**: ruft auf / importiert / wird ausgelöst von → andere Einheiten
|
||||
|
||||
### 2. Artefakt-Sichten — *was das Projekt kann*
|
||||
|
||||
Überlappende Sichten auf die Einheiten. Sichten **referenzieren** Einheiten, kopieren nie —
|
||||
jede Aussage existiert genau einmal. MECE gilt **pro Ebene innerhalb eines Typs**, nie über
|
||||
Typen hinweg.
|
||||
|
||||
- **Kern**: wenige Sätze + die tragenden Einheiten
|
||||
- **Features**: Funktionsbereiche (6–10) → Features → Teilfeatures → Einheiten-Referenzen;
|
||||
jedes Feature mit Flag `kern|rand`. Auf jeder Aufklapp-Ebene stehen 5–15 Dinge, nie 100.
|
||||
- **Flows**: Titel + Beschreibung + Schrittfolge (jeder Schritt → Einheiten)
|
||||
- **Architektur**: Komponenten (jede Komponente → Einheiten), Beziehungen aus den Kanten
|
||||
|
||||
### 3. Entscheidungen — *warum es so ist*
|
||||
|
||||
Decision Records in `.planer/entscheidungen/`, eine Datei pro Entscheidung: Frage, Kontext,
|
||||
Optionen, Wahl, Begründung, betroffene Einheiten/Artefakte. Entstehen aus Eskalationen und
|
||||
strategischen Weichenstellungen. Fließen als Kontext in künftige Aufgaben, deren
|
||||
Einheiten/Thema sie berühren — Filter und Verifizierer prüfen auch dagegen. Effekt: die
|
||||
Eskalationsrate sinkt über die Zeit von selbst.
|
||||
|
||||
### Weitere Dateien
|
||||
|
||||
- `.planer/pruefung.md` — Prüf-/Betriebsanleitung des Projekts: Build-, Test-, Lint- und
|
||||
Start-Kommandos, bekannte Eigenheiten. Wird beim Scan erfasst; Voraussetzung für die
|
||||
mechanischen Gates und den Verifizierer. Fehlen Tests, degradiert das Gate **sichtbar**
|
||||
(„keine Tests vorhanden"), nie still.
|
||||
- `.planer/strategie/<name>.md` — benannte Soll-Entwürfe (siehe Strategie-Bereich)
|
||||
- `.planer/ideen.md` — Ideen-Backlog, wird im Jour Fixe wieder aufgelegt
|
||||
- `.planer/jourfixe/` — Kurzprotokolle der Sitzungen (Anknüpfpunkt für die nächste)
|
||||
|
||||
---
|
||||
|
||||
## Scan & Drift
|
||||
|
||||
- **Erst-Scan**: Voll-Analyse des Repos → Einheiten schneiden → Fakten + Kanten extrahieren
|
||||
→ Sichten aggregieren (Barriere: braucht alle Einheiten) → QA. Läuft als Kanban-Board.
|
||||
- **Re-Scan**: `git diff` seit dem letzten Scan gegen die Anker → nur berührte Einheiten
|
||||
neu prüfen → nur Artefakte prüfen, die diese Einheiten referenzieren. Billig.
|
||||
- **Qualitätssicherung zweistufig**: mechanische Gates zuerst und tokenfrei (existiert der
|
||||
Anker? steht an der Stelle dieser Code? Abdeckung?), LLM-Judge nur für Semantik (stimmt
|
||||
Beschreibung, sind Fakten korrekt gefolgert). Dadurch ist MiniMax als Scan-Generator gut
|
||||
abgesichert.
|
||||
- **Drift-Anzeige**: veraltete Artefakte tragen ein Badge mit Re-Scan-Knopf; Drift-Ampel
|
||||
je Projekt auf der Startseite.
|
||||
|
||||
---
|
||||
|
||||
## Der Änderungs-Kreislauf (Kern-Flow)
|
||||
|
||||
```
|
||||
Ich entscheide (Strategie / Jour Fixe / Chatbox)
|
||||
→ bestätigter Soll-Diff (Artefakt-Ebene: Features neu/geändert/entfernt)
|
||||
→ Plan-Agent zerlegt in kleine Aufgaben (Schnittgrenze = Einheiten,
|
||||
prüfbares Fertig-Kriterium, relevante Decision Records angehängt)
|
||||
→ Team-Board arbeitet ab (siehe unten), Eskalationen landen bei mir
|
||||
→ Integrations-Verifikation gegen den urspr. Soll-Diff
|
||||
(fängt Zerlegungsfehler, die Einzel-Verifizierer nicht sehen)
|
||||
→ Artefakt-Sync (Re-Scan des Diffs, Einheiten + Sichten aktualisieren)
|
||||
→ Commit (Code + Artefakte atomar), Anker auf neuen Commit-Hash
|
||||
```
|
||||
|
||||
Scan, Board und Sync arbeiten **nur auf sauberem Working Tree** — uncommittete Änderungen
|
||||
im Projekt = Abbruch mit Meldung, sonst vermischen sich Agenten-Diffs mit Handarbeit und
|
||||
der Anker-Commit stimmt nicht mehr.
|
||||
|
||||
Der Commit passiert **immer erst nach dem Artefakt-Sync** — sonst bricht „der Planer hat
|
||||
die Wahrheit" bei jeder Änderung. Änderungen über den Planer halten die Artefakte per
|
||||
Konstruktion aktuell; der Drift-Check ist nur das Netz für Änderungen daran vorbei
|
||||
(die dank Artefakten im Repo + Projekt-Skill ebenfalls mitpflegen können).
|
||||
|
||||
## Team-Board (Ausführung)
|
||||
|
||||
Kanban-Board (Engine aus dem Creator): Karten = Aufgaben, Stufen:
|
||||
|
||||
1. **Umsetzer** ändert Code für genau seine Aufgabe
|
||||
2. **Mechanische Gates** (tokenfrei, Kommandos aus `.planer/pruefung.md`): Tests grün,
|
||||
Lint/Format, Diff berührt nur die Einheiten der Aufgabe (Scope-Creep-Check),
|
||||
Diff-Größe plausibel
|
||||
3. **LLM-Stil-Filter** (je eigene Rolle, billige Modelle): kein Aufblähen, minimale
|
||||
Änderung, voller Umfang der Aufgabe — *erst ab v2, wenn Telemetrie zeigt, welche*
|
||||
4. **Verifizierer** (starkes Modell, frischer Kontext — nie der Umsetzer-Kontext): führt
|
||||
aus/testet, prüft Verhalten gegen Aufgabe + Soll-Artefakte + Decision Records
|
||||
|
||||
Regeln:
|
||||
- **Ablehnung** → Befunde-Liste zurück an den Umsetzer (Repair-Muster: Befunde gezielt
|
||||
abarbeiten, nicht neu würfeln). **Max 2 Runden**, dann Eskalation (Kostenschutz).
|
||||
- **Fehlende Info/Entscheidung** = sofortige Eskalation, ohne Rate-Runden.
|
||||
- **Eskalationen kommen entscheidungsreif** an: Blocker + 2–3 Optionen mit Trade-offs +
|
||||
Empfehlung. Meine Antwort wird automatisch Decision Record, die Karte läuft damit weiter.
|
||||
- Aufgaben mit **disjunkten Einheiten dürfen parallel** laufen (ab v2; v1 sequenziell).
|
||||
- Filter, die wiederholt ablehnen, was ich durchwinke → Prompt schärfen statt mehr Runden
|
||||
(Judges rauschen — Creator-Lehre).
|
||||
|
||||
## Gesprächsmodi (vier, klar getrennt)
|
||||
|
||||
| Modus | Zweck | Ergebnis fließt in |
|
||||
|---|---|---|
|
||||
| **Chatbox** | jetzt ändern: Änderung beschreiben → Soll-Diff-Vorschau in den Artefakten → bestätigen → Board | Auftrag |
|
||||
| **Strategie** | Zukunft entwerfen: mit Agent (`stratege`) Soll-Entwürfe ausarbeiten; Live-Vorschau = Feature-Baum mit neu/geändert/entfernt-Badges; mehrere benannte Szenarien, reifen über Sitzungen | etappenweise Soll-Diffs (Roadmap), Decision Records |
|
||||
| **Jour Fixe** | rückblickend verbessern: Agent (`berater`) eröffnet datenbasiert (Agenda aus Telemetrie: Eskalationen/Runden, Drift, Kosten, Ideen-Backlog) | Aufträge, Decision Records, `ideen.md`, Protokoll |
|
||||
| **Eskalation** | blockierte Entscheidung aus dem Board | Decision Record, Karte läuft weiter |
|
||||
|
||||
Chatbox und Strategie sind technisch **eine Komponente in zwei Modi** (sofort ausführen
|
||||
vs. Entwurf pflegen) — gleiche Diff-Engine, gleiche Vorschau.
|
||||
|
||||
## Rollen & Modelle
|
||||
|
||||
Zentrale Rollen-Tabelle (einzige Stelle mit Modellnamen), per UI-Switch gesetzt, Override
|
||||
pro Lauf möglich. Rollen mindestens: `scan-schneiden`, `fakten`, `judge`, `plan`,
|
||||
`umsetzer`, `stil-filter-*`, `verifizierer`, `integrations-verifizierer`, `executor`,
|
||||
`chat`, `stratege`, `berater`.
|
||||
|
||||
- **Start-Belegung: ALLE Rollen auf MiniMax** (der Betreiber hat dort viele Tokens zum
|
||||
Testen). Claude kommt nur in eine Rolle, wenn der Betreiber sie bewusst umschaltet.
|
||||
- Erwartung aus Phase 0: Für Plan/Verifizierer/Stratege/Berater wird sich ein stärkeres
|
||||
Modell lohnen — aber der Wechsel passiert datenbasiert per Telemetrie, nicht vorab.
|
||||
- Vor jedem Lauf zeigt die UI die komplette Rollen-Belegung zum Bestätigen; jeder laufende
|
||||
Agent zeigt sein Modell
|
||||
|
||||
## Telemetrie (Planer-DB, nicht Repo)
|
||||
|
||||
- Je Call: Rolle, Modell, Provider, Tokens rein/raus, Dauer, Status, Projekt, Karte, Stufe
|
||||
- Je Aufgabe: Runden, Filter-/Verifizierer-Urteile mit Grund, eskaliert + Grund
|
||||
- Je Eskalation: meine Entscheidung (Muster-Erkennung: gleiche Frage-Kategorie mehrfach →
|
||||
Kandidat für Decision Record/Regel)
|
||||
- Agent-Transkripte je Lauf aufbewahren (prunebar) — Urteile müssen nachlesbar sein
|
||||
- Zweck: datenbasierte Modellwahl je Rolle („MiniMax 1,6 Runden vs. Sonnet 1,1 bei 40 %
|
||||
der Kosten"), Filter-Tuning, Kosten-Dashboard, Jour-Fixe-Agenda
|
||||
|
||||
## UI
|
||||
|
||||
- **Startseite = Geschäftsführer-Schreibtisch** (Orchestrieren): oben Entscheidungsliste
|
||||
(Eskalationen, entscheidungsreif), darunter laufende Aufträge über alle Projekte
|
||||
(Fortschritt, Modelle, Token), darunter Projektliste mit Drift-Ampel und letztem Scan.
|
||||
- **Projektansicht** (Verstehen): Einstieg über den Kern, aufklappbare Hierarchie
|
||||
Funktionsbereiche → Features → Teilfeatures → Einheiten (Fakten, Anker); Tabs für Flows,
|
||||
Architektur, Entscheidungen; Drift-Badges inline mit Re-Scan-Knopf.
|
||||
- **Doku-Sicht** (Wiedereinlesen): Lesemodus derselben Daten in Einlese-Reihenfolge
|
||||
(Kern → Architektur → Bereiche in Abhängigkeitsreihenfolge aus den Kanten). Reine Sicht,
|
||||
keine eigenen Inhalte — kann nie veralten.
|
||||
- **Chatbox** in der Projektansicht; Soll-Diff-Vorschau direkt in den Artefakten.
|
||||
- **Strategie-Bereich**: Chat links, Feature-Baum mit Live-Änderungs-Badges in der Mitte.
|
||||
- **Rechte Seitenleiste**: Live-Status des Boards (Karten je Stufe, aktive Agenten mit
|
||||
Modell/Laufzeit, Befunde); Eskalationen erscheinen hier UND auf der Startseite.
|
||||
|
||||
## Verwendete Systeme
|
||||
|
||||
Bewusst der Creator-Stack — der Planer unterscheidet sich durch sein Konzept, nicht durch
|
||||
seine Technik; neue Technologie wäre Lernaufwand an der falschen Stelle.
|
||||
|
||||
- **Backend**: Python, FastAPI, uvicorn, aiosqlite — gesetzt durch die Wiederverwendung
|
||||
von `kanban.py`/`agents.py`; Fake-Agenten-Ansatz für Tests übernehmen.
|
||||
- **LLM-Anbindung**: CLI-Subprozesse, keine direkten APIs. `claude`-CLI (läuft über das
|
||||
Abo via OAuth-Token — die Anthropic-API würde separat kosten), `opencode`-CLI für
|
||||
MiniMax. Der Executor ist dieselbe Claude-CLI headless (`claude -p`).
|
||||
- **Frontend**: Vue 3 + Vite, ohne UI-Framework (Muster für Live-Fortschritt, laufende
|
||||
Agenten, Dark Mode aus dem Creator kopieren; `marked` fürs Artefakt-Rendering).
|
||||
Komponentenbibliothek erst, wenn konkret etwas fehlt.
|
||||
- **Daten**: SQLite nur für Betrieb (Telemetrie, Karten, Lauf-Zustand — wegwerfbar).
|
||||
Artefakte als Markdown mit YAML-Frontmatter in `.planer/` (menschenlesbar, parsebar,
|
||||
diff-freundlich; Format-Details in Phase 0).
|
||||
- **Git**: nackte git-CLI + Worktrees für den Executor, keine Bibliothek.
|
||||
- **Deployment**: Makefile lokal, Docker optional für den Server (Setup vom Creator).
|
||||
- **Projekt-Verortung**: Projekte bleiben, wo sie sind (z. B. `~/projects/*`) — der Planer
|
||||
hält nur eine konfigurierte Pfadliste, kein Unterordner-Zwang.
|
||||
|
||||
## Spiel-Verpackung (reine Präsentationsschicht)
|
||||
|
||||
Umschaltbarer Skin über derselben UI und denselben Daten — **keine eigene Mechanik, keine
|
||||
Punkte/Levels/Achievements** (Goodhart-Gefahr, verletzt Messinvarianz). Metaphern sind 1:1
|
||||
isomorph zur Realität:
|
||||
- Budget = echte Token-Kosten (einziges festes „Spielsystem", weil real)
|
||||
- Einstellen = Rollen-Tabelle („teurer Senior Fable oder Junior M2.7?"),
|
||||
Leistungsbeurteilung = Telemetrie je Modell
|
||||
- Personas mit festen Namen je Rolle (macht Logs/Seitenleiste lesbarer)
|
||||
- Werkbank = Chatbox, Vorstandsklausur = Strategie, Quartalsgespräch = Jour Fixe,
|
||||
Posteingang = Eskalationen, Abteilungen = Projekte
|
||||
|
||||
---
|
||||
|
||||
## Dogfooding: der Planer ist das erste Projekt des Systems
|
||||
|
||||
Der Planer verwaltet sich selbst — gestaffelt, um das Henne-Ei-Problem zu umgehen:
|
||||
|
||||
- **Phase 0 validiert am Creator** (der Planer hat noch keinen Code; der Scan muss sich an
|
||||
einem großen, gewachsenen Repo beweisen).
|
||||
- **Ab Phase 1** ist der Planer erstes gescanntes Projekt: Er scannt sich selbst, die
|
||||
Artefakte werden gegen diese README geprüft — Abweichung heißt Scan schlecht ODER Plan
|
||||
veraltet, beides will man wissen. Die `.planer/`-Artefakte wachsen ab Commit eins mit
|
||||
(Soll-first: die Feature-Sicht kann aus dieser README abgeleitet werden, bevor der Code
|
||||
existiert).
|
||||
- **Ab Phase 3** laufen Änderungen am Planer durch den Planer selbst.
|
||||
|
||||
**Regel für Selbst-Änderungen** (Creator-Lehre „kein Backend-Edit bei laufendem Flow"):
|
||||
Der Executor arbeitet immer in einem Worktree/Branch — nie im laufenden Checkout. Gemergt
|
||||
und neu gestartet wird erst, wenn das Board durch ist; sonst killt der Server-Reload die
|
||||
eigenen Läufe.
|
||||
|
||||
## Implementierungsphasen
|
||||
|
||||
Jede Phase liefert eigenständigen Nutzen. Erst abhaken, dann weiter.
|
||||
|
||||
### Phase 0 — Scan validieren (kein App-Code) ✅ (2026-07-09)
|
||||
Die einzige Komponente ohne Creator-Präzedenzfall ist die Scan-Qualität. Alles steht auf ihr.
|
||||
- [x] `.planer/`-Dateiformat definieren (Einheiten, Sichten — Markdown/YAML) → `docs/format.md`
|
||||
- [x] Scan-Prompts von Hand am Creator-Repo ausprobieren (MiniMax) → `phase0/` (Prompts, Beispiele)
|
||||
- [x] Mechanische Prüfskripte: Anker-Existenz, Zitat-Treue → `phase0/pruefe.py`
|
||||
- [x] Bewerten → mechanisch bestanden (M3: 0 Fehler nach Nachfass; Feature-Sicht: 0 kaputte
|
||||
Referenzen). Nutzer-Urteil zur Lesbarkeit folgt im echten Viewer (Phase 2).
|
||||
Alle Befunde: `phase0/erkenntnisse.md` (13 Erkenntnisse, u. a. M3 statt M2.7-HS,
|
||||
Chunking-Pflicht, Stagger-Sperre, Infra-Fehlerklassen)
|
||||
|
||||
### Phase 1 — Scan-Pipeline ✅ (2026-07-09)
|
||||
- [x] Kanban-Engine + Provider-Layer aus dem Creator übernehmen (Fallback entfernen!)
|
||||
- [x] Rollen-Tabelle als Config, harter Stopp bei unkonfigurierter Rolle
|
||||
- [x] Fake-Agenten von Anfang an (Creator-Muster) — Pipeline ohne LLM in Sekunden testbar
|
||||
- [x] Scan-Board: schneiden → Fakten/Kanten → Sichten (Barriere) → mechanische Gates → Judge
|
||||
- [x] Scan erfasst `.planer/pruefung.md` (Build/Test/Start-Kommandos je Projekt)
|
||||
- [x] Scan-Ausschlussliste (`.env`, `node_modules`, Build-Artefakte …)
|
||||
- [x] Re-Scan über git-Diff gegen Anker, Rename/Move-Matching
|
||||
- [x] Events-Telemetrie von Anfang an
|
||||
- [x] An eigenen Projekten laufen lassen (creator-Abnahme bestanden; weitere bei Bedarf)
|
||||
- [x] Dogfooding: Planer scannt sich selbst — abgehakt auf Nutzer-Entscheid; Lauf folgt nach erstem Commit
|
||||
|
||||
### Phase 2 — Viewer
|
||||
- [ ] Startseite: Projektliste mit Drift-Ampel, Scan/Re-Scan auslösbar
|
||||
- [ ] Projektansicht: Kern, aufklappbare Feature-Hierarchie, Tabs Flows/Architektur/Entscheidungen
|
||||
- [ ] Doku-Sicht (Leseordnung aus Kanten)
|
||||
- [ ] Rechte Seitenleiste: laufende Scans live
|
||||
|
||||
### Phase 3 — Ändern (der Kern-Kreislauf)
|
||||
- [ ] Chatbox: Beschreibung → Soll-Diff-Vorschau in den Artefakten → bestätigen
|
||||
- [ ] Plan-Rolle: Zerlegung in Aufgaben entlang der Einheiten, Decision Records anhängen
|
||||
- [ ] Team-Board v1: Umsetzer → mechanische Gates → 1 Verifizierer; sequenziell; 2-Runden-Deckel
|
||||
- [ ] Eskalation v1: Karte pausiert, Antwort im Chat, wird Decision Record
|
||||
- [ ] Integrations-Verifikation → Artefakt-Sync → Commit durch den Agenten
|
||||
- [ ] Fake-E2E: kompletter Änderungs-Kreislauf (Soll-Diff → Board → Sync → Commit) ohne
|
||||
LLM in Sekunden, an einem Test-Repo
|
||||
- [ ] Executor-Rolle delegiert (Claude Code headless), Schnittstelle: Auftrag + Kontext rein, Diff raus;
|
||||
arbeitet immer im Worktree/Branch (Selbst-Änderungs-Regel)
|
||||
- [ ] Dogfooding: Änderungen am Planer laufen ab jetzt durch den Planer selbst
|
||||
|
||||
### Phase 4 — Orchestrieren
|
||||
- [ ] Startseite zum Schreibtisch ausbauen: Entscheidungsliste, laufende Aufträge
|
||||
- [ ] Entscheidungsreife Eskalationen (Optionen + Empfehlung, Klick-Entscheidung)
|
||||
- [ ] Kosten-Dashboard (Tokens je Rolle/Projekt/Woche)
|
||||
- [ ] Parallele Aufgaben über disjunkte Einheiten
|
||||
|
||||
### Phase 5 — Strategie & Jour Fixe
|
||||
- [ ] Strategie-Modus der Chat-Komponente: benannte Entwürfe, Live-Feature-Baum, Roadmap-Abfluss
|
||||
- [ ] Jour Fixe: datenbasierte Agenda aus Telemetrie, Protokoll, Ideen-Backlog
|
||||
- [ ] LLM-Stil-Filter gezielt nachrüsten (nur was die Telemetrie als Lücke zeigt)
|
||||
|
||||
### Phase 6 — Politur
|
||||
- [ ] Spiel-Skin (Personas, Budget, Benennungen; umschaltbar)
|
||||
- [ ] Projektübergreifender Jour Fixe (Muster über alle Projekte)
|
||||
- [ ] Lokale LLMs als Provider (wenn gebraucht)
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen (bei Phase 0/1 klären)
|
||||
|
||||
- Genaues Dateiformat der Einheiten/Sichten (Lesbarkeit vs. maschinelle Diff-Freundlichkeit)
|
||||
- Rename/Move-Matching: wie viel Aufwand lohnt v1?
|
||||
- Wie viel Kontext bekommt der Umsetzer (nur Aufgabe + Einheiten, oder mehr)?
|
||||
@@ -0,0 +1,8 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent)) # `python -m backend` von der Wurzel
|
||||
|
||||
from cli import main # noqa: E402
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,564 @@
|
||||
"""Provider-Layer: LLM-Calls über Claude-CLI, OpenCode-CLI oder direkte MiniMax-API.
|
||||
|
||||
Port aus creator/backend/agents.py mit zwei bewussten Abweichungen:
|
||||
- KEIN Provider-Fallback: resolve_role(role) kommt vollständig aus der .env; ist eine
|
||||
Rolle unkonfiguriert oder der Provider nicht nutzbar, wird der Lauf pausiert
|
||||
(AgentInfraError) — nie still gewechselt (Leitprinzip 2).
|
||||
- Leere-Ausgabe-Triage (Phase-0-Erkenntnis 13): leerer Text bei rc=0 ist ein Fehler.
|
||||
OpenCode-Pfad: keine Session in der OpenCode-DB → Netz/Provider → AgentInfraError
|
||||
(Pause); Session vorhanden → Retry-Fehler (Thinking-Overrun/Stall).
|
||||
|
||||
Der Event-Sink `on_event` wird von außen injiziert (CLI setzt database.add_event) —
|
||||
dieses Modul bleibt DB-frei.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import heapq
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from config import (MAX_CONCURRENT_AGENTS, MAX_CONCURRENT_AGENTS_PER_TOPIC,
|
||||
MAX_CONCURRENT_API_AGENTS, OPENCODE_CONFIG, PROVIDERS,
|
||||
RAM_MIN_FREE_PCT, resolve_role)
|
||||
|
||||
log = logging.getLogger("planer.agents")
|
||||
|
||||
|
||||
class AgentInfraError(Exception):
|
||||
"""Infrastruktur nicht nutzbar (Netz/Provider down, Rolle unkonfiguriert). KEIN
|
||||
inhaltliches Urteil — der Aufrufer pausiert den Lauf, nie fail-open."""
|
||||
|
||||
|
||||
_active_processes: dict[str, asyncio.subprocess.Process] = {}
|
||||
_active_api: set[str] = set() # laufende direkte API-Calls (kein Prozess zu killen)
|
||||
_active_started: dict[str, float] = {} # agent_key → Wandzeit-Start (Live-Anzeige)
|
||||
_active_labels: dict[str, str] = {}
|
||||
|
||||
|
||||
def active_agents(scope_prefix: str | None = None) -> list[dict]:
|
||||
"""Laufende Agenten mit Laufzeit, längste zuerst. → [{key, label, runtime}]"""
|
||||
now = time.time()
|
||||
out = [{"key": k, "label": _active_labels.get(k, ""), "runtime": round(now - t, 1)}
|
||||
for k, t in list(_active_started.items())
|
||||
if (k in _active_processes or k in _active_api)
|
||||
and (not scope_prefix or k.startswith(scope_prefix))]
|
||||
return sorted(out, key=lambda a: -a["runtime"])
|
||||
|
||||
|
||||
# --- Abbruch (Prefix-basiert) --------------------------------------------------------
|
||||
_cancelled_prefixes: set[str] = set()
|
||||
|
||||
|
||||
def cancel_scope(prefix: str) -> None:
|
||||
_cancelled_prefixes.add(prefix)
|
||||
|
||||
|
||||
def clear_scope(prefix: str) -> None:
|
||||
_cancelled_prefixes.discard(prefix)
|
||||
|
||||
|
||||
def _scope_cancelled(agent_key: str) -> bool:
|
||||
return any(agent_key.startswith(p) for p in _cancelled_prefixes)
|
||||
|
||||
|
||||
class _PrioritySemaphore:
|
||||
"""Semaphore-Variante: bei knappen Slots gewinnt die NIEDRIGSTE Prioritätszahl
|
||||
(FIFO innerhalb gleicher Priorität) — frühe Board-Spalten vor späten."""
|
||||
|
||||
def __init__(self, value: int):
|
||||
self._value = value
|
||||
self._waiters: list = [] # Heap aus [priority, seq, future]
|
||||
self._seq = 0
|
||||
|
||||
async def acquire(self, priority: int = 100):
|
||||
if self._value > 0:
|
||||
self._value -= 1
|
||||
return
|
||||
fut = asyncio.get_event_loop().create_future()
|
||||
entry = [priority, self._seq, fut]
|
||||
self._seq += 1
|
||||
heapq.heappush(self._waiters, entry)
|
||||
try:
|
||||
await fut # release() reicht den Slot direkt weiter (kein value-Increment)
|
||||
except BaseException:
|
||||
entry[2] = None # Tombstone: release() überspringt tote Waiter
|
||||
if fut.done() and not fut.cancelled():
|
||||
self.release() # Slot kam exakt vor dem Cancel → weitergeben
|
||||
raise
|
||||
|
||||
def release(self):
|
||||
while self._waiters:
|
||||
entry = heapq.heappop(self._waiters)
|
||||
if entry[2] is not None and not entry[2].done():
|
||||
entry[2].set_result(None)
|
||||
return
|
||||
self._value += 1
|
||||
|
||||
|
||||
_batch_sem = _PrioritySemaphore(MAX_CONCURRENT_AGENTS) # Prozess-Tier (~310 MB RSS)
|
||||
_batch_sem_api = _PrioritySemaphore(MAX_CONCURRENT_API_AGENTS) # API-Tier (~0 RAM)
|
||||
_topic_sems: dict[str, _PrioritySemaphore] = {}
|
||||
|
||||
# Kleinerer Index = höhere Priorität: Karten fertig machen statt neues WIP öffnen.
|
||||
_STAGE_PRIORITY = ("sichten", "judge", "nachfass", "schneiden")
|
||||
|
||||
|
||||
def _agent_priority(key: str) -> int:
|
||||
for i, tag in enumerate(_STAGE_PRIORITY):
|
||||
if f"-{tag}-" in key or key.endswith(f"-{tag}"):
|
||||
return i
|
||||
return len(_STAGE_PRIORITY)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _batch_gate(scope: str | None, priority: int, api: bool = False):
|
||||
"""Erst der Per-Topic-Slot (fair), dann der GLOBALE Slot nach Priorität. Ein Waiter
|
||||
hält beim globalen Anstehen nur seinen Topic-Slot. Topic-Cap gilt über beide Tiers."""
|
||||
topic_sem = _topic_sems.setdefault(scope, _PrioritySemaphore(MAX_CONCURRENT_AGENTS_PER_TOPIC)) if scope else None
|
||||
global_sem = _batch_sem_api if api else _batch_sem
|
||||
if topic_sem is not None:
|
||||
await topic_sem.acquire(priority)
|
||||
await global_sem.acquire(priority)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
global_sem.release()
|
||||
if topic_sem is not None:
|
||||
topic_sem.release()
|
||||
|
||||
|
||||
# Stagger für OpenCode-Starts: gleichzeitige Kaltstarts kollidieren auf der internen
|
||||
# Session-DB (Phase-0-Erkenntnis 13a: alle hängen, keine Session entsteht). Token-Bucket:
|
||||
# der Lock vergibt nur den Start-Slot, geschlafen wird außerhalb.
|
||||
_opencode_start_lock = asyncio.Lock()
|
||||
_OPENCODE_START_DELAY = float(os.getenv("OPENCODE_START_DELAY", "0.5"))
|
||||
_opencode_next_start = 0.0
|
||||
|
||||
|
||||
async def _opencode_slot() -> None:
|
||||
global _opencode_next_start
|
||||
loop = asyncio.get_running_loop()
|
||||
async with _opencode_start_lock:
|
||||
now = loop.time()
|
||||
start_at = max(now, _opencode_next_start)
|
||||
_opencode_next_start = start_at + _OPENCODE_START_DELAY
|
||||
await asyncio.sleep(max(0.0, start_at - now))
|
||||
|
||||
|
||||
# --- RAM-Gate: unter RAM_MIN_FREE_PCT freiem Speicher warten neue CLI-Prozesse ---------
|
||||
_RAM_GATE_FLOOR = 2 # unter so vielen Läufen: immer zulassen (Deadlock-Schutz)
|
||||
_RAM_PER_PROC_KB = 350 * 1024
|
||||
_RAM_COMMIT_WINDOW_S = 10.0 # frische Zulassungen zählen als schon verbrauchtes RAM
|
||||
_RAM_POLL_S = 2.0
|
||||
_cli_running = 0
|
||||
_cli_recent_starts: list[tuple[float, int]] = []
|
||||
|
||||
|
||||
def _meminfo() -> tuple[int, int] | None:
|
||||
try:
|
||||
text = Path("/proc/meminfo").read_text()
|
||||
except OSError:
|
||||
return None
|
||||
m = {k: v for k, v in re.findall(r"^(MemTotal|MemAvailable):\s+(\d+)", text, re.MULTILINE)}
|
||||
if "MemTotal" not in m or "MemAvailable" not in m:
|
||||
return None
|
||||
return int(m["MemAvailable"]), int(m["MemTotal"])
|
||||
|
||||
|
||||
async def _ram_gate(agent_key: str, est_kb: int) -> bool:
|
||||
"""True = Start zugelassen (Commit registriert), False = Scope während des Wartens
|
||||
abgebrochen. Check und Commit-Append im selben synchronen Block (kein await dazwischen)."""
|
||||
if RAM_MIN_FREE_PCT <= 0:
|
||||
return True
|
||||
waited = False
|
||||
while True:
|
||||
if _scope_cancelled(agent_key):
|
||||
return False
|
||||
mem = _meminfo()
|
||||
if mem is None or _cli_running < _RAM_GATE_FLOOR:
|
||||
break
|
||||
avail_kb, total_kb = mem
|
||||
now = time.monotonic()
|
||||
_cli_recent_starts[:] = [(t, kb) for t, kb in _cli_recent_starts
|
||||
if now - t < _RAM_COMMIT_WINDOW_S]
|
||||
committed_kb = sum(kb for _, kb in _cli_recent_starts)
|
||||
if avail_kb - committed_kb >= total_kb * RAM_MIN_FREE_PCT / 100:
|
||||
break
|
||||
if not waited:
|
||||
log.info("agent %s: RAM-Gate wartet (%.0f%% frei)", agent_key, avail_kb * 100 / total_kb)
|
||||
waited = True
|
||||
await asyncio.sleep(_RAM_POLL_S)
|
||||
_cli_recent_starts.append((time.monotonic(), est_kb))
|
||||
return True
|
||||
|
||||
|
||||
# Capability → Claude --allowedTools / OpenCode-Agent
|
||||
_CLAUDE_TOOLS = {"full": "Write,Bash,Read,WebSearch,WebFetch", "files": "Read,Bash,Write",
|
||||
"read": "Read", "none": None}
|
||||
_OPENCODE_AGENTS = {"full": "full", "files": "files", "read": "readonly", "none": "text"}
|
||||
|
||||
|
||||
def _use_text_api(provider: str, model: str, capabilities: str, on_line) -> bool:
|
||||
"""Direkter API-Pfad nur für tool-lose, nicht-streamende MiniMax-Calls — spart den
|
||||
OpenCode-Prozess (und dessen Stall-/Kaltstart-Probleme). PLANER_TEXT_API=0 = aus."""
|
||||
return (capabilities == "none" and on_line is None
|
||||
and PROVIDERS[provider]["cli"] == "opencode"
|
||||
and model.split("/", 1)[0] in ("minimax", "minimax-kalt")
|
||||
and bool(os.environ.get("MINIMAX_API_KEY"))
|
||||
and os.getenv("PLANER_TEXT_API", "1") != "0")
|
||||
|
||||
|
||||
def provider_available(provider: str) -> bool:
|
||||
cfg = PROVIDERS.get(provider)
|
||||
if not cfg:
|
||||
return False
|
||||
if shutil.which(cfg["cli"]) is None:
|
||||
return False
|
||||
env_key = cfg.get("env_key")
|
||||
if env_key and not os.environ.get(env_key):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _kill(process) -> None:
|
||||
"""Agent + Kindprozesse über die Prozessgruppe killen (Kinder halten sonst die
|
||||
Pipes offen und blockieren communicate())."""
|
||||
try:
|
||||
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
try:
|
||||
process.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
|
||||
def kill_process(agent_key_prefix: str) -> None:
|
||||
for key, process in list(_active_processes.items()):
|
||||
if process.returncode is not None:
|
||||
_active_processes.pop(key, None)
|
||||
_active_started.pop(key, None)
|
||||
continue
|
||||
if key.startswith(agent_key_prefix):
|
||||
log.debug("kill agent %s", key)
|
||||
_kill(process)
|
||||
|
||||
|
||||
# Event-Sink (CLI injiziert database.add_event). Fire-and-forget je fertigem Agenten.
|
||||
on_event = None
|
||||
|
||||
|
||||
async def run_agent(
|
||||
agent_key: str,
|
||||
prompt: str,
|
||||
timeout: int,
|
||||
role: str,
|
||||
capabilities: str = "none",
|
||||
scope: str | None = None,
|
||||
on_line=None,
|
||||
label: str = "",
|
||||
) -> tuple[int, str, str]:
|
||||
"""Ein LLM-Call der Rolle `role`. → (rc, stdout, stderr). Wirft AgentInfraError,
|
||||
wenn die Infrastruktur nicht nutzbar ist (Rolle unkonfiguriert, Provider down,
|
||||
leere Antwort ohne Session) — der Lauf soll dann pausieren, nicht raten."""
|
||||
if os.getenv("PLANER_FAKE_AGENTS"): # Sekunden-Smoke: deterministisch statt LLM
|
||||
import fake_agents
|
||||
start = time.monotonic()
|
||||
res = await fake_agents.respond(agent_key, prompt, capabilities)
|
||||
if on_event is not None and scope is not None: # Telemetrie auch im Fake-Pfad testbar
|
||||
await on_event(topic=scope, kind="agent", key=agent_key, label=label,
|
||||
status="ok" if res[0] == 0 else "error",
|
||||
dur_ms=int((time.monotonic() - start) * 1000), wait_ms=0,
|
||||
meta={"provider": "fake", "model": "fake", "role": role, "rc": res[0]})
|
||||
return res
|
||||
if _scope_cancelled(agent_key):
|
||||
return 1, "", "cancelled"
|
||||
try:
|
||||
provider, model = resolve_role(role) # NUR aus .env — kein Default, kein Fallback
|
||||
except RuntimeError as e:
|
||||
raise AgentInfraError(str(e)) from e
|
||||
use_api = _use_text_api(provider, model, capabilities, on_line)
|
||||
if not use_api and shutil.which(PROVIDERS[provider]["cli"]) is None:
|
||||
raise AgentInfraError(f"CLI '{PROVIDERS[provider]['cli']}' nicht installiert (Provider {provider})")
|
||||
queued = time.monotonic()
|
||||
async with _batch_gate(scope, _agent_priority(agent_key), api=use_api):
|
||||
if _scope_cancelled(agent_key):
|
||||
return 1, "", "cancelled"
|
||||
wait_ms = int((time.monotonic() - queued) * 1000)
|
||||
start = time.monotonic()
|
||||
status = "error"
|
||||
rc = None
|
||||
err_tail = ""
|
||||
api_tokens = None
|
||||
try:
|
||||
log.info("agent %s: %s %s (Rolle %s)", agent_key, provider, model, role)
|
||||
if use_api:
|
||||
rc_, out_, err_, api_tokens = await _run_text_api(agent_key, prompt, timeout, model, label=label)
|
||||
res = (rc_, out_, err_)
|
||||
elif PROVIDERS[provider]["cli"] == "opencode":
|
||||
res = await _run_opencode(agent_key, prompt, timeout, model, capabilities,
|
||||
on_line=on_line, label=label)
|
||||
res = await _leere_ausgabe_triage(agent_key, res, on_line)
|
||||
else:
|
||||
res = await _run_claude_cli(agent_key, prompt, timeout, model, capabilities, label=label)
|
||||
rc = res[0]
|
||||
status = "ok" if rc == 0 else ("killed" if rc is not None and rc < 0 else "error")
|
||||
if rc not in (0, None) and rc >= 0:
|
||||
err_tail = (res[2] or res[1] or "").strip()[-300:]
|
||||
return res
|
||||
except asyncio.TimeoutError:
|
||||
status = "timeout"
|
||||
raise
|
||||
except asyncio.CancelledError:
|
||||
status = "cancelled"
|
||||
raise
|
||||
except AgentInfraError:
|
||||
status = "infra"
|
||||
raise
|
||||
finally:
|
||||
if on_event is not None and scope is not None: # nie fatal
|
||||
try:
|
||||
meta = {"provider": provider, "model": model, "role": role, "rc": rc}
|
||||
if err_tail:
|
||||
meta["stderr"] = err_tail
|
||||
if api_tokens:
|
||||
meta["tokens"] = api_tokens
|
||||
elif PROVIDERS[provider]["cli"] == "opencode":
|
||||
if (tok := await asyncio.to_thread(_session_tokens, agent_key)):
|
||||
meta["tokens"] = tok
|
||||
await on_event(topic=scope, kind="agent", key=agent_key, label=label,
|
||||
status=status, dur_ms=int((time.monotonic() - start) * 1000),
|
||||
wait_ms=wait_ms, meta=meta)
|
||||
except Exception:
|
||||
log.debug("on_event failed", exc_info=True)
|
||||
|
||||
|
||||
async def _leere_ausgabe_triage(agent_key: str, res: tuple[int, str, str], on_line) -> tuple[int, str, str]:
|
||||
"""Erkenntnis 13: leerer Text bei rc=0 ist nie ok. Keine OpenCode-Session zum Key →
|
||||
der Call kam nie beim Provider an (Netz) → AgentInfraError (Lauf pausiert).
|
||||
Session vorhanden → Thinking-Overrun/Stall → normaler Fehler (Engine-Retry)."""
|
||||
rc, out, err = res
|
||||
if rc != 0 or out.strip() or on_line is not None:
|
||||
return res
|
||||
tok = await asyncio.to_thread(_session_tokens, agent_key)
|
||||
if tok is None:
|
||||
raise AgentInfraError(f"agent {agent_key}: leere Ausgabe ohne OpenCode-Session — Provider/Netz nicht erreichbar")
|
||||
return 1, "", f"leere Ausgabe (Session vorhanden, output-tokens={tok.get('output')}) — Overrun/Stall"
|
||||
|
||||
|
||||
async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None, timeout: int,
|
||||
stagger: bool = False, on_line=None, label: str = "",
|
||||
env: dict | None = None, est_kb: int = _RAM_PER_PROC_KB) -> tuple[int, str, str]:
|
||||
start = time.monotonic()
|
||||
|
||||
async def spawn():
|
||||
return await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=asyncio.subprocess.PIPE if stdin_data is not None else asyncio.subprocess.DEVNULL,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
start_new_session=True, # eigene Prozessgruppe → killpg trifft auch Kinder
|
||||
env=env,
|
||||
)
|
||||
|
||||
global _cli_running
|
||||
if stagger:
|
||||
if not await _ram_gate(agent_key, est_kb):
|
||||
return 1, "", "cancelled"
|
||||
await _opencode_slot() # Gate VOR dem Start-Slot: Zulassungswelle wird trotzdem gespaced
|
||||
process = await spawn()
|
||||
if stagger:
|
||||
_cli_running += 1
|
||||
# Kollisionssicheres Tracking: identische Keys bekommen ~n-Suffix (Prefix-Kill matcht weiter)
|
||||
track_key = agent_key
|
||||
n = 2
|
||||
while track_key in _active_processes:
|
||||
track_key = f"{agent_key}~{n}"
|
||||
n += 1
|
||||
_active_processes[track_key] = process
|
||||
_active_started[track_key] = time.time()
|
||||
_active_labels[track_key] = label
|
||||
try:
|
||||
try:
|
||||
if on_line is not None:
|
||||
out_chunks: list[str] = []
|
||||
|
||||
async def _pump():
|
||||
async for raw in process.stdout:
|
||||
s = raw.decode("utf-8", errors="replace")
|
||||
out_chunks.append(s)
|
||||
try:
|
||||
on_line(s)
|
||||
except Exception:
|
||||
log.debug("on_line callback failed", exc_info=True)
|
||||
await asyncio.wait_for(_pump(), timeout=timeout)
|
||||
await process.wait()
|
||||
stderr_b = await process.stderr.read()
|
||||
stdout, stderr = "".join(out_chunks).encode("utf-8"), stderr_b
|
||||
else:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(input=stdin_data),
|
||||
timeout=timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
_kill(process)
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
log.info("agent %s: timeout after %ds", agent_key, timeout)
|
||||
raise
|
||||
log.info("agent %s: exit %s after %.1fs (%d bytes stdout)",
|
||||
agent_key, process.returncode, time.monotonic() - start, len(stdout))
|
||||
return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
|
||||
finally:
|
||||
if stagger:
|
||||
_cli_running -= 1
|
||||
if _active_processes.get(track_key) is process:
|
||||
del _active_processes[track_key]
|
||||
_active_started.pop(track_key, None)
|
||||
_active_labels.pop(track_key, None)
|
||||
|
||||
|
||||
async def _run_claude_cli(agent_key: str, prompt: str, timeout: int, model: str,
|
||||
capabilities: str, label: str = "") -> tuple[int, str, str]:
|
||||
cmd = ["claude", "-p", "--model", model]
|
||||
tools = _CLAUDE_TOOLS.get(capabilities)
|
||||
if tools:
|
||||
cmd += ["--allowedTools", tools]
|
||||
cmd += ["--dangerously-skip-permissions"]
|
||||
return await _communicate(agent_key, cmd, prompt.encode("utf-8"), timeout, stagger=True, label=label)
|
||||
|
||||
|
||||
_OPENCODE_DB = Path.home() / ".local" / "share" / "opencode" / "opencode.db"
|
||||
|
||||
|
||||
def _session_tokens(agent_key: str) -> dict | None:
|
||||
"""Token-Zähler der neuesten OpenCode-Session mit title=agent_key (via run --title).
|
||||
Best-effort, read-only, nie fatal — None wenn DB/Zeile fehlt. Auch Timeouts haben
|
||||
Tokens verbraucht; die Verschwendung soll sichtbar bleiben."""
|
||||
try:
|
||||
con = sqlite3.connect(f"file:{_OPENCODE_DB}?mode=ro", uri=True, timeout=1)
|
||||
try:
|
||||
row = con.execute(
|
||||
"SELECT tokens_input, tokens_output, tokens_reasoning,"
|
||||
" tokens_cache_read, tokens_cache_write"
|
||||
" FROM session WHERE title=? ORDER BY time_created DESC LIMIT 1",
|
||||
(agent_key,)).fetchone()
|
||||
finally:
|
||||
con.close()
|
||||
except Exception:
|
||||
return None
|
||||
if row is None:
|
||||
return None
|
||||
keys = ("input", "output", "reasoning", "cache_read", "cache_write")
|
||||
return {k: int(v or 0) for k, v in zip(keys, row)}
|
||||
|
||||
|
||||
async def _run_opencode(agent_key: str, prompt: str, timeout: int, model: str, capabilities: str,
|
||||
on_line=None, label: str = "") -> tuple[int, str, str]:
|
||||
# Prompt per Temp-Datei statt argv (ARG_MAX-Schutz bei großen Chunks)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f:
|
||||
f.write(prompt)
|
||||
prompt_path = Path(f.name)
|
||||
# Die positionale Message MUSS vor -f stehen (-f ist ein Array-Flag).
|
||||
cmd = [
|
||||
"opencode", "run",
|
||||
"Folge exakt den Anweisungen in der angehängten Datei. Sie sind der vollständige Auftrag.",
|
||||
"-m", model,
|
||||
"--agent", _OPENCODE_AGENTS.get(capabilities, "text"),
|
||||
"--title", agent_key, # Token-Accounting: verbindet die OpenCode-Session mit unserem Event
|
||||
"-f", str(prompt_path),
|
||||
]
|
||||
if on_line is not None:
|
||||
cmd += ["--format", "json"]
|
||||
env = {**os.environ, "OPENCODE_CONFIG": str(OPENCODE_CONFIG)}
|
||||
try:
|
||||
rc, stdout, stderr = await _communicate(agent_key, cmd, None, timeout, stagger=True,
|
||||
on_line=on_line, label=label, env=env)
|
||||
return rc, (stdout if on_line is not None else _clean_opencode_output(stdout)), stderr
|
||||
finally:
|
||||
prompt_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
# --- Direkter API-Pfad (MiniMax, Anthropic-Messages-Format) ---------------------------
|
||||
_API_URL = "https://api.minimax.io/anthropic/v1/messages"
|
||||
_API_VERSION = "2023-06-01"
|
||||
_API_MAX_TOKENS = 32_000
|
||||
|
||||
|
||||
def _api_model_opts(model: str) -> dict:
|
||||
"""kalt-Route = niedrige Temperatur ohne Thinking; nativ = Endpoint-Defaults.
|
||||
Bewusst prefix-basiert statt Modell-Tabelle — Modell-IDs gehören nicht in den Code."""
|
||||
if model.startswith("minimax-kalt/"):
|
||||
return {"temperature": 0.3, "thinking": {"type": "disabled"}}
|
||||
return {}
|
||||
|
||||
|
||||
async def _run_text_api(agent_key: str, prompt: str, timeout: int, model: str,
|
||||
label: str = "") -> tuple[int, str, str, dict | None]:
|
||||
"""→ (rc, text, err, tokens). Tokens direkt aus der Response-usage, auch bei rc!=0 —
|
||||
Verschwendung bleibt sichtbar. Leere Antwort und max_tokens-Abbruch sind Fehler."""
|
||||
body = {
|
||||
"model": model.split("/", 1)[1], # ohne OpenCode-Provider-Prefix
|
||||
"max_tokens": _API_MAX_TOKENS,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
**_api_model_opts(model),
|
||||
}
|
||||
headers = {"x-api-key": os.environ.get("MINIMAX_API_KEY", ""), "anthropic-version": _API_VERSION}
|
||||
track_key = agent_key
|
||||
n = 2
|
||||
while track_key in _active_api:
|
||||
track_key = f"{agent_key}~{n}"
|
||||
n += 1
|
||||
_active_api.add(track_key)
|
||||
_active_started[track_key] = time.time()
|
||||
_active_labels[track_key] = label
|
||||
start = time.monotonic()
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout, connect=30)) as client:
|
||||
resp = await asyncio.wait_for(
|
||||
client.post(_API_URL, json=body, headers=headers), timeout=timeout)
|
||||
except httpx.TimeoutException:
|
||||
raise asyncio.TimeoutError
|
||||
except httpx.HTTPError as e:
|
||||
return 1, "", f"{type(e).__name__}: {e}", None
|
||||
finally:
|
||||
_active_api.discard(track_key)
|
||||
_active_started.pop(track_key, None)
|
||||
_active_labels.pop(track_key, None)
|
||||
log.info("agent %s: api done after %.1fs", agent_key, time.monotonic() - start)
|
||||
if resp.status_code != 200:
|
||||
return 1, "", f"HTTP {resp.status_code}: {resp.text[:300]}", None
|
||||
data = resp.json()
|
||||
# Nur Text-Blöcke zählen — Thinking-Blöcke (native Route) werden übersprungen.
|
||||
text = "".join(b.get("text", "") for b in data.get("content", []) if b.get("type") == "text")
|
||||
u = data.get("usage") or {}
|
||||
tokens = {"input": int(u.get("input_tokens") or 0), "output": int(u.get("output_tokens") or 0),
|
||||
"reasoning": 0, "cache_read": int(u.get("cache_read_input_tokens") or 0),
|
||||
"cache_write": int(u.get("cache_creation_input_tokens") or 0)}
|
||||
if not text.strip():
|
||||
return 1, "", f"leere Antwort (stop_reason={data.get('stop_reason')})", tokens
|
||||
err = "stop_reason=max_tokens (abgeschnitten)" if data.get("stop_reason") == "max_tokens" else ""
|
||||
return 0, text, err, tokens
|
||||
|
||||
|
||||
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
||||
|
||||
|
||||
def _clean_opencode_output(text: str) -> str:
|
||||
"""ANSI-Codes und den führenden Banner ("> agent · model") entfernen."""
|
||||
text = _ANSI_RE.sub("", text)
|
||||
lines = text.splitlines()
|
||||
while lines and (not lines[0].strip() or lines[0].lstrip().startswith(">")):
|
||||
lines.pop(0)
|
||||
return "\n".join(lines).strip()
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Artefakt-Schicht: Scan-Fragmente je Quelldatei zusammensetzen und als
|
||||
`.planer/`-Dateien ins Ziel-Repo schreiben. Merge arbeitet auf den ROHEN
|
||||
Markdown-Blöcken (kein Grammatik-Roundtrip — nichts geht verloren).
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
_ERGAENZE_RE = re.compile(r"^ergaenze-anker:\s*(\S+)\s+zu\s+(\S+)\s*$", re.M)
|
||||
|
||||
|
||||
def datei_pfad(datei_rel: str) -> str:
|
||||
"""Quelldatei → Pfad der Einheiten-Datei unter .planer/ (`/` → `__`).
|
||||
Bekannte v1-Schwäche: Quellnamen mit `__` können kollidieren."""
|
||||
return "einheiten/" + datei_rel.replace("/", "__") + ".md"
|
||||
|
||||
|
||||
def _bloecke(text: str) -> dict[str, str]:
|
||||
"""Fragment → {einheiten_id: roher_block}. Blöcke beginnen bei `## <id>`."""
|
||||
out: dict[str, str] = {}
|
||||
aktuelle_id = None
|
||||
zeilen: list[str] = []
|
||||
for zeile in text.splitlines():
|
||||
if zeile.startswith("## ") and ("::" in zeile or re.fullmatch(r"## \S+\.\w+", zeile.strip())):
|
||||
if aktuelle_id is not None:
|
||||
out[aktuelle_id] = "\n".join(zeilen).rstrip()
|
||||
aktuelle_id = zeile[3:].strip()
|
||||
zeilen = [zeile]
|
||||
elif aktuelle_id is not None and not _ERGAENZE_RE.match(zeile):
|
||||
zeilen.append(zeile)
|
||||
if aktuelle_id is not None:
|
||||
out[aktuelle_id] = "\n".join(zeilen).rstrip()
|
||||
return out
|
||||
|
||||
|
||||
def merge_fragmente(fragmente: list[str]) -> str:
|
||||
"""Fragmente EINER Quelldatei → Rumpf der Einheiten-Datei. Einheiten in Reihenfolge
|
||||
des ersten Auftretens, Dubletten (gleiche ID) fallen weg; `ergaenze-anker: X zu Y`
|
||||
aus Nachfass-Antworten wird als `anker: X` in den Block von Y eingefügt."""
|
||||
bloecke: dict[str, str] = {}
|
||||
ergaenzungen: list[tuple[str, str]] = []
|
||||
for frag in fragmente:
|
||||
ergaenzungen += _ERGAENZE_RE.findall(frag)
|
||||
for eid, block in _bloecke(frag).items():
|
||||
bloecke.setdefault(eid, block)
|
||||
for anker_id, ziel_id in ergaenzungen:
|
||||
block = bloecke.get(ziel_id)
|
||||
if block is None:
|
||||
continue # Ziel existiert nicht — Gate hat das bereits als Fehler gemeldet
|
||||
kopf, _, rest = block.partition("\n")
|
||||
if f"anker: {anker_id}" not in block:
|
||||
bloecke[ziel_id] = f"{kopf}\nanker: {anker_id}\n{rest}"
|
||||
return "\n\n".join(bloecke.values())
|
||||
|
||||
|
||||
def einheiten_datei(datei_rel: str, rumpf: str, stand: str) -> str:
|
||||
return f"# Einheiten: {datei_rel}\nstand: {stand}\n\n{rumpf}\n"
|
||||
|
||||
|
||||
def lese_stand(text: str) -> str | None:
|
||||
m = re.search(r"^stand:\s*(\S+)", text, re.M)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def alle_ids(text: str) -> set[str]:
|
||||
"""Alle Einheiten-IDs + Zusatz-Anker einer Einheiten-Datei (für Referenz-Gates)."""
|
||||
ids = {m.group(1) for m in re.finditer(r"^## (\S+)$", text, re.M)}
|
||||
ids |= {m.group(1) for m in re.finditer(r"^anker:\s*(\S+)\s*$", text, re.M)}
|
||||
return ids
|
||||
|
||||
|
||||
def schreibe_planer(ziel_repo: Path, dateien: dict[str, str]) -> list[Path]:
|
||||
"""Schreibt {relativer_pfad_unter_.planer: inhalt} ins Ziel-Repo. → geschriebene Pfade."""
|
||||
geschrieben = []
|
||||
for rel, inhalt in sorted(dateien.items()):
|
||||
pfad = ziel_repo / ".planer" / rel
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_text(inhalt, encoding="utf-8")
|
||||
geschrieben.append(pfad)
|
||||
return geschrieben
|
||||
|
||||
|
||||
def pruefung_geruest(wurzel: Path, projekt: str) -> str:
|
||||
"""pruefung.md-Gerüst mit einfacher Erkennung (Makefile-Targets, pytest).
|
||||
Fehlt etwas, steht dort explizit '(keins vorhanden)' — Gates degradieren sichtbar."""
|
||||
tests = lint = start = "(keins vorhanden)"
|
||||
makefile = wurzel / "Makefile"
|
||||
if makefile.is_file():
|
||||
targets = set(re.findall(r"^([\w-]+):", makefile.read_text(encoding="utf-8"), re.M))
|
||||
if "test" in targets:
|
||||
tests = "make test"
|
||||
if "lint" in targets:
|
||||
lint = "make lint"
|
||||
for kandidat in ("dev", "run", "start"):
|
||||
if kandidat in targets:
|
||||
start = f"make {kandidat}"
|
||||
break
|
||||
elif list(wurzel.rglob("pytest.ini")) or list(wurzel.rglob("conftest.py")):
|
||||
tests = "pytest"
|
||||
return (f"# Prüfung: {projekt}\n"
|
||||
f"tests: {tests}\nlint: {lint}\nstart: {start}\n"
|
||||
f"hinweise:\n- (vom Scan erzeugtes Gerüst — bitte prüfen und ergänzen)\n")
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Chunker: zerlegt ein Repo mechanisch (kein LLM) in kartengroße Scan-Aufträge.
|
||||
|
||||
Phase-0-Befund: dateigroße LLM-Calls dauern Minuten und große Dateien stallen —
|
||||
deshalb Chunks von ~CHUNK_ZEILEN Zeilen, geschnitten ENTLANG von Top-Level-Symbolen
|
||||
(nie mitten durch eine Funktion). v1 scannt nur Python-Dateien.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
CHUNK_ZEILEN = 250 # Zielgröße; ein einzelnes Riesen-Symbol darf größer sein
|
||||
ENDUNGEN = {".py"} # v1: nur Python (Creator/Planer-Backends); Rest später
|
||||
|
||||
AUSSCHLUSS_DIRS = {".git", ".planer", "node_modules", "__pycache__", ".pytest_cache",
|
||||
"dist", "build", "storage", "venv", ".venv", ".idea", ".vscode",
|
||||
"tests"} # v1: Test-Code erzeugt in den Sichten nur Rauschen
|
||||
AUSSCHLUSS_DATEIEN = re.compile(r"^\.env") # Secrets: nie lesen (Leitprinzip 8)
|
||||
|
||||
_SYMBOL_RE = re.compile(r"^(?:async\s+)?(?:def|class)\s+(\w+)")
|
||||
|
||||
|
||||
def dateien(wurzel: Path) -> list[Path]:
|
||||
"""Scanbare Quelldateien unter `wurzel`, Ausschlussliste angewandt, sortiert."""
|
||||
out = []
|
||||
for p in sorted(wurzel.rglob("*")):
|
||||
if not p.is_file() or p.suffix not in ENDUNGEN:
|
||||
continue
|
||||
rel = p.relative_to(wurzel)
|
||||
if any(teil in AUSSCHLUSS_DIRS for teil in rel.parts[:-1]):
|
||||
continue
|
||||
if AUSSCHLUSS_DATEIEN.match(p.name):
|
||||
continue
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def symbol_zeilen(zeilen: list[str]) -> list[tuple[int, str]]:
|
||||
"""(zeilennummer_0basiert, symbolname) aller Top-Level-def/class, in Dateireihenfolge."""
|
||||
return [(i, m.group(1)) for i, z in enumerate(zeilen) if (m := _SYMBOL_RE.match(z))]
|
||||
|
||||
|
||||
def top_level_symbole(text: str) -> set[str]:
|
||||
"""Alle Top-Level-Symbole (def/class/Konstanten) — Abdeckungs-Referenz für Gates."""
|
||||
out = {name for _, name in symbol_zeilen(text.splitlines())}
|
||||
out |= {m.group(1) for m in re.finditer(r"^(\w+)\s*[:=]", text, re.M)}
|
||||
return out
|
||||
|
||||
|
||||
def _block_start(zeilen: list[str], symbol_zeile: int) -> int:
|
||||
"""Dekoratoren/Kommentare direkt über dem Symbol gehören zum Block."""
|
||||
i = symbol_zeile
|
||||
while i > 0 and (zeilen[i - 1].startswith("@") or zeilen[i - 1].lstrip().startswith("#")):
|
||||
i -= 1
|
||||
return i
|
||||
|
||||
|
||||
def schneide(datei_rel: str, text: str) -> list[dict]:
|
||||
"""Datei → Chunks entlang Top-Level-Symbolen, je ~CHUNK_ZEILEN Zeilen.
|
||||
Chunk: {datei, start, ende (exklusiv, 0-basiert), symbole, text, groesse}.
|
||||
Der Kopf vor dem ersten Symbol (Docstring/Imports/Konstanten) gehört zum ersten Chunk."""
|
||||
zeilen = text.splitlines()
|
||||
symbole = symbol_zeilen(zeilen)
|
||||
if not symbole: # Datei ohne def/class (Config, __init__): ein Chunk, Datei-Anker
|
||||
return [{"datei": datei_rel, "start": 0, "ende": len(zeilen),
|
||||
"symbole": [], "text": text, "groesse": len(zeilen)}] if zeilen else []
|
||||
|
||||
# Schnittpunkte: Blockanfänge der Symbole; Kopf hängt am ersten Block.
|
||||
grenzen = [( _block_start(zeilen, z), name) for z, name in symbole]
|
||||
chunks: list[dict] = []
|
||||
start = 0
|
||||
aktuelle_symbole: list[str] = []
|
||||
for idx, (block_start, name) in enumerate(grenzen):
|
||||
naechster = grenzen[idx + 1][0] if idx + 1 < len(grenzen) else len(zeilen)
|
||||
if aktuelle_symbole and (naechster - start) > CHUNK_ZEILEN:
|
||||
# Symbol beginnt neuen Chunk, sonst würde der aktuelle zu groß
|
||||
chunks.append(_chunk(datei_rel, zeilen, start, block_start, aktuelle_symbole))
|
||||
start = block_start
|
||||
aktuelle_symbole = []
|
||||
aktuelle_symbole.append(name)
|
||||
chunks.append(_chunk(datei_rel, zeilen, start, len(zeilen), aktuelle_symbole))
|
||||
return chunks
|
||||
|
||||
|
||||
def _chunk(datei: str, zeilen: list[str], start: int, ende: int, symbole: list[str]) -> dict:
|
||||
return {"datei": datei, "start": start, "ende": ende, "symbole": list(symbole),
|
||||
"text": "\n".join(zeilen[start:ende]), "groesse": ende - start}
|
||||
|
||||
|
||||
def chunks_fuer_repo(wurzel: Path, nur_dateien: set[str] | None = None) -> list[dict]:
|
||||
"""Alle Chunks des Repos; `nur_dateien` (relative Pfade) schränkt ein (Re-Scan)."""
|
||||
out = []
|
||||
for p in dateien(wurzel):
|
||||
rel = str(p.relative_to(wurzel))
|
||||
if nur_dateien is not None and rel not in nur_dateien:
|
||||
continue
|
||||
out.extend(schneide(rel, p.read_text(encoding="utf-8", errors="replace")))
|
||||
return out
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
"""CLI des Planers (Phase 1, kein Server):
|
||||
|
||||
python -m backend scan <pfad> Erst-Scan eines Projekt-Repos
|
||||
python -m backend rescan <pfad> Re-Scan über git-Diff gegen die Anker-Stände
|
||||
python -m backend status Karten-Stände aller bekannten Läufe
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent)) # `python -m backend` von der Wurzel
|
||||
|
||||
import agents # noqa: E402
|
||||
import database as db
|
||||
import scan_dienst
|
||||
|
||||
log = logging.getLogger("planer")
|
||||
|
||||
|
||||
def _abbruch(meldung: str) -> "NoReturn": # noqa: F821
|
||||
print(f"ABBRUCH: {meldung}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _vorflug(wurzel: Path) -> str:
|
||||
try:
|
||||
return scan_dienst.vorflug(wurzel)
|
||||
except scan_dienst.VorflugFehler as e:
|
||||
_abbruch(str(e))
|
||||
|
||||
|
||||
def _fortschritt(text: str) -> None:
|
||||
print(f" {text}", flush=True)
|
||||
|
||||
|
||||
def _bericht(ergebnis: dict) -> None:
|
||||
print(f"Status: {ergebnis['status']}")
|
||||
if ergebnis.get("infra_error"):
|
||||
print(f" Infra: {ergebnis['infra_error']}")
|
||||
for pfad in ergebnis.get("geschrieben", []):
|
||||
print(f" geschrieben: .planer/{pfad}")
|
||||
for card in ergebnis.get("dead", []):
|
||||
print(f" DEAD: {card} (requeue über erneuten Lauf oder DB)")
|
||||
if ergebnis["status"] == "ok":
|
||||
print("Artefakte aktualisiert — jetzt committen (Code + .planer/ atomar).")
|
||||
|
||||
|
||||
async def _scan(wurzel: Path, fortsetzen: bool = False) -> int:
|
||||
stand = _vorflug(wurzel)
|
||||
await db.init_db()
|
||||
agents.on_event = db.add_event
|
||||
import scan_board
|
||||
ergebnis = await scan_board.run_scan(wurzel, wurzel.name, stand, set_p=_fortschritt,
|
||||
fortsetzen=fortsetzen)
|
||||
_bericht(ergebnis)
|
||||
return 0 if ergebnis["status"] == "ok" else 1
|
||||
|
||||
|
||||
async def _rescan(wurzel: Path) -> int:
|
||||
stand_neu = _vorflug(wurzel)
|
||||
await db.init_db()
|
||||
agents.on_event = db.add_event
|
||||
try:
|
||||
ergebnis = await scan_dienst.rescan_lauf(wurzel, wurzel.name, stand_neu,
|
||||
set_p=_fortschritt)
|
||||
except scan_dienst.VorflugFehler as e:
|
||||
_abbruch(str(e))
|
||||
if ergebnis["status"] == "unveraendert":
|
||||
print("Keine Änderungen seit dem letzten Scan — nichts zu tun.")
|
||||
return 0
|
||||
_bericht(ergebnis)
|
||||
return 0 if ergebnis["status"] == "ok" else 1
|
||||
|
||||
|
||||
async def _status() -> None:
|
||||
await db.init_db()
|
||||
verbindung = await db.get_db()
|
||||
cursor = await verbindung.execute("SELECT DISTINCT topic FROM kanban_cards ORDER BY topic")
|
||||
topics = [r[0] for r in await cursor.fetchall()]
|
||||
if not topics:
|
||||
print("Keine Läufe in der Planer-DB.")
|
||||
return
|
||||
for topic in topics:
|
||||
counts = await db.kanban_stage_counts(topic)
|
||||
dead = await db.kanban_dead(topic)
|
||||
print(f"{topic}: " + ", ".join(f"{s}={n}" for b in counts.values() for s, n in b.items())
|
||||
+ (f" — DEAD: {[c['card_id'] for c in dead]}" if dead else ""))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
# Hänger-Diagnose (offener Befund: Prozess lebt nach Board-Ende weiter):
|
||||
# `kill -USR1 <pid>` schreibt alle Python-Stacks nach stderr.
|
||||
import faulthandler
|
||||
import signal as _signal
|
||||
faulthandler.register(_signal.SIGUSR1)
|
||||
parser = argparse.ArgumentParser(prog="planer")
|
||||
sub = parser.add_subparsers(dest="kommando", required=True)
|
||||
for name in ("scan", "rescan"):
|
||||
p = sub.add_parser(name)
|
||||
p.add_argument("pfad", type=Path)
|
||||
if name == "scan":
|
||||
p.add_argument("--fortsetzen", action="store_true",
|
||||
help="abgebrochenen Lauf fortsetzen (nur tote Chunks neu)")
|
||||
sub.add_parser("status")
|
||||
args = parser.parse_args()
|
||||
if args.kommando == "scan":
|
||||
sys.exit(asyncio.run(_scan(args.pfad.resolve(), fortsetzen=args.fortsetzen)))
|
||||
elif args.kommando == "rescan":
|
||||
sys.exit(asyncio.run(_rescan(args.pfad.resolve())))
|
||||
else:
|
||||
asyncio.run(_status())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Konfiguration: Provider-Stacks, Rollen-Auflösung, Engine-Konstanten.
|
||||
|
||||
Rollen-Modelle stehen AUSSCHLIESSLICH in der .env (ROLLE_<NAME>=provider:modell) —
|
||||
nirgendwo im Code ein Default oder Fallback. Unkonfigurierte Rolle = harter Stopp.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
||||
STORAGE_DIR = PROJECT_ROOT / "storage"
|
||||
DB_PATH = STORAGE_DIR / "planer.db"
|
||||
LAEUFE_DIR = STORAGE_DIR / "laeufe"
|
||||
OPENCODE_CONFIG = PROJECT_ROOT / "dev-ops" / "opencode-slim.json"
|
||||
PROJEKTE_DATEI = PROJECT_ROOT / "projekte.txt"
|
||||
|
||||
|
||||
def _load_env(path: Path) -> None:
|
||||
"""Mini .env-Loader (keine Dependency): KEY=VALUE-Zeilen. Die DATEI gewinnt über
|
||||
vererbte Env (Creator-Lehre: vererbte Werte pinnen sonst still veraltete Stände)."""
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key, value = key.strip(), value.strip().strip('"').strip("'")
|
||||
if key:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
_load_env(PROJECT_ROOT / ".env")
|
||||
|
||||
# --- Provider-Stacks: nur Aufrufweg + Auth, KEINE Modelle -------------------------
|
||||
PROVIDERS = {
|
||||
"claude": {"cli": "claude", "env_key": None}, # Auth via OAuth/~/.claude (Abo)
|
||||
"minimax": {"cli": "opencode", "env_key": "MINIMAX_API_KEY"},
|
||||
}
|
||||
|
||||
# LLM-Rollen des Scan-Boards. Jede braucht ROLLE_<NAME> in der .env.
|
||||
ROLLEN = ("schneiden", "nachfass", "sichten", "judge")
|
||||
|
||||
|
||||
def resolve_role(role: str) -> tuple[str, str]:
|
||||
"""→ (provider, modell) aus ROLLE_<NAME>=provider:modell. Fehlt die Konfiguration
|
||||
oder ist sie unbrauchbar: RuntimeError — NIE ein stiller Default (Leitprinzip 2)."""
|
||||
env_name = f"ROLLE_{role.upper()}"
|
||||
wert = os.getenv(env_name, "").strip()
|
||||
if not wert:
|
||||
raise RuntimeError(
|
||||
f"Rolle '{role}' hat kein Modell konfiguriert — {env_name}=provider:modell in .env setzen")
|
||||
provider, _, modell = wert.partition(":")
|
||||
if provider not in PROVIDERS:
|
||||
raise RuntimeError(
|
||||
f"{env_name}: unbekannter Provider '{provider}' (bekannt: {', '.join(PROVIDERS)})")
|
||||
if not modell:
|
||||
raise RuntimeError(f"{env_name}: Modell fehlt (Format provider:modell)")
|
||||
return provider, modell
|
||||
|
||||
|
||||
# --- Engine-Konstanten (Kanban + Agenten) -----------------------------------------
|
||||
KANBAN_BATCH = int(os.getenv("KANBAN_BATCH", "4")) # Karten je Paket
|
||||
MAX_CARD_RETRIES = int(os.getenv("MAX_CARD_RETRIES", "3")) # dann dead-letter
|
||||
RETRY_BACKOFF = float(os.getenv("RETRY_BACKOFF", "30")) # Basis-Sekunden, exponentiell
|
||||
MAX_CONCURRENT_AGENTS = int(os.getenv("MAX_CONCURRENT_AGENTS", "6"))
|
||||
MAX_CONCURRENT_AGENTS_PER_TOPIC = int(os.getenv("MAX_CONCURRENT_AGENTS_PER_TOPIC", "6"))
|
||||
MAX_CONCURRENT_API_AGENTS = int(os.getenv("MAX_CONCURRENT_API_AGENTS", "12"))
|
||||
RAM_MIN_FREE_PCT = int(os.getenv("RAM_MIN_FREE_PCT", "20")) # 0 = RAM-Gate aus
|
||||
AGENT_TIMEOUT = int(os.getenv("AGENT_TIMEOUT", "360")) # Sekunden je LLM-Call
|
||||
@@ -0,0 +1,327 @@
|
||||
"""Planer-DB: NUR Lauf-Zustand (Kanban-Karten) + Telemetrie (Events) — wegwerfbar.
|
||||
Die Wahrheit über Projekte liegt als Dateien in deren Repos (.planer/), nie hier.
|
||||
|
||||
Gekürzte Übernahme aus creator/backend/database.py: eine geteilte aiosqlite-Connection,
|
||||
Schreib-Serialisierung über einen loop-gebundenen Lock (_tx), WAL + busy_timeout.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import aiosqlite
|
||||
|
||||
import config
|
||||
|
||||
CREATE_KANBAN_CARDS = """
|
||||
CREATE TABLE IF NOT EXISTS kanban_cards (
|
||||
topic TEXT NOT NULL,
|
||||
board TEXT NOT NULL,
|
||||
card_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
payload TEXT NOT NULL DEFAULT '{}',
|
||||
retries INTEGER NOT NULL DEFAULT 0,
|
||||
not_before TEXT NOT NULL DEFAULT '',
|
||||
last_error TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (topic, board, card_id)
|
||||
)
|
||||
"""
|
||||
|
||||
CREATE_KANBAN_PULL_INDEX = """
|
||||
CREATE INDEX IF NOT EXISTS idx_kanban_pull ON kanban_cards(topic, board, stage, not_before, updated_at)
|
||||
"""
|
||||
|
||||
CREATE_EVENTS = """
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY,
|
||||
topic TEXT NOT NULL,
|
||||
ts TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
key TEXT NOT NULL DEFAULT '',
|
||||
label TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
dur_ms INTEGER,
|
||||
wait_ms INTEGER,
|
||||
meta TEXT NOT NULL DEFAULT '{}',
|
||||
run_id TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
"""
|
||||
|
||||
CREATE_EVENTS_INDEX = "CREATE INDEX IF NOT EXISTS idx_events ON events(topic, ts)"
|
||||
CREATE_EVENTS_RUN_INDEX = "CREATE INDEX IF NOT EXISTS idx_events_run ON events(topic, run_id)"
|
||||
|
||||
_db: aiosqlite.Connection | None = None
|
||||
|
||||
|
||||
async def get_db() -> aiosqlite.Connection:
|
||||
global _db
|
||||
if _db is None:
|
||||
config.DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_db = await aiosqlite.connect(config.DB_PATH)
|
||||
_db.row_factory = None
|
||||
return _db
|
||||
|
||||
|
||||
async def close_db() -> None:
|
||||
global _db
|
||||
if _db is not None:
|
||||
await _db.close()
|
||||
_db = None
|
||||
|
||||
|
||||
_write_lock: asyncio.Lock | None = None
|
||||
_write_lock_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
|
||||
def _get_write_lock() -> asyncio.Lock:
|
||||
"""Lock lazy an den AKTUELLEN Loop gebunden — ein modul-globaler Lock bricht,
|
||||
wenn Tests jeden Fall in einem frischen Loop fahren."""
|
||||
global _write_lock, _write_lock_loop
|
||||
loop = asyncio.get_running_loop()
|
||||
if _write_lock is None or _write_lock_loop is not loop:
|
||||
_write_lock = asyncio.Lock()
|
||||
_write_lock_loop = loop
|
||||
return _write_lock
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _tx():
|
||||
"""Atomarer Schreibblock: Lock + commit. Nur in Blatt-Funktionen verwenden,
|
||||
die selbst committen — nie in Funktionen, die andere schreibende rufen."""
|
||||
async with _get_write_lock():
|
||||
db = await get_db()
|
||||
try:
|
||||
yield db
|
||||
await db.commit()
|
||||
except BaseException:
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
async def init_db() -> None:
|
||||
db = await get_db()
|
||||
await db.execute("PRAGMA journal_mode=WAL")
|
||||
await db.execute("PRAGMA synchronous=NORMAL")
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
await db.execute(CREATE_KANBAN_CARDS)
|
||||
await db.execute(CREATE_KANBAN_PULL_INDEX)
|
||||
await db.execute(CREATE_EVENTS)
|
||||
await db.execute(CREATE_EVENTS_INDEX)
|
||||
await db.execute(CREATE_EVENTS_RUN_INDEX)
|
||||
await db.commit()
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _now_plus(seconds: float) -> str:
|
||||
return (datetime.now(timezone.utc) + timedelta(seconds=seconds)).isoformat()
|
||||
|
||||
|
||||
def _row_to_dict(row, cursor) -> dict:
|
||||
return {d[0]: row[i] for i, d in enumerate(cursor.description)}
|
||||
|
||||
|
||||
def _card(row, cursor) -> dict:
|
||||
"""Row → dict mit dekodiertem Payload-JSON (bleibt unter 'payload')."""
|
||||
d = _row_to_dict(row, cursor)
|
||||
try:
|
||||
d["payload"] = json.loads(d.get("payload") or "{}")
|
||||
except (TypeError, ValueError):
|
||||
d["payload"] = {}
|
||||
return d
|
||||
|
||||
|
||||
# --- Karten ------------------------------------------------------------------------
|
||||
|
||||
async def kanban_pull(topic: str, board: str, stage: str, limit: int) -> list[dict]:
|
||||
"""`limit` bereite Karten der Stufe (Backoff abgelaufen). LPT: Karten mit
|
||||
`groesse`-Payload zuerst (größter Chunk startet früh), sonst FIFO."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"""SELECT * FROM kanban_cards WHERE topic = ? AND board = ? AND stage = ? AND not_before <= ?
|
||||
ORDER BY COALESCE(json_extract(payload, '$.groesse'), 0) DESC, updated_at LIMIT ?""",
|
||||
(topic, board, stage, _now(), limit))
|
||||
return [_card(row, cursor) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def kanban_count(topic: str, stages, board: str | None = None) -> int:
|
||||
"""Karten in einer der `stages` (str oder Liste) — Queue-Länge/Quieszenz.
|
||||
Karten im Backoff zählen mit: ihre Arbeit ist nicht getan."""
|
||||
if isinstance(stages, str):
|
||||
stages = [stages]
|
||||
if not stages:
|
||||
return 0
|
||||
db = await get_db()
|
||||
ph = ",".join("?" * len(stages))
|
||||
sql = f"SELECT count(*) FROM kanban_cards WHERE topic = ? AND stage IN ({ph})"
|
||||
args: tuple = (topic, *stages)
|
||||
if board:
|
||||
sql += " AND board = ?"
|
||||
args += (board,)
|
||||
cursor = await db.execute(sql, args)
|
||||
return (await cursor.fetchone())[0]
|
||||
|
||||
|
||||
async def kanban_advance(topic: str, board: str, card_id: str, stage: str) -> None:
|
||||
await kanban_advance_many(topic, board, [(card_id, stage)])
|
||||
|
||||
|
||||
async def kanban_advance_many(topic: str, board: str, moves: list[tuple[str, str]]) -> None:
|
||||
"""Batch-Stufenwechsel in EINEM Commit (der Flow advanced ganze Pakete)."""
|
||||
if not moves:
|
||||
return
|
||||
async with _tx() as db:
|
||||
now = _now()
|
||||
await db.executemany(
|
||||
"""UPDATE kanban_cards SET stage = ?, retries = 0, not_before = '', last_error = NULL, updated_at = ?
|
||||
WHERE topic = ? AND board = ? AND card_id = ?""",
|
||||
[(stage, now, topic, board, cid) for cid, stage in moves])
|
||||
await _add_events_many(db, topic, [("stage", f"{board}:{cid}", "", stage) for cid, stage in moves])
|
||||
|
||||
|
||||
async def kanban_upsert_card(topic: str, board: str, card_id: str, kind: str, stage: str,
|
||||
payload: dict | None = None) -> None:
|
||||
"""Insert oder Overwrite (stabile IDs upserted, nie dupliziert).
|
||||
payload=None behält bei Konflikt das bestehende Payload."""
|
||||
async with _tx() as db:
|
||||
await db.execute(
|
||||
"""INSERT INTO kanban_cards (topic, board, card_id, kind, stage, payload, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, COALESCE(?, '{}'), ?)
|
||||
ON CONFLICT(topic, board, card_id) DO UPDATE SET
|
||||
kind = excluded.kind, stage = excluded.stage,
|
||||
payload = COALESCE(?, kanban_cards.payload),
|
||||
retries = 0, not_before = '', last_error = NULL, updated_at = excluded.updated_at""",
|
||||
(topic, board, card_id, kind, stage,
|
||||
json.dumps(payload, ensure_ascii=False) if payload is not None else None, _now(),
|
||||
json.dumps(payload, ensure_ascii=False) if payload is not None else None))
|
||||
|
||||
|
||||
async def kanban_set_payload(topic: str, board: str, card_id: str, payload: dict) -> None:
|
||||
async with _tx() as db:
|
||||
await db.execute(
|
||||
"UPDATE kanban_cards SET payload = ?, updated_at = ? WHERE topic = ? AND board = ? AND card_id = ?",
|
||||
(json.dumps(payload, ensure_ascii=False), _now(), topic, board, card_id))
|
||||
|
||||
|
||||
async def kanban_get_card(topic: str, board: str, card_id: str) -> dict | None:
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM kanban_cards WHERE topic = ? AND board = ? AND card_id = ?", (topic, board, card_id))
|
||||
row = await cursor.fetchone()
|
||||
return _card(row, cursor) if row else None
|
||||
|
||||
|
||||
async def kanban_cards(topic: str, board: str | None = None, stage: str | None = None,
|
||||
kind: str | None = None) -> list[dict]:
|
||||
db = await get_db()
|
||||
sql, args = "SELECT * FROM kanban_cards WHERE topic = ?", [topic]
|
||||
for col, val in (("board", board), ("stage", stage), ("kind", kind)):
|
||||
if val is not None:
|
||||
sql += f" AND {col} = ?"
|
||||
args.append(val)
|
||||
cursor = await db.execute(sql, args)
|
||||
return [_card(row, cursor) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def kanban_fail_card(topic: str, board: str, card_id: str, error: str,
|
||||
max_retries: int, backoff_base: float = 30.0) -> bool:
|
||||
"""Verarbeitungsfehler: retries++, exponentieller Backoff (not_before), nach
|
||||
`max_retries` → stage 'dead' (Dead-Letter, requeue-bar). → True wenn dead."""
|
||||
async with _tx() as db:
|
||||
cursor = await db.execute(
|
||||
"SELECT retries FROM kanban_cards WHERE topic = ? AND board = ? AND card_id = ?",
|
||||
(topic, board, card_id))
|
||||
row = await cursor.fetchone()
|
||||
if row is None:
|
||||
return False
|
||||
retries = (row[0] or 0) + 1
|
||||
dead = retries >= max_retries
|
||||
if dead:
|
||||
await db.execute(
|
||||
"""UPDATE kanban_cards SET stage = 'dead', retries = ?, not_before = '',
|
||||
last_error = ?, updated_at = ? WHERE topic = ? AND board = ? AND card_id = ?""",
|
||||
(retries, error[:500], _now(), topic, board, card_id))
|
||||
else:
|
||||
await db.execute(
|
||||
"""UPDATE kanban_cards SET retries = ?, not_before = ?, last_error = ?, updated_at = ?
|
||||
WHERE topic = ? AND board = ? AND card_id = ?""",
|
||||
(retries, _now_plus(backoff_base * (2 ** (retries - 1))), error[:500], _now(),
|
||||
topic, board, card_id))
|
||||
await db.execute(
|
||||
"INSERT INTO events (topic, ts, kind, key, label, status, dur_ms, wait_ms, meta, run_id) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(topic, _now(), "fail", f"{board}:{card_id}", "", "dead" if dead else f"retry{retries}",
|
||||
None, None, json.dumps({"error": error[:200]}, ensure_ascii=False), _current_run.get(topic, "")))
|
||||
return dead
|
||||
|
||||
|
||||
async def kanban_dead(topic: str) -> list[dict]:
|
||||
return await kanban_cards(topic, stage="dead")
|
||||
|
||||
|
||||
async def kanban_requeue_dead(topic: str, board: str, stage: str) -> int:
|
||||
"""dead → `stage` (frische Retries). → Anzahl requeueter Karten."""
|
||||
async with _tx() as db:
|
||||
cursor = await db.execute(
|
||||
"""UPDATE kanban_cards SET stage = ?, retries = 0, not_before = '', last_error = NULL, updated_at = ?
|
||||
WHERE topic = ? AND board = ? AND stage = 'dead'""",
|
||||
(stage, _now(), topic, board))
|
||||
await _add_events_many(db, topic, [("reset", f"{board}:requeue-dead", "", stage)])
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
async def kanban_stage_counts(topic: str) -> dict[str, dict[str, int]]:
|
||||
"""{board: {stage: count}} — das Live-Board."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT board, stage, count(*) FROM kanban_cards WHERE topic = ? GROUP BY board, stage", (topic,))
|
||||
out: dict[str, dict[str, int]] = {}
|
||||
for board, stage, n in await cursor.fetchall():
|
||||
out.setdefault(board, {})[stage] = n
|
||||
return out
|
||||
|
||||
|
||||
async def letzter_agent_ts(topic: str) -> str | None:
|
||||
"""Zeitstempel des letzten Agent-Events (auch aus CLI-Läufen — gleiche DB)."""
|
||||
db = await get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT MAX(ts) FROM events WHERE topic = ? AND kind = 'agent'", (topic,))
|
||||
return (await cursor.fetchone())[0]
|
||||
|
||||
|
||||
# --- Telemetrie ---------------------------------------------------------------------
|
||||
|
||||
_current_run: dict[str, str] = {}
|
||||
|
||||
|
||||
def set_current_run(topic: str, run_id: str | None) -> None:
|
||||
if run_id:
|
||||
_current_run[topic] = run_id
|
||||
else:
|
||||
_current_run.pop(topic, None)
|
||||
|
||||
|
||||
async def add_event(topic: str, kind: str, key: str = "", label: str = "", status: str = "",
|
||||
dur_ms: int | None = None, wait_ms: int | None = None, meta: dict | None = None) -> None:
|
||||
"""Eine Telemetrie-Zeile, eigener Commit. Aufrufer behandeln das fire-and-forget."""
|
||||
async with _tx() as db:
|
||||
await db.execute(
|
||||
"INSERT INTO events (topic, ts, kind, key, label, status, dur_ms, wait_ms, meta, run_id) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(topic, _now(), kind, key, label, status, dur_ms, wait_ms,
|
||||
json.dumps(meta or {}, ensure_ascii=False), _current_run.get(topic, "")))
|
||||
|
||||
|
||||
async def _add_events_many(db, topic: str, rows: list[tuple]) -> None:
|
||||
"""Batch-Insert OHNE Commit — muss in der Transaktion des Aufrufers laufen."""
|
||||
now = _now()
|
||||
rid = _current_run.get(topic, "")
|
||||
await db.executemany(
|
||||
"INSERT INTO events (topic, ts, kind, key, label, status, dur_ms, wait_ms, meta, run_id) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
[(topic, now, kind, key, label, status, None, None, "{}", rid)
|
||||
for kind, key, label, status in rows])
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Fake-Welt: deterministische Agenten-Antworten ohne LLM (PLANER_FAKE_AGENTS=1).
|
||||
|
||||
Creator-Muster: eine Welt beantwortet Calls anhand von agent_key-Mustern und kann
|
||||
Störungen einspielen ({muster, modus, mal}) — damit testet die Fake-E2E den kompletten
|
||||
Pfad inkl. Retry/Dead-Letter/Pause in Sekunden.
|
||||
"""
|
||||
import re
|
||||
|
||||
# Modul-globale Welt für den ENV-Weg; Tests setzen ihre eigene via set_welt().
|
||||
_WELT = None
|
||||
|
||||
|
||||
def set_welt(welt) -> None:
|
||||
global _WELT
|
||||
_WELT = welt
|
||||
|
||||
|
||||
class Welt:
|
||||
def __init__(self):
|
||||
self.calls: list[str] = [] # Auditspur aller agent_keys
|
||||
self.stoerungen: list[dict] = [] # {muster, modus: fehler|garbage|infra, mal, [antwort]}
|
||||
self.antworten: list[tuple[str, object]] = [] # (regex auf agent_key, fn(prompt) -> str)
|
||||
|
||||
def antwort_auf(self, muster: str, fn) -> None:
|
||||
self.antworten.append((muster, fn))
|
||||
|
||||
def _stoerung(self, agent_key: str) -> tuple[int, str, str] | None:
|
||||
for s in self.stoerungen:
|
||||
if s.get("mal", 0) > 0 and re.search(s["muster"], agent_key):
|
||||
s["mal"] -= 1
|
||||
modus = s.get("modus", "fehler")
|
||||
if modus == "fehler":
|
||||
return 1, "", "fake-stoerung"
|
||||
if modus == "garbage":
|
||||
return 0, "### kaputtes {fragment", ""
|
||||
if modus == "infra":
|
||||
from agents import AgentInfraError
|
||||
raise AgentInfraError("fake-infra-stoerung")
|
||||
if modus == "antwort":
|
||||
return 0, s.get("antwort", ""), ""
|
||||
return None
|
||||
|
||||
async def respond(self, agent_key: str, prompt: str, capabilities: str) -> tuple[int, str, str]:
|
||||
self.calls.append(agent_key)
|
||||
gestoert = self._stoerung(agent_key)
|
||||
if gestoert is not None:
|
||||
return gestoert
|
||||
for muster, fn in self.antworten:
|
||||
if re.search(muster, agent_key):
|
||||
return 0, fn(prompt), ""
|
||||
return 1, "", f"fake: keine Antwort für {agent_key}"
|
||||
|
||||
|
||||
async def respond(agent_key: str, prompt: str, capabilities: str) -> tuple[int, str, str]:
|
||||
global _WELT
|
||||
if _WELT is None:
|
||||
_WELT = Welt()
|
||||
standard_antworten(_WELT)
|
||||
return await _WELT.respond(agent_key, prompt, capabilities)
|
||||
|
||||
|
||||
# --- Standard-Antworten: mechanisch erzeugte Ausgaben, die die ECHTEN Gates bestehen ---
|
||||
|
||||
def _quelle_aus(prompt: str) -> tuple[str, str]:
|
||||
"""(datei, quelltext) aus einem Scan-Prompt (Template-Struktur)."""
|
||||
m = re.search(r"Ausschnitt aus `(.+?)`\n\n```python\n(.*?)\n?```", prompt, re.S)
|
||||
return (m.group(1), m.group(2)) if m else ("", "")
|
||||
|
||||
|
||||
def _symbol_liste(prompt: str, kopf: str) -> list[str]:
|
||||
"""`- symbol`-Liste direkt nach der Zeile mit `kopf` (Leerzeilen dazwischen erlaubt)."""
|
||||
idx = prompt.find(kopf)
|
||||
if idx < 0:
|
||||
return []
|
||||
out: list[str] = []
|
||||
for zeile in prompt[idx:].splitlines()[1:]:
|
||||
z = zeile.strip()
|
||||
if z.startswith("- "):
|
||||
if "(keine)" not in z:
|
||||
out.append(z[2:].strip())
|
||||
elif z:
|
||||
break # erste andere Zeile beendet die Liste
|
||||
return out
|
||||
|
||||
|
||||
def _einheit_fuer(datei: str, symbol: str, quelle: str) -> str:
|
||||
"""Eine gültige Einheit: Beleg = die wörtliche Definitionszeile aus der Quelle."""
|
||||
beleg = next((z.strip() for z in quelle.splitlines()
|
||||
if re.match(rf"\s*(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b", z)
|
||||
or re.match(rf"{re.escape(symbol)}\s*[:=]", z)), "")
|
||||
beleg = beleg[:60].replace('"', "'")
|
||||
return (f"## {datei}::{symbol}\n"
|
||||
f"beschreibung: Fake-Verantwortung von {symbol}.\n"
|
||||
f"input: Fake-Input\noutput: Fake-Output\n"
|
||||
f"entscheidungen:\n- Fake-Entscheidung zu {symbol}.\n beleg: \"{beleg}\"\n"
|
||||
f"kanten:\n")
|
||||
|
||||
|
||||
def _fake_schneiden(prompt: str, lasse_aus: set[str]) -> str:
|
||||
datei, quelle = _quelle_aus(prompt)
|
||||
symbole = [s for s in _symbol_liste(prompt, "MÜSSEN alle abgedeckt sein") if s not in lasse_aus]
|
||||
if not symbole: # Datei ohne def/class: erste Konstante als Einheit
|
||||
m = re.search(r"^(\w+)\s*[:=]", quelle, re.M)
|
||||
symbole = [m.group(1)] if m else []
|
||||
return "\n".join(_einheit_fuer(datei, s, quelle) for s in symbole)
|
||||
|
||||
|
||||
def _fake_nachfass(prompt: str) -> str:
|
||||
datei, quelle = _quelle_aus(prompt)
|
||||
fehlend = _symbol_liste(prompt, "zugeordnet sind:")
|
||||
return "\n".join(_einheit_fuer(datei, s, quelle) for s in fehlend)
|
||||
|
||||
|
||||
def _ids_aus_uebersicht(prompt: str) -> list[str]:
|
||||
return re.findall(r"^- (\S+::\S+):", prompt, re.M)
|
||||
|
||||
|
||||
def _projekt_aus(prompt: str) -> str:
|
||||
m = re.search(r"Einheiten von `(.+?)`", prompt)
|
||||
return m.group(1) if m else "projekt"
|
||||
|
||||
|
||||
def _fake_features(prompt: str) -> str:
|
||||
ids = _ids_aus_uebersicht(prompt)
|
||||
p = _projekt_aus(prompt)
|
||||
return (f"# Kern: {p}\nFake-Kern in zwei Sätzen. Mehr Fake.\n\n"
|
||||
f"tragende-einheiten: {ids[0]}\n\n"
|
||||
f"# Bereich: fake-bereich\nbeschreibung: Ein Fake-Bereich.\n\n"
|
||||
f"## Feature: Fake-Feature [kern]\nbeschreibung: Kann alles Fake.\n\n"
|
||||
f"### Kann fake arbeiten\neinheiten: {', '.join(ids)}\n")
|
||||
|
||||
|
||||
def _fake_architektur(prompt: str) -> str:
|
||||
ids = _ids_aus_uebersicht(prompt)
|
||||
p = _projekt_aus(prompt)
|
||||
return (f"# Architektur: {p}\n\n## Komponente: fake-komponente\n"
|
||||
f"beschreibung: Trägt alles.\neinheiten: {', '.join(ids)}\nbeziehungen:\n")
|
||||
|
||||
|
||||
def _fake_flows(prompt: str) -> str:
|
||||
ids = _ids_aus_uebersicht(prompt)
|
||||
p = _projekt_aus(prompt)
|
||||
schritte = "\n".join(f"{i+1}. Fake-Schritt {i+1} — {eid}" for i, eid in enumerate(ids[:3]))
|
||||
return f"# Flows: {p}\n\n## Flow: fake-ablauf\n{schritte}\n"
|
||||
|
||||
|
||||
def standard_antworten(welt: Welt, lasse_aus: set[str] | None = None) -> None:
|
||||
"""Registriert gültige Fake-Antworten für alle Scan-Rollen. `lasse_aus`: Symbole,
|
||||
die der Erst-Scan absichtlich auslässt (übt den Nachfass-Pfad)."""
|
||||
aus = lasse_aus or set()
|
||||
welt.antwort_auf(r"-schneiden-", lambda pr: _fake_schneiden(pr, aus))
|
||||
welt.antwort_auf(r"-nachfass-", _fake_nachfass)
|
||||
welt.antwort_auf(r"-sichten-sicht-features", _fake_features)
|
||||
welt.antwort_auf(r"-sichten-sicht-architektur", _fake_architektur)
|
||||
welt.antwort_auf(r"-sichten-sicht-flows", _fake_flows)
|
||||
@@ -0,0 +1,310 @@
|
||||
"""Mechanische Gates (tokenfrei): prüfen LLM-Scan-Ausgaben gegen den echten Code.
|
||||
|
||||
Port von phase0/pruefe.py als Bibliothek. Phase-0-validiert: fängt erfundene Anker,
|
||||
umgeschriebene Zitate (auch Minimal-Umformungen) und Format-Drift; Escaping-Rauschen
|
||||
der Modelle wird vor dem Vergleich normalisiert.
|
||||
"""
|
||||
import difflib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
KANTEN_TYPEN = {"ruft-auf", "nutzt", "wird-genutzt-von", "löst-aus", "wird-ausgelöst-von"}
|
||||
|
||||
SCHLUESSEL = ("beschreibung", "input", "output") # Pflichtfelder je Einheit
|
||||
|
||||
|
||||
def entschaerfe(s: str) -> str:
|
||||
"""Escaping-Rauschen normalisieren: \\" → \" und \\\\ → \\ (Erkenntnis 11)."""
|
||||
return s.replace('\\"', '"').replace("\\\\", "\\")
|
||||
|
||||
|
||||
def _beleg_text(rest: str) -> str:
|
||||
"""Beleg-Inhalt aus dem Zeilenrest: äußere Anführungszeichen abstreifen, Rest wörtlich.
|
||||
Kein Quote-Delimiter-Parsing — Belege enthalten selbst Anführungszeichen/Regex
|
||||
(Abnahme-Befund 17), die Zeile gehört ab `beleg:` komplett dem Zitat."""
|
||||
rest = rest.strip()
|
||||
if len(rest) >= 2 and rest.startswith('"') and rest.endswith('"'):
|
||||
rest = rest[1:-1]
|
||||
elif rest.startswith('"'):
|
||||
rest = rest[1:]
|
||||
return entschaerfe(rest)
|
||||
|
||||
|
||||
def parse_einheiten(text: str) -> tuple[list[dict], list[str], list[str]]:
|
||||
"""Einheiten-Markdown → (einheiten, zusatz_anker, struktur_fehler).
|
||||
Einheit: {id, zeile, felder: {beschreibung/input/output}, belege: [(nr, zitat)],
|
||||
kanten: [(nr, typ, ziel)], anker: [ids]}. Streng schreiben, tolerant parsen."""
|
||||
einheiten: list[dict] = []
|
||||
zusatz_anker: list[str] = []
|
||||
fehler: list[str] = []
|
||||
aktuelle = None
|
||||
modus = None
|
||||
for nr, zeile in enumerate(text.splitlines(), 1):
|
||||
# Einheiten-Kopf: `## datei::symbol` oder Datei-Einheit `## pfad.py` (Skripte
|
||||
# ohne def/class — Befund 18)
|
||||
if zeile.startswith("## ") and ("::" in zeile or re.fullmatch(r"## \S+\.\w+", zeile.strip())):
|
||||
aktuelle = {"id": zeile[3:].strip(), "zeile": nr, "felder": {}, "belege": [],
|
||||
"kanten": [], "anker": []}
|
||||
einheiten.append(aktuelle)
|
||||
modus = None
|
||||
elif zeile.startswith(("anker:", "ergaenze-anker:")):
|
||||
aid = zeile.split(":", 1)[1].strip().split(" zu ")[0].strip()
|
||||
zusatz_anker.append(aid)
|
||||
if aktuelle is not None and zeile.startswith("anker:"):
|
||||
aktuelle["anker"].append(aid)
|
||||
elif aktuelle is None:
|
||||
continue
|
||||
elif m := re.match(r"(beschreibung|input|output):(.*)$", zeile):
|
||||
aktuelle["felder"][m.group(1)] = m.group(2).strip()
|
||||
modus = m.group(1)
|
||||
elif zeile.strip() in ("entscheidungen:", "fakten:"):
|
||||
modus = "punkte"
|
||||
elif zeile.strip() == "kanten:":
|
||||
modus = "kanten"
|
||||
elif m := re.match(r"\s+beleg:\s*(.+)$", zeile):
|
||||
aktuelle["belege"].append((nr, _beleg_text(m.group(1))))
|
||||
elif modus == "punkte" and (m := re.search(r"\bbeleg:\s*(.+)$", zeile)):
|
||||
aktuelle["belege"].append((nr, _beleg_text(m.group(1)))) # tolerant: beleg inline
|
||||
elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)\s*$", zeile)):
|
||||
aktuelle["kanten"].append((nr, m.group(1), m.group(2)))
|
||||
elif modus == "kanten" and zeile.startswith("- "):
|
||||
fehler.append(f"Z{nr}: Kante unlesbar (Zusatztext?): {zeile.strip()!r}")
|
||||
return einheiten, zusatz_anker, fehler
|
||||
|
||||
|
||||
def _symbol_in_datei(symbol: str, quelle: str, top_level_nur: bool) -> bool:
|
||||
if "." in symbol: # qualifizierte Methode `Klasse.methode` (Abnahme-Befund)
|
||||
klasse, _, methode = symbol.partition(".")
|
||||
return (_symbol_in_datei(klasse, quelle, top_level_nur)
|
||||
and re.search(rf"def\s+{re.escape(methode)}\b", quelle) is not None)
|
||||
if top_level_nur:
|
||||
muster = rf"^(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b|^{re.escape(symbol)}\s*[:=]"
|
||||
else: # Zusatz-Anker dürfen verschachtelt sein (Innenfunktionen, Erkenntnis M3-Lauf)
|
||||
muster = rf"(?:def|class)\s+{re.escape(symbol)}\b|^{re.escape(symbol)}\s*[:=]"
|
||||
return re.search(muster, quelle, re.M) is not None
|
||||
|
||||
|
||||
def pruefe_fragment(text: str, wurzel: Path, chunk_datei: str,
|
||||
quelle_cache: dict | None = None) -> tuple[list[str], list[str], list[dict]]:
|
||||
"""Gates für EIN Scan-Fragment: Struktur, Anker-Existenz, Zitat-Treue, Pflichtfelder,
|
||||
Kanten-Grammatik. → (fehler, warnungen, einheiten). Kanten-ZIELE in fremden Dateien
|
||||
werden nur auf Datei-Existenz geprüft (das Symbol prüft der Scan der Zieldatei)."""
|
||||
cache = quelle_cache if quelle_cache is not None else {}
|
||||
|
||||
def quelle(datei: str) -> str | None:
|
||||
if datei not in cache:
|
||||
p = wurzel / datei
|
||||
cache[datei] = p.read_text(encoding="utf-8", errors="replace") if p.is_file() else None
|
||||
return cache[datei]
|
||||
|
||||
einheiten, zusatz_anker, fehler = parse_einheiten(text)
|
||||
warnungen: list[str] = []
|
||||
if not einheiten:
|
||||
fehler.append("keine Einheit geparst (Format-Drift oder leere Antwort)")
|
||||
|
||||
def pruefe_id(eid: str, kontext: str, top_level: bool):
|
||||
datei, _, symbol = eid.partition("::")
|
||||
q = quelle(datei)
|
||||
if q is None:
|
||||
fehler.append(f"{kontext}: Datei fehlt: {datei}")
|
||||
elif symbol and not _symbol_in_datei(symbol, q, top_level):
|
||||
fehler.append(f"{kontext}: Symbol '{symbol}' nicht in {datei}")
|
||||
|
||||
for e in einheiten:
|
||||
kontext = e["id"]
|
||||
if not e["id"].startswith(chunk_datei + "::") and e["id"] != chunk_datei:
|
||||
fehler.append(f"{kontext}: Einheit gehört nicht zur gescannten Datei {chunk_datei}")
|
||||
continue
|
||||
pruefe_id(e["id"], f"Z{e['zeile']} einheit", top_level=True)
|
||||
for feld in SCHLUESSEL:
|
||||
if not e["felder"].get(feld):
|
||||
(fehler if feld == "beschreibung" else warnungen).append(f"{kontext}: {feld}: fehlt")
|
||||
q = quelle(chunk_datei) or ""
|
||||
for nr, beleg in e["belege"]:
|
||||
if beleg not in q:
|
||||
fehler.append(f"{kontext} Z{nr}: Zitat nicht wörtlich in {chunk_datei}: \"{beleg[:50]}…\"")
|
||||
if not e["belege"]:
|
||||
warnungen.append(f"{kontext}: keine Belege")
|
||||
for aid in e["anker"]:
|
||||
pruefe_id(aid, f"{kontext} anker", top_level=False)
|
||||
for nr, typ, ziel in e["kanten"]:
|
||||
if typ not in KANTEN_TYPEN:
|
||||
fehler.append(f"{kontext} Z{nr}: unbekannter Kanten-Typ '{typ}'")
|
||||
ziel_datei = ziel.partition("::")[0]
|
||||
if ziel_datei == chunk_datei:
|
||||
pruefe_id(ziel, f"{kontext} Z{nr} kante", top_level=False)
|
||||
elif quelle(ziel_datei) is None:
|
||||
fehler.append(f"{kontext} Z{nr} kante: Datei fehlt: {ziel_datei}")
|
||||
for aid in zusatz_anker:
|
||||
if not aid.startswith(chunk_datei + "::"):
|
||||
fehler.append(f"ergaenze-anker außerhalb der gescannten Datei: {aid}")
|
||||
return fehler, warnungen, einheiten
|
||||
|
||||
|
||||
def fehlende_symbole(chunk_symbole: list[str], einheiten: list[dict],
|
||||
zusatz_anker: list[str]) -> list[str]:
|
||||
"""Abdeckung eines Chunks: welche seiner Top-Level-Symbole sind keiner Einheit
|
||||
zugeordnet (weder als ID noch als Anker)?"""
|
||||
zugeordnet = {e["id"].partition("::")[2] for e in einheiten}
|
||||
zugeordnet |= {a.partition("::")[2] for a in zusatz_anker}
|
||||
zugeordnet |= {a.partition("::")[2] for e in einheiten for a in e["anker"]}
|
||||
return [s for s in chunk_symbole if s not in zugeordnet]
|
||||
|
||||
|
||||
def repariere_kanten(fragment: str, wurzel: Path, chunk_datei: str,
|
||||
quelle_cache: dict | None = None) -> tuple[str, list[str]]:
|
||||
"""Mechanische Reparatur VOR den Gates: Kanten mit ungültigem Ziel (erfundene Datei,
|
||||
fehlendes Symbol, fehlender datei::-Prefix) werden GESTRICHEN statt das ganze
|
||||
Fragment scheitern zu lassen — Kanten sind Zusatz-Metadaten, keine Belege.
|
||||
→ (bereinigtes_fragment, gestrichene_zeilen)."""
|
||||
cache = quelle_cache if quelle_cache is not None else {}
|
||||
|
||||
def quelle(datei: str) -> str | None:
|
||||
if datei not in cache:
|
||||
p = wurzel / datei
|
||||
cache[datei] = p.read_text(encoding="utf-8", errors="replace") if p.is_file() else None
|
||||
return cache[datei]
|
||||
|
||||
def ziel_ok(ziel: str) -> bool:
|
||||
if "::" not in ziel:
|
||||
return False
|
||||
datei, _, symbol = ziel.partition("::")
|
||||
q = quelle(datei)
|
||||
if q is None:
|
||||
return False
|
||||
return datei != chunk_datei or _symbol_in_datei(symbol, q, top_level_nur=False)
|
||||
|
||||
gestrichen: list[str] = []
|
||||
zeilen_neu: list[str] = []
|
||||
for zeile in fragment.splitlines():
|
||||
m = re.match(r"- ([\wäöü-]+):\s*(\S+)\s*$", zeile)
|
||||
if m and m.group(1) in KANTEN_TYPEN and not ziel_ok(m.group(2)):
|
||||
gestrichen.append(zeile.strip())
|
||||
continue
|
||||
# Freitext hinter einem Kanten-Typ ("- löst-aus: SystemExit bei …") → streichen
|
||||
m_frei = re.match(r"- ([\wäöü-]+):\s+\S+\s+\S", zeile)
|
||||
if m_frei and m_frei.group(1) in KANTEN_TYPEN:
|
||||
gestrichen.append(zeile.strip())
|
||||
continue
|
||||
if zeile.startswith(("anker:", "ergaenze-anker:")): # kaputte Anker-Zeilen ebenso
|
||||
aid = zeile.split(":", 1)[1].strip().split(" zu ")[0].strip()
|
||||
datei, _, symbol = aid.partition("::")
|
||||
q = quelle(datei) if datei else None
|
||||
if datei != chunk_datei or not symbol or q is None \
|
||||
or not _symbol_in_datei(symbol, q, top_level_nur=False):
|
||||
gestrichen.append(zeile.strip())
|
||||
continue
|
||||
zeilen_neu.append(zeile)
|
||||
return "\n".join(zeilen_neu), gestrichen
|
||||
|
||||
|
||||
_BELEG_RE = re.compile(r"(\bbeleg:\s*)(.+)$")
|
||||
|
||||
|
||||
def repariere_belege(fragment: str, wurzel: Path, chunk_datei: str,
|
||||
quelle_cache: dict | None = None) -> tuple[str, list[str]]:
|
||||
"""Mechanische Zitat-Reparatur: ein Beleg, der nicht wörtlich in der Quelle steht,
|
||||
wird per Fuzzy-Match auf die ähnlichste Quellzeile GESCHNAPPT und durch deren
|
||||
wörtlichen Text ersetzt (Abnahme-Befund: Modelle schreiben z. B. `:=` zu `=` um —
|
||||
ein Zeichen daneben). Unter der Ähnlichkeitsschwelle bleibt der Beleg unverändert
|
||||
und fällt im Gate durch → Befund-Feedback. → (fragment, korrigierte_belege)."""
|
||||
cache = quelle_cache if quelle_cache is not None else {}
|
||||
if chunk_datei not in cache:
|
||||
p = wurzel / chunk_datei
|
||||
cache[chunk_datei] = p.read_text(encoding="utf-8", errors="replace") if p.is_file() else None
|
||||
quelle = cache[chunk_datei]
|
||||
if quelle is None:
|
||||
return fragment, []
|
||||
zeilen = [z.strip() for z in quelle.splitlines() if z.strip()]
|
||||
korrigiert: list[str] = []
|
||||
|
||||
def snap(m: re.Match) -> str:
|
||||
zitat = _beleg_text(m.group(2))
|
||||
if not zitat or zitat in quelle:
|
||||
return m.group(0)
|
||||
# Fenster-Matching: Abdeckung des Zitats durch gemeinsame Blöcke mit einer
|
||||
# Quellzeile — funktioniert auch für kurze Zitate aus langen Zeilen.
|
||||
beste, beste_deckung = None, 0.0
|
||||
for z in zeilen:
|
||||
sm = difflib.SequenceMatcher(None, zitat.lower(), z.lower(), autojunk=False)
|
||||
if sm.real_quick_ratio() * max(len(zitat), len(z)) < len(zitat) * 0.8:
|
||||
continue
|
||||
bloecke = [b for b in sm.get_matching_blocks() if b.size > 0]
|
||||
deckung = sum(b.size for b in bloecke) / len(zitat)
|
||||
if deckung > beste_deckung:
|
||||
beste_deckung = deckung
|
||||
start = min(b.b for b in bloecke)
|
||||
ende = max(b.b + b.size for b in bloecke)
|
||||
beste = z[start:ende] # wörtliches Quell-Fenster
|
||||
if beste is None or beste_deckung < 0.8 or beste not in quelle:
|
||||
return m.group(0)
|
||||
korrigiert.append(f"{zitat!r} → {beste[:60]!r}")
|
||||
return m.group(1) + '"' + beste[:78] + '"'
|
||||
|
||||
neu = "\n".join(_BELEG_RE.sub(snap, zeile) for zeile in fragment.splitlines())
|
||||
return neu, korrigiert
|
||||
|
||||
|
||||
def repariere_referenzen(sichten_text: str, bekannte_ids: set[str]) -> tuple[str, list[str]]:
|
||||
"""Mechanische Reparatur einer Sicht: unbekannte Einheiten-Referenzen werden aus
|
||||
`einheiten:`-/`tragende-einheiten:`-Zeilen und Flow-Schritten GESTRICHEN; verliert
|
||||
ein Teilfeature/Schritt alle Referenzen, fällt die Zeile bzw. der `###`-Block weg.
|
||||
(Abnahme-Befund: Sichten referenzieren Einheiten toter Chunks — per Feedback nicht
|
||||
heilbar, weil die IDs schlicht nicht existieren.) → (text, gestrichene_refs)."""
|
||||
gestrichen: list[str] = []
|
||||
# Symbol → IDs: falscher Datei-Prefix bei eindeutigem Symbol wird KORRIGIERT statt
|
||||
# gestrichen (Befund 19: repair.py-Einheit als board_inventory.py referenziert)
|
||||
je_symbol: dict[str, list[str]] = {}
|
||||
for eid in bekannte_ids:
|
||||
je_symbol.setdefault(eid.partition("::")[2], []).append(eid)
|
||||
|
||||
def filtere(refs: str) -> list[str]:
|
||||
gueltig = []
|
||||
for ref in refs.split(","):
|
||||
ref = ref.strip()
|
||||
if not ref:
|
||||
continue
|
||||
if ref in bekannte_ids:
|
||||
gueltig.append(ref)
|
||||
continue
|
||||
kandidaten = je_symbol.get(ref.partition("::")[2], [])
|
||||
if len(kandidaten) == 1:
|
||||
gueltig.append(kandidaten[0])
|
||||
else:
|
||||
gestrichen.append(ref)
|
||||
return gueltig
|
||||
|
||||
zeilen_neu: list[str] = []
|
||||
for zeile in sichten_text.splitlines():
|
||||
if m := re.match(r"^(einheiten|tragende-einheiten):\s*(.+)$", zeile):
|
||||
gueltig = filtere(m.group(2))
|
||||
if gueltig:
|
||||
zeilen_neu.append(f"{m.group(1)}: {', '.join(gueltig)}")
|
||||
elif m.group(1) == "einheiten" and zeilen_neu and zeilen_neu[-1].startswith("### "):
|
||||
zeilen_neu.pop() # Teilfeature ohne einzige gültige Referenz fällt weg
|
||||
else:
|
||||
zeilen_neu.append(f"{m.group(1)}:")
|
||||
elif m := re.match(r"^(\d+\.\s.+\s—\s)(.+)$", zeile):
|
||||
gueltig = filtere(m.group(2))
|
||||
if gueltig:
|
||||
zeilen_neu.append(m.group(1) + ", ".join(gueltig))
|
||||
# Schritt ohne gültige Referenz fällt weg
|
||||
else:
|
||||
zeilen_neu.append(zeile)
|
||||
return "\n".join(zeilen_neu), gestrichen
|
||||
|
||||
|
||||
def pruefe_referenzen(sichten_text: str, bekannte_ids: set[str]) -> list[str]:
|
||||
"""Referenz-Integrität einer Sicht: `einheiten:`-/`tragende-einheiten:`-Zeilen und
|
||||
Flow-Schritte (`1. … — id, id`) dürfen nur bekannte Einheiten-IDs nennen."""
|
||||
fehler = []
|
||||
referenz_zeilen = [m.group(1) for m in
|
||||
re.finditer(r"^(?:einheiten|tragende-einheiten):\s*(.+)$", sichten_text, re.M)]
|
||||
referenz_zeilen += [m.group(1) for m in
|
||||
re.finditer(r"^\d+\.\s.+\s—\s(.+)$", sichten_text, re.M)]
|
||||
for zeile in referenz_zeilen:
|
||||
for ref in zeile.split(","):
|
||||
ref = ref.strip()
|
||||
if ref and ref not in bekannte_ids:
|
||||
fehler.append(f"unbekannte Einheiten-Referenz: {ref}")
|
||||
return fehler
|
||||
@@ -0,0 +1,269 @@
|
||||
"""Generic streaming kanban engine (no concrete stages — boards define those).
|
||||
|
||||
Each column is a worker that pulls cards from its input `stage` (the queue = kanban_cards rows
|
||||
WHERE stage = <input>), processes up to KANBAN_BATCH at a time, and advances them. Streaming
|
||||
columns run continuously; BARRIER columns start only at QUIESCENCE of every stage before them
|
||||
(no active worker + no queued card). SERIAL columns process one package at a time (their
|
||||
processor mutates shared cross-card state).
|
||||
|
||||
Failure handling: a processor exception (including parse-fails it raises) sends the package's
|
||||
unadvanced cards into exponential backoff (retries++, not_before); after MAX_CARD_RETRIES the
|
||||
card goes to stage 'dead' (dead-letter — visible on the board, requeue-able via API). No card
|
||||
is ever deleted by the engine.
|
||||
|
||||
Board definitions live in board_inventory.py / board_artefacts.py; run via run_flow().
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import database as db
|
||||
from config import KANBAN_BATCH, MAX_CARD_RETRIES, MAX_CONCURRENT_AGENTS_PER_TOPIC, RETRY_BACKOFF
|
||||
from agents import AgentInfraError
|
||||
|
||||
log = logging.getLogger("creator.kanban")
|
||||
|
||||
# How many packages ONE worker keeps in flight at once. A worker no longer blocks on a single
|
||||
# package — it keeps pulling and dispatching until this many run concurrently, so a busy column
|
||||
# fills the agent slots (the per-topic semaphore is the real cap; over-dispatch just queues cheaply).
|
||||
WORKER_INFLIGHT = MAX_CONCURRENT_AGENTS_PER_TOPIC
|
||||
_POLL = 0.3 # seconds between empty-queue polls
|
||||
|
||||
# Live registry of running flows (topic → Flow), so routes can attach research agents,
|
||||
# report `generating`, and cancel.
|
||||
active_flows: dict[str, "Flow"] = {}
|
||||
|
||||
|
||||
class Flow:
|
||||
"""Shared runtime state of one topic run: active-task counters per stage + a wakeup event.
|
||||
`producers` counts running research agents (initial + any added live); research counts as done
|
||||
only when ALL producers have finished, so the flow stays awake while extras still search."""
|
||||
|
||||
def __init__(self, topic: str, work_dir=None):
|
||||
self.topic = topic
|
||||
self.work_dir = work_dir
|
||||
self.active: dict[str, int] = {}
|
||||
self.producers = 0
|
||||
self.producer_tag = 0
|
||||
self.stop = False
|
||||
self.wake = asyncio.Event()
|
||||
self.spawn_research = None # set by the board: () → coroutine adding one more research agent
|
||||
self.state: dict = {} # board-private shared state (embedding caches, one-shot flags …)
|
||||
self.active_cards: set[str] = set() # "board:card_id" currently inside a processor (live display)
|
||||
|
||||
@property
|
||||
def research_done(self) -> bool:
|
||||
return self.producers <= 0
|
||||
|
||||
def add_producer(self):
|
||||
"""MUST be called synchronously BEFORE create_task of the producer — otherwise workers
|
||||
can pass their exit check in the gap and never see the new producer (quiescence race)."""
|
||||
self.producers += 1
|
||||
self.wake.set()
|
||||
|
||||
def done_producer(self):
|
||||
self.producers -= 1
|
||||
self.wake.set()
|
||||
|
||||
def next_tag(self) -> int:
|
||||
self.producer_tag += 1
|
||||
return self.producer_tag
|
||||
|
||||
def enter(self, stage: str):
|
||||
self.active[stage] = self.active.get(stage, 0) + 1
|
||||
|
||||
def leave(self, stage: str):
|
||||
self.active[stage] = max(0, self.active.get(stage, 0) - 1)
|
||||
self.wake.set()
|
||||
|
||||
def active_in(self, stages) -> bool:
|
||||
return any(self.active.get(s, 0) > 0 for s in stages)
|
||||
|
||||
|
||||
class Stage:
|
||||
"""One column: board + stage name + processor. `upstream` (all stages before it, across
|
||||
boards) is filled by chain_stages(). process(cards) gets the pulled package (list of card
|
||||
dicts with decoded payload).
|
||||
|
||||
barrier: pull only when every upstream stage is quiescent (relational judgements need the
|
||||
full set). gate: extra callable that must be truthy before the stage pulls (works without
|
||||
barrier too — e.g. the consensus gate holds cards until research is done so late reader
|
||||
votes still count). drain: pull the WHOLE queue as one package (global passes like the
|
||||
fragment filter); implies serial."""
|
||||
|
||||
def __init__(self, board: str, stage: str, process, *, barrier: bool = False,
|
||||
serial: bool = False, gate=None, drain: bool = False):
|
||||
self.board = board
|
||||
self.stage = stage
|
||||
self.process = process
|
||||
self.barrier = barrier
|
||||
self.serial = serial or drain
|
||||
self.gate = gate
|
||||
self.drain = drain
|
||||
self.upstream: list[str] = []
|
||||
|
||||
|
||||
def chain_stages(stages: list[Stage]) -> list[Stage]:
|
||||
"""Fill each stage's upstream = every stage listed before it (list order = flow order).
|
||||
Producers are upstream of everything implicitly via flow.research_done."""
|
||||
seen: list[str] = []
|
||||
for s in stages:
|
||||
s.upstream = list(seen)
|
||||
seen.append(s.stage)
|
||||
return stages
|
||||
|
||||
|
||||
async def quiescent(flow: Flow, stages) -> bool:
|
||||
"""True iff no worker is active in `stages` AND no card is queued in any of them.
|
||||
The barrier/exit condition — must include QUEUED cards, not just active workers, or a worker
|
||||
could exit in a momentary lull while an upstream worker still has work to push down."""
|
||||
if not stages:
|
||||
return True
|
||||
if flow.active_in(stages):
|
||||
return False
|
||||
return await db.kanban_count(flow.topic, list(stages)) == 0
|
||||
|
||||
|
||||
async def _sleep_wake(flow: Flow):
|
||||
try:
|
||||
await asyncio.wait_for(flow.wake.wait(), timeout=_POLL)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
flow.wake.clear()
|
||||
|
||||
|
||||
async def _fail_package(flow: Flow, spec: Stage, cards: list[dict], error: str):
|
||||
"""Backoff/dead-letter for the cards the processor did NOT advance (their stage is unchanged —
|
||||
advanced cards must not be punished for a failure after their move)."""
|
||||
for c in cards:
|
||||
cur = await db.kanban_get_card(flow.topic, spec.board, c["card_id"])
|
||||
if cur is None or cur["stage"] != spec.stage:
|
||||
continue
|
||||
dead = await db.kanban_fail_card(flow.topic, spec.board, c["card_id"], error,
|
||||
MAX_CARD_RETRIES, RETRY_BACKOFF)
|
||||
if dead:
|
||||
log.warning("kanban %s/%s: card %s → dead (%s)", flow.topic, spec.stage, c["card_id"], error)
|
||||
|
||||
|
||||
async def _worker(flow: Flow, spec: Stage, inflight: int, all_stages: list[str]):
|
||||
"""Pull cards from spec.stage, run spec.process — keeping up to `inflight` packages running
|
||||
CONCURRENTLY so a busy column fills the agent slots. A barrier worker only pulls when upstream
|
||||
is fully quiescent (and its gate, if any, is open). ANY worker exits only when research is
|
||||
done and the WHOLE flow is quiescent — global instead of per-stage, so a downstream stage
|
||||
that feeds cards back upstream (gap-check → ingest) never strands work. Double-checked over
|
||||
one grace sleep (a producer attached in the lull keeps the flow alive).
|
||||
|
||||
Double-pull safety: each stage has exactly ONE worker, so an in-memory `claimed` set of
|
||||
card-ids (held while a package runs) keeps concurrent pulls from grabbing the same cards."""
|
||||
topic = flow.topic
|
||||
claimed: set[str] = set()
|
||||
tasks: set[asyncio.Task] = set()
|
||||
batch = 100_000 if spec.drain else KANBAN_BATCH
|
||||
|
||||
async def _run(cards):
|
||||
ids = [c["card_id"] for c in cards]
|
||||
flow.enter(spec.stage)
|
||||
flow.active_cards.update(f"{spec.board}:{i}" for i in ids)
|
||||
try:
|
||||
await spec.process(cards)
|
||||
except AgentInfraError as e: # 429/Timeout erschöpft → Lauf pausieren, NICHT dead-letter
|
||||
log.warning("kanban %s/%s: infra-pause: %s", topic, spec.stage, e)
|
||||
flow.state["infra_paused"] = True
|
||||
flow.state["infra_error"] = str(e)
|
||||
flow.stop = True
|
||||
flow.wake.set()
|
||||
except Exception as e: # one bad package must not kill the worker → backoff/dead-letter
|
||||
log.info("kanban %s/%s: %s: %s", topic, spec.stage, type(e).__name__, e)
|
||||
try:
|
||||
await _fail_package(flow, spec, cards, f"{type(e).__name__}: {e}")
|
||||
except Exception:
|
||||
log.exception("kanban %s/%s: fail-handling broke", topic, spec.stage)
|
||||
finally:
|
||||
flow.leave(spec.stage)
|
||||
for i in ids:
|
||||
claimed.discard(i)
|
||||
flow.active_cards.discard(f"{spec.board}:{i}")
|
||||
flow.wake.set()
|
||||
|
||||
async def _idle_exit() -> bool:
|
||||
return (flow.research_done and not flow.active_in(all_stages)
|
||||
and await db.kanban_count(topic, all_stages) == 0)
|
||||
|
||||
async def _may_pull() -> bool:
|
||||
if spec.gate is not None and not spec.gate():
|
||||
return False
|
||||
if not spec.barrier:
|
||||
return True
|
||||
return await quiescent(flow, spec.upstream)
|
||||
|
||||
try:
|
||||
while not flow.stop:
|
||||
tasks = {t for t in tasks if not t.done()}
|
||||
# Fill the pipeline: pull fresh cards and dispatch until `inflight` packages run.
|
||||
if await _may_pull():
|
||||
while len(tasks) < inflight:
|
||||
rows = await db.kanban_pull(topic, spec.board, spec.stage, batch + len(claimed))
|
||||
fresh = [r for r in rows if r["card_id"] not in claimed][:batch]
|
||||
if not fresh:
|
||||
break
|
||||
for r in fresh:
|
||||
claimed.add(r["card_id"])
|
||||
tasks.add(asyncio.create_task(_run(list(fresh))))
|
||||
if tasks: # busy → wait for a package to finish, then refill
|
||||
await asyncio.wait(tasks, timeout=_POLL, return_when=asyncio.FIRST_COMPLETED)
|
||||
continue
|
||||
# idle: nothing in flight and nothing pulled
|
||||
if await _idle_exit():
|
||||
# Real grace sleep (NOT _sleep_wake — the wake event is usually already set
|
||||
# by the last package and would collapse the window to 0ms). A producer
|
||||
# attached during the lull flips research_done and keeps us alive.
|
||||
await asyncio.sleep(_POLL)
|
||||
if await _idle_exit():
|
||||
return # nothing left and nothing upstream can produce
|
||||
continue
|
||||
await _sleep_wake(flow)
|
||||
finally:
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
async def run_flow(flow: Flow, stages: list[Stage], producers=(), set_p=None) -> None:
|
||||
"""Run producers + one worker per stage until global quiescence. `producers` are coroutines
|
||||
already counted via flow.add_producer() BEFORE this call (quiescence race). Registers the
|
||||
flow in active_flows for live attach/cancel."""
|
||||
active_flows[flow.topic] = flow
|
||||
names = [s.stage for s in stages]
|
||||
|
||||
def _spawn_workers():
|
||||
return [asyncio.ensure_future(_worker(flow, s, 1 if s.serial else WORKER_INFLIGHT, names))
|
||||
for s in stages]
|
||||
|
||||
workers = [asyncio.ensure_future(p) for p in producers] + _spawn_workers()
|
||||
progress = asyncio.create_task(_progress(flow, set_p)) if set_p else None
|
||||
try:
|
||||
while True:
|
||||
await asyncio.gather(*workers, return_exceptions=True)
|
||||
# Restart round: a producer attached exactly as the workers exited (missed even the
|
||||
# grace sleep) leaves live producers or queued cards behind → run the workers again.
|
||||
if flow.stop or (flow.research_done and await quiescent(flow, names)):
|
||||
break
|
||||
workers = _spawn_workers()
|
||||
finally:
|
||||
flow.stop = True
|
||||
if progress:
|
||||
progress.cancel()
|
||||
if active_flows.get(flow.topic) is flow:
|
||||
active_flows.pop(flow.topic, None)
|
||||
|
||||
|
||||
async def _progress(flow: Flow, set_p):
|
||||
while not flow.stop:
|
||||
try:
|
||||
counts = await db.kanban_stage_counts(flow.topic)
|
||||
total = sum(n for stages in counts.values() for n in stages.values())
|
||||
set_p(f"Kanban: {total} Karten im Fluss")
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(1.0)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Projektliste: konfigurierte Pfade aus projekte.txt (eine Zeile = absoluter Pfad,
|
||||
#-Kommentare). Von Hand editierbar — UI-Verwaltung kommt später (YAGNI)."""
|
||||
from pathlib import Path
|
||||
|
||||
import config
|
||||
|
||||
|
||||
def lade() -> list[dict]:
|
||||
"""→ [{name, pfad, fehler?}]. Toter Pfad oder Basename-Kollision wird als Eintrag
|
||||
mit `fehler` gemeldet, nie still verschluckt."""
|
||||
if not config.PROJEKTE_DATEI.is_file():
|
||||
return []
|
||||
out: list[dict] = []
|
||||
gesehen: dict[str, str] = {}
|
||||
for zeile in config.PROJEKTE_DATEI.read_text(encoding="utf-8").splitlines():
|
||||
zeile = zeile.strip()
|
||||
if not zeile or zeile.startswith("#"):
|
||||
continue
|
||||
pfad = Path(zeile).expanduser()
|
||||
name = pfad.name
|
||||
eintrag = {"name": name, "pfad": str(pfad)}
|
||||
if not pfad.is_dir():
|
||||
eintrag["fehler"] = "Pfad existiert nicht"
|
||||
elif name in gesehen:
|
||||
eintrag["fehler"] = f"Namenskollision mit {gesehen[name]}"
|
||||
else:
|
||||
gesehen[name] = str(pfad)
|
||||
out.append(eintrag)
|
||||
return out
|
||||
|
||||
|
||||
def finde(name: str) -> Path | None:
|
||||
for p in lade():
|
||||
if p["name"] == name and "fehler" not in p:
|
||||
return Path(p["pfad"])
|
||||
return None
|
||||
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
testpaths = tests
|
||||
@@ -0,0 +1,53 @@
|
||||
"""git-Helfer (nackte CLI, keine Bibliothek): Anker-Stände und Diff fürs Re-Scannen."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _git(wurzel: Path, *args: str) -> str:
|
||||
res = subprocess.run(["git", "-C", str(wurzel), *args],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args)}: {res.stderr.strip() or res.stdout.strip()}")
|
||||
return res.stdout
|
||||
|
||||
|
||||
def ist_repo(wurzel: Path) -> bool:
|
||||
try:
|
||||
_git(wurzel, "rev-parse", "--git-dir")
|
||||
return True
|
||||
except (RuntimeError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
|
||||
def head_hash(wurzel: Path) -> str:
|
||||
return _git(wurzel, "rev-parse", "--short=12", "HEAD").strip()
|
||||
|
||||
|
||||
def working_tree_sauber(wurzel: Path) -> bool:
|
||||
"""False bei uncommitteten Änderungen (inkl. untracked) — Scan/Board arbeiten nur
|
||||
auf sauberem Tree, sonst vermischen sich Stände. AUSNAHME `.planer/`: die eigenen
|
||||
Artefakte (z. B. aus einem Teillauf) dürfen den Folgelauf nicht blockieren."""
|
||||
for zeile in _git(wurzel, "status", "--porcelain").splitlines():
|
||||
pfad = zeile[3:].split(" -> ")[-1].strip().strip('"')
|
||||
if not pfad.startswith(".planer/"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def geaenderte_dateien(wurzel: Path, alt: str, neu: str = "HEAD") -> tuple[set[str], set[str]]:
|
||||
"""(geändert_oder_neu, gelöscht) zwischen `alt` und `neu`, `.planer/` ausgefiltert.
|
||||
Renames (R) = alt gelöscht + neu geändert (v1: kein Anker-Matching über Renames)."""
|
||||
geaendert: set[str] = set()
|
||||
geloescht: set[str] = set()
|
||||
for zeile in _git(wurzel, "diff", "--name-status", f"{alt}..{neu}").splitlines():
|
||||
teile = zeile.split("\t")
|
||||
status = teile[0]
|
||||
if status.startswith("R") and len(teile) == 3:
|
||||
geloescht.add(teile[1])
|
||||
geaendert.add(teile[2])
|
||||
elif status == "D":
|
||||
geloescht.add(teile[1])
|
||||
elif len(teile) >= 2:
|
||||
geaendert.add(teile[1])
|
||||
kein_planer = lambda s: not s.startswith(".planer/") # noqa: E731
|
||||
return set(filter(kein_planer, geaendert)), set(filter(kein_planer, geloescht))
|
||||
@@ -0,0 +1,108 @@
|
||||
"""API des Viewers (Phase 2). Lese-Routen über viewer.py, Lauf-Routen über scan_dienst."""
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
import agents
|
||||
import database as db
|
||||
import projekte
|
||||
import scan_dienst
|
||||
import viewer
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
def _wurzel(name: str) -> Path:
|
||||
wurzel = projekte.finde(name)
|
||||
if wurzel is None:
|
||||
raise HTTPException(404, f"Projekt '{name}' nicht in projekte.txt")
|
||||
return wurzel
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/projekte")
|
||||
async def liste():
|
||||
out = []
|
||||
for p in projekte.lade():
|
||||
eintrag = dict(p)
|
||||
if "fehler" not in p:
|
||||
wurzel = Path(p["pfad"])
|
||||
eintrag["hat_planer"] = (wurzel / ".planer" / "kern.md").is_file()
|
||||
staende = {d: s for d, (s, _) in scan_dienst.einheiten_bestand(wurzel).items()}
|
||||
eintrag["drift"] = viewer.drift(wurzel, staende)["veraltet"]
|
||||
eintrag["letzter_scan_ts"] = await db.letzter_agent_ts(p["name"])
|
||||
lauf = scan_dienst.status(p["name"])
|
||||
eintrag["scan_laeuft"] = bool(lauf and lauf["laeuft"])
|
||||
out.append(eintrag)
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/projekte/{name}")
|
||||
async def projekt(name: str):
|
||||
wurzel = _wurzel(name)
|
||||
daten = viewer.lade_projekt(wurzel)
|
||||
einheiten_kurz = {eid: {"beschreibung": e["beschreibung"]}
|
||||
for eid, e in daten["einheiten"].items()}
|
||||
return {
|
||||
"name": name,
|
||||
"kern": daten["kern"],
|
||||
"bereiche": daten["bereiche"],
|
||||
"architektur": daten["architektur"],
|
||||
"flows": daten["flows"],
|
||||
"pruefung": daten["pruefung"],
|
||||
"einheiten": einheiten_kurz,
|
||||
"doku_ordnung": viewer.doku_ordnung(daten),
|
||||
"drift": viewer.drift(wurzel, daten["staende"]),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/projekte/{name}/einheit")
|
||||
async def einheit(name: str, id: str):
|
||||
wurzel = _wurzel(name)
|
||||
daten = viewer.lade_projekt(wurzel)
|
||||
e = daten["einheiten"].get(id)
|
||||
if e is None:
|
||||
raise HTTPException(404, f"Einheit '{id}' unbekannt")
|
||||
quelldatei = id.partition("::")[0]
|
||||
veraltet = quelldatei in viewer.drift(wurzel, daten["staende"])["veraltet"]
|
||||
code = viewer.code_extrahieren(wurzel, id)
|
||||
for anker in e["anker"]:
|
||||
zusatz = viewer.code_extrahieren(wurzel, anker)
|
||||
if zusatz and zusatz not in code:
|
||||
code += ("\n\n" if code else "") + zusatz
|
||||
return {**e, "code": code, "veraltet": veraltet}
|
||||
|
||||
|
||||
@router.post("/projekte/{name}/scan")
|
||||
async def scan_starten(name: str, body: dict):
|
||||
wurzel = _wurzel(name)
|
||||
art = body.get("art", "scan")
|
||||
if art not in ("scan", "rescan"):
|
||||
raise HTTPException(400, f"unbekannte Art '{art}'")
|
||||
try:
|
||||
status = scan_dienst.starte(name, wurzel, art)
|
||||
except scan_dienst.VorflugFehler as e:
|
||||
raise HTTPException(409, str(e))
|
||||
return {"status": status}
|
||||
|
||||
|
||||
@router.get("/projekte/{name}/scan-status")
|
||||
async def scan_status(name: str):
|
||||
_wurzel(name)
|
||||
lauf = scan_dienst.status(name)
|
||||
if lauf is None:
|
||||
return {"laeuft": False, "art": None, "ergebnis": None, "stufen": {}, "agenten": [], "dead": []}
|
||||
out = dict(lauf)
|
||||
if lauf["laeuft"]:
|
||||
out["stufen"] = (await db.kanban_stage_counts(name)).get("scan", {})
|
||||
out["agenten"] = agents.active_agents(f"scan-{name}-")
|
||||
out["dead"] = [c["card_id"] for c in await db.kanban_dead(name)]
|
||||
else:
|
||||
out["stufen"] = {}
|
||||
out["agenten"] = []
|
||||
out["dead"] = (lauf["ergebnis"] or {}).get("dead", [])
|
||||
return out
|
||||
@@ -0,0 +1,319 @@
|
||||
"""Scan-Board: zerlegt ein Projekt-Repo in Einheiten und Sichten (.planer/-Dateien).
|
||||
|
||||
Stages (Engine: kanban.py):
|
||||
Producer (Chunker, mechanisch) → schneiden (LLM je Chunk) → nachfass (LLM, nur bei
|
||||
Abdeckungslücken) → sammeln (mechanisch, drain: Fragmente je Quelldatei mergen,
|
||||
Sicht-Karten anlegen) → sichten (LLM je Sicht-Art) → schreiben (mechanisch, drain:
|
||||
.planer/ ins Ziel-Repo).
|
||||
|
||||
Payload-Disziplin: Chunk-Text und Fragmente bleiben in der Karte (~15 KB), ab `sammeln`
|
||||
tragen Karten nur noch Staging-Pfade (storage/laeufe/<run>/).
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import artefakte
|
||||
import chunker
|
||||
import config
|
||||
import database as db
|
||||
import gates
|
||||
from agents import AgentInfraError, run_agent
|
||||
from kanban import Flow, Stage, chain_stages, run_flow
|
||||
|
||||
log = logging.getLogger("planer.scan")
|
||||
|
||||
BOARD = "scan"
|
||||
SICHT_ARTEN = ("features", "architektur", "flows")
|
||||
MAX_NACHFASS_SYMBOLE = 40 # Notbremse gegen endlos wachsende Nachfass-Prompts
|
||||
|
||||
|
||||
def _template(name: str) -> str:
|
||||
return (config.TEMPLATES_DIR / f"{name}.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _prompt(name: str, **ersetzungen: str) -> str:
|
||||
text = _template(name)
|
||||
for k, v in ersetzungen.items():
|
||||
text = text.replace(f"<{k}>", v)
|
||||
return text
|
||||
|
||||
|
||||
async def _llm(flow: Flow, stufe: str, card_id: str, rolle: str, prompt: str, label: str) -> str:
|
||||
"""Ein Gate-gesicherter LLM-Call: rc!=0 → ValueError (Engine-Backoff);
|
||||
AgentInfraError propagiert (Lauf pausiert)."""
|
||||
rc, out, err = await run_agent(
|
||||
f"scan-{flow.topic}-{stufe}-{card_id}", prompt, config.AGENT_TIMEOUT,
|
||||
role=rolle, scope=flow.topic, label=label)
|
||||
if rc != 0:
|
||||
raise ValueError(f"{stufe} rc={rc}: {(err or out)[:200]}")
|
||||
return out
|
||||
|
||||
|
||||
class ScanKontext:
|
||||
def __init__(self, wurzel: Path, stand: str, lauf_dir: Path):
|
||||
self.wurzel = wurzel
|
||||
self.stand = stand # HEAD-Hash des Ziel-Repos beim Scan-Start
|
||||
self.lauf_dir = lauf_dir # Staging: storage/laeufe/<run>/
|
||||
self.quelle_cache: dict = {} # Datei → Text (Gates lesen viel)
|
||||
self.bestand: dict[str, str] = {} # unveränderte Einheiten-Dateien (Re-Scan)
|
||||
|
||||
|
||||
# --- Prozessoren ---------------------------------------------------------------------
|
||||
|
||||
async def _je_karte(flow: Flow, cards: list[dict], verarbeite) -> None:
|
||||
"""Karten eines Pakets parallel verarbeiten; Erfolge advancen, dann ersten Fehler
|
||||
werfen (nur unadvancte Karten gehen in den Backoff). AgentInfraError hat Vorrang."""
|
||||
ergebnisse = await asyncio.gather(*(verarbeite(c) for c in cards), return_exceptions=True)
|
||||
moves = [(c["card_id"], ziel) for c, ziel in zip(cards, ergebnisse) if isinstance(ziel, str)]
|
||||
if moves:
|
||||
await db.kanban_advance_many(flow.topic, BOARD, moves)
|
||||
flow.wake.set()
|
||||
fehler = [e for e in ergebnisse if isinstance(e, BaseException)]
|
||||
for e in fehler:
|
||||
if isinstance(e, AgentInfraError):
|
||||
raise e
|
||||
if fehler:
|
||||
raise fehler[0]
|
||||
|
||||
|
||||
def _mit_befunden(prompt: str, payload: dict) -> str:
|
||||
"""Repair-Muster (Creator-Lehre): der Retry bekommt die Gate-Befunde des letzten
|
||||
Versuchs — blinde Wiederholung heilt systematische Fehler nicht."""
|
||||
befunde = payload.get("befunde")
|
||||
if not befunde:
|
||||
return prompt
|
||||
liste = "\n".join(f"- {b}" for b in befunde[:6])
|
||||
return (prompt + "\n\n## WICHTIG: Korrektur\n\nDein vorheriger Versuch scheiterte an "
|
||||
"diesen Prüf-Befunden. Behebe GENAU diese Punkte (Zitate müssen wörtlich aus "
|
||||
"der Quelle stammen — kopiere sie zeichengenau):\n" + liste + "\n")
|
||||
|
||||
|
||||
async def _befunde_merken(flow: Flow, card: dict, fehler: list[str]) -> None:
|
||||
card["payload"]["befunde"] = fehler[:6]
|
||||
await db.kanban_set_payload(flow.topic, BOARD, card["card_id"], card["payload"])
|
||||
|
||||
|
||||
async def _proc_schneiden(ctx: ScanKontext, flow: Flow, cards: list[dict]) -> None:
|
||||
async def verarbeite(c: dict) -> str:
|
||||
p = c["payload"]
|
||||
prompt = _mit_befunden(
|
||||
_prompt("einheiten", DATEI=p["datei"], QUELLE=p["text"],
|
||||
SYMBOLE="\n".join(f"- {s}" for s in p["symbole"]) or "- (keine)"), p)
|
||||
out = await _llm(flow, "schneiden", c["card_id"], "schneiden", prompt, p["datei"])
|
||||
out, gestrichen = gates.repariere_kanten(out, ctx.wurzel, p["datei"], ctx.quelle_cache)
|
||||
out, korrigiert = gates.repariere_belege(out, ctx.wurzel, p["datei"], ctx.quelle_cache)
|
||||
fehler, warnungen, einheiten = gates.pruefe_fragment(out, ctx.wurzel, p["datei"],
|
||||
ctx.quelle_cache)
|
||||
if fehler:
|
||||
await _befunde_merken(flow, c, fehler)
|
||||
raise ValueError(f"Gates: {'; '.join(fehler[:3])}")
|
||||
warnungen += [f"Kante gestrichen: {z}" for z in gestrichen]
|
||||
warnungen += [f"Beleg geschnappt: {z}" for z in korrigiert]
|
||||
_, zusatz, _ = gates.parse_einheiten(out)
|
||||
fehlend = gates.fehlende_symbole(p["symbole"], einheiten, zusatz)
|
||||
p["fragmente"] = [out]
|
||||
p["fehlend"] = fehlend
|
||||
p["warnungen"] = warnungen
|
||||
p.pop("befunde", None)
|
||||
await db.kanban_set_payload(flow.topic, BOARD, c["card_id"], p)
|
||||
return "nachfass" if fehlend else "sammeln"
|
||||
await _je_karte(flow, cards, verarbeite)
|
||||
|
||||
|
||||
async def _proc_nachfass(ctx: ScanKontext, flow: Flow, cards: list[dict]) -> None:
|
||||
async def verarbeite(c: dict) -> str:
|
||||
p = c["payload"]
|
||||
fehlend = p["fehlend"][:MAX_NACHFASS_SYMBOLE]
|
||||
prompt = _mit_befunden(
|
||||
_prompt("nachfass", DATEI=p["datei"], QUELLE=p["text"],
|
||||
FEHLEND="\n".join(f"- {s}" for s in fehlend),
|
||||
VORHANDEN="\n\n".join(p["fragmente"])), p)
|
||||
out = await _llm(flow, "nachfass", c["card_id"], "nachfass", prompt, p["datei"])
|
||||
out, _ = gates.repariere_kanten(out, ctx.wurzel, p["datei"], ctx.quelle_cache)
|
||||
out, _ = gates.repariere_belege(out, ctx.wurzel, p["datei"], ctx.quelle_cache)
|
||||
fehler, _, _ = gates.pruefe_fragment(out, ctx.wurzel, p["datei"], ctx.quelle_cache) \
|
||||
if out.strip().startswith("## ") else ([], [], [])
|
||||
if fehler:
|
||||
await _befunde_merken(flow, c, fehler)
|
||||
raise ValueError(f"Gates (Nachfass): {'; '.join(fehler[:3])}")
|
||||
# Abdeckung über ALLE Fragmente des Chunks prüfen
|
||||
gesamt = "\n\n".join(p["fragmente"] + [out])
|
||||
einheiten, zusatz, _ = gates.parse_einheiten(gesamt)
|
||||
immer_noch = gates.fehlende_symbole(p["symbole"], einheiten, zusatz)
|
||||
if immer_noch:
|
||||
raise ValueError(f"Nachfass ließ Symbole offen: {', '.join(immer_noch[:5])}")
|
||||
p["fragmente"].append(out)
|
||||
p["fehlend"] = []
|
||||
p.pop("befunde", None)
|
||||
await db.kanban_set_payload(flow.topic, BOARD, c["card_id"], p)
|
||||
return "sammeln"
|
||||
await _je_karte(flow, cards, verarbeite)
|
||||
|
||||
|
||||
async def _proc_sammeln(ctx: ScanKontext, flow: Flow, cards: list[dict]) -> None:
|
||||
"""Drain: alle Chunk-Karten liegen vor. Fragmente je Quelldatei mergen, Staging
|
||||
schreiben, Übersichten bauen, eine Karte je Sicht-Art anlegen."""
|
||||
je_datei: dict[str, list[dict]] = {}
|
||||
for c in cards:
|
||||
je_datei.setdefault(c["payload"]["datei"], []).append(c)
|
||||
(ctx.lauf_dir / "einheiten").mkdir(parents=True, exist_ok=True)
|
||||
ids: set[str] = set()
|
||||
kurz_zeilen: list[str] = [] # ID + beschreibung (features)
|
||||
kanten_zeilen: list[str] = [] # ID + beschreibung + kanten (architektur/flows)
|
||||
for datei, karten in sorted(je_datei.items()):
|
||||
karten.sort(key=lambda c: c["payload"]["start"])
|
||||
rumpf = artefakte.merge_fragmente([f for c in karten for f in c["payload"]["fragmente"]])
|
||||
text = artefakte.einheiten_datei(datei, rumpf, ctx.stand)
|
||||
(ctx.lauf_dir / artefakte.datei_pfad(datei)).write_text(text, encoding="utf-8")
|
||||
ids |= artefakte.alle_ids(text)
|
||||
einheiten, _, _ = gates.parse_einheiten(text)
|
||||
for e in einheiten:
|
||||
kurz_zeilen.append(f"- {e['id']}: {e['felder'].get('beschreibung', '')}")
|
||||
kanten = "; ".join(f"{typ} {ziel}" for _, typ, ziel in e["kanten"])
|
||||
kanten_zeilen.append(kurz_zeilen[-1] + (f"\n kanten: {kanten}" if kanten else ""))
|
||||
for datei, text in ctx.bestand.items(): # Re-Scan: unveränderte Dateien zählen mit
|
||||
ids |= artefakte.alle_ids(text)
|
||||
einheiten, _, _ = gates.parse_einheiten(text)
|
||||
for e in einheiten:
|
||||
kurz_zeilen.append(f"- {e['id']}: {e['felder'].get('beschreibung', '')}")
|
||||
kanten = "; ".join(f"{typ} {ziel}" for _, typ, ziel in e["kanten"])
|
||||
kanten_zeilen.append(kurz_zeilen[-1] + (f"\n kanten: {kanten}" if kanten else ""))
|
||||
(ctx.lauf_dir / "uebersicht-kurz.md").write_text("\n".join(sorted(kurz_zeilen)), encoding="utf-8")
|
||||
(ctx.lauf_dir / "uebersicht-kanten.md").write_text("\n".join(sorted(kanten_zeilen)), encoding="utf-8")
|
||||
(ctx.lauf_dir / "ids.txt").write_text("\n".join(sorted(ids)), encoding="utf-8")
|
||||
for art in SICHT_ARTEN:
|
||||
await db.kanban_upsert_card(flow.topic, BOARD, f"sicht-{art}", "sicht", "sichten",
|
||||
{"art": art})
|
||||
await db.kanban_advance_many(flow.topic, BOARD,
|
||||
[(c["card_id"], "chunk-fertig") for c in cards])
|
||||
flow.wake.set()
|
||||
|
||||
|
||||
async def _proc_sichten(ctx: ScanKontext, flow: Flow, cards: list[dict]) -> None:
|
||||
ids = set((ctx.lauf_dir / "ids.txt").read_text(encoding="utf-8").splitlines())
|
||||
|
||||
async def verarbeite(c: dict) -> str:
|
||||
art = c["payload"]["art"]
|
||||
uebersicht = "uebersicht-kurz.md" if art == "features" else "uebersicht-kanten.md"
|
||||
prompt = _mit_befunden(
|
||||
_prompt(f"sichten-{art}", PROJEKT=flow.topic,
|
||||
EINHEITEN=(ctx.lauf_dir / uebersicht).read_text(encoding="utf-8")), c["payload"])
|
||||
out = await _llm(flow, "sichten", c["card_id"], "sichten", prompt, art)
|
||||
out, gestrichen = gates.repariere_referenzen(out, ids)
|
||||
if gestrichen:
|
||||
log.info("scan %s: %s — %d unbekannte Referenzen gestrichen", flow.topic, art,
|
||||
len(gestrichen))
|
||||
fehler = gates.pruefe_referenzen(out, ids)
|
||||
if fehler:
|
||||
await _befunde_merken(flow, c, fehler)
|
||||
raise ValueError(f"Referenzen ({art}): {'; '.join(fehler[:3])}")
|
||||
(ctx.lauf_dir / f"sicht-{art}.md").write_text(out, encoding="utf-8")
|
||||
return "schreiben"
|
||||
await _je_karte(flow, cards, verarbeite)
|
||||
|
||||
|
||||
async def _proc_schreiben(ctx: ScanKontext, flow: Flow, cards: list[dict]) -> None:
|
||||
"""Drain nach allen Sichten: .planer/ im Ziel-Repo schreiben."""
|
||||
dateien: dict[str, str] = {}
|
||||
for p in (ctx.lauf_dir / "einheiten").glob("*.md"):
|
||||
dateien[f"einheiten/{p.name}"] = p.read_text(encoding="utf-8")
|
||||
for datei, text in ctx.bestand.items():
|
||||
dateien.setdefault(artefakte.datei_pfad(datei), text)
|
||||
fehlend: list[str] = [] # gescheiterte Sichten: auslassen statt Kaskade (sichtbar degradieren)
|
||||
if (ctx.lauf_dir / "sicht-features.md").is_file():
|
||||
features = (ctx.lauf_dir / "sicht-features.md").read_text(encoding="utf-8")
|
||||
kern, _, bereiche = features.partition("# Bereich:")
|
||||
dateien["kern.md"] = kern.strip() + "\n"
|
||||
for i, block in enumerate(("# Bereich:" + bereiche).split("# Bereich:")):
|
||||
if not block.strip():
|
||||
continue
|
||||
name = block.splitlines()[0].strip().lower().replace(" ", "-").replace("/", "-")
|
||||
dateien[f"features/{name or f'bereich-{i}'}.md"] = "# Bereich:" + block.rstrip() + "\n"
|
||||
else:
|
||||
fehlend.append("features")
|
||||
for art, zieldatei in (("architektur", "architektur.md"), ("flows", "flows.md")):
|
||||
if (ctx.lauf_dir / f"sicht-{art}.md").is_file():
|
||||
dateien[zieldatei] = (ctx.lauf_dir / f"sicht-{art}.md").read_text(encoding="utf-8")
|
||||
else:
|
||||
fehlend.append(art)
|
||||
if fehlend:
|
||||
flow.state["sichten_fehlend"] = fehlend
|
||||
log.warning("scan %s: Sichten fehlen (Karten dead): %s", flow.topic, ", ".join(fehlend))
|
||||
if not (ctx.wurzel / ".planer" / "pruefung.md").is_file(): # nie Nutzer-Edits überschreiben
|
||||
dateien["pruefung.md"] = artefakte.pruefung_geruest(ctx.wurzel, flow.topic)
|
||||
artefakte.schreibe_planer(ctx.wurzel, dateien)
|
||||
flow.state["geschrieben"] = sorted(dateien)
|
||||
await db.kanban_advance_many(flow.topic, BOARD, [(c["card_id"], "fertig") for c in cards])
|
||||
flow.wake.set()
|
||||
|
||||
|
||||
# --- Flow-Start ------------------------------------------------------------------------
|
||||
|
||||
def scan_stages(ctx: ScanKontext, flow: Flow) -> list[Stage]:
|
||||
# gate=research_done an den Barriere-Stufen ist PFLICHT (Abnahme-Befund): die Barriere
|
||||
# prüft nur die Upstream-Stufen — ohne Gate drained `sammeln`, während der Producer
|
||||
# noch Karten einreiht, und die Sichten sehen eine Teil-Übersicht.
|
||||
fertig_produziert = lambda: flow.research_done # noqa: E731
|
||||
return chain_stages([
|
||||
Stage(BOARD, "schneiden", lambda cs: _proc_schneiden(ctx, flow, cs)),
|
||||
Stage(BOARD, "nachfass", lambda cs: _proc_nachfass(ctx, flow, cs)),
|
||||
Stage(BOARD, "sammeln", lambda cs: _proc_sammeln(ctx, flow, cs),
|
||||
barrier=True, drain=True, gate=fertig_produziert),
|
||||
Stage(BOARD, "sichten", lambda cs: _proc_sichten(ctx, flow, cs)),
|
||||
Stage(BOARD, "schreiben", lambda cs: _proc_schreiben(ctx, flow, cs),
|
||||
barrier=True, drain=True, gate=fertig_produziert),
|
||||
])
|
||||
|
||||
|
||||
async def run_scan(wurzel: Path, projekt: str, stand: str, set_p=None,
|
||||
nur_dateien: set[str] | None = None,
|
||||
bestand: dict[str, str] | None = None,
|
||||
fortsetzen: bool = False) -> dict:
|
||||
"""Scan eines Projekt-Repos. `nur_dateien` beschränkt auf geänderte Dateien (Re-Scan);
|
||||
`bestand` = unveränderte Einheiten-Dateien {quelldatei: text}. `fortsetzen=True`
|
||||
setzt einen abgebrochenen/fehlgeschlagenen Lauf fort: tote Chunks laufen neu durchs
|
||||
LLM, fertige nur durchs mechanische Sammeln (Fragmente stecken im Payload) — setzt
|
||||
unveränderten HEAD voraus. → {status, geschrieben, dead}."""
|
||||
run_id = f"scan-{time.time_ns()}" # eindeutig auch bei zwei Läufen in derselben Sekunde
|
||||
lauf_dir = config.LAEUFE_DIR / projekt / run_id
|
||||
lauf_dir.mkdir(parents=True, exist_ok=True)
|
||||
ctx = ScanKontext(wurzel, stand, lauf_dir)
|
||||
ctx.bestand = bestand or {}
|
||||
flow = Flow(projekt)
|
||||
db.set_current_run(projekt, run_id)
|
||||
|
||||
async def producer():
|
||||
try:
|
||||
if fortsetzen:
|
||||
n_neu = n_fertig = 0
|
||||
for c in await db.kanban_cards(flow.topic, BOARD, kind="chunk"):
|
||||
if c["stage"] == "dead":
|
||||
await db.kanban_upsert_card(flow.topic, BOARD, c["card_id"],
|
||||
"chunk", "schneiden", None)
|
||||
n_neu += 1
|
||||
elif c["stage"] == "chunk-fertig":
|
||||
await db.kanban_upsert_card(flow.topic, BOARD, c["card_id"],
|
||||
"chunk", "sammeln", None)
|
||||
n_fertig += 1
|
||||
log.info("scan %s: Fortsetzung — %d neu, %d nur sammeln", projekt, n_neu, n_fertig)
|
||||
return
|
||||
chunks = await asyncio.to_thread(chunker.chunks_fuer_repo, wurzel, nur_dateien)
|
||||
for i, chunk in enumerate(chunks):
|
||||
await db.kanban_upsert_card(flow.topic, BOARD, f"chunk-{i:04d}", "chunk",
|
||||
"schneiden", chunk)
|
||||
log.info("scan %s: %d Chunks angelegt", projekt, len(chunks))
|
||||
finally:
|
||||
flow.done_producer()
|
||||
|
||||
flow.add_producer() # SYNCHRON vor create_task (Quieszenz-Race)
|
||||
try:
|
||||
await run_flow(flow, scan_stages(ctx, flow), [producer()], set_p)
|
||||
finally:
|
||||
db.set_current_run(projekt, None)
|
||||
dead = await db.kanban_dead(projekt)
|
||||
status = "pausiert" if flow.state.get("infra_paused") else ("fehler" if dead else "ok")
|
||||
return {"status": status, "infra_error": flow.state.get("infra_error", ""),
|
||||
"geschrieben": flow.state.get("geschrieben", []),
|
||||
"dead": [c["card_id"] for c in dead]}
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Scan-Dienst: Vorflug-Checks, Re-Scan-Planung und Hintergrund-Läufe.
|
||||
|
||||
Eine Wahrheit für CLI und Server: cli.py und routes.py delegieren beide hierher.
|
||||
Doppelstart-Guard nach Creator-Muster: prozess-lokales Dict + idempotente Antwort +
|
||||
Re-Entry-Check in der Coroutine (gilt nur im eigenen Prozess — bei laufendem Server
|
||||
Scans über die UI starten, nicht parallel per CLI).
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import agents
|
||||
import artefakte
|
||||
import chunker
|
||||
import config
|
||||
import repo
|
||||
import scan_board
|
||||
|
||||
log = logging.getLogger("planer.dienst")
|
||||
|
||||
|
||||
class VorflugFehler(Exception):
|
||||
"""Lauf darf nicht starten (kein Repo, dirty tree, Rolle/Provider unbrauchbar)."""
|
||||
|
||||
|
||||
def vorflug(wurzel: Path) -> str:
|
||||
"""Checks vor jedem Lauf → HEAD-Hash. Wirft VorflugFehler (Erkenntnis 13:
|
||||
lieber vorher stoppen als leer verbrennen)."""
|
||||
if not wurzel.is_dir():
|
||||
raise VorflugFehler(f"{wurzel} ist kein Verzeichnis")
|
||||
if not repo.ist_repo(wurzel):
|
||||
raise VorflugFehler(f"{wurzel} ist kein git-Repo — Anker brauchen Commit-Hashes (git init && commit)")
|
||||
if not repo.working_tree_sauber(wurzel):
|
||||
raise VorflugFehler(f"{wurzel} hat uncommittete Änderungen — erst committen, dann scannen")
|
||||
if not os.getenv("PLANER_FAKE_AGENTS"):
|
||||
for rolle in config.ROLLEN:
|
||||
try:
|
||||
provider, _ = config.resolve_role(rolle)
|
||||
except RuntimeError as e:
|
||||
raise VorflugFehler(str(e)) from e
|
||||
if not agents.provider_available(provider):
|
||||
raise VorflugFehler(f"Provider '{provider}' (Rolle {rolle}) nicht nutzbar — CLI/Key prüfen")
|
||||
return repo.head_hash(wurzel)
|
||||
|
||||
|
||||
def einheiten_bestand(wurzel: Path) -> dict[str, tuple[str, str]]:
|
||||
"""{quelldatei: (stand, text)} aus den vorhandenen .planer/einheiten/-Dateien."""
|
||||
out: dict[str, tuple[str, str]] = {}
|
||||
verzeichnis = wurzel / ".planer" / "einheiten"
|
||||
for p in verzeichnis.glob("*.md") if verzeichnis.is_dir() else []:
|
||||
text = p.read_text(encoding="utf-8")
|
||||
kopf = text.splitlines()[0] if text else ""
|
||||
datei = kopf.removeprefix("# Einheiten:").strip()
|
||||
stand = artefakte.lese_stand(text)
|
||||
if datei and stand:
|
||||
out[datei] = (stand, text)
|
||||
return out
|
||||
|
||||
|
||||
def rescan_plan(wurzel: Path) -> tuple[set[str], dict[str, str], list[str]]:
|
||||
"""→ (zu_scannen, unveraendert {datei: text}, geloeschte_quelldateien).
|
||||
Wirft VorflugFehler, wenn noch kein Scan existiert."""
|
||||
bestand = einheiten_bestand(wurzel)
|
||||
if not bestand:
|
||||
raise VorflugFehler("keine .planer/einheiten/ gefunden — erst `scan` ausführen")
|
||||
scanbar = {str(p.relative_to(wurzel)) for p in chunker.dateien(wurzel)}
|
||||
diff_cache: dict[str, set[str]] = {}
|
||||
|
||||
def geaendert_seit(stand: str) -> set[str]:
|
||||
if stand not in diff_cache:
|
||||
diff_cache[stand] = repo.geaenderte_dateien(wurzel, stand)[0]
|
||||
return diff_cache[stand]
|
||||
|
||||
zu_scannen: set[str] = scanbar - set(bestand)
|
||||
unveraendert: dict[str, str] = {}
|
||||
for datei, (stand_alt, text) in bestand.items():
|
||||
if datei not in scanbar:
|
||||
continue
|
||||
if stand_alt == "UNBEKANNT" or datei in geaendert_seit(stand_alt):
|
||||
zu_scannen.add(datei)
|
||||
else:
|
||||
unveraendert[datei] = text
|
||||
geloescht = [datei for datei in bestand if datei not in scanbar]
|
||||
return zu_scannen, unveraendert, geloescht
|
||||
|
||||
|
||||
async def rescan_lauf(wurzel: Path, projekt: str, stand: str, set_p=None) -> dict:
|
||||
"""Kompletter Re-Scan: Plan, Aufräumen gelöschter Artefakte, Lauf. Keine Änderungen →
|
||||
{status: 'unveraendert'}."""
|
||||
zu_scannen, unveraendert, geloescht = rescan_plan(wurzel)
|
||||
for datei in geloescht:
|
||||
(wurzel / ".planer" / artefakte.datei_pfad(datei)).unlink(missing_ok=True)
|
||||
if not zu_scannen and not geloescht:
|
||||
return {"status": "unveraendert", "geschrieben": [], "dead": []}
|
||||
if not zu_scannen and unveraendert:
|
||||
# Nur Löschungen: Sichten müssen neu — kleinste verbliebene Datei mit-rescannen
|
||||
zu_scannen = {min(unveraendert, key=lambda d: len(unveraendert[d]))}
|
||||
unveraendert.pop(next(iter(zu_scannen)))
|
||||
log.info("rescan %s: %d neu, %d unverändert, %d gelöscht",
|
||||
projekt, len(zu_scannen), len(unveraendert), len(geloescht))
|
||||
return await scan_board.run_scan(wurzel, projekt, stand, set_p=set_p,
|
||||
nur_dateien=zu_scannen, bestand=unveraendert)
|
||||
|
||||
|
||||
# --- Hintergrund-Läufe (Server) --------------------------------------------------------
|
||||
|
||||
_laeufe: dict[str, dict] = {} # projekt → {task, art, gestartet, ergebnis}
|
||||
|
||||
|
||||
def starte(name: str, wurzel: Path, art: str, fortsetzen: bool = False) -> str:
|
||||
"""Startet scan|rescan als Hintergrund-Task. → 'gestartet' | 'laeuft'.
|
||||
Wirft VorflugFehler synchron (Route antwortet 409 mit Meldung)."""
|
||||
eintrag = _laeufe.get(name)
|
||||
if eintrag and not eintrag["task"].done():
|
||||
return "laeuft"
|
||||
stand = vorflug(wurzel)
|
||||
if art == "rescan":
|
||||
rescan_plan(wurzel) # wirft synchron, wenn es keinen Erst-Scan gibt
|
||||
|
||||
async def lauf():
|
||||
if (e := _laeufe.get(name)) and e.get("laeuft_intern"): # Re-Entry-Schutz
|
||||
return
|
||||
_laeufe[name]["laeuft_intern"] = True
|
||||
try:
|
||||
if art == "rescan":
|
||||
ergebnis = await rescan_lauf(wurzel, name, stand)
|
||||
else:
|
||||
ergebnis = await scan_board.run_scan(wurzel, name, stand, fortsetzen=fortsetzen)
|
||||
_laeufe[name]["ergebnis"] = ergebnis
|
||||
except Exception as e: # Status muss den Fehler zeigen, nie stiller Tod
|
||||
log.exception("lauf %s (%s) abgebrochen", name, art)
|
||||
_laeufe[name]["ergebnis"] = {"status": "abbruch", "infra_error": str(e),
|
||||
"geschrieben": [], "dead": []}
|
||||
|
||||
_laeufe[name] = {"task": asyncio.create_task(lauf()), "art": art,
|
||||
"gestartet": time.time(), "ergebnis": None}
|
||||
return "gestartet"
|
||||
|
||||
|
||||
def status(name: str) -> dict | None:
|
||||
eintrag = _laeufe.get(name)
|
||||
if eintrag is None:
|
||||
return None
|
||||
return {"laeuft": not eintrag["task"].done(), "art": eintrag["art"],
|
||||
"gestartet": eintrag["gestartet"], "ergebnis": eintrag["ergebnis"]}
|
||||
@@ -0,0 +1,48 @@
|
||||
"""FastAPI-Server des Planers (Phase 2).
|
||||
|
||||
Start: `uvicorn server:app --port 8100` aus backend/ — OHNE --reload: der Reload killt
|
||||
laufende Scans im eigenen Prozess (Creator-Lehre; siehe README, Selbst-Änderungs-Regel).
|
||||
"""
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from fastapi import FastAPI # noqa: E402
|
||||
from fastapi.staticfiles import StaticFiles # noqa: E402
|
||||
from starlette.responses import Response # noqa: E402
|
||||
|
||||
import agents # noqa: E402
|
||||
import config # noqa: E402
|
||||
import database as db # noqa: E402
|
||||
from routes import router # noqa: E402
|
||||
|
||||
FRONTEND_DIST = config.PROJECT_ROOT / "frontend" / "dist"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await db.init_db()
|
||||
agents.on_event = db.add_event
|
||||
yield
|
||||
await db.close_db()
|
||||
|
||||
|
||||
class CachedStatic(StaticFiles):
|
||||
"""assets/ = unveränderlich (gehashte Namen), index.html = nie cachen."""
|
||||
|
||||
def file_response(self, full_path, stat_result, scope, status_code=200) -> Response:
|
||||
resp = super().file_response(full_path, stat_result, scope, status_code)
|
||||
pfad = str(full_path)
|
||||
if "/assets/" in pfad:
|
||||
resp.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
||||
elif pfad.endswith("index.html"):
|
||||
resp.headers["Cache-Control"] = "no-cache"
|
||||
return resp
|
||||
|
||||
|
||||
app = FastAPI(title="Planer", lifespan=lifespan)
|
||||
app.include_router(router)
|
||||
if FRONTEND_DIST.exists():
|
||||
app.mount("/", CachedStatic(directory=FRONTEND_DIST, html=True), name="frontend")
|
||||
@@ -0,0 +1,53 @@
|
||||
# Auftrag: Code-Ausschnitt in Einheiten zerlegen
|
||||
|
||||
Du bekommst unten einen Ausschnitt der Datei `<DATEI>` aus einem Software-Projekt.
|
||||
Zerlege ihn in **Einheiten** und gib NUR die Einheiten-Abschnitte im unten definierten
|
||||
Format zurück — nichts davor, nichts danach, keine Code-Fences um das Gesamtergebnis.
|
||||
|
||||
Diese Top-Level-Symbole des Ausschnitts MÜSSEN alle abgedeckt sein:
|
||||
<SYMBOLE>
|
||||
|
||||
## Was ist eine Einheit
|
||||
|
||||
Das Kleinste mit **eigener Verantwortung**, das sich in einem Satz beschreiben lässt —
|
||||
meist eine Funktion oder Klasse; mehrere kleine, zusammengehörige Symbole dürfen eine
|
||||
Einheit bilden (Hauptsymbol als ID, weitere je als `anker:`-Zeile). Jedes gelistete
|
||||
Symbol gehört zu genau einer Einheit (MECE).
|
||||
|
||||
Hat der Ausschnitt KEINE def/class-Symbole (reines Skript, Konfiguration), erzeuge
|
||||
GENAU EINE Datei-Einheit mit der ID `<DATEI>` (ohne `::`) — erfinde NIE Pseudo-Symbole.
|
||||
|
||||
## Ausgabeformat (exakt einhalten)
|
||||
|
||||
```
|
||||
## <DATEI>::<hauptsymbol>
|
||||
beschreibung: <genau ein Satz: die Verantwortung — EINE Aufgabe, kein „und"-Katalog>
|
||||
input: <was die Einheit entgegennimmt — eine knappe Zeile>
|
||||
output: <was sie liefert bzw. bewirkt — eine knappe Zeile>
|
||||
entscheidungen:
|
||||
- <kurzer Satz: eine im Code getroffene Design-Entscheidung oder Garantie>
|
||||
beleg: "<kurzes WÖRTLICHES Zitat aus dem Ausschnitt, das sie stützt>"
|
||||
kanten:
|
||||
- ruft-auf: <einheiten-id>
|
||||
- nutzt: <einheiten-id>
|
||||
- wird-genutzt-von: <einheiten-id>
|
||||
```
|
||||
|
||||
Regeln:
|
||||
- `beschreibung:` genau EIN Satz, EINE Aufgabe. `input:`/`output:` sind Pflicht.
|
||||
- 2–5 Entscheidungen je Einheit: Design-Entscheidungen und Garantien, KEIN
|
||||
Implementierungs-Nacherzählen. Jede hat GENAU EINE `beleg:`-Zeile auf eigener Zeile.
|
||||
- Der Beleg muss WÖRTLICH (Zeichen für Zeichen) im Ausschnitt vorkommen und ein
|
||||
zusammenhängender Ausschnitt aus GENAU EINER Quellzeile sein — NIEMALS mehrere
|
||||
Zeilen zu einer zusammenziehen. Max. ~60 Zeichen.
|
||||
- Kanten-Typen nur: `ruft-auf`, `nutzt`, `wird-genutzt-von`, `löst-aus`,
|
||||
`wird-ausgelöst-von`. Hinter dem Ziel steht NICHTS mehr. Ziele in anderen Modulen
|
||||
(aus Imports ableitbar) als `pfad/zur/datei.py::symbol`. Keine Kante erfinden —
|
||||
lieber weniger als falsche.
|
||||
- Schreibe auf Deutsch.
|
||||
|
||||
## Ausschnitt aus `<DATEI>`
|
||||
|
||||
```python
|
||||
<QUELLE>
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Auftrag: Fehlende Einheiten ergänzen
|
||||
|
||||
Ein Ausschnitt der Datei `<DATEI>` wurde bereits in Einheiten zerlegt (Bestand unten).
|
||||
Die Abdeckungsprüfung meldet, dass folgende Top-Level-Symbole noch KEINER Einheit
|
||||
zugeordnet sind:
|
||||
|
||||
<FEHLEND>
|
||||
|
||||
Erzeuge NUR die fehlenden Einheiten-Abschnitte im selben Format wie der Bestand —
|
||||
nichts davor, nichts danach, keine Wiederholung bestehender Einheiten, keine
|
||||
Code-Fences um das Gesamtergebnis.
|
||||
|
||||
NUR WENN ein fehlendes Symbol ein unselbstständiger Hilfssatellit einer bestehenden
|
||||
Einheit ist — es wird AUSSCHLIESSLICH vom Hauptsymbol dieser Einheit verwendet und hat
|
||||
keine eigene Verantwortung — darfst du statt einer neuen Einheit eine Zeile
|
||||
`ergaenze-anker: <DATEI>::<symbol> zu <bestehende-einheiten-id>` ausgeben.
|
||||
Im Zweifel IMMER eine eigene Einheit. Symbole mit benennbarer eigener Aufgabe
|
||||
(Exception-Klasse, Schema-Bauer, Konstante mit Bedeutung) sind IMMER eigene Einheiten.
|
||||
|
||||
Format-Regeln wie beim Erst-Scan: `beschreibung:` (ein Satz), `input:`, `output:`,
|
||||
2–5 `entscheidungen:` mit je GENAU EINER `beleg:`-Zeile (WÖRTLICHES Zitat aus GENAU
|
||||
EINER Quellzeile, max. ~60 Zeichen), `kanten:` nur mit den bekannten Typen.
|
||||
Schreibe auf Deutsch.
|
||||
|
||||
## Bestand (bereits zerlegte Einheiten dieses Ausschnitts)
|
||||
|
||||
<VORHANDEN>
|
||||
|
||||
## Ausschnitt aus `<DATEI>`
|
||||
|
||||
```python
|
||||
<QUELLE>
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# Auftrag: Architektur-Sicht aggregieren
|
||||
|
||||
Unten stehen alle Einheiten des Projekts `<PROJEKT>` (ID + Beschreibung + Kanten).
|
||||
Aggregiere daraus die **Architektur-Sicht**: die Komponenten des Systems und ihre
|
||||
Beziehungen. Beschreibe NUR, was Einheiten und Kanten belegen.
|
||||
|
||||
## Ausgabeformat (exakt einhalten)
|
||||
|
||||
```
|
||||
# Architektur: <PROJEKT>
|
||||
|
||||
## Komponente: <kurzer Name, OHNE Klammern, OHNE „und">
|
||||
beschreibung: <EIN kurzer Satz: die Verantwortung der Komponente>
|
||||
einheiten: <einheiten-id>, <einheiten-id>, …
|
||||
beziehungen:
|
||||
- nutzt: <andere Komponente (exakter Name)>
|
||||
- wird-genutzt-von: <andere Komponente (exakter Name)>
|
||||
```
|
||||
|
||||
Regeln:
|
||||
- 3–8 Komponenten; jede Einheit gehört zu GENAU EINER Komponente (MECE).
|
||||
- Eine Beziehung nur, wenn mindestens eine Kante zwischen Einheiten der beiden
|
||||
Komponenten sie trägt — keine Beziehung erfinden.
|
||||
- `einheiten:` nur IDs, die unten wirklich vorkommen (exakte Schreibweise).
|
||||
- Schreibe auf Deutsch. Gib NUR das Markdown aus, ohne Code-Fences drumherum.
|
||||
|
||||
## Einheiten von `<PROJEKT>` (mit Kanten)
|
||||
|
||||
<EINHEITEN>
|
||||
@@ -0,0 +1,51 @@
|
||||
# Auftrag: Feature-Sicht und Kern aggregieren
|
||||
|
||||
Unten stehen alle Einheiten des Projekts `<PROJEKT>` (ID + Ein-Satz-Beschreibung).
|
||||
Aggregiere daraus die **Feature-Sicht** (was kann das Projekt aus Betreibersicht?)
|
||||
und den **Kern** (was trägt das Projekt?). Beschreibe NUR, was die Einheiten belegen —
|
||||
erfinde nichts aus Weltwissen.
|
||||
|
||||
WICHTIG — Zielgruppe ist die GESCHÄFTSFÜHRUNG, nicht Entwickler:
|
||||
- Beschreibe, WAS das Produkt kann und WOZU es gut ist — NIE, wie es im Code
|
||||
umgesetzt ist.
|
||||
- Verboten: Code-Begriffe und Jargon (Funktions-/Modul-/Dateinamen, Backticks,
|
||||
Wörter wie Präfix, Spawn, Semaphore, CLI, Cache, Payload, Thread, Lock, Registry,
|
||||
Callback, Endpoint). Wenn du so ein Wort brauchst, beschreibe stattdessen die
|
||||
Wirkung in Alltagssprache.
|
||||
- Titel und Sätze so, dass sie jemand ohne Programmierkenntnisse sofort versteht.
|
||||
Beispiel: statt „Markiert einen Schlüssel-Präfix global als abgebrochen" →
|
||||
„Laufende Arbeiten lassen sich gezielt stoppen".
|
||||
- Die `einheiten:`-Referenzzeilen bleiben unverändert technisch (exakte IDs) —
|
||||
sie sind Maschinen-Verweise, keine Leser-Texte.
|
||||
|
||||
## Ausgabeformat (exakt einhalten; zuerst der Kern, dann die Bereiche)
|
||||
|
||||
```
|
||||
# Kern: <PROJEKT>
|
||||
<2-4 Sätze: wofür das Projekt existiert und was es im Kern tut>
|
||||
|
||||
tragende-einheiten: <einheiten-id>, <einheiten-id>, …
|
||||
|
||||
# Bereich: <kurzer Name, OHNE Klammern, OHNE „und">
|
||||
beschreibung: <EIN kurzer Satz, der den Kern des Bereichs trifft — keine Aufzählung>
|
||||
|
||||
## Feature: <Titel OHNE „und" — genau EINE Aufgabe> [kern|rand]
|
||||
beschreibung: <EIN kurzer Satz ohne „und"-Aufzählung>
|
||||
|
||||
### <Teilfeature: Fähigkeits-Satz aus Betreibersicht, z. B. „Kann nach Abbruch fortsetzen">
|
||||
einheiten: <einheiten-id>, <einheiten-id>
|
||||
```
|
||||
|
||||
Regeln:
|
||||
- 3–10 Bereiche; MECE je Ebene: Bereiche zerlegen das Projekt, Features ihren Bereich,
|
||||
Teilfeatures ihr Feature — vollständig, überschneidungsfrei.
|
||||
- Braucht ein Titel „und", mach zwei Bereiche/Features daraus.
|
||||
- `[kern]` = trägt das Projekt, `[rand]` = Komfort/Beiwerk.
|
||||
- `einheiten:` listet NUR IDs, die unten wirklich vorkommen (exakte Schreibweise);
|
||||
jedes Teilfeature hat mindestens eine. Jede Einheit sollte in mindestens einem
|
||||
Teilfeature auftauchen; reine interne Helfer dürfen unreferenziert bleiben.
|
||||
- Schreibe auf Deutsch. Gib NUR das Markdown aus, ohne Code-Fences drumherum.
|
||||
|
||||
## Einheiten von `<PROJEKT>`
|
||||
|
||||
<EINHEITEN>
|
||||
@@ -0,0 +1,26 @@
|
||||
# Auftrag: Flow-Sicht aggregieren
|
||||
|
||||
Unten stehen alle Einheiten des Projekts `<PROJEKT>` (ID + Beschreibung + Kanten).
|
||||
Aggregiere daraus die **Flow-Sicht**: die wichtigsten Abläufe des Systems als
|
||||
Schrittfolgen. Beschreibe NUR Abläufe, die die Einheiten und ihre Kanten belegen.
|
||||
|
||||
## Ausgabeformat (exakt einhalten)
|
||||
|
||||
```
|
||||
# Flows: <PROJEKT>
|
||||
|
||||
## Flow: <kurzer Titel des Ablaufs, OHNE „und">
|
||||
1. <Schritt in einem Satz> — <einheiten-id>
|
||||
2. <Schritt in einem Satz> — <einheiten-id>, <einheiten-id>
|
||||
```
|
||||
|
||||
Regeln:
|
||||
- 2–6 Flows, je 3–10 Schritte; nur die tragenden Abläufe, keine Randfälle.
|
||||
- Jeder Schritt endet mit ` — ` und mindestens einer Einheiten-ID, die unten wirklich
|
||||
vorkommt (exakte Schreibweise).
|
||||
- Die Schrittfolge muss zu den Kanten passen (wer ruft wen) — nichts erfinden.
|
||||
- Schreibe auf Deutsch. Gib NUR das Markdown aus, ohne Code-Fences drumherum.
|
||||
|
||||
## Einheiten von `<PROJEKT>` (mit Kanten)
|
||||
|
||||
<EINHEITEN>
|
||||
@@ -0,0 +1,66 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import config # noqa: E402
|
||||
import database # noqa: E402
|
||||
import fake_agents # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def testdb(tmp_path, monkeypatch):
|
||||
"""Frische SQLite-Datei je Test; setzt die modul-globale Connection zurück."""
|
||||
monkeypatch.setattr(config, "DB_PATH", tmp_path / "test.db")
|
||||
database._db = None
|
||||
await database.init_db()
|
||||
yield database
|
||||
await database.close_db()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def gescanntes_repo(tmp_path, monkeypatch, testdb):
|
||||
"""mini_repo als echtes git-Repo mit echtem .planer/ (Fake-Scan, echter HEAD-Hash) —
|
||||
Grundlage für Viewer-/Drift-/Routen-Tests."""
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import fake_agents
|
||||
import kanban
|
||||
import repo as repo_mod
|
||||
import scan_board
|
||||
|
||||
wurzel = tmp_path / "mini"
|
||||
shutil.copytree(Path(__file__).parent / "fixtures" / "mini_repo", wurzel)
|
||||
env = {"PATH": "/usr/bin:/bin", "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
|
||||
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t", "HOME": str(wurzel)}
|
||||
|
||||
def git(*args):
|
||||
subprocess.run(["git", "-C", str(wurzel), *args], check=True, capture_output=True, env=env)
|
||||
|
||||
git("init", "-b", "main")
|
||||
git("add", "-A")
|
||||
git("commit", "-m", "start")
|
||||
welt = fake_agents.Welt()
|
||||
fake_agents.set_welt(welt)
|
||||
fake_agents.standard_antworten(welt)
|
||||
monkeypatch.setenv("PLANER_FAKE_AGENTS", "1")
|
||||
monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.05)
|
||||
ergebnis = await scan_board.run_scan(wurzel, "mini", stand=repo_mod.head_hash(wurzel))
|
||||
assert ergebnis["status"] == "ok", ergebnis
|
||||
git("add", "-A")
|
||||
git("commit", "-m", "artefakte")
|
||||
yield wurzel, git
|
||||
fake_agents.set_welt(None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_welt(monkeypatch):
|
||||
"""Frische Fake-Welt + PLANER_FAKE_AGENTS aktiv; Tests hängen Antworten/Störungen an."""
|
||||
welt = fake_agents.Welt()
|
||||
fake_agents.set_welt(welt)
|
||||
monkeypatch.setenv("PLANER_FAKE_AGENTS", "1")
|
||||
yield welt
|
||||
fake_agents.set_welt(None)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"""Mini-Fixture: Ausgabe."""
|
||||
from rechner import addiere
|
||||
|
||||
|
||||
def zeige(a, b):
|
||||
"""Formatiert die Summe."""
|
||||
return f"Ergebnis: {addiere(a, b)}"
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
"""Mini-Fixture: kleiner Rechner."""
|
||||
|
||||
BASIS = 10
|
||||
|
||||
|
||||
def addiere(a, b):
|
||||
"""Summe zweier Zahlen."""
|
||||
return a + b
|
||||
|
||||
|
||||
def skaliere(wert):
|
||||
"""Skaliert mit der Modul-Basis."""
|
||||
return wert * BASIS
|
||||
|
||||
|
||||
class Speicher:
|
||||
"""Merkt sich das letzte Ergebnis."""
|
||||
|
||||
def __init__(self):
|
||||
self.letztes = None
|
||||
|
||||
def merke(self, wert):
|
||||
self.letztes = wert
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Provider-Layer: Fake-Umschaltung, harter Stopp ohne Rollen-Config, kein Fallback."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import agents
|
||||
|
||||
|
||||
async def test_fake_umschaltung(fake_welt):
|
||||
fake_welt.antwort_auf(r"-schneiden-", lambda prompt: "ANTWORT")
|
||||
rc, out, err = await agents.run_agent("scan-x-schneiden-1", "egal", 5, role="schneiden")
|
||||
assert (rc, out) == (0, "ANTWORT")
|
||||
assert fake_welt.calls == ["scan-x-schneiden-1"]
|
||||
|
||||
|
||||
async def test_stoerung_dann_fehler(fake_welt):
|
||||
fake_welt.antwort_auf(r".", lambda prompt: "ok")
|
||||
fake_welt.stoerungen.append({"muster": r"-schneiden-", "modus": "fehler", "mal": 1})
|
||||
rc1, _, err1 = await agents.run_agent("scan-x-schneiden-1", "egal", 5, role="schneiden")
|
||||
rc2, out2, _ = await agents.run_agent("scan-x-schneiden-1", "egal", 5, role="schneiden")
|
||||
assert (rc1, err1) == (1, "fake-stoerung")
|
||||
assert (rc2, out2) == (0, "ok")
|
||||
|
||||
|
||||
async def test_infra_stoerung_wirft(fake_welt):
|
||||
fake_welt.stoerungen.append({"muster": r".", "modus": "infra", "mal": 1})
|
||||
with pytest.raises(agents.AgentInfraError):
|
||||
await agents.run_agent("scan-x-schneiden-1", "egal", 5, role="schneiden")
|
||||
|
||||
|
||||
async def test_unkonfigurierte_rolle_pausiert(monkeypatch):
|
||||
"""Ohne Fake-Modus und ohne ROLLE_-Env: AgentInfraError (Lauf pausiert), kein Default."""
|
||||
monkeypatch.delenv("PLANER_FAKE_AGENTS", raising=False)
|
||||
monkeypatch.delenv("ROLLE_JUDGE", raising=False)
|
||||
with pytest.raises(agents.AgentInfraError, match="kein Modell konfiguriert"):
|
||||
await agents.run_agent("scan-x-judge-1", "egal", 5, role="judge")
|
||||
|
||||
|
||||
def test_kein_fallback_codepfad():
|
||||
"""Der Creator-Fallback (_available_cached / 'routed provider unavailable') darf
|
||||
nicht mitportiert worden sein."""
|
||||
quelle = Path(agents.__file__).read_text(encoding="utf-8")
|
||||
assert "_available_cached" not in quelle
|
||||
assert not re.search(r"run_provider", quelle)
|
||||
|
||||
|
||||
def test_prioritaet_der_stufen():
|
||||
assert agents._agent_priority("scan-p-sichten-1") < agents._agent_priority("scan-p-schneiden-9")
|
||||
assert agents._agent_priority("irgendwas") == len(agents._STAGE_PRIORITY)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Artefakt-Schicht: Merge, ergaenze-anker, Writer, Prüfungs-Gerüst."""
|
||||
from pathlib import Path
|
||||
|
||||
import artefakte
|
||||
|
||||
FRAG_A = """## a.py::eins
|
||||
beschreibung: Erste.
|
||||
input: x
|
||||
output: y
|
||||
|
||||
## a.py::zwei
|
||||
beschreibung: Zweite.
|
||||
"""
|
||||
|
||||
FRAG_NACHFASS = """ergaenze-anker: a.py::_helfer zu a.py::eins
|
||||
|
||||
## a.py::drei
|
||||
beschreibung: Dritte.
|
||||
"""
|
||||
|
||||
|
||||
def test_merge_mit_nachfass():
|
||||
rumpf = artefakte.merge_fragmente([FRAG_A, FRAG_NACHFASS])
|
||||
assert rumpf.index("## a.py::eins") < rumpf.index("## a.py::zwei") < rumpf.index("## a.py::drei")
|
||||
assert "anker: a.py::_helfer" in rumpf.split("## a.py::zwei")[0] # im eins-Block
|
||||
assert "ergaenze-anker" not in rumpf
|
||||
|
||||
|
||||
def test_merge_dubletten_fallen_weg():
|
||||
rumpf = artefakte.merge_fragmente([FRAG_A, FRAG_A])
|
||||
assert rumpf.count("## a.py::eins") == 1
|
||||
|
||||
|
||||
def test_einheiten_datei_und_stand():
|
||||
text = artefakte.einheiten_datei("a.py", "## a.py::eins\nbeschreibung: X.", "abc123")
|
||||
assert text.startswith("# Einheiten: a.py\nstand: abc123\n")
|
||||
assert artefakte.lese_stand(text) == "abc123"
|
||||
|
||||
|
||||
def test_alle_ids():
|
||||
text = "## a.py::eins\nanker: a.py::_helfer\n\n## a.py::zwei\n"
|
||||
assert artefakte.alle_ids(text) == {"a.py::eins", "a.py::_helfer", "a.py::zwei"}
|
||||
|
||||
|
||||
def test_datei_pfad():
|
||||
assert artefakte.datei_pfad("backend/kanban.py") == "einheiten/backend__kanban.py.md"
|
||||
|
||||
|
||||
def test_schreibe_planer(tmp_path):
|
||||
pfade = artefakte.schreibe_planer(tmp_path, {"kern.md": "# Kern\n",
|
||||
"einheiten/a.py.md": "# Einheiten: a.py\n"})
|
||||
assert (tmp_path / ".planer" / "kern.md").read_text() == "# Kern\n"
|
||||
assert len(pfade) == 2
|
||||
|
||||
|
||||
def test_pruefung_geruest_mit_makefile(tmp_path):
|
||||
(tmp_path / "Makefile").write_text("test:\n\tpytest\ndev:\n\tuvicorn\n")
|
||||
text = artefakte.pruefung_geruest(tmp_path, "demo")
|
||||
assert "tests: make test" in text and "start: make dev" in text
|
||||
assert "lint: (keins vorhanden)" in text
|
||||
|
||||
|
||||
def test_pruefung_geruest_ohne_alles(tmp_path):
|
||||
text = artefakte.pruefung_geruest(tmp_path, "demo")
|
||||
assert "tests: (keins vorhanden)" in text
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Chunker: Ausschlussliste, Symbol-Schnitt, Chunk-Größen."""
|
||||
from pathlib import Path
|
||||
|
||||
import chunker
|
||||
|
||||
MINI = Path(__file__).parent / "fixtures" / "mini_repo"
|
||||
|
||||
|
||||
def test_mini_repo_dateien():
|
||||
rel = [str(p.relative_to(MINI)) for p in chunker.dateien(MINI)]
|
||||
assert rel == ["anzeige.py", "rechner.py"]
|
||||
|
||||
|
||||
def test_ausschluss(tmp_path):
|
||||
(tmp_path / "ok.py").write_text("X = 1\n")
|
||||
(tmp_path / ".env").write_text("GEHEIM=1\n")
|
||||
(tmp_path / ".env.local").write_text("GEHEIM=2\n")
|
||||
(tmp_path / "node_modules").mkdir()
|
||||
(tmp_path / "node_modules" / "paket.py").write_text("Y = 1\n")
|
||||
(tmp_path / ".planer").mkdir()
|
||||
(tmp_path / ".planer" / "alt.py").write_text("Z = 1\n")
|
||||
rel = [str(p.relative_to(tmp_path)) for p in chunker.dateien(tmp_path)]
|
||||
assert rel == ["ok.py"]
|
||||
|
||||
|
||||
def test_schnitt_mini():
|
||||
text = (MINI / "rechner.py").read_text()
|
||||
chunks = chunker.schneide("rechner.py", text)
|
||||
assert len(chunks) == 1 # klein → ein Chunk
|
||||
assert chunks[0]["symbole"] == ["addiere", "skaliere", "Speicher"]
|
||||
assert chunks[0]["text"] == text.rstrip("\n") # Kopf (Docstring, BASIS) im ersten Chunk
|
||||
|
||||
|
||||
def test_schnitt_grosse_datei():
|
||||
"""Symbole à 60 Zeilen → Chunks bleiben um CHUNK_ZEILEN, Schnitt nie mitten im Symbol."""
|
||||
bloecke = [f"def f{i}():\n" + "".join(f" x{j} = {j}\n" for j in range(60)) for i in range(20)]
|
||||
text = "\n".join(bloecke)
|
||||
chunks = chunker.schneide("gross.py", text)
|
||||
assert len(chunks) > 1
|
||||
assert all(c["groesse"] <= chunker.CHUNK_ZEILEN + 61 for c in chunks)
|
||||
alle = [s for c in chunks for s in c["symbole"]]
|
||||
assert alle == [f"f{i}" for i in range(20)] # jedes Symbol genau einmal, in Reihenfolge
|
||||
for c in chunks:
|
||||
assert c["text"].splitlines()[0].startswith(("def ", "\n", '"')) # Schnitt an Blockgrenze
|
||||
|
||||
|
||||
def test_creator_smoke():
|
||||
"""Read-only gegen das echte Creator-Backend: board_inventory.py (2344 Z.) wird zerlegt."""
|
||||
wurzel = Path("/home/arbeit/projects/creator")
|
||||
chunks = [c for c in chunker.chunks_fuer_repo(wurzel, {"backend/board_inventory.py"})]
|
||||
assert len(chunks) >= 6
|
||||
assert all(c["groesse"] <= 700 for c in chunks) # kein Riesen-Chunk
|
||||
|
||||
|
||||
def test_top_level_symbole():
|
||||
text = (MINI / "rechner.py").read_text()
|
||||
assert chunker.top_level_symbole(text) == {"BASIS", "addiere", "skaliere", "Speicher"}
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Rollen-Auflösung: explizit konfiguriert oder harter Stopp — nie ein Default."""
|
||||
import pytest
|
||||
|
||||
import config
|
||||
|
||||
|
||||
def test_konfigurierte_rolle(monkeypatch):
|
||||
monkeypatch.setenv("ROLLE_SCHNEIDEN", "minimax:minimax/MiniMax-M3")
|
||||
assert config.resolve_role("schneiden") == ("minimax", "minimax/MiniMax-M3")
|
||||
|
||||
|
||||
def test_unkonfigurierte_rolle_stoppt(monkeypatch):
|
||||
monkeypatch.delenv("ROLLE_JUDGE", raising=False)
|
||||
with pytest.raises(RuntimeError, match="kein Modell konfiguriert"):
|
||||
config.resolve_role("judge")
|
||||
|
||||
|
||||
def test_unbekannter_provider_stoppt(monkeypatch):
|
||||
monkeypatch.setenv("ROLLE_SICHTEN", "openai:gpt-4")
|
||||
with pytest.raises(RuntimeError, match="unbekannter Provider"):
|
||||
config.resolve_role("sichten")
|
||||
|
||||
|
||||
def test_fehlendes_modell_stoppt(monkeypatch):
|
||||
monkeypatch.setenv("ROLLE_NACHFASS", "minimax")
|
||||
with pytest.raises(RuntimeError, match="Modell fehlt"):
|
||||
config.resolve_role("nachfass")
|
||||
|
||||
|
||||
def test_alle_rollen_deklariert():
|
||||
assert set(config.ROLLEN) == {"schneiden", "nachfass", "sichten", "judge"}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""DB-Roundtrip: Karten + Telemetrie."""
|
||||
import json
|
||||
|
||||
|
||||
async def test_karten_roundtrip(testdb):
|
||||
await testdb.kanban_upsert_card("p", "scan", "c1", "chunk", "schneiden", {"groesse": 10})
|
||||
await testdb.kanban_upsert_card("p", "scan", "c2", "chunk", "schneiden", {"groesse": 99})
|
||||
karten = await testdb.kanban_pull("p", "scan", "schneiden", 10)
|
||||
assert [k["card_id"] for k in karten] == ["c2", "c1"] # LPT: größte zuerst
|
||||
assert karten[0]["payload"]["groesse"] == 99
|
||||
|
||||
await testdb.kanban_advance_many("p", "scan", [("c1", "sammeln")])
|
||||
assert await testdb.kanban_count("p", "schneiden") == 1
|
||||
assert (await testdb.kanban_stage_counts("p"))["scan"] == {"schneiden": 1, "sammeln": 1}
|
||||
|
||||
|
||||
async def test_payload_erhalt_bei_upsert_ohne_payload(testdb):
|
||||
await testdb.kanban_upsert_card("p", "scan", "c1", "chunk", "schneiden", {"datei": "a.py"})
|
||||
await testdb.kanban_upsert_card("p", "scan", "c1", "chunk", "nachfass", None)
|
||||
karte = await testdb.kanban_get_card("p", "scan", "c1")
|
||||
assert karte["stage"] == "nachfass" and karte["payload"]["datei"] == "a.py"
|
||||
|
||||
|
||||
async def test_events_mit_run_id(testdb):
|
||||
testdb.set_current_run("p", "lauf-1")
|
||||
await testdb.add_event("p", "agent", key="scan:c1", status="ok", dur_ms=1200,
|
||||
meta={"tokens": {"output": 500}})
|
||||
db = await testdb.get_db()
|
||||
cursor = await db.execute("SELECT kind, status, run_id, meta FROM events WHERE topic='p'")
|
||||
kind, status, run_id, meta = await cursor.fetchone()
|
||||
assert (kind, status, run_id) == ("agent", "ok", "lauf-1")
|
||||
assert json.loads(meta)["tokens"]["output"] == 500
|
||||
testdb.set_current_run("p", None)
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Gates gegen das mini_repo-Fixture: gültiges Fragment passiert, jede Manipulation
|
||||
wird mit genau dem erwarteten Fehler gefangen."""
|
||||
from pathlib import Path
|
||||
|
||||
import gates
|
||||
|
||||
MINI = Path(__file__).parent / "fixtures" / "mini_repo"
|
||||
|
||||
GUT = '''# Einheiten: rechner.py
|
||||
stand: UNBEKANNT
|
||||
|
||||
## rechner.py::addiere
|
||||
beschreibung: Addiert zwei Zahlen.
|
||||
input: zwei Zahlen a, b
|
||||
output: die Summe
|
||||
entscheidungen:
|
||||
- Reine Funktion ohne Zustand.
|
||||
beleg: "return a + b"
|
||||
kanten:
|
||||
- wird-genutzt-von: anzeige.py::zeige
|
||||
|
||||
## rechner.py::Speicher
|
||||
beschreibung: Merkt sich das letzte Ergebnis.
|
||||
input: Werte über merke()
|
||||
output: letzter Wert in self.letztes
|
||||
entscheidungen:
|
||||
- Startet leer.
|
||||
beleg: "self.letztes = None"
|
||||
kanten:
|
||||
- nutzt: rechner.py::addiere
|
||||
'''
|
||||
|
||||
|
||||
def test_gutes_fragment_passiert():
|
||||
fehler, warnungen, einheiten = gates.pruefe_fragment(GUT, MINI, "rechner.py")
|
||||
assert fehler == []
|
||||
assert [e["id"] for e in einheiten] == ["rechner.py::addiere", "rechner.py::Speicher"]
|
||||
|
||||
|
||||
def test_falsches_zitat():
|
||||
kaputt = GUT.replace('beleg: "return a + b"', 'beleg: "return a - b"')
|
||||
fehler, _, _ = gates.pruefe_fragment(kaputt, MINI, "rechner.py")
|
||||
assert any("Zitat nicht wörtlich" in f for f in fehler)
|
||||
|
||||
|
||||
def test_erfundener_anker():
|
||||
kaputt = GUT.replace("## rechner.py::addiere", "## rechner.py::subtrahiere")
|
||||
fehler, _, _ = gates.pruefe_fragment(kaputt, MINI, "rechner.py")
|
||||
assert any("Symbol 'subtrahiere' nicht in" in f for f in fehler)
|
||||
|
||||
|
||||
def test_kaputte_kante():
|
||||
kaputt = GUT.replace("- nutzt: rechner.py::addiere",
|
||||
"- nutzt: rechner.py::addiere (wichtig!)")
|
||||
fehler, _, _ = gates.pruefe_fragment(kaputt, MINI, "rechner.py")
|
||||
assert any("Kante unlesbar" in f for f in fehler)
|
||||
|
||||
|
||||
def test_unbekannter_kanten_typ():
|
||||
kaputt = GUT.replace("- nutzt:", "- verwendet:")
|
||||
fehler, _, _ = gates.pruefe_fragment(kaputt, MINI, "rechner.py")
|
||||
assert any("unbekannter Kanten-Typ" in f for f in fehler)
|
||||
|
||||
|
||||
def test_fremde_datei_abgelehnt():
|
||||
fehler, _, _ = gates.pruefe_fragment(GUT, MINI, "anzeige.py")
|
||||
assert any("gehört nicht zur gescannten Datei" in f for f in fehler)
|
||||
|
||||
|
||||
def test_leere_antwort():
|
||||
fehler, _, _ = gates.pruefe_fragment("", MINI, "rechner.py")
|
||||
assert any("keine Einheit geparst" in f for f in fehler)
|
||||
|
||||
|
||||
def test_escaping_normalisierung():
|
||||
frag = GUT.replace('beleg: "return a + b"', 'beleg: "return f\\"Ergebnis: {addiere(a, b)}\\""')
|
||||
frag = frag.replace("## rechner.py::addiere", "## anzeige.py::zeige").replace(
|
||||
"# Einheiten: rechner.py", "# Einheiten: anzeige.py")
|
||||
# nur den umgebauten zeige-Block prüfen
|
||||
block = frag.split("\n\n")[1]
|
||||
fehler, _, _ = gates.pruefe_fragment(block, MINI, "anzeige.py")
|
||||
assert not any("Zitat" in f for f in fehler)
|
||||
|
||||
|
||||
def test_fehlende_symbole():
|
||||
_, _, einheiten = gates.pruefe_fragment(GUT, MINI, "rechner.py")
|
||||
fehlt = gates.fehlende_symbole(["addiere", "skaliere", "Speicher", "BASIS"], einheiten, [])
|
||||
assert fehlt == ["skaliere", "BASIS"]
|
||||
fehlt2 = gates.fehlende_symbole(["skaliere"], einheiten, ["rechner.py::skaliere"])
|
||||
assert fehlt2 == []
|
||||
|
||||
|
||||
def test_repariere_kanten():
|
||||
"""Erfundene Kanten-Ziele werden gestrichen (Datei erfunden, Symbol erfunden,
|
||||
Prefix fehlt) — gültige Kanten und alle anderen Zeilen bleiben unangetastet."""
|
||||
kaputt = GUT.replace(
|
||||
"- nutzt: rechner.py::addiere",
|
||||
"- nutzt: rechner.py::addiere\n- ruft-auf: erfunden.py::x\n"
|
||||
"- nutzt: rechner.py::gibtsnicht\n- löst-aus: nur_symbol")
|
||||
bereinigt, gestrichen = gates.repariere_kanten(kaputt, MINI, "rechner.py")
|
||||
assert len(gestrichen) == 3
|
||||
assert "- nutzt: rechner.py::addiere" in bereinigt
|
||||
assert "erfunden.py" not in bereinigt and "gibtsnicht" not in bereinigt
|
||||
fehler, _, _ = gates.pruefe_fragment(bereinigt, MINI, "rechner.py")
|
||||
assert fehler == []
|
||||
|
||||
|
||||
def test_repariere_belege_schnappt_fast_richtige_zitate():
|
||||
"""Ein-Zeichen-Abweichung (`=` statt Original) wird auf die echte Quellzeile
|
||||
geschnappt; frei erfundene Zitate bleiben stehen und fallen im Gate durch."""
|
||||
kaputt = GUT.replace('beleg: "return a + b"', 'beleg: "return a+b"')
|
||||
repariert, korrigiert = gates.repariere_belege(kaputt, MINI, "rechner.py")
|
||||
assert len(korrigiert) == 1
|
||||
fehler, _, _ = gates.pruefe_fragment(repariert, MINI, "rechner.py")
|
||||
assert fehler == []
|
||||
|
||||
erfunden = GUT.replace('beleg: "return a + b"', 'beleg: "raise TotalErfunden(xyz)"')
|
||||
repariert2, korrigiert2 = gates.repariere_belege(erfunden, MINI, "rechner.py")
|
||||
assert korrigiert2 == []
|
||||
fehler2, _, _ = gates.pruefe_fragment(repariert2, MINI, "rechner.py")
|
||||
assert any("Zitat nicht wörtlich" in f for f in fehler2)
|
||||
|
||||
|
||||
def test_qualifizierte_methode_als_symbol():
|
||||
frag = GUT.replace("## rechner.py::Speicher", "## rechner.py::Speicher.merke")
|
||||
fehler, _, _ = gates.pruefe_fragment(frag, MINI, "rechner.py")
|
||||
assert not any("Speicher.merke" in f for f in fehler)
|
||||
|
||||
|
||||
def test_beleg_mit_anfuehrungszeichen():
|
||||
"""Belege mit eingebetteten Quotes/Regex (Befund 17) parsen und passieren."""
|
||||
frag = ('## anzeige.py::zeige\nbeschreibung: Formatiert.\ninput: a, b\noutput: Text\n'
|
||||
'entscheidungen:\n- Nutzt einen f-String.\n'
|
||||
' beleg: "return f"Ergebnis: {addiere(a, b)}""\nkanten:\n')
|
||||
fehler, _, _ = gates.pruefe_fragment(frag, MINI, "anzeige.py")
|
||||
assert fehler == []
|
||||
|
||||
|
||||
def test_datei_einheit_ohne_symbole(tmp_path):
|
||||
"""Skript ohne def/class: Datei-Einheit `## pfad` (Befund 18)."""
|
||||
(tmp_path / "skript.py").write_text('import sys\nprint(sys.argv)\n')
|
||||
frag = ('## skript.py\nbeschreibung: Gibt Argumente aus.\ninput: argv\noutput: stdout\n'
|
||||
'entscheidungen:\n- Direkter Druck ohne Formatierung.\n beleg: "print(sys.argv)"\nkanten:\n')
|
||||
fehler, _, einheiten = gates.pruefe_fragment(frag, tmp_path, "skript.py")
|
||||
assert fehler == [] and einheiten[0]["id"] == "skript.py"
|
||||
|
||||
|
||||
def test_referenz_korrektur_falscher_prefix():
|
||||
"""Symbol existiert eindeutig unter anderer Datei → ID wird korrigiert (Befund 19)."""
|
||||
sicht = "### Kann reparieren\neinheiten: falsch.py::_fix, kaputt.py::gibtsnirgends\n"
|
||||
text, gestrichen = gates.repariere_referenzen(sicht, {"richtig.py::_fix", "a.py::x"})
|
||||
assert "richtig.py::_fix" in text
|
||||
assert gestrichen == ["kaputt.py::gibtsnirgends"]
|
||||
|
||||
|
||||
def test_referenz_integritaet():
|
||||
sicht = "### Kann rechnen\neinheiten: rechner.py::addiere, rechner.py::gibtsnicht\n"
|
||||
fehler = gates.pruefe_referenzen(sicht, {"rechner.py::addiere"})
|
||||
assert fehler == ["unbekannte Einheiten-Referenz: rechner.py::gibtsnicht"]
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Engine tests with fake processors (no LLM): flow, barrier, retry/dead-letter, producer race."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
import kanban
|
||||
from kanban import Flow, Stage, chain_stages, run_flow
|
||||
|
||||
TOPIC = "t"
|
||||
BOARD = "inventory"
|
||||
|
||||
|
||||
def _advance_proc(db, to_stage):
|
||||
async def proc(cards):
|
||||
await db.kanban_advance_many(TOPIC, BOARD, [(c["card_id"], to_stage) for c in cards])
|
||||
return proc
|
||||
|
||||
|
||||
async def _seed(db, n, stage="s1"):
|
||||
for i in range(n):
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, f"card-{i}", "title", stage, {"title": f"T{i}"})
|
||||
|
||||
|
||||
async def test_cards_flow_through_stages(testdb):
|
||||
db = testdb
|
||||
await _seed(db, 7)
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([
|
||||
Stage(BOARD, "s1", _advance_proc(db, "s2")),
|
||||
Stage(BOARD, "s2", _advance_proc(db, "done")),
|
||||
])
|
||||
await asyncio.wait_for(run_flow(flow, stages), timeout=10)
|
||||
assert await db.kanban_count(TOPIC, "done", board=BOARD) == 7
|
||||
assert await db.kanban_count(TOPIC, ["s1", "s2"], board=BOARD) == 0
|
||||
|
||||
|
||||
async def test_barrier_waits_for_upstream(testdb):
|
||||
db = testdb
|
||||
await _seed(db, 6)
|
||||
upstream_left: list[int] = []
|
||||
|
||||
async def slow_s1(cards):
|
||||
await asyncio.sleep(0.05) # keep upstream busy so an eager barrier would see queued cards
|
||||
await db.kanban_advance_many(TOPIC, BOARD, [(c["card_id"], "gate") for c in cards])
|
||||
|
||||
async def barrier_proc(cards):
|
||||
upstream_left.append(await db.kanban_count(TOPIC, ["s1"], board=BOARD))
|
||||
await db.kanban_advance_many(TOPIC, BOARD, [(c["card_id"], "done") for c in cards])
|
||||
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([
|
||||
Stage(BOARD, "s1", slow_s1),
|
||||
Stage(BOARD, "gate", barrier_proc, barrier=True),
|
||||
])
|
||||
await asyncio.wait_for(run_flow(flow, stages), timeout=10)
|
||||
assert await db.kanban_count(TOPIC, "done", board=BOARD) == 6
|
||||
assert upstream_left and all(n == 0 for n in upstream_left) # barrier never ran with s1 queued
|
||||
|
||||
|
||||
async def test_retry_backoff_then_dead(testdb, monkeypatch):
|
||||
db = testdb
|
||||
monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.02)
|
||||
await _seed(db, 1)
|
||||
attempts = []
|
||||
|
||||
async def failing(cards):
|
||||
attempts.append(cards[0]["retries"])
|
||||
raise RuntimeError("kaputt")
|
||||
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([Stage(BOARD, "s1", failing)])
|
||||
await asyncio.wait_for(run_flow(flow, stages), timeout=10)
|
||||
card = await db.kanban_get_card(TOPIC, BOARD, "card-0")
|
||||
assert card["stage"] == "dead"
|
||||
assert card["retries"] == kanban.MAX_CARD_RETRIES
|
||||
assert "kaputt" in card["last_error"]
|
||||
assert attempts == [0, 1, 2] # backoff between attempts, then dead-letter
|
||||
|
||||
|
||||
async def test_requeue_dead(testdb):
|
||||
db = testdb
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-0", "title", "s1")
|
||||
for _ in range(kanban.MAX_CARD_RETRIES):
|
||||
await db.kanban_fail_card(TOPIC, BOARD, "card-0", "x", kanban.MAX_CARD_RETRIES, 0.0)
|
||||
assert (await db.kanban_get_card(TOPIC, BOARD, "card-0"))["stage"] == "dead"
|
||||
assert await db.kanban_requeue_dead(TOPIC, BOARD, "s1") == 1
|
||||
card = await db.kanban_get_card(TOPIC, BOARD, "card-0")
|
||||
assert card["stage"] == "s1" and card["retries"] == 0
|
||||
|
||||
|
||||
async def test_producer_attach_in_idle_lull(testdb):
|
||||
"""Fix-6 regression: a producer attached while workers sit in the exit grace poll
|
||||
must keep the flow alive and its cards must still be processed."""
|
||||
db = testdb
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([Stage(BOARD, "s1", _advance_proc(db, "done"))])
|
||||
|
||||
async def producer_a():
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-a", "title", "s1")
|
||||
flow.wake.set()
|
||||
flow.done_producer()
|
||||
|
||||
async def attacher():
|
||||
while await db.kanban_count(TOPIC, "done", board=BOARD) == 0: # wait for card-a done
|
||||
await asyncio.sleep(0.01)
|
||||
flow.add_producer() # synchronous BEFORE the work — the grace poll must see it
|
||||
|
||||
async def producer_b():
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-b", "title", "s1")
|
||||
flow.wake.set()
|
||||
flow.done_producer()
|
||||
await producer_b()
|
||||
|
||||
flow.add_producer() # producer_a, counted before run_flow (sync add)
|
||||
asyncio.get_event_loop().create_task(attacher())
|
||||
await asyncio.wait_for(run_flow(flow, stages, producers=[producer_a()]), timeout=10)
|
||||
assert await db.kanban_count(TOPIC, "done", board=BOARD) == 2
|
||||
|
||||
|
||||
async def test_backoff_delays_pull(testdb, monkeypatch):
|
||||
db = testdb
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-0", "title", "s1")
|
||||
await db.kanban_fail_card(TOPIC, BOARD, "card-0", "x", 5, 0.2)
|
||||
assert await db.kanban_pull(TOPIC, BOARD, "s1", 10) == [] # in backoff → not pullable
|
||||
assert await db.kanban_count(TOPIC, "s1", board=BOARD) == 1 # but still counts as queued
|
||||
await asyncio.sleep(0.25)
|
||||
assert len(await db.kanban_pull(TOPIC, BOARD, "s1", 10)) == 1
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Wächter für Leitprinzip 2: Modell-IDs stehen NUR in der .env, nie im Code.
|
||||
|
||||
Greppt den gesamten backend-Quellcode (ohne tests/) nach Modell-ID-Mustern.
|
||||
Schlägt dieser Test an, hat jemand ein Modell hart verdrahtet.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
BACKEND = Path(__file__).resolve().parent.parent
|
||||
MUSTER = re.compile(r"claude-\w+-\d|MiniMax-M\d|\bopus\b|\bsonnet\b|\bhaiku\b|gpt-\d",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
def test_keine_modell_ids_im_code():
|
||||
treffer = []
|
||||
for pfad in BACKEND.rglob("*.py"):
|
||||
if "tests" in pfad.parts:
|
||||
continue
|
||||
for nr, zeile in enumerate(pfad.read_text(encoding="utf-8").splitlines(), 1):
|
||||
if MUSTER.search(zeile):
|
||||
treffer.append(f"{pfad.relative_to(BACKEND)}:{nr}: {zeile.strip()}")
|
||||
assert not treffer, "Modell-IDs im Code gefunden (gehören in die .env):\n" + "\n".join(treffer)
|
||||
|
||||
|
||||
def test_keine_modell_ids_in_templates():
|
||||
templates = BACKEND / "templates"
|
||||
if not templates.is_dir():
|
||||
return
|
||||
treffer = [f"{p.name}:{nr}" for p in templates.rglob("*.md")
|
||||
for nr, zeile in enumerate(p.read_text(encoding="utf-8").splitlines(), 1)
|
||||
if MUSTER.search(zeile)]
|
||||
assert not treffer, f"Modell-IDs in Templates: {treffer}"
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Re-Scan über git-Diff: nur berührte Dateien werden neu gescannt, Löschungen räumen
|
||||
auf, sauberer Tree ist Pflicht. Läuft komplett in der Fake-Welt."""
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import fake_agents
|
||||
import kanban
|
||||
import repo as repo_mod
|
||||
|
||||
MINI = Path(__file__).parent / "fixtures" / "mini_repo"
|
||||
|
||||
|
||||
def _git(wurzel, *args):
|
||||
subprocess.run(["git", "-C", str(wurzel), *args], check=True, capture_output=True,
|
||||
env={"PATH": "/usr/bin:/bin", "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
|
||||
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
|
||||
"HOME": str(wurzel)})
|
||||
|
||||
|
||||
def _repo(tmp_path: Path) -> Path:
|
||||
ziel = tmp_path / "repo"
|
||||
shutil.copytree(MINI, ziel)
|
||||
_git(ziel, "init", "-b", "main")
|
||||
_git(ziel, "add", "-A")
|
||||
_git(ziel, "commit", "-m", "start")
|
||||
return ziel
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cli(monkeypatch):
|
||||
import cli as cli_mod
|
||||
monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.05)
|
||||
return cli_mod
|
||||
|
||||
|
||||
async def test_scan_dann_rescan_nur_geaenderte(cli, fake_welt, testdb, tmp_path):
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
wurzel = _repo(tmp_path)
|
||||
assert await cli._scan(wurzel) == 0
|
||||
stand1 = repo_mod.head_hash(wurzel)
|
||||
assert f"stand: {stand1}" in (wurzel / ".planer" / "einheiten" / "rechner.py.md").read_text()
|
||||
|
||||
_git(wurzel, "add", "-A")
|
||||
_git(wurzel, "commit", "-m", "scan-artefakte")
|
||||
(wurzel / "rechner.py").write_text((wurzel / "rechner.py").read_text() + "\n\ndef neu():\n return 1\n")
|
||||
_git(wurzel, "add", "-A")
|
||||
_git(wurzel, "commit", "-m", "neue funktion")
|
||||
stand2 = repo_mod.head_hash(wurzel)
|
||||
|
||||
fake_welt.calls.clear()
|
||||
assert await cli._rescan(wurzel) == 0
|
||||
# Nur rechner.py wurde neu gescannt …
|
||||
assert all("anzeige" not in k for k in fake_welt.calls if "-schneiden-" in k)
|
||||
rechner = (wurzel / ".planer" / "einheiten" / "rechner.py.md").read_text()
|
||||
anzeige = (wurzel / ".planer" / "einheiten" / "anzeige.py.md").read_text()
|
||||
assert f"stand: {stand2}" in rechner and "## rechner.py::neu" in rechner
|
||||
assert f"stand: {stand1}" in anzeige # unverändert übernommen
|
||||
# … aber die Sichten wurden neu aggregiert und referenzieren auch den Bestand
|
||||
assert any("-sichten-" in k for k in fake_welt.calls)
|
||||
kern = (wurzel / ".planer" / "kern.md").read_text()
|
||||
assert kern.startswith("# Kern:")
|
||||
|
||||
|
||||
async def test_rescan_ohne_aenderung_ruft_kein_llm(cli, fake_welt, testdb, tmp_path):
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
wurzel = _repo(tmp_path)
|
||||
assert await cli._scan(wurzel) == 0
|
||||
_git(wurzel, "add", "-A")
|
||||
_git(wurzel, "commit", "-m", "scan-artefakte")
|
||||
fake_welt.calls.clear()
|
||||
assert await cli._rescan(wurzel) == 0
|
||||
assert fake_welt.calls == []
|
||||
|
||||
|
||||
async def test_rescan_geloeschte_datei_raeumt_auf(cli, fake_welt, testdb, tmp_path):
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
wurzel = _repo(tmp_path)
|
||||
assert await cli._scan(wurzel) == 0
|
||||
_git(wurzel, "add", "-A")
|
||||
_git(wurzel, "commit", "-m", "scan-artefakte")
|
||||
(wurzel / "anzeige.py").unlink()
|
||||
_git(wurzel, "add", "-A")
|
||||
_git(wurzel, "commit", "-m", "anzeige weg")
|
||||
assert await cli._rescan(wurzel) == 0
|
||||
assert not (wurzel / ".planer" / "einheiten" / "anzeige.py.md").exists()
|
||||
|
||||
|
||||
async def test_unsauberer_tree_bricht_ab(cli, fake_welt, testdb, tmp_path):
|
||||
wurzel = _repo(tmp_path)
|
||||
(wurzel / "schmutz.py").write_text("X = 1\n")
|
||||
with pytest.raises(SystemExit):
|
||||
await cli._scan(wurzel)
|
||||
|
||||
|
||||
async def test_kein_git_repo_bricht_ab(cli, fake_welt, testdb, tmp_path):
|
||||
kein_repo = tmp_path / "kein-repo"
|
||||
kein_repo.mkdir()
|
||||
with pytest.raises(SystemExit):
|
||||
await cli._scan(kein_repo)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""API-Routen: Lesen + Scan-Start über httpx/ASGITransport (gleicher Event-Loop wie
|
||||
create_task und der DB-Write-Lock — fastapi.TestClient wäre die Loop-Falle)."""
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import config
|
||||
import scan_dienst
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(gescanntes_repo, monkeypatch, tmp_path):
|
||||
wurzel, git = gescanntes_repo
|
||||
datei = tmp_path / "projekte.txt"
|
||||
datei.write_text(f"{wurzel}\n{tmp_path}/tot\n")
|
||||
monkeypatch.setattr(config, "PROJEKTE_DATEI", datei)
|
||||
scan_dienst._laeufe.clear()
|
||||
from server import app
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c, wurzel, git
|
||||
|
||||
|
||||
async def test_projektliste(client):
|
||||
c, wurzel, _ = client
|
||||
r = await c.get("/api/projekte")
|
||||
assert r.status_code == 200
|
||||
eintraege = {p["name"]: p for p in r.json()}
|
||||
assert eintraege["mini"]["hat_planer"] is True
|
||||
assert eintraege["mini"]["drift"] == []
|
||||
assert eintraege["mini"]["scan_laeuft"] is False
|
||||
assert "fehler" in eintraege["tot"]
|
||||
|
||||
|
||||
async def test_projekt_detail(client):
|
||||
c, _, _ = client
|
||||
r = await c.get("/api/projekte/mini")
|
||||
daten = r.json()
|
||||
assert daten["kern"]["titel"] == "mini"
|
||||
assert daten["bereiche"] and daten["bereiche"][0]["features"]
|
||||
assert "rechner.py::addiere" in daten["einheiten"]
|
||||
assert daten["doku_ordnung"]
|
||||
assert (await c.get("/api/projekte/gibtsnicht")).status_code == 404
|
||||
|
||||
|
||||
async def test_einheit_mit_code(client):
|
||||
c, _, _ = client
|
||||
r = await c.get("/api/projekte/mini/einheit", params={"id": "rechner.py::addiere"})
|
||||
e = r.json()
|
||||
assert "return a + b" in e["code"]
|
||||
assert e["veraltet"] is False
|
||||
assert (await c.get("/api/projekte/mini/einheit", params={"id": "x.py::y"})).status_code == 404
|
||||
|
||||
|
||||
async def test_drift_nach_commit(client):
|
||||
c, wurzel, git = client
|
||||
(wurzel / "rechner.py").write_text((wurzel / "rechner.py").read_text() + "\nX9 = 9\n")
|
||||
git("add", "-A")
|
||||
git("commit", "-m", "drift")
|
||||
r = await c.get("/api/projekte")
|
||||
mini = next(p for p in r.json() if p["name"] == "mini")
|
||||
assert mini["drift"] == ["rechner.py"]
|
||||
e = (await c.get("/api/projekte/mini/einheit", params={"id": "rechner.py::addiere"})).json()
|
||||
assert e["veraltet"] is True
|
||||
|
||||
|
||||
async def test_scan_starten_und_pollen(client):
|
||||
c, _, _ = client
|
||||
r = await c.post("/api/projekte/mini/scan", json={"art": "rescan"})
|
||||
assert r.json()["status"] == "gestartet"
|
||||
doppel = await c.post("/api/projekte/mini/scan", json={"art": "rescan"})
|
||||
assert doppel.json()["status"] in ("laeuft", "gestartet") # idempotent, nie Doppellauf
|
||||
for _ in range(100):
|
||||
s = (await c.get("/api/projekte/mini/scan-status")).json()
|
||||
if not s["laeuft"]:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
assert s["ergebnis"]["status"] in ("ok", "unveraendert")
|
||||
|
||||
|
||||
async def test_scan_409_bei_dirty_tree(client):
|
||||
c, wurzel, _ = client
|
||||
(wurzel / "schmutz.py").write_text("X = 1\n")
|
||||
r = await c.post("/api/projekte/mini/scan", json={"art": "scan"})
|
||||
assert r.status_code == 409
|
||||
assert "uncommittete" in r.json()["detail"]
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Fake-E2E: kompletter Scan-Lauf (Chunker → Board → .planer/) in Sekunden, ohne LLM.
|
||||
Die Fake-Antworten durchlaufen die ECHTEN Gates — getestet wird der volle Pfad
|
||||
inkl. Nachfass, Retry, Dead-Letter und Infra-Pause."""
|
||||
import asyncio
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import fake_agents
|
||||
import kanban
|
||||
import scan_board
|
||||
|
||||
MINI = Path(__file__).parent / "fixtures" / "mini_repo"
|
||||
|
||||
|
||||
def _repo(tmp_path: Path) -> Path:
|
||||
ziel = tmp_path / "repo"
|
||||
shutil.copytree(MINI, ziel)
|
||||
return ziel
|
||||
|
||||
|
||||
async def _scan(repo: Path, monkeypatch) -> dict:
|
||||
monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.05)
|
||||
return await asyncio.wait_for(
|
||||
scan_board.run_scan(repo, "mini", stand="deadbeef"), timeout=30)
|
||||
|
||||
|
||||
async def test_e2e_voller_scan(fake_welt, testdb, tmp_path, monkeypatch):
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
repo = _repo(tmp_path)
|
||||
ergebnis = await _scan(repo, monkeypatch)
|
||||
assert ergebnis["status"] == "ok", ergebnis
|
||||
planer = repo / ".planer"
|
||||
assert (planer / "einheiten" / "rechner.py.md").is_file()
|
||||
assert (planer / "einheiten" / "anzeige.py.md").is_file()
|
||||
assert (planer / "kern.md").read_text().startswith("# Kern: mini")
|
||||
assert (planer / "architektur.md").is_file()
|
||||
assert (planer / "flows.md").is_file()
|
||||
assert (planer / "pruefung.md").is_file()
|
||||
assert list((planer / "features").glob("*.md"))
|
||||
einheiten = (planer / "einheiten" / "rechner.py.md").read_text()
|
||||
assert "stand: deadbeef" in einheiten
|
||||
assert "## rechner.py::addiere" in einheiten and "## rechner.py::Speicher" in einheiten
|
||||
|
||||
|
||||
async def test_e2e_nachfass_pfad(fake_welt, testdb, tmp_path, monkeypatch):
|
||||
"""Erst-Scan lässt `skaliere` aus → Abdeckungs-Gate schickt den Chunk in den Nachfass."""
|
||||
fake_agents.standard_antworten(fake_welt, lasse_aus={"skaliere"})
|
||||
repo = _repo(tmp_path)
|
||||
ergebnis = await _scan(repo, monkeypatch)
|
||||
assert ergebnis["status"] == "ok", ergebnis
|
||||
assert any("-nachfass-" in k for k in fake_welt.calls)
|
||||
assert "## rechner.py::skaliere" in (repo / ".planer" / "einheiten" / "rechner.py.md").read_text()
|
||||
|
||||
|
||||
async def test_e2e_stoerung_retry(fake_welt, testdb, tmp_path, monkeypatch):
|
||||
"""Einmal Garbage → Gates lehnen ab → Engine-Backoff → zweiter Versuch gelingt."""
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
fake_welt.stoerungen.append({"muster": r"-schneiden-chunk-0000", "modus": "garbage", "mal": 1})
|
||||
repo = _repo(tmp_path)
|
||||
ergebnis = await _scan(repo, monkeypatch)
|
||||
assert ergebnis["status"] == "ok", ergebnis
|
||||
|
||||
|
||||
async def test_e2e_dauerfehler_dead(fake_welt, testdb, tmp_path, monkeypatch):
|
||||
"""Dauerhafter Fehler auf einem Chunk → Karte landet im Dead-Letter, Lauf endet."""
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
fake_welt.stoerungen.append({"muster": r"-schneiden-chunk-0000", "modus": "fehler", "mal": 99})
|
||||
repo = _repo(tmp_path)
|
||||
ergebnis = await _scan(repo, monkeypatch)
|
||||
assert ergebnis["status"] == "fehler"
|
||||
assert ergebnis["dead"] == ["chunk-0000"]
|
||||
|
||||
|
||||
async def test_e2e_infra_pause(fake_welt, testdb, tmp_path, monkeypatch):
|
||||
"""AgentInfraError → Lauf pausiert sofort (kein Dead-Letter, kein Raten)."""
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
fake_welt.stoerungen.append({"muster": r"-schneiden-", "modus": "infra", "mal": 1})
|
||||
repo = _repo(tmp_path)
|
||||
ergebnis = await _scan(repo, monkeypatch)
|
||||
assert ergebnis["status"] == "pausiert"
|
||||
assert "fake-infra" in ergebnis["infra_error"]
|
||||
|
||||
|
||||
async def test_e2e_fortsetzen_eine_sichten_runde(fake_welt, testdb, tmp_path, monkeypatch):
|
||||
"""Fortsetzung mit Mix aus toten und fertigen Chunks: die Sammeln-Barriere darf erst
|
||||
öffnen, wenn der Producer ALLE Karten requeued hat — genau eine Sichten-Runde, und
|
||||
die Sichten sehen die volle Übersicht (Abnahme-Befund: Teil-Übersicht durch frühes
|
||||
Drainen)."""
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
repo = _repo(tmp_path)
|
||||
ergebnis = await _scan(repo, monkeypatch)
|
||||
assert ergebnis["status"] == "ok"
|
||||
# Zustand eines abgebrochenen Laufs nachstellen: ein Chunk tot, Rest fertig
|
||||
karten = await testdb.kanban_cards("mini", scan_board.BOARD, kind="chunk")
|
||||
tot = karten[0]["card_id"]
|
||||
await testdb.kanban_upsert_card("mini", scan_board.BOARD, tot, "chunk", "dead", None)
|
||||
fake_welt.calls.clear()
|
||||
ergebnis2 = await asyncio.wait_for(
|
||||
scan_board.run_scan(repo, "mini", stand="deadbeef", fortsetzen=True), timeout=30)
|
||||
assert ergebnis2["status"] == "ok", ergebnis2
|
||||
sichten_calls = [k for k in fake_welt.calls if "-sichten-" in k]
|
||||
assert len(sichten_calls) == 3 # genau EINE Runde, kein Doppel-Drain
|
||||
features = (repo / ".planer" / "features" / "fake-bereich.md").read_text()
|
||||
assert "rechner.py::addiere" in features and "anzeige.py::zeige" in features # volle Übersicht
|
||||
|
||||
|
||||
async def test_e2e_events_geschrieben(fake_welt, testdb, tmp_path, monkeypatch):
|
||||
"""Telemetrie: Agent-Events mit run_id landen in der DB."""
|
||||
import agents
|
||||
import database
|
||||
monkeypatch.setattr(agents, "on_event", database.add_event)
|
||||
fake_agents.standard_antworten(fake_welt)
|
||||
repo = _repo(tmp_path)
|
||||
ergebnis = await _scan(repo, monkeypatch)
|
||||
assert ergebnis["status"] == "ok"
|
||||
db = await testdb.get_db()
|
||||
cursor = await db.execute(
|
||||
"SELECT count(*) FROM events WHERE topic='mini' AND kind='agent' AND run_id LIKE 'scan-%'")
|
||||
assert (await cursor.fetchone())[0] >= 5 # 2 Chunks + 3 Sichten
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Viewer-Service: Parser, Drift, Doku-Ordnung, Projektliste."""
|
||||
from pathlib import Path
|
||||
|
||||
import config
|
||||
import projekte
|
||||
import viewer
|
||||
|
||||
MINI = Path(__file__).parent / "fixtures" / "mini_repo"
|
||||
|
||||
|
||||
def test_lese_einheiten_mit_entscheidungen():
|
||||
text = """# Einheiten: a.py
|
||||
stand: abc
|
||||
|
||||
## a.py::eins
|
||||
beschreibung: Macht eins.
|
||||
input: x
|
||||
output: y
|
||||
entscheidungen:
|
||||
- Erste Entscheidung.
|
||||
beleg: "code hier"
|
||||
- Zweite mit Umbruch
|
||||
und Fortsetzung.
|
||||
kanten:
|
||||
- ruft-auf: a.py::zwei
|
||||
|
||||
## b.py
|
||||
beschreibung: Datei-Einheit.
|
||||
"""
|
||||
e = viewer.lese_einheiten(text)
|
||||
eins = e["a.py::eins"]
|
||||
assert eins["entscheidungen"] == ["Erste Entscheidung.", "Zweite mit Umbruch und Fortsetzung."]
|
||||
assert eins["kanten"] == [{"typ": "ruft-auf", "ziel": "a.py::zwei"}]
|
||||
assert (eins["input"], eins["output"]) == ("x", "y")
|
||||
assert e["b.py"]["beschreibung"] == "Datei-Einheit."
|
||||
|
||||
|
||||
def test_lese_kern_und_pruefung():
|
||||
kern = viewer.lese_kern("# Kern: demo\nZwei Sätze.\n\ntragende-einheiten: a.py::x, b.py::y\n")
|
||||
assert kern["titel"] == "demo" and kern["tragende_einheiten"] == ["a.py::x", "b.py::y"]
|
||||
pr = viewer.lese_pruefung("# Prüfung: demo\ntests: make test\nlint: (keins vorhanden)\n"
|
||||
"start: make dev\nhinweise:\n- Vorsicht.\n")
|
||||
assert pr["tests"] == "make test" and pr["hinweise"] == ["Vorsicht."]
|
||||
|
||||
|
||||
def test_lese_features_architektur_flows():
|
||||
b = viewer.lese_features("# Bereich: rechnen\nbeschreibung: Rechnet.\n\n"
|
||||
"## Feature: Addieren [kern]\nbeschreibung: Summen.\n\n"
|
||||
"### Kann addieren\neinheiten: a.py::eins\n")
|
||||
assert b["features"][0]["flag"] == "kern"
|
||||
assert b["features"][0]["teilfeatures"][0]["einheiten"] == ["a.py::eins"]
|
||||
a = viewer.lese_architektur("# Architektur: demo\n\n## Komponente: kernstück\n"
|
||||
"beschreibung: Trägt.\neinheiten: a.py::eins\n"
|
||||
"beziehungen:\n- nutzt: anzeige\n")
|
||||
assert a[0]["beziehungen"] == [{"typ": "nutzt", "ziel": "anzeige"}]
|
||||
f = viewer.lese_flows("# Flows: demo\n\n## Flow: ablauf\n1. Schritt eins — a.py::eins\n"
|
||||
"2. Schritt ohne Referenz\n")
|
||||
assert f[0]["schritte"][0]["einheiten"] == ["a.py::eins"]
|
||||
assert f[0]["schritte"][1]["einheiten"] == []
|
||||
|
||||
|
||||
def test_code_extrahieren():
|
||||
assert "return a + b" in viewer.code_extrahieren(MINI, "rechner.py::addiere")
|
||||
assert viewer.code_extrahieren(MINI, "rechner.py::BASIS").startswith("BASIS = 10")
|
||||
assert viewer.code_extrahieren(MINI, "rechner.py::gibtsnicht") == ""
|
||||
assert viewer.code_extrahieren(MINI, "wegdatei.py::x") == ""
|
||||
assert "def merke" in viewer.code_extrahieren(MINI, "rechner.py::Speicher.merke")
|
||||
|
||||
|
||||
async def test_lade_projekt_und_drift(gescanntes_repo):
|
||||
wurzel, git = gescanntes_repo
|
||||
projekt = viewer.lade_projekt(wurzel)
|
||||
assert "rechner.py::addiere" in projekt["einheiten"]
|
||||
assert projekt["kern"]["titel"] == "mini"
|
||||
assert projekt["staende"]["rechner.py"]
|
||||
assert viewer.drift(wurzel, projekt["staende"])["veraltet"] == []
|
||||
|
||||
(wurzel / "rechner.py").write_text((wurzel / "rechner.py").read_text() + "\nX2 = 2\n")
|
||||
git("add", "-A")
|
||||
git("commit", "-m", "aenderung")
|
||||
d = viewer.drift(wurzel, projekt["staende"])
|
||||
assert d["veraltet"] == ["rechner.py"] # anzeige.py unverändert
|
||||
|
||||
|
||||
def test_doku_ordnung_mit_zyklus():
|
||||
projekt = {
|
||||
"bereiche": [
|
||||
{"name": "oben", "features": [{"teilfeatures": [{"einheiten": ["a.py::x"]}]}]},
|
||||
{"name": "unten", "features": [{"teilfeatures": [{"einheiten": ["b.py::y"]}]}]},
|
||||
],
|
||||
"einheiten": {
|
||||
"a.py::x": {"kanten": [{"typ": "ruft-auf", "ziel": "b.py::y"}]},
|
||||
"b.py::y": {"kanten": []},
|
||||
},
|
||||
}
|
||||
assert viewer.doku_ordnung(projekt) == ["unten", "oben"] # Fundament zuerst
|
||||
# Zyklus: beide nutzen einander → deterministisch, beide enthalten
|
||||
projekt["einheiten"]["b.py::y"]["kanten"] = [{"typ": "nutzt", "ziel": "a.py::x"}]
|
||||
ordnung = viewer.doku_ordnung(projekt)
|
||||
assert sorted(ordnung) == ["oben", "unten"] and len(ordnung) == 2
|
||||
|
||||
|
||||
def test_projektliste(tmp_path, monkeypatch):
|
||||
echt = tmp_path / "echt"
|
||||
echt.mkdir()
|
||||
(tmp_path / "kopie").mkdir()
|
||||
datei = tmp_path / "projekte.txt"
|
||||
datei.write_text(f"# Kommentar\n{echt}\n{tmp_path}/weg\n{tmp_path}/kopie\n{echt}\n")
|
||||
monkeypatch.setattr(config, "PROJEKTE_DATEI", datei)
|
||||
liste = projekte.lade()
|
||||
assert [p.get("fehler") is None for p in liste] == [True, False, True, False]
|
||||
assert "existiert nicht" in liste[1]["fehler"]
|
||||
assert "Namenskollision" in liste[3]["fehler"]
|
||||
assert projekte.finde("echt") == echt and projekte.finde("weg") is None
|
||||
@@ -0,0 +1,279 @@
|
||||
"""Viewer-Service: liest .planer/-Dateien eines Projekts in Strukturen für die API.
|
||||
|
||||
Reine Bibliothek (kein FastAPI-Import). Eigener toleranter Lese-Parser — gates.py bleibt
|
||||
unangetastet (scan-kritisch) und verwirft z. B. die Entscheidungs-SÄTZE, die die UI zeigt.
|
||||
Code je Einheit wird LIVE aus dem Anker extrahiert, nie gespeichert (Single Source).
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import artefakte
|
||||
import repo
|
||||
|
||||
_EINHEIT_KOPF = re.compile(r"^## (\S+::\S+|\S+\.\w+)$")
|
||||
|
||||
|
||||
def lese_einheiten(text: str) -> dict[str, dict]:
|
||||
"""Einheiten-Datei → {id: {beschreibung, input, output, entscheidungen, kanten, anker}}."""
|
||||
out: dict[str, dict] = {}
|
||||
aktuelle = None
|
||||
modus = None
|
||||
for zeile in text.splitlines():
|
||||
if (m := _EINHEIT_KOPF.match(zeile.strip())) and zeile.startswith("## "):
|
||||
aktuelle = {"id": m.group(1), "beschreibung": "", "input": "", "output": "",
|
||||
"entscheidungen": [], "kanten": [], "anker": []}
|
||||
out[aktuelle["id"]] = aktuelle
|
||||
modus = None
|
||||
elif aktuelle is None:
|
||||
continue
|
||||
elif m := re.match(r"(beschreibung|input|output):(.*)$", zeile):
|
||||
aktuelle[m.group(1)] = m.group(2).strip()
|
||||
modus = m.group(1)
|
||||
elif zeile.startswith("anker:"):
|
||||
aktuelle["anker"].append(zeile.split(":", 1)[1].strip())
|
||||
elif zeile.strip() in ("entscheidungen:", "fakten:"):
|
||||
modus = "punkte"
|
||||
elif zeile.strip() == "kanten:":
|
||||
modus = "kanten"
|
||||
elif modus == "punkte" and re.match(r"\s+beleg:", zeile):
|
||||
continue # UI zeigt echten Code statt Zitaten
|
||||
elif modus == "punkte" and zeile.startswith("- "):
|
||||
aktuelle["entscheidungen"].append(re.sub(r"\s*\bbeleg:.*$", "", zeile[2:]).strip())
|
||||
elif modus == "punkte" and zeile.startswith(" ") and aktuelle["entscheidungen"]:
|
||||
aktuelle["entscheidungen"][-1] += " " + zeile.strip()
|
||||
elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)", zeile)):
|
||||
aktuelle["kanten"].append({"typ": m.group(1), "ziel": m.group(2)})
|
||||
elif modus in ("beschreibung", "input", "output") and zeile.startswith(" "):
|
||||
aktuelle[modus] += " " + zeile.strip()
|
||||
return out
|
||||
|
||||
|
||||
def lese_kern(text: str) -> dict:
|
||||
zeilen = text.splitlines()
|
||||
titel = zeilen[0].removeprefix("# Kern:").strip() if zeilen else ""
|
||||
tragend: list[str] = []
|
||||
rumpf: list[str] = []
|
||||
for zeile in zeilen[1:]:
|
||||
if zeile.startswith("tragende-einheiten:"):
|
||||
tragend = [t.strip() for t in zeile.split(":", 1)[1].split(",") if t.strip()]
|
||||
else:
|
||||
rumpf.append(zeile)
|
||||
return {"titel": titel, "text": "\n".join(rumpf).strip(), "tragende_einheiten": tragend}
|
||||
|
||||
|
||||
def _einheiten_refs(zeile: str) -> list[str]:
|
||||
return [t.strip() for t in zeile.split(":", 1)[1].split(",") if t.strip()]
|
||||
|
||||
|
||||
def lese_features(text: str) -> dict:
|
||||
"""Eine Bereichs-Datei → {name, beschreibung, features: [{titel, flag, beschreibung,
|
||||
teilfeatures: [{titel, einheiten}]}]}."""
|
||||
bereich = {"name": "", "beschreibung": "", "features": []}
|
||||
feature = None
|
||||
for zeile in text.splitlines():
|
||||
if zeile.startswith("# Bereich:"):
|
||||
bereich["name"] = zeile.split(":", 1)[1].strip()
|
||||
feature = None
|
||||
elif zeile.startswith("## Feature:"):
|
||||
titel = zeile[11:].strip()
|
||||
flag = "kern" if "[kern]" in titel else "rand"
|
||||
feature = {"titel": re.sub(r"\s*\[(kern|rand)\]", "", titel).strip(),
|
||||
"flag": flag, "beschreibung": "", "teilfeatures": []}
|
||||
bereich["features"].append(feature)
|
||||
elif zeile.startswith("### ") and feature is not None:
|
||||
feature["teilfeatures"].append({"titel": zeile[4:].strip(), "einheiten": []})
|
||||
elif zeile.startswith("einheiten:") and feature and feature["teilfeatures"]:
|
||||
feature["teilfeatures"][-1]["einheiten"] = _einheiten_refs(zeile)
|
||||
elif zeile.startswith("beschreibung:"):
|
||||
ziel = feature if feature is not None else bereich
|
||||
ziel["beschreibung"] = zeile.split(":", 1)[1].strip()
|
||||
return bereich
|
||||
|
||||
|
||||
def lese_architektur(text: str) -> list[dict]:
|
||||
komponenten: list[dict] = []
|
||||
k = None
|
||||
modus = None
|
||||
for zeile in text.splitlines():
|
||||
if zeile.startswith("## Komponente:"):
|
||||
k = {"name": zeile.split(":", 1)[1].strip(), "beschreibung": "",
|
||||
"einheiten": [], "beziehungen": []}
|
||||
komponenten.append(k)
|
||||
modus = None
|
||||
elif k is None:
|
||||
continue
|
||||
elif zeile.startswith("beschreibung:"):
|
||||
k["beschreibung"] = zeile.split(":", 1)[1].strip()
|
||||
elif zeile.startswith("einheiten:"):
|
||||
k["einheiten"] = _einheiten_refs(zeile)
|
||||
elif zeile.strip() == "beziehungen:":
|
||||
modus = "beziehungen"
|
||||
elif modus == "beziehungen" and (m := re.match(r"- ([\wäöü-]+):\s*(.+)$", zeile)):
|
||||
k["beziehungen"].append({"typ": m.group(1), "ziel": m.group(2).strip()})
|
||||
return komponenten
|
||||
|
||||
|
||||
def lese_flows(text: str) -> list[dict]:
|
||||
flows: list[dict] = []
|
||||
f = None
|
||||
for zeile in text.splitlines():
|
||||
if zeile.startswith("## Flow:"):
|
||||
f = {"titel": zeile.split(":", 1)[1].strip(), "schritte": []}
|
||||
flows.append(f)
|
||||
elif f is not None and (m := re.match(r"^\d+\.\s(.+?)(?:\s—\s(.+))?$", zeile)):
|
||||
refs = [t.strip() for t in (m.group(2) or "").split(",") if t.strip()]
|
||||
f["schritte"].append({"text": m.group(1).strip(), "einheiten": refs})
|
||||
return flows
|
||||
|
||||
|
||||
def lese_pruefung(text: str) -> dict:
|
||||
out = {"tests": "", "lint": "", "start": "", "hinweise": []}
|
||||
modus = None
|
||||
for zeile in text.splitlines():
|
||||
if m := re.match(r"(tests|lint|start):(.*)$", zeile):
|
||||
out[m.group(1)] = m.group(2).strip()
|
||||
modus = None
|
||||
elif zeile.strip() == "hinweise:":
|
||||
modus = "hinweise"
|
||||
elif modus == "hinweise" and zeile.startswith("- "):
|
||||
out["hinweise"].append(zeile[2:].strip())
|
||||
return out
|
||||
|
||||
|
||||
def code_extrahieren(wurzel: Path, eid: str) -> str:
|
||||
"""Quellcode eines Ankers (pfad::symbol) LIVE aus der Quelle — nie Exception:
|
||||
fehlende Datei/Symbol → '' (UI zeigt Hinweis)."""
|
||||
try:
|
||||
datei, _, symbol = eid.partition("::")
|
||||
p = wurzel / datei
|
||||
if not p.is_file():
|
||||
return ""
|
||||
text = p.read_text(encoding="utf-8", errors="replace")
|
||||
if not symbol:
|
||||
return text[:8000] # Datei-Einheit: ganze (gekappte) Datei
|
||||
if "." in symbol:
|
||||
symbol = symbol.split(".", 1)[1] # qualifizierte Methode → Methode suchen
|
||||
zeilen = text.splitlines()
|
||||
start = None
|
||||
for i, z in enumerate(zeilen):
|
||||
if re.match(rf"(\s*)(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b", z):
|
||||
start = i
|
||||
break
|
||||
if re.match(rf"{re.escape(symbol)}\s*[:=]", z):
|
||||
start = i
|
||||
ende = i + 1
|
||||
while ende < len(zeilen) and (zeilen[ende].startswith((" ", "\t", ")", "]", "}"))
|
||||
or not zeilen[ende].strip()):
|
||||
ende += 1
|
||||
return "\n".join(zeilen[start:ende]).rstrip()
|
||||
if start is None:
|
||||
return ""
|
||||
einzug = len(zeilen[start]) - len(zeilen[start].lstrip())
|
||||
ende = start + 1
|
||||
while ende < len(zeilen):
|
||||
z = zeilen[ende]
|
||||
if z.strip() and (len(z) - len(z.lstrip())) <= einzug:
|
||||
break
|
||||
ende += 1
|
||||
return "\n".join(zeilen[start:ende]).rstrip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def lade_projekt(wurzel: Path) -> dict:
|
||||
"""Alle .planer/-Dateien eines Projekts → Struktur für die API. Fehlende Dateien
|
||||
ergeben leere Teile (degradieren, nie werfen)."""
|
||||
planer = wurzel / ".planer"
|
||||
|
||||
def text(rel: str) -> str:
|
||||
p = planer / rel
|
||||
return p.read_text(encoding="utf-8") if p.is_file() else ""
|
||||
|
||||
einheiten: dict[str, dict] = {}
|
||||
staende: dict[str, str] = {} # quelldatei → stand
|
||||
for p in sorted((planer / "einheiten").glob("*.md")) if (planer / "einheiten").is_dir() else []:
|
||||
inhalt = p.read_text(encoding="utf-8")
|
||||
einheiten.update(lese_einheiten(inhalt))
|
||||
kopf = inhalt.splitlines()[0] if inhalt else ""
|
||||
datei = kopf.removeprefix("# Einheiten:").strip()
|
||||
if datei and (stand := artefakte.lese_stand(inhalt)):
|
||||
staende[datei] = stand
|
||||
bereiche = [lese_features((planer / "features" / p.name).read_text(encoding="utf-8"))
|
||||
for p in sorted((planer / "features").glob("*.md"))] \
|
||||
if (planer / "features").is_dir() else []
|
||||
return {
|
||||
"kern": lese_kern(text("kern.md")),
|
||||
"bereiche": bereiche,
|
||||
"architektur": lese_architektur(text("architektur.md")),
|
||||
"flows": lese_flows(text("flows.md")),
|
||||
"pruefung": lese_pruefung(text("pruefung.md")),
|
||||
"einheiten": einheiten,
|
||||
"staende": staende,
|
||||
}
|
||||
|
||||
|
||||
# --- Drift ---------------------------------------------------------------------------
|
||||
|
||||
_diff_cache: dict[tuple[str, str, str], set[str]] = {} # (pfad, stand, head) → geändert
|
||||
|
||||
|
||||
def drift(wurzel: Path, staende: dict[str, str]) -> dict:
|
||||
"""Welche gescannten Quelldateien sind seit ihrem stand: berührt?
|
||||
→ {head, veraltet: [quelldatei]}. Cache-Schlüssel enthält HEAD → selbst-invalidierend."""
|
||||
try:
|
||||
head = repo.head_hash(wurzel)
|
||||
except (RuntimeError, FileNotFoundError):
|
||||
return {"head": "", "veraltet": []}
|
||||
veraltet = []
|
||||
for datei, stand in sorted(staende.items()):
|
||||
if stand == head:
|
||||
continue
|
||||
schluessel = (str(wurzel), stand, head)
|
||||
if schluessel not in _diff_cache:
|
||||
try:
|
||||
_diff_cache[schluessel] = repo.geaenderte_dateien(wurzel, stand)[0]
|
||||
except RuntimeError:
|
||||
_diff_cache[schluessel] = set()
|
||||
if datei in _diff_cache[schluessel]:
|
||||
veraltet.append(datei)
|
||||
return {"head": head, "veraltet": veraltet}
|
||||
|
||||
|
||||
# --- Doku-Leseordnung ------------------------------------------------------------------
|
||||
|
||||
def doku_ordnung(projekt: dict) -> list[str]:
|
||||
"""Bereichs-Namen in Abhängigkeitsordnung: Kanten der Einheiten (ruft-auf/nutzt) werden
|
||||
zu Bereichs-Kanten aggregiert; viel Genutztes kommt früher (Kahn-Toposort, zyklenfest:
|
||||
bei Zyklus gewinnt der Bereich mit kleinstem In-Degree, dann alphabetisch)."""
|
||||
einheit_zu_bereich: dict[str, str] = {}
|
||||
for bereich in projekt["bereiche"]:
|
||||
for f in bereich["features"]:
|
||||
for tf in f["teilfeatures"]:
|
||||
for eid in tf["einheiten"]:
|
||||
einheit_zu_bereich.setdefault(eid, bereich["name"])
|
||||
namen = [b["name"] for b in projekt["bereiche"]]
|
||||
eingehend: dict[str, set[str]] = {n: set() for n in namen}
|
||||
for eid, e in projekt["einheiten"].items():
|
||||
von = einheit_zu_bereich.get(eid)
|
||||
for kante in e["kanten"]:
|
||||
if kante["typ"] not in ("ruft-auf", "nutzt"):
|
||||
continue
|
||||
nach = einheit_zu_bereich.get(kante["ziel"])
|
||||
if von and nach and von != nach:
|
||||
eingehend[nach].add(von) # "von nutzt nach" → nach ist Fundament → früher
|
||||
# Kahn: Bereiche ohne offene Nutzer-Abhängigkeit zuerst — Fundament vor Nutzern:
|
||||
# ein Bereich kommt, sobald alles, was ER nutzt, schon gelesen wurde.
|
||||
nutzt: dict[str, set[str]] = {n: set() for n in namen}
|
||||
for nach, nutzer in eingehend.items():
|
||||
for von in nutzer:
|
||||
nutzt[von].add(nach)
|
||||
ordnung: list[str] = []
|
||||
offen = set(namen)
|
||||
while offen:
|
||||
bereit = sorted(n for n in offen if not (nutzt[n] & offen))
|
||||
if not bereit: # Zyklus: kleinste offene Abhängigkeitszahl, dann Name
|
||||
bereit = [min(offen, key=lambda n: (len(nutzt[n] & offen), n))]
|
||||
for n in bereit:
|
||||
ordnung.append(n)
|
||||
offen.discard(n)
|
||||
return ordnung
|
||||
@@ -0,0 +1,113 @@
|
||||
// Auto-Ableitung von opencode.json OHNE mcp-Server: Batch-Agenten (files/readonly/text)
|
||||
// brauchen keine Web-MCPs — jeder opencode-Prozess startet sonst ~3 MCP-Prozesse (~300 MB).
|
||||
// Bei Änderungen an opencode.json hier nachziehen (nur der mcp-Block fehlt).
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"minimax": {
|
||||
"options": {
|
||||
"apiKey": "{env:MINIMAX_API_KEY}"
|
||||
},
|
||||
"models": {
|
||||
"MiniMax-M3": {
|
||||
"name": "MiniMax M3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimax-kalt": {
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"name": "MiniMax (kalt — niedrige Temperature, ohne Thinking)",
|
||||
"options": {
|
||||
"baseURL": "https://api.minimax.io/anthropic/v1",
|
||||
"apiKey": "{env:MINIMAX_API_KEY}"
|
||||
},
|
||||
"models": {
|
||||
"MiniMax-M3": {
|
||||
"name": "MiniMax M3 (kalt)",
|
||||
"options": {
|
||||
"temperature": 0.2,
|
||||
"thinking": {
|
||||
"type": "disabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MiniMax-M2.7-highspeed": {
|
||||
"name": "MiniMax M2.7 highspeed (kalt)",
|
||||
"options": {
|
||||
"temperature": 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Ollama (lokal)",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:11434/v1"
|
||||
},
|
||||
"models": {
|
||||
"qwen3.6:27b": {
|
||||
"name": "Qwen3.6 27B"
|
||||
},
|
||||
"qwen3.5:9b": {
|
||||
"name": "Qwen3.5 9B"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"full": {
|
||||
"description": "Alle Tools: Dateien, Bash, Websuche",
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"bash": "allow",
|
||||
"webfetch": "allow"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"description": "Dateien lesen/schreiben + Bash, keine Websuche",
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"bash": "allow",
|
||||
"webfetch": "deny"
|
||||
},
|
||||
"tools": {
|
||||
"minimax-search*": false,
|
||||
"searxng*": false
|
||||
}
|
||||
},
|
||||
"readonly": {
|
||||
"description": "Nur Dateien lesen",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny",
|
||||
"webfetch": "deny"
|
||||
},
|
||||
"tools": {
|
||||
"write": false,
|
||||
"edit": false,
|
||||
"bash": false,
|
||||
"minimax-search*": false,
|
||||
"searxng*": false
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"description": "Reine Textantwort, keine Tools",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny",
|
||||
"webfetch": "deny"
|
||||
},
|
||||
"tools": {
|
||||
"write": false,
|
||||
"edit": false,
|
||||
"bash": false,
|
||||
"read": false,
|
||||
"glob": false,
|
||||
"grep": false,
|
||||
"minimax-search*": false,
|
||||
"searxng*": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# `.planer/`-Dateiformat (Spezifikation, Phase 0)
|
||||
|
||||
Format der Wissensschichten im Projekt-Repo. Entwurf — wird in Phase 0 gegen echte
|
||||
Scan-Ergebnisse am Creator validiert und nachgeschärft.
|
||||
|
||||
## Design-Entscheidungen
|
||||
|
||||
- **Markdown mit einfachen `schlüssel: wert`-Zeilen**, kein reines YAML/JSON: Menschen
|
||||
lesen und editieren die Dateien direkt (Soll-Änderungen!), Agenten schreiben sie,
|
||||
Skripte parsen sie mit simplen Regeln (Überschriften-Ebenen + Schlüsselzeilen).
|
||||
- **Einheiten-ID = `pfad::symbol`** (z. B. `backend/kanban.py::_worker`) — stabil,
|
||||
ableitbar, ohne separate ID-Verwaltung. Bei Datei-Ebene ohne Symbol: nur `pfad`.
|
||||
- **Belege = wörtliche Kurzzitate** aus dem Code (nicht Zeilennummern — die verschieben
|
||||
sich bei jedem Diff). Mechanisch prüfbar per Substring-Suche in der Anker-Datei.
|
||||
Ein Beleg ist ein Ausschnitt aus GENAU EINER Quellzeile (Phase-0-Erkenntnis:
|
||||
Modelle ziehen sonst mehrzeilige Statements zusammen — nicht prüfbar).
|
||||
- **Eine Einheiten-Datei pro Quelldatei** (nicht pro Einheit — sonst hunderte Dateien;
|
||||
nicht eine große — sonst Merge-Hölle). Diff-Lokalität: Quelldatei ändert sich →
|
||||
genau eine Einheiten-Datei ändert sich.
|
||||
- **Sichten referenzieren Einheiten nur per ID**, kopieren nie Fakten (Single Source).
|
||||
|
||||
## Verzeichnis-Layout
|
||||
|
||||
```
|
||||
<projekt>/.planer/
|
||||
kern.md Kern des Projekts + tragende Einheiten
|
||||
architektur.md Komponenten + Beziehungen
|
||||
flows.md Abläufe als Schrittfolgen
|
||||
features/<bereich>.md eine Datei je Funktionsbereich
|
||||
einheiten/<quelle>.md eine Datei je Quelldatei (Pfad, `/` → `__`)
|
||||
pruefung.md Build-/Test-/Start-Kommandos, Eigenheiten
|
||||
entscheidungen/*.md Decision Records
|
||||
strategie/*.md benannte Soll-Entwürfe
|
||||
ideen.md Ideen-Backlog
|
||||
jourfixe/*.md Sitzungsprotokolle
|
||||
```
|
||||
|
||||
## Einheiten: `einheiten/backend__kanban.py.md`
|
||||
|
||||
````markdown
|
||||
# Einheiten: backend/kanban.py
|
||||
stand: 3f2a91c ← Commit-Hash des letzten Scans dieser Datei (der Anker-Stand)
|
||||
|
||||
## backend/kanban.py::_worker
|
||||
beschreibung: Zieht Karten aus der Stufe der Spalte und arbeitet sie nebenläufig ab.
|
||||
input: Flow, Stage-Spezifikation, Inflight-Limit, Liste aller Stage-Namen
|
||||
output: kehrt erst zurück, wenn flowweit nichts mehr zu tun ist; Karten sind advanced,
|
||||
im Backoff oder dead
|
||||
entscheidungen:
|
||||
- Exit erst nach doppelter Leerlauf-Prüfung mit Karenz-Schlaf.
|
||||
beleg: "if await _idle_exit():"
|
||||
- Ein Barriere-Worker zieht nur bei Quieszenz aller Upstream-Stufen.
|
||||
beleg: "return await quiescent(flow, spec.upstream)"
|
||||
kanten:
|
||||
- ruft-auf: backend/kanban.py::quiescent
|
||||
- nutzt: backend/database.py::kanban_pull
|
||||
- wird-genutzt-von: backend/kanban.py::run_flow
|
||||
````
|
||||
|
||||
Regeln:
|
||||
- `## `-Überschrift = Einheiten-ID. Freitext darunter bis zur nächsten Schlüsselzeile
|
||||
ist verboten — nur `beschreibung:`, `input:`, `output:`, `entscheidungen:`, `kanten:`
|
||||
(feste Reihenfolge).
|
||||
- `beschreibung:` genau ein Satz (die Verantwortung), Umbruch mit Einrückung erlaubt.
|
||||
- `input:` was die Einheit entgegennimmt, `output:` was sie liefert bzw. bewirkt —
|
||||
je eine knappe Zeile, keine Prosa.
|
||||
- Jede Entscheidung: kurzer Satz (`- `-Zeile) — eine im Code getroffene Design-
|
||||
Entscheidung oder Garantie, kein Implementierungs-Nacherzählen — gefolgt von
|
||||
`beleg: "<wörtliches Kurzzitat aus der Anker-Datei>"`.
|
||||
- **Der konkrete Code wird nie in Artefakte kopiert** — die UI zieht ihn live über den
|
||||
Anker (Pfad + Symbol) aus der Quelle. Single Source, kein Drift.
|
||||
- `kanten:`-Typen: `ruft-auf`, `nutzt`, `wird-genutzt-von`, `löst-aus`, `wird-ausgelöst-von`.
|
||||
Ziel ist immer eine Einheiten-ID.
|
||||
- MECE-Anspruch v1: Symbol-Ebene; Kleinkram (Imports, Konstanten) darf unzugeordnet
|
||||
bleiben. Jede Funktion/Klasse der Quelldatei gehört zu genau einer Einheit (eine
|
||||
Einheit darf mehrere kleine, zusammengehörige Symbole bündeln — dann mehrere
|
||||
`anker:`-Zeilen unter der Überschrift, ID ist der Hauptanker).
|
||||
|
||||
## Feature-Sicht: `features/<bereich>.md`
|
||||
|
||||
````markdown
|
||||
# Bereich: Generierungs-Pipeline
|
||||
beschreibung: Erzeugt aus einer Quelle die Lerninhalte in drei Board-Stufen.
|
||||
|
||||
## Feature: Resümierbare Kanban-Boards [kern]
|
||||
beschreibung: Generierung läuft als Karten durch Spalten und übersteht Abbrüche.
|
||||
|
||||
### Kann nach Abbruch fortsetzen
|
||||
einheiten: backend/kanban.py::run_flow, backend/database.py::kanban_pull
|
||||
|
||||
### Fehlerhafte Karten landen sichtbar im Dead-Letter
|
||||
einheiten: backend/kanban.py::_worker
|
||||
````
|
||||
|
||||
Regeln:
|
||||
- `# Bereich` → `## Feature` → `### Teilfeature`; Flag `[kern]` oder `[rand]` am Feature.
|
||||
- **Titel kurz, ohne Klammern, ohne „und"** — ein Bereich/Feature erfüllt genau EINE
|
||||
Aufgabe; wer „und" braucht, muss splitten. Beschreibungen: ein kurzer Satz, der den
|
||||
Kern trifft, ebenfalls ohne „und"-Aufzählung.
|
||||
- Teilfeatures tragen NUR `einheiten:`-Referenzen (+ optional `beschreibung:`).
|
||||
Alles Weitere zieht die UI live aus den Einheiten: die Entscheidungs-Liste
|
||||
(aggregiert über die referenzierten Einheiten), je Methode Input/Output und den
|
||||
konkreten Code aus dem Anker.
|
||||
- MECE je Ebene: Bereiche zerlegen das Projekt, Features ihren Bereich, Teilfeatures
|
||||
ihr Feature — vollständig, überschneidungsfrei. Richtwert 6–10 Bereiche, 5–15
|
||||
Features je Bereich.
|
||||
|
||||
## Kern: `kern.md`
|
||||
|
||||
````markdown
|
||||
# Kern: creator
|
||||
Aus einem Thema/Skript/Ordner/Link entsteht ein vollständiger, belegter Lernguide
|
||||
mit Übungssystem — zerlegt nach MECE, jede Aussage mit Beleg.
|
||||
|
||||
tragende-einheiten: backend/kanban.py::run_flow, backend/board_inventory.py, …
|
||||
````
|
||||
|
||||
Wenige Sätze; `tragende-einheiten:` verweist auf das, was das Projekt trägt.
|
||||
|
||||
## Architektur: `architektur.md`
|
||||
|
||||
````markdown
|
||||
# Architektur: creator
|
||||
|
||||
## Komponente: Kanban-Engine
|
||||
beschreibung: Generischer Streaming-Motor; Boards definieren die Stufen.
|
||||
einheiten: backend/kanban.py::Flow, backend/kanban.py::_worker, …
|
||||
beziehungen:
|
||||
- nutzt: Komponente: Datenbank
|
||||
- wird-genutzt-von: Komponente: Boards
|
||||
````
|
||||
|
||||
Komponenten-Beziehungen werden aus den Einheiten-Kanten aggregiert und beim Scan
|
||||
gegen sie geprüft (keine Beziehung ohne mindestens eine tragende Kante).
|
||||
|
||||
## Flows: `flows.md`
|
||||
|
||||
````markdown
|
||||
# Flows: creator
|
||||
|
||||
## Flow: Thema → fertiger Guide
|
||||
1. Research-Agenten sammeln Quellen — backend/board_inventory.py::research
|
||||
2. Titel werden geclustert und benannt — backend/board_inventory.py::cluster
|
||||
3. …
|
||||
````
|
||||
|
||||
Nummerierte Schritte, jeder Schritt endet mit ` — <einheiten-id>[, <einheiten-id>]`.
|
||||
|
||||
## Prüfung: `pruefung.md`
|
||||
|
||||
````markdown
|
||||
# Prüfung: creator
|
||||
tests: make test
|
||||
lint: (keins vorhanden)
|
||||
start: make dev
|
||||
hinweise:
|
||||
- Backend-Edits bei laufendem Flow triggern uvicorn-Reload und killen Läufe.
|
||||
````
|
||||
|
||||
Feste Schlüssel `tests:`, `lint:`, `start:`; fehlt etwas, steht dort explizit
|
||||
`(keins vorhanden)` — Gates degradieren sichtbar, nie still.
|
||||
|
||||
## Decision Record: `entscheidungen/<datum>-<slug>.md`
|
||||
|
||||
````markdown
|
||||
# Entscheidung: Belege als Kurzzitate statt Zeilennummern
|
||||
datum: 2026-07-09
|
||||
status: aktiv
|
||||
betrifft: einheiten-format
|
||||
frage: Wie referenzieren Fakten die Code-Stelle?
|
||||
optionen: Zeilennummern (präzise, aber driften) | Kurzzitate (robust, substring-prüfbar)
|
||||
entscheidung: Kurzzitate.
|
||||
begründung: Zeilennummern veralten bei jedem Diff; Zitate sind mechanisch prüfbar.
|
||||
````
|
||||
|
||||
## Mechanische Prüfungen (Skripte, tokenfrei)
|
||||
|
||||
1. **Anker-Existenz**: jede Einheiten-ID zeigt auf existierende Datei + Symbol darin.
|
||||
2. **Zitat-Treue**: jeder `beleg:`-String kommt wörtlich in der Anker-Datei vor.
|
||||
3. **Referenz-Integrität**: jede `einheiten:`-Referenz in Sichten existiert; jedes
|
||||
Kanten-Ziel existiert.
|
||||
4. **Abdeckung**: jedes Symbol der Quelldatei ist einer Einheit zugeordnet (Report,
|
||||
v1 kein harter Fehler).
|
||||
5. **Struktur**: Dateien folgen der Grammatik (Schlüssel, Reihenfolge, Flags).
|
||||
+1
@@ -0,0 +1 @@
|
||||
.karte[data-v-f7a93d8e]{background:var(--panel);border:1px solid var(--rand);border-radius:10px;padding:1rem 1.2rem}.karte.tot[data-v-f7a93d8e]{opacity:.7}.zeile[data-v-f7a93d8e]{align-items:baseline;gap:.6rem;display:flex}.name[data-v-f7a93d8e]{color:var(--text);font-weight:600;text-decoration:none}a.name[data-v-f7a93d8e]:hover{color:var(--akzent)}.detail[data-v-f7a93d8e]{color:var(--dezent);overflow-wrap:anywhere;margin:.3rem 0 0;font-size:.85rem}.drift[data-v-f7a93d8e]{color:var(--gelb)}.aktionen[data-v-f7a93d8e]{margin-top:.7rem}.meldung[data-v-f7a93d8e]{color:var(--rot);margin:.5rem 0 0;font-size:.85rem}.liste[data-v-78297d84]{grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem;display:grid}.fehler[data-v-78297d84]{color:var(--rot)}.leer[data-v-78297d84]{color:var(--dezent)}.bereich[data-v-f039a64a]{margin-bottom:1.6rem}.bereich h3[data-v-f039a64a]{margin:0 0 .1rem;font-size:1.05rem}.bereich>.besch[data-v-f039a64a]{margin:0 0 .7rem}.besch[data-v-f039a64a]{color:var(--dezent);margin:.3rem 0 0;font-size:.9rem}.raster[data-v-f039a64a]{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:.8rem;display:grid}.feature[data-v-f039a64a]{cursor:pointer}.feature[data-v-f039a64a]:hover{border-color:var(--akzent)}.feature.offen[data-v-f039a64a]{grid-column:1/-1}.kopf[data-v-f039a64a]{align-items:baseline;gap:.6rem;display:flex}.faehigkeiten[data-v-f039a64a]{margin:.6rem 0 0;padding-left:1.3rem}.faehigkeiten li[data-v-f039a64a]{margin:.25rem 0}.mehr[data-v-f039a64a]{color:var(--akzent);margin-top:.5rem;font-size:.82rem;display:block}.einheit[data-v-58d66282]{border:1px solid var(--rand);background:var(--bg);border-radius:8px;margin:.3rem 0 .5rem 1.1rem}.einheit>summary[data-v-58d66282]{gap:.5rem;padding:.4rem .6rem;font-size:.82rem;display:flex}.inhalt[data-v-58d66282]{padding:.1rem .8rem .7rem;font-size:.9rem}.besch[data-v-58d66282]{margin:.2rem 0}.io[data-v-58d66282]{color:var(--dezent);grid-template-columns:max-content 1fr;gap:.1rem .7rem;margin:.3rem 0;font-size:.85rem;display:grid}.io dt[data-v-58d66282]{font-weight:600}.io dd[data-v-58d66282]{margin:0}.entscheidungen[data-v-58d66282]{margin:.3rem 0;padding-left:1.2rem}.entscheidungen li[data-v-58d66282]{margin:.15rem 0}.kanten[data-v-58d66282]{color:var(--dezent);font-size:.82rem}.kein-code[data-v-58d66282]{color:var(--gelb);font-size:.85rem}.fehler[data-v-58d66282]{color:var(--rot)}.laden[data-v-58d66282]{color:var(--dezent)}.komponente[data-v-dc3c6459]{background:var(--panel);border:1px solid var(--rand);border-radius:10px;margin-bottom:1rem;padding:.9rem 1.2rem}.komponente h3[data-v-dc3c6459]{margin:0;font-size:1.02rem}.besch[data-v-dc3c6459]{color:var(--dezent);margin:.2rem 0;font-size:.9rem}.beziehungen[data-v-dc3c6459]{color:var(--akzent);margin:.2rem 0 .5rem;font-size:.85rem}summary[data-v-dc3c6459]{color:var(--dezent);padding:.3rem 0;font-size:.9rem}.leer[data-v-dc3c6459]{color:var(--dezent)}.flow[data-v-9b2e52a0]{background:var(--panel);border:1px solid var(--rand);border-radius:10px;margin-bottom:1rem;padding:.9rem 1.2rem}.flow h3[data-v-9b2e52a0]{margin:0 0 .4rem;font-size:1.02rem}.flow li[data-v-9b2e52a0]{margin:.3rem 0}.refs>summary[data-v-9b2e52a0]{color:var(--dezent);padding:.15rem 0;font-size:.8rem}.leer[data-v-9b2e52a0]{color:var(--dezent)}.pruefung[data-v-480e8270]{background:var(--panel);border:1px solid var(--rand);border-radius:10px;padding:.9rem 1.2rem}dl[data-v-480e8270]{grid-template-columns:max-content 1fr;gap:.3rem 1rem;margin:0;display:grid}dt[data-v-480e8270]{font-weight:600}dd[data-v-480e8270]{margin:0}ul[data-v-480e8270]{color:var(--dezent);margin:.7rem 0 0}.doku h2[data-v-b57ab26e]{color:var(--dezent);margin:1.4rem 0 .6rem;font-size:1.05rem}.doku h2[data-v-b57ab26e]:first-child{margin-top:0}.leiste[data-v-5d2db867]{background:var(--panel);border:1px solid var(--rand);border-radius:10px;align-self:start;padding:.9rem 1.1rem;position:sticky;top:1rem}.leiste h3[data-v-5d2db867]{margin:0 0 .5rem;font-size:.95rem}.leiste h4[data-v-5d2db867]{color:var(--dezent);text-transform:uppercase;letter-spacing:.04em;margin:.7rem 0 .3rem;font-size:.8rem}.stufen[data-v-5d2db867]{grid-template-columns:1fr max-content;gap:.15rem .8rem;margin:0;font-size:.85rem;display:grid}.stufen dt[data-v-5d2db867]{color:var(--dezent)}.stufen dd[data-v-5d2db867]{text-align:right;margin:0}.agent[data-v-5d2db867]{overflow-wrap:anywhere;margin:.2rem 0;font-size:.78rem}.dead[data-v-5d2db867]{color:var(--rot);margin:.6rem 0 0;font-size:.82rem}.rahmen.mitLeiste[data-v-50cf4d28]{grid-template-columns:1fr 300px;gap:1.2rem;display:grid}.kern[data-v-50cf4d28]{margin-bottom:1.2rem}.kern h2[data-v-50cf4d28]{margin:0 0 .4rem;font-size:1.2rem}.kern-text[data-v-50cf4d28] p{color:var(--dezent);margin:.3rem 0}.drift-hinweis[data-v-50cf4d28]{color:var(--gelb);margin:.6rem 0 0;font-size:.88rem}.fehler[data-v-50cf4d28]{color:var(--rot)}.laden[data-v-50cf4d28]{color:var(--dezent)}.shell[data-v-bb207b1a]{grid-template-columns:220px 1fr;min-height:100vh;display:Line truncated
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){let t=Object.create(null);for(let n of e.split(`,`))t[n]=1;return e=>e in t}var t={},n=[],r=()=>{},i=()=>!1,a=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),o=e=>e.startsWith(`onUpdate:`),s=Object.assign,c=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},l=Object.prototype.hasOwnProperty,u=(e,t)=>l.call(e,t),d=Array.isArray,f=e=>x(e)===`[object Map]`,p=e=>x(e)===`[object Set]`,m=e=>x(e)===`[object Date]`,h=e=>typeof e==`function`,g=e=>typeof e==`string`,_=e=>typeof e==`symbol`,v=e=>typeof e==`object`&&!!e,y=e=>(v(e)||h(e))&&h(e.then)&&h(e.catch),b=Object.prototype.toString,x=e=>b.call(e),ee=e=>x(e).slice(8,-1),S=e=>x(e)===`[object Object]`,C=e=>g(e)&&e!==`NaN`&&e[0]!==`-`&&``+parseInt(e,10)===e,te=e(`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`),ne=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},re=/-\w/g,w=ne(e=>e.replace(re,e=>e.slice(1).toUpperCase())),ie=/\B([A-Z])/g,T=ne(e=>e.replace(ie,`-$1`).toLowerCase()),ae=ne(e=>e.charAt(0).toUpperCase()+e.slice(1)),oe=ne(e=>e?`on${ae(e)}`:``),E=(e,t)=>!Object.is(e,t),se=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},D=(e,t,n,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},ce=e=>{let t=parseFloat(e);return isNaN(t)?e:t},le,ue=()=>le||=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{};function de(e){if(d(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],i=g(r)?he(r):de(r);if(i)for(let e in i)t[e]=i[e]}return t}else if(g(e)||v(e))return e}var fe=/;(?![^(]*\))/g,pe=/:([^]+)/,me=/\/\*[^]*?\*\//g;function he(e){let t={};return e.replace(me,``).split(fe).forEach(e=>{if(e){let n=e.split(pe);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function O(e){let t=``;if(g(e))t=e;else if(d(e))for(let n=0;n<e.length;n++){let r=O(e[n]);r&&(t+=r+` `)}else if(v(e))for(let n in e)e[n]&&(t+=n+` `);return t.trim()}var ge=`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`,_e=e(ge);ge+``;function ve(e){return!!e||e===``}function ye(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&&r<e.length;r++)n=be(e[r],t[r]);return n}function be(e,t){if(e===t)return!0;let n=m(e),r=m(t);if(n||r)return n&&r?e.getTime()===t.getTime():!1;if(n=_(e),r=_(t),n||r)return e===t;if(n=d(e),r=d(t),n||r)return n&&r?ye(e,t):!1;if(n=v(e),r=v(t),n||r){if(!n||!r||Object.keys(e).length!==Object.keys(t).length)return!1;for(let n in e){let r=e.hasOwnProperty(n),i=t.hasOwnProperty(n);if(r&&!i||!r&&i||!be(e[n],t[n]))return!1}}return String(e)===String(t)}var xe=e=>!!(e&&e.__v_isRef===!0),k=e=>g(e)?e:e==null?``:d(e)||v(e)&&(e.toString===b||!h(e.toString))?xe(e)?k(e.value):JSON.stringify(e,Se,2):String(e),Se=(e,t)=>xe(t)?Se(e,t.value):f(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((e,[t,n],r)=>(e[Ce(t,r)+` =>`]=n,e),{})}:p(t)?{[`Set(${t.size})`]:[...t.values()].map(e=>Ce(e))}:_(t)?Ce(t):v(t)&&!d(t)&&!S(t)?String(t):t,Ce=(e,t=``)=>_(e)?`Symbol(${e.description??t})`:e,A,we=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!e&&A&&(A.active?(this.parent=A,this.index=(A.scopes||=[]).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].pause();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].resume();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].resume()}}run(e){if(this._active){let t=A;try{return A=this,e()}finally{A=t}}}on(){++this._on===1&&(this.prevScope=A,A=this)}off(){if(this._on>0&&--this._on===0){if(A===this)A=this.prevScope;else{let e=A;for(;e;){if(e.prevScope===this){e.prevScope=this.prevScope;break}e=e.prevScope}}this.prevScope=void 0}}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t<n;t++)this.effects[t].stop();for(this.effects.length=0,t=0,n=this.cleanups.length;t<n;t++)this.cleanups[t]();if(this.cleanups.lenLine truncated
|
||||
]`).replace(`lheading`,Go).replace(`|table`,``).replace(`blockquote`,` {0,3}>`).replace(`|fences`,``).replace(`|list`,``).replace(`|html`,``).replace(`|tag`,``).getRegex()},os=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ss=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,cs=/^( {2,}|\\)\n(?!\s*$)/,ls=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,us=/[\p{P}\p{S}]/u,ds=/[\s\p{P}\p{S}]/u,fs=/[^\s\p{P}\p{S}]/u,ps=Z(/^((?![*_])punctSpace)/,`u`).replace(/punctSpace/g,ds).getRegex(),ms=/(?!~)[\p{P}\p{S}]/u,hs=/(?!~)[\s\p{P}\p{S}]/u,gs=/(?:[^\s\p{P}\p{S}]|~)/u,_s=Z(/link|precode-code|html/,`g`).replace(`link`,/\[(?:[^\[\]`]|(?<a>`+)[^`]+\k<a>(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace(`precode-`,Lo?"(?<!`)()":"(^^|[^`])").replace(`code`,/(?<b>`+)[^`]+\k<b>(?!`)/).replace(`html`,/<(?! )[^<>]*?>/).getRegex(),vs=/^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/,ys=Z(vs,`u`).replace(/punct/g,us).getRegex(),bs=Z(vs,`u`).replace(/punct/g,ms).getRegex(),xs=`^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)`,Ss=Z(xs,`gu`).replace(/notPunctSpace/g,fs).replace(/punctSpace/g,ds).replace(/punct/g,us).getRegex(),Cs=Z(xs,`gu`).replace(/notPunctSpace/g,gs).replace(/punctSpace/g,hs).replace(/punct/g,ms).getRegex(),ws=Z(`^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)`,`gu`).replace(/notPunctSpace/g,fs).replace(/punctSpace/g,ds).replace(/punct/g,us).getRegex(),Ts=Z(/^~~?(?:((?!~)punct)|[^\s~])/,`u`).replace(/punct/g,us).getRegex(),Es=Z(`^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)`,`gu`).replace(/notPunctSpace/g,fs).replace(/punctSpace/g,ds).replace(/punct/g,us).getRegex(),Ds=Z(/\\(punct)/,`gu`).replace(/punct/g,us).getRegex(),Os=Z(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace(`scheme`,/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace(`email`,/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),ks=Z($o).replace(`(?:-->|$)`,`-->`).getRegex(),As=Z(`^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>`).replace(`comment`,ks).replace(`attribute`,/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),js=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/,Ms=Z(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/).replace(`label`,js).replace(`href`,/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace(`title`,/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Ns=Z(/^!?\[(label)\]\[(ref)\]/).replace(`label`,js).replace(`ref`,Yo).getRegex(),Ps=Z(/^!?\[(ref)\](?:\[\])?/).replace(`ref`,Yo).getRegex(),Fs=Z(`reflink|nolink(?!\\()`,`g`).replace(`reflink`,Ns).replace(`nolink`,Ps).getRegex(),Is=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,Ls={_backpedal:Fo,anyPunctuation:Ds,autolink:Os,blockSkip:_s,br:cs,code:ss,del:Fo,delLDelim:Fo,delRDelim:Fo,emStrongLDelim:ys,emStrongRDelimAst:Ss,emStrongRDelimUnd:ws,escape:os,link:Ms,nolink:Ps,punctuation:ps,reflink:Ns,reflinkSearch:Fs,tag:As,text:ls,url:Fo},Rs={...Ls,link:Z(/^!?\[(label)\]\((.*?)\)/).replace(`label`,js).getRegex(),reflink:Z(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace(`label`,js).getRegex()},zs={...Ls,emStrongRDelimAst:Cs,emStrongLDelim:bs,delLDelim:Ts,delRDelim:Es,url:Z(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace(`protocol`,Is).replace(`email`,/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:Z(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/).replace(`protocol`,Is).getRegex()},Bs={...zs,br:Z(cs).replace(`{2,}`,`*`).getRegex(),text:Z(zs.text).replace(`\\b_`,`\\b_| {2,}\\n`).replace(/\{2,\}/g,`*`).getRegex()},Vs={normal:ns,gfm:is,pedantic:as},Hs={normal:Ls,gfm:zs,breaks:Bs,pedantic:Rs},Us={"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`},Ws=e=>Us[e];function Gs(e,t){if(t){if(Q.escapeTest.test(e))return e.replace(Q.escapeReplace,Ws)}else if(Q.escapeTestNoEncode.test(e))return e.replace(Q.escapeReLine truncated
|
||||
`),n=t.length-1;for(;n>=0&&Q.blankLine.test(t[n]);)n--;return t.length-n<=2?e:t.slice(0,n+1).join(`
|
||||
`)}function Xs(e,t){if(e.indexOf(t[1])===-1)return-1;let n=0;for(let r=0;r<e.length;r++)if(e[r]===`\\`)r++;else if(e[r]===t[0])n++;else if(e[r]===t[1]&&(n--,n<0))return r;return n>0?-2:-1}function Zs(e,t=0){let n=t,r=``;for(let t of e)if(t===` `){let e=4-n%4;r+=` `.repeat(e),n+=e}else r+=t,n++;return r}function Qs(e,t,n,r,i){let a=t.href,o=t.title||null,s=e[1].replace(i.other.outputLinkReplace,`$1`);r.state.inLink=!0;let c={type:e[0].charAt(0)===`!`?`image`:`link`,raw:n,href:a,title:o,text:s,tokens:r.inlineTokens(s)};return r.state.inLink=!1,c}function $s(e,t,n){let r=e.match(n.other.indentCodeCompensation);if(r===null)return t;let i=r[1];return t.split(`
|
||||
`).map(e=>{let t=e.match(n.other.beginningSpace);if(t===null)return e;let[r]=t;return r.length>=i.length?e.slice(i.length):e}).join(`
|
||||
`)}var ec=class{options;rules;lexer;constructor(e){this.options=e||No}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:`space`,raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let e=this.options.pedantic?t[0]:Ys(t[0]);return{type:`code`,raw:e,codeBlockStyle:`indented`,text:e.replace(this.rules.other.codeRemoveIndent,``)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let e=t[0],n=$s(e,t[3]||``,this.rules);return{type:`code`,raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,`$1`):t[2],text:n}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(this.rules.other.endingHash.test(e)){let t=Js(e,`#`);(this.options.pedantic||!t||this.rules.other.endingSpaceChar.test(t))&&(e=t.trim())}return{type:`heading`,raw:Js(t[0],`
|
||||
`),depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:`hr`,raw:Js(t[0],`
|
||||
`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let e=Js(t[0],`
|
||||
`).split(`
|
||||
`),n=``,r=``,i=[];for(;e.length>0;){let t=!1,a=[],o;for(o=0;o<e.length;o++)if(this.rules.other.blockquoteStart.test(e[o]))a.push(e[o]),t=!0;else if(!t)a.push(e[o]);else break;e=e.slice(o);let s=a.join(`
|
||||
`),c=s.replace(this.rules.other.blockquoteSetextReplace,`
|
||||
$1`).replace(this.rules.other.blockquoteSetextReplace2,``);n=n?`${n}
|
||||
${s}`:s,r=r?`${r}
|
||||
${c}`:c;let l=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(c,i,!0),this.lexer.state.top=l,e.length===0)break;let u=i.at(-1);if(u?.type===`code`)break;if(u?.type===`blockquote`){let t=u,a=t.raw+`
|
||||
`+e.join(`
|
||||
`),o=this.blockquote(a);i[i.length-1]=o,n=n.substring(0,n.length-t.raw.length)+o.raw,r=r.substring(0,r.length-t.text.length)+o.text;break}else if(u?.type===`list`){let t=u,a=t.raw+`
|
||||
`+e.join(`
|
||||
`),o=this.list(a);i[i.length-1]=o,n=n.substring(0,n.length-u.raw.length)+o.raw,r=r.substring(0,r.length-t.raw.length)+o.raw,e=a.substring(i.at(-1).raw.length).split(`
|
||||
`);continue}}return{type:`blockquote`,raw:n,tokens:i,text:r}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),r=n.length>1,i={type:`list`,raw:``,ordered:r,start:r?+n.slice(0,-1):``,loose:!1,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:`[*+-]`);let a=this.rules.other.listItemRegex(n),o=!1;for(;e;){let n=!1,r=``,s=``;if(!(t=a.exec(e))||this.rules.block.hr.test(e))break;r=t[0],e=e.substring(r.length);let c=Zs(t[2].split(`
|
||||
`,1)[0],t[1].length),l=e.split(`
|
||||
`,1)[0],u=!c.trim(),d=0;if(this.options.pedantic?(d=2,s=c.trimStart()):u?d=t[1].length+1:(d=c.search(this.rules.other.nonSpaceChar),d=d>4?1:d,s=c.slice(d),d+=t[1].length),u&&this.rules.other.blankLine.test(l)&&(r+=l+`
|
||||
`,e=e.substring(l.length+1),n=!0),!n){let t=this.rules.other.nextBulletRegex(d),n=this.rules.other.hrRegex(d),i=this.rules.other.fencesBeginRegex(d),a=this.rules.other.headingBeginRegex(d),o=this.rules.other.htmlBeginRegex(d),f=this.rules.other.blockquoteBeginRegex(d);for(;e;){let p=e.split(`
|
||||
`,1)[0],m;if(l=p,this.options.pedantic?(l=l.replace(this.rules.other.listReplaceNesting,` `),m=l):m=l.replace(this.rules.other.tabCharGlobal,` `),i.test(l)||a.test(l)||o.test(l)||f.test(l)||t.test(l)||n.test(l))break;if(m.search(this.rules.other.nonSpaceChar)>=d||!l.trim())s+=`
|
||||
`+m.slice(d);else{if(u||c.replace(this.rules.other.tabCharGlobal,` `).search(this.rules.other.nonSpaceChar)>=4||i.test(c)||a.test(c)||n.test(c))break;s+=`
|
||||
`+l}u=!l.trim(),r+=p+`
|
||||
`,e=e.substring(p.length+1),c=m.slice(d)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(r)&&(o=!0)),i.items.push({type:`list_item`,raw:r,task:!!this.options.gfm&&this.rules.other.listIsTask.test(s),loose:!1,text:s,tokens:[]}),i.raw+=r}let s=i.items.at(-1);if(s)s.raw=s.raw.trimEnd(),s.text=s.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let e of i.items){this.lexer.state.top=!1,e.tokens=this.lexer.blockTokens(e.text,[]);let t=e.tokens[0];if(e.task&&(t?.type===`text`||t?.type===`paragraph`)){e.text=e.text.replace(this.rules.other.listReplaceTask,``),t.raw=t.raw.replace(this.rules.other.listReplaceTask,``),t.text=t.text.replace(this.rules.other.listReplaceTask,``);for(let e=this.lexer.inlineQueue.length-1;e>=0;e--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[e].src)){this.lexer.inlineQueue[e].src=this.lexer.inlineQueue[e].src.replace(this.rules.other.listReplaceTask,``);break}let n=this.rules.other.listTaskCheckbox.exec(e.raw);if(n){let t={type:`checkbox`,raw:n[0]+` `,checked:n[0]!==`[ ]`};e.checked=t.checked,i.loose?e.tokens[0]&&[`paragraph`,`text`].includes(e.tokens[0].type)&&`tokens`in e.tokens[0]&&e.tokens[0].tokens?(e.tokens[0].raw=t.raw+e.tokens[0].raw,e.tokens[0].text=t.raw+e.tokens[0].text,e.tokens[0].tokens.unshift(t)):e.tokens.unshift({type:`paragraph`,raw:t.raw,text:t.raw,tokens:[t]}):e.tokens.unshift(t)}}else e.task&&=!1;if(!i.loose){let t=e.tokens.filter(e=>e.type===`space`);i.loose=t.length>0&&t.some(e=>this.rules.other.anyLine.test(e.raw))}}if(i.loose)for(let e of i.items){e.loose=!0;for(let t of e.tokens)t.type===`text`&&(t.type=`paragraph`)}return i}}html(e){let t=this.rules.block.html.exec(e);if(t){let e=Ys(t[0]);return{type:`html`,block:!0,raw:e,pre:t[1]===`pre`||t[1]===`script`||t[1]===`style`,text:e}}}def(e){let t=this.rules.block.def.exec(e);if(t){let e=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal,` `),n=t[2]?t[2].replace(this.rules.other.hrefBrackets,`$1`).replace(this.rules.inline.anyPunctuation,`$1`):``,r=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,`$1`):t[3];return{type:`def`,tag:e,raw:Js(t[0],`
|
||||
`),href:n,title:r}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=qs(t[1]),r=t[2].replace(this.rules.other.tableAlignChars,``).split(`|`),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,``).split(`
|
||||
`):[],a={type:`table`,raw:Js(t[0],`
|
||||
`),header:[],align:[],rows:[]};if(n.length===r.length){for(let e of r)this.rules.other.tableAlignRight.test(e)?a.align.push(`right`):this.rules.other.tableAlignCenter.test(e)?a.align.push(`center`):this.rules.other.tableAlignLeft.test(e)?a.align.push(`left`):a.align.push(null);for(let e=0;e<n.length;e++)a.header.push({text:n[e],tokens:this.lexer.inline(n[e]),header:!0,align:a.align[e]});for(let e of i)a.rows.push(qs(e,a.header.length).map((e,t)=>({text:e,tokens:this.lexer.inline(e),header:!1,align:a.align[t]})));return a}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t){let e=t[1].trim();return{type:`heading`,raw:Js(t[0],`
|
||||
`),depth:t[2].charAt(0)===`=`?1:2,text:e,tokens:this.lexer.inline(e)}}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let e=t[1].charAt(t[1].length-1)===`
|
||||
`?t[1].slice(0,-1):t[1];return{type:`paragraph`,raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:`text`,raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:`escape`,raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:`html`,raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let e=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(e)){if(!this.rules.other.endAngleBracket.test(e))return;let t=Js(e.slice(0,-1),`\\`);if((e.length-t.length)%2==0)return}else{let e=Xs(t[2],`()`);if(e===-2)return;if(e>-1){let n=(t[0].indexOf(`!`)===0?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=``}}let n=t[2],r=``;if(this.options.pedantic){let e=this.rules.other.pedanticHrefTitle.exec(n);e&&(n=e[1],r=e[3])}else r=t[3]?t[3].slice(1,-1):``;return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(n=this.options.pedantic&&!this.rules.other.endAngleBracket.test(e)?n.slice(1):n.slice(1,-1)),Qs(t,{href:n&&n.replace(this.rules.inline.anyPunctuation,`$1`),title:r&&r.replace(this.rules.inline.anyPunctuation,`$1`)},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let e=t[(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal,` `).toLowerCase()];if(!e){let e=n[0].charAt(0);return{type:`text`,raw:e,text:e}}return Qs(n,e,n[0],this.lexer,this.rules)}}emStrong(e,t,n=``){let r=this.rules.inline.emStrongLDelim.exec(e);if(!(!r||!r[1]&&!r[2]&&!r[3]&&!r[4]||r[4]&&n.match(this.rules.other.unicodeAlphaNumeric))&&(!(r[1]||r[3])||!n||this.rules.inline.punctuation.exec(n))){let n=[...r[0]].length-1,i,a,o=n,s=0,c=r[0][0]===`*`?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+n);(r=c.exec(t))!==null;){if(i=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!i)continue;if(a=[...i].length,r[3]||r[4]){o+=a;continue}else if((r[5]||r[6])&&n%3&&!((n+a)%3)){s+=a;continue}if(o-=a,o>0)continue;a=Math.min(a,a+o+s);let t=[...r[0]][0].length,c=e.slice(0,n+r.index+t+a);if(Math.min(n,a)%2){let e=c.slice(1,-1);return{type:`em`,raw:c,text:e,tokens:this.lexer.inlineTokens(e)}}let l=c.slice(2,-2);return{type:`strong`,raw:c,text:l,tokens:this.lexer.inlineTokens(l)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(this.rules.other.newLineCharGlobal,` `),n=this.rules.other.nonSpaceChar.test(e),r=this.rules.other.startingSpaceChar.test(e)&&this.rules.other.endingSpaceChar.test(e);return n&&r&&(e=e.substring(1,e.length-1)),{type:`codespan`,raw:t[0],text:e}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:`br`,raw:t[0]}}del(e,t,n=``){let r=this.rules.inline.delLDelim.exec(e);if(r&&(!r[1]||!n||this.rules.inline.punctuation.exec(n))){let n=[...r[0]].length-1,i,a,o=n,s=this.rules.inline.delRDelim;for(s.lastIndex=0,t=t.slice(-1*e.length+n);(r=s.exec(t))!==null;){if(i=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!i||(a=[...i].length,a!==n))continue;if(r[3]||r[4]){o+=a;continue}if(o-=a,o>0)continue;a=Math.min(a,a+o);let t=[...r[0]][0].length,s=e.slice(0,n+r.index+t+a),c=s.slice(n,-n);return{type:`del`,raw:s,text:c,tokens:this.lexer.inlineTokens(c)}}}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let e,n;return t[2]===`@`?(e=t[1],n=`mailto:`+e):(e=t[1],n=e),{type:`link`,raw:t[0],text:e,href:n,tokens:[{type:`text`,raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if(t[2]===`@`)e=t[0],n=`mailto:`+e;else{let r;do r=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??``;while(r!==t[0]);e=t[0],n=t[1]===`www.`?`http://`+t[0]:t[0]}return{type:`link`,raw:t[0],text:e,href:n,tokens:[{type:`text`,raw:e,text:e}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let e=this.lexer.state.inRawBlock;return{type:`text`,raw:t[0],text:t[0],escaped:e}}}},tc=class e{tokens;options;state;inlineQueue;tokenizer;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||No,this.options.tokenizer=this.options.tokenizer||new ec,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:Q,block:Vs.normal,inline:Hs.normal};this.options.pedantic?(t.block=Vs.pedantic,t.inline=Hs.pedantic):this.options.gfm&&(t.block=Vs.gfm,this.options.breaks?t.inline=Hs.breaks:t.inline=Hs.gfm
Line truncated
|
||||
`),this.blockTokens(e,this.tokens);for(let e=0;e<this.inlineQueue.length;e++){let t=this.inlineQueue[e];this.inlineTokens(t.src,t.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){this.tokenizer.lexer=this,this.options.pedantic&&(e=e.replace(Q.tabCharGlobal,` `).replace(Q.spaceLine,``));let r=1/0;for(;e;){if(e.length<r)r=e.length;else{this.infiniteLoopError(e.charCodeAt(0));break}let i;if(this.options.extensions?.block?.some(n=>(i=n.call({lexer:this},e,t))?(e=e.substring(i.raw.length),t.push(i),!0):!1))continue;if(i=this.tokenizer.space(e)){e=e.substring(i.raw.length);let n=t.at(-1);i.raw.length===1&&n!==void 0?n.raw+=`
|
||||
`:t.push(i);continue}if(i=this.tokenizer.code(e)){e=e.substring(i.raw.length);let n=t.at(-1);n?.type===`paragraph`||n?.type===`text`?(n.raw+=(n.raw.endsWith(`
|
||||
`)?``:`
|
||||
`)+i.raw,n.text+=`
|
||||
`+i.text,this.inlineQueue.at(-1).src=n.text):t.push(i);continue}if(i=this.tokenizer.fences(e)){e=e.substring(i.raw.length),t.push(i);continue}if(i=this.tokenizer.heading(e)){e=e.substring(i.raw.length),t.push(i);continue}if(i=this.tokenizer.hr(e)){e=e.substring(i.raw.length),t.push(i);continue}if(i=this.tokenizer.blockquote(e)){e=e.substring(i.raw.length),t.push(i);continue}if(i=this.tokenizer.list(e)){e=e.substring(i.raw.length),t.push(i);continue}if(i=this.tokenizer.html(e)){e=e.substring(i.raw.length),t.push(i);continue}if(i=this.tokenizer.def(e)){e=e.substring(i.raw.length);let n=t.at(-1);n?.type===`paragraph`||n?.type===`text`?(n.raw+=(n.raw.endsWith(`
|
||||
`)?``:`
|
||||
`)+i.raw,n.text+=`
|
||||
`+i.raw,this.inlineQueue.at(-1).src=n.text):this.tokens.links[i.tag]||(this.tokens.links[i.tag]={href:i.href,title:i.title},t.push(i));continue}if(i=this.tokenizer.table(e)){e=e.substring(i.raw.length),t.push(i);continue}if(i=this.tokenizer.lheading(e)){e=e.substring(i.raw.length),t.push(i);continue}let a=e;if(this.options.extensions?.startBlock){let t=1/0,n=e.slice(1),r;this.options.extensions.startBlock.forEach(e=>{r=e.call({lexer:this},n),typeof r==`number`&&r>=0&&(t=Math.min(t,r))}),t<1/0&&t>=0&&(a=e.substring(0,t+1))}if(this.state.top&&(i=this.tokenizer.paragraph(a))){let r=t.at(-1);n&&r?.type===`paragraph`?(r.raw+=(r.raw.endsWith(`
|
||||
`)?``:`
|
||||
`)+i.raw,r.text+=`
|
||||
`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(i),n=a.length!==e.length,e=e.substring(i.raw.length);continue}if(i=this.tokenizer.text(e)){e=e.substring(i.raw.length);let n=t.at(-1);n?.type===`text`?(n.raw+=(n.raw.endsWith(`
|
||||
`)?``:`
|
||||
`)+i.raw,n.text+=`
|
||||
`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=n.text):t.push(i);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){this.tokenizer.lexer=this;let n=e,r=null;if(this.tokens.links){let e=Object.keys(this.tokens.links);if(e.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!==null;)e.includes(r[0].slice(r[0].lastIndexOf(`[`)+1,-1))&&(n=n.slice(0,r.index)+`[`+`a`.repeat(r[0].length-2)+`]`+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!==null;)n=n.slice(0,r.index)+`++`+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let i;for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!==null;)i=r[2]?r[2].length:0,n=n.slice(0,r.index+i)+`[`+`a`.repeat(r[0].length-i-2)+`]`+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let a=!1,o=``,s=1/0;for(;e;){if(e.length<s)s=e.length;else{this.infiniteLoopError(e.charCodeAt(0));break}a||(o=``),a=!1;let r;if(this.options.extensions?.inline?.some(n=>(r=n.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.escape(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.tag(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.link(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(r.raw.length);let n=t.at(-1);r.type===`text`&&n?.type===`text`?(n.raw+=r.raw,n.text+=r.text):t.push(r);continue}if(r=this.tokenizer.emStrong(e,n,o)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.codespan(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.br(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.del(e,n,o)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.autolink(e)){e=e.substring(r.raw.length),t.push(r);continue}if(!this.state.inLink&&(r=this.tokenizer.url(e))){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startInline){let t=1/0,n=e.slice(1),r;this.options.extensions.startInline.forEach(e=>{r=e.call({lexer:this},n),typeof r==`number`&&r>=0&&(t=Math.min(t,r))}),t<1/0&&t>=0&&(i=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(i)){e=e.substring(r.raw.length),r.raw.slice(-1)!==`_`&&(o=r.raw.slice(-1)),a=!0;let n=t.at(-1);n?.type===`text`?(n.raw+=r.raw,n.text+=r.text):t.push(r);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return t}infiniteLoopError(e){let t=`Infinite loop on byte: `+e;if(this.options.silent)console.error(t);else throw Error(t)}},nc=class{options;parser;constructor(e){this.options=e||No}space(e){return``}code({text:e,lang:t,escaped:n}){let r=(t||``).match(Q.notSpaceStart)?.[0],i=e.replace(Q.endingNewline,``)+`
|
||||
`;return r?`<pre><code class="language-`+Gs(r)+`">`+(n?i:Gs(i,!0))+`</code></pre>
|
||||
`:`<pre><code>`+(n?i:Gs(i,!0))+`</code></pre>
|
||||
`}blockquote({tokens:e}){return`<blockquote>
|
||||
${this.parser.parse(e)}</blockquote>
|
||||
`}html({text:e}){return e}def(e){return``}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>
|
||||
`}hr(e){return`<hr>
|
||||
`}list(e){let t=e.ordered,n=e.start,r=``;for(let t=0;t<e.items.length;t++){let n=e.items[t];r+=this.listitem(n)}let i=t?`ol`:`ul`,a=t&&n!==1?` start="`+n+`"`:``;return`<`+i+a+`>
|
||||
`+r+`</`+i+`>
|
||||
`}listitem(e){return`<li>${this.parser.parse(e.tokens)}</li>
|
||||
`}checkbox({checked:e}){return`<input `+(e?`checked="" `:``)+`disabled="" type="checkbox"> `}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>
|
||||
`}table(e){let t=``,n=``;for(let t=0;t<e.header.length;t++)n+=this.tablecell(e.header[t]);t+=this.tablerow({text:n});let r=``;for(let t=0;t<e.rows.length;t++){let i=e.rows[t];n=``;for(let e=0;e<i.length;e++)n+=this.tablecell(i[e]);r+=this.tablerow({text:n})}return r&&=`<tbody>${r}</tbody>`,`<table>
|
||||
<thead>
|
||||
`+t+`</thead>
|
||||
`+r+`</table>
|
||||
`}tablerow({text:e}){return`<tr>
|
||||
${e}</tr>
|
||||
`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?`th`:`td`;return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
|
||||
`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${Gs(e,!0)}</code>`}br(e){return`<br>`}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),i=Ks(e);if(i===null)return r;e=i;let a=`<a href="`+e+`"`;return t&&(a+=` title="`+Gs(t)+`"`),a+=`>`+r+`</a>`,a}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let i=Ks(e);if(i===null)return Gs(n);e=i;let a=`<img src="${e}" alt="${Gs(n)}"`;return t&&(a+=` title="${Gs(t)}"`),a+=`>`,a}text(e){return`tokens`in e&&e.tokens?this.parser.parseInline(e.tokens):`escaped`in e&&e.escaped?e.text:Gs(e.text)}},rc=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return``+e}image({text:e}){return``+e}br(){return``}checkbox({raw:e}){return e}},ic=class e{options;renderer;textRenderer;constructor(e){this.options=e||No,this.options.renderer=this.options.renderer||new nc,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new rc}static parse(t,n){return new e(n).parse(t)}static parseInline(t,n){return new e(n).parseInline(t)}parse(e){this.renderer.parser=this;let t=``;for(let n=0;n<e.length;n++){let r=e[n];if(this.options.extensions?.renderers?.[r.type]){let e=r,n=this.options.extensions.renderers[e.type].call({parser:this},e);if(n!==!1||![`space`,`hr`,`heading`,`code`,`table`,`blockquote`,`list`,`html`,`def`,`paragraph`,`text`].includes(e.type)){t+=n||``;continue}}let i=r;switch(i.type){case`space`:t+=this.renderer.space(i);break;case`hr`:t+=this.renderer.hr(i);break;case`heading`:t+=this.renderer.heading(i);break;case`code`:t+=this.renderer.code(i);break;case`table`:t+=this.renderer.table(i);break;case`blockquote`:t+=this.renderer.blockquote(i);break;case`list`:t+=this.renderer.list(i);break;case`checkbox`:t+=this.renderer.checkbox(i);break;case`html`:t+=this.renderer.html(i);break;case`def`:t+=this.renderer.def(i);break;case`paragraph`:t+=this.renderer.paragraph(i);break;case`text`:t+=this.renderer.text(i);break;default:{let e=`Token with "`+i.type+`" type was not found.`;if(this.options.silent)return console.error(e),``;throw Error(e)}}}return t}parseInline(e,t=this.renderer){this.renderer.parser=this;let n=``;for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let e=this.options.extensions.renderers[i.type].call({parser:this},i);if(e!==!1||![`escape`,`html`,`link`,`image`,`strong`,`em`,`codespan`,`br`,`del`,`text`].includes(i.type)){n+=e||``;continue}}let a=i;switch(a.type){case`escape`:n+=t.text(a);break;case`html`:n+=t.html(a);break;case`link`:n+=t.link(a);break;case`image`:n+=t.image(a);break;case`checkbox`:n+=t.checkbox(a);break;case`strong`:n+=t.strong(a);break;case`em`:n+=t.em(a);break;case`codespan`:n+=t.codespan(a);break;case`br`:n+=t.br(a);break;case`del`:n+=t.del(a);break;case`text`:n+=t.text(a);break;default:{let e=`Token with "`+a.type+`" type was not found.`;if(this.options.silent)return console.error(e),``;throw Error(e)}}}return n}},ac=class{options;block;constructor(e){this.options=e||No}static passThroughHooks=new Set([`preprocess`,`postprocess`,`processAllTokens`,`emStrongMask`]);static passThroughHooksRespectAsync=new Set([`preprocess`,`postprocess`,`processAllTokens`]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}emStrongMask(e){return e}provideLexer(e=this.block){return e?tc.lex:tc.lexInline}provideParser(e=this.block){return e?ic.parse:ic.parseInline}},oc=new class{defaults=Mo();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=ic;Renderer=nc;TextRenderer=rc;Lexer=tc;Tokenizer=ec;Hooks=ac;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(let r of e)switch(n=n.concat(t.call(this,r)),r.type){case`table`:{let e=r;for(let r of e.header)n=n.concat(this.walkTokens(r.tokens,t));for(let r of e.rows)for(let e of r)n=n.concat(this.walkTokens(e.tokens,t));break}case`list`:{let e=r;n=n.concat(this.walkTokens(e.items,t));break}default:{let e=r;this.defaults.extensions?.childTokens?.[e.type]?this.defaults.extensions.childTokens[e.type].forEach(r=>{let i=e[r].flat(1/0);n=n.concat(this.walkTokens(i,t))}):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(e=>{let n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach(e=>{if(!e.name)throw Error(`extension name required`);if(`renderer`in e){let n=t.renderers[e.name];n?t.renderers[e.name]=function(...t){let r=e.renderer.apply(this,t);return r===!1&&(r=n.apply(this,t)),r}:t.renderers[e.name]=e.renderer}if(`tokenizer`in e){if(!e.level||e.level!==`block`&&e.lLine truncated
|
||||
Please report this to https://github.com/markedjs/marked.`,e){let e=`<p>An error occurred:</p><pre>`+Gs(n.message+``,!0)+`</pre>`;return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}};function $(e,t){return oc.parse(e,t)}$.options=$.setOptions=function(e){return oc.setOptions(e),$.defaults=oc.defaults,Po($.defaults),$},$.getDefaults=Mo,$.defaults=No,$.use=function(...e){return oc.use(...e),$.defaults=oc.defaults,Po($.defaults),$},$.walkTokens=function(e,t){return oc.walkTokens(e,t)},$.parseInline=oc.parseInline,$.Parser=ic,$.parser=ic.parse,$.Renderer=nc,$.TextRenderer=rc,$.Lexer=tc,$.lexer=tc.lex,$.Tokenizer=ec,$.Hooks=ac,$.parse=$,$.options,$.setOptions,$.use,$.walkTokens,$.parseInline,ic.parse,tc.lex;var sc={class:`besch`},cc={class:`raster`},lc=[`onClick`],uc={class:`kopf`},dc={class:`besch`},fc={key:0,class:`faehigkeiten`},pc={key:1,class:`mehr`},mc=fo({__name:`FeatureBaum`,props:{projekt:{type:Object,required:!0}},setup(e){let t=I(new Set);function n(e){t.value.has(e)?t.value.delete(e):t.value.add(e),t.value=new Set(t.value)}return(r,i)=>(W(!0),G(H,null,z(e.projekt.bereiche,e=>(W(),G(`section`,{key:e.name,class:`bereich`},[q(`h3`,null,k(e.name),1),q(`p`,sc,k(e.beschreibung),1),q(`div`,cc,[(W(!0),G(H,null,z(e.features,r=>(W(),G(`div`,{key:r.titel,class:O([`karte feature`,{offen:t.value.has(e.name+r.titel)}]),onClick:t=>n(e.name+r.titel)},[q(`div`,uc,[q(`strong`,null,k(r.titel),1),q(`span`,{class:O([`badge`,r.flag===`kern`?`gruen`:`gelb`])},k(r.flag),3)]),q(`p`,dc,k(r.beschreibung),1),t.value.has(e.name+r.titel)?(W(),G(`ul`,fc,[(W(!0),G(H,null,z(r.teilfeatures,e=>(W(),G(`li`,{key:e.titel},k(e.titel),1))),128))])):(W(),G(`span`,pc,k(r.teilfeatures.length)+` Fähigkeiten ▸`,1))],10,lc))),128))])]))),128))}},[[`__scopeId`,`data-v-f039a64a`]]),hc={key:0,class:`badge gelb`},gc={class:`inhalt`},_c={key:0,class:`fehler`},vc={class:`besch`},yc={class:`io`},bc={key:0,class:`entscheidungen`},xc={key:1,class:`kanten`},Sc={key:0},Cc={key:2},wc={key:3,class:`kein-code`},Tc={key:2,class:`laden`},Ec=fo({__name:`EinheitDetail`,props:{projektName:{type:String,required:!0},id:{type:String,required:!0},kurz:{type:String,default:``}},setup(e){let t=e,n=I(null),r=I(``);async function i(){if(!(n.value||r.value))try{n.value=await so(t.projektName,t.id)}catch(e){r.value=e.message}}return(t,a)=>(W(),G(`details`,{class:`einheit`,onToggle:a[0]||=e=>e.target.open&&i()},[q(`summary`,null,[q(`code`,null,k(e.id),1),n.value?.veraltet?(W(),G(`span`,hc,`veraltet`)):Y(``,!0)]),q(`div`,gc,[r.value?(W(),G(`p`,_c,k(r.value),1)):n.value?(W(),G(H,{key:1},[q(`p`,vc,[q(`em`,null,k(n.value.beschreibung),1)]),q(`dl`,yc,[a[1]||=q(`dt`,null,`Input`,-1),q(`dd`,null,k(n.value.input||`—`),1),a[2]||=q(`dt`,null,`Output`,-1),q(`dd`,null,k(n.value.output||`—`),1)]),n.value.entscheidungen.length?(W(),G(`ul`,bc,[(W(!0),G(H,null,z(n.value.entscheidungen,(e,t)=>(W(),G(`li`,{key:t},k(e),1))),128))])):Y(``,!0),n.value.kanten.length?(W(),G(`p`,xc,[(W(!0),G(H,null,z(n.value.kanten,(e,t)=>(W(),G(H,{key:t},[t?(W(),G(`span`,Sc,` · `)):Y(``,!0),Pi(k(e.typ)+` → `,1),q(`code`,null,k(e.ziel),1)],64))),128))])):Y(``,!0),n.value.code?(W(),G(`pre`,Cc,k(n.value.code),1)):(W(),G(`p`,wc,`Anker nicht im aktuellen Code gefunden.`))],64)):(W(),G(`p`,Tc,`lädt…`))])],32))}},[[`__scopeId`,`data-v-58d66282`]]),Dc={class:`besch`},Oc={key:0,class:`beziehungen`},kc={key:0},Ac={key:0,class:`leer`},jc=fo({__name:`ArchitekturTab`,props:{projekt:{type:Object,required:!0}},setup(e){return(t,n)=>(W(),G(H,null,[(W(!0),G(H,null,z(e.projekt.architektur,t=>(W(),G(`section`,{key:t.name,class:`komponente`},[q(`h3`,null,k(t.name),1),q(`p`,Dc,k(t.beschreibung),1),t.beziehungen.length?(W(),G(`p`,Oc,[(W(!0),G(H,null,z(t.beziehungen,(e,t)=>(W(),G(H,{key:t},[t?(W(),G(`span`,kc,` · `)):Y(``,!0),Pi(k(e.typ)+` → `+k(e.ziel),1)],64))),128))])):Y(``,!0),q(`details`,null,[q(`summary`,null,k(t.einheiten.length)+` Einheiten`,1),(W(!0),G(H,null,z(t.einheiten,t=>(W(),K(Ec,{key:t,"projekt-name":e.projekt.name,id:t},null,8,[`projekt-name`,`id`]))),128))])]))),128)),e.projekt.architektur.length?Y(``,!0):(W(),G(`p`,Ac,`Keine Architektur-Sicht vorhanden.`))],64))}},[[`__scopeId`,`data-v-dc3c6459`]]),Mc={key:0,class:`refs`},Nc={key:0,class:`leer`},Pc=fo({__name:`FlowsTab`,props:{projekt:{type:Object,required:!0}},setup(e){return(t,n)=>(W(),G(H,null,[(W(!0),G(H,null,z(e.projekt.flows,t=>(W(),G(`section`,{key:t.titel,class:`flow`},[q(`h3`,null,k(t.titel),1),q(`ol`,null,[(W(!0),G(H,null,z(t.schritte,(t,n)=>(W(),G(`li`,{key:n},[Pi(k(t.text)+` `,1),t.einheiten.length?(W(),G(`details`,Mc,[q(`summary`,null,[(W(!0),G(H,null,z(t.einheiten,(e,t)=>(W(),G(`code`,{key:e},k(t?`, `:``)+k(e),1))),128))]),(W(!0),G(H,null,z(t.einheiten,t=>(W(),K(Ec,{key:t,"projekt-name":e.projekt.name,id:t},null,8,[`projekt-name`,`id`]))),128))])):Y(``,!0)]))),128))])]))),128)),e.projekt.flows.length?Y(``,!0):(W(),G(`p`,Nc,`Keine Flow-Sicht vorhanden.`))],64))}},[[`__scopeId`,`data-v-9b2e52a0`]]),Fc={class:`pruefung`},I
Line truncated
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Planer</title>
|
||||
<script>
|
||||
(function () {
|
||||
var stored = localStorage.getItem('darkMode')
|
||||
var dark = stored === null
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: stored === 'true'
|
||||
if (dark) document.documentElement.classList.add('dark')
|
||||
})()
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DIiv29cS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-AxkPXHNh.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Planer</title>
|
||||
<script>
|
||||
(function () {
|
||||
var stored = localStorage.getItem('darkMode')
|
||||
var dark = stored === null
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: stored === 'true'
|
||||
if (dark) document.documentElement.classList.add('dark')
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+1
@@ -0,0 +1 @@
|
||||
../marked/bin/marked.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../nanoid/bin/nanoid.cjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../@babel/parser/bin/babel-parser.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../rolldown/bin/cli.mjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../vite/bin/vite.js
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
{
|
||||
"name": "planer-frontend",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.139.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
||||
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
||||
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
|
||||
"integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
|
||||
"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/shared": "3.5.39",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
|
||||
"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
|
||||
"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/compiler-core": "3.5.39",
|
||||
"@vue/compiler-dom": "3.5.39",
|
||||
"@vue/compiler-ssr": "3.5.39",
|
||||
"@vue/shared": "3.5.39",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.15",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
|
||||
"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
|
||||
"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
|
||||
"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
|
||||
"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.39",
|
||||
"@vue/runtime-core": "3.5.39",
|
||||
"@vue/shared": "3.5.39",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
|
||||
"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.39"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
|
||||
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "18.0.5",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz",
|
||||
"integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.139.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-arm64": "1.1.5",
|
||||
"@rolldown/binding-darwin-x64": "1.1.5",
|
||||
"@rolldown/binding-freebsd-x64": "1.1.5",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
||||
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
||||
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
||||
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.16",
|
||||
"rolldown": "~1.1.4",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.3.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.39",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
|
||||
"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.39",
|
||||
"@vue/compiler-sfc": "3.5.39",
|
||||
"@vue/runtime-dom": "3.5.39",
|
||||
"@vue/server-renderer": "3.5.39",
|
||||
"@vue/shared": "3.5.39"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"hash": "56f8b5bc",
|
||||
"configHash": "ad91a1a3",
|
||||
"lockfileHash": "85ee1cb2",
|
||||
"browserHash": "8797f163",
|
||||
"optimized": {
|
||||
"marked": {
|
||||
"src": "../../marked/lib/marked.esm.js",
|
||||
"file": "marked.js",
|
||||
"fileHash": "2600b6a1",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vue": {
|
||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "298c1225",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {}
|
||||
}
|
||||
+1610
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"marked.js","names":[],"sources":["../../marked/lib/marked.esm.js"],"sourcesContent":["/**\n * marked v18.0.5 - a markdown parser\n * Copyright (c) 2018-2026, MarkedJS. (MIT License)\n * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License)\n * https://github.com/markedjs/marked\n */\n\n/**\n * DO NOT EDIT THIS FILE\n * The code in this file is generated from files in ./src/\n */\n\nfunction M(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var T=M();function N(l){T=l}var _={exec:()=>null};function E(l){let e=[];return t=>{let n=Math.max(0,Math.min(3,t-1)),s=e[n];return s||(s=l(n),e[n]=s),s}}function d(l,e=\"\"){let t=typeof l==\"string\"?l:l.source,n={replace:(s,r)=>{let i=typeof r==\"string\"?r:r.source;return i=i.replace(m.caret,\"$1\"),t=t.replace(s,i),n},getRegex:()=>new RegExp(t,e)};return n}var Te=((l=\"\")=>{try{return!!new RegExp(\"(?<=1)(?<!1)\"+l)}catch{return!1}})(),m={codeRemoveIndent:/^(?: {1,4}| {0,3}\\t)/gm,outputLinkReplace:/\\\\([\\[\\]])/g,indentCodeCompensation:/^(\\s+)(?:```)/,beginningSpace:/^\\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\\n/g,tabCharGlobal:/\\t/g,multipleSpaceGlobal:/\\s+/g,blankLine:/^[ \\t]*$/,doubleBlankLine:/\\n[ \\t]*\\n[ \\t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\\n {0,3}((?:=+|-+) *)(?=\\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \\t]?/gm,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\\[[ xX]\\] +\\S/,listReplaceTask:/^\\[[ xX]\\] +/,listTaskCheckbox:/\\[[ xX]\\]/,anyLine:/\\n.*\\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\\||\\| *$/g,tableRowBlankLine:/\\n[ \\t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\\s|>)/i,endPreScriptTag:/^<\\/(pre|code|kbd|script)(\\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'\"]*[^\\s])\\s+(['\"])(.*)\\2/,unicodeAlphaNumeric:/[\\p{L}\\p{N}]/u,escapeTest:/[&<>\"']/,escapeReplace:/[&<>\"']/g,escapeTestNoEncode:/[<>\"']|&(?!(#\\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\\w+);)/,escapeReplaceNoEncode:/[<>\"']|&(?!(#\\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\\w+);)/g,caret:/(^|[^\\[])\\^/g,percentDecode:/%25/g,findPipe:/\\|/g,splitPipe:/ \\|/,slashPipe:/\\\\\\|/g,carriageReturn:/\\r\\n|\\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\\S*/,endingNewline:/\\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[\t ][^\\\\n]*)?(?:\\\\n|$))`),nextBulletRegex:E(l=>new RegExp(`^ {0,${l}}(?:[*+-]|\\\\d{1,9}[.)])((?:[ \t][^\\\\n]*)?(?:\\\\n|$))`)),hrRegex:E(l=>new RegExp(`^ {0,${l}}((?:- *){3,}|(?:_ *){3,}|(?:\\\\* *){3,})(?:\\\\n+|$)`)),fencesBeginRegex:E(l=>new RegExp(`^ {0,${l}}(?:\\`\\`\\`|~~~)`)),headingBeginRegex:E(l=>new RegExp(`^ {0,${l}}#`)),htmlBeginRegex:E(l=>new RegExp(`^ {0,${l}}<(?:[a-z].*>|!--)`,\"i\")),blockquoteBeginRegex:E(l=>new RegExp(`^ {0,${l}}>`))},Oe=/^(?:[ \\t]*(?:\\n|$))+/,we=/^((?: {4}| {0,3}\\t)[^\\n]+(?:\\n(?:[ \\t]*(?:\\n|$))*)?)+/,ye=/^ {0,3}(`{3,}(?=[^`\\n]*(?:\\n|$))|~{3,})([^\\n]*)(?:\\n|$)(?:|([\\s\\S]*?)(?:\\n|$))(?: {0,3}\\1[~`]* *(?=\\n|$)|$)/,B=/^ {0,3}((?:-[\\t ]*){3,}|(?:_[ \\t]*){3,}|(?:\\*[ \\t]*){3,})(?:\\n+|$)/,Pe=/^ {0,3}(#{1,6})(?=\\s|$)(.*)(?:\\n+|$)/,j=/ {0,3}(?:[*+-]|\\d{1,9}[.)])/,oe=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\\n(?!\\s*?\\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\\n {0,3}(=+|-+) *(?:\\n+|$)/,ae=d(oe).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\\n>]+>\\n/).replace(/\\|table/g,\"\").getRegex(),Se=d(oe).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\\n>]+>\\n/).replace(/table/g,/ {0,3}\\|?(?:[:\\- ]*\\|)+[\\:\\- ]*\\n/).getRegex(),F=/^([^\\n]+(?:\\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\\n)[^\\n]+)*)/,$e=/^[^\\n]+/,U=/(?!\\s*\\])(?:\\\\[\\s\\S]|[^\\[\\]\\\\])+/,Le=d(/^ {0,3}\\[(label)\\]: *(?:\\n[ \\t]*)?([^<\\s][^\\s]*|<.*?>)(?:(?: +(?:\\n[ \\t]*)?| *\\n[ \\t]*)(title))? *(?:\\n+|$)/).replace(\"label\",U).replace(\"title\",/(?:\"(?:\\\\\"?|[^\"\\\\])*\"|'[^'\\n]*(?:\\n[^'\\n]+)*\\n?'|\\([^()]*\\))/).getRegex(),_e=d(/^(bull)([ \\t][^\\n]*?)?(?:\\n|$)/).replace(/bull/g,j).getRegex(),H=\"address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul\",K=/<!--(?:-?>|[\\s\\S]*?(?:-->|$))/,ze=d(\"^ {0,3}(?:<Line truncated
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
+8654
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"vue.js","names":["hasOwnProperty","isRef","warn","computed","watch","enterCbKey","callHook","compile","camelize$1"],"sources":["../../@vue/shared/dist/shared.esm-bundler.js","../../@vue/reactivity/dist/reactivity.esm-bundler.js","../../@vue/runtime-core/dist/runtime-core.esm-bundler.js","../../@vue/runtime-dom/dist/runtime-dom.esm-bundler.js","../../vue/dist/vue.runtime.esm-bundler.js"],"sourcesContent":["/**\n* @vue/shared v3.5.39\n* (c) 2018-present Yuxi (Evan) You and Vue contributors\n* @license MIT\n**/\n// @__NO_SIDE_EFFECTS__\nfunction makeMap(str) {\n const map = /* @__PURE__ */ Object.create(null);\n for (const key of str.split(\",\")) map[key] = 1;\n return (val) => val in map;\n}\n\nconst EMPTY_OBJ = !!(process.env.NODE_ENV !== \"production\") ? Object.freeze({}) : {};\nconst EMPTY_ARR = !!(process.env.NODE_ENV !== \"production\") ? Object.freeze([]) : [];\nconst NOOP = () => {\n};\nconst NO = () => false;\nconst isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter\n(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97);\nconst isModelListener = (key) => key.startsWith(\"onUpdate:\");\nconst extend = Object.assign;\nconst remove = (arr, el) => {\n const i = arr.indexOf(el);\n if (i > -1) {\n arr.splice(i, 1);\n }\n};\nconst hasOwnProperty = Object.prototype.hasOwnProperty;\nconst hasOwn = (val, key) => hasOwnProperty.call(val, key);\nconst isArray = Array.isArray;\nconst isMap = (val) => toTypeString(val) === \"[object Map]\";\nconst isSet = (val) => toTypeString(val) === \"[object Set]\";\nconst isDate = (val) => toTypeString(val) === \"[object Date]\";\nconst isRegExp = (val) => toTypeString(val) === \"[object RegExp]\";\nconst isFunction = (val) => typeof val === \"function\";\nconst isString = (val) => typeof val === \"string\";\nconst isSymbol = (val) => typeof val === \"symbol\";\nconst isObject = (val) => val !== null && typeof val === \"object\";\nconst isPromise = (val) => {\n return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch);\n};\nconst objectToString = Object.prototype.toString;\nconst toTypeString = (value) => objectToString.call(value);\nconst toRawType = (value) => {\n return toTypeString(value).slice(8, -1);\n};\nconst isPlainObject = (val) => toTypeString(val) === \"[object Object]\";\nconst isIntegerKey = (key) => isString(key) && key !== \"NaN\" && key[0] !== \"-\" && \"\" + parseInt(key, 10) === key;\nconst isReservedProp = /* @__PURE__ */ makeMap(\n // the leading comma is intentional so empty string \"\" is also included\n \",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted\"\n);\nconst isBuiltInDirective = /* @__PURE__ */ makeMap(\n \"bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo\"\n);\nconst cacheStringFunction = (fn) => {\n const cache = /* @__PURE__ */ Object.create(null);\n return ((str) => {\n const hit = cache[str];\n return hit || (cache[str] = fn(str));\n });\n};\nconst camelizeRE = /-\\w/g;\nconst camelize = cacheStringFunction(\n (str) => {\n return str.replace(camelizeRE, (c) => c.slice(1).toUpperCase());\n }\n);\nconst hyphenateRE = /\\B([A-Z])/g;\nconst hyphenate = cacheStringFunction(\n (str) => str.replace(hyphenateRE, \"-$1\").toLowerCase()\n);\nconst capitalize = cacheStringFunction((str) => {\n return str.charAt(0).toUpperCase() + str.slice(1);\n});\nconst toHandlerKey = cacheStringFunction(\n (str) => {\n const s = str ? `on${capitalize(str)}` : ``;\n return s;\n }\n);\nconst hasChanged = (value, oldValue) => !Object.is(value, oldValue);\nconst invokeArrayFns = (fns, ...arg) => {\n for (let i = 0; i < fns.length; i++) {\n fns[i](...arg);\n }\n};\nconst def = (obj, key, value, writable = false) => {\n Object.defineProperty(obj, key, {\n configurable: true,\n enumerable: false,\n writable,\n value\n });\n};\nconst looseToNumber = (val) => {\n const n = parseFloat(val);\n return isNaN(n) ? val : n;\n};\nconst toNumber = (val) => {\n const n = isString(val) ? Number(val) : NaN;\n return isNaN(n) ? val : n;\n};\nlet _globalThis;\nconst getGlobalThis = () => {\n return _globalThis || (_globalThis = typeof globalThis !== \"undefined\" ? globalThis : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : typeof global !== \"undefined\" ? global : {});\n};\nconst identRE = /^[_$a-zA-Z\\xA0-\\uFFFF][_$a-zA-Z0-9\\xA0-\\uFFFF]*$/;\nfunction genPropsAccessExp(name) {\n return identRE.test(name) ? `__props.${name}` : `__props[${JSON.stringify(name)}]`;\n}\nfunction genCacheKey(source, options) {\n return source + JSON.stringify(\n options,\n (_, val) => typeof val === \"function\" ? val.toString() : val\n );\n}\n\nconst PatchFlags = {\n \"TEXT\": 1,\n \"1\": \"TEXT\",\n \"CLASS\": 2,\n \"2\": \"CLASS\",\n \"STYLE\": 4,\n \"4\": \"STYLE\",\n \"PROPS\": 8,\n \"Line truncated
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# @babel/helper-string-parser
|
||||
|
||||
> A utility package to parse strings
|
||||
|
||||
See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save @babel/helper-string-parser
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/helper-string-parser
|
||||
```
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.readCodePoint = readCodePoint;
|
||||
exports.readInt = readInt;
|
||||
exports.readStringContents = readStringContents;
|
||||
var _isDigit = function isDigit(code) {
|
||||
return code >= 48 && code <= 57;
|
||||
};
|
||||
const forbiddenNumericSeparatorSiblings = {
|
||||
decBinOct: new Set([46, 66, 69, 79, 95, 98, 101, 111]),
|
||||
hex: new Set([46, 88, 95, 120])
|
||||
};
|
||||
const isAllowedNumericSeparatorSibling = {
|
||||
bin: ch => ch === 48 || ch === 49,
|
||||
oct: ch => ch >= 48 && ch <= 55,
|
||||
dec: ch => ch >= 48 && ch <= 57,
|
||||
hex: ch => ch >= 48 && ch <= 57 || ch >= 65 && ch <= 70 || ch >= 97 && ch <= 102
|
||||
};
|
||||
function readStringContents(type, input, pos, lineStart, curLine, errors) {
|
||||
const initialPos = pos;
|
||||
const initialLineStart = lineStart;
|
||||
const initialCurLine = curLine;
|
||||
let out = "";
|
||||
let firstInvalidLoc = null;
|
||||
let chunkStart = pos;
|
||||
const {
|
||||
length
|
||||
} = input;
|
||||
for (;;) {
|
||||
if (pos >= length) {
|
||||
errors.unterminated(initialPos, initialLineStart, initialCurLine);
|
||||
out += input.slice(chunkStart, pos);
|
||||
break;
|
||||
}
|
||||
const ch = input.charCodeAt(pos);
|
||||
if (isStringEnd(type, ch, input, pos)) {
|
||||
out += input.slice(chunkStart, pos);
|
||||
break;
|
||||
}
|
||||
if (ch === 92) {
|
||||
out += input.slice(chunkStart, pos);
|
||||
const res = readEscapedChar(input, pos, lineStart, curLine, type === "template", errors);
|
||||
if (res.ch === null && !firstInvalidLoc) {
|
||||
firstInvalidLoc = {
|
||||
pos,
|
||||
lineStart,
|
||||
curLine
|
||||
};
|
||||
} else {
|
||||
out += res.ch;
|
||||
}
|
||||
({
|
||||
pos,
|
||||
lineStart,
|
||||
curLine
|
||||
} = res);
|
||||
chunkStart = pos;
|
||||
} else if (ch === 8232 || ch === 8233) {
|
||||
++pos;
|
||||
++curLine;
|
||||
lineStart = pos;
|
||||
} else if (ch === 10 || ch === 13) {
|
||||
if (type === "template") {
|
||||
out += input.slice(chunkStart, pos) + "\n";
|
||||
++pos;
|
||||
if (ch === 13 && input.charCodeAt(pos) === 10) {
|
||||
++pos;
|
||||
}
|
||||
++curLine;
|
||||
chunkStart = lineStart = pos;
|
||||
} else {
|
||||
errors.unterminated(initialPos, initialLineStart, initialCurLine);
|
||||
}
|
||||
} else {
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
return {
|
||||
pos,
|
||||
str: out,
|
||||
firstInvalidLoc,
|
||||
lineStart,
|
||||
curLine,
|
||||
containsInvalid: !!firstInvalidLoc
|
||||
};
|
||||
}
|
||||
function isStringEnd(type, ch, input, pos) {
|
||||
if (type === "template") {
|
||||
return ch === 96 || ch === 36 && input.charCodeAt(pos + 1) === 123;
|
||||
}
|
||||
return ch === (type === "double" ? 34 : 39);
|
||||
}
|
||||
function readEscapedChar(input, pos, lineStart, curLine, inTemplate, errors) {
|
||||
const throwOnInvalid = !inTemplate;
|
||||
pos++;
|
||||
const res = ch => ({
|
||||
pos,
|
||||
ch,
|
||||
lineStart,
|
||||
curLine
|
||||
});
|
||||
const ch = input.charCodeAt(pos++);
|
||||
switch (ch) {
|
||||
case 110:
|
||||
return res("\n");
|
||||
case 114:
|
||||
return res("\r");
|
||||
case 120:
|
||||
{
|
||||
let code;
|
||||
({
|
||||
code,
|
||||
pos
|
||||
} = readHexChar(input, pos, lineStart, curLine, 2, false, throwOnInvalid, errors));
|
||||
return res(code === null ? null : String.fromCharCode(code));
|
||||
}
|
||||
case 117:
|
||||
{
|
||||
let code;
|
||||
({
|
||||
code,
|
||||
pos
|
||||
} = readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors));
|
||||
return res(code === null ? null : String.fromCodePoint(code));
|
||||
}
|
||||
case 116:
|
||||
return res("\t");
|
||||
case 98:
|
||||
return res("\b");
|
||||
case 118:
|
||||
return res("\u000b");
|
||||
case 102:
|
||||
return res("\f");
|
||||
case 13:
|
||||
if (input.charCodeAt(pos) === 10) {
|
||||
++pos;
|
||||
}
|
||||
case 10:
|
||||
lineStart = pos;
|
||||
++curLine;
|
||||
case 8232:
|
||||
case 8233:
|
||||
return res("");
|
||||
case 56:
|
||||
case 57:
|
||||
if (inTemplate) {
|
||||
return res(null);
|
||||
} else {
|
||||
errors.strictNumericEscape(pos - 1, lineStart, curLine);
|
||||
}
|
||||
default:
|
||||
if (ch >= 48 && ch <= 55) {
|
||||
const startPos = pos - 1;
|
||||
const match = /^[0-7]+/.exec(input.slice(startPos, pos + 2));
|
||||
let octalStr = match[0];
|
||||
let octal = parseInt(octalStr, 8);
|
||||
if (octal > 255) {
|
||||
octalStr = octalStr.slice(0, -1);
|
||||
octal = parseInt(octalStr, 8);
|
||||
}
|
||||
pos += octalStr.length - 1;
|
||||
const next = input.charCodeAt(pos);
|
||||
if (octalStr !== "0" || next === 56 || next === 57) {
|
||||
if (inTemplate) {
|
||||
return res(null);
|
||||
} else {
|
||||
errors.strictNumericEscape(startPos, lineStart, curLine);
|
||||
}
|
||||
}
|
||||
return res(String.fromCharCode(octal));
|
||||
}
|
||||
return res(String.fromCharCode(ch));
|
||||
}
|
||||
}
|
||||
function readHexChar(input, pos, lineStart, curLine, len, forceLen, throwOnInvalid, errors) {
|
||||
const initialPos = pos;
|
||||
let n;
|
||||
({
|
||||
n,
|
||||
pos
|
||||
} = readInt(input, pos, lineStart, curLine, 16, len, forceLen, false, errors, !throwOnInvalid));
|
||||
if (n === null) {
|
||||
if (throwOnInvalid) {
|
||||
errors.invalidEscapeSequence(initialPos, lineStart, curLine);
|
||||
} else {
|
||||
pos = initialPos - 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: n,
|
||||
pos
|
||||
};
|
||||
}
|
||||
function readInt(input, pos, lineStart, curLine, radix, len, forceLen, allowNumSeparator, errors, bailOnError) {
|
||||
const start = pos;
|
||||
const forbiddenSiblings = radix === 16 ? forbiddenNumericSeparatorSiblings.hex : forbiddenNumericSeparatorSiblings.decBinOct;
|
||||
const isAllowedSibling = radix === 16 ? isAllowedNumericSeparatorSibling.hex : radix === 10 ? isAllowedNumericSeparatorSibling.dec : radix === 8 ? isAllowedNumericSeparatorSibling.oct : isAllowedNumericSeparatorSibling.bin;
|
||||
let invalid = false;
|
||||
let total = 0;
|
||||
for (let i = 0, e = len == null ? Infinity : len; i < e; ++i) {
|
||||
const code = input.charCodeAt(pos);
|
||||
let val;
|
||||
if (code === 95 && allowNumSeparator !== "bail") {
|
||||
const prev = input.charCodeAt(pos - 1);
|
||||
const next = input.charCodeAt(pos + 1);
|
||||
if (!allowNumSeparator) {
|
||||
if (bailOnError) return {
|
||||
n: null,
|
||||
pos
|
||||
};
|
||||
errors.numericSeparatorInEscapeSequence(pos, lineStart, curLine);
|
||||
} else if (Number.isNaN(next) || !isAllowedSibling(next) || forbiddenSiblings.has(prev) || forbiddenSiblings.has(next)) {
|
||||
if (bailOnError) return {
|
||||
n: null,
|
||||
pos
|
||||
};
|
||||
errors.unexpectedNumericSeparator(pos, lineStart, curLine);
|
||||
}
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (code >= 97) {
|
||||
val = code - 97 + 10;
|
||||
} else if (code >= 65) {
|
||||
val = code - 65 + 10;
|
||||
} else if (_isDigit(code)) {
|
||||
val = code - 48;
|
||||
} else {
|
||||
val = Infinity;
|
||||
}
|
||||
if (val >= radix) {
|
||||
if (val <= 9 && bailOnError) {
|
||||
return {
|
||||
n: null,
|
||||
pos
|
||||
};
|
||||
} else if (val <= 9 && errors.invalidDigit(pos, lineStart, curLine, radix)) {
|
||||
val = 0;
|
||||
} else if (forceLen) {
|
||||
val = 0;
|
||||
invalid = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
++pos;
|
||||
total = total * radix + val;
|
||||
}
|
||||
if (pos === start || len != null && pos - start !== len || invalid) {
|
||||
return {
|
||||
n: null,
|
||||
pos
|
||||
};
|
||||
}
|
||||
return {
|
||||
n: total,
|
||||
pos
|
||||
};
|
||||
}
|
||||
function readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors) {
|
||||
const ch = input.charCodeAt(pos);
|
||||
let code;
|
||||
if (ch === 123) {
|
||||
++pos;
|
||||
({
|
||||
code,
|
||||
pos
|
||||
} = readHexChar(input, pos, lineStart, curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, errors));
|
||||
++pos;
|
||||
if (code !== null && code > 0x10ffff) {
|
||||
if (throwOnInvalid) {
|
||||
errors.invalidCodePoint(pos, lineStart, curLine);
|
||||
} else {
|
||||
return {
|
||||
code: null,
|
||||
pos
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
({
|
||||
code,
|
||||
pos
|
||||
} = readHexChar(input, pos, lineStart, curLine, 4, false, throwOnInvalid, errors));
|
||||
}
|
||||
return {
|
||||
code,
|
||||
pos
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["isDigit","code","forbiddenNumericSeparatorSiblings","decBinOct","Set","hex","isAllowedNumericSeparatorSibling","bin","ch","oct","dec","readStringContents","type","input","pos","lineStart","curLine","errors","initialPos","initialLineStart","initialCurLine","out","firstInvalidLoc","chunkStart","length","unterminated","slice","charCodeAt","isStringEnd","res","readEscapedChar","str","containsInvalid","inTemplate","throwOnInvalid","readHexChar","String","fromCharCode","readCodePoint","fromCodePoint","strictNumericEscape","startPos","match","exec","octalStr","octal","parseInt","next","len","forceLen","n","readInt","invalidEscapeSequence","radix","allowNumSeparator","bailOnError","start","forbiddenSiblings","isAllowedSibling","invalid","total","i","e","Infinity","val","prev","numericSeparatorInEscapeSequence","Number","isNaN","has","unexpectedNumericSeparator","_isDigit","invalidDigit","indexOf","invalidCodePoint"],"sources":["../src/index.ts"],"sourcesContent":["// We inline this package\n// eslint-disable-next-line import/no-extraneous-dependencies\nimport * as charCodes from \"charcodes\";\n\n// The following character codes are forbidden from being\n// an immediate sibling of NumericLiteralSeparator _\nconst forbiddenNumericSeparatorSiblings = {\n decBinOct: new Set<number>([\n charCodes.dot,\n charCodes.uppercaseB,\n charCodes.uppercaseE,\n charCodes.uppercaseO,\n charCodes.underscore, // multiple separators are not allowed\n charCodes.lowercaseB,\n charCodes.lowercaseE,\n charCodes.lowercaseO,\n ]),\n hex: new Set<number>([\n charCodes.dot,\n charCodes.uppercaseX,\n charCodes.underscore, // multiple separators are not allowed\n charCodes.lowercaseX,\n ]),\n};\n\nconst isAllowedNumericSeparatorSibling = {\n // 0 - 1\n bin: (ch: number) => ch === charCodes.digit0 || ch === charCodes.digit1,\n\n // 0 - 7\n oct: (ch: number) => ch >= charCodes.digit0 && ch <= charCodes.digit7,\n\n // 0 - 9\n dec: (ch: number) => ch >= charCodes.digit0 && ch <= charCodes.digit9,\n\n // 0 - 9, A - F, a - f,\n hex: (ch: number) =>\n (ch >= charCodes.digit0 && ch <= charCodes.digit9) ||\n (ch >= charCodes.uppercaseA && ch <= charCodes.uppercaseF) ||\n (ch >= charCodes.lowercaseA && ch <= charCodes.lowercaseF),\n};\n\nexport type StringContentsErrorHandlers = EscapedCharErrorHandlers & {\n unterminated(\n initialPos: number,\n initialLineStart: number,\n initialCurLine: number,\n ): void;\n};\n\nexport function readStringContents(\n type: \"single\" | \"double\" | \"template\",\n input: string,\n pos: number,\n lineStart: number,\n curLine: number,\n errors: StringContentsErrorHandlers,\n) {\n const initialPos = pos;\n const initialLineStart = lineStart;\n const initialCurLine = curLine;\n\n let out = \"\";\n let firstInvalidLoc = null;\n let chunkStart = pos;\n const { length } = input;\n for (;;) {\n if (pos >= length) {\n errors.unterminated(initialPos, initialLineStart, initialCurLine);\n out += input.slice(chunkStart, pos);\n break;\n }\n const ch = input.charCodeAt(pos);\n if (isStringEnd(type, ch, input, pos)) {\n out += input.slice(chunkStart, pos);\n break;\n }\n if (ch === charCodes.backslash) {\n out += input.slice(chunkStart, pos);\n const res = readEscapedChar(\n input,\n pos,\n lineStart,\n curLine,\n type === \"template\",\n errors,\n );\n if (res.ch === null && !firstInvalidLoc) {\n firstInvalidLoc = { pos, lineStart, curLine };\n } else {\n out += res.ch;\n }\n ({ pos, lineStart, curLine } = res);\n chunkStart = pos;\n } else if (\n ch === charCodes.lineSeparator ||\n ch === charCodes.paragraphSeparator\n ) {\n ++pos;\n ++curLine;\n lineStart = pos;\n } else if (ch === charCodes.lineFeed || ch === charCodes.carriageReturn) {\n if (type === \"template\") {\n out += input.slice(chunkStart, pos) + \"\\n\";\n ++pos;\n if (\n ch === charCodes.carriageReturn &&\n input.charCodeAt(pos) === charCodes.lineFeed\n ) {\n ++pos;\n }\n ++curLine;\n chunkStart = lineStart = pos;\n } else {\n errors.unterminated(initialPos, initialLineStart, initialCurLine);\n }\n } else {\n ++pos;\n }\n }\n return process.env.BABEL_8_BREAKING\n ? { pos, str: out, firstInvalidLoc, lineStart, curLine }\n : {\n pos,\n str: out,\n firstInvalidLoc,\n lineStart,\n curLine,\n containsInvalid: !!firstInvalidLoc,\n };\n}\n\nfunction isStringEnd(\n type: \"single\" | \"double\" | \"template\",\n ch: number,\n input: string,\n pos: number,\n) {\n if (type === \"template\") {\n return (\n ch === charCodes.graveAccent ||\n (ch === charCodes.dollarSign &&\n input.charCodeAt(pos + 1) === charCodes.leftCuLine truncated
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@babel/helper-string-parser",
|
||||
"version": "7.29.7",
|
||||
"description": "A utility package to parse strings",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/babel/babel.git",
|
||||
"directory": "packages/babel-helper-string-parser"
|
||||
},
|
||||
"homepage": "https://babel.dev/docs/en/next/babel-helper-string-parser",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"devDependencies": {
|
||||
"charcodes": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"author": "The Babel Team (https://babel.dev/team)",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"type": "commonjs"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# @babel/helper-validator-identifier
|
||||
|
||||
> Validate identifier/keywords name
|
||||
|
||||
See our website [@babel/helper-validator-identifier](https://babeljs.io/docs/babel-helper-validator-identifier) for more information.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save @babel/helper-validator-identifier
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/helper-validator-identifier
|
||||
```
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.isIdentifierChar = isIdentifierChar;
|
||||
exports.isIdentifierName = isIdentifierName;
|
||||
exports.isIdentifierStart = isIdentifierStart;
|
||||
let nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u0870-\u0887\u0889-\u088f\u08a0-\u08c9\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c5c\u0c5d\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cdc-\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u1711\u171f-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4c\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c8a\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7dc\ua7f1-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc";
|
||||
let nonASCIIidentifierChars = "\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0897-\u089f\u08ca-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c04\u0c3c\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0cf3\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ece\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u180f-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf-\u1add\u1ae0-\u1aeb\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\u30fb\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f\uff65";
|
||||
const nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
|
||||
const nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
|
||||
nonASCIIidentifierStartChars = nonASCIIidentifierChars = null;
|
||||
const astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 7, 25, 39, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 5, 57, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 24, 43, 261, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 33, 24, 3, 24, 45, 74, 6, 0, 67, 12, 65, 1, 2, 0, 15, 4, 10, 7381, 42, 31, 98, 114, 8702, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 208, 30, 2, 2, 2, 1, 2, 6, 3, 4, 10, 1, 225, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4381, 3, 5773, 3, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 8489];
|
||||
const astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 78, 5, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 199, 7, 137, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 55, 9, 266, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 233, 0, 3, 0, 8, 1, 6, 0, 475, 6, 110, 6, 6, 9, 4759, 9, 787719, 239];
|
||||
function isInAstralSet(code, set) {
|
||||
let pos = 0x10000;
|
||||
for (let i = 0, length = set.length; i < length; i += 2) {
|
||||
pos += set[i];
|
||||
if (pos > code) return false;
|
||||
pos += set[i + 1];
|
||||
if (pos >= code) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isIdentifierStart(code) {
|
||||
if (code < 65) return code === 36;
|
||||
if (code <= 90) return true;
|
||||
if (code < 97) return code === 95;
|
||||
if (code <= 122) return true;
|
||||
if (code <= 0xffff) {
|
||||
return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code));
|
||||
}
|
||||
return isInAstralSet(code, astralIdentifierStartCodes);
|
||||
}
|
||||
function isIdentifierChar(code) {
|
||||
if (code < 48) return code === 36;
|
||||
if (code < 58) return true;
|
||||
if (code < 65) return false;
|
||||
if (code <= 90) return true;
|
||||
if (code < 97) return code === 95;
|
||||
if (code <= 122) return true;
|
||||
if (code <= 0xffff) {
|
||||
return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code));
|
||||
}
|
||||
return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes);
|
||||
}
|
||||
function isIdentifierName(name) {
|
||||
let isFirst = true;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
let cp = name.charCodeAt(i);
|
||||
if ((cp & 0xfc00) === 0xd800 && i + 1 < name.length) {
|
||||
const trail = name.charCodeAt(++i);
|
||||
if ((trail & 0xfc00) === 0xdc00) {
|
||||
cp = 0x10000 + ((cp & 0x3ff) << 10) + (trail & 0x3ff);
|
||||
}
|
||||
}
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
if (!isIdentifierStart(cp)) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isIdentifierChar(cp)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !isFirst;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=identifier.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["nonASCIIidentifierStartChars","nonASCIIidentifierChars","nonASCIIidentifierStart","RegExp","nonASCIIidentifier","astralIdentifierStartCodes","astralIdentifierCodes","isInAstralSet","code","set","pos","i","length","isIdentifierStart","test","String","fromCharCode","isIdentifierChar","isIdentifierName","name","isFirst","cp","charCodeAt","trail"],"sources":["../src/identifier.ts"],"sourcesContent":["// We inline this package\n// eslint-disable-next-line import/no-extraneous-dependencies\nimport * as charCodes from \"charcodes\";\n\n// ## Character categories\n\n// Big ugly regular expressions that match characters in the\n// whitespace, identifier, and identifier-start categories. These\n// are only applied when a character is found to actually have a\n// code point between 0x80 and 0xffff.\n// Generated by `scripts/generate-identifier-regex.cjs`.\n\n/* prettier-ignore */\nlet nonASCIIidentifierStartChars = \"\\xaa\\xb5\\xba\\xc0-\\xd6\\xd8-\\xf6\\xf8-\\u02c1\\u02c6-\\u02d1\\u02e0-\\u02e4\\u02ec\\u02ee\\u0370-\\u0374\\u0376\\u0377\\u037a-\\u037d\\u037f\\u0386\\u0388-\\u038a\\u038c\\u038e-\\u03a1\\u03a3-\\u03f5\\u03f7-\\u0481\\u048a-\\u052f\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05d0-\\u05ea\\u05ef-\\u05f2\\u0620-\\u064a\\u066e\\u066f\\u0671-\\u06d3\\u06d5\\u06e5\\u06e6\\u06ee\\u06ef\\u06fa-\\u06fc\\u06ff\\u0710\\u0712-\\u072f\\u074d-\\u07a5\\u07b1\\u07ca-\\u07ea\\u07f4\\u07f5\\u07fa\\u0800-\\u0815\\u081a\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086a\\u0870-\\u0887\\u0889-\\u088f\\u08a0-\\u08c9\\u0904-\\u0939\\u093d\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098c\\u098f\\u0990\\u0993-\\u09a8\\u09aa-\\u09b0\\u09b2\\u09b6-\\u09b9\\u09bd\\u09ce\\u09dc\\u09dd\\u09df-\\u09e1\\u09f0\\u09f1\\u09fc\\u0a05-\\u0a0a\\u0a0f\\u0a10\\u0a13-\\u0a28\\u0a2a-\\u0a30\\u0a32\\u0a33\\u0a35\\u0a36\\u0a38\\u0a39\\u0a59-\\u0a5c\\u0a5e\\u0a72-\\u0a74\\u0a85-\\u0a8d\\u0a8f-\\u0a91\\u0a93-\\u0aa8\\u0aaa-\\u0ab0\\u0ab2\\u0ab3\\u0ab5-\\u0ab9\\u0abd\\u0ad0\\u0ae0\\u0ae1\\u0af9\\u0b05-\\u0b0c\\u0b0f\\u0b10\\u0b13-\\u0b28\\u0b2a-\\u0b30\\u0b32\\u0b33\\u0b35-\\u0b39\\u0b3d\\u0b5c\\u0b5d\\u0b5f-\\u0b61\\u0b71\\u0b83\\u0b85-\\u0b8a\\u0b8e-\\u0b90\\u0b92-\\u0b95\\u0b99\\u0b9a\\u0b9c\\u0b9e\\u0b9f\\u0ba3\\u0ba4\\u0ba8-\\u0baa\\u0bae-\\u0bb9\\u0bd0\\u0c05-\\u0c0c\\u0c0e-\\u0c10\\u0c12-\\u0c28\\u0c2a-\\u0c39\\u0c3d\\u0c58-\\u0c5a\\u0c5c\\u0c5d\\u0c60\\u0c61\\u0c80\\u0c85-\\u0c8c\\u0c8e-\\u0c90\\u0c92-\\u0ca8\\u0caa-\\u0cb3\\u0cb5-\\u0cb9\\u0cbd\\u0cdc-\\u0cde\\u0ce0\\u0ce1\\u0cf1\\u0cf2\\u0d04-\\u0d0c\\u0d0e-\\u0d10\\u0d12-\\u0d3a\\u0d3d\\u0d4e\\u0d54-\\u0d56\\u0d5f-\\u0d61\\u0d7a-\\u0d7f\\u0d85-\\u0d96\\u0d9a-\\u0db1\\u0db3-\\u0dbb\\u0dbd\\u0dc0-\\u0dc6\\u0e01-\\u0e30\\u0e32\\u0e33\\u0e40-\\u0e46\\u0e81\\u0e82\\u0e84\\u0e86-\\u0e8a\\u0e8c-\\u0ea3\\u0ea5\\u0ea7-\\u0eb0\\u0eb2\\u0eb3\\u0ebd\\u0ec0-\\u0ec4\\u0ec6\\u0edc-\\u0edf\\u0f00\\u0f40-\\u0f47\\u0f49-\\u0f6c\\u0f88-\\u0f8c\\u1000-\\u102a\\u103f\\u1050-\\u1055\\u105a-\\u105d\\u1061\\u1065\\u1066\\u106e-\\u1070\\u1075-\\u1081\\u108e\\u10a0-\\u10c5\\u10c7\\u10cd\\u10d0-\\u10fa\\u10fc-\\u1248\\u124a-\\u124d\\u1250-\\u1256\\u1258\\u125a-\\u125d\\u1260-\\u1288\\u128a-\\u128d\\u1290-\\u12b0\\u12b2-\\u12b5\\u12b8-\\u12be\\u12c0\\u12c2-\\u12c5\\u12c8-\\u12d6\\u12d8-\\u1310\\u1312-\\u1315\\u1318-\\u135a\\u1380-\\u138f\\u13a0-\\u13f5\\u13f8-\\u13fd\\u1401-\\u166c\\u166f-\\u167f\\u1681-\\u169a\\u16a0-\\u16ea\\u16ee-\\u16f8\\u1700-\\u1711\\u171f-\\u1731\\u1740-\\u1751\\u1760-\\u176c\\u176e-\\u1770\\u1780-\\u17b3\\u17d7\\u17dc\\u1820-\\u1878\\u1880-\\u18a8\\u18aa\\u18b0-\\u18f5\\u1900-\\u191e\\u1950-\\u196d\\u1970-\\u1974\\u1980-\\u19ab\\u19b0-\\u19c9\\u1a00-\\u1a16\\u1a20-\\u1a54\\u1aa7\\u1b05-\\u1b33\\u1b45-\\u1b4c\\u1b83-\\u1ba0\\u1bae\\u1baf\\u1bba-\\u1be5\\u1c00-\\u1c23\\u1c4d-\\u1c4f\\u1c5a-\\u1c7d\\u1c80-\\u1c8a\\u1c90-\\u1cba\\u1cbd-\\u1cbf\\u1ce9-\\u1cec\\u1cee-\\u1cf3\\u1cf5\\u1cf6\\u1cfa\\u1d00-\\u1dbf\\u1e00-\\u1f15\\u1f18-\\u1f1d\\u1f20-\\u1f45\\u1f48-\\u1f4d\\u1f50-\\u1f57\\u1f59\\u1f5b\\u1f5d\\u1f5f-\\u1f7d\\u1f80-\\u1fb4\\u1fb6-\\u1fbc\\u1fbe\\u1fc2-\\u1fc4\\u1fc6-\\u1fcc\\u1fd0-\\u1fd3\\u1fd6-\\u1fdb\\u1fe0-\\u1fec\\u1ff2-\\u1ff4\\u1ff6-\\u1ffc\\u2071\\u207f\\u2090-\\u209c\\u2102\\u2107\\u210a-\\u2113\\u2115\\u2118-\\u211d\\u2124\\u2126\\u2128\\u212a-\\u2139\\u213c-\\u213f\\u2145-\\u2149\\u214e\\u2160-\\u2188\\u2c00-\\u2ce4\\u2ceb-\\u2cee\\u2cf2\\u2cf3\\u2d00-\\u2d25\\u2d27\\u2d2d\\u2d30-\\u2d67\\u2d6f\\u2d80-\\u2d96\\u2da0-\\u2da6\\u2da8-\\u2dae\\u2db0-\\u2db6\\u2db8-\\u2dbe\\u2dc0-\\u2dc6\\u2dc8-\\u2dce\\u2dd0-\\u2dd6\\u2dd8-\\u2dde\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303c\\u3041-\\u3096\\u309b-\\u309f\\u30a1-\\u30fa\\u30fc-\\u30ff\\u3105-\\u312f\\u3131-\\u318e\\u31a0-\\u31bf\\u31f0-\\u31ff\\u3400-\\u4dbf\\u4e00-\\ua48c\\ua4d0-\\ua4fd\\ua500-\\ua60c\\ua610-\\ua61f\\ua62a\\ua62b\\ua640-\\ua66e\\ua67f-\\ua69d\\ua6a0-\\ua6ef\\ua717-\\ua71f\\ua722-\\ua788\\ua78b-\\ua7dc\\ua7f1-\\ua801\\ua803-\\ua805\\ua807-\\ua80a\\ua80c-\\ua822\\ua840-\\ua873\\ua882Line truncated
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
Object.defineProperty(exports, "isIdentifierChar", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _identifier.isIdentifierChar;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "isIdentifierName", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _identifier.isIdentifierName;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "isIdentifierStart", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _identifier.isIdentifierStart;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "isKeyword", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _keyword.isKeyword;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "isReservedWord", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _keyword.isReservedWord;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "isStrictBindOnlyReservedWord", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _keyword.isStrictBindOnlyReservedWord;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "isStrictBindReservedWord", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _keyword.isStrictBindReservedWord;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "isStrictReservedWord", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _keyword.isStrictReservedWord;
|
||||
}
|
||||
});
|
||||
var _identifier = require("./identifier.js");
|
||||
var _keyword = require("./keyword.js");
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["_identifier","require","_keyword"],"sources":["../src/index.ts"],"sourcesContent":["export {\n isIdentifierName,\n isIdentifierChar,\n isIdentifierStart,\n} from \"./identifier.ts\";\nexport {\n isReservedWord,\n isStrictBindOnlyReservedWord,\n isStrictBindReservedWord,\n isStrictReservedWord,\n isKeyword,\n} from \"./keyword.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,WAAA,GAAAC,OAAA;AAKA,IAAAC,QAAA,GAAAD,OAAA","ignoreList":[]}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.isKeyword = isKeyword;
|
||||
exports.isReservedWord = isReservedWord;
|
||||
exports.isStrictBindOnlyReservedWord = isStrictBindOnlyReservedWord;
|
||||
exports.isStrictBindReservedWord = isStrictBindReservedWord;
|
||||
exports.isStrictReservedWord = isStrictReservedWord;
|
||||
const reservedWords = {
|
||||
keyword: ["break", "case", "catch", "continue", "debugger", "default", "do", "else", "finally", "for", "function", "if", "return", "switch", "throw", "try", "var", "const", "while", "with", "new", "this", "super", "class", "extends", "export", "import", "null", "true", "false", "in", "instanceof", "typeof", "void", "delete"],
|
||||
strict: ["implements", "interface", "let", "package", "private", "protected", "public", "static", "yield"],
|
||||
strictBind: ["eval", "arguments"]
|
||||
};
|
||||
const keywords = new Set(reservedWords.keyword);
|
||||
const reservedWordsStrictSet = new Set(reservedWords.strict);
|
||||
const reservedWordsStrictBindSet = new Set(reservedWords.strictBind);
|
||||
function isReservedWord(word, inModule) {
|
||||
return inModule && word === "await" || word === "enum";
|
||||
}
|
||||
function isStrictReservedWord(word, inModule) {
|
||||
return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);
|
||||
}
|
||||
function isStrictBindOnlyReservedWord(word) {
|
||||
return reservedWordsStrictBindSet.has(word);
|
||||
}
|
||||
function isStrictBindReservedWord(word, inModule) {
|
||||
return isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word);
|
||||
}
|
||||
function isKeyword(word) {
|
||||
return keywords.has(word);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=keyword.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["reservedWords","keyword","strict","strictBind","keywords","Set","reservedWordsStrictSet","reservedWordsStrictBindSet","isReservedWord","word","inModule","isStrictReservedWord","has","isStrictBindOnlyReservedWord","isStrictBindReservedWord","isKeyword"],"sources":["../src/keyword.ts"],"sourcesContent":["const reservedWords = {\n keyword: [\n \"break\",\n \"case\",\n \"catch\",\n \"continue\",\n \"debugger\",\n \"default\",\n \"do\",\n \"else\",\n \"finally\",\n \"for\",\n \"function\",\n \"if\",\n \"return\",\n \"switch\",\n \"throw\",\n \"try\",\n \"var\",\n \"const\",\n \"while\",\n \"with\",\n \"new\",\n \"this\",\n \"super\",\n \"class\",\n \"extends\",\n \"export\",\n \"import\",\n \"null\",\n \"true\",\n \"false\",\n \"in\",\n \"instanceof\",\n \"typeof\",\n \"void\",\n \"delete\",\n ],\n strict: [\n \"implements\",\n \"interface\",\n \"let\",\n \"package\",\n \"private\",\n \"protected\",\n \"public\",\n \"static\",\n \"yield\",\n ],\n strictBind: [\"eval\", \"arguments\"],\n};\nconst keywords = new Set(reservedWords.keyword);\nconst reservedWordsStrictSet = new Set(reservedWords.strict);\nconst reservedWordsStrictBindSet = new Set(reservedWords.strictBind);\n\n/**\n * Checks if word is a reserved word in non-strict mode\n */\nexport function isReservedWord(word: string, inModule: boolean): boolean {\n return (inModule && word === \"await\") || word === \"enum\";\n}\n\n/**\n * Checks if word is a reserved word in non-binding strict mode\n *\n * Includes non-strict reserved words\n */\nexport function isStrictReservedWord(word: string, inModule: boolean): boolean {\n return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode, but it is allowed as\n * a normal identifier.\n */\nexport function isStrictBindOnlyReservedWord(word: string): boolean {\n return reservedWordsStrictBindSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode\n *\n * Includes non-strict reserved words and non-binding strict reserved words\n */\nexport function isStrictBindReservedWord(\n word: string,\n inModule: boolean,\n): boolean {\n return (\n isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word)\n );\n}\n\nexport function isKeyword(word: string): boolean {\n return keywords.has(word);\n}\n"],"mappings":";;;;;;;;;;AAAA,MAAMA,aAAa,GAAG;EACpBC,OAAO,EAAE,CACP,OAAO,EACP,MAAM,EACN,OAAO,EACP,UAAU,EACV,UAAU,EACV,SAAS,EACT,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EACL,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,KAAK,EACL,MAAM,EACN,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,MAAM,EACN,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,QAAQ,CACT;EACDC,MAAM,EAAE,CACN,YAAY,EACZ,WAAW,EACX,KAAK,EACL,SAAS,EACT,SAAS,EACT,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,OAAO,CACR;EACDC,UAAU,EAAE,CAAC,MAAM,EAAE,WAAW;AAClC,CAAC;AACD,MAAMC,QAAQ,GAAG,IAAIC,GAAG,CAACL,aAAa,CAACC,OAAO,CAAC;AAC/C,MAAMK,sBAAsB,GAAG,IAAID,GAAG,CAACL,aAAa,CAACE,MAAM,CAAC;AAC5D,MAAMK,0BAA0B,GAAG,IAAIF,GAAG,CAACL,aAAa,CAACG,UAAU,CAAC;AAK7D,SAASK,cAAcA,CAACC,IAAY,EAAEC,QAAiB,EAAW;EACvE,OAAQA,QAAQ,IAAID,IAAI,KAAK,OAAO,IAAKA,IAAI,KAAK,MAAM;AAC1D;AAOO,SAASE,oBAAoBA,CAACF,IAAY,EAAEC,QAAiB,EAAW;EAC7E,OAAOF,cAAc,CAACC,IAAI,EAAEC,QAAQ,CAAC,IAAIJ,sBAAsB,CAACM,GAAG,CAACH,IAAI,CAAC;AAC3E;AAMO,SAASI,4BAA4BA,CAACJ,IAAY,EAAW;EAClE,OAAOF,0BAA0B,CAACK,GAAG,CAACH,IAAI,CAAC;AAC7C;AAOO,SAASK,wBAAwBA,CACtCL,IAAY,EACZC,QAAiB,EACR;EACT,OACEC,oBAAoB,CAACF,IAAI,EAAEC,QAAQ,CAAC,IAAIG,4BAA4B,CAACJ,IAAI,CAAC;AAE9E;AAEO,SAASM,SAASA,CAACN,IAAY,EAAW;EAC/C,OAAOL,QAAQ,CAACQ,GAAG,CAACH,IAAI,CAAC;AAC3B","ignoreList":[]}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@babel/helper-validator-identifier",
|
||||
"version": "7.29.7",
|
||||
"description": "Validate identifier/keywords name",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/babel/babel.git",
|
||||
"directory": "packages/babel-helper-validator-identifier"
|
||||
},
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@unicode/unicode-17.0.0": "^1.6.10",
|
||||
"charcodes": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"author": "The Babel Team (https://babel.dev/team)",
|
||||
"type": "commonjs"
|
||||
}
|
||||
+1073
File diff suppressed because it is too large.
Load diff
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# @babel/parser
|
||||
|
||||
> A JavaScript parser
|
||||
|
||||
See our website [@babel/parser](https://babeljs.io/docs/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%22+is%3Aopen) associated with this package.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save-dev @babel/parser
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/parser --dev
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-var, unicorn/prefer-node-protocol */
|
||||
|
||||
var parser = require("..");
|
||||
var fs = require("fs");
|
||||
|
||||
var filename = process.argv[2];
|
||||
if (!filename) {
|
||||
console.error("no filename specified");
|
||||
} else {
|
||||
var file = fs.readFileSync(filename, "utf8");
|
||||
var ast = parser.parse(file);
|
||||
|
||||
console.log(JSON.stringify(ast, null, " "));
|
||||
}
|
||||
+14599
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sources":["../src/util/location.ts","../src/parse-error/module-errors.ts","../src/parse-error/to-node-description.ts","../src/parse-error/standard-errors.ts","../src/parse-error/strict-mode-errors.ts","../src/parse-error/parse-expression-errors.ts","../src/parse-error/pipeline-operator-errors.ts","../src/parse-error.ts","../src/options.ts","../src/plugins/estree.ts","../src/tokenizer/context.ts","../src/tokenizer/types.ts","../../babel-helper-validator-identifier/src/identifier.ts","../../babel-helper-validator-identifier/src/keyword.ts","../src/util/identifier.ts","../src/util/scope.ts","../src/plugins/flow/scope.ts","../src/plugins/flow/index.ts","../src/plugins/jsx/xhtml.ts","../src/util/whitespace.ts","../src/plugins/jsx/index.ts","../src/plugins/typescript/scope.ts","../src/util/production-parameter.ts","../src/parser/base.ts","../src/parser/comments.ts","../src/tokenizer/state.ts","../../babel-helper-string-parser/src/index.ts","../src/tokenizer/index.ts","../src/util/class-scope.ts","../src/util/expression-scope.ts","../src/parser/util.ts","../src/parser/node.ts","../src/parser/lval.ts","../src/plugins/typescript/index.ts","../src/plugins/placeholders.ts","../src/plugins/v8intrinsic.ts","../src/plugin-utils.ts","../src/parser/expression.ts","../src/parser/statement.ts","../src/parser/index.ts","../src/index.ts"],"sourcesContent":["export type Pos = {\n start: number;\n};\n\n// These are used when `options.locations` is on, for the\n// `startLoc` and `endLoc` properties.\n\nexport class Position {\n line: number;\n column: number;\n index: number;\n\n constructor(line: number, col: number, index: number) {\n this.line = line;\n this.column = col;\n this.index = index;\n }\n}\n\nexport class SourceLocation {\n start: Position;\n end: Position;\n filename: string | undefined;\n identifierName: string | undefined | null;\n\n constructor(start: Position, end?: Position) {\n this.start = start;\n // (may start as null, but initialized later)\n this.end = end!;\n }\n}\n\n/**\n * creates a new position with a non-zero column offset from the given position.\n * This function should be only be used when we create AST node out of the token\n * boundaries, such as TemplateElement ends before tt.templateNonTail. This\n * function does not skip whitespaces.\n */\nexport function createPositionWithColumnOffset(\n position: Position,\n columnOffset: number,\n) {\n const { line, column, index } = position;\n return new Position(line, column + columnOffset, index + columnOffset);\n}\n","import type { ParseErrorTemplates } from \"../parse-error.ts\";\n\nconst code = \"BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED\";\n\nexport default {\n ImportMetaOutsideModule: {\n message: `import.meta may appear only with 'sourceType: \"module\"'`,\n code,\n },\n ImportOutsideModule: {\n message: `'import' and 'export' may appear only with 'sourceType: \"module\"'`,\n code,\n },\n} satisfies ParseErrorTemplates;\n","const NodeDescriptions = {\n ArrayPattern: \"array destructuring pattern\",\n AssignmentExpression: \"assignment expression\",\n AssignmentPattern: \"assignment expression\",\n ArrowFunctionExpression: \"arrow function expression\",\n ConditionalExpression: \"conditional expression\",\n CatchClause: \"catch clause\",\n ForOfStatement: \"for-of statement\",\n ForInStatement: \"for-in statement\",\n ForStatement: \"for-loop\",\n FormalParameters: \"function parameter list\",\n Identifier: \"identifier\",\n ImportSpecifier: \"import specifier\",\n ImportDefaultSpecifier: \"import default specifier\",\n ImportNamespaceSpecifier: \"import namespace specifier\",\n ObjectPattern: \"object destructuring pattern\",\n ParenthesizedExpression: \"parenthesized expression\",\n RestElement: \"rest element\",\n UpdateExpression: {\n true: \"prefix operation\",\n false: \"postfix operation\",\n },\n VariableDeclarator: \"variable declaration\",\n YieldExpression: \"yield expression\",\n};\n\ntype NodeTypesWithDescriptions = keyof Omit<\n typeof NodeDescriptions,\n \"UpdateExpression\"\n>;\n\ntype NodeWithDescription =\n | {\n type: \"UpdateExpression\";\n prefix: boolean;\n }\n | {\n type: NodeTypesWithDescriptions;\n };\n\nconst toNodeDescription = (node: NodeWithDescription) =>\n node.type === \"UpdateExpression\"\n ? NodeDescriptions.UpdateExpression[`${node.prefix}`]\n : NodeDescriptions[node.type];\n\nexport default toNodeDescription;\n","import type { ParseErrorTemplates } from \"../parse-error.ts\";\nimport toNodeDescription from \"./to-node-description.ts\";\n\nexport type LValAncestor =\n | { type: \"UpdateExpression\"; prefix: boolean }\n | {\n type:\n | \"ArrayPattern\"\n | \"AssignmentExpression\"\n | \"CatchClause\"\n | \"ForOfStatement\"\n | \"FormalParameters\"\n | \"ForInStatement\"\n | \"ForStatement\"\n | \"ImportSpecifier\Line truncated
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@babel/parser",
|
||||
"version": "7.29.7",
|
||||
"description": "A JavaScript parser",
|
||||
"author": "The Babel Team (https://babel.dev/team)",
|
||||
"homepage": "https://babel.dev/docs/en/next/babel-parser",
|
||||
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"babel",
|
||||
"javascript",
|
||||
"parser",
|
||||
"tc39",
|
||||
"ecmascript",
|
||||
"@babel/parser"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/babel/babel.git",
|
||||
"directory": "packages/babel-parser"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"types": "./typings/babel-parser.d.ts",
|
||||
"files": [
|
||||
"bin",
|
||||
"lib",
|
||||
"typings/babel-parser.d.ts",
|
||||
"index.cjs"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"# dependencies": "This package doesn't actually have runtime dependencies. @babel/types is only needed for type definitions.",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/helper-check-duplicate-nodes": "^7.29.7",
|
||||
"@babel/helper-fixtures": "^7.29.7",
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"charcodes": "^0.2.0"
|
||||
},
|
||||
"bin": "./bin/babel-parser.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
// This file is auto-generated! Do not modify it directly.
|
||||
// Run `yarn gulp bundle-dts` to re-generate it.
|
||||
/* eslint-disable @typescript-eslint/consistent-type-imports, @typescript-eslint/no-redundant-type-constituents */
|
||||
import { File, Expression } from '@babel/types';
|
||||
|
||||
declare class Position {
|
||||
line: number;
|
||||
column: number;
|
||||
index: number;
|
||||
constructor(line: number, col: number, index: number);
|
||||
}
|
||||
|
||||
type SyntaxPlugin = "flow" | "typescript" | "jsx" | "pipelineOperator" | "placeholders";
|
||||
type ParseErrorCode = "BABEL_PARSER_SYNTAX_ERROR" | "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED";
|
||||
interface ParseErrorSpecification<ErrorDetails> {
|
||||
code: ParseErrorCode;
|
||||
reasonCode: string;
|
||||
syntaxPlugin?: SyntaxPlugin;
|
||||
missingPlugin?: string | string[];
|
||||
loc: Position;
|
||||
details: ErrorDetails;
|
||||
pos: number;
|
||||
}
|
||||
type ParseError$1<ErrorDetails> = SyntaxError & ParseErrorSpecification<ErrorDetails>;
|
||||
|
||||
type BABEL_8_BREAKING = false;
|
||||
type IF_BABEL_7<V> = false extends BABEL_8_BREAKING ? V : never;
|
||||
|
||||
type Plugin$1 =
|
||||
| "asyncDoExpressions"
|
||||
| IF_BABEL_7<"asyncGenerators">
|
||||
| IF_BABEL_7<"bigInt">
|
||||
| IF_BABEL_7<"classPrivateMethods">
|
||||
| IF_BABEL_7<"classPrivateProperties">
|
||||
| IF_BABEL_7<"classProperties">
|
||||
| IF_BABEL_7<"classStaticBlock">
|
||||
| IF_BABEL_7<"decimal">
|
||||
| "decorators-legacy"
|
||||
| "deferredImportEvaluation"
|
||||
| "decoratorAutoAccessors"
|
||||
| "destructuringPrivate"
|
||||
| IF_BABEL_7<"deprecatedImportAssert">
|
||||
| "doExpressions"
|
||||
| IF_BABEL_7<"dynamicImport">
|
||||
| IF_BABEL_7<"explicitResourceManagement">
|
||||
| "exportDefaultFrom"
|
||||
| IF_BABEL_7<"exportNamespaceFrom">
|
||||
| "flow"
|
||||
| "flowComments"
|
||||
| "functionBind"
|
||||
| "functionSent"
|
||||
| "importMeta"
|
||||
| "jsx"
|
||||
| IF_BABEL_7<"jsonStrings">
|
||||
| IF_BABEL_7<"logicalAssignment">
|
||||
| IF_BABEL_7<"importAssertions">
|
||||
| IF_BABEL_7<"importReflection">
|
||||
| "moduleBlocks"
|
||||
| IF_BABEL_7<"moduleStringNames">
|
||||
| IF_BABEL_7<"nullishCoalescingOperator">
|
||||
| IF_BABEL_7<"numericSeparator">
|
||||
| IF_BABEL_7<"objectRestSpread">
|
||||
| IF_BABEL_7<"optionalCatchBinding">
|
||||
| IF_BABEL_7<"optionalChaining">
|
||||
| "partialApplication"
|
||||
| "placeholders"
|
||||
| IF_BABEL_7<"privateIn">
|
||||
| IF_BABEL_7<"regexpUnicodeSets">
|
||||
| "sourcePhaseImports"
|
||||
| "throwExpressions"
|
||||
| IF_BABEL_7<"topLevelAwait">
|
||||
| "v8intrinsic"
|
||||
| ParserPluginWithOptions[0];
|
||||
|
||||
type ParserPluginWithOptions =
|
||||
| ["decorators", DecoratorsPluginOptions]
|
||||
| ["discardBinding", { syntaxType: "void" }]
|
||||
| ["estree", { classFeatures?: boolean }]
|
||||
| IF_BABEL_7<["importAttributes", { deprecatedAssertSyntax: boolean }]>
|
||||
| IF_BABEL_7<["moduleAttributes", { version: "may-2020" }]>
|
||||
| ["optionalChainingAssign", { version: "2023-07" }]
|
||||
| ["pipelineOperator", PipelineOperatorPluginOptions]
|
||||
| ["recordAndTuple", RecordAndTuplePluginOptions]
|
||||
| ["flow", FlowPluginOptions]
|
||||
| ["typescript", TypeScriptPluginOptions];
|
||||
|
||||
type PluginConfig = Plugin$1 | ParserPluginWithOptions;
|
||||
|
||||
interface DecoratorsPluginOptions {
|
||||
decoratorsBeforeExport?: boolean;
|
||||
allowCallParenthesized?: boolean;
|
||||
}
|
||||
|
||||
interface PipelineOperatorPluginOptions {
|
||||
proposal: BABEL_8_BREAKING extends false
|
||||
? "minimal" | "fsharp" | "hack" | "smart"
|
||||
: "fsharp" | "hack";
|
||||
topicToken?: "%" | "#" | "@@" | "^^" | "^";
|
||||
}
|
||||
|
||||
interface RecordAndTuplePluginOptions {
|
||||
syntaxType: "bar" | "hash";
|
||||
}
|
||||
|
||||
type FlowPluginOptions = BABEL_8_BREAKING extends true
|
||||
? {
|
||||
all?: boolean;
|
||||
enums?: boolean;
|
||||
}
|
||||
: {
|
||||
all?: boolean;
|
||||
};
|
||||
|
||||
interface TypeScriptPluginOptions {
|
||||
dts?: boolean;
|
||||
disallowAmbiguousJSXLike?: boolean;
|
||||
}
|
||||
|
||||
type Plugin = PluginConfig;
|
||||
|
||||
type SourceType = "script" | "commonjs" | "module" | "unambiguous";
|
||||
interface Options {
|
||||
/**
|
||||
* By default, import and export declarations can only appear at a program's top level.
|
||||
* Setting this option to true allows them anywhere where a statement is allowed.
|
||||
*/
|
||||
allowImportExportEverywhere?: boolean;
|
||||
/**
|
||||
* By default, await use is not allowed outside of an async function.
|
||||
* Set this to true to accept such code.
|
||||
*/
|
||||
allowAwaitOutsideFunction?: boolean;
|
||||
/**
|
||||
* By default, a return statement at the top level raises an error.
|
||||
* Set this to true to accept such code.
|
||||
*/
|
||||
allowReturnOutsideFunction?: boolean;
|
||||
/**
|
||||
* By default, new.target use is not allowed outside of a function or class.
|
||||
* Set this to true to accept such code.
|
||||
*/
|
||||
allowNewTargetOutsideFunction?: boolean;
|
||||
/**
|
||||
* By default, super calls are not allowed outside of a method.
|
||||
* Set this to true to accept such code.
|
||||
*/
|
||||
allowSuperOutsideMethod?: boolean;
|
||||
/**
|
||||
* By default, exported identifiers must refer to a declared variable.
|
||||
* Set this to true to allow export statements to reference undeclared variables.
|
||||
*/
|
||||
allowUndeclaredExports?: boolean;
|
||||
/**
|
||||
* By default, yield use is not allowed outside of a generator function.
|
||||
* Set this to true to accept such code.
|
||||
*/
|
||||
allowYieldOutsideFunction?: boolean;
|
||||
/**
|
||||
* By default, Babel parser JavaScript code according to Annex B syntax.
|
||||
* Set this to `false` to disable such behavior.
|
||||
*/
|
||||
annexB?: boolean;
|
||||
/**
|
||||
* By default, Babel attaches comments to adjacent AST nodes.
|
||||
* When this option is set to false, comments are not attached.
|
||||
* It can provide up to 30% performance improvement when the input code has many comments.
|
||||
* @babel/eslint-parser will set it for you.
|
||||
* It is not recommended to use attachComment: false with Babel transform,
|
||||
* as doing so removes all the comments in output code, and renders annotations such as
|
||||
* /* istanbul ignore next *\/ nonfunctional.
|
||||
*/
|
||||
attachComment?: boolean;
|
||||
/**
|
||||
* By default, Babel always throws an error when it finds some invalid code.
|
||||
* When this option is set to true, it will store the parsing error and
|
||||
* try to continue parsing the invalid input file.
|
||||
*/
|
||||
errorRecovery?: boolean;
|
||||
/**
|
||||
* Indicate the mode the code should be parsed in.
|
||||
* Can be one of "script", "commonjs", "module", or "unambiguous". Defaults to "script".
|
||||
* "unambiguous" will make @babel/parser attempt to guess, based on the presence
|
||||
* of ES6 import or export statements.
|
||||
* Files with ES6 imports and exports are considered "module" and are otherwise "script".
|
||||
*
|
||||
* Use "commonjs" to parse code that is intended to be run in a CommonJS environment such as Node.js.
|
||||
*/
|
||||
sourceType?: SourceType;
|
||||
/**
|
||||
* Correlate output AST nodes with their source filename.
|
||||
* Useful when generating code and source maps from the ASTs of multiple input files.
|
||||
*/
|
||||
sourceFilename?: string;
|
||||
/**
|
||||
* By default, all source indexes start from 0.
|
||||
* You can provide a start index to alternatively start with.
|
||||
* Useful for integration with other source tools.
|
||||
*/
|
||||
startIndex?: number;
|
||||
/**
|
||||
* By default, the first line of code parsed is treated as line 1.
|
||||
* You can provide a line number to alternatively start with.
|
||||
* Useful for integration with other source tools.
|
||||
*/
|
||||
startLine?: number;
|
||||
/**
|
||||
* By default, the parsed code is treated as if it starts from line 1, column 0.
|
||||
* You can provide a column number to alternatively start with.
|
||||
* Useful for integration with other source tools.
|
||||
*/
|
||||
startColumn?: number;
|
||||
/**
|
||||
* Array containing the plugins that you want to enable.
|
||||
*/
|
||||
plugins?: Plugin[];
|
||||
/**
|
||||
* Should the parser work in strict mode.
|
||||
* Defaults to true if sourceType === 'module'. Otherwise, false.
|
||||
*/
|
||||
strictMode?: boolean;
|
||||
/**
|
||||
* Adds a ranges property to each node: [node.start, node.end]
|
||||
*/
|
||||
ranges?: boolean;
|
||||
/**
|
||||
* Adds all parsed tokens to a tokens property on the File node.
|
||||
*/
|
||||
tokens?: boolean;
|
||||
/**
|
||||
* By default, the parser adds information about parentheses by setting
|
||||
* `extra.parenthesized` to `true` as needed.
|
||||
* When this option is `true` the parser creates `ParenthesizedExpression`
|
||||
* AST nodes instead of using the `extra` property.
|
||||
*/
|
||||
createParenthesizedExpressions?: boolean;
|
||||
/**
|
||||
* The default is false in Babel 7 and true in Babel 8
|
||||
* Set this to true to parse it as an `ImportExpression` node.
|
||||
* Otherwise `import(foo)` is parsed as `CallExpression(Import, [Identifier(foo)])`.
|
||||
*/
|
||||
createImportExpressions?: boolean;
|
||||
}
|
||||
|
||||
type ParserOptions = Partial<Options>;
|
||||
type ParseError = ParseError$1<object>;
|
||||
type ParseResult<Result extends File | Expression = File> = Result & {
|
||||
comments: File["comments"];
|
||||
errors: null | ParseError[];
|
||||
tokens?: File["tokens"];
|
||||
};
|
||||
/**
|
||||
* Parse the provided code as an entire ECMAScript program.
|
||||
*/
|
||||
declare function parse(input: string, options?: ParserOptions): ParseResult<File>;
|
||||
declare function parseExpression(input: string, options?: ParserOptions): ParseResult<Expression>;
|
||||
|
||||
declare const tokTypes: {
|
||||
// todo(flow->ts) real token type
|
||||
[name: string]: any;
|
||||
};
|
||||
|
||||
export { DecoratorsPluginOptions, FlowPluginOptions, ParseError, ParseResult, ParserOptions, PluginConfig as ParserPlugin, ParserPluginWithOptions, PipelineOperatorPluginOptions, RecordAndTuplePluginOptions, TypeScriptPluginOptions, parse, parseExpression, tokTypes };
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# @babel/types
|
||||
|
||||
> Babel Types is a Lodash-esque utility library for AST nodes
|
||||
|
||||
See our website [@babel/types](https://babeljs.io/docs/babel-types) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20types%22+is%3Aopen) associated with this package.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save-dev @babel/types
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/types --dev
|
||||
```
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = assertNode;
|
||||
var _isNode = require("../validators/isNode.js");
|
||||
function assertNode(node) {
|
||||
if (!(0, _isNode.default)(node)) {
|
||||
var _node$type;
|
||||
const type = (_node$type = node == null ? void 0 : node.type) != null ? _node$type : JSON.stringify(node);
|
||||
throw new TypeError(`Not a valid node of type "${type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=assertNode.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["_isNode","require","assertNode","node","isNode","_node$type","type","JSON","stringify","TypeError"],"sources":["../../src/asserts/assertNode.ts"],"sourcesContent":["import isNode from \"../validators/isNode.ts\";\nimport type * as t from \"../index.ts\";\n\nexport default function assertNode(node?: any): asserts node is t.Node {\n if (!isNode(node)) {\n const type = node?.type ?? JSON.stringify(node);\n throw new TypeError(`Not a valid node of type \"${type}\"`);\n }\n}\n"],"mappings":";;;;;;AAAA,IAAAA,OAAA,GAAAC,OAAA;AAGe,SAASC,UAAUA,CAACC,IAAU,EAA0B;EACrE,IAAI,CAAC,IAAAC,eAAM,EAACD,IAAI,CAAC,EAAE;IAAA,IAAAE,UAAA;IACjB,MAAMC,IAAI,IAAAD,UAAA,GAAGF,IAAI,oBAAJA,IAAI,CAAEG,IAAI,YAAAD,UAAA,GAAIE,IAAI,CAACC,SAAS,CAACL,IAAI,CAAC;IAC/C,MAAM,IAAIM,SAAS,CAAC,6BAA6BH,IAAI,GAAG,CAAC;EAC3D;AACF","ignoreList":[]}
|
||||
+1251
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["_is","require","_deprecationWarning","assert","type","node","opts","is","Error","JSON","stringify","assertArrayExpression","assertAssignmentExpression","assertBinaryExpression","assertInterpreterDirective","assertDirective","assertDirectiveLiteral","assertBlockStatement","assertBreakStatement","assertCallExpression","assertCatchClause","assertConditionalExpression","assertContinueStatement","assertDebuggerStatement","assertDoWhileStatement","assertEmptyStatement","assertExpressionStatement","assertFile","assertForInStatement","assertForStatement","assertFunctionDeclaration","assertFunctionExpression","assertIdentifier","assertIfStatement","assertLabeledStatement","assertStringLiteral","assertNumericLiteral","assertNullLiteral","assertBooleanLiteral","assertRegExpLiteral","assertLogicalExpression","assertMemberExpression","assertNewExpression","assertProgram","assertObjectExpression","assertObjectMethod","assertObjectProperty","assertRestElement","assertReturnStatement","assertSequenceExpression","assertParenthesizedExpression","assertSwitchCase","assertSwitchStatement","assertThisExpression","assertThrowStatement","assertTryStatement","assertUnaryExpression","assertUpdateExpression","assertVariableDeclaration","assertVariableDeclarator","assertWhileStatement","assertWithStatement","assertAssignmentPattern","assertArrayPattern","assertArrowFunctionExpression","assertClassBody","assertClassExpression","assertClassDeclaration","assertExportAllDeclaration","assertExportDefaultDeclaration","assertExportNamedDeclaration","assertExportSpecifier","assertForOfStatement","assertImportDeclaration","assertImportDefaultSpecifier","assertImportNamespaceSpecifier","assertImportSpecifier","assertImportExpression","assertMetaProperty","assertClassMethod","assertObjectPattern","assertSpreadElement","assertSuper","assertTaggedTemplateExpression","assertTemplateElement","assertTemplateLiteral","assertYieldExpression","assertAwaitExpression","assertImport","assertBigIntLiteral","assertExportNamespaceSpecifier","assertOptionalMemberExpression","assertOptionalCallExpression","assertClassProperty","assertClassAccessorProperty","assertClassPrivateProperty","assertClassPrivateMethod","assertPrivateName","assertStaticBlock","assertImportAttribute","assertAnyTypeAnnotation","assertArrayTypeAnnotation","assertBooleanTypeAnnotation","assertBooleanLiteralTypeAnnotation","assertNullLiteralTypeAnnotation","assertClassImplements","assertDeclareClass","assertDeclareFunction","assertDeclareInterface","assertDeclareModule","assertDeclareModuleExports","assertDeclareTypeAlias","assertDeclareOpaqueType","assertDeclareVariable","assertDeclareExportDeclaration","assertDeclareExportAllDeclaration","assertDeclaredPredicate","assertExistsTypeAnnotation","assertFunctionTypeAnnotation","assertFunctionTypeParam","assertGenericTypeAnnotation","assertInferredPredicate","assertInterfaceExtends","assertInterfaceDeclaration","assertInterfaceTypeAnnotation","assertIntersectionTypeAnnotation","assertMixedTypeAnnotation","assertEmptyTypeAnnotation","assertNullableTypeAnnotation","assertNumberLiteralTypeAnnotation","assertNumberTypeAnnotation","assertObjectTypeAnnotation","assertObjectTypeInternalSlot","assertObjectTypeCallProperty","assertObjectTypeIndexer","assertObjectTypeProperty","assertObjectTypeSpreadProperty","assertOpaqueType","assertQualifiedTypeIdentifier","assertStringLiteralTypeAnnotation","assertStringTypeAnnotation","assertSymbolTypeAnnotation","assertThisTypeAnnotation","assertTupleTypeAnnotation","assertTypeofTypeAnnotation","assertTypeAlias","assertTypeAnnotation","assertTypeCastExpression","assertTypeParameter","assertTypeParameterDeclaration","assertTypeParameterInstantiation","assertUnionTypeAnnotation","assertVariance","assertVoidTypeAnnotation","assertEnumDeclaration","assertEnumBooleanBody","assertEnumNumberBody","assertEnumStringBody","assertEnumSymbolBody","assertEnumBooleanMember","assertEnumNumberMember","assertEnumStringMember","assertEnumDefaultedMember","assertIndexedAccessType","assertOptionalIndexedAccessType","assertJSXAttribute","assertJSXClosingElement","assertJSXElement","assertJSXEmptyExpression","assertJSXExpressionContainer","assertJSXSpreadChild","assertJSXIdentifier","assertJSXMemberExpression","assertJSXNamespacedName","assertJSXOpeningElement","assertJSXSpreadAttribute","assertJSXText","assertJSXFragment","assertJSXOpeningFragment","assertJSXClosingFragment","assertNoop","assertPlaceholder","assertV8IntrinsicIdentifier","assertArgumentPlaceholder","assertBindExpression","assertDecorator","assertDoExpression","assertExportDefaultSpecifier","assertRecordExpression","assertTupleExpression","assertDecimalLiteral","assertModuleExpression","assertTopicReference","assertPipelineTopicExpression","assertPipelineBareFunction","assertPipelinePrimaryTopicReference","assertVoidPattern","assertTSParameterProperty","assertTSDeclareFunction","assertTSDeclareMethod","assertTSQualifiedName","assertTSCallSignatureLine truncated
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":[],"sources":["../../../src/ast-types/generated/index.ts"],"sourcesContent":["// NOTE: This file is autogenerated. Do not modify.\n// See packages/babel-types/scripts/generators/ast-types.ts for script used.\n\ninterface BaseComment {\n value: string;\n start?: number;\n end?: number;\n loc?: SourceLocation;\n // generator will skip the comment if ignore is true\n ignore?: boolean;\n type: \"CommentBlock\" | \"CommentLine\";\n}\n\ninterface Position {\n line: number;\n column: number;\n index: number;\n}\n\nexport interface CommentBlock extends BaseComment {\n type: \"CommentBlock\";\n}\n\nexport interface CommentLine extends BaseComment {\n type: \"CommentLine\";\n}\n\nexport type Comment = CommentBlock | CommentLine;\n\nexport interface SourceLocation {\n start: Position;\n end: Position;\n filename: string;\n identifierName: string | undefined | null;\n}\n\ninterface BaseNode {\n leadingComments?: Comment[] | null;\n innerComments?: Comment[] | null;\n trailingComments?: Comment[] | null;\n start?: number | null;\n end?: number | null;\n loc?: SourceLocation | null;\n range?: [number, number];\n extra?: Record<string, unknown>;\n}\n\nexport type CommentTypeShorthand = \"leading\" | \"inner\" | \"trailing\";\n\nexport type Node =\n | AnyTypeAnnotation\n | ArgumentPlaceholder\n | ArrayExpression\n | ArrayPattern\n | ArrayTypeAnnotation\n | ArrowFunctionExpression\n | AssignmentExpression\n | AssignmentPattern\n | AwaitExpression\n | BigIntLiteral\n | BinaryExpression\n | BindExpression\n | BlockStatement\n | BooleanLiteral\n | BooleanLiteralTypeAnnotation\n | BooleanTypeAnnotation\n | BreakStatement\n | CallExpression\n | CatchClause\n | ClassAccessorProperty\n | ClassBody\n | ClassDeclaration\n | ClassExpression\n | ClassImplements\n | ClassMethod\n | ClassPrivateMethod\n | ClassPrivateProperty\n | ClassProperty\n | ConditionalExpression\n | ContinueStatement\n | DebuggerStatement\n | DecimalLiteral\n | DeclareClass\n | DeclareExportAllDeclaration\n | DeclareExportDeclaration\n | DeclareFunction\n | DeclareInterface\n | DeclareModule\n | DeclareModuleExports\n | DeclareOpaqueType\n | DeclareTypeAlias\n | DeclareVariable\n | DeclaredPredicate\n | Decorator\n | Directive\n | DirectiveLiteral\n | DoExpression\n | DoWhileStatement\n | EmptyStatement\n | EmptyTypeAnnotation\n | EnumBooleanBody\n | EnumBooleanMember\n | EnumDeclaration\n | EnumDefaultedMember\n | EnumNumberBody\n | EnumNumberMember\n | EnumStringBody\n | EnumStringMember\n | EnumSymbolBody\n | ExistsTypeAnnotation\n | ExportAllDeclaration\n | ExportDefaultDeclaration\n | ExportDefaultSpecifier\n | ExportNamedDeclaration\n | ExportNamespaceSpecifier\n | ExportSpecifier\n | ExpressionStatement\n | File\n | ForInStatement\n | ForOfStatement\n | ForStatement\n | FunctionDeclaration\n | FunctionExpression\n | FunctionTypeAnnotation\n | FunctionTypeParam\n | GenericTypeAnnotation\n | Identifier\n | IfStatement\n | Import\n | ImportAttribute\n | ImportDeclaration\n | ImportDefaultSpecifier\n | ImportExpression\n | ImportNamespaceSpecifier\n | ImportSpecifier\n | IndexedAccessType\n | InferredPredicate\n | InterfaceDeclaration\n | InterfaceExtends\n | InterfaceTypeAnnotation\n | InterpreterDirective\n | IntersectionTypeAnnotation\n | JSXAttribute\n | JSXClosingElement\n | JSXClosingFragment\n | JSXElement\n | JSXEmptyExpression\n | JSXExpressionContainer\n | JSXFragment\n | JSXIdentifier\n | JSXMemberExpression\n | JSXNamespacedName\n | JSXOpeningElement\n | JSXOpeningFragment\n | JSXSpreadAttribute\n | JSXSpreadChild\n | JSXText\n | LabeledStatement\n | LogicalExpression\n | MemberExpression\n | MetaProperty\n | MixedTypeAnnotation\n | ModuleExpression\n | NewExpression\n | Noop\n | NullLiteral\n | NullLiteralTypeAnnotation\n | NullableTypeAnnotation\n | NumberLiteral\n | NumberLiteralTypeAnnotation\n | NumberTypeAnnotation\n | NumericLiteral\n | ObjectExpression\n | ObjectMethod\n | ObjectPattern\n | ObjectProperty\n | ObjectTypeAnnotation\n | ObjectTypeCallProperty\n | ObjectTypeIndexer\n | ObjectTypeInternalSlot\n | ObjectTypeProperty\n | ObjectTypeSpreadProperty\n | OpaqueType\n | OptionalCallExpression\n | OptionalIndexedAccessType\n | OptionalMemberExpression\n | ParenthesizedExpression\n | PipelineBareFunction\n | PipelinePrimaryTopicReference\n | PipelineTopicExpression\n | Placeholder\n | PrivateName\n | Program\n | QualifiedTypeIdentifier\n | RecordExpression\n | RegExpLiteral\n | RegexLiteral\n | RestElement\n | RestProperty\n | ReturnStatement\n | SequenceExpression\n | SpreadElement\n | SpreadProperty\n | StaticBlock\n | StringLiteral\n | StringLiteralTypeAnnotation\n | StringTypeAnnotation\n | Super\n | SwitchCase\n | SwitchStatement\n | SymbolTypeAnnotation\n | TSAnyKeyword\n | TSArrayType\n | TSAsExpression\n | TSBigIntKeyword\nLine truncated
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = createFlowUnionType;
|
||||
var _index = require("../generated/index.js");
|
||||
var _removeTypeDuplicates = require("../../modifications/flow/removeTypeDuplicates.js");
|
||||
function createFlowUnionType(types) {
|
||||
const flattened = (0, _removeTypeDuplicates.default)(types);
|
||||
if (flattened.length === 1) {
|
||||
return flattened[0];
|
||||
} else {
|
||||
return (0, _index.unionTypeAnnotation)(flattened);
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=createFlowUnionType.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["_index","require","_removeTypeDuplicates","createFlowUnionType","types","flattened","removeTypeDuplicates","length","unionTypeAnnotation"],"sources":["../../../src/builders/flow/createFlowUnionType.ts"],"sourcesContent":["import { unionTypeAnnotation } from \"../generated/index.ts\";\nimport removeTypeDuplicates from \"../../modifications/flow/removeTypeDuplicates.ts\";\nimport type * as t from \"../../index.ts\";\n\n/**\n * Takes an array of `types` and flattens them, removing duplicates and\n * returns a `UnionTypeAnnotation` node containing them.\n */\nexport default function createFlowUnionType<T extends t.FlowType>(\n types: [T] | T[],\n): T | t.UnionTypeAnnotation {\n const flattened = removeTypeDuplicates(types);\n\n if (flattened.length === 1) {\n return flattened[0] as T;\n } else {\n return unionTypeAnnotation(flattened);\n }\n}\n"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,qBAAA,GAAAD,OAAA;AAOe,SAASE,mBAAmBA,CACzCC,KAAgB,EACW;EAC3B,MAAMC,SAAS,GAAG,IAAAC,6BAAoB,EAACF,KAAK,CAAC;EAE7C,IAAIC,SAAS,CAACE,MAAM,KAAK,CAAC,EAAE;IAC1B,OAAOF,SAAS,CAAC,CAAC,CAAC;EACrB,CAAC,MAAM;IACL,OAAO,IAAAG,0BAAmB,EAACH,SAAS,CAAC;EACvC;AACF","ignoreList":[]}
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = void 0;
|
||||
var _index = require("../generated/index.js");
|
||||
var _default = exports.default = createTypeAnnotationBasedOnTypeof;
|
||||
function createTypeAnnotationBasedOnTypeof(type) {
|
||||
switch (type) {
|
||||
case "string":
|
||||
return (0, _index.stringTypeAnnotation)();
|
||||
case "number":
|
||||
return (0, _index.numberTypeAnnotation)();
|
||||
case "undefined":
|
||||
return (0, _index.voidTypeAnnotation)();
|
||||
case "boolean":
|
||||
return (0, _index.booleanTypeAnnotation)();
|
||||
case "function":
|
||||
return (0, _index.genericTypeAnnotation)((0, _index.identifier)("Function"));
|
||||
case "object":
|
||||
return (0, _index.genericTypeAnnotation)((0, _index.identifier)("Object"));
|
||||
case "symbol":
|
||||
return (0, _index.genericTypeAnnotation)((0, _index.identifier)("Symbol"));
|
||||
case "bigint":
|
||||
return (0, _index.anyTypeAnnotation)();
|
||||
}
|
||||
throw new Error("Invalid typeof value: " + type);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=createTypeAnnotationBasedOnTypeof.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["_index","require","_default","exports","default","createTypeAnnotationBasedOnTypeof","type","stringTypeAnnotation","numberTypeAnnotation","voidTypeAnnotation","booleanTypeAnnotation","genericTypeAnnotation","identifier","anyTypeAnnotation","Error"],"sources":["../../../src/builders/flow/createTypeAnnotationBasedOnTypeof.ts"],"sourcesContent":["import {\n anyTypeAnnotation,\n stringTypeAnnotation,\n numberTypeAnnotation,\n voidTypeAnnotation,\n booleanTypeAnnotation,\n genericTypeAnnotation,\n identifier,\n} from \"../generated/index.ts\";\nimport type * as t from \"../../index.ts\";\n\nexport default createTypeAnnotationBasedOnTypeof as {\n (type: \"string\"): t.StringTypeAnnotation;\n (type: \"number\"): t.NumberTypeAnnotation;\n (type: \"undefined\"): t.VoidTypeAnnotation;\n (type: \"boolean\"): t.BooleanTypeAnnotation;\n (type: \"function\"): t.GenericTypeAnnotation;\n (type: \"object\"): t.GenericTypeAnnotation;\n (type: \"symbol\"): t.GenericTypeAnnotation;\n (type: \"bigint\"): t.AnyTypeAnnotation;\n};\n\n/**\n * Create a type annotation based on typeof expression.\n */\nfunction createTypeAnnotationBasedOnTypeof(type: string): t.FlowType {\n switch (type) {\n case \"string\":\n return stringTypeAnnotation();\n case \"number\":\n return numberTypeAnnotation();\n case \"undefined\":\n return voidTypeAnnotation();\n case \"boolean\":\n return booleanTypeAnnotation();\n case \"function\":\n return genericTypeAnnotation(identifier(\"Function\"));\n case \"object\":\n return genericTypeAnnotation(identifier(\"Object\"));\n case \"symbol\":\n return genericTypeAnnotation(identifier(\"Symbol\"));\n case \"bigint\":\n // todo: use BigInt annotation when Flow supports BigInt\n // https://github.com/facebook/flow/issues/6639\n return anyTypeAnnotation();\n }\n throw new Error(\"Invalid typeof value: \" + type);\n}\n"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAQ+B,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAGhBC,iCAAiC;AAchD,SAASA,iCAAiCA,CAACC,IAAY,EAAc;EACnE,QAAQA,IAAI;IACV,KAAK,QAAQ;MACX,OAAO,IAAAC,2BAAoB,EAAC,CAAC;IAC/B,KAAK,QAAQ;MACX,OAAO,IAAAC,2BAAoB,EAAC,CAAC;IAC/B,KAAK,WAAW;MACd,OAAO,IAAAC,yBAAkB,EAAC,CAAC;IAC7B,KAAK,SAAS;MACZ,OAAO,IAAAC,4BAAqB,EAAC,CAAC;IAChC,KAAK,UAAU;MACb,OAAO,IAAAC,4BAAqB,EAAC,IAAAC,iBAAU,EAAC,UAAU,CAAC,CAAC;IACtD,KAAK,QAAQ;MACX,OAAO,IAAAD,4BAAqB,EAAC,IAAAC,iBAAU,EAAC,QAAQ,CAAC,CAAC;IACpD,KAAK,QAAQ;MACX,OAAO,IAAAD,4BAAqB,EAAC,IAAAC,iBAAU,EAAC,QAAQ,CAAC,CAAC;IACpD,KAAK,QAAQ;MAGX,OAAO,IAAAC,wBAAiB,EAAC,CAAC;EAC9B;EACA,MAAM,IAAIC,KAAK,CAAC,wBAAwB,GAAGR,IAAI,CAAC;AAClD","ignoreList":[]}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _lowercase = require("./lowercase.js");
|
||||
Object.keys(_lowercase).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (key in exports && exports[key] === _lowercase[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _lowercase[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
var _uppercase = require("./uppercase.js");
|
||||
Object.keys(_uppercase).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (key in exports && exports[key] === _uppercase[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _uppercase[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["_lowercase","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get","_uppercase"],"sources":["../../../src/builders/generated/index.ts"],"sourcesContent":["/*\n * This file is auto-generated! Do not modify it directly.\n * To re-generate run 'make build'\n */\n\nexport * from \"./lowercase.ts\";\nexport * from \"./uppercase.ts\";\n\n// Uppercase builders and AST types conflict with each other, which is\n// not allowed by TypeScript when using `export * from ...`\n// We instead explicity list the AST types here, so that:\n// - From a TypeScript perspective, the AST types win over the uppercase\n// builders (which is the standard behavior for JS when a named\n// re-export conflicts with a * re-export.)\n// - At runtime, this `export type` is removed, leaving only the uppercase\n// builders behind (which are thus visible to JavaScript code).\n// This ensures compatibility with legacy code that uses the uppercase\n// builders, while allowing TypeScript users to use the lowercase builders\n// together with the AST types.\n\n// prettier-ignore\nexport type {\n ArrayExpression, AssignmentExpression, BinaryExpression, InterpreterDirective, Directive, DirectiveLiteral, BlockStatement, BreakStatement, CallExpression, CatchClause, ConditionalExpression, ContinueStatement, DebuggerStatement, DoWhileStatement, EmptyStatement, ExpressionStatement, File, ForInStatement, ForStatement, FunctionDeclaration, FunctionExpression, Identifier, IfStatement, LabeledStatement, StringLiteral, NumericLiteral, NullLiteral, BooleanLiteral, RegExpLiteral, LogicalExpression, MemberExpression, NewExpression, Program, ObjectExpression, ObjectMethod, ObjectProperty, RestElement, ReturnStatement, SequenceExpression, ParenthesizedExpression, SwitchCase, SwitchStatement, ThisExpression, ThrowStatement, TryStatement, UnaryExpression, UpdateExpression, VariableDeclaration, VariableDeclarator, WhileStatement, WithStatement, AssignmentPattern, ArrayPattern, ArrowFunctionExpression, ClassBody, ClassExpression, ClassDeclaration, ExportAllDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ForOfStatement, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportExpression, MetaProperty, ClassMethod, ObjectPattern, SpreadElement, Super, TaggedTemplateExpression, TemplateElement, TemplateLiteral, YieldExpression, AwaitExpression, Import, BigIntLiteral, ExportNamespaceSpecifier, OptionalMemberExpression, OptionalCallExpression, ClassProperty, ClassAccessorProperty, ClassPrivateProperty, ClassPrivateMethod, PrivateName, StaticBlock, ImportAttribute, AnyTypeAnnotation, ArrayTypeAnnotation, BooleanTypeAnnotation, BooleanLiteralTypeAnnotation, NullLiteralTypeAnnotation, ClassImplements, DeclareClass, DeclareFunction, DeclareInterface, DeclareModule, DeclareModuleExports, DeclareTypeAlias, DeclareOpaqueType, DeclareVariable, DeclareExportDeclaration, DeclareExportAllDeclaration, DeclaredPredicate, ExistsTypeAnnotation, FunctionTypeAnnotation, FunctionTypeParam, GenericTypeAnnotation, InferredPredicate, InterfaceExtends, InterfaceDeclaration, InterfaceTypeAnnotation, IntersectionTypeAnnotation, MixedTypeAnnotation, EmptyTypeAnnotation, NullableTypeAnnotation, NumberLiteralTypeAnnotation, NumberTypeAnnotation, ObjectTypeAnnotation, ObjectTypeInternalSlot, ObjectTypeCallProperty, ObjectTypeIndexer, ObjectTypeProperty, ObjectTypeSpreadProperty, OpaqueType, QualifiedTypeIdentifier, StringLiteralTypeAnnotation, StringTypeAnnotation, SymbolTypeAnnotation, ThisTypeAnnotation, TupleTypeAnnotation, TypeofTypeAnnotation, TypeAlias, TypeAnnotation, TypeCastExpression, TypeParameter, TypeParameterDeclaration, TypeParameterInstantiation, UnionTypeAnnotation, Variance, VoidTypeAnnotation, EnumDeclaration, EnumBooleanBody, EnumNumberBody, EnumStringBody, EnumSymbolBody, EnumBooleanMember, EnumNumberMember, EnumStringMember, EnumDefaultedMember, IndexedAccessType, OptionalIndexedAccessType, JSXAttribute, JSXClosingElement, JSXElement, JSXEmptyExpression, JSXExpressionContainer, JSXSpreadChild, JSXIdentifier, JSXMemberExpression, JSXNamespacedName, JSXOpeningElement, JSXSpreadAttribute, JSXText, JSXFragment, JSXOpeningFragment, JSXClosingFragment, Noop, Placeholder, V8IntrinsicIdentifier, ArgumentPlaceholder, BindExpression, Decorator, DoExpression, ExportDefaultSpecifier, RecordExpression, TupleExpression, DecimalLiteral, ModuleExpression, TopicReference, PipelineTopicExpression, PipelineBareFunction, PipelinePrimaryTopicReference, VoidPattern, TSParameterProperty, TSDeclareFunction, TSDeclareMethod, TSQualifiedName, TSCallSignatureDeclaration, TSConstructSignatureDeclaration, TSPropertySignature, TSMethodSignature, TSIndexSignature, TSAnyKeyword, TSBooleanKeyword, TSBigIntKeyword, TSIntrinsicKeyword, TSNeverKeyword, TSNullKeyword, TSNumberKeyword, TSObjectKeyword, TSStringKeyword, TSSymbolKeyword, TSUndefinedKeyword, TSUnknownKeyword, TSVoidKeyword, TLine truncated
|
||||
+2902
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["_validate","require","_deprecationWarning","utils","validateInternal","validate","NODE_FIELDS","bigIntLiteral","value","toString","node","type","defs","BigIntLiteral","arrayExpression","elements","ArrayExpression","assignmentExpression","operator","left","right","AssignmentExpression","binaryExpression","BinaryExpression","interpreterDirective","InterpreterDirective","directive","Directive","directiveLiteral","DirectiveLiteral","blockStatement","body","directives","BlockStatement","breakStatement","label","BreakStatement","callExpression","callee","_arguments","arguments","CallExpression","catchClause","param","CatchClause","conditionalExpression","test","consequent","alternate","ConditionalExpression","continueStatement","ContinueStatement","debuggerStatement","doWhileStatement","DoWhileStatement","emptyStatement","expressionStatement","expression","ExpressionStatement","file","program","comments","tokens","File","forInStatement","ForInStatement","forStatement","init","update","ForStatement","functionDeclaration","id","params","generator","async","FunctionDeclaration","functionExpression","FunctionExpression","identifier","name","Identifier","ifStatement","IfStatement","labeledStatement","LabeledStatement","stringLiteral","StringLiteral","numericLiteral","NumericLiteral","nullLiteral","booleanLiteral","BooleanLiteral","regExpLiteral","pattern","flags","RegExpLiteral","logicalExpression","LogicalExpression","memberExpression","object","property","computed","optional","MemberExpression","newExpression","NewExpression","sourceType","interpreter","Program","objectExpression","properties","ObjectExpression","objectMethod","kind","key","ObjectMethod","objectProperty","shorthand","decorators","ObjectProperty","restElement","argument","RestElement","returnStatement","ReturnStatement","sequenceExpression","expressions","SequenceExpression","parenthesizedExpression","ParenthesizedExpression","switchCase","SwitchCase","switchStatement","discriminant","cases","SwitchStatement","thisExpression","throwStatement","ThrowStatement","tryStatement","block","handler","finalizer","TryStatement","unaryExpression","prefix","UnaryExpression","updateExpression","UpdateExpression","variableDeclaration","declarations","VariableDeclaration","variableDeclarator","VariableDeclarator","whileStatement","WhileStatement","withStatement","WithStatement","assignmentPattern","AssignmentPattern","arrayPattern","ArrayPattern","arrowFunctionExpression","ArrowFunctionExpression","classBody","ClassBody","classExpression","superClass","ClassExpression","classDeclaration","ClassDeclaration","exportAllDeclaration","source","attributes","ExportAllDeclaration","exportDefaultDeclaration","declaration","ExportDefaultDeclaration","exportNamedDeclaration","specifiers","ExportNamedDeclaration","exportSpecifier","local","exported","ExportSpecifier","forOfStatement","_await","await","ForOfStatement","importDeclaration","ImportDeclaration","importDefaultSpecifier","ImportDefaultSpecifier","importNamespaceSpecifier","ImportNamespaceSpecifier","importSpecifier","imported","ImportSpecifier","importExpression","options","ImportExpression","metaProperty","meta","MetaProperty","classMethod","_static","static","ClassMethod","objectPattern","ObjectPattern","spreadElement","SpreadElement","_super","taggedTemplateExpression","tag","quasi","TaggedTemplateExpression","templateElement","tail","TemplateElement","templateLiteral","quasis","TemplateLiteral","yieldExpression","delegate","YieldExpression","awaitExpression","AwaitExpression","_import","exportNamespaceSpecifier","ExportNamespaceSpecifier","optionalMemberExpression","OptionalMemberExpression","optionalCallExpression","OptionalCallExpression","classProperty","typeAnnotation","ClassProperty","classAccessorProperty","ClassAccessorProperty","classPrivateProperty","ClassPrivateProperty","classPrivateMethod","ClassPrivateMethod","privateName","PrivateName","staticBlock","StaticBlock","importAttribute","ImportAttribute","anyTypeAnnotation","arrayTypeAnnotation","elementType","ArrayTypeAnnotation","booleanTypeAnnotation","booleanLiteralTypeAnnotation","BooleanLiteralTypeAnnotation","nullLiteralTypeAnnotation","classImplements","typeParameters","ClassImplements","declareClass","_extends","extends","DeclareClass","declareFunction","DeclareFunction","declareInterface","DeclareInterface","declareModule","DeclareModule","declareModuleExports","DeclareModuleExports","declareTypeAlias","DeclareTypeAlias","declareOpaqueType","supertype","DeclareOpaqueType","declareVariable","DeclareVariable","declareExportDeclaration","DeclareExportDeclaration","declareExportAllDeclaration","DeclareExportAllDeclaration","declaredPredicate","DeclaredPredicate","existsTypeAnnotation","functionTypeAnnotation","rest","returnType","FunctionTypeAnnotation","functionTypeParam","FunctionTypeParam","genericTypeAnnotation","GenericTypeAnnotation","inferredPredicate","interfaceExtends","InterfaceExtends","interfaceDeclaration","InterLine truncated
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.JSXIdentifier = exports.JSXFragment = exports.JSXExpressionContainer = exports.JSXEmptyExpression = exports.JSXElement = exports.JSXClosingFragment = exports.JSXClosingElement = exports.JSXAttribute = exports.IntersectionTypeAnnotation = exports.InterpreterDirective = exports.InterfaceTypeAnnotation = exports.InterfaceExtends = exports.InterfaceDeclaration = exports.InferredPredicate = exports.IndexedAccessType = exports.ImportSpecifier = exports.ImportNamespaceSpecifier = exports.ImportExpression = exports.ImportDefaultSpecifier = exports.ImportDeclaration = exports.ImportAttribute = exports.Import = exports.IfStatement = exports.Identifier = exports.GenericTypeAnnotation = exports.FunctionTypeParam = exports.FunctionTypeAnnotation = exports.FunctionExpression = exports.FunctionDeclaration = exports.ForStatement = exports.ForOfStatement = exports.ForInStatement = exports.File = exports.ExpressionStatement = exports.ExportSpecifier = exports.ExportNamespaceSpecifier = exports.ExportNamedDeclaration = exports.ExportDefaultSpecifier = exports.ExportDefaultDeclaration = exports.ExportAllDeclaration = exports.ExistsTypeAnnotation = exports.EnumSymbolBody = exports.EnumStringMember = exports.EnumStringBody = exports.EnumNumberMember = exports.EnumNumberBody = exports.EnumDefaultedMember = exports.EnumDeclaration = exports.EnumBooleanMember = exports.EnumBooleanBody = exports.EmptyTypeAnnotation = exports.EmptyStatement = exports.DoWhileStatement = exports.DoExpression = exports.DirectiveLiteral = exports.Directive = exports.Decorator = exports.DeclaredPredicate = exports.DeclareVariable = exports.DeclareTypeAlias = exports.DeclareOpaqueType = exports.DeclareModuleExports = exports.DeclareModule = exports.DeclareInterface = exports.DeclareFunction = exports.DeclareExportDeclaration = exports.DeclareExportAllDeclaration = exports.DeclareClass = exports.DecimalLiteral = exports.DebuggerStatement = exports.ContinueStatement = exports.ConditionalExpression = exports.ClassProperty = exports.ClassPrivateProperty = exports.ClassPrivateMethod = exports.ClassMethod = exports.ClassImplements = exports.ClassExpression = exports.ClassDeclaration = exports.ClassBody = exports.ClassAccessorProperty = exports.CatchClause = exports.CallExpression = exports.BreakStatement = exports.BooleanTypeAnnotation = exports.BooleanLiteralTypeAnnotation = exports.BooleanLiteral = exports.BlockStatement = exports.BindExpression = exports.BinaryExpression = exports.BigIntLiteral = exports.AwaitExpression = exports.AssignmentPattern = exports.AssignmentExpression = exports.ArrowFunctionExpression = exports.ArrayTypeAnnotation = exports.ArrayPattern = exports.ArrayExpression = exports.ArgumentPlaceholder = exports.AnyTypeAnnotation = void 0;
|
||||
exports.TSNumberKeyword = exports.TSNullKeyword = exports.TSNonNullExpression = exports.TSNeverKeyword = exports.TSNamespaceExportDeclaration = exports.TSNamedTupleMember = exports.TSModuleDeclaration = exports.TSModuleBlock = exports.TSMethodSignature = exports.TSMappedType = exports.TSLiteralType = exports.TSIntrinsicKeyword = exports.TSIntersectionType = exports.TSInterfaceDeclaration = exports.TSInterfaceBody = exports.TSInstantiationExpression = exports.TSInferType = exports.TSIndexedAccessType = exports.TSIndexSignature = exports.TSImportType = exports.TSImportEqualsDeclaration = exports.TSFunctionType = exports.TSExternalModuleReference = exports.TSExpressionWithTypeArguments = exports.TSExportAssignment = exports.TSEnumMember = exports.TSEnumDeclaration = exports.TSEnumBody = exports.TSDeclareMethod = exports.TSDeclareFunction = exports.TSConstructorType = exports.TSConstructSignatureDeclaration = exports.TSConditionalType = exports.TSCallSignatureDeclaration = exports.TSBooleanKeyword = exports.TSBigIntKeyword = exports.TSAsExpression = exports.TSArrayType = exports.TSAnyKeyword = exports.SymbolTypeAnnotation = exports.SwitchStatement = exports.SwitchCase = exports.Super = exports.StringTypeAnnotation = exports.StringLiteralTypeAnnotation = exports.StringLiteral = exports.StaticBlock = exports.SpreadProperty = exports.SpreadElement = exports.SequenceExpression = exports.ReturnStatement = exports.RestProperty = exports.RestElement = exports.RegexLiteral = exports.RegExpLiteral = exports.RecordExpression = exports.QualifiedTypeIdentifier = exports.Program = exports.PrivateName = exports.Placeholder = exports.PipelineTopicExpression = exports.PipelinePrimaryTopicReference = exports.PipelineBareFunction = exports.ParenthesizedExpression = exports.OptionalMemberExpression = exports.OptionalIndexedAccessType = exports.OptionalCallExpression = exports.OpaqueType = exports.ObjectTypeSpreadProperty = exports.ObjectTypeProperty = exports.ObjectTypeInternalSlot = exports.ObjectTypeIndexer = exports.ObjectTypeCallProperty = exports.ObjectTypeAnnotation = exports.ObjectProperty = exports.ObjectPattern = exports.ObjectMethod = exports.ObjectExpression = exports.NumericLiteral = exports.NumberTypeAnnotation = exports.NumberLiteralTypeAnnotation = exports.NumberLiteral = exports.NullableTypeAnnotation = exports.NullLiteralTypeAnnotation = exports.NullLiteral = exports.Noop = exports.NewExpression = exports.ModuleExpression = exports.MixedTypeAnnotation = exports.MetaProperty = exports.MemberExpression = exports.LogicalExpression = exports.LabeledStatement = exports.JSXText = exports.JSXSpreadChild = exports.JSXSpreadAttribute = exports.JSXOpeningFragment = exports.JSXOpeningElement = exports.JSXNamespacedName = exports.JSXMemberExpression = void 0;
|
||||
exports.YieldExpression = exports.WithStatement = exports.WhileStatement = exports.VoidTypeAnnotation = exports.VoidPattern = exports.Variance = exports.VariableDeclarator = exports.VariableDeclaration = exports.V8IntrinsicIdentifier = exports.UpdateExpression = exports.UnionTypeAnnotation = exports.UnaryExpression = exports.TypeofTypeAnnotation = exports.TypeParameterInstantiation = exports.TypeParameterDeclaration = exports.TypeParameter = exports.TypeCastExpression = exports.TypeAnnotation = exports.TypeAlias = exports.TupleTypeAnnotation = exports.TupleExpression = exports.TryStatement = exports.TopicReference = exports.ThrowStatement = exports.ThisTypeAnnotation = exports.ThisExpression = exports.TemplateLiteral = exports.TemplateElement = exports.TaggedTemplateExpression = exports.TSVoidKeyword = exports.TSUnknownKeyword = exports.TSUnionType = exports.TSUndefinedKeyword = exports.TSTypeReference = exports.TSTypeQuery = exports.TSTypePredicate = exports.TSTypeParameterInstantiation = exports.TSTypeParameterDeclaration = exports.TSTypeParameter = exports.TSTypeOperator = exports.TSTypeLiteral = exports.TSTypeAssertion = exports.TSTypeAnnotation = exports.TSTypeAliasDeclaration = exports.TSTupleType = exports.TSThisType = exports.TSTemplateLiteralType = exports.TSSymbolKeyword = exports.TSStringKeyword = exports.TSSatisfiesExpression = exports.TSRestType = exports.TSQualifiedName = exports.TSPropertySignature = exports.TSParenthesizedType = exports.TSParameterProperty = exports.TSOptionalType = exports.TSObjectKeyword = void 0;
|
||||
var b = require("./lowercase.js");
|
||||
var _deprecationWarning = require("../../utils/deprecationWarning.js");
|
||||
function alias(lowercase) {
|
||||
return b[lowercase];
|
||||
}
|
||||
const ArrayExpression = exports.ArrayExpression = alias("arrayExpression"),
|
||||
AssignmentExpression = exports.AssignmentExpression = alias("assignmentExpression"),
|
||||
BinaryExpression = exports.BinaryExpression = alias("binaryExpression"),
|
||||
InterpreterDirective = exports.InterpreterDirective = alias("interpreterDirective"),
|
||||
Directive = exports.Directive = alias("directive"),
|
||||
DirectiveLiteral = exports.DirectiveLiteral = alias("directiveLiteral"),
|
||||
BlockStatement = exports.BlockStatement = alias("blockStatement"),
|
||||
BreakStatement = exports.BreakStatement = alias("breakStatement"),
|
||||
CallExpression = exports.CallExpression = alias("callExpression"),
|
||||
CatchClause = exports.CatchClause = alias("catchClause"),
|
||||
ConditionalExpression = exports.ConditionalExpression = alias("conditionalExpression"),
|
||||
ContinueStatement = exports.ContinueStatement = alias("continueStatement"),
|
||||
DebuggerStatement = exports.DebuggerStatement = alias("debuggerStatement"),
|
||||
DoWhileStatement = exports.DoWhileStatement = alias("doWhileStatement"),
|
||||
EmptyStatement = exports.EmptyStatement = alias("emptyStatement"),
|
||||
ExpressionStatement = exports.ExpressionStatement = alias("expressionStatement"),
|
||||
File = exports.File = alias("file"),
|
||||
ForInStatement = exports.ForInStatement = alias("forInStatement"),
|
||||
ForStatement = exports.ForStatement = alias("forStatement"),
|
||||
FunctionDeclaration = exports.FunctionDeclaration = alias("functionDeclaration"),
|
||||
FunctionExpression = exports.FunctionExpression = alias("functionExpression"),
|
||||
Identifier = exports.Identifier = alias("identifier"),
|
||||
IfStatement = exports.IfStatement = alias("ifStatement"),
|
||||
LabeledStatement = exports.LabeledStatement = alias("labeledStatement"),
|
||||
StringLiteral = exports.StringLiteral = alias("stringLiteral"),
|
||||
NumericLiteral = exports.NumericLiteral = alias("numericLiteral"),
|
||||
NullLiteral = exports.NullLiteral = alias("nullLiteral"),
|
||||
BooleanLiteral = exports.BooleanLiteral = alias("booleanLiteral"),
|
||||
RegExpLiteral = exports.RegExpLiteral = alias("regExpLiteral"),
|
||||
LogicalExpression = exports.LogicalExpression = alias("logicalExpression"),
|
||||
MemberExpression = exports.MemberExpression = alias("memberExpression"),
|
||||
NewExpression = exports.NewExpression = alias("newExpression"),
|
||||
Program = exports.Program = alias("program"),
|
||||
ObjectExpression = exports.ObjectExpression = alias("objectExpression"),
|
||||
ObjectMethod = exports.ObjectMethod = alias("objectMethod"),
|
||||
ObjectProperty = exports.ObjectProperty = alias("objectProperty"),
|
||||
RestElement = exports.RestElement = alias("restElement"),
|
||||
ReturnStatement = exports.ReturnStatement = alias("returnStatement"),
|
||||
SequenceExpression = exports.SequenceExpression = alias("sequenceExpression"),
|
||||
ParenthesizedExpression = exports.ParenthesizedExpression = alias("parenthesizedExpression"),
|
||||
SwitchCase = exports.SwitchCase = alias("switchCase"),
|
||||
SwitchStatement = exports.SwitchStatement = alias("switchStatement"),
|
||||
ThisExpression = exports.ThisExpression = alias("thisExpression"),
|
||||
ThrowStatement = exports.ThrowStatement = alias("throwStatement"),
|
||||
TryStatement = exports.TryStatement = alias("tryStatement"),
|
||||
UnaryExpression = exports.UnaryExpression = alias("unaryExpression"),
|
||||
UpdateExpression = exports.UpdateExpression = alias("updateExpression"),
|
||||
VariableDeclaration = exports.VariableDeclaration = alias("variableDeclaration"),
|
||||
VariableDeclarator = exports.VariableDeclarator = alias("variableDeclarator"),
|
||||
WhileStatement = exports.WhileStatement = alias("whileStatement"),
|
||||
WithStatement = exports.WithStatement = alias("withStatement"),
|
||||
AssignmentPattern = exports.AssignmentPattern = alias("assignmentPattern"),
|
||||
ArrayPattern = exports.ArrayPattern = alias("arrayPattern"),
|
||||
ArrowFunctionExpression = exports.ArrowFunctionExpression = alias("arrowFunctionExpression"),
|
||||
ClassBody = exports.ClassBody = alias("classBody"),
|
||||
ClassExpression = exports.ClassExpression = alias("classExpression"),
|
||||
ClassDeclaration = exports.ClassDeclaration = alias("classDeclaration"),
|
||||
ExportAllDeclaration = exports.ExportAllDeclaration = alias("exportAllDeclaration"),
|
||||
ExportDefaultDeclaration = exports.ExportDefaultDeclaration = alias("exportDefaultDeclaration"),
|
||||
ExportNamedDeclaration = exports.ExportNamedDeclaration = alias("exportNamedDeclaration"),
|
||||
ExportSpecifier = exports.ExportSpecifier = alias("exportSpecifier"),
|
||||
ForOfStatement = exports.ForOfStatement = alias("forOfStatement"),
|
||||
ImportDeclaration = exports.ImportDeclaration = alias("importDeclaration"),
|
||||
ImportDefaultSpecifier = exports.ImportDefaultSpecifier = alias("importDefaultSpecifier"),
|
||||
ImportNamespaceSpecifier = exports.ImportNamespaceSpecifier = alias("importNamespaceSpecifier"),
|
||||
ImportSpecifier = exports.ImportSpecifier = alias("importSpecifier"),
|
||||
ImportExpression = exports.ImportExpression = alias("importExpression"),
|
||||
MetaProperty = exports.MetaProperty = alias("metaProperty"),
|
||||
ClassMethod = exports.ClassMethod = alias("classMethod"),
|
||||
ObjectPattern = exports.ObjectPattern = alias("objectPattern"),
|
||||
SpreadElement = exports.SpreadElement = alias("spreadElement"),
|
||||
Super = exports.Super = alias("super"),
|
||||
TaggedTemplateExpression = exports.TaggedTemplateExpression = alias("taggedTemplateExpression"),
|
||||
TemplateElement = exports.TemplateElement = alias("templateElement"),
|
||||
TemplateLiteral = exports.TemplateLiteral = alias("templateLiteral"),
|
||||
YieldExpression = exports.YieldExpression = alias("yieldExpression"),
|
||||
AwaitExpression = exports.AwaitExpression = alias("awaitExpression"),
|
||||
Import = exports.Import = alias("import"),
|
||||
BigIntLiteral = exports.BigIntLiteral = alias("bigIntLiteral"),
|
||||
ExportNamespaceSpecifier = exports.ExportNamespaceSpecifier = alias("exportNamespaceSpecifier"),
|
||||
OptionalMemberExpression = exports.OptionalMemberExpression = alias("optionalMemberExpression"),
|
||||
OptionalCallExpression = exports.OptionalCallExpression = alias("optionalCallExpression"),
|
||||
ClassProperty = exports.ClassProperty = alias("classProperty"),
|
||||
ClassAccessorProperty = exports.ClassAccessorProperty = alias("classAccessorProperty"),
|
||||
ClassPrivateProperty = exports.ClassPrivateProperty = alias("classPrivateProperty"),
|
||||
ClassPrivateMethod = exports.ClassPrivateMethod = alias("classPrivateMethod"),
|
||||
PrivateName = exports.PrivateName = alias("privateName"),
|
||||
StaticBlock = exports.StaticBlock = alias("staticBlock"),
|
||||
ImportAttribute = exports.ImportAttribute = alias("importAttribute"),
|
||||
AnyTypeAnnotation = exports.AnyTypeAnnotation = alias("anyTypeAnnotation"),
|
||||
ArrayTypeAnnotation = exports.ArrayTypeAnnotation = alias("arrayTypeAnnotation"),
|
||||
BooleanTypeAnnotation = exports.BooleanTypeAnnotation = alias("booleanTypeAnnotation"),
|
||||
BooleanLiteralTypeAnnotation = exports.BooleanLiteralTypeAnnotation = alias("booleanLiteralTypeAnnotation"),
|
||||
NullLiteralTypeAnnotation = exports.NullLiteralTypeAnnotation = alias("nullLiteralTypeAnnotation"),
|
||||
ClassImplements = exports.ClassImplements = alias("classImplements"),
|
||||
DeclareClass = exports.DeclareClass = alias("declareClass"),
|
||||
DeclareFunction = exports.DeclareFunction = alias("declareFunction"),
|
||||
DeclareInterface = exports.DeclareInterface = alias("declareInterface"),
|
||||
DeclareModule = exports.DeclareModule = alias("declareModule"),
|
||||
DeclareModuleExports = exports.DeclareModuleExports = alias("declareModuleExports"),
|
||||
DeclareTypeAlias = exports.DeclareTypeAlias = alias("declareTypeAlias"),
|
||||
DeclareOpaqueType = exports.DeclareOpaqueType = alias("declareOpaqueType"),
|
||||
DeclareVariable = exports.DeclareVariable = alias("declareVariable"),
|
||||
DeclareExportDeclaration = exports.DeclareExportDeclaration = alias("declareExportDeclaration"),
|
||||
DeclareExportAllDeclaration = exports.DeclareExportAllDeclaration = alias("declareExportAllDeclaration"),
|
||||
DeclaredPredicate = exports.DeclaredPredicate = alias("declaredPredicate"),
|
||||
ExistsTypeAnnotation = exports.ExistsTypeAnnotation = alias("existsTypeAnnotation"),
|
||||
FunctionTypeAnnotation = exports.FunctionTypeAnnotation = alias("functionTypeAnnotation"),
|
||||
FunctionTypeParam = exports.FunctionTypeParam = alias("functionTypeParam"),
|
||||
GenericTypeAnnotation = exports.GenericTypeAnnotation = alias("genericTypeAnnotation"),
|
||||
InferredPredicate = exports.InferredPredicate = alias("inferredPredicate"),
|
||||
InterfaceExtends = exports.InterfaceExtends = alias("interfaceExtends"),
|
||||
InterfaceDeclaration = exports.InterfaceDeclaration = alias("interfaceDeclaration"),
|
||||
InterfaceTypeAnnotation = exports.InterfaceTypeAnnotation = alias("interfaceTypeAnnotation"),
|
||||
IntersectionTypeAnnotation = exports.IntersectionTypeAnnotation = alias("intersectionTypeAnnotation"),
|
||||
MixedTypeAnnotation = exports.MixedTypeAnnotation = alias("mixedTypeAnnotation"),
|
||||
EmptyTypeAnnotation = exports.EmptyTypeAnnotation = alias("emptyTypeAnnotation"),
|
||||
NullableTypeAnnotation = exports.NullableTypeAnnotation = alias("nullableTypeAnnotation"),
|
||||
NumberLiteralTypeAnnotation = exports.NumberLiteralTypeAnnotation = alias("numberLiteralTypeAnnotation"),
|
||||
NumberTypeAnnotation = exports.NumberTypeAnnotation = alias("numberTypeAnnotation"),
|
||||
ObjectTypeAnnotation = exports.ObjectTypeAnnotation = alias("objectTypeAnnotation"),
|
||||
ObjectTypeInternalSlot = exports.ObjectTypeInternalSlot = alias("objectTypeInternalSlot"),
|
||||
ObjectTypeCallProperty = exports.ObjectTypeCallProperty = alias("objectTypeCallProperty"),
|
||||
ObjectTypeIndexer = exports.ObjectTypeIndexer = alias("objectTypeIndexer"),
|
||||
ObjectTypeProperty = exports.ObjectTypeProperty = alias("objectTypeProperty"),
|
||||
ObjectTypeSpreadProperty = exports.ObjectTypeSpreadProperty = alias("objectTypeSpreadProperty"),
|
||||
OpaqueType = exports.OpaqueType = alias("opaqueType"),
|
||||
QualifiedTypeIdentifier = exports.QualifiedTypeIdentifier = alias("qualifiedTypeIdentifier"),
|
||||
StringLiteralTypeAnnotation = exports.StringLiteralTypeAnnotation = alias("stringLiteralTypeAnnotation"),
|
||||
StringTypeAnnotation = exports.StringTypeAnnotation = alias("stringTypeAnnotation"),
|
||||
SymbolTypeAnnotation = exports.SymbolTypeAnnotation = alias("symbolTypeAnnotation"),
|
||||
ThisTypeAnnotation = exports.ThisTypeAnnotation = alias("thisTypeAnnotation"),
|
||||
TupleTypeAnnotation = exports.TupleTypeAnnotation = alias("tupleTypeAnnotation"),
|
||||
TypeofTypeAnnotation = exports.TypeofTypeAnnotation = alias("typeofTypeAnnotation"),
|
||||
TypeAlias = exports.TypeAlias = alias("typeAlias"),
|
||||
TypeAnnotation = exports.TypeAnnotation = alias("typeAnnotation"),
|
||||
TypeCastExpression = exports.TypeCastExpression = alias("typeCastExpression"),
|
||||
TypeParameter = exports.TypeParameter = alias("typeParameter"),
|
||||
TypeParameterDeclaration = exports.TypeParameterDeclaration = alias("typeParameterDeclaration"),
|
||||
TypeParameterInstantiation = exports.TypeParameterInstantiation = alias("typeParameterInstantiation"),
|
||||
UnionTypeAnnotation = exports.UnionTypeAnnotation = alias("unionTypeAnnotation"),
|
||||
Variance = exports.Variance = alias("variance"),
|
||||
VoidTypeAnnotation = exports.VoidTypeAnnotation = alias("voidTypeAnnotation"),
|
||||
EnumDeclaration = exports.EnumDeclaration = alias("enumDeclaration"),
|
||||
EnumBooleanBody = exports.EnumBooleanBody = alias("enumBooleanBody"),
|
||||
EnumNumberBody = exports.EnumNumberBody = alias("enumNumberBody"),
|
||||
EnumStringBody = exports.EnumStringBody = alias("enumStringBody"),
|
||||
EnumSymbolBody = exports.EnumSymbolBody = alias("enumSymbolBody"),
|
||||
EnumBooleanMember = exports.EnumBooleanMember = alias("enumBooleanMember"),
|
||||
EnumNumberMember = exports.EnumNumberMember = alias("enumNumberMember"),
|
||||
EnumStringMember = exports.EnumStringMember = alias("enumStringMember"),
|
||||
EnumDefaultedMember = exports.EnumDefaultedMember = alias("enumDefaultedMember"),
|
||||
IndexedAccessType = exports.IndexedAccessType = alias("indexedAccessType"),
|
||||
OptionalIndexedAccessType = exports.OptionalIndexedAccessType = alias("optionalIndexedAccessType"),
|
||||
JSXAttribute = exports.JSXAttribute = alias("jsxAttribute"),
|
||||
JSXClosingElement = exports.JSXClosingElement = alias("jsxClosingElement"),
|
||||
JSXElement = exports.JSXElement = alias("jsxElement"),
|
||||
JSXEmptyExpression = exports.JSXEmptyExpression = alias("jsxEmptyExpression"),
|
||||
JSXExpressionContainer = exports.JSXExpressionContainer = alias("jsxExpressionContainer"),
|
||||
JSXSpreadChild = exports.JSXSpreadChild = alias("jsxSpreadChild"),
|
||||
JSXIdentifier = exports.JSXIdentifier = alias("jsxIdentifier"),
|
||||
JSXMemberExpression = exports.JSXMemberExpression = alias("jsxMemberExpression"),
|
||||
JSXNamespacedName = exports.JSXNamespacedName = alias("jsxNamespacedName"),
|
||||
JSXOpeningElement = exports.JSXOpeningElement = alias("jsxOpeningElement"),
|
||||
JSXSpreadAttribute = exports.JSXSpreadAttribute = alias("jsxSpreadAttribute"),
|
||||
JSXText = exports.JSXText = alias("jsxText"),
|
||||
JSXFragment = exports.JSXFragment = alias("jsxFragment"),
|
||||
JSXOpeningFragment = exports.JSXOpeningFragment = alias("jsxOpeningFragment"),
|
||||
JSXClosingFragment = exports.JSXClosingFragment = alias("jsxClosingFragment"),
|
||||
Noop = exports.Noop = alias("noop"),
|
||||
Placeholder = exports.Placeholder = alias("placeholder"),
|
||||
V8IntrinsicIdentifier = exports.V8IntrinsicIdentifier = alias("v8IntrinsicIdentifier"),
|
||||
ArgumentPlaceholder = exports.ArgumentPlaceholder = alias("argumentPlaceholder"),
|
||||
BindExpression = exports.BindExpression = alias("bindExpression"),
|
||||
Decorator = exports.Decorator = alias("decorator"),
|
||||
DoExpression = exports.DoExpression = alias("doExpression"),
|
||||
ExportDefaultSpecifier = exports.ExportDefaultSpecifier = alias("exportDefaultSpecifier"),
|
||||
RecordExpression = exports.RecordExpression = alias("recordExpression"),
|
||||
TupleExpression = exports.TupleExpression = alias("tupleExpression"),
|
||||
DecimalLiteral = exports.DecimalLiteral = alias("decimalLiteral"),
|
||||
ModuleExpression = exports.ModuleExpression = alias("moduleExpression"),
|
||||
TopicReference = exports.TopicReference = alias("topicReference"),
|
||||
PipelineTopicExpression = exports.PipelineTopicExpression = alias("pipelineTopicExpression"),
|
||||
PipelineBareFunction = exports.PipelineBareFunction = alias("pipelineBareFunction"),
|
||||
PipelinePrimaryTopicReference = exports.PipelinePrimaryTopicReference = alias("pipelinePrimaryTopicReference"),
|
||||
VoidPattern = exports.VoidPattern = alias("voidPattern"),
|
||||
TSParameterProperty = exports.TSParameterProperty = alias("tsParameterProperty"),
|
||||
TSDeclareFunction = exports.TSDeclareFunction = alias("tsDeclareFunction"),
|
||||
TSDeclareMethod = exports.TSDeclareMethod = alias("tsDeclareMethod"),
|
||||
TSQualifiedName = exports.TSQualifiedName = alias("tsQualifiedName"),
|
||||
TSCallSignatureDeclaration = exports.TSCallSignatureDeclaration = alias("tsCallSignatureDeclaration"),
|
||||
TSConstructSignatureDeclaration = exports.TSConstructSignatureDeclaration = alias("tsConstructSignatureDeclaration"),
|
||||
TSPropertySignature = exports.TSPropertySignature = alias("tsPropertySignature"),
|
||||
TSMethodSignature = exports.TSMethodSignature = alias("tsMethodSignature"),
|
||||
TSIndexSignature = exports.TSIndexSignature = alias("tsIndexSignature"),
|
||||
TSAnyKeyword = exports.TSAnyKeyword = alias("tsAnyKeyword"),
|
||||
TSBooleanKeyword = exports.TSBooleanKeyword = alias("tsBooleanKeyword"),
|
||||
TSBigIntKeyword = exports.TSBigIntKeyword = alias("tsBigIntKeyword"),
|
||||
TSIntrinsicKeyword = exports.TSIntrinsicKeyword = alias("tsIntrinsicKeyword"),
|
||||
TSNeverKeyword = exports.TSNeverKeyword = alias("tsNeverKeyword"),
|
||||
TSNullKeyword = exports.TSNullKeyword = alias("tsNullKeyword"),
|
||||
TSNumberKeyword = exports.TSNumberKeyword = alias("tsNumberKeyword"),
|
||||
TSObjectKeyword = exports.TSObjectKeyword = alias("tsObjectKeyword"),
|
||||
TSStringKeyword = exports.TSStringKeyword = alias("tsStringKeyword"),
|
||||
TSSymbolKeyword = exports.TSSymbolKeyword = alias("tsSymbolKeyword"),
|
||||
TSUndefinedKeyword = exports.TSUndefinedKeyword = alias("tsUndefinedKeyword"),
|
||||
TSUnknownKeyword = exports.TSUnknownKeyword = alias("tsUnknownKeyword"),
|
||||
TSVoidKeyword = exports.TSVoidKeyword = alias("tsVoidKeyword"),
|
||||
TSThisType = exports.TSThisType = alias("tsThisType"),
|
||||
TSFunctionType = exports.TSFunctionType = alias("tsFunctionType"),
|
||||
TSConstructorType = exports.TSConstructorType = alias("tsConstructorType"),
|
||||
TSTypeReference = exports.TSTypeReference = alias("tsTypeReference"),
|
||||
TSTypePredicate = exports.TSTypePredicate = alias("tsTypePredicate"),
|
||||
TSTypeQuery = exports.TSTypeQuery = alias("tsTypeQuery"),
|
||||
TSTypeLiteral = exports.TSTypeLiteral = alias("tsTypeLiteral"),
|
||||
TSArrayType = exports.TSArrayType = alias("tsArrayType"),
|
||||
TSTupleType = exports.TSTupleType = alias("tsTupleType"),
|
||||
TSOptionalType = exports.TSOptionalType = alias("tsOptionalType"),
|
||||
TSRestType = exports.TSRestType = alias("tsRestType"),
|
||||
TSNamedTupleMember = exports.TSNamedTupleMember = alias("tsNamedTupleMember"),
|
||||
TSUnionType = exports.TSUnionType = alias("tsUnionType"),
|
||||
TSIntersectionType = exports.TSIntersectionType = alias("tsIntersectionType"),
|
||||
TSConditionalType = exports.TSConditionalType = alias("tsConditionalType"),
|
||||
TSInferType = exports.TSInferType = alias("tsInferType"),
|
||||
TSParenthesizedType = exports.TSParenthesizedType = alias("tsParenthesizedType"),
|
||||
TSTypeOperator = exports.TSTypeOperator = alias("tsTypeOperator"),
|
||||
TSIndexedAccessType = exports.TSIndexedAccessType = alias("tsIndexedAccessType"),
|
||||
TSMappedType = exports.TSMappedType = alias("tsMappedType"),
|
||||
TSTemplateLiteralType = exports.TSTemplateLiteralType = alias("tsTemplateLiteralType"),
|
||||
TSLiteralType = exports.TSLiteralType = alias("tsLiteralType"),
|
||||
TSExpressionWithTypeArguments = exports.TSExpressionWithTypeArguments = alias("tsExpressionWithTypeArguments"),
|
||||
TSInterfaceDeclaration = exports.TSInterfaceDeclaration = alias("tsInterfaceDeclaration"),
|
||||
TSInterfaceBody = exports.TSInterfaceBody = alias("tsInterfaceBody"),
|
||||
TSTypeAliasDeclaration = exports.TSTypeAliasDeclaration = alias("tsTypeAliasDeclaration"),
|
||||
TSInstantiationExpression = exports.TSInstantiationExpression = alias("tsInstantiationExpression"),
|
||||
TSAsExpression = exports.TSAsExpression = alias("tsAsExpression"),
|
||||
TSSatisfiesExpression = exports.TSSatisfiesExpression = alias("tsSatisfiesExpression"),
|
||||
TSTypeAssertion = exports.TSTypeAssertion = alias("tsTypeAssertion"),
|
||||
TSEnumBody = exports.TSEnumBody = alias("tsEnumBody"),
|
||||
TSEnumDeclaration = exports.TSEnumDeclaration = alias("tsEnumDeclaration"),
|
||||
TSEnumMember = exports.TSEnumMember = alias("tsEnumMember"),
|
||||
TSModuleDeclaration = exports.TSModuleDeclaration = alias("tsModuleDeclaration"),
|
||||
TSModuleBlock = exports.TSModuleBlock = alias("tsModuleBlock"),
|
||||
TSImportType = exports.TSImportType = alias("tsImportType"),
|
||||
TSImportEqualsDeclaration = exports.TSImportEqualsDeclaration = alias("tsImportEqualsDeclaration"),
|
||||
TSExternalModuleReference = exports.TSExternalModuleReference = alias("tsExternalModuleReference"),
|
||||
TSNonNullExpression = exports.TSNonNullExpression = alias("tsNonNullExpression"),
|
||||
TSExportAssignment = exports.TSExportAssignment = alias("tsExportAssignment"),
|
||||
TSNamespaceExportDeclaration = exports.TSNamespaceExportDeclaration = alias("tsNamespaceExportDeclaration"),
|
||||
TSTypeAnnotation = exports.TSTypeAnnotation = alias("tsTypeAnnotation"),
|
||||
TSTypeParameterInstantiation = exports.TSTypeParameterInstantiation = alias("tsTypeParameterInstantiation"),
|
||||
TSTypeParameterDeclaration = exports.TSTypeParameterDeclaration = alias("tsTypeParameterDeclaration"),
|
||||
TSTypeParameter = exports.TSTypeParameter = alias("tsTypeParameter");
|
||||
const NumberLiteral = exports.NumberLiteral = b.numberLiteral,
|
||||
RegexLiteral = exports.RegexLiteral = b.regexLiteral,
|
||||
RestProperty = exports.RestProperty = b.restProperty,
|
||||
SpreadProperty = exports.SpreadProperty = b.spreadProperty;
|
||||
|
||||
//# sourceMappingURL=uppercase.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"names":["b","require","_deprecationWarning","alias","lowercase","ArrayExpression","exports","AssignmentExpression","BinaryExpression","InterpreterDirective","Directive","DirectiveLiteral","BlockStatement","BreakStatement","CallExpression","CatchClause","ConditionalExpression","ContinueStatement","DebuggerStatement","DoWhileStatement","EmptyStatement","ExpressionStatement","File","ForInStatement","ForStatement","FunctionDeclaration","FunctionExpression","Identifier","IfStatement","LabeledStatement","StringLiteral","NumericLiteral","NullLiteral","BooleanLiteral","RegExpLiteral","LogicalExpression","MemberExpression","NewExpression","Program","ObjectExpression","ObjectMethod","ObjectProperty","RestElement","ReturnStatement","SequenceExpression","ParenthesizedExpression","SwitchCase","SwitchStatement","ThisExpression","ThrowStatement","TryStatement","UnaryExpression","UpdateExpression","VariableDeclaration","VariableDeclarator","WhileStatement","WithStatement","AssignmentPattern","ArrayPattern","ArrowFunctionExpression","ClassBody","ClassExpression","ClassDeclaration","ExportAllDeclaration","ExportDefaultDeclaration","ExportNamedDeclaration","ExportSpecifier","ForOfStatement","ImportDeclaration","ImportDefaultSpecifier","ImportNamespaceSpecifier","ImportSpecifier","ImportExpression","MetaProperty","ClassMethod","ObjectPattern","SpreadElement","Super","TaggedTemplateExpression","TemplateElement","TemplateLiteral","YieldExpression","AwaitExpression","Import","BigIntLiteral","ExportNamespaceSpecifier","OptionalMemberExpression","OptionalCallExpression","ClassProperty","ClassAccessorProperty","ClassPrivateProperty","ClassPrivateMethod","PrivateName","StaticBlock","ImportAttribute","AnyTypeAnnotation","ArrayTypeAnnotation","BooleanTypeAnnotation","BooleanLiteralTypeAnnotation","NullLiteralTypeAnnotation","ClassImplements","DeclareClass","DeclareFunction","DeclareInterface","DeclareModule","DeclareModuleExports","DeclareTypeAlias","DeclareOpaqueType","DeclareVariable","DeclareExportDeclaration","DeclareExportAllDeclaration","DeclaredPredicate","ExistsTypeAnnotation","FunctionTypeAnnotation","FunctionTypeParam","GenericTypeAnnotation","InferredPredicate","InterfaceExtends","InterfaceDeclaration","InterfaceTypeAnnotation","IntersectionTypeAnnotation","MixedTypeAnnotation","EmptyTypeAnnotation","NullableTypeAnnotation","NumberLiteralTypeAnnotation","NumberTypeAnnotation","ObjectTypeAnnotation","ObjectTypeInternalSlot","ObjectTypeCallProperty","ObjectTypeIndexer","ObjectTypeProperty","ObjectTypeSpreadProperty","OpaqueType","QualifiedTypeIdentifier","StringLiteralTypeAnnotation","StringTypeAnnotation","SymbolTypeAnnotation","ThisTypeAnnotation","TupleTypeAnnotation","TypeofTypeAnnotation","TypeAlias","TypeAnnotation","TypeCastExpression","TypeParameter","TypeParameterDeclaration","TypeParameterInstantiation","UnionTypeAnnotation","Variance","VoidTypeAnnotation","EnumDeclaration","EnumBooleanBody","EnumNumberBody","EnumStringBody","EnumSymbolBody","EnumBooleanMember","EnumNumberMember","EnumStringMember","EnumDefaultedMember","IndexedAccessType","OptionalIndexedAccessType","JSXAttribute","JSXClosingElement","JSXElement","JSXEmptyExpression","JSXExpressionContainer","JSXSpreadChild","JSXIdentifier","JSXMemberExpression","JSXNamespacedName","JSXOpeningElement","JSXSpreadAttribute","JSXText","JSXFragment","JSXOpeningFragment","JSXClosingFragment","Noop","Placeholder","V8IntrinsicIdentifier","ArgumentPlaceholder","BindExpression","Decorator","DoExpression","ExportDefaultSpecifier","RecordExpression","TupleExpression","DecimalLiteral","ModuleExpression","TopicReference","PipelineTopicExpression","PipelineBareFunction","PipelinePrimaryTopicReference","VoidPattern","TSParameterProperty","TSDeclareFunction","TSDeclareMethod","TSQualifiedName","TSCallSignatureDeclaration","TSConstructSignatureDeclaration","TSPropertySignature","TSMethodSignature","TSIndexSignature","TSAnyKeyword","TSBooleanKeyword","TSBigIntKeyword","TSIntrinsicKeyword","TSNeverKeyword","TSNullKeyword","TSNumberKeyword","TSObjectKeyword","TSStringKeyword","TSSymbolKeyword","TSUndefinedKeyword","TSUnknownKeyword","TSVoidKeyword","TSThisType","TSFunctionType","TSConstructorType","TSTypeReference","TSTypePredicate","TSTypeQuery","TSTypeLiteral","TSArrayType","TSTupleType","TSOptionalType","TSRestType","TSNamedTupleMember","TSUnionType","TSIntersectionType","TSConditionalType","TSInferType","TSParenthesizedType","TSTypeOperator","TSIndexedAccessType","TSMappedType","TSTemplateLiteralType","TSLiteralType","TSExpressionWithTypeArguments","TSInterfaceDeclaration","TSInterfaceBody","TSTypeAliasDeclaration","TSInstantiationExpression","TSAsExpression","TSSatisfiesExpression","TSTypeAssertion","TSEnumBody","TSEnumDeclaration","TSEnumMember","TSModuleDeclaration","TSModuleBlock","TSImportType","TSImportEqualsDeclaration","TSExternalModuleReference","TSNonNullExpression","TSExportAssignment","TSNamespaceExportDeclaration","TSTypeAnnotation","TSTypeParameterInstaLine truncated
|
||||
Loaded 100 of 851 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user