PBI-58: Developer manual + in-app /manual page (#148)

* docs(PBI-58): add developer manual chapters under docs/manual/

Adds a 7-file English-language manual targeted at new human contributors:
index, overview, statuses & transitions (with mermaid state diagrams),
git workflow, MCP integration, docker, and troubleshooting. The manual
is the *map* — it cross-references existing runbooks/ADRs/architecture
docs rather than duplicating their content.

Regenerates docs/INDEX.md and validates with check-doc-links.mjs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(PBI-58): add markdown rendering deps + manual:build script

Adds mermaid, rehype-slug, rehype-autolink-headings for the in-app
/manual page. Wires manual:build into prebuild so production builds
always regenerate the chapter TOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PBI-58): codegen script for in-app manual TOC

scripts/build-manual.mjs walks docs/manual/, parses YAML front-matter,
strips it from the body, and emits lib/manual.generated.ts with a typed
ManualEntry[] containing slug, title, description, filePath, and the
embedded markdown body. Pure Node 20, mirrors generate-docs-index.mjs.

Inlining the markdown at build time keeps runtime serverless functions
free of filesystem reads, which avoids whole-project NFT tracing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PBI-58): /manual route renders developer manual chapters in-app

Catch-all route at app/(app)/manual/[[...slug]]/page.tsx with
generateStaticParams covering every TOC entry. Server-side
MarkdownView uses react-markdown with remark-gfm, rehype-slug, and
rehype-autolink-headings; mermaid code blocks are routed to a
client-only MermaidBlock that dynamic-imports mermaid on mount.

ManualSidebar (client) reads the typed TOC and highlights the active
chapter via usePathname.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(PBI-58): add Manual link to main nav bar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-07 18:00:10 +02:00 committed by GitHub
parent d750676f5e
commit bd7478861b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2239 additions and 105 deletions

View file

@ -0,0 +1,42 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getManualChapter, getManualToc } from '@/lib/manual-server'
import { MarkdownView } from '../_components/markdown-view'
type Params = { slug?: string[] }
export async function generateStaticParams(): Promise<Params[]> {
return getManualToc().map((entry) => ({
slug: entry.slug.length > 0 ? [...entry.slug] : undefined,
}))
}
export async function generateMetadata({
params,
}: {
params: Promise<Params>
}): Promise<Metadata> {
const { slug = [] } = await params
const chapter = getManualChapter(slug)
if (!chapter) return { title: 'Manual — not found' }
return {
title: `${chapter.entry.title} — Scrum4Me Manual`,
description: chapter.entry.description.slice(0, 200),
}
}
export default async function ManualChapterPage({
params,
}: {
params: Promise<Params>
}) {
const { slug = [] } = await params
const chapter = getManualChapter(slug)
if (!chapter) notFound()
return (
<div className="mx-auto w-full max-w-3xl px-6 py-8">
<MarkdownView markdown={chapter.body} />
</div>
)
}

View file

@ -0,0 +1,51 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
import type { ManualEntry } from '@/lib/manual.generated'
type Props = {
toc: readonly ManualEntry[]
}
function entryHref(entry: ManualEntry): string {
if (entry.slug.length === 0) return '/manual'
return '/manual/' + entry.slug.join('/')
}
export function ManualSidebar({ toc }: Props) {
const pathname = usePathname()
return (
<nav
aria-label="Manual chapters"
className="sticky top-20 hidden h-[calc(100vh-6rem)] w-64 shrink-0 overflow-y-auto border-r border-border px-4 py-6 lg:block"
>
<p className="mb-2 px-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
Manual
</p>
<ul className="space-y-1">
{toc.map((entry) => {
const href = entryHref(entry)
const isActive = pathname === href
return (
<li key={href}>
<Link
href={href}
className={cn(
'block rounded-md px-3 py-2 text-sm transition-colors',
isActive
? 'bg-primary/10 font-medium text-primary'
: 'text-foreground hover:bg-muted hover:text-foreground'
)}
>
{entry.title}
</Link>
</li>
)
})}
</ul>
</nav>
)
}

View file

@ -0,0 +1,42 @@
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import { MermaidBlock } from './mermaid-block'
type Props = {
markdown: string
}
export function MarkdownView({ markdown }: Props) {
return (
<article className="prose prose-neutral max-w-none dark:prose-invert prose-headings:scroll-mt-20 prose-a:text-primary prose-code:rounded prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:text-sm prose-pre:bg-muted prose-pre:text-foreground">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
]}
components={{
code(props) {
const { className, children } = props as {
className?: string
children?: React.ReactNode
}
const match = /language-(\w+)/.exec(className ?? '')
const lang = match?.[1]
const text = String(children ?? '').replace(/\n$/, '')
if (lang === 'mermaid') {
return <MermaidBlock source={text} />
}
return (
<code className={className}>{children}</code>
)
},
}}
>
{markdown}
</ReactMarkdown>
</article>
)
}

View file

@ -0,0 +1,73 @@
'use client'
import { useEffect, useId, useRef, useState } from 'react'
type Props = {
source: string
}
let mermaidPromise: Promise<typeof import('mermaid').default> | null = null
function loadMermaid() {
if (!mermaidPromise) {
mermaidPromise = import('mermaid').then((mod) => {
const mermaid = mod.default
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'strict',
fontFamily: 'inherit',
})
return mermaid
})
}
return mermaidPromise
}
export function MermaidBlock({ source }: Props) {
const id = useId().replace(/[^a-zA-Z0-9]/g, '')
const containerRef = useRef<HTMLDivElement | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
loadMermaid()
.then(async (mermaid) => {
if (cancelled) return
try {
const { svg } = await mermaid.render(`mermaid-${id}`, source)
if (cancelled) return
if (containerRef.current) containerRef.current.innerHTML = svg
setError(null)
} catch (err) {
if (cancelled) return
setError(err instanceof Error ? err.message : String(err))
}
})
.catch((err) => {
if (cancelled) return
setError(err instanceof Error ? err.message : String(err))
})
return () => {
cancelled = true
}
}, [id, source])
if (error) {
return (
<pre className="overflow-x-auto rounded-md bg-muted p-3 text-xs text-destructive">
<code>
{`Mermaid render failed: ${error}\n\n${source}`}
</code>
</pre>
)
}
return (
<div
ref={containerRef}
className="my-4 flex justify-center overflow-x-auto rounded-md bg-muted p-4 [&_svg]:max-w-full"
aria-label="Diagram"
/>
)
}

View file

@ -0,0 +1,16 @@
import { getManualToc } from '@/lib/manual-server'
import { ManualSidebar } from './_components/manual-sidebar'
export default function ManualLayout({
children,
}: {
children: React.ReactNode
}) {
const toc = getManualToc()
return (
<div className="flex w-full">
<ManualSidebar toc={toc} />
<main className="min-w-0 flex-1">{children}</main>
</div>
)
}

View file

