- TypeScript 90%
- JavaScript 9.8%
- CSS 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
QA / qa (push) Has been cancelled
Neue Aktion "verwerfen" an /api/prototype-plan: setzt das TODO ueber cancelTodo auf cancelled und schliesst die Planung. Erlaubt in offen, laeuft, fragen und vorgelegt; eine freigegebene Planung gibt 409. Im Reiter ein zweistufiger Knopf "Verwerfen". |
||
| .forgejo/workflows | ||
| bin | ||
| src | ||
| tooling | ||
| .gitignore | ||
| node_modules | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
@kryptance/prototyping
Die Prototyping-Schicht für Next.js-Apps: Shift+Klick auf dem Desktop,
Long-Press (500 ms halten) auf dem Handy auf irgendein Element
der Oberfläche legt ein TODO im Repo an; eine responsive Sidebar (vollbreites
Blatt auf dem Handy, 420px ab sm) zeigt TODOs, Commits,
Review-Zustand und Deploy; ein Release-Panel rollt per helm zurück. Alles
gemeinsam trägt eine zwei-Parteien-Disziplin: ohne fertigen Review ohne
Blocker geht nichts live — erzwungen in der Route, nicht nur im Knopf.
Die Schicht ist rein entwicklungsseitig: unter NODE_ENV=production rendert
keine Komponente etwas und jede Route antwortet 404.
Entstanden in drannamariabeck, seither hier zentral — beide Apps
(drannamariabeck, SpielerNet) binden sie als Git-Dependency ein, damit
Verbesserungen überall ankommen.
Einbau in eine App
pnpm add @kryptance/prototyping@git+https://git.kryptance.io/kryptance/prototyping.git#<sha>
# bzw. npm i @kryptance/prototyping@git+https://git.kryptance.io/kryptance/prototyping.git#<sha>
Das Paket hat keine Abhängigkeiten (nur peer: react, react-dom, optional
next) und liefert TypeScript-Quellen — die App braucht
transpilePackages: ["@kryptance/prototyping"] in ihrer Next-Konfiguration.
1. Konfiguration (eine Datei, z. B. src/lib/prototyping.ts)
import type { PrototypingConfig } from "@kryptance/prototyping";
export const prototypingConfig: PrototypingConfig = {
deploy: { mode: "push" }, // oder { mode: "script", command: "node", args: ["scripts/deploy.mjs"] }
release: { release: "meine-app" }, // helm; namespaceDefault "apps", kubeconfigDefault ~/.kube/dev3-config
tour: { file: "src/lib/onboarding-tours.json" }, // nur wenn die App Touren hat
};
2. Routen (dünn, jede Datei nur Re-Exports)
// src/app/api/prototype-todo/route.ts
import { createTodoRoutes } from "@kryptance/prototyping";
import { prototypingConfig } from "@/lib/prototyping";
export const { GET, POST } = createTodoRoutes(prototypingConfig);
Genauso: createClarifyRoute (prototype-todo/[id]/clarify),
createUploadsRoute (prototype-todo/uploads/[filename]),
createFrageRoutes (prototype-frage), createPlanRoutes
(prototype-plan), createGitRoutes (prototype-git),
createReleaseRoutes (prototype-release), createEventsRoute
(prototype-events), optional createTourRoutes (prototype-tour).
prototype-frage und prototype-plan sind optional: fehlt die Route,
antwortet sie 404, und der zugehörige Reiter verschwindet statt ins Leere zu
führen. Beide brauchen aber einen eigenen Daemon neben der App — ohne ihn
wartet die Sidebar ewig.
prototype-events ist die Live-Verbindung und sollte nicht fehlen: ohne
sie fällt die Sidebar auf Abfragen alle 10 Sekunden zurück. Sie schickt
Server-Sent Events, sobald sich in todo/ oder .prototype-review/ etwas
rührt — oder in .prototype-frage/ und .prototype-plan/. Eine Richtung
genügt, es läuft durch jeden Proxy, und EventSource
bringt die Wiederverbindung mit. Steht die App hinter nginx, gehört
proxy_buffering off für diesen Pfad dazu.
3. Layout
import { PrototypeLayer } from "@kryptance/prototyping/client";
// im Rootlayout:
<PrototypeLayer />
extras reicht App-eigenes in den TODO-Reiter (z. B. einen
„Tutorials zurücksetzen“-Knopf). tabs hängt ganze eigene Reiter hinter die
festen — Beschriftung und Inhalt kommen von der App, die Schicht rendert sie nur:
<PrototypeLayer tabs={[{ id: "demo", label: "Demo", inhalt: <DemoReiter /> }]} />
Mit vorne: true steht ein eigener Reiter vor den festen, und der erste solche
Reiter ist beim Aufklappen der Sidebar offen statt „TODOs“.
Wer Touren hat, zusätzlich:
import { PrototypeTourEditor } from "@kryptance/prototyping/tour";
<PrototypeTourEditor onPlayTour={startTour} />
4. .gitignore der App
/todo-uploads/
.prototype-review/ # Review-Zustand, deploy.log, deploy.lock
.prototype-frage/ # Gespräche — erklären nur, ändern nichts
.prototype-plan/ # Plan-Dialoge; der fertige Plan steht im TODO
todo/ selbst wird committet — die TODOs sind das Arbeitsmaterial.
TODO-Status
status im Frontmatter |
in der Sidebar |
|---|---|
pending (oder fehlend) |
Offen — noch nicht einmal geplant |
planning |
Plant, mit hochzählender Laufzeit |
plan-dialog |
Wartet auf dich — Rückfragen oder Freigabe, bedient im Reiter Plan |
freigegeben |
Freigegeben — Plan steht, wartet auf die Umsetzung |
in-progress |
In Arbeit, mit hochzählender Laufzeit |
testing |
Im Test — umgesetzt, Testlauf entscheidet noch |
needs-clarification |
Braucht Klärung, mit Antwortfeld |
done |
Reiter Erledigt |
cancelled |
Reiter Erledigt, als „Abgebrochen“ gekennzeichnet |
Ein TODO in Klärung lässt sich in der Sidebar verwerfen (Knopf
„Abbrechen“, zweistufig). Das geht über denselben Endpunkt wie die
Klärung — POST /api/prototype-todo/<datei>/clarify mit
{ action: "abbrechen" } —, damit der Knopf allein mit einer neuen
Paketversion in jeder App ankommt. Die Datei bleibt liegen: die Frage, die
niemand mehr beantworten will, ist oft aufschlussreicher als ihr
Verschwinden.
Die drei plan-Status stehen vor der Umsetzung, siehe Plan-Kanal weiter
unten. Wichtig für Apps mit eigener Schleife: seit dem Plan-Schritt gehört
pending dem Plan-Daemon, und die Umsetzung greift nach freigegeben. Eine
App ohne Plan-Kanal bleibt davon unberührt — ohne die Route gibt es den Reiter
nicht, und ihre Schleife darf weiter pending nehmen.
in-progress braucht zusätzlich startedAt: <ISO-Zeit> — daraus läuft die
Uhr. Gezählt wird im Browser, verankert an der Server-Zeit: GET /api/prototype-todo liefert sein eigenes now mit, die Sidebar rechnet
daraus den Versatz beider Uhren heraus. Gesetzt wird der Status von dem, der
das TODO anfasst — bei einer Schleife von ihrem Abschluss-Skript. Bleibt eine
Sitzung auf halbem Weg liegen, gehört das TODO zurück auf pending, sonst
läuft die Uhr ewig weiter.
Deploy-Modi
- push — der Knopf pusht den Branch; Bauen und Ausrollen macht die CI.
- script — der Knopf startet ein App-eigenes Skript (feste Argumentliste,
keine Shell): pushen, Images bauen,
helm upgrade. Das Protokoll steht in.prototype-review/deploy.log, ein Lock verhindert Parallel-Deploys, und der Review-Zwang greift vorher in der Route.
Review-Schleife (CLI)
Die Gegenseite des Prüfschritts, lauffähig ohne Abhängigkeiten im Repo der App:
node node_modules/@kryptance/prototyping/bin/review.mjs --pending
node node_modules/@kryptance/prototyping/bin/review.mjs --start <head>
node node_modules/@kryptance/prototyping/bin/review.mjs --finish <head> --result <datei.json>
Ergebnisdatei: { "note": "…", "findings": [{ "file": "…", "line": 12, "severity": "blocker" | "hinweis", "summary": "…" }] }.
Plan-Kanal (CLI)
Vor der Umsetzung plant ein eigener Daemon. Er darf zurückfragen — und das ist der ganze Punkt: eine unbeaufsichtigte Umsetzungs-Sitzung kann es nicht, sie hat nur „umgesetzt" oder „in die Klärung geschoben", und die Klärung beginnt ohne Kontext von vorn.
Der Dialog liegt als JSON in .prototype-plan/<todo-ohne-.md>.json
(gitignoriert): Sitzungs-Kennung, offene Fragen, vorgelegter Plan, Verlauf. Der
freigegebene Plan gehört dagegen als Abschnitt ## Plan in die committete
TODO-Datei — die Umsetzung ist eine frische Sitzung und liest ihren Auftrag aus
der Datei, nicht aus dem Gedächtnis des Plan-Agenten.
zustand der Planung |
status des TODOs |
wer ist am Zug |
|---|---|---|
offen |
pending |
der Daemon (noch nicht angefasst) |
laeuft |
planning |
der Agent |
fragen |
plan-dialog |
du — Rückfragen beantworten |
vorgelegt |
plan-dialog |
du — freigeben oder nachbessern lassen |
freigegeben |
freigegeben |
die Umsetzungsschleife |
fehler |
needs-clarification |
du |
Die Aufteilung der Schreibrechte ist Absicht und sollte so bleiben:
src/lib/plan.tsist die Seite des Menschen — sie liest, und sie schreibt, was aus der Oberfläche kommt (Antworten, Freigabe, Ablehnung).bin/plan.mjsist die Seite des Agenten, als gewöhnliches JavaScript, weil der Daemon ein node-Prozess ist und.tsnicht laden kann. Anders alsbin/frage.mjsist es in erster Linie eine Bibliothek: der Daemon importiert die Funktionen, statt sich je Schritt einen Unterprozess zu starten.- Den
statusim TODO schreibt keines von beiden, sondern allein das Abschluss-Skript der App. Zwei Schreiber auf derselben Zeile wären genau die Art Fehler, die man erst bemerkt, wenn ein TODO zweimal umgesetzt wurde.
node bin/plan.mjs --pending # TODOs, die auf einen Plan warten
node bin/plan.mjs --state <todo.md> # die Planung als JSON
node bin/plan.mjs --reset # hängende Planungen zurück in die Schlange
--reset holt nur laeuft zurück — was bei einem Menschen liegt, bleibt
liegen. Die Sitzungs-Kennung bleibt dabei erhalten, damit der nächste Lauf das
Gespräch per resume fortsetzt statt von vorn zu planen.
Selbstfreigabe. Ein TODO, das ohne Zutun eines Menschen entstanden ist
(pfad: konsolenfehler, oder eine jira:-Zeile ohne jira-art: review), gibt
sich seinen Plan selbst frei, sofern der Agent keine Rückfragen hat. Sonst
stünde die Schlange bis zum nächsten Morgen — diese TODOs entstehen nachts. Der
Plan steht trotzdem in der Datei.
Die Rückfragen kommen genau in der Form, in der das Werkzeug
AskUserQuestion des Agent-SDK sie stellt (question, header, options,
multiSelect), und gehen genau so als answers zurück. Bewusst nicht
umgeformt: jede eigene Zwischenform wäre eine zweite Stelle, die bei einer
SDK-Änderung bricht.
Frage-Kanal (CLI)
Der Shift-Klick-Dialog kann zweierlei: „Als TODO speichern" beauftragt eine Änderung,
„Nur fragen" stellt eine Frage zum angeklickten Element — der Agent liest mit,
antwortet und ändert nichts. Die Gespräche liegen als JSON in .prototype-frage/
(gitignored, sie gehören nicht in die Projektgeschichte) und erscheinen in der Sidebar
im Reiter „Fragen" als Faden mit Rückfrage-Feld.
Der Reiter zeigt sich nur, wenn die App die Route angelegt hat:
// src/app/api/prototype-frage/route.ts
export const { GET, POST } = createFrageRoutes(prototypingConfig);
Den Datensatz hinter dem Klick steuert die App bei — das Paket kennt ihre Routen nicht:
frageKontext: async (pfad, stelle) => {
const id = stelle.datensaetze.find((d) => d.entitaet === 'gesuch')?.id;
return id ? beschreibeGesuch(id) : null;
},
Gerufen wird der Haken von beiden Kanälen, Frage wie TODO. Das zweite Argument
(selector, pfad, heading und die Datensatz-Kette) ist neu und optional: Eine
Funktion mit weniger Parametern bleibt zuweisbar, bestehende Apps ändern nichts.
Was das Paket von der Oberfläche erwartet
Zwei Attribute, mehr nicht — beides wird beim Klick ausgelesen:
| Attribut | Wofür |
|---|---|
data-testid="gesuch-karte" |
benennt das Bedienelement. Steht eines da (oder eine id), verschwinden die Klassen aus dem Selektor. |
data-<entität>-id="abc123" |
markiert einen Datensatz-Container, z.B. data-gesuch-id. |
Die Datensatz-Kette wird vom angeklickten Element bis zum body gesammelt, Wurzel zuerst
(gesuch abc123 > bewerbung def456), und landet im Frontmatter des TODOs sowie
strukturiert im Frage-Faden. Ohne die Attribute funktioniert alles weiter — der Selektor
fällt dann auf zwei Klassen je Ebene zurück, und das ist genau der wenig aussagekräftige
Zustand, den die Attribute beheben.
Der Bindestrich vor id ist wesentlich: data-testid endet auf testid, nicht auf
-id, und gilt deshalb richtigerweise nicht als Datensatz.
Beantwortet wird ausserhalb der Anwendung, von einem eigenen Daemon. Sein Griff:
node node_modules/@kryptance/prototyping/bin/frage.mjs --pending
node node_modules/@kryptance/prototyping/bin/frage.mjs --start <id>
node node_modules/@kryptance/prototyping/bin/frage.mjs --answer <id> --text <datei>
node node_modules/@kryptance/prototyping/bin/frage.mjs --fail <id> --text "…"
node node_modules/@kryptance/prototyping/bin/frage.mjs --reset
Die Antwort kommt aus einer Datei, nicht als Argument: Antworten sind mehrzeilig und
enthalten Code-Fences — als Shell-Argument geht das früher oder später schief. Die
Faden-Kennung ist zugleich die Sitzungs-Kennung des Agenten, damit eine Rückfrage
dasselbe Gespräch fortsetzt (claude -p --resume <id>).
Angehängte Bilder (Frage wie Rückfrage) landen wie beim TODO in todo-uploads/; der
Beitrag im Faden trägt nur ihre Dateinamen (verlauf[].images). Der Daemon reicht sie
dem Agenten als Pfade weiter — ohne das beantwortet er „warum sieht das nicht mehr aus
wie im Anhang?" ohne den Anhang.
ENV
| Variable | Wirkung |
|---|---|
FORGEJO_TOKEN |
Pipeline-Anzeige im Deploy-Reiter (Forgejo-API, privat braucht Token) |
HELM_KUBECONFIG |
übersteuert release.kubeconfig |
HELM_NAMESPACE |
übersteuert release.namespace |
HELM_RELEASE |
übersteuert release.release |
Sicherheit
Alles Subprocess-artige läuft über execFile mit festen Argumentlisten, nie
über eine Shell; Commit-Hashes werden gegen /^[0-9a-f]{7,40}$/ geprüft;
Upload-Dateinamen weisen Verzeichnisbestandteile zurück; das Rollback-Ziel
wählt der Server aus helm history, nie die Anfrage. Und: In Produktion
(next build) existiert nichts hiervon — 404 statt Oberfläche.