Unwind design: Rewind · Spec · Play
In short: This folder describes where Unwind is heading and how we get there. Teams work on a large codebase through a shared, self-hosted Unwind Server, splitting it into slices. Unwind splits into Rewind, which understands a legacy system and compiles it into a stack-neutral Spec, and Play, which rebuilds the Spec deterministically from a client's Target Kit of recipes, with an LLM filling the explicit holes. The CLI does the work next to the code and pushes artifacts (never code) to the server, which stores them, converges slice Specs into one project Spec and shows progress. Both halves are verified by computation, not by assertion. The ideas borrow heavily from OpenRewrite/Moderne, but Unwind takes no dependency on their stack.
Status: design, for review. Nothing here is built yet. Live HTML version: https://unwind-design.cliftonc.nl
One-page summary
Where Unwind is today. Unwind is a Claude Code plugin with this pipeline:
- deterministic scripts (
@unwind/core, tree-sitter) inventory a codebase; - LLM specialists write
[MUST]/[SHOULD]/[DON'T]-tagged layer docs; - coverage is proven by set arithmetic (
manifest − docs); uw-grillattacks the business logic;uw-planinterviews the user;uw-buildhas LLM subagents write the target code, whichverify-rebuildre-scans and diffs.
Every line of target code is still written by an LLM. The extracted facts are names only: no types, no call graph.
What we learned from OpenRewrite/Moderne (01):
- Their Lossless Semantic Tree (type-attributed, built by the real compiler) and their recipe model (scan → generate → edit, declarative composition, before/after golden tests, data tables) are excellent ideas.
- But OpenRewrite performs no cross-language translation. Its non-JVM languages and most recipe packs are under a source-available licence (MSAL) or are proprietary, and they run through a JVM host and the Moderne CLI.
- Decision: we copy the concepts and depend on none of the code.
The flow, end to end (02 §2.1):
- A team stands up the Unwind Server (03), creates a project, and developers
unwind loginwith a simple token. - The codebase is scanned once locally; the server proposes slices (units of work) and people or agents claim them.
- Each slice is analysed locally with Rewind (scan → tagged docs → grill → context gaps → parity observations); the CLI pushes artifacts per slice. Source code never leaves the developer's machine.
- The server converges slice Spec fragments into one project Spec, reporting uncovered items, overlaps, conflicts and seams.
- Play rebuilds slice by slice from the Spec plus the client's Target Kit; verification results are pushed back, so the server shows completeness and parity per slice.
The destination (02):
- Unwind Server (from day 0): the team's shared system of record and UI. Hono + React, git + SQLite, one Docker image; token login; artifacts only; slices are first-class and converge into one project Spec (03).
@unwind/modelis the shared contract: ids, Semantic Model, Spec, Kit and verification schemas.- Rewind (understand) covers scan, typed Semantic Model, tagged docs, grill and context gaps, and produces the Spec: a typed, stack-neutral IR of entities, endpoints, events, config and operations, each carrying priority and provenance.
- Play (rebuild) takes a Spec plus a Target Kit and runs recipes and blueprints to generate code. The LLM fills the
@unwind-holes, then verification runs: structural, typed and behavioural. - Target Kits are versioned per-client git repos encoding their golden path. They are mined primarily from the client's own reference app (04).
- Behaviour parity (06): scenarios generated from the Spec run against the legacy app (to observe it and enrich the Spec) and against the rebuild (to give a parity verdict).
- Context gaps (07): an agent round finds what the code can't tell us and produces interview briefs for stakeholders, end users, developers and ops. The answers flow back into the Spec with provenance.
- Surfaces: one engine with a CLI first (the skills shell out to it; it works offline and pushes to the server when logged in), the server for shared state and UI, and an MCP adapter later.
How we get there (08): phases 0–8 (with 5b–5d in parallel), each shippable on its own, each keeping the graceful fallback to today's pure-LLM flow.
Reading order
| # | Doc | Read it if you want… |
|---|---|---|
| 01 | Review: OpenRewrite / Moderne vs Unwind | what we looked at, what we borrow, and why there is no dependency |
| 02 | Destination architecture | the whole picture: server, Rewind / Spec / Play / Kits / surfaces |
| 03 | Unwind Server and slices | the shared day-0 server, auth, push/pull, slices and convergence |
| 04 | Target Kits, recipes, blueprints, holes | how the target becomes deterministic |
| 05 | Semantic Model and store | the LST-inspired typed model behind Rewind |
| 06 | Behaviour parity | tests that run against both the legacy app and the rebuild |
| 07 | Context gaps and interviews | finding and closing gaps the code can't answer |
| 08 | Roadmap | the phased plan with exit criteria |
| 09 | Open questions | decisions still to make, each with a recommendation |
How to review
-
Sections are numbered (
§4.4,§7.2). Cite them in offline comments. -
The markdown here is the source of truth. The HTML site is generated from it.
-
Conventions every proposal must respect come from the repo's
CLAUDE.md:- the manifest schema is additive-only;
- AST/real parsers over regex;
- graceful fallback when Node/core is unavailable;
- candidate ids are the join key.
A proposal that bends one of these says so explicitly.
01 · Review: OpenRewrite / Moderne compared with Unwind
In short: OpenRewrite's Lossless Semantic Tree and its recipe model are the best existing answer to "deterministic, verifiable code change at scale", and almost every concept is worth adopting. But OpenRewrite only transforms code within one language, and its non-JVM languages and most recipe packs are source-available or proprietary. Unwind therefore copies the concepts and takes no dependency on the code.
Research was done on 2026-10-07 from primary sources: the openrewrite/rewrite monorepo, docs.openrewrite.org, docs.moderne.io and moderne.ai. Items we could not confirm are marked [unverified].
1.1 What the LST is
A Lossless Semantic Tree (LST) is "a tree representation of code" that adds two things a classic AST lacks: type attribution and format preservation (docs: LSTs, moderne.ai/platform/lst).
- Type attribution. Every typed node carries a
JavaType(docs: type attribution). The variants areClass,Parameterized,GenericTypeVariable,Method,Variable,Array,PrimitiveandUnknown. Between them they hold the fully-qualified names, supertypes, method signatures, generics and annotations. The same type model is reused for Kotlin, TypeScript, Python, C# and Go. - Built by the real compiler: javac for Java, the TypeScript compiler API (
ts.createProgram+getTypeChecker()) for JS/TS, Roslyn for C#. That is why OpenRewrite needs the classpath or dependencies. When it can't resolve them, types fall back toJavaType.Unknownand type-gated recipes silently match nothing (FAQ). - Format preserving. Whitespace and comments are stored on the nodes, so printing the tree reproduces the source byte-for-byte. Edits pick up the local style.
- Markers are immutable metadata attached to any node: search results, provenance, build-tool facts (docs: markers).
- Storage.
- The OSS Maven/Gradle plugins hold the whole LST in memory for one run.
- The Moderne CLI (
mod build) uses the build tool only for discovery, parses in its own JVM, and serialises LSTs into JAR artifacts published to Artifactory/Nexus (how LST artifacts are produced). The on-disk format is proprietary [unverified detail].
- Languages and bridging. Non-JVM languages run as peers over JSON-RPC (
RewriteRpc). They are implemented in their own language: TypeScript for JS/TS, Python, C# and Go peers.
1.2 What a recipe is
| Concept | What it does | Source |
|---|---|---|
| Visitor recipe | Recipe.getVisitor() walks the LST and edits nodes | recipes |
ScanningRecipe<Acc> | Three phases: scan every file into an accumulator, then generate new files, then edit. Cross-file knowledge only flows through the accumulator | scanning recipes |
| Declarative recipe | YAML recipeList that composes other recipes with options | docs: types of recipes |
Refaster / JavaTemplate | Before→after templates, compiled into type-attributed snippets | refaster recipes |
| Preconditions | Preconditions.check(...) gates a recipe on facts (e.g. "repo has dependency X") | recipes docs |
| Search recipes + data tables | Recipes that report instead of edit. They emit SearchResult markers and typed rows (CSV) | data tables |
| Testing | RewriteTest: rewriteRun(java(before, after)), with type validation after the run and repeated cycles to catch non-idempotent recipes | recipe testing |
| Generation | Only ScanningRecipe.generate() creates files (e.g. CreateTextFile). All the documented examples are small artifacts, not applications | scanning recipes docs |
Recipes can be authored in TypeScript for JS/TS targets: @openrewrite/rewrite provides Recipe, JavaScriptVisitor, and pattern/template rewrite rules. They still run through the Moderne CLI (writing a JS recipe).
1.3 The Moderne platform around it
- Moderne CLI (
mod). Multi-repo builds and runs. Free for public repositories; private repositories need a licence (CLI licence). - Platform / DX. Multi-repo LST store, Trigrep search (trigram plus structural), data-table analytics, and coordinated pull requests.
- AI.
- An MCP server inside the CLI, with tools like
find_types,find_methods,run_recipe,query_datatableandlearn_recipe(MCP overview). - The Moddy agent, which uses recipes as deterministic tools [current status unverified].
- An MCP server inside the CLI, with tools like
- Prethink (docs). About 140 recipes that write agent context into
.moderne/context/: service endpoints, DB connections, DTO schemas, test gaps, code smells and a FINOS CALM architecture model. This is the nearest analogue to Rewind.
1.4 The two facts that decide our strategy
1. No cross-language translation. We found no recipe that translates code between languages: not Java→Kotlin, not JS→TS, not COBOL→Java. Every visitor edits one language's tree and prints it back in that same language. Cross-language work stops at three things:
- multi-file-type recipes (a POM condition driving a YAML edit);
- reading one language as evidence for another;
- framework migrations within one language (Spring→Quarkus).
Unwind's core job is to rebuild in a different stack. OpenRewrite does not do that.
2. Licensing and runtime.
The licensing below is our reading of OpenRewrite licensing. It is not legal advice.
| Layer | Licence |
|---|---|
rewrite-core, Java, Kotlin, Groovy, XML/YAML/JSON/HCL/TOML/Protobuf, Maven/Gradle, rewrite-templating, rewrite-analysis, RewriteRpc | Apache-2.0 |
JS/TS (@openrewrite/rewrite), Python, C#, Go, Ruby, Scala; rewrite-static-analysis, rewrite-spring, rewrite-migrate-java, rewrite-prethink … | MSAL: may not be commercialised or provided as a managed service |
io.moderne.* recipes, Prethink at scale, data-flow / impact analysis | Proprietary |
In practice, non-JVM languages only run through the JVM host plus the Moderne CLI. That conflicts with Unwind's lightweight Node runtime, its "degrade gracefully" rule, and its OSS (MIT) distribution.
Decision: no dependency on the Moderne stack, not even interoperability. We adopt the concepts.
1.5 Known limits we must not reproduce
- Missing types fail silently. Type-gated logic silently matches nothing when resolution fails. Unwind must instead report the tier it reached (§5.2).
- The whole LST in memory causes out-of-memory failures on large repos. Unwind keeps facts, not trees, and writes them to disk (§5.5).
- Noisy whole-file rewrites and huge pull requests were found too unwieldy to review (Adyen case study). Play generates into a new target repo slice by slice, with holes clearly marked.
- Stateful recipes are bug-prone (cronn). Unwind recipes are pure functions with golden fixtures (§4.3).
1.6 Concept transfer
| OpenRewrite / Moderne | Unwind destination concept | Lives in | Doc |
|---|---|---|---|
| LST (typed, lossless) | Semantic Model: typed and bound, but not lossless. Unwind never edits the source, so format preservation is not needed | Rewind | 05 |
| LST artifacts, mass ingest | Model Store: per-commit, versioned, multi-repo queries | Store | 05 |
| Search recipe + data tables | Detector recipes emitting typed fact tables | Rewind | 05 |
| Prethink context | Spec plus tagged layer docs | Rewind → Spec | 02 |
Visitor / ScanningRecipe | Target recipe: scan Spec → generate → edit | Play | 04 |
| Declarative YAML recipe | Blueprint: a whole service or module | Kit | 04 |
| Recipe marketplace / BOM | Target Kit / Recipe Book: versioned per client | Kit repo | 04 |
| Preconditions | appliesTo(specNode, profile) | Play | 04 |
RewriteTest before/after | Golden fixtures (Spec fragment → expected files) plus an idempotence run | Kit | 04 |
JavaTemplate / Refaster | Parameterised templates mined from exemplar code | Kit | 04 |
Moddy / learn_recipe | Exemplar → recipe promotion | Play | 04 |
| Moderne MCP | unwind mcp: a thin adapter over the CLI | Surfaces | 02 |
SearchResult markers | Holes (@unwind-hole) and provenance markers | Play | 04 |
1.7 What stays uniquely Unwind
[MUST]/[SHOULD]/[DON'T]tagging of every documented item, with rationale.- Completeness proven by set arithmetic (
scan − docs,Spec − target), never asserted. - Grilling plus domain-expert verdicts, which decide whether a behaviour deserves to be reproduced.
- Cross-stack rebuild with verification, which OpenRewrite does not attempt.
- New in this design: behaviour parity against the legacy app (06) and context-gap interviews (07).
02 · Destination architecture
In short: A self-hosted Unwind Server is the shared backbone from day 0: the team's system of record and UI, git- and SQLite-backed, receiving artifacts only, with slices as the first-class unit of work (03). Around it sit one shared model package, two plugins and one contract between them. Rewind understands a legacy system, slice by slice, and compiles it into a stack-neutral Spec. Play rebuilds the Spec from a client's Target Kit. A single engine sits behind a CLI-first surface that does the work next to the code and pushes results to the server. MCP is a thin adapter. Deterministic code owns the facts and the structure; the LLM owns semantics and holes; completeness is always computed.
2.1 The shape
The flow. Developers and agents run the unwind CLI locally, next to the code → they push artifacts per slice to the Unwind Server (the shared record and UI) → the server converges slice fragments into one project Spec → Play rebuilds per slice from the Spec plus a Target Kit → verification results are pushed back, so progress, completeness and parity are visible per slice. Code never leaves the developer's machine; everything works offline and syncs when logged in.
Components, in flow order:
- Unwind Server (03): projects, slices and owners, artifacts in git, state and index in SQLite, basic UI, token auth. Present from day 0.
@unwind/model(§2.3): the shared ids and schemas every other part speaks.- Rewind (§2.4): understands the source, per slice, and produces Spec fragments.
- The Spec (§2.5): converged on the server into one project Spec; the only Rewind → Play contract.
- Play (§2.6) with Target Kits (§2.7): rebuilds per slice and verifies.
- Surfaces (§2.8): CLI first, server UI, MCP later.
┌──────────────── UNWIND SERVER (shared backbone, day 0) ────────────────┐
│ projects · slices + owners · artifacts (git) · state/index (SQLite) · │
│ convergence → project Spec · metrics · UI · token auth │
└───────▲───────────────────────▲─────────────────────────▲───────────────┘
push │ per slice push │ fragments push │ verify results
┌──────────── @unwind/model (shared contract) ────────────┐
│ ids · Semantic Model · Spec (IR) · Kit schema · │
│ verification + state schemas · versioning │
└──────────────────────────────────────────────────────────┘
SOURCE REPO ─► REWIND ─► Semantic Model ─► tagged docs ─► SPEC ──────┐
(understand) (facts) (LLM + grill) (neutral) │
▼
CLIENT KIT REPO ─► TARGET KIT (profile · conventions · recipes · PLAY ─► TARGET REPO
(mined from ref app) blueprints · golden fixtures) (rebuild) │
▲ │
VERIFY (target re-scanned by Rewind ◄───────────┘
→ diffed against the Spec)
SPEC ─► CONTEXT GAPS ─► interview briefs ─► (external interview tool) ─► answers ─► enrich SPEC
SPEC ─► PARITY SCENARIOS ─► run vs LEGACY (observe → goldens → enrich SPEC)
└──► run vs TARGET (same scenarios → behavioural parity)
2.2 Today compared with the destination
Today (uw-*) | Destination | Change |
|---|---|---|
uw-scan → scan-manifest.json | rw-scan → Semantic Model (typed, bound) | Additive manifest fields; extra tiers (05) |
uw-analyze-*, uw-verify, uw-complete | Same, under rw-* | Rename plus aliases |
uw-grill → questions/ | rw-grill, folded into context gaps | One questionnaire mechanism (07) |
| (none) | rw-spec → Spec | New: the Rewind→Play contract |
| (none) | rw-observe / pl-parity | New: behaviour parity (06) |
uw-plan, free-text stack decisions | pl-plan: choose or tailor a Kit | Typed profile replaces free text |
uw-build-layer writes every line | pl-build: generate from Kit, LLM fills holes | Deterministic first (04) |
verify-rebuild: names, method+path, field-name Jaccard | Plus field types, holes, behavioural parity | Stronger verdicts |
skills/scripts/*.mjs | unwind CLI (scripts become shims) | Consolidation (§2.8) |
Single user, local docs/unwind/ | Unwind Server: shared, git + SQLite, slices, UI | New from day 0 (03) |
2.3 @unwind/model: the shared contract
The only package both plugins import. It holds:
- The id scheme. Today this is
manifest/candidates.tsandsymbolIdinmanifest/manifest-schema.ts(kind:path:name). The destination version adds class-qualified, overload-safe ids (e.g.method:src/a.ts:Orders.create(2)), because methods and overloads collide today. Ids remain the join key across the model, Spec, coverage, graph, kit output and verification. - Schemas: Semantic Model (additive extension of
ScanManifest), Spec, Kit manifest, rebuild state and verification (today'sgraph/rebuild-state-schema.tsandgraph/rebuild-verification.tstypes), the gap register and Scenario. - Versioning. Every artifact carries
schemaVersion. Changes are additive-only, as with the manifest today; breaking changes need a migration and a major bump.
2.4 Rewind (understand)
Today's pipeline (scan → seed → analyze → verify-coverage → complete → grill), extended with:
- Semantic Model: a typed, bound fact model in tiers T0/T1/T2 (05).
- Detector recipes:
layers/contract-detectors.tssplit into a fixture-tested registry. rw-spec: compiles manifest, graph and tagged docs into the Spec.rw-observe: runs Spec scenarios against the legacy app and enriches the Spec (06).rw-context-gaps: finds what the code can't answer and produces interview briefs (07).
Every step runs locally through the CLI, scoped to a slice when one is claimed (--slice), and unwind push sends the resulting artifacts to the server, which converges the fragments into the project Spec (03 §3.8).
Rewind is useful on its own for documentation, onboarding, audits and due diligence, without ever running Play.
2.5 The Spec: the only Rewind → Play contract
A stack-neutral, typed IR of everything the target must preserve. Where a standard exists, the Spec embeds it rather than inventing a format: JSON Schema for shapes, OpenAPI-style operations for endpoints.
| Node kind | Carries |
|---|---|
entity | Typed fields (neutral types), keys, FKs/relations, enums, physical name |
endpoint | Full composed path, method, params, request/response schema refs, auth, status codes, handler ref |
event / job | Payload schema, producer/consumer, schedule |
config | Env/config keys, defaults, secrets flag |
integration | External system, protocol, calls made |
operation | A named business rule: description, doc ref, behavioural assertions, linked scenarios |
scenario | Given/when/then at a boundary (06) |
Every node carries id, priority (MUST/SHOULD/DONT plus rationale), docRef, sourceRef and provenance: scanned, inferred, observed, interview:<role>:<date> or expert.
{
"schemaVersion": "1.0",
"entities": [{
"id": "table:src/db/schema.ts:orders",
"name": "orders", "priority": "MUST",
"fields": [
{ "name": "id", "type": "uuid", "pk": true },
{ "name": "customerId", "type": "ref(customers)", "nullable": false },
{ "name": "status", "type": "enum(pending,paid,shipped,cancelled)" },
{ "name": "totalCents", "type": "int" },
{ "name": "createdAt", "type": "datetime", "default": "now" }
],
"docRef": "layers/database/tables.md#orders",
"provenance": ["scanned", "inferred"]
}],
"endpoints": [{
"id": "endpoint:src/routes/orders.ts:POST /api/orders",
"method": "POST", "path": "/api/orders", "priority": "MUST",
"request": { "$ref": "#/schemas/CreateOrder" },
"responses": { "201": { "$ref": "#/schemas/Order" }, "422": { "$ref": "#/schemas/ValidationError" } },
"auth": "session", "handlerRef": "function:src/services/orders.ts:createOrder",
"provenance": ["scanned", "observed"]
}],
"operations": [{
"id": "operation:src/services/orders.ts:applyDiscount",
"priority": "MUST",
"rule": "Orders over 10000 cents get 5% off; never stack with coupon discounts.",
"rationale": "Contractual with wholesale customers (interview:stakeholder:2026-11-02)",
"docRef": "layers/service/orders.md#applydiscount",
"scenarios": ["scenario:orders:discount-threshold"],
"provenance": ["inferred", "interview:stakeholder:2026-11-02"]
}]
}
A Spec can also be hand-written, which makes Play usable for greenfield work.
2.6 Play (rebuild)
pl-plan. The interview narrows to choosing or tailoring a Kit: phasing, re-use and risk. It writes a typed profile in place of today's free-textrebuild-decisions.json(which stays as a record).- Resolve the Kit. Pin
kit@versioninrebuild-state.json. - Generate. Recipes and blueprints run over the Spec and write target files, map entries (the existing
rebuild-map/*.jsonformat) and holes (04). The scaffold recipe finally sets the dormantconfig.scaffoldedflag inrebuild-state-schema.ts. - Fill.
pl-build-layersubagents fill only holes plus any unmapped[MUST]items. - Verify. Structural and typed diff (an extended
graph/rebuild-verification.ts), hole accounting, and behavioural parity (pl-parity). - Loop. The existing loop-until-verified mechanics, with completeness % and parity % as termination signals.
- Push. Rebuild maps and verification results are pushed per slice; the server tracks each slice through its Play states (planned → generating → filling → verified → cut-over) and orders slices by their seams (03 §3.7–3.8).
Fallback. If there is no Kit match, or no Node/core, today's pure-LLM uw-build flow runs unchanged, and the skill says so.
2.7 Target Kits
Per-client, versioned git repos that encode the client's golden path: stack profile, conventions, type map, recipes, blueprints and golden fixtures. They are mined primarily from the client's reference app. Starter kits (first: hono-drizzle-zod) ship with Play. See 04.
2.8 Surfaces: one engine, CLI first, a shared server from day 0
@unwind/engine (library: model, rewind, play, kits, parity, gaps, slices)
├── unwind CLI ← PRIMARY execution surface. Skills shell out to it. CI / humans / any agent.
│ works fully offline; when logged in, pushes artifacts to the server
├── unwind serve ← DAY 0: shared system of record + basic UI (Hono API, git + SQLite, slices)
└── unwind mcp ← later: thin stdio MCP adapter; tools map 1:1 to CLI commands / API routes
Division of labour:
- The CLI does the work (scan, analyze, generate, verify) next to the code.
- The server stores, merges, indexes and shows it: projects, slices, history, convergence and metrics.
- Source code never goes to the server; only artifacts do. See 03 for storage, auth (simple bearer tokens via
unwind login), push/pull, slices, convergence, UI and the API.
Why the CLI rather than MCP for the skills:
- The skills already shell out to
node skills/scripts/*.mjsvia_resolve-plugin-root.sh/ensure_unwind_core. That is a CLI in all but name, so this is a consolidation. - A CLI works in CI, for humans and from any agent, with no daemon or connection state. MCP availability varies by client and would weaken the graceful fallback.
--jsonoutput and exit codes make it testable. Long-running steps (generate, parity) fit processes better than tool calls.
Command map:
unwind rewind scan | seed | coverage | grill-brief | spec | observe | context-gaps | context-ingest
unwind play plan-brief | kit mine | kit test | generate | merge | verify | parity
unwind slices propose | list | claim | release | status
unwind login | logout | whoami | project link|create | push | pull | status
unwind graph | publish | serve | mcp
common flags --project <src> --target <dir> --kit <repo@ver> --slice <id> --json --plan (dry run)
Distribution is an npm package (npx @unwind/cli) plus the server Docker image. ensure_unwind_core becomes ensure_unwind_cli. The current .mjs scripts become one-line shims for one release.
Server tech stack (detail in 03 §3.3):
- Hono on Node (
@hono/node-server), with zod validation (@hono/zod-validator) andhono/clientRPC types shared with the CLI and UI; - React + Vite UI with TanStack Query (TanStack Router recommended) and Tailwind + daisyUI, dark by default and mapped onto the dashboard tokens, reusing the React Flow/ELK graph and
DocsViewerfrompackages/dashboard; node:sqlite(Drizzle recommended for schema and migrations) plus system git;- one Docker image serving API and UI from the same Hono app.
- This is the stack of the pilot starter kit, so Unwind dogfoods its own target kit.
The MCP adapter and the HTTP API add no logic of their own, so behaviour is identical across surfaces.
2.9 Packaging and naming
- One repo and two plugins: Rewind (
rw-*skills) and Play (pl-*skills). Unwind stays the umbrella brand. - Packages:
@unwind/model,@unwind/engine(initially today's@unwind/core, renamed and grown),@unwind/cli,@unwind/server(Hono API + storage) and@unwind/app(today's@unwind/dashboard, grown into the server UI). - The
uw-*skills stay as deprecated aliases for one release.
2.10 Invariants
- Hybrid rule. Deterministic code owns every verifiable fact and every structural artifact. The LLM owns semantics and holes. Completeness is computed by set arithmetic over ids.
- Additive schemas only. No reshaping
FileSymbols; add optional fields. - AST and real parsers over regex, on both sides: detectors read ASTs, and recipes edit target files with ts-morph or tree-sitter.
- Graceful fallback at every step, announced to the user.
- Files are the source of truth. Locally, that is
docs/unwind/. On the server, it is the project's git repo. SQLite holds auth and operational state plus a rebuildable index. - Code stays local. Only artifacts are pushed to the server (03 §3.11).
- Slices are first-class. Candidate ids define slice membership, and convergence is set arithmetic over them (03 §3.7–3.8).
03 · Unwind Server and slices
In short: From day 0, a self-hosted Unwind Server (one Node process in one Docker image) is the team's shared system of record. It provides a small web UI. The CLI keeps doing the work locally and pushes artifacts only: source code never leaves developers' machines. Storage is git (reviewable artifacts, full history) plus SQLite (auth, slices, metrics, query index). Slices are first-class units of work that run through both halves. Teams analyze them in parallel, the server converges their Spec fragments into one project Spec, and the same slices later become Play's strangler-style rebuild units.
3.1 Why a server from day 0
On a large codebase, one person running Unwind in one session does not scale.
- Analysis is spread across people and agents over weeks.
- Reviewers who are not developers need to read the docs and answer questions.
- Leads need to see progress, and the Spec has to be one shared thing, not N local copies.
Files in each developer's docs/unwind/ are fine for one person. For a team they need a home with history, ownership, progress and a UI. The server provides that home without changing how the work is done: the CLI stays the execution surface (doc 02 §2.8), and the server stores, merges, indexes and shows.
3.2 Shape: unwind serve
- A single Node process (
unwind serve), shipped as one Docker image (ghcr.io/nearform/unwind-server). - One Hono app serves the HTTP JSON API and the built UI as static assets.
- Everything lives under one data volume:
/data
unwind.db node:sqlite: auth, projects, slices, runs, metrics, index (WAL mode)
repos/<project>.git bare git repo per project: the reviewable artifacts
tmp/ push staging
docker run -d --name unwind -p 8080:8080 \
-v unwind-data:/data \
-e UNWIND_ADMIN_TOKEN="$(openssl rand -hex 32)" \
ghcr.io/nearform/unwind-server:latest
- Backup is a copy of
/data. Run the SQLite online backup, or stop the container first. - TLS and SSO come from a reverse proxy in front (Caddy, nginx, Cloudflare Tunnel). They are out of scope for day 0 (§3.12).
- Running without Docker works too:
npx @unwind/cli serve --data ./unwind-data.
3.3 Tech stack
The server is built on the same stack as the pilot starter kit (hono-drizzle-zod, doc 04). Unwind therefore dogfoods its own target kit: the server is a living reference app that the kit can be mined from and tested against.
| Concern | Choice | Notes |
|---|---|---|
| HTTP API | Hono on Node (@hono/node-server) | Typed routes. The one app also serves the UI's static assets. |
| Validation | zod via @hono/zod-validator | The schemas live in @unwind/model, shared with the CLI. |
| Typed client | hono/client RPC types | The CLI and UI call the API through the same inferred types, with no hand-written client. |
| UI | React + Vite | It grows out of packages/dashboard. |
| Server state in UI | TanStack Query | Caching, refetch and optimistic slice claims. |
| Routing in UI | TanStack Router (recommended) | Type-safe routes and search params. It replaces today's hand-rolled urlState.ts over time. |
| Components and theming | Tailwind + daisyUI | Dark default. daisyUI themes are mapped onto the existing dashboard tokens (--color-* → --c-*). |
| Reused UI | React Flow + ELK graph, DocsViewer/MarkdownView | Taken as they are from packages/dashboard. |
| Database | node:sqlite (built into Node ≥22.5) | WAL mode. FTS5 for search. |
| ORM / migrations | Drizzle over node:sqlite (recommended) | Typed schema plus generated migrations. Matches the starter kit. |
| Git | System git, called from the server process (recommended), installed in the image | Fully compatible with real git (packs, receive, mirror push). isomorphic-git is a fallback for git-less environments. |
| Packaging | One Docker image (node:22-slim + git) | unwind serve is the entrypoint. |
3.4 Storage: git for artifacts, SQLite for state and index
Git is the source of truth for reviewable artifacts. Each project has one bare repo, with the same layout as a local docs/unwind/ plus slice folders:
architecture.md
layers/** tagged layer docs (shared)
slices/<slice-id>/
slice.json scope, owners, state
docs/** slice-specific layer docs
spec.fragment.json this slice's Spec fragment
findings.json · gaps.json grill findings, context gaps
spec/project.spec.json converged Spec (written by the server, §3.8)
questions/** · interviews/** questionnaires, briefs, responses
rebuild/** rebuild-map, state, verification (Play)
.cache/scan-manifest.json manifest (ids are the join key)
- Every push is a commit authored by the token's owner:
Author: Dana Lee <dana@client.com>, with a trailerUnwind-Slice: orders. - That gives history, diff, blame and revert for free. It also allows an optional mirror push to a GitHub/GitLab repo for clients who want the artifacts next to their code.
SQLite holds operational state plus a query index:
| Table group | Contents | Rebuildable from git? |
|---|---|---|
users, tokens | Identity and hashed tokens (§3.5) | No (back them up) |
projects, slices, claims | Ownership, state, claims | Partly: slice state is mirrored to slice.json |
runs | Who ran which CLI command, when and against which commit, with result metrics | No (audit) |
metrics | Time series: doc coverage, context coverage, parity %, completeness %, convergence % per slice and per project | Yes, recomputable per commit |
spec_nodes, gaps, findings | Parsed Spec, gap register and grill findings, for queries and the UI | Yes |
search (FTS5) | Full-text search over docs and the Spec | Yes |
unwind serve --reindex rebuilds every "Yes" table by walking git history, so the index can always be thrown away and rebuilt.
3.5 Auth: simple bearer tokens
The model is deliberately minimal.
- Bootstrap.
UNWIND_ADMIN_TOKEN(env) is an admin token on first start. The admin creates users, for example in the UI, by name and email. - Personal tokens. Users create tokens in the UI (
Settings → Tokens). Each has a name, scopes and an optional expiry. A token is shown once asuwt_<32 random bytes base62>and stored sha256-hashed. - Scopes:
read(view and pull),write(push, claim slices, answer questions) andadmin(users, projects, tokens). Project membership is a simple allow-list per project. - Requests use
Authorization: Bearer uwt_…. The UI uses the same token, kept in an HttpOnly cookie after a token-paste login.
CLI:
unwind login https://unwind.internal.client.com # prompts for a token, verifies via /api/me
# → ~/.config/unwind/credentials.json (mode 0600): { "servers": { "<url>": { "token": "uwt_…", "user": "dana" } }, "default": "<url>" }
unwind whoami # user, server, scopes, projects
unwind logout [--server <url>]
UNWIND_TOKEN / UNWIND_SERVER env vars override the file, for CI.
3.6 CLI sync: local working copy, push and pull
- The local
docs/unwind/stays the working copy. Every command still works with no server, in line with the graceful-fallback principle. The server is additive. - The project is linked to a server project in
docs/unwind/.unwind.json:{ "server": "<url>", "project": "acme-billing" }.
unwind project link acme-billing # or: unwind project create acme-billing
unwind status # local vs server: ahead/behind, changed files, slice claims
unwind push [--slice orders] [-m "message"] # changed files → one commit on the server
unwind pull [--slice orders] # fast-forward the working copy
How push works:
- The CLI sends a bundle of changed files (paths, content and hashes) plus the base revision it last pulled.
- The server writes a commit if
base == HEAD, or if none of the touched paths changed sincebase(a non-conflicting path-level merge). - Otherwise it returns 409 with the conflicting paths. The CLI pulls, replays and retries.
- Because each slice owns its own folder (§3.4), concurrent pushes from different slices almost never conflict.
Before a push, the CLI:
- scrubs secrets (token, key and connection-string detectors; the push is refused if anything is found, with
--allowto override per file); - enforces the artifact allow-list (§3.11).
Skills call the CLI as they do today. When logged in and linked, long-running skills (rw-analyze, rw-grill, pl-build) push at checkpoints, so progress shows up live in the UI.
3.7 Slices: the first-class unit of work
A slice is a bounded area of the codebase that one owner or team carries from analysis through rebuild:
{
"id": "orders",
"name": "Orders & checkout",
"scope": {
"paths": ["src/orders/**", "src/checkout/**", "db/migrations/*orders*"],
"layers": ["service", "api", "database"],
"capabilities": ["ordering", "payments"]
},
"owners": ["dana", "sam"],
"rewind": { "state": "covered" },
"play": { "state": "planned" },
"metrics": { "coverage": 0.94, "contextCoverage": 0.61, "parity": null, "completeness": null }
}
Proposal. After the first local full scan, unwind slices propose (or the UI) suggests slices deterministically:
- clustering the import graph (today's
importMap, later call edges); - seeded by top-level directories and layers;
- balanced by candidate count.
Humans then rename, merge, split and assign. The scope globs resolve to a set of candidate ids, and that set is the slice's membership.
States run through both halves:
| Half | States |
|---|---|
| Rewind | proposed → claimed → analyzing → covered → grilled → spec-ready → accepted |
| Play | planned → generating → filling → verified → cut-over |
- Transitions are mostly computed. For example,
coveredmeans slice coverage is 100% perverify-coverageover the slice's ids, andverifiedmeans[MUST]completeness is at its target. claimed,acceptedandcut-overare explicit human actions.
Per-slice artifacts: docs, coverage and gaps, Spec fragment, grill findings, context gaps, interview briefs and, later, rebuild map, verification and parity.
3.8 Convergence: fragments into one project Spec
The server merges the slices' spec.fragment.json files into spec/project.spec.json, using candidate ids as the join key. This is the same set arithmetic Unwind already uses for coverage. On every push it reports:
| Signal | Definition | Resolution |
|---|---|---|
| Uncovered | Manifest ids in no slice scope | Widen a scope, or create a slice |
| Overlaps | An id in two or more slice scopes | Assign one owner; the other slice references it |
| Conflicts | The same id with a different priority or content across fragments | Owners resolve in the UI; the decision is recorded with rationale |
| Seams | Imports, calls or reads/writes crossing slice boundaries | Become interface contracts in the Spec that both slices must honour |
- Convergence % = ids that are in exactly one slice, conflict-free and spec-ready, divided by all manifest ids (excluding
[DON'T]). The project Spec is converged at 100%. - Seams drive Play. They form a slice dependency graph, which gives the strangler-style rebuild order. A slice can be rebuilt and cut over once its upstream seams are satisfied, either by already-rebuilt slices or by an adapter onto the legacy app. Seams are also first-class scenarios for parity (doc 06).
3.9 UI (basic, day 0)
The UI is built by evolving packages/dashboard into the app served by unwind serve, reusing its graph and docs components.
| View | Shows |
|---|---|
| Projects | List, with last push and headline metrics |
| Project overview | Coverage, context, convergence, parity and completeness over time (from metrics); recent activity |
| Slice board | Kanban by state (Rewind and Play lanes), owners and a Claim button; filter by owner or capability |
| Slice detail | Rendered docs (existing DocsViewer), gaps, Spec fragment, findings, open questions and its metrics |
| Graph | Existing React Flow + ELK view, coloured by slice, with seams highlighted |
| Convergence | Uncovered, overlaps and conflicts (with resolve actions), and seams |
| Questions & interviews | Grill questionnaires and interview briefs; answer in place (writes back via a commit) |
| Activity | Git log per project and slice, with diffs |
| Settings | Tokens, users and project members (admin) |
3.10 API sketch
All routes are JSON and bearer-authenticated, typed through hono/client. The future MCP adapter maps onto these routes.
| Method and route | Purpose | Scope |
|---|---|---|
GET /api/me | Current user, scopes, projects | read |
GET/POST/DELETE /api/tokens | Personal token management | read / write |
GET/POST /api/projects | List or create projects | read / admin |
GET /api/projects/:p | Overview and headline metrics | read |
GET/POST /api/projects/:p/slices | List slices, or create/propose them | read / write |
PATCH /api/projects/:p/slices/:s | Rename, rescope or change state | write |
POST /api/projects/:p/slices/:s/claim | Claim or release a slice | write |
POST /api/projects/:p/push | File bundle plus base revision → commit (409 on conflict) | write |
GET /api/projects/:p/pull?since=<rev> | Changed files since a revision | read |
GET /api/projects/:p/artifacts/*path | Read any artifact (optionally ?rev=) | read |
GET /api/projects/:p/spec | The converged project Spec | read |
GET /api/projects/:p/graph | rebuild-graph.json plus slice colouring | read |
GET /api/projects/:p/convergence | Uncovered, overlaps, conflicts, seams, % | read |
GET/POST /api/projects/:p/runs | Record or list CLI runs and metrics | read / write |
GET /api/projects/:p/search?q= | FTS5 over docs and Spec | read |
3.11 Security and data policy
- Code stays local. The server never needs repository access or credentials. The CLI scans and analyzes locally and pushes artifacts only:
- manifests (paths, symbol names, line numbers);
- docs;
- Spec;
- findings and questions;
- rebuild maps and verification.
- Code appears on the server only where docs already quote it, such as grill evidence quotes. Clients can disable quotes per project (
policy.quotes: false), in which case the CLI strips fenced code from docs on push. - Allow-list on push. Only known artifact paths are accepted, on both the server and the CLI.
- Secret scrubbing before push (§3.6).
- Audit: every write is a git commit plus a
runsrow, tied to a named user. - Self-hosted by default. The server runs inside the client's network next to the code, so artifacts never leave it unless the client mirrors them.
3.12 Scale and scope
Large codebases:
- Run one full scan locally, push the manifest, and propose slices.
- People and agents then analyze slices in parallel: each
rw-analyze --slice ordersrun sees only that slice's seeds. - The server's convergence keeps the whole picture honest.
- Incremental refresh (
detect-changes) re-opens only the slices whose ids changed.
Out of scope for day 0 (possible later options):
- SSO/OIDC (put a reverse proxy in front for now);
- multi-tenant SaaS;
- server-side repo cloning and scheduled re-scans;
- a Cloudflare-native variant;
- fine-grained RBAC beyond read/write/admin plus project allow-lists;
- real-time collaborative editing (a git commit per push is enough).
04 · Target Kits, recipes, blueprints and holes
In short: A Target Kit is a client's golden path packaged as a versioned git repo: a typed stack profile, conventions, a type map, recipes (pure Spec→code generators) and blueprints (recipes composed into whole services). Play runs the Kit over the Spec to generate code that is correct by construction. Anything it can't derive becomes an explicit hole for the LLM. Kits are mined mainly from the client's own reference app.
4.1 Why kits
Today uw-build-layer writes every line with an LLM. The trouble with that:
- Forty tables come out forty slightly different ways.
- Conventions drift between slices.
- Every re-run is a fresh roll of the dice.
Most of a rebuilt service is structural: schema, routes, validators, wiring, repositories, test skeletons. Structure is fully determined by the Spec plus the client's conventions. Kits make that part deterministic, reviewable and repeatable, and leave the LLM to do what only it can: business logic.
Kits are per client because "the target" is never just "Hono + Drizzle". It is this client's Hono + Drizzle: their error envelope, logging, id strategy, folder layout and internal SDKs.
4.2 Kit repo layout
acme-kit/
├── kit.yaml # name, version, extends, compatible spec schemaVersion
├── profile.yaml # typed stack profile
├── conventions.yaml # naming, layout/module map, error model, logging, ids, pagination
├── type-map.yaml # Spec neutral types → target types
├── recipes/
│ └── entity-drizzle-table/
│ ├── recipe.ts # or recipe.yaml for simple declarative recipes
│ ├── templates/table.ts.tmpl
│ └── fixtures/{spec.json, expected/**}
├── blueprints/
│ ├── crud-service.yaml
│ └── full-app.yaml
└── exemplars/ # provenance: ref-app files each recipe was mined from (commit-pinned)
# kit.yaml
name: acme-kit
version: 1.4.0
extends: unwind/hono-drizzle-zod@^1 # inherit the starter kit, override what differs
spec: ">=1.0 <2"
# profile.yaml
runtime: cloudflare-workers
language: typescript
framework: hono
apiStyle: rest
orm: drizzle
db: d1
validation: zod
auth: acme-session-sdk
testing: vitest
infra: wrangler
# conventions.yaml (excerpt)
naming: { tables: snake_plural, columns: snake, files: kebab }
layout: { routes: "src/routes/{module}.ts", schema: "src/db/schema/{entity}.ts" }
errors: { envelope: "{ error: { code, message, details } }", validationStatus: 422 }
ids: uuidv7
pagination: cursor
# type-map.yaml (excerpt)
string: { drizzle: "text", zod: "z.string()" }
int: { drizzle: "integer", zod: "z.number().int()" }
datetime: { drizzle: "integer({ mode: 'timestamp' })", zod: "z.coerce.date()" }
uuid: { drizzle: "text", zod: "z.string().uuid()" }
4.3 The recipe contract
Modelled on OpenRewrite's ScanningRecipe: scan → generate → edit.
export interface TargetRecipe<Acc = unknown> {
name: string;
description: string;
/** Precondition: does this recipe apply to this Spec node under this profile? */
appliesTo(node: SpecNode, profile: StackProfile): boolean;
/** Optional cross-node pass (e.g. collect all entities for a schema barrel). */
scan?(spec: Spec, acc: Acc): void;
/** Pure: Spec node + kit → files, source→target map entries, holes. */
generate(node: SpecNode, kit: ResolvedKit, acc: Acc): {
files: GeneratedFile[];
mappings: MapEntry[]; // same shape as rebuild-map/*.json mappings
holes: Hole[];
};
/** Optional: AST edits to shared files (router registry, DI, migrations index). */
edit?(project: TargetProject, acc: Acc): void;
}
Rules:
- Pure and deterministic. The same Spec plus the same Kit version always produce byte-identical output.
- Idempotent. Re-running gives no diff. Generator-owned regions are rewritten, and filled holes are never clobbered.
- AST edits only for shared files (ts-morph for TS targets, tree-sitter otherwise). No regex, per repo convention.
- Equivalent by construction. The output must verify as
equivalentingraph/rebuild-verification.ts: same method plus normalised path, and the same field names and (now) types. - Golden-tested.
fixtures/spec.json→expected/**is checked byunwind play kit test, and every recipe is run twice to assert idempotence. This is our version ofRewriteTest.
Recipes are TS modules plus template files. Recipes for non-TS targets still emit text and are finished by a target-language formatter. Simple recipes can be declarative (recipe.yaml: template plus appliesTo selector).
4.4 Blueprints
A blueprint is a declarative composition of recipes, the analogue of a declarative recipe list. Each one covers a whole service, module or application skeleton.
# blueprints/crud-service.yaml
name: crud-service
description: REST CRUD module per entity, with validation, repository and tests
recipes:
- scaffold # package.json, tsconfig, app entry, wrangler, vitest (once)
- entity-drizzle-table: { for: entity }
- entity-zod-schema: { for: entity }
- entity-repository: { for: entity, options: { softDelete: true } }
- endpoint-hono-route: { for: endpoint }
- route-registry # edit phase: mount routes in src/app.ts
- endpoint-parity-test: { for: scenario } # native tests from Spec scenarios (§6.6)
During pl-plan, each Spec slice is assigned a blueprint: crud-service for the orders module, event-consumer for webhooks, and so on.
4.5 Holes: the deterministic/LLM boundary
A recipe emits a hole wherever the Spec does not determine the code. Typical cases are business-rule bodies, non-trivial mappings and bespoke validation.
// src/routes/orders.ts (generated by endpoint-hono-route@1.4.0; do not edit outside holes)
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { CreateOrder } from "../schemas/order";
import { ordersRepo } from "../db/repos/orders";
export const orders = new Hono();
// @unwind-id endpoint:src/routes/orders.ts:POST /api/orders
orders.post("/api/orders", zValidator("json", CreateOrder), async (c) => {
const input = c.req.valid("json");
// @unwind-hole id=operation:src/services/orders.ts:applyDiscount kind=operation doc=layers/service/orders.md#applydiscount
throw new Error("unwind-hole: applyDiscount not implemented");
// @unwind-hole-end
const order = await ordersRepo.create(c.env.DB, input);
return c.json(order, 201);
});
- The LLM builder fills only holes. It works from the linked doc and Spec node, and keeps the
@unwind-idprovenance marker. - Verification. A Spec node whose target still contains an unfilled hole is
claimed, notpresent, so it does not count toward completeness. - Regeneration is safe. The generator rewrites everything outside
@unwind-hole … @unwind-hole-end, and preserves the hole's contents once filled.
4.6 The Play build loop
unwind play generate --slice database --plan: a dry run that lists the files, map entries and holes.unwind play generate --slice databasewrites:- the target files;
docs/unwind/.cache/rebuild-map/<slice>.generated.json, whichmerge-rebuild-mapfolds in unchanged;holes.json.
pl-build-layeris dispatched with holes plus the unmapped[MUST]nodes fromrebuild-graph.json. This also fixes today's mismatch, where the untagged seed file is pasted in.unwind play merge, thenverify, thenparity. The loop continues until completeness % and parity % reach their targets, or until two dry rounds pass (the existingLoopState).
4.7 Kit mining (primary authoring path)
Clients rarely want "a generic Hono app". They want their service template. So we mine it:
- Rewind the reference app. Run the normal scan, Semantic Model and Spec on the client's golden-path service.
- Infer the deterministic parts with
unwind play kit mine --from <ref-repo>:profile.yaml, from dependency manifests and detected frameworks;conventions.yaml, from naming and layout statistics;type-map.yaml, from observed Spec type → target type pairs.
- Pick an exemplar per recipe slot that the chosen blueprint needs (entity, route, repository, test, …). The selection is ranked by Spec completeness and conformity with conventions.
- Parameterise. The LLM turns each exemplar into a template plus
recipe.ts. The exemplar's own Spec fragment becomes the golden fixture. - Regenerate gate. A recipe is accepted only if it regenerates its exemplar from that Spec fragment, structurally equivalent per the verifier (and textually close after formatting). Golden tests then lock it in.
- Version. Tag the kit (
v1.0.0). Each rebuild pinskit@versioninrebuild-state.json.
Secondary paths:
- Hand-authored recipes, written by platform engineers like any other TS module.
- In-flight promotion. During a rebuild, the LLM hand-builds the first instance of a pattern. Play offers to promote it into a recipe (through the same regenerate gate), and the recipe then generates the remaining N.
4.8 Starter kits
Play ships a starter kit as the reference implementation of the format. The first is hono-drizzle-zod (TypeScript, Workers/Node; recipes: scaffold, entity → Drizzle table, entity → Zod schema, entity → repository, endpoint → Hono route, route registry, scenario → vitest test). Client kits usually extends a starter and override conventions and templates. Later starters are listed in 08 (Spring/JPA, FastAPI/SQLAlchemy).
4.9 What kits do not do
- They do not translate code. Kits generate from the Spec, never from the source code. That is the whole point of the Spec boundary.
- They do not guess. An unknown type or rule becomes a hole and is reported, never invented.
- They do not replace review. Generated slices arrive as small, mapped diffs in the target repo, and the holes are listed for reviewers.
05 · Semantic Model and store
In short: Borrow the semantics of the LST (types and symbol binding), not its losslessness, because Unwind never edits the source. Build the model in tiers (compiler-accurate T2, index-based T1, tree-sitter T0), each falling back gracefully to the one below. Store it as additive manifest facts plus fact tables, and add a local index later for multi-repo queries.
5.1 What we have today
The analysis is in packages/core/src (see structure/, imports/, layers/, graph/). Its limits:
| Area | Today | Gap |
|---|---|---|
| Symbols | Functions, classes, method/property names, param names (TS/JS, Python, Rust, Java, C#) | No types, no return types, no generics, no inheritance, no interfaces/type aliases/enums (TS) |
| Binding | File→file importMap only (relative TS/JS, Java FQN from the path, Python dotted) | No symbol→symbol binding; Rust/C#/TS path aliases unresolved |
| Calls | None | No call graph, no endpoint→handler, no function→table reads/writes. derives_from is declared but never emitted |
| Data model | Table/entity field names (Drizzle and JPA via AST, others via regex) | No column types, keys, FKs, relations or indexes; EF Core has no fields |
| Endpoints | Method + path (Express-style regex; Spring/Nest/FastAPI/ASP.NET via AST) | Class/router prefixes not composed; no request/response shapes, auth or status codes |
| Lifetime | The tree is deleted after per-file extraction (structure/tree-sitter-plugin.ts) | Nothing survives for later queries |
| Ids | kind:path:name | Overloads and same-named methods collide |
These are exactly the facts a rebuild needs to be typed and checkable, and exactly what the verifier lacks (graph/rebuild-verification.ts notes that field types are not in the manifest).
5.2 Tiers
| Tier | How | Languages (initially) | Gives |
|---|---|---|---|
| T2 compiler-accurate | The language's own type checker, in-process in Node | TS/JS via the TypeScript compiler API (ts.createProgram + getTypeChecker(), the same route OpenRewrite takes for JS/TS); Python via pyright (an npm package) later | Resolved types, symbol binding, references, calls, inheritance |
| T1 index-based | Optional external SCIP indexers (Apache-2.0: scip-typescript, scip-python, scip-java, scip-dotnet) | Java, C#, Python, others | Definitions, references, signatures |
| T0 syntactic | Today's tree-sitter extractors, extended | All six grammars | Syntactic types (annotations, DDL column types), decorators as data, composed route prefixes |
Rules:
- Fall back, and report. T2 runs only when the prerequisites exist (e.g. a
tsconfig.jsonand a resolvabletypescript). Otherwise the extractor drops to T1 or T0 and records the tier reached per file:semanticTier: 0|1|2. Unlike OpenRewrite's silentUnknown, a tier shortfall is visible in coverage reports and in the dashboard. - No hand-rolled regex for new facts. Tree-sitter, compiler APIs or real parsers only. SQL uses a real SQL parser, and ORM schemas use the ORM's own snapshot or schema (Drizzle
meta/*_snapshot.json,schema.prisma). - Facts, not trees. We extract and persist facts. We never hold or serialise whole trees, which avoids the LST memory ceiling.
5.3 New facts (all additive)
Additions to manifest/manifest-schema.ts are optional fields only. FileSymbols is never reshaped.
| Fact | Where it lands |
|---|---|
| Field / param / return types | SymbolDefinition.fieldTypes?, SymbolFunction.paramTypes?, returnType? |
| Inheritance, interfaces, type aliases, enums | SymbolClass.extends?, implements?; new optional types?[] on FileSymbols |
| Decorators / annotations as data | decorators?: {name, args}[] on symbols |
| Symbol references | `symbolRefs?: {from, to, kind: call |
| Endpoint handler + full path | SymbolEndpoint.handler?, fullPath? |
| Column types, keys, FKs, relations, indexes | SymbolDefinition.columns?: {name, type, nullable, pk, fk?, default?}[] |
| Config/env surface | New fact table config |
| Messaging producers/consumers | New fact table messaging (finally setting hasMessaging in the layer map) |
| Tier and provenance | semanticTier? per file; provenance on facts |
The graph gains real edges. build-graph.ts emits calls, reads and writes, plus derives_from (already declared in rebuild-graph-schema.ts). This enables:
- endpoint → service → repository → table slicing;
- impact analysis for
uw-refresh; - better ordering for Play.
Ids gain class-qualified and arity-qualified forms (method:path:Class.name(n)). The old forms are kept as aliases, so existing anchor-id docs keep matching.
5.4 Detector recipes and fact tables
layers/contract-detectors.ts (about 1,160 lines, mixed AST and regex) becomes a registry of small detectors, the analogue of OpenRewrite's search recipes and data tables:
export interface Detector<Row> {
name: string; // "drizzle-tables", "spring-endpoints", "prisma-models"
appliesTo(file: ManifestFile): boolean;
detect(ctx: { tree?: Tree; source: string; model: SemanticModelView }): Row[];
table: FactTableName; // "entities" | "endpoints" | "config" | "messaging" | ...
}
- One detector per framework or ORM. Each has fixtures (input file → expected rows), like the existing
layers/*.test.ts. - Each fact table is typed and becomes a queryable rows file (
.cache/facts/<table>.json). - Community-extensible: adding a framework means adding a detector and its fixtures, not editing a monolith.
- The regex fallback stays only for files with no grammar or parser (graceful degradation), as today.
5.5 Store
- Phase 1 (now to the App). Files stay authoritative under
docs/unwind/.cache/:scan-manifest.json,facts/*.json,spec.json. They are git-friendly and diffable, as today. - Later. A local index in
node:sqliteover models, Specs, fact tables and verification results, keyed byrepo@commit. It enables:- multi-repo portfolio queries ("every endpoint touching
ordersacross 40 services"); - the Recipe Book (which kits and recipes were used where);
- the App's views.
- multi-repo portfolio queries ("every endpoint touching
- Rebuildable. The index can always be rebuilt from the files. It is never the only copy.
- Incremental. Today's fingerprints (
fingerprint/fingerprint.ts) drive re-extraction. Once call edges exist, a body change that alters calls, reads or writes is no longer "cosmetic".
5.6 Spec compilation (rw-spec)
unwind rewind spec builds the Spec (02 §2.5) from three sources:
- the Semantic Model's facts (types, shapes, bindings);
rebuild-graph.json(priorities, coverage, grill verdicts);- fenced blocks in the tagged layer docs: DDL, JSON Schema and OpenAPI, which
analysis-principles.md§2/§8 already asks specialists to write. These are parsed with real parsers.
Precedence: T2 facts first, then T1, then doc blocks, then T0. Disagreements are recorded as conflicts, not silently resolved. Each unresolved type becomes unknown and is flagged; Play will turn it into a hole.
5.7 What we deliberately don't copy
- Losslessness and format preservation. Unwind never prints the source back.
- A JVM host, RPC peers and proprietary artifacts. Everything runs in Node with optional external indexers.
- In-place source edits. Play generates into a new target repo.
06 · Behaviour parity: Spec-derived tests against legacy and rebuild
In short: Structural verification proves the rebuild has the right endpoints and tables. It never proves it behaves the same. We generate stack-neutral scenarios from the Spec and run them twice. Against the legacy app, the results observe and enrich the Spec; against the rebuild, the same scenarios give a behavioural parity verdict. This is characterization (golden-master) testing, driven by the Spec.
6.1 Why
rebuild-principles.md§8 already says present ≠ correct. Today's verifier (graph/rebuild-verification.ts) checks method plus path and field names, and every other kind of node can only ever reachpresent.VerificationDepthalready declaresrun-tests(graph/rebuild-state-schema.ts:31), but nothing implements it.- Running tests against the legacy app has a second payoff. It turns inferred behaviour into observed behaviour:
- real response shapes;
- validation messages;
- edge cases;
- confirmation or refutation of grill hypotheses.
6.2 Scenarios
A scenario is a new Spec node kind. It is stack-neutral and lives in the Spec.
id: scenario:orders:discount-threshold
covers: [endpoint:src/routes/orders.ts:POST /api/orders, operation:src/services/orders.ts:applyDiscount]
priority: MUST
source: generated # generated | legacy-test | grill | traffic | interview | expert
given:
db:
customers: [{ id: c1, tier: wholesale }]
auth: { as: customer, id: c1 }
when:
http: { method: POST, path: /api/orders, json: { customerId: c1, items: [{ sku: A, qty: 3, priceCents: 4000 }] } }
then:
status: 201
json:
totalCents: 11400 # 12000 − 5%
status: pending
db:
orders: { count: +1 }
record: false # true = expected unknown; capture from legacy as golden
coverslinks scenarios to Spec node ids. Behavioural coverage is computed with the same set arithmetic as documentation coverage:[MUST]nodes minus nodes covered by a passing scenario.- Sources:
- Generated from the Spec: per endpoint and entity (happy path, validation failure, auth failure, not-found, pagination), and per
[MUST]operation. - Mined from legacy tests. The
uw-analyze-*-testslayers already catalogue them; we translate their intent into scenarios. - Grill findings: each suspected bug or edge case becomes a probe.
- Captured traffic: HAR files, proxy logs and recorded UI sessions, scrubbed (§6.4).
- Interviews and experts: "users rely on X" (07).
- Generated from the Spec: per endpoint and entity (happy path, validation failure, auth failure, not-found, pagination), and per
6.3 Boundary drivers
Drivers are pluggable. When a driver cannot run, the scenario is still kept and marked manual / not runnable.
| Driver | Mechanism | Difficulty | Order |
|---|---|---|---|
| HTTP / API | fetch-based runner; JSON/body/status/header assertions | Low | 1st |
| DB state | Seed fixtures plus before/after table snapshots; assert side effects, not just responses | Low–Med | 1st |
| CLI / batch / files | Inputs → stdout, exit code and output files vs goldens | Low | 2nd |
| Messaging | Publish/consume against a local broker; assert emitted events | Med | 2nd |
| Web UI | Playwright (Apache-2.0) for replay. Agent-driven browser use (Chrome DevTools MCP / Claude in Chrome) to explore and record, then frozen into a deterministic Playwright script | Med–High | 3rd |
| Desktop / legacy GUI | Agent computer-use to explore and record, plus OS accessibility automation (e.g. Windows UI Automation) for replay | High (best-effort) | 4th |
Exploration by agents is non-deterministic, but replay must be deterministic. Recorded sessions are therefore converted into scripted scenarios before they count toward parity.
6.4 Normalisation and intentional differences
- Scrubbers for nondeterminism: generated ids, timestamps, ordering of unordered collections, tokens and nonces. Each scrubber is declared per scenario or globally.
- A mapping layer for intentional differences, reusing the structural verifier's equivalence rules:
- path-parameter normalisation (
normalizeEndpointPath); - field-name normalisation (
norm); - target conventions from the Kit (e.g. error envelope, status 422 vs 400).
- path-parameter normalisation (
- Verdict-aware.
[DON'T]items are excluded.fix-in-rebuildgrill verdicts expect the legacy result to differ; the scenario holds the corrected expectation, and legacy is recorded only as a reference.
6.5 Lifecycle
-
rw-observe(Rewind):- generate or collect scenarios;
- run them against a sandboxed legacy instance;
- record goldens where
record: true; - write observations back into the Spec with provenance
observed.
Where an observation disagrees with the docs, a grill question or context gap is raised (07).
-
pl-parity(Play): run the same scenarios against the target, apply scrubbers and the mapping layer, and writeparity-report.jsonplusparity-gaps.md. -
Verification depth
behavioural, which implements today'srun-testsdepth. Parity % over[MUST]scenarios becomes a second termination signal for loop mode, alongside structural completeness %.
6.6 Parity tests live on in the target
Kit recipes (e.g. endpoint-parity-test in 04 §4.4) can emit scenarios as native tests in the target stack, for example vitest plus Hono's test client. The parity suite then stays in the rebuilt repo as its permanent regression suite, owned by the team, and needs no Unwind at runtime.
6.7 Safety and limits
- Never run against production.
rw-observerequires an explicit legacy base URL or connection, plus a sandbox confirmation. - Read-only first. Scenarios that change state run only against disposable or seeded environments (a Docker recipe, a snapshot restore).
- Secrets and personal data. Captured traffic is scrubbed before it is stored. Goldens never contain credentials.
- When the legacy app can't run at all:
- scenarios still exist as Spec-level acceptance criteria;
- expected results come from domain experts via the questionnaire flow (
docs/unwind/questions/, see 07); - they run against the target only.
- Not a proof. Parity covers the scenarios that exist. Behavioural coverage (§6.2) makes the uncovered remainder visible rather than implied.
07 · Context gaps: find what the code can't tell us, and ask the people who know
In short: Once a Spec exists, an agent round sweeps it for context gaps: intent, usage, non-functional requirements and tribal knowledge that no amount of code reading can recover. Each gap is routed to who can answer it, packaged into interview briefs for stakeholders, end users, existing developers and ops, and the answers are ingested back into the Spec with provenance. How the interviews are conducted is deliberately left open; an external AI-interview tool plugs in through a file contract.
7.1 Why
Each existing mechanism has a blind spot:
- Coverage proves every item is documented.
- Parity proves what the system does (06).
- Grilling challenges whether behaviour should be kept.
None of them capture intent and lived context, which is where rebuilds fail quietly:
- why a rule exists, and whether the reason still holds;
- which features are actually used, and which are dead weight;
- workarounds users rely on, including "bugs" that became features;
- volumes, SLAs, peaks and retention: non-functional requirements that never appear in code;
- regulatory and contractual drivers;
- planned changes the rebuild should anticipate;
- what developers know is fragile, and what ops does by hand.
7.2 Where it sits
It runs after rw-spec, ideally after rw-grill and a first rw-observe pass, and before pl-plan. It can be re-run whenever the Spec changes; uw-refresh can trigger it for affected slices.
7.3 rw-context-gaps: the agent round
Specialist agents sweep these inputs:
- the Spec;
- the layer docs;
- grill findings;
- parity observations;
- git signals: churn, authorship, age, dead code, TODO/FIXME/HACK comments, reverted commits.
Each gap they find is classified:
| Gap type | Example signal |
|---|---|
Unexplained [MUST] rule | Operation with no rationale; magic threshold 10000 |
| Usage unknown | Endpoint with no tests, no observed traffic, no frontend caller |
| Observed ≠ documented | Parity recorded 400 where the docs say 422 |
| Implicit workflow | Five endpoints always called in sequence from one screen |
| Missing NFR | No timeout/retry config; no retention policy for audit_log |
| Integration ownership | External API called with no owner and no contract |
| Manual ops | Runbook-shaped scripts; cron entries outside the repo |
| Persona / permission | Auth roles referenced in code but not explained anywhere |
Each gap records:
- evidence (quoted code and line, like grill findings);
- the Spec node ids it would resolve;
- priority, derived from the
[MUST]impact; - the who-can-answer route: business stakeholder, end user (by persona), existing developer, ops/support, or compliance.
As in the grill, gaps the code can answer are settled in-run. The rest go into the gap register (docs/unwind/.cache/gaps/register.json).
7.4 Interview briefs: the outbound contract
The briefs are written to docs/unwind/interviews/briefs/<audience>/<capability>.{md,json}: one per audience and business capability, in both a human-readable and a machine-readable form.
# Brief · Stakeholder · Order pricing
**Context.** The current system applies a 5% discount to orders over €100 and never
combines it with coupons. (Diagram: order flow.) We are rebuilding this service.
**Goals.** Confirm which pricing rules must carry over, and why.
## Questions
1. **Why does the 5% wholesale discount exist?** *(gap G-014 · operation:…:applyDiscount)*
- Intent: is this contractual, promotional, or historical?
- Probe: is €100 still the right threshold? Who could change it?
2. **Should discounts ever stack with coupons?** *(gap G-015)*
- Intent: the code forbids it, but support tickets suggest customers expect it.
**Suggested interviewees:** Head of Sales; wholesale account manager.
The JSON form carries the same content plus:
gapIdsandspecNodeIds;- the question intent and follow-up probes;
- suggested interviewees: derived from git authorship for developers and from personas for end users, but never contacting anyone automatically.
The format is designed so an external AI-interview tool can run a rich, adaptive interview from it, while staying simple enough for a human interviewer.
7.5 Responses and ingest: the inbound contract
- In:
docs/unwind/interviews/responses/*, holding transcripts or structured answers in whatever format the interview tool produces. - Adapter contract (small and tool-specific), mapping each answer to
{ gapId, answer, confidence, intervieweeRole, date, quote? }. - Ingest (
unwind rewind context-ingest): an agent applies the answers. It can:- add a rationale to rules;
- retag priorities, with a mandatory rationale, exactly like grill verdicts (
drop→[DON'T]); - write a
fix-in-rebuildcorrection into the doc body; - create parity scenarios from answers like "users rely on X" (06 §6.2);
- raise follow-up gaps.
- Provenance. Every change is stamped
interview:<role>:<date>. - Conflicts are surfaced, never resolved silently. Disagreements between the code, observed behaviour and interviews become new gaps or grill questions.
7.6 One mechanism, not two
Today uw-grill writes checkbox questionnaires for domain experts into docs/unwind/questions/, and grill-answers.mjs ingests the ticks. In the destination design, those questionnaires become one audience-specific output of the context-gap round: a "domain expert, checkbox format" brief. They use the same register, the same ingest path and the same provenance. The grill keeps its job of finding hotspots; the context-gap round owns asking people.
7.7 Context coverage
Context coverage is the share of [MUST] Spec nodes with no open high-priority gap. It is reported alongside:
- documentation coverage (
verify-coverage); - structural completeness and behavioural parity (
verify/parity).
All three together make readiness for Play measured, not asserted. pl-plan shows them up front and warns before building slices that still have open high-priority gaps.
7.8 Open by design
How interviews are scheduled and conducted is deliberately left out of scope: human, AI-led, async survey, or a workshop. The brief and response formats are the contract. Integrating a specific tool (for example the user's own AI-interview product) is an adapter: either a file hand-off or an API push, to be decided (09).
08 · Roadmap
In short: Nine phases from today's
uw-*plugin to the destination. Each phase is shippable on its own, keeps the graceful fallback to today's flow, and has a testable exit criterion, proven on the drizzle-cube example where possible. The order delivers value early: Spec first, then the CLI, the shared server MVP (team use on large codebases, slices as first-class) and the split, then deterministic generation, then richer semantics, mining, behaviour, context and slice convergence, then surfaces.
8.1 Phases
| Phase | Steps | Exit criteria |
|---|---|---|
| 0. Design | This document set and the HTML site. | Reviewed and agreed. |
| 1. Shared model + Spec v1 | Extract @unwind/model from packages/core (ids, schemas). Define Spec v1 (02 §2.5). Add rw-spec, compiling the Spec from manifest + graph + tagged docs, with fenced DDL/JSON-Schema/OpenAPI parsed by real parsers. Add a typed stack profile written by the plan interview, replacing the API-style regex in skills/scripts/verify-rebuild.mjs. | drizzle-cube produces a valid Spec with typed entities and endpoints. |
| 2. CLI + Server MVP + Rewind/Play split | Consolidate skills/scripts/*.mjs into the unwind CLI (@unwind/engine + @unwind/cli, --json everywhere); turn the scripts into shims. Server MVP (03): unwind serve (Hono + node:sqlite + system git, one Docker image); bearer-token auth (login / whoami / logout); projects; push / pull / status with optimistic concurrency and secret scrubbing; slices (propose from the import graph, claim, state machine, per-slice coverage); basic UI (projects, slice board, slice detail with DocsViewer, graph coloured by slice, activity, tokens). Two plugins in one repo; rw-* / pl-* skills; uw-* aliases; manifests and marketplace updated. Play reads the Spec, plus docs for semantics. | Both plugins install independently, and the pipeline passes on drizzle-cube offline. Two users push the drizzle-cube analysis as ≥ 3 slices to one Docker server and see it on the slice board; a concurrent push to a different slice does not conflict, and a push touching the same path returns 409 and succeeds after pull. |
| 3. Kit format + recipe engine + starter kit | Kit schema and loader with extends; recipe runtime (scan / generate / edit, idempotent apply, hole protection); blueprint composer; golden-fixture runner (kit test); hono-drizzle-zod starter kit; pl-build runs generate → holes → LLM → verify; the verifier counts holes and checks types; the scaffold recipe sets config.scaffolded. | drizzle-cube's database and API slices generate, compile and verify equivalent. Re-runs give no diff. |
| 4. Semantic Model T0/T2 | TS compiler-API tier; tree-sitter type and route-prefix extraction; calls/reads/writes/derives_from edges; handler binding; per-file semanticTier; detector-recipe registry refactor of contract-detectors.ts. | Spec entities and endpoints are fully typed for TS sources. The verifier diffs field types. |
| 5. Kit mining | play kit mine (profile, conventions, type map); exemplar selection; LLM parameterisation behind the regenerate gate; kit versioning and pinning; in-flight promotion. | A kit mined from one reference repo rebuilds another repo's slice in that house style. |
| 5b. Behaviour parity | Scenario schema in the Spec; generation from Spec + legacy tests + grill findings; HTTP + DB-state drivers first; scrubbers and mapping layer; rw-observe (record goldens, enrich the Spec with provenance); pl-parity and the behavioural verification depth; kit recipes emit native target tests. Then browser (Playwright plus agent recording), CLI/batch and messaging drivers; desktop is best-effort. | drizzle-cube's API scenarios are recorded against the legacy app and replayed against the rebuild, with a parity % over [MUST] scenarios. |
| 5d. Slice convergence + Play by slice | Slice Spec fragments merged into spec/project.spec.json on push; uncovered / overlaps / conflicts / seams report and resolve UI; convergence % metric; seams as interface contracts plus parity scenarios; Play slice states (planned → generating → filling → verified → cut-over); seam graph drives the strangler-style build order; legacy adapters for unsatisfied seams. | drizzle-cube reaches 100% convergence across its slices; one slice is rebuilt and verified on its own, with its seams to unrebuilt slices satisfied by adapters. |
| 5c. Context gaps | Gap taxonomy and register schema; rw-context-gaps agent round; audience routing; brief format (md + json); response adapter contract plus context-ingest; provenance on Spec nodes; grill questionnaires folded in; context-coverage metric. The external interview tool is an adapter, not core. | drizzle-cube produces routed briefs for ≥ 3 audiences, and a sample response set ingests back into the Spec with provenance. |
| 6. MCP adapter | unwind mcp: a stdio MCP server whose tools map 1:1 onto CLI commands and server API routes, with no separate logic. The skills keep using the CLI. | Non-Claude-Code agents can drive Rewind/Play with results identical to the CLI. |
| 7. App depth + portfolio | Server UI grows: metrics over time, convergence and conflict resolution UX, questions and interview briefs answered in place, FTS search, multi-project portfolio, Recipe Book and Kit browser, kit editor, optional mirror push of artifacts to GitHub/GitLab. | A portfolio view across N projects; kits browsable and editable in the App; questions answered in the UI land as commits. |
| 8. Breadth | SCIP tier for Java/C#/Python; starter kits for Spring/JPA and FastAPI/SQLAlchemy; blueprints for event consumers, scheduled jobs and BFFs; more parity drivers. | ≥ 3 source languages typed, and ≥ 3 starter kits. |
8.2 Dependencies
0 ─► 1 ─► 2 ─► 3 ─► 5 (mining needs the recipe engine)
│ ├──► 5b (parity uses the Spec; native tests use kits)
│ └──► 5d (convergence needs server slices (2) + Spec; Play-by-slice needs 3)
├──► 4 (can start after 1; strengthens 3's verification and seam detection)
└──► 5c (needs the Spec; better after 5b observations)
2 ─► 6 ─► 7 ─► 8
Phases 4, 5b, 5c and 5d can run in parallel with 3 once the Spec exists. Phase 8 is open-ended.
8.3 Cross-cutting rules for every phase
- Graceful fallback. If the new path is unavailable (no kit match, no TS compiler, no runnable legacy app, no server), the previous behaviour runs and the skill says so.
- Additive schemas. Optional fields only. The
schemaVersionbumps and migrations are documented in@unwind/model. - AST over regex for every new detector and every target edit.
- Tests next to the source (
node --test, as inpackages/core/src/**/*.test.ts). Every recipe and detector ships with fixtures. - Docs move with the code.
CLAUDE.md, the README and the principle files (skills/analysis-principles.md,skills/rebuild-principles.md) are updated in the same phase. In particular,rebuild-principles.mdgains sections on holes, the Spec and kits.
8.4 Files most affected (for orientation)
| Today | Becomes |
|---|---|
packages/core/src/manifest/{manifest-schema.ts, candidates.ts} | @unwind/model |
packages/core/src/layers/contract-detectors.ts | Detector-recipe registry |
packages/core/src/structure/tree-sitter-plugin.ts | T0 tier plus the T2 hook |
packages/core/src/graph/{build-graph.ts, rebuild-verification.ts, rebuild-state-schema.ts} | New edges; hole, type and behavioural verdicts; kit pin; scaffolded used |
skills/uw-plan, skills/uw-build, skills/uw-build-layer | pl-plan, pl-build, pl-build-layer (Kit-aware) |
skills/scripts/*.mjs | unwind CLI subcommands (with shims) |
skills/uw-grill questionnaires | One output of rw-context-gaps |
packages/dashboard | @unwind/app: the server UI (served by unwind serve) |
| (new) | @unwind/server: Hono API, git + node:sqlite storage, auth, slices, convergence |
09 · Open questions
In short: These decisions are deliberately deferred. Each has a recommendation, so we can move forward by default and revisit when the evidence arrives. Comment on them by number.
9.1 Naming and prefixes
- Question: Are "Rewind" (understand) and "Play" (rebuild) the plugin names, with Unwind as the umbrella brand? Are the skill prefixes
rw-andpl-? - Recommendation: Yes. Keep the
uw-*names as deprecated aliases for one release.
9.2 Recipe language
- Question: Should recipes be TS modules plus templates, fully declarative YAML, or both?
- Recommendation: TS modules plus template files as the main form, because the engine and the first starter target are TS. Offer declarative
recipe.yamlfor simple template-only recipes. Recipes for non-TS targets emit text and are finished by a target-language formatter.
9.3 Kit licensing and sharing
- Question: How are kits licensed and shared?
- Recommendation: Client kits are private repos owned by the client. Starter kits use the same licence as Unwind (MIT). A future public "Recipe Book" index lists starter and community kits only.
9.4 Spec and external standards
- Question: Should the Spec define its own formats, or embed existing standards?
- Recommendation: Embed. Use JSON Schema for shapes and OpenAPI-style operations for endpoints, wrapped in Spec nodes that carry id, priority and provenance. This gives import/export to existing tooling for free.
9.5 Store timing
- Question: When does the
node:sqliteindex arrive? - Recommendation: Day 0, inside the server (phase 2, 03 §3.4). Locally, the CLI stays file-only. On the server, git holds the artifacts and SQLite holds auth/ops state plus a rebuildable index.
9.6 Interview tool integration
- Question: Do we push briefs to the external AI-interview tool via its API, or hand off files?
- Recommendation: Start with file hand-off (briefs out, responses in), because it needs no credentials and is easy to audit. Add an API adapter once the tool's interface is stable. The brief and response formats (07) are the contract either way.
9.7 Parity environments
- Question: Who provides a runnable legacy sandbox?
- Recommendation: Support all three options: a client-provided environment, a Docker recipe produced from the infrastructure layer, and recorded traffic as the zero-setup fallback. Never production.
9.8 Scenario format
- Question: Should scenarios use a custom YAML schema, or reuse an existing one (e.g. OpenAPI Arazzo workflows for API flows)?
- Recommendation: Custom and minimal (given / when / then / covers), with import/export adapters for Arazzo and HAR. Arazzo is API-only, and scenarios must also cover UI, desktop, CLI and messaging.
9.9 Behavioural verification depth
- Question: Should
run-tests(graph/rebuild-state-schema.ts:31) be renamedbehavioural, and how should it combine with structural completeness? - Recommendation: Keep
run-testsas the stored enum value for compatibility and label it "behavioural" in the UI. Loop termination requires both structural completeness % and[MUST]parity % to reach their targets.
9.10 Id migration
- Question: How do we introduce class-qualified, overload-safe ids without breaking existing anchor-id docs?
- Recommendation: Emit the new ids alongside the old ones as aliases. Coverage matches on either. A one-time
rw-completepass can rewrite anchors.
9.11 Optional external tools
- Question: Should the plugin bundle SCIP indexers, pyright or Playwright, or discover them?
- Recommendation: Discover, never bundle. Each tool is optional, its tier or driver is reported when it is missing, and a helper suggests the install command.
9.12 Generated code ownership
- Question: After hand-off, who owns the generated files, and can a team "eject" from regeneration?
- Recommendation: Yes.
unwind play eject <slice>strips the generator markers and keeps the@unwind-idprovenance comments, so verification still works. From then on the slice is hand-maintained.
9.13 Slice auto-proposal algorithm
-
Question: How should
unwind slices proposecut a large codebase into slices? -
Recommendation: Deterministic and explainable first:
- community detection (e.g. Louvain/Leiden) over the import graph, later the call graph;
- seeded by top-level directories and layers;
- balanced by candidate count (target ~200–800 candidates per slice);
- each proposal shows its cohesion and seam count.
An LLM pass may then suggest business-capability names. Humans always confirm.
9.14 Conflict resolution UX
- Question: When two slice fragments disagree on the same id (priority or content), how is it resolved?
- Recommendation:
- The server never auto-picks. The convergence view shows both versions side by side with their provenance, and one owner resolves.
- The resolution is a normal commit with a rationale, like grill verdicts.
- Push still succeeds, but the slice can't reach
acceptedwhile it has open conflicts.
9.15 Git layout: per project, or a branch per slice
- Question: Should each slice work on its own git branch in the project repo, or should everything live on
mainwith per-slice folders? - Recommendation: One repo per project and one
mainbranch, with per-slice folders (03 §3.4). Path-level optimistic concurrency makes slice conflicts rare, convergence always reads one tree, and history stays linear. Revisit "review branches" (push to a branch, approve intomain) if teams want PR-style review of analysis.
9.16 Cloudflare / hosted variant
- Question: Should the server also run Cloudflare-native (Workers + Durable Objects SQLite + R2) or as hosted SaaS?
- Recommendation: Later, behind a storage interface. Day 0 is self-hosted Node + Docker only, because clients want artifacts inside their network and real git is simplest there. Keep the git and SQLite access behind a small
ArtifactStore/StateStoreinterface so a Cloudflare adapter is possible without touching the API.
9.17 Server auth beyond tokens
- Question: When do we need SSO/OIDC and finer-grained roles?
- Recommendation: Not on day 0. Use bearer tokens with read/write/admin scopes plus project allow-lists, and put a reverse proxy (or Cloudflare Access) in front for SSO. Add native OIDC only when a client requires it.