init
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@@ -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
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
storage/
|
||||
.pytest_cache/
|
||||
26
Makefile
Normal file
26
Makefile
Normal file
@@ -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
|
||||
331
README.md
Normal file
331
README.md
Normal file
@@ -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)?
|
||||
8
backend/__main__.py
Normal file
8
backend/__main__.py
Normal file
@@ -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()
|
||||
564
backend/agents.py
Normal file
564
backend/agents.py
Normal file
@@ -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()
|
||||
101
backend/artefakte.py
Normal file
101
backend/artefakte.py
Normal file
@@ -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")
|
||||
96
backend/chunker.py
Normal file
96
backend/chunker.py
Normal file
@@ -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
backend/cli.py
Normal file
119
backend/cli.py
Normal file
@@ -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()
|
||||
72
backend/config.py
Normal file
72
backend/config.py
Normal file
@@ -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
|
||||
327
backend/database.py
Normal file
327
backend/database.py
Normal file
@@ -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])
|
||||
155
backend/fake_agents.py
Normal file
155
backend/fake_agents.py
Normal file
@@ -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)
|
||||
310
backend/gates.py
Normal file
310
backend/gates.py
Normal file
@@ -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
|
||||
269
backend/kanban.py
Normal file
269
backend/kanban.py
Normal file
@@ -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)
|
||||
36
backend/projekte.py
Normal file
36
backend/projekte.py
Normal file
@@ -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
|
||||
3
backend/pytest.ini
Normal file
3
backend/pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
testpaths = tests
|
||||
53
backend/repo.py
Normal file
53
backend/repo.py
Normal file
@@ -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))
|
||||
108
backend/routes.py
Normal file
108
backend/routes.py
Normal file
@@ -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
|
||||
319
backend/scan_board.py
Normal file
319
backend/scan_board.py
Normal file
@@ -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]}
|
||||
147
backend/scan_dienst.py
Normal file
147
backend/scan_dienst.py
Normal file
@@ -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"]}
|
||||
48
backend/server.py
Normal file
48
backend/server.py
Normal file
@@ -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")
|
||||
53
backend/templates/einheiten.md
Normal file
53
backend/templates/einheiten.md
Normal file
@@ -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>
|
||||
```
|
||||
33
backend/templates/nachfass.md
Normal file
33
backend/templates/nachfass.md
Normal file
@@ -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>
|
||||
```
|
||||
29
backend/templates/sichten-architektur.md
Normal file
29
backend/templates/sichten-architektur.md
Normal file
@@ -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>
|
||||
51
backend/templates/sichten-features.md
Normal file
51
backend/templates/sichten-features.md
Normal file
@@ -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>
|
||||
26
backend/templates/sichten-flows.md
Normal file
26
backend/templates/sichten-flows.md
Normal file
@@ -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>
|
||||
66
backend/tests/conftest.py
Normal file
66
backend/tests/conftest.py
Normal file
@@ -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
backend/tests/fixtures/mini_repo/anzeige.py
vendored
Normal file
7
backend/tests/fixtures/mini_repo/anzeige.py
vendored
Normal file
@@ -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
backend/tests/fixtures/mini_repo/rechner.py
vendored
Normal file
23
backend/tests/fixtures/mini_repo/rechner.py
vendored
Normal file
@@ -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
|
||||
50
backend/tests/test_agents.py
Normal file
50
backend/tests/test_agents.py
Normal file
@@ -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)
|
||||
65
backend/tests/test_artefakte.py
Normal file
65
backend/tests/test_artefakte.py
Normal file
@@ -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
|
||||
57
backend/tests/test_chunker.py
Normal file
57
backend/tests/test_chunker.py
Normal file
@@ -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"}
|
||||
31
backend/tests/test_config_rollen.py
Normal file
31
backend/tests/test_config_rollen.py
Normal file
@@ -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"}
|
||||
33
backend/tests/test_database.py
Normal file
33
backend/tests/test_database.py
Normal file
@@ -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)
|
||||
159
backend/tests/test_gates.py
Normal file
159
backend/tests/test_gates.py
Normal file
@@ -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"]
|
||||
128
backend/tests/test_kanban.py
Normal file
128
backend/tests/test_kanban.py
Normal file
@@ -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
|
||||
32
backend/tests/test_modell_ids.py
Normal file
32
backend/tests/test_modell_ids.py
Normal file
@@ -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}"
|
||||
102
backend/tests/test_rescan.py
Normal file
102
backend/tests/test_rescan.py
Normal file
@@ -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)
|
||||
87
backend/tests/test_routes.py
Normal file
87
backend/tests/test_routes.py
Normal file
@@ -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"]
|
||||
119
backend/tests/test_scan_fake.py
Normal file
119
backend/tests/test_scan_fake.py
Normal file
@@ -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
|
||||
114
backend/tests/test_viewer.py
Normal file
114
backend/tests/test_viewer.py
Normal file
@@ -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
|
||||
279
backend/viewer.py
Normal file
279
backend/viewer.py
Normal file
@@ -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
|
||||
113
dev-ops/opencode-slim.json
Normal file
113
dev-ops/opencode-slim.json
Normal file
@@ -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
docs/format.md
Normal file
184
docs/format.md
Normal file
@@ -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
frontend/dist/assets/index-AxkPXHNh.css
vendored
Normal file
1
frontend/dist/assets/index-AxkPXHNh.css
vendored
Normal file
File diff suppressed because one or more lines are too long
64
frontend/dist/assets/index-DIiv29cS.js
vendored
Normal file
64
frontend/dist/assets/index-DIiv29cS.js
vendored
Normal file
File diff suppressed because one or more lines are too long
22
frontend/dist/index.html
vendored
Normal file
22
frontend/dist/index.html
vendored
Normal file
@@ -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>
|
||||
21
frontend/index.html
Normal file
21
frontend/index.html
Normal file
@@ -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
frontend/node_modules/.bin/marked
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/marked
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../marked/bin/marked.js
|
||||
1
frontend/node_modules/.bin/nanoid
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/nanoid
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../nanoid/bin/nanoid.cjs
|
||||
1
frontend/node_modules/.bin/parser
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/parser
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../@babel/parser/bin/babel-parser.js
|
||||
1
frontend/node_modules/.bin/rolldown
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/rolldown
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../rolldown/bin/cli.mjs
|
||||
1
frontend/node_modules/.bin/vite
generated
vendored
Symbolic link
1
frontend/node_modules/.bin/vite
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../vite/bin/vite.js
|
||||
559
frontend/node_modules/.package-lock.json
generated
vendored
Normal file
559
frontend/node_modules/.package-lock.json
generated
vendored
Normal file
@@ -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
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
Normal file
21
frontend/node_modules/.vite/deps/_metadata.json
generated
vendored
Normal file
@@ -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
frontend/node_modules/.vite/deps/marked.js
generated
vendored
Normal file
1610
frontend/node_modules/.vite/deps/marked.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/node_modules/.vite/deps/marked.js.map
generated
vendored
Normal file
1
frontend/node_modules/.vite/deps/marked.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
frontend/node_modules/.vite/deps/package.json
generated
vendored
Normal file
3
frontend/node_modules/.vite/deps/package.json
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
8654
frontend/node_modules/.vite/deps/vue.js
generated
vendored
Normal file
8654
frontend/node_modules/.vite/deps/vue.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/node_modules/.vite/deps/vue.js.map
generated
vendored
Normal file
1
frontend/node_modules/.vite/deps/vue.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
22
frontend/node_modules/@babel/helper-string-parser/LICENSE
generated
vendored
Normal file
22
frontend/node_modules/@babel/helper-string-parser/LICENSE
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-string-parser/README.md
generated
vendored
Normal file
19
frontend/node_modules/@babel/helper-string-parser/README.md
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-string-parser/lib/index.js
generated
vendored
Normal file
295
frontend/node_modules/@babel/helper-string-parser/lib/index.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-string-parser/lib/index.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/helper-string-parser/lib/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
31
frontend/node_modules/@babel/helper-string-parser/package.json
generated
vendored
Normal file
31
frontend/node_modules/@babel/helper-string-parser/package.json
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/LICENSE
generated
vendored
Normal file
22
frontend/node_modules/@babel/helper-validator-identifier/LICENSE
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/README.md
generated
vendored
Normal file
19
frontend/node_modules/@babel/helper-validator-identifier/README.md
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js
generated
vendored
Normal file
70
frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/helper-validator-identifier/lib/identifier.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
57
frontend/node_modules/@babel/helper-validator-identifier/lib/index.js
generated
vendored
Normal file
57
frontend/node_modules/@babel/helper-validator-identifier/lib/index.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/lib/index.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/helper-validator-identifier/lib/index.js.map
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js
generated
vendored
Normal file
35
frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/helper-validator-identifier/lib/keyword.js.map
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/helper-validator-identifier/package.json
generated
vendored
Normal file
31
frontend/node_modules/@babel/helper-validator-identifier/package.json
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/parser/CHANGELOG.md
generated
vendored
Normal file
1073
frontend/node_modules/@babel/parser/CHANGELOG.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
19
frontend/node_modules/@babel/parser/LICENSE
generated
vendored
Normal file
19
frontend/node_modules/@babel/parser/LICENSE
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/parser/README.md
generated
vendored
Normal file
19
frontend/node_modules/@babel/parser/README.md
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/parser/bin/babel-parser.js
generated
vendored
Executable file
15
frontend/node_modules/@babel/parser/bin/babel-parser.js
generated
vendored
Executable file
@@ -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
frontend/node_modules/@babel/parser/lib/index.js
generated
vendored
Normal file
14599
frontend/node_modules/@babel/parser/lib/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/node_modules/@babel/parser/lib/index.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/parser/lib/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
50
frontend/node_modules/@babel/parser/package.json
generated
vendored
Normal file
50
frontend/node_modules/@babel/parser/package.json
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/parser/typings/babel-parser.d.ts
generated
vendored
Normal file
262
frontend/node_modules/@babel/parser/typings/babel-parser.d.ts
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/LICENSE
generated
vendored
Normal file
22
frontend/node_modules/@babel/types/LICENSE
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/README.md
generated
vendored
Normal file
19
frontend/node_modules/@babel/types/README.md
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/lib/asserts/assertNode.js
generated
vendored
Normal file
16
frontend/node_modules/@babel/types/lib/asserts/assertNode.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/lib/asserts/assertNode.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/asserts/assertNode.js.map
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/lib/asserts/generated/index.js
generated
vendored
Normal file
1251
frontend/node_modules/@babel/types/lib/asserts/generated/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/node_modules/@babel/types/lib/asserts/generated/index.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/asserts/generated/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
frontend/node_modules/@babel/types/lib/ast-types/generated/index.js
generated
vendored
Normal file
3
frontend/node_modules/@babel/types/lib/ast-types/generated/index.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
frontend/node_modules/@babel/types/lib/ast-types/generated/index.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/ast-types/generated/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
18
frontend/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js
generated
vendored
Normal file
18
frontend/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js.map
generated
vendored
Normal file
@@ -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":[]}
|
||||
31
frontend/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js
generated
vendored
Normal file
31
frontend/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js
generated
vendored
Normal file
@@ -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
|
||||
1
frontend/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js.map
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/lib/builders/generated/index.js
generated
vendored
Normal file
29
frontend/node_modules/@babel/types/lib/builders/generated/index.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/lib/builders/generated/index.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/builders/generated/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2902
frontend/node_modules/@babel/types/lib/builders/generated/lowercase.js
generated
vendored
Normal file
2902
frontend/node_modules/@babel/types/lib/builders/generated/lowercase.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/node_modules/@babel/types/lib/builders/generated/lowercase.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/builders/generated/lowercase.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
272
frontend/node_modules/@babel/types/lib/builders/generated/uppercase.js
generated
vendored
Normal file
272
frontend/node_modules/@babel/types/lib/builders/generated/uppercase.js
generated
vendored
Normal file
@@ -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
frontend/node_modules/@babel/types/lib/builders/generated/uppercase.js.map
generated
vendored
Normal file
1
frontend/node_modules/@babel/types/lib/builders/generated/uppercase.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user