@ -143,6 +143,7 @@ export function NavBar({
: disabledSpan('Solo')}
{navLink('/insights', 'Insights', pathname.startsWith('/insights'))}
{navLink('/ideas', 'Ideas', pathname.startsWith('/ideas'))}
{navLink('/manual', 'Manual', pathname.startsWith('/manual'))}
{roles.includes('ADMIN') && navLink('/admin', 'Admin', pathname.startsWith('/admin'))}
</nav>
</div>

View file

@ -105,6 +105,13 @@ Auto-generated on 2026-05-07 from front-matter and headings.
| [Docker smoke test — task 2](./docker-smoke/2-mei-task-2.md) | `docker-smoke/2-mei-task-2.md` | done | 2026-05-03 |
| [Scrum4Me — Functionele Specificatie](./functional.md) | `functional.md` | active | 2026-05-03 |
| [Scrum4Me — Glossary](./glossary.md) | `glossary.md` | active | 2026-05-03 |
| [Overview](./manual/01-overview.md) | `manual/01-overview.md` | active | 2026-05-07 |
| [Statuses & Transitions](./manual/02-statuses-and-transitions.md) | `manual/02-statuses-and-transitions.md` | active | 2026-05-07 |
| [Git Workflow](./manual/03-git-workflow.md) | `manual/03-git-workflow.md` | active | 2026-05-07 |
| [MCP Integration](./manual/04-mcp-integration.md) | `manual/04-mcp-integration.md` | active | 2026-05-07 |
| [Docker](./manual/05-docker.md) | `manual/05-docker.md` | active | 2026-05-07 |
| [Troubleshooting](./manual/06-troubleshooting.md) | `manual/06-troubleshooting.md` | active | 2026-05-07 |
| [Scrum4Me Developer Manual](./manual/index.md) | `manual/index.md` | active | 2026-05-07 |
| [Scrum4Me — Styling & Design System](./md3-color-scheme.md) | `md3-color-scheme.md` | active | 2026-05-03 |
| [Obsidian as Personal Authoring Layer](./obsidian-authoring.md) | `obsidian-authoring.md` | active | 2026-05-02 |
| [PbiDialog Profiel](./pbi-dialog.md) | `pbi-dialog.md` | active | 2026-05-03 |

View file

@ -0,0 +1,99 @@
---
title: "Overview"
status: active
audience: [contributor]
language: en
last_updated: 2026-05-07
when_to_read: "First chapter — start here for the elevator pitch and project structure."
---
# 01 — Overview
## What is Scrum4Me?
Scrum4Me is a **desktop-first fullstack web app for solo developers and small Scrum teams** who manage multiple software projects in parallel. It models the Scrum hierarchy explicitly (Product → PBI → Story → Task), supports Sprints with split-screen drag-and-drop planning, and integrates Claude Code as an automated implementation worker — every result the agent produces is logged back into the originating story.
The app is deployable to **Vercel + Neon** (default) and can run **fully local** via the worker container. A built-in demo user has read-only access; Product Owners add Developers by username, and those Developers gain write access to that product's stories, tasks, and sprints.
## Entity hierarchy
```mermaid
flowchart TB
Product["Product<br/>(per repo)"]
Idea["Idea<br/>(pre-PBI staging)"]
PBI["PBI<br/>(Product Backlog Item)"]
Story["Story"]
Task["Task"]
Sprint["Sprint<br/>(cross-cutting)"]
Product --> Idea
Idea -.->|"AI-grilled & planned"| PBI
Product --> PBI
PBI --> Story
Story --> Task
Sprint -.->|"contains stories<br/>denormalised on tasks"| Story
Sprint -.-> Task
```
- **Product** — one row per repo. `repo_url`, `definition_of_done`, members.
- **Idea** — pre-PBI staging entity introduced in M12. Goes through `IDEA_GRILL` (AI Q&A loop) and `IDEA_MAKE_PLAN` jobs to produce a structured plan that can be turned into a PBI tree.
- **PBI** — a Product Backlog Item. Has `priority` (14) and `sort_order` (float, see [`docs/patterns/sort-order.md`](../patterns/sort-order.md)).
- **Story** — a unit of value under a PBI; has acceptance criteria. Lives in the backlog (`OPEN`) until added to a sprint.
- **Task** — the smallest unit; has an `implementation_plan` consumed by the Claude worker. `sprint_id` is denormalised from the parent story for query efficiency.
- **Sprint** — cross-cutting time-box. Stories are added to a sprint; tasks inherit `sprint_id`. Sprint execution has two modes: `PER_TASK` and `SPRINT_BATCH` — see [`docs/architecture/sprint-execution-modes.md`](../architecture/sprint-execution-modes.md).
For status lifecycles of each entity, see [02 — Statuses & Transitions](./02-statuses-and-transitions.md).
## Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) + React 19 |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS + shadcn/ui + Material Design 3 tokens via [`app/styles/theme.css`](../../app/styles/theme.css) |
| Client state | Zustand + dnd-kit |
| Database | Prisma v7 + PostgreSQL (Neon) |
| Auth | iron-session + bcryptjs |
| Utilities | Zod, Sonner, Sharp, Vercel Analytics |
| Hosting | Vercel (app), Neon (DB), Mac/NAS Docker (worker) |
For the rationale behind each choice and the technologies we explicitly **don't** use, see [`docs/architecture/overview.md`](../architecture/overview.md).
## Repository layout
```
Scrum4Me/
├── app/ # Next.js App Router routes
│ ├── (app)/ # authenticated desktop UI
│ ├── (auth)/ # login, register, demo
│ ├── (mobile)/ # /m/* mobile shell (3 screens)
│ ├── api/ # REST route handlers (Claude integration)
│ └── styles/ # MD3 token CSS
├── components/ # shared UI components
├── lib/ # server/client utilities
│ └── task-status.ts # the ONLY place DB↔API enum mapping happens
├── prisma/ # schema + migrations
├── docs/ # this manual + ADRs, runbooks, patterns, specs
└── scripts/ # codegen, seeders, link checkers
```
The `*-server.ts` filename suffix marks server-only modules (DB, Node APIs). They must never be imported into a client component — see the hardstop in [`CLAUDE.md`](../../CLAUDE.md#hardstop-regels).
For a deeper structural breakdown including stores, realtime channels, and the job queue, see [`docs/architecture/project-structure.md`](../architecture/project-structure.md).
## Glossary refresh
A few terms used throughout this manual that often differ from "generic Scrum" usage:
- **PBI** — Product Backlog Item. Not "Feature" or "Epic".
- **Story** — A unit of work under a PBI. Not "Ticket" or "Issue".
- **Sprint Goal** — The narrative for a sprint. Not "Objective".
- **Worker** — A Claude Code agent claiming jobs from the Scrum4Me queue (M13).
- **Demo user** — A read-only built-in user; writes return `403`. See [`docs/adr/0006-demo-user-three-layer-policy.md`](../adr/0006-demo-user-three-layer-policy.md).
- **Idea** — Pre-PBI staging artefact (M12). Has its own state machine; see [02](./02-statuses-and-transitions.md#idea).
The complete glossary lives at [`docs/glossary.md`](../glossary.md).
## What's next
→ [02 — Statuses & Transitions](./02-statuses-and-transitions.md) covers how each entity moves through its lifecycle, with state-machine diagrams.

View file

@ -0,0 +1,222 @@
---
title: "Statuses & Transitions"
status: active
audience: [contributor]
language: en
last_updated: 2026-05-07
when_to_read: "Whenever an entity's status changes unexpectedly or you need to know what status comes next."
---
# 02 — Statuses & Transitions
Every persistent entity in Scrum4Me has an explicit status enum. This chapter documents them all, with state-machine diagrams showing allowed transitions, the trigger for each transition (user action vs system / job-driven), and the side effects.
> **Hardstop:** the database stores enums in `UPPER_SNAKE`; the REST API exposes them in `lowercase`. Conversion happens **only** through [`lib/task-status.ts`](../../lib/task-status.ts) — never call `.toLowerCase()` or `.toUpperCase()` directly. See the [DB vs API mapping](#db-vs-api-mapping) section at the end.
## Quick reference
| Entity | Source enum | Statuses |
|---|---|---|
| [PBI](#pbi) | `PbiStatus` | `READY`, `BLOCKED`, `DONE`, `FAILED` |
| [Story](#story) | `StoryStatus` | `OPEN`, `IN_SPRINT`, `DONE`, `FAILED` |
| [Task](#task) | `TaskStatus` | `TO_DO`, `IN_PROGRESS`, `REVIEW`, `DONE`, `FAILED` |
| [Sprint](#sprint) | `SprintStatus` | `ACTIVE`, `COMPLETED`, `FAILED` |
| [SprintRun](#sprintrun) | `SprintRunStatus` | `QUEUED`, `RUNNING`, `PAUSED`, `DONE`, `FAILED`, `CANCELLED` |
| [ClaudeJob](#claudejob) | `ClaudeJobStatus` | `QUEUED`, `CLAIMED`, `RUNNING`, `DONE`, `FAILED`, `CANCELLED`, `SKIPPED` |
| [Idea](#idea) | `IdeaStatus` | `DRAFT`, `GRILLING`, `GRILL_FAILED`, `GRILLED`, `PLANNING`, `PLAN_FAILED`, `PLAN_READY`, `PLANNED` |
## PBI
A **Product Backlog Item** holds one or more stories. Its status reflects whether the PBI as a whole is ready to be picked up, blocked on something external, finished, or written off.
```mermaid
stateDiagram-v2
[*] --> READY: create_pbi
READY --> BLOCKED: user marks blocked
BLOCKED --> READY: user unblocks
READY --> DONE: all stories DONE
READY --> FAILED: user gives up
BLOCKED --> FAILED: user gives up
DONE --> [*]
FAILED --> [*]
```
| Transition | Trigger | Side effect |
|---|---|---|
| `* → READY` | `create_pbi` MCP tool or PBI dialog | New PBI lands in `priority` group, `sort_order = last + 1` |
| `READY ↔ BLOCKED` | User toggles via PBI dialog | None besides log entry |
| `READY → DONE` | All child stories reach `DONE` | Auto-promotion (see [ST-1109 plan](../plans/ST-1109-pbi-status.md)) |
| `* → FAILED` | User gives up on the PBI | Stories may remain `OPEN`; PBI is filtered out of active boards |
## Story
A **Story** sits under a PBI. It moves out of the backlog when added to a Sprint, and reaches `DONE` when its tasks are complete and the implementation is verified.
```mermaid
stateDiagram-v2
[*] --> OPEN: create_story
OPEN --> IN_SPRINT: added to sprint
IN_SPRINT --> OPEN: removed from sprint
IN_SPRINT --> DONE: all tasks DONE + verify passes
IN_SPRINT --> FAILED: verify fails / abandoned
DONE --> [*]
FAILED --> [*]
```
| Transition | Trigger | Side effect |
|---|---|---|
| `* → OPEN` | `create_story` MCP tool or Story dialog | Lives in product backlog |
| `OPEN ↔ IN_SPRINT` | Drag onto Sprint board, or sprint-removal | Tasks denormalise `sprint_id` |
| `IN_SPRINT → DONE` | Story completion via MCP / UI; auto-PR flow may trigger | Auto-PR flow ([`runbooks/auto-pr-flow.md`](../runbooks/auto-pr-flow.md)) may run; PBI is re-evaluated for `READY → DONE` |
| `IN_SPRINT → FAILED` | Verification failure or manual abandon | Logged in story log |
## Task
A **Task** is the smallest unit. The Claude worker mainly reads `implementation_plan` and writes status transitions through MCP tools.
```mermaid
stateDiagram-v2
[*] --> TO_DO: create_task
TO_DO --> IN_PROGRESS: agent claims / user starts
IN_PROGRESS --> REVIEW: implementation done, awaiting verify
REVIEW --> DONE: verify passes
REVIEW --> IN_PROGRESS: verify fails, retry
IN_PROGRESS --> FAILED: unrecoverable error
REVIEW --> FAILED: gives up after retries
DONE --> [*]
FAILED --> [*]
```
| Transition | Trigger | Side effect |
|---|---|---|
| `* → TO_DO` | `create_task` MCP tool / Task dialog | Inherits `sprint_id` from parent story |
| `TO_DO → IN_PROGRESS` | Worker claim or user starts | Story may auto-promote to `IN_SPRINT` |
| `IN_PROGRESS → REVIEW` | Implementation logged | Optional `verify_task_against_plan` runs |
| `REVIEW → DONE` | Verify passes / human accepts | When all sibling tasks are `DONE`, the parent story is eligible for `DONE` |
| `* → FAILED` | Unrecoverable error or human marks failed | Story may auto-promote to `FAILED` |
The MCP tool is `update_task_status({ task_id, status })` accepting lowercase API values: `todo | in_progress | review | done | failed`.
## Sprint
A **Sprint** is the cross-cutting time-box. Its status tracks the overall sprint container, not the agent execution.
```mermaid
stateDiagram-v2
[*] --> ACTIVE: create sprint
ACTIVE --> COMPLETED: user closes sprint
ACTIVE --> FAILED: user abandons sprint
COMPLETED --> [*]
FAILED --> [*]
```
For execution semantics (PER_TASK vs SPRINT_BATCH) see [`docs/architecture/sprint-execution-modes.md`](../architecture/sprint-execution-modes.md).
## SprintRun
A **SprintRun** is one execution attempt of a sprint by the agent worker. Multiple runs may exist over a sprint's lifetime (if a run is cancelled or paused and restarted).
```mermaid
stateDiagram-v2
[*] --> QUEUED: trigger sprint run
QUEUED --> RUNNING: worker claims
RUNNING --> PAUSED: pause requested
PAUSED --> RUNNING: resume
RUNNING --> DONE: all tasks done
RUNNING --> FAILED: unrecoverable
QUEUED --> CANCELLED: user cancels
RUNNING --> CANCELLED: user cancels
PAUSED --> CANCELLED: user cancels
DONE --> [*]
FAILED --> [*]
CANCELLED --> [*]
```
The cascade rules (which task transitions automatically promote the SprintRun) are described in [`docs/plans/sprint-pr-worktree-state-machines.md`](../plans/sprint-pr-worktree-state-machines.md). When calling `update_task_status` from inside a sprint run, pass the optional `sprint_run_id` so the server can validate ownership and propagate cascades.
## ClaudeJob
The agent **job queue** (M13). Each enqueued unit of work is a `ClaudeJob` with a `kind` (`TASK_IMPLEMENTATION`, `IDEA_GRILL`, `IDEA_MAKE_PLAN`, `PLAN_CHAT`, `SPRINT_IMPLEMENTATION`).
```mermaid
stateDiagram-v2
[*] --> QUEUED: enqueue
QUEUED --> CLAIMED: wait_for_job (FOR UPDATE SKIP LOCKED)
CLAIMED --> RUNNING: worker starts
RUNNING --> DONE: update_job_status('done')
RUNNING --> FAILED: update_job_status('failed')
QUEUED --> CANCELLED: user cancels
CLAIMED --> QUEUED: stale (>30min)
QUEUED --> SKIPPED: superseded
DONE --> [*]
FAILED --> [*]
CANCELLED --> [*]
SKIPPED --> [*]
```
| Transition | Trigger | Side effect |
|---|---|---|
| `QUEUED → CLAIMED` | `wait_for_job` atomically claims | Bearer token is bound to the job (`claimed_by_token_id`) |
| `CLAIMED → QUEUED` | Stale claim (>30 min) | Auto-requeue on next `wait_for_job` |
| `RUNNING → DONE` | `update_job_status('done')` | Optional token-cost telemetry stored on the row |
| `RUNNING → FAILED` | `update_job_status('failed')` | For `IDEA_GRILL`/`IDEA_MAKE_PLAN`, idea status auto-rolls to `GRILL_FAILED` / `PLAN_FAILED` |
For idempotency rules and recovery procedures see [`docs/runbooks/worker-idempotency.md`](../runbooks/worker-idempotency.md).
## Idea
The **Idea** entity (M12) is a pre-PBI staging area. It goes through two AI-driven phases: a **grill** (Q&A loop with the user to clarify the idea) and a **plan** (single-pass output of a structured PBI tree). Failures are explicit terminal-ish states that allow retry.
```mermaid
stateDiagram-v2
[*] --> DRAFT: create idea
DRAFT --> GRILLING: enqueue IDEA_GRILL
GRILLING --> GRILLED: update_idea_grill_md
GRILLING --> GRILL_FAILED: job failed
GRILL_FAILED --> GRILLING: retry
GRILLED --> PLANNING: enqueue IDEA_MAKE_PLAN
PLANNING --> PLAN_READY: update_idea_plan_md (parse ok)
PLANNING --> PLAN_FAILED: parsePlanMd rejected
PLAN_FAILED --> PLANNING: retry
PLAN_READY --> PLANNED: PBI tree created
PLANNED --> [*]
```
| Transition | Trigger | Side effect |
|---|---|---|
| `DRAFT → GRILLING` | User clicks "Grill" | Enqueues `IDEA_GRILL` job; worker reads `prompt_text` + `idea.grill_md` |
| `GRILLING → GRILLED` | `update_idea_grill_md` | Logs `IdeaLog{GRILL_RESULT}` |
| `* → GRILL_FAILED` | `update_job_status('failed')` for `IDEA_GRILL` | Idea remains usable; user can retry |
| `GRILLED → PLANNING` | User clicks "Make plan" | Enqueues `IDEA_MAKE_PLAN`; worker outputs strict YAML-frontmatter |
| `PLANNING → PLAN_READY` | `update_idea_plan_md` parse ok | Logs `IdeaLog{PLAN_RESULT}` |
| `PLANNING → PLAN_FAILED` | `parsePlanMd` rejected | Logs `IdeaLog{JOB_EVENT, errors}` |
| `PLAN_READY → PLANNED` | PBI tree generated from plan | Idea is archived; PBI/Story/Task tree appears in the backlog |
For the full Idea workflow, prompts, and `prompt_text` contents, see [`docs/plans/M12-ideas.md`](../plans/M12-ideas.md).
## DB vs API mapping
> **Hardstop:** never bypass [`lib/task-status.ts`](../../lib/task-status.ts).
The database stores enums in `UPPER_SNAKE` (`TO_DO`, `IN_PROGRESS`, `IN_SPRINT`, …) because Prisma + PostgreSQL prefer that convention. The REST API exposes them in `lowercase` (`todo`, `in_progress`, `in_sprint`, …) because that's the convention HTTP consumers expect.
The two are mapped **only** through the helpers in [`lib/task-status.ts`](../../lib/task-status.ts):
```ts
taskStatusToApi(status) // DB → API
taskStatusFromApi(input) // API → DB (returns null on bad input)
storyStatusToApi(status)
storyStatusFromApi(input)
pbiStatusToApi(status)
pbiStatusFromApi(input)
sprintStatusToApi(status)
sprintStatusFromApi(input)
sprintRunStatusToApi(status)
sprintRunStatusFromApi(input)
```
Bad input on the inbound side (`*FromApi`) returns `null` — the route handler converts that to a `422` Zod-style error. See [`docs/adr/0004-status-enum-mapping.md`](../adr/0004-status-enum-mapping.md) for the rationale.
## What's next
→ [03 — Git Workflow](./03-git-workflow.md) covers branching, commits, and the cost-driven PR rules.

View file

@ -0,0 +1,99 @@
---
title: "Git Workflow"
status: active
audience: [contributor]
language: en
last_updated: 2026-05-07
when_to_read: "Before creating a branch, before committing, and especially before pushing or opening a PR."
---
# 03 — Git Workflow
The Scrum4Me git workflow is shaped by two pressures that don't usually appear together:
1. An **AI agent** that can produce many commits per hour without human review,
2. A **Vercel Hobby plan** that meters preview deployments and bills for them.
These two together drive a workflow that looks unusual compared to "feature-branch + PR-per-story". This chapter explains the *why*; the authoritative *how* lives in the runbooks linked at the bottom.
## The five guiding rules
### 1. One branch per milestone, not per story
A milestone (e.g. `M10-qr-login`) groups multiple stories that ship together. The agent runs through them on a single branch named `feat/M{N}-{slug}` (or `feat/ST-XXX-{slug}` for one-off stories without a milestone). All commits accumulate on that branch.
> **Why?** Every push to a feature branch triggers a Vercel preview build. Pushing per story would multiply the build cost without producing more reviewable units of work — the user reviews the milestone, not the story.
See [`docs/adr/0003-one-branch-per-milestone.md`](../adr/0003-one-branch-per-milestone.md) for the full rationale.
### 2. Commit per layer, not per task
A single task can touch the database, the API, and the UI. Each of those layers gets its own commit. The pattern:
```
feat(ST-XXX): add field X to Prisma schema # DB
feat(ST-XXX): add Y endpoint accepting X # API
feat(ST-XXX): wire X into the editor component # UI
chore(ST-XXX): configure sharp for X processing # config
docs(ST-XXX): document the X feature # docs
```
> **Why?** Reviewers and `git bisect` both benefit when one commit can be reverted without touching unrelated layers. A `feat: add profile system` mega-commit is an antipattern.
### 3. Push only after the user has tested
Commits accumulate **locally** until the milestone is functionally complete and the user has confirmed it works. Then — and only then — `git push` and `gh pr create`.
> **Why?** Same cost reason as rule 1. Mid-milestone "save points" should be local tags or `git stash`, not pushes. Some exceptions exist (planning-only PRs, emergency hotfixes); they're enumerated in [`branch-and-commit.md`](../runbooks/branch-and-commit.md#uitzonderingen-op-de-push-regel).
### 4. One PR per batch → one preview build
When the worker runs through a queue of jobs, the entire run produces **one** PR with one commit per task. No interim pushes, no force-pushes to clean up history, no PR-per-story splits.
The end-to-end verification — that one batch produces exactly one Vercel deployment — is in [`branch-and-commit.md`](../runbooks/branch-and-commit.md) (see the *End-to-end verificatie* section).
### 5. Auto-PR flow at the end
Once a story reaches `DONE`, the auto-PR flow takes over: it pushes the branch, opens a PR, waits for the scope to be complete, waits for checks, and merges. The contract for "scope complete" and the path-filter / label rules that decide whether a deploy actually runs are split between two runbooks:
- **End-to-end pipeline**: [`docs/runbooks/auto-pr-flow.md`](../runbooks/auto-pr-flow.md)
- **Selective deploy controls** (`skip-deploy` label, path-filter for `app/`/`components/`/`lib/`): [`docs/runbooks/deploy-control.md`](../runbooks/deploy-control.md)
## Commit message format
```
<type>(ST-XXX): short description
```
Where `<type>` is one of `feat`, `fix`, `chore`, `docs`. The story code in parentheses links the commit back to the Scrum4Me MCP entity.
For PBI-level work (no single story), use the PBI code: `docs(PBI-58): scaffold developer manual`.
## Merge conflicts
| Scenario | Conflict? | Mitigation |
|---|---|---|
| Multiple tasks on the same batch branch | No — they stack linearly on one branch | None needed |
| Two parallel batches touching the same files | Yes, possible | Serialise batches via the MCP `get_claude_context` flow (one story at a time per agent), or rebase before push |
| Long-lived branch drifting from `main` | Yes, possible | `git fetch origin main && git rebase origin/main` before `gh pr create` |
`git push --force` to "wipe" earlier preview builds is forbidden — it costs the same build again on recreation, defeating the purpose of the cost-control rules.
## When **not** to follow the strict rules
When the Vercel account moves to Pro (or another billing tier without per-build cost), this workflow can revert to the more conventional "branch + PR per story". When that happens, update the rule in [`branch-and-commit.md`](../runbooks/branch-and-commit.md) and log the change in [`docs/decisions/agent-instructions-history.md`](../decisions/agent-instructions-history.md).
## Deep links
| Topic | Authoritative source |
|---|---|
| Branch & commit rules (full normative spec) | [`docs/runbooks/branch-and-commit.md`](../runbooks/branch-and-commit.md) |
| Auto-PR flow (story-DONE → merged-PR pipeline) | [`docs/runbooks/auto-pr-flow.md`](../runbooks/auto-pr-flow.md) |
| Deploy controls (labels, path-filter) | [`docs/runbooks/deploy-control.md`](../runbooks/deploy-control.md) |
| Vercel deployment specifics | [`docs/runbooks/deploy-vercel.md`](../runbooks/deploy-vercel.md) |
| Decision rationale (one-branch-per-milestone) | [`docs/adr/0003-one-branch-per-milestone.md`](../adr/0003-one-branch-per-milestone.md) |
| Worker idempotency & job-status protocol | [`docs/runbooks/worker-idempotency.md`](../runbooks/worker-idempotency.md) |
## What's next
→ [04 — MCP Integration](./04-mcp-integration.md) covers how the Claude agent drives this workflow from the queue side.

View file

@ -0,0 +1,121 @@
---
title: "MCP Integration"
status: active
audience: [contributor]
language: en
last_updated: 2026-05-07
when_to_read: "Whenever Claude Code is interacting with Scrum4Me — opening a story, claiming a job, asking the user a question."
---
# 04 — MCP Integration
Scrum4Me exposes its REST API as native Claude Code tools through a dedicated **MCP server** living in [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp). Schemas are shared via a git submodule (`vendor/scrum4me`) so there's exactly one definition of every type. From the agent's perspective, Scrum4Me looks like a set of native tools prefixed `mcp__scrum4me__*`.
This chapter is the **onboarding tour**. The full tool reference (all 18 tools, their parameters, and edge cases) is in [`docs/runbooks/mcp-integration.md`](../runbooks/mcp-integration.md).
## Three ways the agent works
| Mode | Triggered by | Loop |
|---|---|---|
| **Track A — MCP-driven** | User says *"implement the next story"* | `get_claude_context` → execute tasks → `update_task_status` → commit per layer → repeat until queue empty → push + PR |
| **Track B — Manual** | User describes a one-off change in chat | Read pattern + styling → edit → verify → wait for `commit it` → commit |
| **Worker — Queue-driven** | Background worker container running on a Mac/NAS | `wait_for_job` (blocks ≤600s) → switch on `kind` → execute → `update_job_status` → loop forever |
CLAUDE.md describes Track A and Track B; this manual focuses on the **Worker** mode because it's the most novel and the most likely to surprise a new contributor reading server logs.
## A typical Track A run
```mermaid
sequenceDiagram
participant U as User
participant C as Claude
participant M as MCP server
participant DB as Postgres
U->>C: "implement the next story"
C->>M: get_claude_context(product_id)
M->>DB: SELECT product, sprint, next story, tasks
M-->>C: { story, tasks[], pbi, sprint }
loop per task in sort_order
C->>M: update_task_status(task_id, 'in_progress')
C->>C: read pattern + styling, edit files
C->>M: log_implementation(story_id, content)
C->>M: update_task_status(task_id, 'review')
C->>M: log_test_result(story_id, 'PASSED')
C->>M: update_task_status(task_id, 'done')
end
C->>U: "milestone ready for your test"
U->>C: "looks good, push it"
C->>C: git push + gh pr create
```
The contract every step relies on:
- All inputs are **lowercase API enums** (`'in_progress'`, never `'IN_PROGRESS'`); the MCP server applies [`lib/task-status.ts`](../../lib/task-status.ts) under the hood.
- Status writes are **forbidden for demo accounts** — they return `403`. See [02 — Statuses](./02-statuses-and-transitions.md#db-vs-api-mapping) and [`docs/adr/0006-demo-user-three-layer-policy.md`](../adr/0006-demo-user-three-layer-policy.md).
- Bearer tokens are bound to a product. `list_products` returns only what the token can see; `get_claude_context` is product-scoped.
## Idea jobs vs task implementation
The worker `wait_for_job` returns a payload with a `kind` discriminator. The agent must switch on it:
| `kind` | Behaviour |
|---|---|
| `TASK_IMPLEMENTATION` | Default. Execute the `implementation_plan`, follow the [git workflow](./03-git-workflow.md), end with `update_job_status('done')`. |
| `IDEA_GRILL` | Read embedded `prompt_text` + existing `idea.grill_md`. Iterate with `ask_user_question` / `get_question_answer`. End with `update_idea_grill_md(markdown)`. |
| `IDEA_MAKE_PLAN` | Read `prompt_text` + `idea.grill_md`. **Do not ask questions** — single-pass output in strict YAML-frontmatter. End with `update_idea_plan_md(markdown)`. Server-side parser may reject → `PLAN_FAILED`. |
| `PLAN_CHAT` | Conversational refinement loop on an existing plan (M12+). |
| `SPRINT_IMPLEMENTATION` | Sprint-level run that cascades through every task; `update_task_status` calls must include the `sprint_run_id`. |
For the full Idea state machine (DRAFT → GRILLING → … → PLANNED) see [02 — Statuses & Transitions § Idea](./02-statuses-and-transitions.md#idea).
## The Q&A channel
When Claude needs a human decision mid-run, it doesn't block silently — it posts a question through the MCP and either polls or returns control:
```mermaid
sequenceDiagram
participant C as Claude
participant M as MCP
participant DB as Postgres
participant U as User (NavBar bell)
C->>M: ask_user_question({ story_id, question, wait_seconds: 600 })
M->>DB: INSERT user_question; NOTIFY user_question_created
DB-->>U: SSE event → bell pulses
U->>M: POST /api/questions/:id/answer
M->>DB: UPDATE user_question; NOTIFY user_question_answered
DB-->>C: ask_user_question returns { answer }
C->>C: continue execution
```
Key facts:
- `wait_seconds` is capped at 600. If the user doesn't answer in time, `ask_user_question` returns with status `pending`; Claude can resume later via `get_question_answer(question_id)`.
- Idea questions (`{ idea_id }` instead of `{ story_id }`) are **user-private** — they bypass `productAccessFilter`, so collaborators don't see them.
- A question can be cancelled by the asker via `cancel_question`.
The persistent design (table + `LISTEN/NOTIFY`) is documented in [`docs/architecture/claude-question-channel.md`](../architecture/claude-question-channel.md).
## The worker's pre-flight quota check
The worker doesn't blindly call `wait_for_job`. Each iteration it first checks Anthropic API quota via `bin/worker-quota-probe.sh` so it doesn't burn a 10-minute block on a queue it can't actually process. The full algorithm — settings, `worker_heartbeat` SSE event, sleep-until-reset — is in [`docs/runbooks/mcp-integration.md`](../runbooks/mcp-integration.md#pre-flight-quota-check-m13). The Docker chapter ([05](./05-docker.md#quota-probe)) shows how to test it locally.
## Schema-drift watchdog
If Scrum4Me's Prisma schema changes but `scrum4me-mcp` isn't synced, the MCP server will fail at runtime — not at deploy. To prevent that, a remote agent runs every Monday at 08:00 Amsterdam time, syncs `vendor/scrum4me`, and runs `prisma:generate` + `tsc --noEmit` in `scrum4me-mcp`. Drift reports must be resolved **before** any Scrum4Me PR with schema changes can merge. See [`docs/runbooks/mcp-integration.md`](../runbooks/mcp-integration.md#schema-drift-bewaking).
## Deep links
| Topic | Authoritative source |
|---|---|
| Tool reference (all 18 tools) | [`docs/runbooks/mcp-integration.md`](../runbooks/mcp-integration.md) |
| Worker idempotency & job-status protocol | [`docs/runbooks/worker-idempotency.md`](../runbooks/worker-idempotency.md) |
| Q&A channel architecture (table + LISTEN/NOTIFY) | [`docs/architecture/claude-question-channel.md`](../architecture/claude-question-channel.md) |
| Idea-laag plan & prompts | [`docs/plans/M12-ideas.md`](../plans/M12-ideas.md) |
| Sprint execution modes (PER_TASK vs SPRINT_BATCH) | [`docs/architecture/sprint-execution-modes.md`](../architecture/sprint-execution-modes.md) |
| Realtime NOTIFY payload contract | [`docs/patterns/realtime-notify-payload.md`](../patterns/realtime-notify-payload.md) |
| Demo-user write protection | [`docs/adr/0006-demo-user-three-layer-policy.md`](../adr/0006-demo-user-three-layer-policy.md) |
## What's next
→ [05 — Docker](./05-docker.md) covers how the worker container is run, debugged, and operated.

149
docs/manual/05-docker.md Normal file
View file

@ -0,0 +1,149 @@
---
title: "Docker"
status: active
audience: [contributor]
language: en
last_updated: 2026-05-07
when_to_read: "Before running the worker locally, debugging a stuck job, or operating the Mac/NAS deployment."
---
# 05 — Docker
This chapter is the contributor's tour of the Docker side of Scrum4Me. Two important up-front facts:
1. **The Next.js app is not containerised.** The web UI, API routes, server actions, and database connection all run on **Vercel** (serverless functions + Edge runtime). There is no `Dockerfile` in this repo and no `docker-compose.yml`.
2. **Only the worker is containerised.** The "worker" is a Claude Code agent in a long-running container that polls the Scrum4Me job queue via MCP and executes `TASK_IMPLEMENTATION` / `IDEA_GRILL` / `IDEA_MAKE_PLAN` / `SPRINT_IMPLEMENTATION` jobs.
The container image and its supporting scripts live in a **separate repo**: [`madhura68/scrum4me-docker`](https://github.com/madhura68/scrum4me-docker). This manual documents the consumer side — what the worker is, how it relates to Scrum4Me, and how to diagnose issues. The container internals (Dockerfile, entrypoint, agent provisioning) are out of scope for this manual; see that repo's README.
> **Note:** A separate sandbox repo `scrum4me-sbx` ([`SC-4`](https://github.com/madhura68/scrum4me-sbx)) exists for Docker exploration. Treat it as a scratchpad, not as the production worker.
## Topology
```mermaid
flowchart LR
subgraph Vercel
App[Next.js app<br/>+ API routes]
end
subgraph Neon
DB[(Postgres)]
end
subgraph Mac["Mac (default) / NAS (opt-in)"]
Worker[Worker container<br/>Claude Code + MCP]
end
Worker -- MCP over HTTPS --> App
App -- Prisma --> DB
Worker -- git push --> GH[GitHub]
GH -- webhooks --> App
```
- The worker **never connects to the database directly**. All state changes go through MCP tools, which call the Vercel-hosted REST API, which writes to Neon via Prisma.
- The worker **does** push commits directly to GitHub. GitHub then notifies Vercel and the auto-PR flow ([03 — Git Workflow](./03-git-workflow.md)) takes over.
## Mac vs NAS
| Flow | When to use | Status |
|---|---|---|
| **Mac-native (arm64)** | Default for development and small teams | Active |
| **NAS** | Self-hosted always-on worker on a Synology / Asustor / similar | Opt-in, validated by historical smoke tests in [`docs/docker-smoke/`](../docker-smoke/) |
The Mac flow is the default because it doesn't require dedicated hardware. The container runs natively on Apple Silicon (arm64) — no x86 emulation overhead.
## Environment variables the worker needs
The worker container needs **only** what's required to authenticate to MCP and push to GitHub:
| Var | Purpose |
|---|---|
| `SCRUM4ME_BEARER_TOKEN` | Bearer token bound to a product. Returned by the user's API-token settings page. |
| `SCRUM4ME_BASE_URL` | Usually `https://scrum4me.vercel.app` (or the user's domain). |
| `GITHUB_TOKEN` | Personal access token with `repo` scope, used by `git push` and `gh pr create`. |
| `ANTHROPIC_API_KEY` | The Claude API key used by the worker process. |
| `MIN_QUOTA_PCT` | Optional. Worker pauses if Anthropic quota drops below this percentage. |
> **Hardstop:** the worker does **not** need `DATABASE_URL`, `SESSION_SECRET`, or `CRON_SECRET`. Those belong to the Next.js app; the worker only talks to MCP. If you find yourself adding DB env vars to the worker, stop — you're solving the wrong problem.
The full list and provisioning instructions live in the [`scrum4me-docker` README](https://github.com/madhura68/scrum4me-docker). **TODO:** link to specific sections of that README once it's stable.
## What the worker loop does, on a single iteration
```mermaid
sequenceDiagram
participant W as Worker
participant Q as worker-quota-probe.sh
participant M as MCP server
W->>Q: probe Anthropic quota
Q-->>W: { pct, reset_at_iso }
alt pct < MIN_QUOTA_PCT
W->>M: worker_heartbeat(pct, last_quota_check_at)
W->>W: sleep until reset_at_iso (cap 1h)
else quota ok
W->>M: worker_heartbeat(pct, last_quota_check_at)
W->>M: wait_for_job (block ≤600s, claim atomically)
alt queue empty
W->>W: continue (no work, loop again)
else got job
W->>W: execute by `kind`
W->>M: update_job_status(done|failed)
end
end
Note over W: continue forever
```
The loop is described authoritatively in [`docs/runbooks/mcp-integration.md`](../runbooks/mcp-integration.md#batch-loop-verplichte-agent-flow) and [`docs/runbooks/worker-idempotency.md`](../runbooks/worker-idempotency.md).
### Quota probe
`bin/worker-quota-probe.sh` (in `scrum4me-docker`) makes a tiny call to the Anthropic API to read the current quota percentage and reset time. Cost: ~1 output token per probe (~12 tokens/hour at 5-minute intervals). The default `MIN_QUOTA_PCT` is **20%** — typically high enough on Pro/Max plans that the worker never pauses during normal day-job hours.
### Heartbeat
Every iteration the worker calls `worker_heartbeat({ last_quota_pct, last_quota_check_at })`. The MCP server emits an SSE event so the NavBar in the Next.js app shows the worker as live. A heartbeat older than 15 seconds is rendered as "offline" / "stand-by" in the UI.
### Stale-claim recovery
If a worker dies mid-job (process crash, container kill, network partition), its claimed job stays as `CLAIMED` in the database. After **30 minutes** the next `wait_for_job` call automatically requeues it (`CLAIMED → QUEUED`) before claiming a fresh one. No manual intervention is required for clean recovery.
When you **do** need to manually requeue a job (e.g. you killed it intentionally and don't want to wait 30 min), the operator route is the admin board → "Requeue job" button. **TODO:** confirm the exact UI path; this is not yet documented in `docs/runbooks/`.
## Running the worker locally
The intended local workflow per the project's standing memory is **Mac-native Docker** (the user's `project_docker_default_target` memory). High-level steps (verify against the [scrum4me-docker README](https://github.com/madhura68/scrum4me-docker) for exact commands):
1. Clone `scrum4me-docker` next to `Scrum4Me/` (so `~/Development/Scrum4Me/scrum4me-docker/`).
2. Provision the env vars above (typically a `.env` file in that repo, **not committed**).
3. `docker build` the image and `docker run` it with the env file mounted.
4. Watch container logs for the heartbeat/quota cycle.
5. Trigger a job from the UI ("Voer alle uit" on the Solo Board) and verify the worker picks it up within ~5 seconds.
> **TODO:** once the `scrum4me-docker` README has stabilised, replace the bullets above with copy-paste-ready commands. Until then, defer to that repo for canonical instructions.
## Debugging a stuck worker
| Symptom | Likely cause | Fix |
|---|---|---|
| Worker shows offline in NavBar but container is running | `worker_heartbeat` not reaching MCP | Check `SCRUM4ME_BASE_URL` and `SCRUM4ME_BEARER_TOKEN`; tail container logs for HTTP errors |
| Worker logs say "stand-by" indefinitely | `pct < MIN_QUOTA_PCT` and reset_at not reached | Lower `MIN_QUOTA_PCT` for testing, or wait for the printed `reset_at_iso` |
| Job stuck `CLAIMED` for >30 min | Worker died mid-job | Wait — auto-requeue triggers on next `wait_for_job` |
| Worker claims job but never updates status | Crashed before `update_job_status`; container restarted in a loop | Check `docker logs`; the next `wait_for_job` will requeue stale claims |
| `update_job_status` returns `403` | Bearer token doesn't match `claimed_by_token_id` | The token was rotated mid-run; restart with fresh token |
For deeper troubleshooting see [06 — Troubleshooting](./06-troubleshooting.md).
## Smoke-test references
Historical Docker smoke tests live in [`docs/docker-smoke/`](../docker-smoke/). They validated the worktree-isolation + branch-per-story flow when the Docker worker was first introduced. They are **historical** — don't expect them to be runnable as-is — but they're a useful reference when you want to verify the same flow on a new container image.
## Deep links
| Topic | Source |
|---|---|
| Container image, Dockerfile, build | [`scrum4me-docker` repo](https://github.com/madhura68/scrum4me-docker) |
| Worker loop & quota check | [`docs/runbooks/mcp-integration.md`](../runbooks/mcp-integration.md#pre-flight-quota-check-m13) |
| Worker idempotency / job-status protocol | [`docs/runbooks/worker-idempotency.md`](../runbooks/worker-idempotency.md) |
| Historical smoke tests | [`docs/docker-smoke/`](../docker-smoke/) |
| Sandbox / exploration repo | [`scrum4me-sbx` repo](https://github.com/madhura68/scrum4me-sbx) |
## What's next
→ [06 — Troubleshooting](./06-troubleshooting.md) covers error codes and recovery procedures across the full stack.

View file

@ -0,0 +1,112 @@
---
title: "Troubleshooting"
status: active
audience: [contributor]
language: en
last_updated: 2026-05-07
when_to_read: "When something breaks. Start with the symptom table; fall back to the error-code reference."
---
# 06 — Troubleshooting
This chapter is the **first place to look** when something is wrong. Each row links to the authoritative source so you can dig deeper without losing your trail.
## Error code reference
These three HTTP status codes are non-negotiable hardstops in the API surface — they always mean the same thing across every route handler.
| Code | Meaning | Where it comes from |
|---|---|---|
| **`400`** | JSON parse error | Body couldn't be parsed as JSON. Usually a malformed request from a client. |
| **`422`** | Zod validation error | Body parsed, but failed schema validation. Response includes the offending field path. |
| **`403`** | Demo-user write blocked | Authenticated user `is_demo = true` attempted a write. Three layers enforce this — see [`docs/adr/0006-demo-user-three-layer-policy.md`](../adr/0006-demo-user-three-layer-policy.md). |
> **Hardstop:** these codes are reserved. Do not use `400` for validation errors or `422` for unauthorised access. The contract is enforced at the route-handler level — see the [Route Handler pattern](../patterns/route-handler.md).
Other common codes:
| Code | Meaning |
|---|---|
| `401` | No session / invalid bearer token |
| `404` | Resource not found, or token does not have access |
| `409` | State conflict — e.g. trying to claim a job that's already `CLAIMED` |
| `429` | Rate-limited — typically the Anthropic quota cap, not Scrum4Me itself |
| `500` | Unhandled server error. Always check Vercel function logs. |
## Symptom → cause → fix
### MCP
| Symptom | Likely cause | Fix |
|---|---|---|
| `mcp__scrum4me__get_claude_context` returns `null` or empty story | Bearer token doesn't have access to that product | Run `mcp__scrum4me__list_products` to confirm scope; rotate the token if needed |
| `mcp__scrum4me__update_task_status` returns `403` | Demo user, or token mismatch in a sprint run | Check user identity; if inside a sprint run, the bearer token must match `claimed_by_token_id` of the parent job |
| `mcp__scrum4me__wait_for_job` returns nothing for the full 600s block | Queue is genuinely empty | This is normal — loop and call again. See [`runbooks/mcp-integration.md`](../runbooks/mcp-integration.md#batch-loop-verplichte-agent-flow) |
| Job stays `CLAIMED` for >30 minutes | Worker died mid-job | Auto-requeue triggers on next `wait_for_job`; no manual action needed |
| `update_idea_plan_md` causes idea to flip to `PLAN_FAILED` | `parsePlanMd` server-side rejected the YAML-frontmatter | Inspect `IdeaLog{JOB_EVENT, errors}` for the parse error; re-run `IDEA_MAKE_PLAN` after fixing the prompt |
### Statuses & data integrity
| Symptom | Likely cause | Fix |
|---|---|---|
| Status displayed differently in DB vs UI | Some code path bypassed `lib/task-status.ts` | Grep the codebase for direct enum string usage; force everything through the mappers. See [`adr/0004-status-enum-mapping.md`](../adr/0004-status-enum-mapping.md) |
| Story stuck `IN_SPRINT` when all tasks are `DONE` | Auto-promotion not triggered | Check the most recent `update_task_status` call — it may have failed silently. Re-issue with the correct task |
| PBI not auto-promoting to `DONE` | Not all child stories are `DONE` yet | List stories under the PBI; one is probably still `OPEN` or `IN_SPRINT` |
| `422` from `create_pbi` / `create_story` / `create_task` | Zod validation failed (length cap, missing required field) | Response body includes field path — fix and retry |
| `IdeaStatus` stays `GRILLING` long after the worker stopped | The job ended without calling `update_idea_grill_md` | Check the worker logs for an exception; manually requeue or mark `GRILL_FAILED` to allow retry |
### Git & deploy
| Symptom | Likely cause | Fix |
|---|---|---|
| Unexpected Vercel preview build appeared mid-batch | An interim push happened that shouldn't have | Inspect `git log --all --graph` for the offending push; review [`runbooks/branch-and-commit.md`](../runbooks/branch-and-commit.md) |
| PR has multiple Vercel deployments for the same commit range | Force-push, or push-then-revert | Don't force-push. If genuinely needed, document in the PR description |
| Auto-PR didn't open after story `DONE` | Story not actually `DONE`, or auto-PR pre-conditions unmet | Walk through [`runbooks/auto-pr-flow.md`](../runbooks/auto-pr-flow.md); typically a missing `update_task_status('done')` for the last task |
| Vercel skipped the deploy entirely | `skip-deploy` label or path-filter excluded the changed paths | See [`runbooks/deploy-control.md`](../runbooks/deploy-control.md) for the rules |
| Merge conflict between two parallel batches | Two branches touched the same files | Serialise: merge the first PR before pushing the second. Then `git fetch origin main && git rebase origin/main` |
### Realtime
| Symptom | Likely cause | Fix |
|---|---|---|
| Solo Board doesn't update when status changes | SSE connection dropped, or NOTIFY payload missing fields | Reload the page; if it persists, check `DIRECT_URL` (LISTEN/NOTIFY needs the pooler-bypass URL). See [`patterns/realtime-notify-payload.md`](../patterns/realtime-notify-payload.md) |
| NavBar bell doesn't pulse on new question | SSE/event channel mismatched, or payload missing required fields | Confirm the question was actually inserted (`mcp__scrum4me__list_open_questions`); inspect the Network tab for the SSE connection |
| Worker shows offline despite a running container | `worker_heartbeat` not reaching MCP | Verify `SCRUM4ME_BASE_URL` and bearer token; tail container logs |
### Auth & sessions
| Symptom | Likely cause | Fix |
|---|---|---|
| Login redirects in a loop | Session cookie not set; usually `SESSION_SECRET` mismatch between deployments | Check Vercel env vars for `SESSION_SECRET` (must be ≥32 chars); see [`patterns/iron-session.md`](../patterns/iron-session.md) |
| All write buttons disabled with "Niet beschikbaar in demo-modus" tooltip | You're logged in as the demo user | Log out and log in with a real account |
| `403` on a route that should be allowed | Proxy or server-action layer rejected the request | Walk through the three layers in [`adr/0006-demo-user-three-layer-policy.md`](../adr/0006-demo-user-three-layer-policy.md); each can independently say "no" |
### Build & dev-server
| Symptom | Likely cause | Fix |
|---|---|---|
| `npm run build` fails with `Cannot find module '@/...'` | TypeScript path alias mismatch | Check `tsconfig.json` `paths`; rerun `npm run prebuild` if codegen is stale |
| Mermaid diagram renders as plain text in the in-app `/manual` viewer | `MermaidBlock` not picking up `language-mermaid` | See [04 — MCP Integration](./04-mcp-integration.md) won't help here — open `app/(app)/manual/_components/mermaid-block.tsx` and confirm the dynamic import is `ssr: false` |
| "Server-only" import error in browser | A `*-server.ts` module was imported into a client component | Refactor — split server logic out, or use a server action. Hardstop in [`CLAUDE.md`](../../CLAUDE.md#hardstop-regels) |
| `npm run dev` shows hydration mismatch | Server and client render diverge — usually time-based or random values | Wrap in `useEffect` for client-only state, or pass server time as a prop |
## When in doubt
1. **Read the runbook.** Each runbook in [`docs/runbooks/`](../runbooks/) starts with a `when_to_read` field — match the situation.
2. **Check the ADRs.** The ADR index in [`docs/INDEX.md`](../INDEX.md) lists the rationale for every cross-cutting decision. If your fix would contradict an ADR, talk to a maintainer first.
3. **Read the agent-flow pitfalls log.** [`docs/runbooks/agent-flow-pitfalls.md`](../runbooks/agent-flow-pitfalls.md) is a living list of issues found during agent runs and how they were resolved.
4. **Look at recent commits.** `git log --oneline --since='7 days ago'` often reveals the very change that broke whatever you're debugging.
## Escalation
If after the steps above the issue is still unresolved:
- **AI agent / MCP issues** → file in the [`scrum4me-mcp` repo](https://github.com/madhura68/scrum4me-mcp).
- **Worker container issues** → file in the [`scrum4me-docker` repo](https://github.com/madhura68/scrum4me-docker).
- **App / data / status issues** → file in the [`Scrum4Me` repo](https://github.com/madhura68/Scrum4Me).
## What's next
You've reached the end of the manual. Bookmark this troubleshooting chapter — it's the most-revisited page once you're past onboarding.
Back to [index](./index.md).

64
docs/manual/index.md Normal file
View file

@ -0,0 +1,64 @@
---
title: "Scrum4Me Developer Manual"
status: active
audience: [contributor]
language: en
last_updated: 2026-05-07
when_to_read: "Onboarding to Scrum4Me as a human contributor."
---
# Scrum4Me Developer Manual
Welcome. This manual is the **map** of Scrum4Me — a guided tour through the moving parts of the project. It is written for a new human contributor who needs to understand how the pieces fit together before diving into the authoritative reference docs (the runbooks, ADRs, and patterns under [`docs/`](../INDEX.md)).
> **The manual is the map. The runbooks are the territory.**
> When two sources disagree, trust the runbook or ADR linked from this manual.
## Audience
- **New human contributors** picking up the project for the first time.
- **Returning contributors** who want a quick refresher on how a specific subsystem (statuses, git, MCP, Docker) fits into the whole.
- **Not for**: AI agents — they should follow [`CLAUDE.md`](../../CLAUDE.md) and the agent-specific runbooks under [`docs/runbooks/`](../runbooks/).
## How to read this manual
| You want to… | Read |
|---|---|
| …get the elevator pitch and project structure | [01 — Overview](./01-overview.md) |
| …understand how a PBI/Story/Task moves through its lifecycle | [02 — Statuses & Transitions](./02-statuses-and-transitions.md) |
| …know when to branch, commit, push, and open a PR | [03 — Git Workflow](./03-git-workflow.md) |
| …see how Claude Code drives stories via the MCP server | [04 — MCP Integration](./04-mcp-integration.md) |
| …run the worker container locally or understand the deploy topology | [05 — Docker](./05-docker.md) |
| …diagnose an error code, stuck job, or weird state | [06 — Troubleshooting](./06-troubleshooting.md) |
A linear read takes about 30 minutes. As a lookup reference, jump straight to a chapter — each one stands alone.
## Conventions
- **Cross-references** use relative links (`../runbooks/...`) so they work both in GitHub and inside the in-app `/manual` viewer.
- **Callouts** use blockquotes prefixed with a label: `> **Note:**`, `> **Warning:**`, `> **Hardstop:**` (a non-negotiable rule from [`CLAUDE.md`](../../CLAUDE.md)).
- **Code blocks** show shell commands with no `$` prefix, so they're copy-pasteable.
- **State diagrams** use Mermaid `stateDiagram-v2`; they render in GitHub and in the in-app viewer.
- **Status labels** are written in `UPPER_SNAKE` when referring to the database value and `lowercase` when referring to the API representation — see [02 — Statuses & Transitions](./02-statuses-and-transitions.md#db-vs-api-mapping) for the contract.
## In-app rendering
Every chapter in this manual is also browsable inside the running Scrum4Me app at `/manual`. The in-app sidebar mirrors this index, and Mermaid diagrams render in place. The markdown files under `docs/manual/` are the **source of truth** — the in-app page reads them at build time via the `scripts/build-manual.mjs` generator.
## What this manual does **not** cover
- **REST API reference** → [`docs/api/rest-contract.md`](../api/rest-contract.md)
- **Component & dialog specs** → [`docs/specs/dialogs/`](../specs/dialogs/)
- **Architecture deep-dives** → [`docs/architecture.md`](../architecture.md) breadcrumb
- **Decision rationale** → [`docs/adr/`](../adr/)
- **Implementation patterns** → [`docs/patterns/`](../patterns/)
- **AI-agent instructions** → [`CLAUDE.md`](../../CLAUDE.md) and [`docs/runbooks/mcp-integration.md`](../runbooks/mcp-integration.md)
## Table of contents
1. [Overview](./01-overview.md) — what Scrum4Me is, the entity hierarchy, the stack, repository layout
2. [Statuses & Transitions](./02-statuses-and-transitions.md) — state machines for every entity
3. [Git Workflow](./03-git-workflow.md) — branching, commits, PRs, deploy controls
4. [MCP Integration](./04-mcp-integration.md) — the agent loop, idea jobs, the Q&A channel
5. [Docker](./05-docker.md) — worker container, local dev, scrum4me-docker
6. [Troubleshooting](./06-troubleshooting.md) — error codes, stuck jobs, recovery procedures

28
lib/manual-server.ts Normal file
View file

@ -0,0 +1,28 @@
import { MANUAL_TOC, type ManualEntry } from './manual.generated'
export type { ManualEntry } from './manual.generated'
export type ManualChapter = {
entry: ManualEntry
body: string
}
export function getManualToc(): readonly ManualEntry[] {
return MANUAL_TOC
}
function slugMatches(a: readonly string[], b: readonly string[]): boolean {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
return true
}
export function findManualEntry(slug: readonly string[]): ManualEntry | null {
return MANUAL_TOC.find((e) => slugMatches(e.slug, slug)) ?? null
}
export function getManualChapter(slug: readonly string[]): ManualChapter | null {
const entry = findManualEntry(slug)
if (!entry) return null
return { entry, body: entry.markdown }
}

865
lib/manual.generated.ts Normal file
View file

@ -0,0 +1,865 @@
// AUTO-GENERATED by scripts/build-manual.mjs. Do not edit by hand.
// Run `npm run manual:build` to regenerate.
export type ManualEntry = {
slug: readonly string[]
title: string
description: string
filePath: string
markdown: string
}
export const MANUAL_TOC: readonly ManualEntry[] = [
{
slug: [] as const,
title: 'Scrum4Me Developer Manual',
description: 'Welcome. This manual is the **map** of Scrum4Me — a guided tour through the moving parts of the project. It is written for a new human contributor who needs to understand how the pieces fit together before diving into the authoritative reference docs (the runbooks, ADRs, and patterns under [`docs/`](../INDEX.md)).',
filePath: 'docs/manual/index.md',
markdown: `# Scrum4Me Developer Manual
Welcome. This manual is the **map** of Scrum4Me a guided tour through the moving parts of the project. It is written for a new human contributor who needs to understand how the pieces fit together before diving into the authoritative reference docs (the runbooks, ADRs, and patterns under [\`docs/\`](../INDEX.md)).
> **The manual is the map. The runbooks are the territory.**
> When two sources disagree, trust the runbook or ADR linked from this manual.
## Audience
- **New human contributors** picking up the project for the first time.
- **Returning contributors** who want a quick refresher on how a specific subsystem (statuses, git, MCP, Docker) fits into the whole.
- **Not for**: AI agents they should follow [\`CLAUDE.md\`](../../CLAUDE.md) and the agent-specific runbooks under [\`docs/runbooks/\`](../runbooks/).
## How to read this manual
| You want to | Read |
|---|---|
| get the elevator pitch and project structure | [01 Overview](./01-overview.md) |
| understand how a PBI/Story/Task moves through its lifecycle | [02 Statuses & Transitions](./02-statuses-and-transitions.md) |
| know when to branch, commit, push, and open a PR | [03 Git Workflow](./03-git-workflow.md) |
| see how Claude Code drives stories via the MCP server | [04 MCP Integration](./04-mcp-integration.md) |
| run the worker container locally or understand the deploy topology | [05 Docker](./05-docker.md) |
| diagnose an error code, stuck job, or weird state | [06 Troubleshooting](./06-troubleshooting.md) |
A linear read takes about 30 minutes. As a lookup reference, jump straight to a chapter each one stands alone.
## Conventions
- **Cross-references** use relative links (\`../runbooks/...\`) so they work both in GitHub and inside the in-app \`/manual\` viewer.
- **Callouts** use blockquotes prefixed with a label: \`> **Note:**\`, \`> **Warning:**\`, \`> **Hardstop:**\` (a non-negotiable rule from [\`CLAUDE.md\`](../../CLAUDE.md)).
- **Code blocks** show shell commands with no \`$\` prefix, so they're copy-pasteable.
- **State diagrams** use Mermaid \`stateDiagram-v2\`; they render in GitHub and in the in-app viewer.
- **Status labels** are written in \`UPPER_SNAKE\` when referring to the database value and \`lowercase\` when referring to the API representation — see [02 — Statuses & Transitions](./02-statuses-and-transitions.md#db-vs-api-mapping) for the contract.
## In-app rendering
Every chapter in this manual is also browsable inside the running Scrum4Me app at \`/manual\`. The in-app sidebar mirrors this index, and Mermaid diagrams render in place. The markdown files under \`docs/manual/\` are the **source of truth** — the in-app page reads them at build time via the \`scripts/build-manual.mjs\` generator.
## What this manual does **not** cover
- **REST API reference** [\`docs/api/rest-contract.md\`](../api/rest-contract.md)
- **Component & dialog specs** [\`docs/specs/dialogs/\`](../specs/dialogs/)
- **Architecture deep-dives** [\`docs/architecture.md\`](../architecture.md) breadcrumb
- **Decision rationale** [\`docs/adr/\`](../adr/)
- **Implementation patterns** [\`docs/patterns/\`](../patterns/)
- **AI-agent instructions** [\`CLAUDE.md\`](../../CLAUDE.md) and [\`docs/runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md)
## Table of contents
1. [Overview](./01-overview.md) what Scrum4Me is, the entity hierarchy, the stack, repository layout
2. [Statuses & Transitions](./02-statuses-and-transitions.md) state machines for every entity
3. [Git Workflow](./03-git-workflow.md) branching, commits, PRs, deploy controls
4. [MCP Integration](./04-mcp-integration.md) the agent loop, idea jobs, the Q&A channel
5. [Docker](./05-docker.md) worker container, local dev, scrum4me-docker
6. [Troubleshooting](./06-troubleshooting.md) error codes, stuck jobs, recovery procedures
`,
},
{
slug: ['01-overview'] as const,
title: 'Overview',
description: 'Scrum4Me is a **desktop-first fullstack web app for solo developers and small Scrum teams** who manage multiple software projects in parallel. It models the Scrum hierarchy explicitly (Product → PBI → Story → Task), supports Sprints with split-screen drag-and-drop planning, and integrates Claude Code as an automated implementation worker — every result the agent produces is logged back into the originating story.',
filePath: 'docs/manual/01-overview.md',
markdown: `# 01 — Overview
## What is Scrum4Me?
Scrum4Me is a **desktop-first fullstack web app for solo developers and small Scrum teams** who manage multiple software projects in parallel. It models the Scrum hierarchy explicitly (Product PBI Story Task), supports Sprints with split-screen drag-and-drop planning, and integrates Claude Code as an automated implementation worker every result the agent produces is logged back into the originating story.
The app is deployable to **Vercel + Neon** (default) and can run **fully local** via the worker container. A built-in demo user has read-only access; Product Owners add Developers by username, and those Developers gain write access to that product's stories, tasks, and sprints.
## Entity hierarchy
\`\`\`mermaid
flowchart TB
Product["Product<br/>(per repo)"]
Idea["Idea<br/>(pre-PBI staging)"]
PBI["PBI<br/>(Product Backlog Item)"]
Story["Story"]
Task["Task"]
Sprint["Sprint<br/>(cross-cutting)"]
Product --> Idea
Idea -.->|"AI-grilled & planned"| PBI
Product --> PBI
PBI --> Story
Story --> Task
Sprint -.->|"contains stories<br/>denormalised on tasks"| Story
Sprint -.-> Task
\`\`\`
- **Product** one row per repo. \`repo_url\`, \`definition_of_done\`, members.
- **Idea** pre-PBI staging entity introduced in M12. Goes through \`IDEA_GRILL\` (AI Q&A loop) and \`IDEA_MAKE_PLAN\` jobs to produce a structured plan that can be turned into a PBI tree.
- **PBI** a Product Backlog Item. Has \`priority\` (14) and \`sort_order\` (float, see [\`docs/patterns/sort-order.md\`](../patterns/sort-order.md)).
- **Story** a unit of value under a PBI; has acceptance criteria. Lives in the backlog (\`OPEN\`) until added to a sprint.
- **Task** the smallest unit; has an \`implementation_plan\` consumed by the Claude worker. \`sprint_id\` is denormalised from the parent story for query efficiency.
- **Sprint** cross-cutting time-box. Stories are added to a sprint; tasks inherit \`sprint_id\`. Sprint execution has two modes: \`PER_TASK\` and \`SPRINT_BATCH\` — see [\`docs/architecture/sprint-execution-modes.md\`](../architecture/sprint-execution-modes.md).
For status lifecycles of each entity, see [02 Statuses & Transitions](./02-statuses-and-transitions.md).
## Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) + React 19 |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS + shadcn/ui + Material Design 3 tokens via [\`app/styles/theme.css\`](../../app/styles/theme.css) |
| Client state | Zustand + dnd-kit |
| Database | Prisma v7 + PostgreSQL (Neon) |
| Auth | iron-session + bcryptjs |
| Utilities | Zod, Sonner, Sharp, Vercel Analytics |
| Hosting | Vercel (app), Neon (DB), Mac/NAS Docker (worker) |
For the rationale behind each choice and the technologies we explicitly **don't** use, see [\`docs/architecture/overview.md\`](../architecture/overview.md).
## Repository layout
\`\`\`
Scrum4Me/
app/ # Next.js App Router routes
(app)/ # authenticated desktop UI
(auth)/ # login, register, demo
(mobile)/ # /m/* mobile shell (3 screens)
api/ # REST route handlers (Claude integration)
styles/ # MD3 token CSS
components/ # shared UI components
lib/ # server/client utilities
task-status.ts # the ONLY place DBAPI enum mapping happens
prisma/ # schema + migrations
docs/ # this manual + ADRs, runbooks, patterns, specs
scripts/ # codegen, seeders, link checkers
\`\`\`
The \`*-server.ts\` filename suffix marks server-only modules (DB, Node APIs). They must never be imported into a client component — see the hardstop in [\`CLAUDE.md\`](../../CLAUDE.md#hardstop-regels).
For a deeper structural breakdown including stores, realtime channels, and the job queue, see [\`docs/architecture/project-structure.md\`](../architecture/project-structure.md).
## Glossary refresh
A few terms used throughout this manual that often differ from "generic Scrum" usage:
- **PBI** Product Backlog Item. Not "Feature" or "Epic".
- **Story** A unit of work under a PBI. Not "Ticket" or "Issue".
- **Sprint Goal** The narrative for a sprint. Not "Objective".
- **Worker** A Claude Code agent claiming jobs from the Scrum4Me queue (M13).
- **Demo user** A read-only built-in user; writes return \`403\`. See [\`docs/adr/0006-demo-user-three-layer-policy.md\`](../adr/0006-demo-user-three-layer-policy.md).
- **Idea** Pre-PBI staging artefact (M12). Has its own state machine; see [02](./02-statuses-and-transitions.md#idea).
The complete glossary lives at [\`docs/glossary.md\`](../glossary.md).
## What's next
[02 Statuses & Transitions](./02-statuses-and-transitions.md) covers how each entity moves through its lifecycle, with state-machine diagrams.
`,
},
{
slug: ['02-statuses-and-transitions'] as const,
title: 'Statuses & Transitions',
description: 'Every persistent entity in Scrum4Me has an explicit status enum. This chapter documents them all, with state-machine diagrams showing allowed transitions, the trigger for each transition (user action vs system / job-driven), and the side effects.',
filePath: 'docs/manual/02-statuses-and-transitions.md',
markdown: `# 02 — Statuses & Transitions
Every persistent entity in Scrum4Me has an explicit status enum. This chapter documents them all, with state-machine diagrams showing allowed transitions, the trigger for each transition (user action vs system / job-driven), and the side effects.
> **Hardstop:** the database stores enums in \`UPPER_SNAKE\`; the REST API exposes them in \`lowercase\`. Conversion happens **only** through [\`lib/task-status.ts\`](../../lib/task-status.ts) — never call \`.toLowerCase()\` or \`.toUpperCase()\` directly. See the [DB vs API mapping](#db-vs-api-mapping) section at the end.
## Quick reference
| Entity | Source enum | Statuses |
|---|---|---|
| [PBI](#pbi) | \`PbiStatus\` | \`READY\`, \`BLOCKED\`, \`DONE\`, \`FAILED\` |
| [Story](#story) | \`StoryStatus\` | \`OPEN\`, \`IN_SPRINT\`, \`DONE\`, \`FAILED\` |
| [Task](#task) | \`TaskStatus\` | \`TO_DO\`, \`IN_PROGRESS\`, \`REVIEW\`, \`DONE\`, \`FAILED\` |
| [Sprint](#sprint) | \`SprintStatus\` | \`ACTIVE\`, \`COMPLETED\`, \`FAILED\` |
| [SprintRun](#sprintrun) | \`SprintRunStatus\` | \`QUEUED\`, \`RUNNING\`, \`PAUSED\`, \`DONE\`, \`FAILED\`, \`CANCELLED\` |
| [ClaudeJob](#claudejob) | \`ClaudeJobStatus\` | \`QUEUED\`, \`CLAIMED\`, \`RUNNING\`, \`DONE\`, \`FAILED\`, \`CANCELLED\`, \`SKIPPED\` |
| [Idea](#idea) | \`IdeaStatus\` | \`DRAFT\`, \`GRILLING\`, \`GRILL_FAILED\`, \`GRILLED\`, \`PLANNING\`, \`PLAN_FAILED\`, \`PLAN_READY\`, \`PLANNED\` |
## PBI
A **Product Backlog Item** holds one or more stories. Its status reflects whether the PBI as a whole is ready to be picked up, blocked on something external, finished, or written off.
\`\`\`mermaid
stateDiagram-v2
[*] --> READY: create_pbi
READY --> BLOCKED: user marks blocked
BLOCKED --> READY: user unblocks
READY --> DONE: all stories DONE
READY --> FAILED: user gives up
BLOCKED --> FAILED: user gives up
DONE --> [*]
FAILED --> [*]
\`\`\`
| Transition | Trigger | Side effect |
|---|---|---|
| \`* → READY\` | \`create_pbi\` MCP tool or PBI dialog | New PBI lands in \`priority\` group, \`sort_order = last + 1\` |
| \`READY ↔ BLOCKED\` | User toggles via PBI dialog | None besides log entry |
| \`READY → DONE\` | All child stories reach \`DONE\` | Auto-promotion (see [ST-1109 plan](../plans/ST-1109-pbi-status.md)) |
| \`* → FAILED\` | User gives up on the PBI | Stories may remain \`OPEN\`; PBI is filtered out of active boards |
## Story
A **Story** sits under a PBI. It moves out of the backlog when added to a Sprint, and reaches \`DONE\` when its tasks are complete and the implementation is verified.
\`\`\`mermaid
stateDiagram-v2
[*] --> OPEN: create_story
OPEN --> IN_SPRINT: added to sprint
IN_SPRINT --> OPEN: removed from sprint
IN_SPRINT --> DONE: all tasks DONE + verify passes
IN_SPRINT --> FAILED: verify fails / abandoned
DONE --> [*]
FAILED --> [*]
\`\`\`
| Transition | Trigger | Side effect |
|---|---|---|
| \`* → OPEN\` | \`create_story\` MCP tool or Story dialog | Lives in product backlog |
| \`OPEN ↔ IN_SPRINT\` | Drag onto Sprint board, or sprint-removal | Tasks denormalise \`sprint_id\` |
| \`IN_SPRINT → DONE\` | Story completion via MCP / UI; auto-PR flow may trigger | Auto-PR flow ([\`runbooks/auto-pr-flow.md\`](../runbooks/auto-pr-flow.md)) may run; PBI is re-evaluated for \`READY → DONE\` |
| \`IN_SPRINT → FAILED\` | Verification failure or manual abandon | Logged in story log |
## Task
A **Task** is the smallest unit. The Claude worker mainly reads \`implementation_plan\` and writes status transitions through MCP tools.
\`\`\`mermaid
stateDiagram-v2
[*] --> TO_DO: create_task
TO_DO --> IN_PROGRESS: agent claims / user starts
IN_PROGRESS --> REVIEW: implementation done, awaiting verify
REVIEW --> DONE: verify passes
REVIEW --> IN_PROGRESS: verify fails, retry
IN_PROGRESS --> FAILED: unrecoverable error
REVIEW --> FAILED: gives up after retries
DONE --> [*]
FAILED --> [*]
\`\`\`
| Transition | Trigger | Side effect |
|---|---|---|
| \`* → TO_DO\` | \`create_task\` MCP tool / Task dialog | Inherits \`sprint_id\` from parent story |
| \`TO_DO → IN_PROGRESS\` | Worker claim or user starts | Story may auto-promote to \`IN_SPRINT\` |
| \`IN_PROGRESS → REVIEW\` | Implementation logged | Optional \`verify_task_against_plan\` runs |
| \`REVIEW → DONE\` | Verify passes / human accepts | When all sibling tasks are \`DONE\`, the parent story is eligible for \`DONE\` |
| \`* → FAILED\` | Unrecoverable error or human marks failed | Story may auto-promote to \`FAILED\` |
The MCP tool is \`update_task_status({ task_id, status })\` accepting lowercase API values: \`todo | in_progress | review | done | failed\`.
## Sprint
A **Sprint** is the cross-cutting time-box. Its status tracks the overall sprint container, not the agent execution.
\`\`\`mermaid
stateDiagram-v2
[*] --> ACTIVE: create sprint
ACTIVE --> COMPLETED: user closes sprint
ACTIVE --> FAILED: user abandons sprint
COMPLETED --> [*]
FAILED --> [*]
\`\`\`
For execution semantics (PER_TASK vs SPRINT_BATCH) see [\`docs/architecture/sprint-execution-modes.md\`](../architecture/sprint-execution-modes.md).
## SprintRun
A **SprintRun** is one execution attempt of a sprint by the agent worker. Multiple runs may exist over a sprint's lifetime (if a run is cancelled or paused and restarted).
\`\`\`mermaid
stateDiagram-v2
[*] --> QUEUED: trigger sprint run
QUEUED --> RUNNING: worker claims
RUNNING --> PAUSED: pause requested
PAUSED --> RUNNING: resume
RUNNING --> DONE: all tasks done
RUNNING --> FAILED: unrecoverable
QUEUED --> CANCELLED: user cancels
RUNNING --> CANCELLED: user cancels
PAUSED --> CANCELLED: user cancels
DONE --> [*]
FAILED --> [*]
CANCELLED --> [*]
\`\`\`
The cascade rules (which task transitions automatically promote the SprintRun) are described in [\`docs/plans/sprint-pr-worktree-state-machines.md\`](../plans/sprint-pr-worktree-state-machines.md). When calling \`update_task_status\` from inside a sprint run, pass the optional \`sprint_run_id\` so the server can validate ownership and propagate cascades.
## ClaudeJob
The agent **job queue** (M13). Each enqueued unit of work is a \`ClaudeJob\` with a \`kind\` (\`TASK_IMPLEMENTATION\`, \`IDEA_GRILL\`, \`IDEA_MAKE_PLAN\`, \`PLAN_CHAT\`, \`SPRINT_IMPLEMENTATION\`).
\`\`\`mermaid
stateDiagram-v2
[*] --> QUEUED: enqueue
QUEUED --> CLAIMED: wait_for_job (FOR UPDATE SKIP LOCKED)
CLAIMED --> RUNNING: worker starts
RUNNING --> DONE: update_job_status('done')
RUNNING --> FAILED: update_job_status('failed')
QUEUED --> CANCELLED: user cancels
CLAIMED --> QUEUED: stale (>30min)
QUEUED --> SKIPPED: superseded
DONE --> [*]
FAILED --> [*]
CANCELLED --> [*]
SKIPPED --> [*]
\`\`\`
| Transition | Trigger | Side effect |
|---|---|---|
| \`QUEUED → CLAIMED\` | \`wait_for_job\` atomically claims | Bearer token is bound to the job (\`claimed_by_token_id\`) |
| \`CLAIMED → QUEUED\` | Stale claim (>30 min) | Auto-requeue on next \`wait_for_job\` |
| \`RUNNING → DONE\` | \`update_job_status('done')\` | Optional token-cost telemetry stored on the row |
| \`RUNNING → FAILED\` | \`update_job_status('failed')\` | For \`IDEA_GRILL\`/\`IDEA_MAKE_PLAN\`, idea status auto-rolls to \`GRILL_FAILED\` / \`PLAN_FAILED\` |
For idempotency rules and recovery procedures see [\`docs/runbooks/worker-idempotency.md\`](../runbooks/worker-idempotency.md).
## Idea
The **Idea** entity (M12) is a pre-PBI staging area. It goes through two AI-driven phases: a **grill** (Q&A loop with the user to clarify the idea) and a **plan** (single-pass output of a structured PBI tree). Failures are explicit terminal-ish states that allow retry.
\`\`\`mermaid
stateDiagram-v2
[*] --> DRAFT: create idea
DRAFT --> GRILLING: enqueue IDEA_GRILL
GRILLING --> GRILLED: update_idea_grill_md
GRILLING --> GRILL_FAILED: job failed
GRILL_FAILED --> GRILLING: retry
GRILLED --> PLANNING: enqueue IDEA_MAKE_PLAN
PLANNING --> PLAN_READY: update_idea_plan_md (parse ok)
PLANNING --> PLAN_FAILED: parsePlanMd rejected
PLAN_FAILED --> PLANNING: retry
PLAN_READY --> PLANNED: PBI tree created
PLANNED --> [*]
\`\`\`
| Transition | Trigger | Side effect |
|---|---|---|
| \`DRAFT → GRILLING\` | User clicks "Grill" | Enqueues \`IDEA_GRILL\` job; worker reads \`prompt_text\` + \`idea.grill_md\` |
| \`GRILLING → GRILLED\` | \`update_idea_grill_md\` | Logs \`IdeaLog{GRILL_RESULT}\` |
| \`* → GRILL_FAILED\` | \`update_job_status('failed')\` for \`IDEA_GRILL\` | Idea remains usable; user can retry |
| \`GRILLED → PLANNING\` | User clicks "Make plan" | Enqueues \`IDEA_MAKE_PLAN\`; worker outputs strict YAML-frontmatter |
| \`PLANNING → PLAN_READY\` | \`update_idea_plan_md\` parse ok | Logs \`IdeaLog{PLAN_RESULT}\` |
| \`PLANNING → PLAN_FAILED\` | \`parsePlanMd\` rejected | Logs \`IdeaLog{JOB_EVENT, errors}\` |
| \`PLAN_READY → PLANNED\` | PBI tree generated from plan | Idea is archived; PBI/Story/Task tree appears in the backlog |
For the full Idea workflow, prompts, and \`prompt_text\` contents, see [\`docs/plans/M12-ideas.md\`](../plans/M12-ideas.md).
## DB vs API mapping
> **Hardstop:** never bypass [\`lib/task-status.ts\`](../../lib/task-status.ts).
The database stores enums in \`UPPER_SNAKE\` (\`TO_DO\`, \`IN_PROGRESS\`, \`IN_SPRINT\`, …) because Prisma + PostgreSQL prefer that convention. The REST API exposes them in \`lowercase\` (\`todo\`, \`in_progress\`, \`in_sprint\`, …) because that's the convention HTTP consumers expect.
The two are mapped **only** through the helpers in [\`lib/task-status.ts\`](../../lib/task-status.ts):
\`\`\`ts
taskStatusToApi(status) // DB → API
taskStatusFromApi(input) // API → DB (returns null on bad input)
storyStatusToApi(status)
storyStatusFromApi(input)
pbiStatusToApi(status)
pbiStatusFromApi(input)
sprintStatusToApi(status)
sprintStatusFromApi(input)
sprintRunStatusToApi(status)
sprintRunStatusFromApi(input)
\`\`\`
Bad input on the inbound side (\`*FromApi\`) returns \`null\` — the route handler converts that to a \`422\` Zod-style error. See [\`docs/adr/0004-status-enum-mapping.md\`](../adr/0004-status-enum-mapping.md) for the rationale.
## What's next
[03 Git Workflow](./03-git-workflow.md) covers branching, commits, and the cost-driven PR rules.
`,
},
{
slug: ['03-git-workflow'] as const,
title: 'Git Workflow',
description: 'The Scrum4Me git workflow is shaped by two pressures that don\'t usually appear together:',
filePath: 'docs/manual/03-git-workflow.md',
markdown: `# 03 — Git Workflow
The Scrum4Me git workflow is shaped by two pressures that don't usually appear together:
1. An **AI agent** that can produce many commits per hour without human review,
2. A **Vercel Hobby plan** that meters preview deployments and bills for them.
These two together drive a workflow that looks unusual compared to "feature-branch + PR-per-story". This chapter explains the *why*; the authoritative *how* lives in the runbooks linked at the bottom.
## The five guiding rules
### 1. One branch per milestone, not per story
A milestone (e.g. \`M10-qr-login\`) groups multiple stories that ship together. The agent runs through them on a single branch named \`feat/M{N}-{slug}\` (or \`feat/ST-XXX-{slug}\` for one-off stories without a milestone). All commits accumulate on that branch.
> **Why?** Every push to a feature branch triggers a Vercel preview build. Pushing per story would multiply the build cost without producing more reviewable units of work the user reviews the milestone, not the story.
See [\`docs/adr/0003-one-branch-per-milestone.md\`](../adr/0003-one-branch-per-milestone.md) for the full rationale.
### 2. Commit per layer, not per task
A single task can touch the database, the API, and the UI. Each of those layers gets its own commit. The pattern:
\`\`\`
feat(ST-XXX): add field X to Prisma schema # DB
feat(ST-XXX): add Y endpoint accepting X # API
feat(ST-XXX): wire X into the editor component # UI
chore(ST-XXX): configure sharp for X processing # config
docs(ST-XXX): document the X feature # docs
\`\`\`
> **Why?** Reviewers and \`git bisect\` both benefit when one commit can be reverted without touching unrelated layers. A \`feat: add profile system\` mega-commit is an antipattern.
### 3. Push only after the user has tested
Commits accumulate **locally** until the milestone is functionally complete and the user has confirmed it works. Then and only then \`git push\` and \`gh pr create\`.
> **Why?** Same cost reason as rule 1. Mid-milestone "save points" should be local tags or \`git stash\`, not pushes. Some exceptions exist (planning-only PRs, emergency hotfixes); they're enumerated in [\`branch-and-commit.md\`](../runbooks/branch-and-commit.md#uitzonderingen-op-de-push-regel).
### 4. One PR per batch one preview build
When the worker runs through a queue of jobs, the entire run produces **one** PR with one commit per task. No interim pushes, no force-pushes to clean up history, no PR-per-story splits.
The end-to-end verification that one batch produces exactly one Vercel deployment is in [\`branch-and-commit.md\`](../runbooks/branch-and-commit.md) (see the *End-to-end verificatie* section).
### 5. Auto-PR flow at the end
Once a story reaches \`DONE\`, the auto-PR flow takes over: it pushes the branch, opens a PR, waits for the scope to be complete, waits for checks, and merges. The contract for "scope complete" and the path-filter / label rules that decide whether a deploy actually runs are split between two runbooks:
- **End-to-end pipeline**: [\`docs/runbooks/auto-pr-flow.md\`](../runbooks/auto-pr-flow.md)
- **Selective deploy controls** (\`skip-deploy\` label, path-filter for \`app/\`/\`components/\`/\`lib/\`): [\`docs/runbooks/deploy-control.md\`](../runbooks/deploy-control.md)
## Commit message format
\`\`\`
<type>(ST-XXX): short description
\`\`\`
Where \`<type>\` is one of \`feat\`, \`fix\`, \`chore\`, \`docs\`. The story code in parentheses links the commit back to the Scrum4Me MCP entity.
For PBI-level work (no single story), use the PBI code: \`docs(PBI-58): scaffold developer manual\`.
## Merge conflicts
| Scenario | Conflict? | Mitigation |
|---|---|---|
| Multiple tasks on the same batch branch | No they stack linearly on one branch | None needed |
| Two parallel batches touching the same files | Yes, possible | Serialise batches via the MCP \`get_claude_context\` flow (one story at a time per agent), or rebase before push |
| Long-lived branch drifting from \`main\` | Yes, possible | \`git fetch origin main && git rebase origin/main\` before \`gh pr create\` |
\`git push --force\` to "wipe" earlier preview builds is forbidden — it costs the same build again on recreation, defeating the purpose of the cost-control rules.
## When **not** to follow the strict rules
When the Vercel account moves to Pro (or another billing tier without per-build cost), this workflow can revert to the more conventional "branch + PR per story". When that happens, update the rule in [\`branch-and-commit.md\`](../runbooks/branch-and-commit.md) and log the change in [\`docs/decisions/agent-instructions-history.md\`](../decisions/agent-instructions-history.md).
## Deep links
| Topic | Authoritative source |
|---|---|
| Branch & commit rules (full normative spec) | [\`docs/runbooks/branch-and-commit.md\`](../runbooks/branch-and-commit.md) |
| Auto-PR flow (story-DONE merged-PR pipeline) | [\`docs/runbooks/auto-pr-flow.md\`](../runbooks/auto-pr-flow.md) |
| Deploy controls (labels, path-filter) | [\`docs/runbooks/deploy-control.md\`](../runbooks/deploy-control.md) |
| Vercel deployment specifics | [\`docs/runbooks/deploy-vercel.md\`](../runbooks/deploy-vercel.md) |
| Decision rationale (one-branch-per-milestone) | [\`docs/adr/0003-one-branch-per-milestone.md\`](../adr/0003-one-branch-per-milestone.md) |
| Worker idempotency & job-status protocol | [\`docs/runbooks/worker-idempotency.md\`](../runbooks/worker-idempotency.md) |
## What's next
[04 MCP Integration](./04-mcp-integration.md) covers how the Claude agent drives this workflow from the queue side.
`,
},
{
slug: ['04-mcp-integration'] as const,
title: 'MCP Integration',
description: 'Scrum4Me exposes its REST API as native Claude Code tools through a dedicated **MCP server** living in [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp). Schemas are shared via a git submodule (`vendor/scrum4me`) so there\'s exactly one definition of every type. From the agent\'s perspective, Scrum4Me looks like a set of native tools prefixed `mcp__scrum4me__*`.',
filePath: 'docs/manual/04-mcp-integration.md',
markdown: `# 04 — MCP Integration
Scrum4Me exposes its REST API as native Claude Code tools through a dedicated **MCP server** living in [\`madhura68/scrum4me-mcp\`](https://github.com/madhura68/scrum4me-mcp). Schemas are shared via a git submodule (\`vendor/scrum4me\`) so there's exactly one definition of every type. From the agent's perspective, Scrum4Me looks like a set of native tools prefixed \`mcp__scrum4me__*\`.
This chapter is the **onboarding tour**. The full tool reference (all 18 tools, their parameters, and edge cases) is in [\`docs/runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md).
## Three ways the agent works
| Mode | Triggered by | Loop |
|---|---|---|
| **Track A MCP-driven** | User says *"implement the next story"* | \`get_claude_context\` → execute tasks → \`update_task_status\` → commit per layer → repeat until queue empty → push + PR |
| **Track B Manual** | User describes a one-off change in chat | Read pattern + styling edit verify wait for \`commit it\` → commit |
| **Worker Queue-driven** | Background worker container running on a Mac/NAS | \`wait_for_job\` (blocks ≤600s) → switch on \`kind\` → execute → \`update_job_status\` → loop forever |
CLAUDE.md describes Track A and Track B; this manual focuses on the **Worker** mode because it's the most novel and the most likely to surprise a new contributor reading server logs.
## A typical Track A run
\`\`\`mermaid
sequenceDiagram
participant U as User
participant C as Claude
participant M as MCP server
participant DB as Postgres
U->>C: "implement the next story"
C->>M: get_claude_context(product_id)
M->>DB: SELECT product, sprint, next story, tasks
M-->>C: { story, tasks[], pbi, sprint }
loop per task in sort_order
C->>M: update_task_status(task_id, 'in_progress')
C->>C: read pattern + styling, edit files
C->>M: log_implementation(story_id, content)
C->>M: update_task_status(task_id, 'review')
C->>M: log_test_result(story_id, 'PASSED')
C->>M: update_task_status(task_id, 'done')
end
C->>U: "milestone ready for your test"
U->>C: "looks good, push it"
C->>C: git push + gh pr create
\`\`\`
The contract every step relies on:
- All inputs are **lowercase API enums** (\`'in_progress'\`, never \`'IN_PROGRESS'\`); the MCP server applies [\`lib/task-status.ts\`](../../lib/task-status.ts) under the hood.
- Status writes are **forbidden for demo accounts** they return \`403\`. See [02 — Statuses](./02-statuses-and-transitions.md#db-vs-api-mapping) and [\`docs/adr/0006-demo-user-three-layer-policy.md\`](../adr/0006-demo-user-three-layer-policy.md).
- Bearer tokens are bound to a product. \`list_products\` returns only what the token can see; \`get_claude_context\` is product-scoped.
## Idea jobs vs task implementation
The worker \`wait_for_job\` returns a payload with a \`kind\` discriminator. The agent must switch on it:
| \`kind\` | Behaviour |
|---|---|
| \`TASK_IMPLEMENTATION\` | Default. Execute the \`implementation_plan\`, follow the [git workflow](./03-git-workflow.md), end with \`update_job_status('done')\`. |
| \`IDEA_GRILL\` | Read embedded \`prompt_text\` + existing \`idea.grill_md\`. Iterate with \`ask_user_question\` / \`get_question_answer\`. End with \`update_idea_grill_md(markdown)\`. |
| \`IDEA_MAKE_PLAN\` | Read \`prompt_text\` + \`idea.grill_md\`. **Do not ask questions** — single-pass output in strict YAML-frontmatter. End with \`update_idea_plan_md(markdown)\`. Server-side parser may reject → \`PLAN_FAILED\`. |
| \`PLAN_CHAT\` | Conversational refinement loop on an existing plan (M12+). |
| \`SPRINT_IMPLEMENTATION\` | Sprint-level run that cascades through every task; \`update_task_status\` calls must include the \`sprint_run_id\`. |
For the full Idea state machine (DRAFT GRILLING PLANNED) see [02 Statuses & Transitions § Idea](./02-statuses-and-transitions.md#idea).
## The Q&A channel
When Claude needs a human decision mid-run, it doesn't block silently it posts a question through the MCP and either polls or returns control:
\`\`\`mermaid
sequenceDiagram
participant C as Claude
participant M as MCP
participant DB as Postgres
participant U as User (NavBar bell)
C->>M: ask_user_question({ story_id, question, wait_seconds: 600 })
M->>DB: INSERT user_question; NOTIFY user_question_created
DB-->>U: SSE event bell pulses
U->>M: POST /api/questions/:id/answer
M->>DB: UPDATE user_question; NOTIFY user_question_answered
DB-->>C: ask_user_question returns { answer }
C->>C: continue execution
\`\`\`
Key facts:
- \`wait_seconds\` is capped at 600. If the user doesn't answer in time, \`ask_user_question\` returns with status \`pending\`; Claude can resume later via \`get_question_answer(question_id)\`.
- Idea questions (\`{ idea_id }\` instead of \`{ story_id }\`) are **user-private** — they bypass \`productAccessFilter\`, so collaborators don't see them.
- A question can be cancelled by the asker via \`cancel_question\`.
The persistent design (table + \`LISTEN/NOTIFY\`) is documented in [\`docs/architecture/claude-question-channel.md\`](../architecture/claude-question-channel.md).
## The worker's pre-flight quota check
The worker doesn't blindly call \`wait_for_job\`. Each iteration it first checks Anthropic API quota via \`bin/worker-quota-probe.sh\` so it doesn't burn a 10-minute block on a queue it can't actually process. The full algorithm settings, \`worker_heartbeat\` SSE event, sleep-until-reset — is in [\`docs/runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md#pre-flight-quota-check-m13). The Docker chapter ([05](./05-docker.md#quota-probe)) shows how to test it locally.
## Schema-drift watchdog
If Scrum4Me's Prisma schema changes but \`scrum4me-mcp\` isn't synced, the MCP server will fail at runtime not at deploy. To prevent that, a remote agent runs every Monday at 08:00 Amsterdam time, syncs \`vendor/scrum4me\`, and runs \`prisma:generate\` + \`tsc --noEmit\` in \`scrum4me-mcp\`. Drift reports must be resolved **before** any Scrum4Me PR with schema changes can merge. See [\`docs/runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md#schema-drift-bewaking).
## Deep links
| Topic | Authoritative source |
|---|---|
| Tool reference (all 18 tools) | [\`docs/runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md) |
| Worker idempotency & job-status protocol | [\`docs/runbooks/worker-idempotency.md\`](../runbooks/worker-idempotency.md) |
| Q&A channel architecture (table + LISTEN/NOTIFY) | [\`docs/architecture/claude-question-channel.md\`](../architecture/claude-question-channel.md) |
| Idea-laag plan & prompts | [\`docs/plans/M12-ideas.md\`](../plans/M12-ideas.md) |
| Sprint execution modes (PER_TASK vs SPRINT_BATCH) | [\`docs/architecture/sprint-execution-modes.md\`](../architecture/sprint-execution-modes.md) |
| Realtime NOTIFY payload contract | [\`docs/patterns/realtime-notify-payload.md\`](../patterns/realtime-notify-payload.md) |
| Demo-user write protection | [\`docs/adr/0006-demo-user-three-layer-policy.md\`](../adr/0006-demo-user-three-layer-policy.md) |
## What's next
[05 Docker](./05-docker.md) covers how the worker container is run, debugged, and operated.
`,
},
{
slug: ['05-docker'] as const,
title: 'Docker',
description: 'This chapter is the contributor\'s tour of the Docker side of Scrum4Me. Two important up-front facts:',
filePath: 'docs/manual/05-docker.md',
markdown: `# 05 — Docker
This chapter is the contributor's tour of the Docker side of Scrum4Me. Two important up-front facts:
1. **The Next.js app is not containerised.** The web UI, API routes, server actions, and database connection all run on **Vercel** (serverless functions + Edge runtime). There is no \`Dockerfile\` in this repo and no \`docker-compose.yml\`.
2. **Only the worker is containerised.** The "worker" is a Claude Code agent in a long-running container that polls the Scrum4Me job queue via MCP and executes \`TASK_IMPLEMENTATION\` / \`IDEA_GRILL\` / \`IDEA_MAKE_PLAN\` / \`SPRINT_IMPLEMENTATION\` jobs.
The container image and its supporting scripts live in a **separate repo**: [\`madhura68/scrum4me-docker\`](https://github.com/madhura68/scrum4me-docker). This manual documents the consumer side — what the worker is, how it relates to Scrum4Me, and how to diagnose issues. The container internals (Dockerfile, entrypoint, agent provisioning) are out of scope for this manual; see that repo's README.
> **Note:** A separate sandbox repo \`scrum4me-sbx\` ([\`SC-4\`](https://github.com/madhura68/scrum4me-sbx)) exists for Docker exploration. Treat it as a scratchpad, not as the production worker.
## Topology
\`\`\`mermaid
flowchart LR
subgraph Vercel
App[Next.js app<br/>+ API routes]
end
subgraph Neon
DB[(Postgres)]
end
subgraph Mac["Mac (default) / NAS (opt-in)"]
Worker[Worker container<br/>Claude Code + MCP]
end
Worker -- MCP over HTTPS --> App
App -- Prisma --> DB
Worker -- git push --> GH[GitHub]
GH -- webhooks --> App
\`\`\`
- The worker **never connects to the database directly**. All state changes go through MCP tools, which call the Vercel-hosted REST API, which writes to Neon via Prisma.
- The worker **does** push commits directly to GitHub. GitHub then notifies Vercel and the auto-PR flow ([03 Git Workflow](./03-git-workflow.md)) takes over.
## Mac vs NAS
| Flow | When to use | Status |
|---|---|---|
| **Mac-native (arm64)** | Default for development and small teams | Active |
| **NAS** | Self-hosted always-on worker on a Synology / Asustor / similar | Opt-in, validated by historical smoke tests in [\`docs/docker-smoke/\`](../docker-smoke/) |
The Mac flow is the default because it doesn't require dedicated hardware. The container runs natively on Apple Silicon (arm64) no x86 emulation overhead.
## Environment variables the worker needs
The worker container needs **only** what's required to authenticate to MCP and push to GitHub:
| Var | Purpose |
|---|---|
| \`SCRUM4ME_BEARER_TOKEN\` | Bearer token bound to a product. Returned by the user's API-token settings page. |
| \`SCRUM4ME_BASE_URL\` | Usually \`https://scrum4me.vercel.app\` (or the user's domain). |
| \`GITHUB_TOKEN\` | Personal access token with \`repo\` scope, used by \`git push\` and \`gh pr create\`. |
| \`ANTHROPIC_API_KEY\` | The Claude API key used by the worker process. |
| \`MIN_QUOTA_PCT\` | Optional. Worker pauses if Anthropic quota drops below this percentage. |
> **Hardstop:** the worker does **not** need \`DATABASE_URL\`, \`SESSION_SECRET\`, or \`CRON_SECRET\`. Those belong to the Next.js app; the worker only talks to MCP. If you find yourself adding DB env vars to the worker, stop — you're solving the wrong problem.
The full list and provisioning instructions live in the [\`scrum4me-docker\` README](https://github.com/madhura68/scrum4me-docker). **TODO:** link to specific sections of that README once it's stable.
## What the worker loop does, on a single iteration
\`\`\`mermaid
sequenceDiagram
participant W as Worker
participant Q as worker-quota-probe.sh
participant M as MCP server
W->>Q: probe Anthropic quota
Q-->>W: { pct, reset_at_iso }
alt pct < MIN_QUOTA_PCT
W->>M: worker_heartbeat(pct, last_quota_check_at)
W->>W: sleep until reset_at_iso (cap 1h)
else quota ok
W->>M: worker_heartbeat(pct, last_quota_check_at)
W->>M: wait_for_job (block 600s, claim atomically)
alt queue empty
W->>W: continue (no work, loop again)
else got job
W->>W: execute by \`kind\`
W->>M: update_job_status(done|failed)
end
end
Note over W: continue forever
\`\`\`
The loop is described authoritatively in [\`docs/runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md#batch-loop-verplichte-agent-flow) and [\`docs/runbooks/worker-idempotency.md\`](../runbooks/worker-idempotency.md).
### Quota probe
\`bin/worker-quota-probe.sh\` (in \`scrum4me-docker\`) makes a tiny call to the Anthropic API to read the current quota percentage and reset time. Cost: ~1 output token per probe (~12 tokens/hour at 5-minute intervals). The default \`MIN_QUOTA_PCT\` is **20%** — typically high enough on Pro/Max plans that the worker never pauses during normal day-job hours.
### Heartbeat
Every iteration the worker calls \`worker_heartbeat({ last_quota_pct, last_quota_check_at })\`. The MCP server emits an SSE event so the NavBar in the Next.js app shows the worker as live. A heartbeat older than 15 seconds is rendered as "offline" / "stand-by" in the UI.
### Stale-claim recovery
If a worker dies mid-job (process crash, container kill, network partition), its claimed job stays as \`CLAIMED\` in the database. After **30 minutes** the next \`wait_for_job\` call automatically requeues it (\`CLAIMED → QUEUED\`) before claiming a fresh one. No manual intervention is required for clean recovery.
When you **do** need to manually requeue a job (e.g. you killed it intentionally and don't want to wait 30 min), the operator route is the admin board "Requeue job" button. **TODO:** confirm the exact UI path; this is not yet documented in \`docs/runbooks/\`.
## Running the worker locally
The intended local workflow per the project's standing memory is **Mac-native Docker** (the user's \`project_docker_default_target\` memory). High-level steps (verify against the [scrum4me-docker README](https://github.com/madhura68/scrum4me-docker) for exact commands):
1. Clone \`scrum4me-docker\` next to \`Scrum4Me/\` (so \`~/Development/Scrum4Me/scrum4me-docker/\`).
2. Provision the env vars above (typically a \`.env\` file in that repo, **not committed**).
3. \`docker build\` the image and \`docker run\` it with the env file mounted.
4. Watch container logs for the heartbeat/quota cycle.
5. Trigger a job from the UI ("Voer alle uit" on the Solo Board) and verify the worker picks it up within ~5 seconds.
> **TODO:** once the \`scrum4me-docker\` README has stabilised, replace the bullets above with copy-paste-ready commands. Until then, defer to that repo for canonical instructions.
## Debugging a stuck worker
| Symptom | Likely cause | Fix |
|---|---|---|
| Worker shows offline in NavBar but container is running | \`worker_heartbeat\` not reaching MCP | Check \`SCRUM4ME_BASE_URL\` and \`SCRUM4ME_BEARER_TOKEN\`; tail container logs for HTTP errors |
| Worker logs say "stand-by" indefinitely | \`pct < MIN_QUOTA_PCT\` and reset_at not reached | Lower \`MIN_QUOTA_PCT\` for testing, or wait for the printed \`reset_at_iso\` |
| Job stuck \`CLAIMED\` for >30 min | Worker died mid-job | Wait — auto-requeue triggers on next \`wait_for_job\` |
| Worker claims job but never updates status | Crashed before \`update_job_status\`; container restarted in a loop | Check \`docker logs\`; the next \`wait_for_job\` will requeue stale claims |
| \`update_job_status\` returns \`403\` | Bearer token doesn't match \`claimed_by_token_id\` | The token was rotated mid-run; restart with fresh token |
For deeper troubleshooting see [06 Troubleshooting](./06-troubleshooting.md).
## Smoke-test references
Historical Docker smoke tests live in [\`docs/docker-smoke/\`](../docker-smoke/). They validated the worktree-isolation + branch-per-story flow when the Docker worker was first introduced. They are **historical** — don't expect them to be runnable as-is — but they're a useful reference when you want to verify the same flow on a new container image.
## Deep links
| Topic | Source |
|---|---|
| Container image, Dockerfile, build | [\`scrum4me-docker\` repo](https://github.com/madhura68/scrum4me-docker) |
| Worker loop & quota check | [\`docs/runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md#pre-flight-quota-check-m13) |
| Worker idempotency / job-status protocol | [\`docs/runbooks/worker-idempotency.md\`](../runbooks/worker-idempotency.md) |
| Historical smoke tests | [\`docs/docker-smoke/\`](../docker-smoke/) |
| Sandbox / exploration repo | [\`scrum4me-sbx\` repo](https://github.com/madhura68/scrum4me-sbx) |
## What's next
[06 Troubleshooting](./06-troubleshooting.md) covers error codes and recovery procedures across the full stack.
`,
},
{
slug: ['06-troubleshooting'] as const,
title: 'Troubleshooting',
description: 'This chapter is the **first place to look** when something is wrong. Each row links to the authoritative source so you can dig deeper without losing your trail.',
filePath: 'docs/manual/06-troubleshooting.md',
markdown: `# 06 — Troubleshooting
This chapter is the **first place to look** when something is wrong. Each row links to the authoritative source so you can dig deeper without losing your trail.
## Error code reference
These three HTTP status codes are non-negotiable hardstops in the API surface they always mean the same thing across every route handler.
| Code | Meaning | Where it comes from |
|---|---|---|
| **\`400\`** | JSON parse error | Body couldn't be parsed as JSON. Usually a malformed request from a client. |
| **\`422\`** | Zod validation error | Body parsed, but failed schema validation. Response includes the offending field path. |
| **\`403\`** | Demo-user write blocked | Authenticated user \`is_demo = true\` attempted a write. Three layers enforce this — see [\`docs/adr/0006-demo-user-three-layer-policy.md\`](../adr/0006-demo-user-three-layer-policy.md). |
> **Hardstop:** these codes are reserved. Do not use \`400\` for validation errors or \`422\` for unauthorised access. The contract is enforced at the route-handler level — see the [Route Handler pattern](../patterns/route-handler.md).
Other common codes:
| Code | Meaning |
|---|---|
| \`401\` | No session / invalid bearer token |
| \`404\` | Resource not found, or token does not have access |
| \`409\` | State conflict — e.g. trying to claim a job that's already \`CLAIMED\` |
| \`429\` | Rate-limited — typically the Anthropic quota cap, not Scrum4Me itself |
| \`500\` | Unhandled server error. Always check Vercel function logs. |
## Symptom cause fix
### MCP
| Symptom | Likely cause | Fix |
|---|---|---|
| \`mcp__scrum4me__get_claude_context\` returns \`null\` or empty story | Bearer token doesn't have access to that product | Run \`mcp__scrum4me__list_products\` to confirm scope; rotate the token if needed |
| \`mcp__scrum4me__update_task_status\` returns \`403\` | Demo user, or token mismatch in a sprint run | Check user identity; if inside a sprint run, the bearer token must match \`claimed_by_token_id\` of the parent job |
| \`mcp__scrum4me__wait_for_job\` returns nothing for the full 600s block | Queue is genuinely empty | This is normal — loop and call again. See [\`runbooks/mcp-integration.md\`](../runbooks/mcp-integration.md#batch-loop-verplichte-agent-flow) |
| Job stays \`CLAIMED\` for >30 minutes | Worker died mid-job | Auto-requeue triggers on next \`wait_for_job\`; no manual action needed |
| \`update_idea_plan_md\` causes idea to flip to \`PLAN_FAILED\` | \`parsePlanMd\` server-side rejected the YAML-frontmatter | Inspect \`IdeaLog{JOB_EVENT, errors}\` for the parse error; re-run \`IDEA_MAKE_PLAN\` after fixing the prompt |
### Statuses & data integrity
| Symptom | Likely cause | Fix |
|---|---|---|
| Status displayed differently in DB vs UI | Some code path bypassed \`lib/task-status.ts\` | Grep the codebase for direct enum string usage; force everything through the mappers. See [\`adr/0004-status-enum-mapping.md\`](../adr/0004-status-enum-mapping.md) |
| Story stuck \`IN_SPRINT\` when all tasks are \`DONE\` | Auto-promotion not triggered | Check the most recent \`update_task_status\` call — it may have failed silently. Re-issue with the correct task |
| PBI not auto-promoting to \`DONE\` | Not all child stories are \`DONE\` yet | List stories under the PBI; one is probably still \`OPEN\` or \`IN_SPRINT\` |
| \`422\` from \`create_pbi\` / \`create_story\` / \`create_task\` | Zod validation failed (length cap, missing required field) | Response body includes field path — fix and retry |
| \`IdeaStatus\` stays \`GRILLING\` long after the worker stopped | The job ended without calling \`update_idea_grill_md\` | Check the worker logs for an exception; manually requeue or mark \`GRILL_FAILED\` to allow retry |
### Git & deploy
| Symptom | Likely cause | Fix |
|---|---|---|
| Unexpected Vercel preview build appeared mid-batch | An interim push happened that shouldn't have | Inspect \`git log --all --graph\` for the offending push; review [\`runbooks/branch-and-commit.md\`](../runbooks/branch-and-commit.md) |
| PR has multiple Vercel deployments for the same commit range | Force-push, or push-then-revert | Don't force-push. If genuinely needed, document in the PR description |
| Auto-PR didn't open after story \`DONE\` | Story not actually \`DONE\`, or auto-PR pre-conditions unmet | Walk through [\`runbooks/auto-pr-flow.md\`](../runbooks/auto-pr-flow.md); typically a missing \`update_task_status('done')\` for the last task |
| Vercel skipped the deploy entirely | \`skip-deploy\` label or path-filter excluded the changed paths | See [\`runbooks/deploy-control.md\`](../runbooks/deploy-control.md) for the rules |
| Merge conflict between two parallel batches | Two branches touched the same files | Serialise: merge the first PR before pushing the second. Then \`git fetch origin main && git rebase origin/main\` |
### Realtime
| Symptom | Likely cause | Fix |
|---|---|---|
| Solo Board doesn't update when status changes | SSE connection dropped, or NOTIFY payload missing fields | Reload the page; if it persists, check \`DIRECT_URL\` (LISTEN/NOTIFY needs the pooler-bypass URL). See [\`patterns/realtime-notify-payload.md\`](../patterns/realtime-notify-payload.md) |
| NavBar bell doesn't pulse on new question | SSE/event channel mismatched, or payload missing required fields | Confirm the question was actually inserted (\`mcp__scrum4me__list_open_questions\`); inspect the Network tab for the SSE connection |
| Worker shows offline despite a running container | \`worker_heartbeat\` not reaching MCP | Verify \`SCRUM4ME_BASE_URL\` and bearer token; tail container logs |
### Auth & sessions
| Symptom | Likely cause | Fix |
|---|---|---|
| Login redirects in a loop | Session cookie not set; usually \`SESSION_SECRET\` mismatch between deployments | Check Vercel env vars for \`SESSION_SECRET\` (must be ≥32 chars); see [\`patterns/iron-session.md\`](../patterns/iron-session.md) |
| All write buttons disabled with "Niet beschikbaar in demo-modus" tooltip | You're logged in as the demo user | Log out and log in with a real account |
| \`403\` on a route that should be allowed | Proxy or server-action layer rejected the request | Walk through the three layers in [\`adr/0006-demo-user-three-layer-policy.md\`](../adr/0006-demo-user-three-layer-policy.md); each can independently say "no" |
### Build & dev-server
| Symptom | Likely cause | Fix |
|---|---|---|
| \`npm run build\` fails with \`Cannot find module '@/...'\` | TypeScript path alias mismatch | Check \`tsconfig.json\` \`paths\`; rerun \`npm run prebuild\` if codegen is stale |
| Mermaid diagram renders as plain text in the in-app \`/manual\` viewer | \`MermaidBlock\` not picking up \`language-mermaid\` | See [04 — MCP Integration](./04-mcp-integration.md) won't help here — open \`app/(app)/manual/_components/mermaid-block.tsx\` and confirm the dynamic import is \`ssr: false\` |
| "Server-only" import error in browser | A \`*-server.ts\` module was imported into a client component | Refactor — split server logic out, or use a server action. Hardstop in [\`CLAUDE.md\`](../../CLAUDE.md#hardstop-regels) |
| \`npm run dev\` shows hydration mismatch | Server and client render diverge — usually time-based or random values | Wrap in \`useEffect\` for client-only state, or pass server time as a prop |
## When in doubt
1. **Read the runbook.** Each runbook in [\`docs/runbooks/\`](../runbooks/) starts with a \`when_to_read\` field — match the situation.
2. **Check the ADRs.** The ADR index in [\`docs/INDEX.md\`](../INDEX.md) lists the rationale for every cross-cutting decision. If your fix would contradict an ADR, talk to a maintainer first.
3. **Read the agent-flow pitfalls log.** [\`docs/runbooks/agent-flow-pitfalls.md\`](../runbooks/agent-flow-pitfalls.md) is a living list of issues found during agent runs and how they were resolved.
4. **Look at recent commits.** \`git log --oneline --since='7 days ago'\` often reveals the very change that broke whatever you're debugging.
## Escalation
If after the steps above the issue is still unresolved:
- **AI agent / MCP issues** file in the [\`scrum4me-mcp\` repo](https://github.com/madhura68/scrum4me-mcp).
- **Worker container issues** file in the [\`scrum4me-docker\` repo](https://github.com/madhura68/scrum4me-docker).
- **App / data / status issues** file in the [\`Scrum4Me\` repo](https://github.com/madhura68/Scrum4Me).
## What's next
You've reached the end of the manual. Bookmark this troubleshooting chapter — it's the most-revisited page once you're past onboarding.
Back to [index](./index.md).
`,
},
] as const;

189
package-lock.json generated
View file

@ -25,6 +25,7 @@
"dotenv": "^17.4.2",
"iron-session": "^8.0.4",
"lucide-react": "^1.8.0",
"mermaid": "^11.14.0",
"next": "16.2.4",
"next-themes": "^0.4.6",
"pg": "^8.20.0",
@ -36,6 +37,8 @@
"react-markdown": "^10.1.0",
"react-textarea-autosize": "^8.5.9",
"recharts": "^3.8.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"shadcn": "^4.4.0",
"sharp": "^0.34.5",
@ -96,7 +99,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"package-manager-detector": "^1.3.0",
@ -645,7 +647,6 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
"integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
"dev": true,
"license": "MIT"
},
"node_modules/@bramus/specificity": {
@ -665,7 +666,6 @@
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz",
"integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "12.0.0",
@ -676,7 +676,6 @@
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz",
"integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "12.0.0"
@ -686,21 +685,18 @@
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz",
"integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@chevrotain/types": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz",
"integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@chevrotain/utils": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz",
"integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@csstools/color-helpers": {
@ -2058,14 +2054,12 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
"dev": true,
"license": "MIT"
},
"node_modules/@iconify/utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz",
"integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@antfu/install-pkg": "^1.1.0",
@ -2813,7 +2807,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz",
"integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"langium": "^4.0.0"
@ -6208,7 +6201,6 @@
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
@ -6253,7 +6245,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
@ -6263,7 +6254,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
@ -6273,7 +6263,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-color": {
@ -6286,7 +6275,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
@ -6297,21 +6285,18 @@
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-dispatch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
@ -6321,7 +6306,6 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-ease": {
@ -6334,7 +6318,6 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-dsv": "*"
@ -6344,21 +6327,18 @@
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-format": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-geo": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
@ -6368,7 +6348,6 @@
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
@ -6390,21 +6369,18 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-quadtree": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-random": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-scale": {
@ -6420,14 +6396,12 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-shape": {
@ -6449,7 +6423,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-timer": {
@ -6462,7 +6435,6 @@
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
@ -6472,7 +6444,6 @@
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "*",
@ -6536,7 +6507,6 @@
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
@ -6661,7 +6631,6 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT",
"optional": true
},
@ -7293,7 +7262,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
"integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"d3-selection": "^3.0.0",
@ -8963,7 +8931,6 @@
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz",
"integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "12.0.0",
@ -8980,7 +8947,6 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz",
"integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash-es": "^4.17.21"
@ -9683,7 +9649,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
"integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
"dev": true,
"license": "MIT",
"dependencies": {
"layout-base": "^1.0.0"
@ -9781,7 +9746,6 @@
"version": "3.33.2",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz",
"integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10"
@ -9791,7 +9755,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
"integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"cose-base": "^1.0.0"
@ -9804,7 +9767,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
"integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"cose-base": "^2.2.0"
@ -9817,7 +9779,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
"integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"layout-base": "^2.0.0"
@ -9827,14 +9788,12 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
"integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
"dev": true,
"license": "MIT"
},
"node_modules/d3": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-array": "3",
@ -9888,7 +9847,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@ -9898,7 +9856,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
@ -9915,7 +9872,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-path": "1 - 3"
@ -9937,7 +9893,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-array": "^3.2.0"
@ -9950,7 +9905,6 @@
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
"dev": true,
"license": "ISC",
"dependencies": {
"delaunator": "5"
@ -9963,7 +9917,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@ -9973,7 +9926,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
@ -9987,7 +9939,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
"dev": true,
"license": "ISC",
"dependencies": {
"commander": "7",
@ -10013,7 +9964,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
@ -10023,7 +9973,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@ -10045,7 +9994,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-dsv": "1 - 3"
@ -10058,7 +10006,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
@ -10082,7 +10029,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-array": "2.5.0 - 3"
@ -10095,7 +10041,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@ -10126,7 +10071,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@ -10136,7 +10080,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@ -10146,7 +10089,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@ -10156,7 +10098,6 @@
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
"integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "1 - 2",
@ -10167,7 +10108,6 @@
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"internmap": "^1.0.0"
@ -10177,14 +10117,12 @@
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/d3-sankey/node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"d3-path": "1"
@ -10194,7 +10132,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
"dev": true,
"license": "ISC"
},
"node_modules/d3-scale": {
@ -10217,7 +10154,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
@ -10231,7 +10167,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@ -10286,7 +10221,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
@ -10306,7 +10240,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
@ -10323,7 +10256,6 @@
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
"integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"d3": "^7.9.0",
@ -10418,7 +10350,6 @@
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
@ -10629,7 +10560,6 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
"integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"robust-predicates": "^3.0.2"
@ -10746,7 +10676,6 @@
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz",
"integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==",
"dev": true,
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@ -12447,6 +12376,12 @@
"giget": "dist/cli.mjs"
}
},
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
"node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
@ -12593,7 +12528,6 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
"integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
"dev": true,
"license": "MIT"
},
"node_modules/has-bigints": {
@ -12687,6 +12621,32 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-heading-rank": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz",
"integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-is-element": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
"integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@ -12714,6 +12674,19 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-string": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz",
"integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@ -14042,7 +14015,6 @@
"version": "0.16.45",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
"integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
"dev": true,
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
@ -14059,7 +14031,6 @@
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
@ -14078,8 +14049,7 @@
"node_modules/khroma": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==",
"dev": true
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
},
"node_modules/kleur": {
"version": "4.1.5",
@ -14094,7 +14064,6 @@
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz",
"integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@chevrotain/regexp-to-ast": "~12.0.0",
@ -14133,7 +14102,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
"integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
"dev": true,
"license": "MIT"
},
"node_modules/levn": {
@ -14568,7 +14536,6 @@
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.debounce": {
@ -15200,7 +15167,6 @@
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz",
"integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
@ -15230,7 +15196,6 @@
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
"dev": true,
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@ -15914,7 +15879,6 @@
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.16.0",
@ -15927,14 +15891,12 @@
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
"dev": true,
"license": "MIT"
},
"node_modules/mlly/node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.1.8",
@ -16704,7 +16666,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
"dev": true,
"license": "MIT"
},
"node_modules/pako": {
@ -16813,7 +16774,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
"dev": true,
"license": "MIT"
},
"node_modules/path-exists": {
@ -17093,14 +17053,12 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
"integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
"dev": true,
"license": "MIT"
},
"node_modules/points-on-path": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
"integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-data-parser": "0.1.0",
@ -18142,6 +18100,41 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rehype-autolink-headings": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz",
"integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@ungap/structured-clone": "^1.0.0",
"hast-util-heading-rank": "^3.0.0",
"hast-util-is-element": "^3.0.0",
"unified": "^11.0.0",
"unist-util-visit": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-slug": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz",
"integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"github-slugger": "^2.0.0",
"hast-util-heading-rank": "^3.0.0",
"hast-util-to-string": "^3.0.0",
"unist-util-visit": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@ -18356,7 +18349,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
"integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
"dev": true,
"license": "Unlicense"
},
"node_modules/rolldown": {
@ -18441,7 +18433,6 @@
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
"integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hachure-fill": "^0.5.2",
@ -18515,7 +18506,6 @@
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/rxjs": {
@ -19671,7 +19661,6 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
"integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
"dev": true,
"license": "MIT"
},
"node_modules/sucrase": {
@ -19959,7 +19948,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
"integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@ -20134,7 +20122,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.10"
@ -20388,7 +20375,6 @@
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
"dev": true,
"license": "MIT"
},
"node_modules/unbox-primitive": {
@ -20735,7 +20721,6 @@
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@ -21032,7 +21017,6 @@
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@ -21042,7 +21026,6 @@
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"vscode-languageserver-protocol": "3.17.5"
@ -21055,7 +21038,6 @@
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"dev": true,
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
@ -21066,21 +21048,18 @@
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
"dev": true,
"license": "MIT"
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"dev": true,
"license": "MIT"
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"dev": true,
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {

View file

@ -5,6 +5,7 @@
"scripts": {
"predev": "npx --yes kill-port 3000 || exit 0",
"dev": "next dev -p 3000",
"prebuild": "npm run manual:build",
"build": "next build",
"start": "next start",
"lint": "eslint",
@ -21,6 +22,7 @@
"seed": "prisma db seed",
"docs:index": "node scripts/generate-docs-index.mjs",
"docs:check-links": "node scripts/check-doc-links.mjs",
"manual:build": "node scripts/build-manual.mjs",
"docs": "npm run docs:index && npm run docs:check-links",
"diagrams": "mmdc -i docs/diagrams/architecture.mmd -t default -b transparent -o public/diagrams/architecture-light.svg && mmdc -i docs/diagrams/architecture.mmd -t dark -b transparent -o public/diagrams/architecture-dark.svg"
},
@ -41,6 +43,7 @@
"dotenv": "^17.4.2",
"iron-session": "^8.0.4",
"lucide-react": "^1.8.0",
"mermaid": "^11.14.0",
"next": "16.2.4",
"next-themes": "^0.4.6",
"pg": "^8.20.0",
@ -52,6 +55,8 @@
"react-markdown": "^10.1.0",
"react-textarea-autosize": "^8.5.9",
"recharts": "^3.8.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"shadcn": "^4.4.0",
"sharp": "^0.34.5",

159
scripts/build-manual.mjs Normal file
View file

@ -0,0 +1,159 @@
#!/usr/bin/env node
// Generate lib/manual.generated.ts — a typed TOC of the docs/manual/ chapters.
// Walks docs/manual/, parses front-matter, extracts title and description, and
// emits a single TS file consumed by the in-app /manual route.
//
// Usage: `npm run manual:build` (also chained into `prebuild`).
//
// Pure Node 20 — no external deps. Mirrors scripts/generate-docs-index.mjs.
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { join, relative, basename, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
const REPO_ROOT = join(SCRIPT_DIR, '..');
const MANUAL_DIR = join(REPO_ROOT, 'docs', 'manual');
const OUT_PATH = join(REPO_ROOT, 'lib', 'manual.generated.ts');
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const e of entries) {
const full = join(dir, e.name);
if (e.isDirectory()) {
files.push(...(await walk(full)));
} else if (e.isFile() && e.name.endsWith('.md')) {
files.push(full);
}
}
return files;
}
function parseFrontMatter(content) {
if (!content.startsWith('---\n')) return { data: {}, body: content };
const end = content.indexOf('\n---\n', 4);
if (end === -1) return { data: {}, body: content };
const block = content.slice(4, end);
const data = {};
for (const raw of block.split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const m = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*?)\s*$/);
if (!m) continue;
let val = m[2];
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1);
}
data[m[1]] = val;
}
return { data, body: content.slice(end + 5) };
}
function extractFirstH1(body) {
const m = body.match(/^#\s+(.+?)\s*$/m);
return m ? m[1] : null;
}
function extractFirstParagraph(body) {
// Skip leading H1, then take the first non-heading, non-blank block.
const lines = body.split('\n');
let i = 0;
while (i < lines.length && (lines[i].trim() === '' || lines[i].startsWith('#'))) i++;
const para = [];
while (i < lines.length && lines[i].trim() !== '') {
if (lines[i].startsWith('>') || lines[i].startsWith('|') || lines[i].startsWith('```')) break;
para.push(lines[i]);
i++;
}
return para.join(' ').replace(/\s+/g, ' ').trim();
}
// docs/manual/01-overview.md → ['01-overview']
// docs/manual/index.md → []
function fileToSlug(rel) {
const stripped = rel.replace(/^docs\/manual\//, '').replace(/\.md$/, '');
if (stripped === 'index') return [];
return stripped.split('/');
}
function escapeTs(s) {
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function escapeBacktick(s) {
return String(s).replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
}
function stripFrontMatter(content) {
if (!content.startsWith('---\n')) return content;
const end = content.indexOf('\n---\n', 4);
if (end === -1) return content;
return content.slice(end + 5).replace(/^\s*\n/, '');
}
async function main() {
const files = (await walk(MANUAL_DIR)).sort();
const entries = [];
for (const full of files) {
const rel = relative(REPO_ROOT, full).split(sep).join('/');
const content = await readFile(full, 'utf8');
const { data, body } = parseFrontMatter(content);
const slug = fileToSlug(rel);
const title = data.title || extractFirstH1(body) || basename(full, '.md');
const description = extractFirstParagraph(body) || '';
const markdown = stripFrontMatter(content);
entries.push({
slug,
title,
description,
filePath: rel,
markdown,
});
}
// Sort: index first, then by filename so numeric prefixes drive order.
entries.sort((a, b) => {
if (a.slug.length === 0) return -1;
if (b.slug.length === 0) return 1;
return a.filePath.localeCompare(b.filePath);
});
const lines = [];
lines.push('// AUTO-GENERATED by scripts/build-manual.mjs. Do not edit by hand.');
lines.push('// Run `npm run manual:build` to regenerate.');
lines.push('');
lines.push('export type ManualEntry = {');
lines.push(' slug: readonly string[]');
lines.push(' title: string');
lines.push(' description: string');
lines.push(' filePath: string');
lines.push(' markdown: string');
lines.push('}');
lines.push('');
lines.push('export const MANUAL_TOC: readonly ManualEntry[] = [');
for (const e of entries) {
const slugLit = '[' + e.slug.map((s) => `'${escapeTs(s)}'`).join(', ') + '] as const';
lines.push(' {');
lines.push(` slug: ${slugLit},`);
lines.push(` title: '${escapeTs(e.title)}',`);
lines.push(` description: '${escapeTs(e.description)}',`);
lines.push(` filePath: '${escapeTs(e.filePath)}',`);
lines.push(` markdown: \`${escapeBacktick(e.markdown)}\`,`);
lines.push(' },');
}
lines.push('] as const;');
lines.push('');
await writeFile(OUT_PATH, lines.join('\n'), 'utf8');
console.log(`Wrote ${relative(REPO_ROOT, OUT_PATH)} (${entries.length} chapters)`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});