Add document converter, seeder data structure, and project wiki

- ai-service/convert.py: converts Office/PDF files to markdown with frontmatter
- database/seeders/data/: folder structure for themas, projects, documents, etc.
- database/seeders/data/raw/: drop zone for Office/PDF files to convert
- wiki/: project architecture, concepts, and knowledge graph documentation
- Remove unused Laravel example tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-04-08 08:33:30 +02:00
parent 302c790c13
commit 926872a082
23 changed files with 1785 additions and 76 deletions

View File

@@ -43,6 +43,22 @@ All agent definitions are in `.claude/agents/`:
- Basic AI chat + project summaries + semantic search
- Dashboard with personal overview
## Wiki (Knowledge Base)
The project wiki lives in `wiki/` and follows the wiki-template schema. It is the canonical knowledge base for agents.
**Quick access:**
- `python wiki/tools/query.py health` — project health overview
- `python wiki/tools/query.py entity "project"` — everything about an entity
- `python wiki/tools/query.py test "phpunit"` — test results
- `python wiki/tools/query.py status "proven"` — all pages with status
- `bash wiki/tools/search.sh "keyword"` — full-text search
- `bash wiki/tools/lint.sh` — check wiki health (orphans, missing frontmatter)
**Source of truth hierarchy:** test results > code > knowledge-graph.yaml > wiki pages > chat
**After making significant changes:** update relevant wiki pages, `knowledge-graph.yaml`, and `wiki/log.md`.
## What Requires Human Validation
- Architecture decisions
- Domain model changes

View File

@@ -1,58 +1,71 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
# Innovatieplatform
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
Innovation governance platform for the R&D Lab at Waterschap Brabantse Delta. Supports the full lifecycle of innovation trajectories — from signal to handover — with AI-powered search, summarization, and project assistance.
## About Laravel
## Tech Stack
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- **Backend:** Laravel 13 (PHP 8.3+) — service-oriented, event-driven, API-first
- **Frontend:** Vue 3 + Inertia.js + Vite 8 + Tailwind CSS 4.2
- **Visualization:** D3.js 7.9 (zoomable metro map)
- **Database:** PostgreSQL 16 + pgvector
- **AI Service:** Python FastAPI + LangGraph + RAG
- **Infrastructure:** Docker Compose (nginx, php-fpm, worker, scheduler, ai-service, postgresql, redis)
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
## Agentic Development
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
## Quick Start
```bash
composer require laravel/boost --dev
# Clone and setup
git clone https://gitea.wbd-rd.nl/vps1_gitea_admin/innovatieplatform.git
cd innovatieplatform
composer setup
php artisan boost:install
# Development (starts Laravel server, queue worker, logs, and Vite)
composer dev
```
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
### Docker
## Contributing
```bash
docker compose up -d
```
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
Services: nginx (:80), postgresql (:5432), redis (:6379), ai-service (:8000)
## Code of Conduct
## Project Structure
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
```
app/
Models/ 21 Eloquent models (Project, Fase, Commitment, Document, ...)
Enums/ 14 status/type enums (ProjectStatus, FaseType, ...)
Services/ Business logic (ProjectService, MapDataService, ThemaService)
Http/Controllers/ API + Inertia controllers
resources/js/
Pages/ Inertia page components (MetroMap, Auth, Dashboard)
Components/ Vue components (MetroCanvas, CliBar, NodePreview, Breadcrumb)
Layouts/ App layout wrapper
ai-service/ Python FastAPI AI service
wiki/ Project knowledge base (wiki-template schema)
docker/ Docker configs (php, nginx, scheduler)
.claude/agents/ 10 Claude Code agent definitions
```
## Security Vulnerabilities
## Documentation
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
- **CLAUDE.md** — Architecture principles, build agents, MVP scope
- **STYLE_GUIDE.md** — Metro map UI + retro-futurism design system
- **wiki/** — Knowledge base with structured data, architecture docs, and query tools
## License
## Wiki
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
```bash
python wiki/tools/query.py health # Project health overview
python wiki/tools/query.py entity "project" # Everything about an entity
bash wiki/tools/search.sh "keyword" # Full-text search
bash wiki/tools/lint.sh # Check wiki health
```
## Tests
```bash
composer test
```

331
ai-service/convert.py Normal file
View File

@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""
Convert raw Office/PDF files into markdown with YAML frontmatter.
Usage:
python convert.py # convert all files in raw/
python convert.py path/to/file.docx # convert a single file
python convert.py --out documents # override output subfolder
Reads from: database/seeders/data/raw/
Writes to: database/seeders/data/documents/ (default, or specify --out)
Supported formats: .docx, .pptx, .xlsx, .pdf, .txt, .md, .csv
"""
import argparse
import hashlib
import re
import sys
from datetime import date
from pathlib import Path
# ── Extractors ──────────────────────────────────────────────────────
def extract_docx(path: Path) -> tuple[str, dict]:
"""Extract text from Word .docx files, preserving heading structure."""
from docx import Document
doc = Document(str(path))
lines = []
for para in doc.paragraphs:
text = para.text.strip()
if not text:
lines.append("")
continue
style = para.style.name.lower() if para.style else ""
if "heading 1" in style:
lines.append(f"# {text}")
elif "heading 2" in style:
lines.append(f"## {text}")
elif "heading 3" in style:
lines.append(f"### {text}")
elif "title" in style:
lines.append(f"# {text}")
else:
lines.append(text)
# Extract tables
for table in doc.tables:
lines.append("")
for i, row in enumerate(table.rows):
cells = [cell.text.strip().replace("\n", " ") for cell in row.cells]
lines.append("| " + " | ".join(cells) + " |")
if i == 0:
lines.append("| " + " | ".join(["---"] * len(cells)) + " |")
lines.append("")
meta = {}
props = doc.core_properties
if props.author:
meta["auteur"] = props.author
if props.title:
meta["titel"] = props.title
if props.created:
meta["datum"] = props.created.strftime("%Y-%m-%d")
content = "\n".join(lines).strip()
# Clean up excessive blank lines
content = re.sub(r"\n{3,}", "\n\n", content)
return content, meta
def extract_pptx(path: Path) -> tuple[str, dict]:
"""Extract text from PowerPoint .pptx files, one section per slide."""
from pptx import Presentation
prs = Presentation(str(path))
lines = []
for i, slide in enumerate(prs.slides, 1):
slide_title = ""
slide_texts = []
for shape in slide.shapes:
if shape.has_text_frame:
for para in shape.text_frame.paragraphs:
text = para.text.strip()
if text:
slide_texts.append(text)
if hasattr(shape, "name") and "title" in shape.name.lower():
if shape.has_text_frame:
title_text = shape.text_frame.text.strip()
if title_text:
slide_title = title_text
# Extract tables from slides
for shape in slide.shapes:
if shape.has_table:
table = shape.table
for row_idx, row in enumerate(table.rows):
cells = [cell.text.strip().replace("\n", " ") for cell in row.cells]
slide_texts.append("| " + " | ".join(cells) + " |")
if row_idx == 0:
slide_texts.append("| " + " | ".join(["---"] * len(cells)) + " |")
heading = slide_title or f"Slide {i}"
lines.append(f"## {heading}")
lines.append("")
lines.extend(slide_texts)
lines.append("")
# Slide notes
if slide.has_notes_slide and slide.notes_slide.notes_text_frame:
notes = slide.notes_slide.notes_text_frame.text.strip()
if notes:
lines.append(f"> Notities: {notes}")
lines.append("")
meta = {}
content = "\n".join(lines).strip()
content = re.sub(r"\n{3,}", "\n\n", content)
return content, meta
def extract_xlsx(path: Path) -> tuple[str, dict]:
"""Extract data from Excel .xlsx files, one section per sheet."""
from openpyxl import load_workbook
wb = load_workbook(str(path), data_only=True)
lines = []
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
rows = list(ws.iter_rows(values_only=True))
if not rows:
continue
lines.append(f"## {sheet_name}")
lines.append("")
for i, row in enumerate(rows):
cells = [str(cell).strip() if cell is not None else "" for cell in row]
# Skip completely empty rows
if not any(cells):
continue
lines.append("| " + " | ".join(cells) + " |")
if i == 0:
lines.append("| " + " | ".join(["---"] * len(cells)) + " |")
lines.append("")
meta = {}
content = "\n".join(lines).strip()
content = re.sub(r"\n{3,}", "\n\n", content)
return content, meta
def extract_pdf(path: Path) -> tuple[str, dict]:
"""Extract text from PDF files using PyMuPDF."""
import fitz # pymupdf
doc = fitz.open(str(path))
lines = []
for page_num, page in enumerate(doc, 1):
text = page.get_text("text").strip()
if text:
if len(doc) > 1:
lines.append(f"## Pagina {page_num}")
lines.append("")
lines.append(text)
lines.append("")
meta = {}
pdf_meta = doc.metadata
if pdf_meta:
if pdf_meta.get("author"):
meta["auteur"] = pdf_meta["author"]
if pdf_meta.get("title"):
meta["titel"] = pdf_meta["title"]
if pdf_meta.get("creationDate"):
try:
raw = pdf_meta["creationDate"]
# PDF dates: D:YYYYMMDDHHmmSS
if raw.startswith("D:"):
raw = raw[2:]
meta["datum"] = f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}"
except (IndexError, ValueError):
pass
doc.close()
content = "\n".join(lines).strip()
content = re.sub(r"\n{3,}", "\n\n", content)
return content, meta
def extract_text(path: Path) -> tuple[str, dict]:
"""Read plain text / markdown / csv files."""
import chardet
raw_bytes = path.read_bytes()
detected = chardet.detect(raw_bytes)
encoding = detected.get("encoding", "utf-8") or "utf-8"
try:
content = raw_bytes.decode(encoding)
except (UnicodeDecodeError, LookupError):
content = raw_bytes.decode("utf-8", errors="replace")
return content.strip(), {}
# ── File type routing ───────────────────────────────────────────────
EXTRACTORS = {
".docx": extract_docx,
".doc": None, # needs LibreOffice, warn user
".pptx": extract_pptx,
".ppt": None,
".xlsx": extract_xlsx,
".xls": None,
".pdf": extract_pdf,
".txt": extract_text,
".md": extract_text,
".csv": extract_text,
}
def slugify(text: str) -> str:
"""Convert text to a filename-safe slug."""
text = text.lower().strip()
text = re.sub(r"[^\w\s-]", "", text)
text = re.sub(r"[\s_]+", "-", text)
text = re.sub(r"-+", "-", text)
return text[:80].strip("-")
def build_frontmatter(filename: str, meta: dict) -> str:
"""Build YAML frontmatter from extracted metadata + filename."""
titel = meta.get("titel") or filename.rsplit(".", 1)[0].replace("-", " ").replace("_", " ").title()
auteur = meta.get("auteur", "")
datum = meta.get("datum", date.today().isoformat())
lines = ["---"]
lines.append(f"titel: \"{titel}\"")
if auteur:
lines.append(f"auteur: \"{auteur}\"")
lines.append(f"type: document")
lines.append(f"datum: {datum}")
lines.append(f"bron: \"{filename}\"")
lines.append("---")
return "\n".join(lines)
def convert_file(file_path: Path, out_dir: Path) -> Path | None:
"""Convert a single file to markdown with frontmatter."""
suffix = file_path.suffix.lower()
if suffix not in EXTRACTORS:
print(f" SKIP {file_path.name} — unsupported format ({suffix})")
return None
if EXTRACTORS[suffix] is None:
print(f" SKIP {file_path.name} — old Office format ({suffix}), save as {suffix}x first")
return None
try:
content, meta = EXTRACTORS[suffix](file_path)
except Exception as e:
print(f" ERROR {file_path.name}{e}")
return None
if not content.strip():
print(f" EMPTY {file_path.name} — no text extracted")
return None
frontmatter = build_frontmatter(file_path.name, meta)
slug = slugify(meta.get("titel", file_path.stem))
out_path = out_dir / f"{slug}.md"
# Avoid overwriting — append hash if collision
if out_path.exists():
short_hash = hashlib.md5(file_path.name.encode()).hexdigest()[:6]
out_path = out_dir / f"{slug}-{short_hash}.md"
out_path.write_text(f"{frontmatter}\n\n{content}\n", encoding="utf-8")
print(f" OK {file_path.name}{out_path.relative_to(out_dir.parent)}")
return out_path
# ── CLI ─────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Convert Office/PDF files to markdown for seeding")
parser.add_argument("files", nargs="*", help="Specific files to convert (default: all in raw/)")
parser.add_argument("--out", default="documents", help="Output subfolder name (default: documents)")
parser.add_argument("--data-dir", default=None, help="Override data directory path")
args = parser.parse_args()
# Resolve paths
script_dir = Path(__file__).resolve().parent
project_root = script_dir.parent
data_dir = Path(args.data_dir) if args.data_dir else project_root / "database" / "seeders" / "data"
raw_dir = data_dir / "raw"
out_dir = data_dir / args.out
out_dir.mkdir(parents=True, exist_ok=True)
if args.files:
files = [Path(f) for f in args.files]
else:
if not raw_dir.exists():
print(f"No raw/ directory at {raw_dir}")
sys.exit(1)
files = sorted(f for f in raw_dir.iterdir() if f.is_file() and not f.name.startswith("."))
if not files:
print(f"No files found in {raw_dir}")
print("Drop .docx, .pptx, .xlsx, .pdf files there and re-run.")
sys.exit(0)
print(f"Converting {len(files)} file(s) → {out_dir.relative_to(project_root)}/\n")
converted = 0
for f in files:
result = convert_file(f, out_dir)
if result:
converted += 1
print(f"\nDone: {converted}/{len(files)} files converted.")
if __name__ == "__main__":
main()

View File

@@ -9,3 +9,10 @@ numpy>=1.26.0
pydantic>=2.0.0
python-dotenv>=1.0.0
httpx>=0.27.0
# Document extraction
python-docx>=1.1.0
python-pptx>=0.6.23
openpyxl>=3.1.0
pymupdf>=1.24.0
chardet>=5.2.0

View File

@@ -0,0 +1,126 @@
# Seeder Data
Drop example documents here. The seeder reads these files and loads them into PostgreSQL, which then triggers embedding generation via the AI service.
## Option 1: Drop raw Office/PDF files
Put `.docx`, `.pptx`, `.xlsx`, `.pdf` files in the `raw/` folder, then run the converter:
```bash
cd ai-service
pip install python-docx python-pptx openpyxl pymupdf chardet
python convert.py # convert all files in raw/
python convert.py --out kennis_artikelen # output to a specific subfolder
python convert.py path/to/file.docx # convert a single file
```
This extracts text, preserves structure (headings, tables, slides), and writes markdown files with frontmatter into the `documents/` folder (or whichever `--out` you specify).
## Option 2: Write markdown directly
Each file is **Markdown with YAML frontmatter**. The seeder parses the frontmatter for metadata and the body as content.
## Folder Structure
```
data/
raw/ ← DROP OFFICE/PDF FILES HERE (auto-converted)
themas/ ← Strategic themes
projects/ ← Project descriptions (linked to a thema)
documents/ ← Meeting notes, specs, analyses (linked to a project)
kennis_artikelen/ ← Knowledge articles (standalone)
besluiten/ ← Decisions with rationale (linked to a project)
lessons_learned/ ← Lessons learned (linked to a project + fase)
```
## File Templates
### themas/waterkwaliteit.md
```markdown
---
naam: Waterkwaliteit & Monitoring
beschrijving: Innovaties rondom waterkwaliteitsbewaking en meetnetwerken
prioriteit: hoog
---
```
### projects/sensor-netwerk.md
```markdown
---
naam: Sensor Netwerk Pilot
thema: Waterkwaliteit & Monitoring
eigenaar: Jan de Vries
status: experiment
prioriteit: hoog
startdatum: 2025-09-01
streef_einddatum: 2026-06-30
---
Beschrijving van het project hier. Kan meerdere alinea's zijn.
Dit wordt het `beschrijving` veld.
```
### documents/notulen-stuurgroep-2026-03.md
```markdown
---
titel: Notulen Stuurgroep 15 maart 2026
project: Sensor Netwerk Pilot
type: vergaderverslag
auteur: Pieter Jansen
versie: 1
---
De volledige documentinhoud hier. Kan lang zijn — wordt automatisch
gechunkt als het meer dan 1500 tekens is.
```
### kennis_artikelen/iot-waterbeheer.md
```markdown
---
titel: IoT-sensoren in het waterbeheer
auteur: Lisa de Groot
tags: [IoT, sensoren, waterbeheer, telemetrie]
---
Artikelinhoud hier. Alles na de frontmatter wordt `inhoud`.
```
### besluiten/go-pilot-sensor.md
```markdown
---
titel: Go voor pilotfase Sensor Netwerk
project: Sensor Netwerk Pilot
type: go_no_go
status: goedgekeurd
datum: 2026-02-20
---
Beschrijving van het besluit.
## Onderbouwing
De onderbouwing / rationale hier.
```
### lessons_learned/sensor-kalibratie.md
```markdown
---
titel: Kalibratiefrequentie pH-sensoren onderschat
project: Sensor Netwerk Pilot
fase: experiment
tags: [sensoren, kalibratie, pH]
---
Wat we geleerd hebben. Body wordt `inhoud`.
```
## Naming Convention
Use lowercase slugs: `korte-beschrijving.md`. The filename is not stored — all metadata comes from frontmatter.
## Tips
- Write in Dutch (the embedding model and full-text search are configured for Dutch)
- Real content is better than lorem ipsum — the search quality depends on it
- Longer documents are fine — they get chunked automatically
- Link projects to themas and documents to projects via the `thema:` and `project:` fields (matched by naam)

View File

View File

@@ -1,19 +0,0 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}

89
wiki/SCHEMA.md Normal file
View File

@@ -0,0 +1,89 @@
# Project Wiki Schema
## Purpose
LLM-maintained knowledge base for this project. The LLM writes and maintains everything. You read it (ideally in Obsidian). Knowledge compounds across sessions instead of being lost in chat history.
## Directory Structure
```
wiki/
SCHEMA.md — this file (how to maintain the wiki)
index.md — catalog of all pages with one-line summaries
log.md — chronological record of updates
overview.md — project overview and current status
metrics.md — all numbers with provenance
knowledge-graph.yaml — structured data, machine-queryable
tools/ — search, lint, query scripts
concepts/ — core ideas and mechanisms
architecture/ — design decisions, system internals
findings/ — honest results (what worked AND what didn't)
sessions/ — per-session summaries
```
## Page Conventions
### Frontmatter
Every page starts with YAML frontmatter:
```yaml
---
title: Page Title
created: YYYY-MM-DD
updated: YYYY-MM-DD
status: proven | disproven | evolving | speculative
tags: [tag1, tag2]
sources: [path/to/file.py, commit abc1234]
---
```
### Status values
- **proven**: tested and verified with evidence
- **disproven**: tested and honestly shown NOT to work (document WHY)
- **evolving**: partially working, boundary not fully mapped
- **speculative**: proposed but not yet tested
### Cross-references
Use `[[Page Name]]` Obsidian-style wikilinks.
### Contradictions
When new evidence contradicts a prior claim, DON'T delete the old claim. Add:
```
> [!warning] Superseded
> This was shown to be incorrect on YYYY-MM-DD. See [[New Finding]].
```
### Honesty rule
If something doesn't work, say so. If a result was a false positive, document how it was discovered. The wiki must be trustworthy.
## Operations
### Ingest (after a session or new source)
1. Read outputs, commits, findings
2. Update relevant pages
3. Create new pages for new concepts
4. Update `index.md`, `log.md`, `knowledge-graph.yaml`
5. Check for contradictions with existing pages
### Query
1. Use `python3 wiki/tools/query.py` for structured lookup
2. Use `wiki/tools/search.sh` for full-text
3. Read `index.md` to find relevant pages
4. File valuable answers back into the wiki
### Lint (periodically)
```bash
bash wiki/tools/lint.sh
```
Checks: orphan pages, broken wikilinks, missing frontmatter, index completeness.
## Data Layer
- `knowledge-graph.yaml` — structured YAML with every metric and data point
- `metrics.md` — human-readable dashboard
- When adding new results, update BOTH the wiki page AND the knowledge graph
- The knowledge graph is the single source of truth for numbers
## Source of Truth Hierarchy
1. **Test results** (actual outputs) — highest authority
2. **Code** (current state) — second authority
3. **Knowledge graph** (knowledge-graph.yaml) — structured metrics
4. **Wiki pages** — synthesis, may lag
5. **Chat/memory** — ephemeral, may be stale

View File

@@ -0,0 +1,112 @@
---
title: Domain Model
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [domain, models, relationships, lifecycle]
sources: [app/Models/, app/Enums/, database/migrations/]
---
# Domain Model
21 Eloquent models organized in 8 layers, with 14 enums for type safety.
## Entity Relationship Overview
```
Thema (strategic theme)
└── Speerpunt (focus area)
└── Project (innovation project)
├── Fase (lifecycle phase)
├── Risico (risk)
├── Commitment → Actie (action)
├── Besluit (decision) → Commitment
├── Budget → Besteding (expenditure)
├── Document ← Tag (M:N)
├── LessonLearned
├── Afhankelijkheid (dependency, Project ↔ Project)
├── Overdrachtsplan (handover plan)
│ ├── Criterium
│ └── Acceptatie
└── ProjectUser (team membership, pivot)
User ──── Role (M:N)
AuditLog (append-only, all mutations)
KennisArtikel ← Tag (M:N)
```
## Layer Details
### Strategic Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **Thema** | naam, beschrijving, prioriteit, periode | has many Speerpunten |
| **Speerpunt** | naam, beschrijving, eigenaar, status | belongs to Thema, has many Projects |
| **RoadmapItem** | titel, start, eind, type, status | belongs to Thema |
### Project Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **Project** | naam, beschrijving, eigenaar_id, status, prioriteit, startdatum, streef_einddatum | SoftDeletes, belongs to Speerpunt + User (eigenaar), has many of everything |
| **Fase** | type (enum), status (enum), startdatum, einddatum, opmerkingen | belongs to Project |
| **Risico** | beschrijving, impact, kans, mitigatie, eigenaar | belongs to Project |
| **Afhankelijkheid** | type, beschrijving, status | self-referential N:M between Projects |
### Commitment Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **Commitment** | beschrijving, eigenaar_id, deadline, status, bron | belongs to Project + Besluit, has many Acties |
| **Actie** | beschrijving, eigenaar_id, deadline, status, prioriteit | belongs to Commitment |
### Governance Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **Besluit** | titel, beschrijving, datum, type, status, onderbouwing | belongs to Project, has many Commitments |
| **Budget** | bedrag, type, periode, status | belongs to Project, has many Bestedingen |
| **Besteding** | bedrag, beschrijving, datum, categorie | belongs to Budget |
### Knowledge Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **Document** | titel, type, inhoud, versie, auteur, datum, **embedding** (vector) | belongs to Project + Fase, M:N Tags |
| **KennisArtikel** | titel, inhoud, tags, auteur, datum, **embedding** (vector) | M:N Tags |
| **LessonLearned** | titel, inhoud, project, fase, tags | belongs to Project + Fase |
| **Tag** | naam, categorie | M:N with Documents, KennisArtikels |
### Handover Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **Overdrachtsplan** | type, status, eigenaar_rnd, eigenaar_ontvanger | belongs to Project, has many Criteria + Acceptaties |
| **Criterium** | beschrijving, status, verificatie | belongs to Overdrachtsplan |
| **Acceptatie** | datum, door, opmerkingen, status | belongs to Overdrachtsplan |
### Auth Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **User** | name, email, password, phone, afdeling, functie, 2FA fields | has many ProjectUsers, can own Projects/Commitments/Risicos |
| **Role** | naam, beschrijving, permissies | M:N with Users |
| **ProjectUser** | project_id, user_id, rol (enum) | pivot table |
### System Layer
| Model | Key Fields | Relationships |
|---|---|---|
| **AuditLog** | user_id, action, entity_type, entity_id, payload (JSON) | append-only |
## Innovation Lifecycle Phases (FaseType enum)
```
signaal → verkenning → concept → experiment → pilot → besluitvorming → overdracht_bouwen → overdracht_beheer → evaluatie
```
Special statuses (ProjectStatus only, not FaseType):
- `geparkeerd` — temporarily halted
- `gestopt` — permanently stopped
- `afgerond` — completed
## Key Design Decisions
1. **Dutch naming** — all models, fields, enums use Dutch names to match domain language
2. **Soft deletes on Project only** — projects are never hard-deleted
3. **Embedding vectors on Document + KennisArtikel** — pgvector columns for semantic search
4. **ProjectUser pivot with role** — team membership is role-typed (Eigenaar, Lid, Reviewer, Stakeholder)
5. **FaseType maps 1:1 to first 9 ProjectStatus values**`FaseType::tryFrom($projectStatus->value)` is used for phase transitions

View File

@@ -0,0 +1,121 @@
---
title: Metro Map UI Architecture
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [architecture, ui, d3, metro-map, retro]
sources: [resources/js/Components/MetroMap/MetroCanvas.vue, STYLE_GUIDE.md, resources/js/Components/Cli/CliBar.vue]
---
# Metro Map UI Architecture
The entire platform navigates via a **zoomable metro/transit map**. No sidebar, no traditional dashboard. Users explore the innovation landscape visually.
## Map Levels
| Level | What You See | Lines = | Stations = | Zoom Target |
|---|---|---|---|---|
| 1. Strategy | Full innovation landscape | Strategic themes (Thema) | Projects | Click station → Level 2 |
| 2. Project | Single project lifecycle | Lifecycle + Commitments + Documents | Phases, items | Click station → Level 3 |
| 3. Detail | Individual item | (not yet implemented) | — | Click to open |
## Technical Implementation
### MetroCanvas.vue (356 LOC)
- **Rendering**: D3.js 7.9 on SVG
- **Zoom**: `d3.zoom()` with `scaleExtent([0.3, 5])`, pan + zoom
- **Lines**: `d3.curveMonotoneX` paths connecting stations
- **Stations**: Two concentric circles (outer ring = line color, inner dot = status color)
- **Labels**: VT323 monospace font, positioned below stations
- **Interactions**: hover → glow effect + tooltip, click → emit node-click
- **Dependencies**: Dashed lines between connected stations across lines
### Data Contract (MapDataService → MetroCanvas)
```json
{
"lines": [
{ "id": "thema-1", "name": "Waterkwaliteit", "color": "#00d2ff" }
],
"nodes": [
{
"id": "project-1",
"entityId": 1,
"entityType": "project",
"name": "Sensor Netwerk",
"lineId": "thema-1",
"x": -200, "y": 0,
"order": 1,
"status": "verkenning",
"description": "...",
"owner": "Jan",
"badge": "Verkenning",
"children": 5
}
],
"connections": [
{ "from": "project-1", "to": "project-3" }
],
"level": 1
}
```
### Station Status Colors
| Status | Color | Hex |
|---|---|---|
| afgerond / completed | Neon green | `#00ff88` |
| actief / active | Vivid cyan | `#00d2ff` |
| geparkeerd | Warning yellow | `#ffd93d` |
| gestopt | Signal red | `#e94560` |
| default (pending) | Dark fill | `#16213e` |
### Visual Effects
- **Glow filter**: SVG `feGaussianBlur` with `stdDeviation=4`, applied on hover
- **Scanline pattern**: 4px repeating lines at 8% opacity (subtle CRT effect)
- **Hover transition**: 200ms radius 8→12 + glow filter
- **Zoom animation**: 500ms d3 zoom transition for `zoomTo()` method
## C64 CLI Bar (CliBar.vue)
Fixed at bottom of screen. Commodore 64 aesthetic:
- Monospace font (Press Start 2P / VT323)
- Blinking block cursor
- Dark background, cyan/green text
- Natural language input
- Responses slide up above bar with `[AI]` prefix
## Design System
### Color Palette
| Role | Hex | Usage |
|---|---|---|
| Background | `#1a1a2e` | Canvas, page background |
| Surface | `#16213e` | Cards, tooltips, panels |
| Primary | `#0f3460` | Metro lines, primary actions |
| Accent (cyan) | `#00d2ff` | Active states, highlights, CLI cursor |
| Accent (red) | `#e94560` | Warnings, deadlines |
| Accent (green) | `#00ff88` | Success, completed |
| Accent (purple) | `#7b68ee` | Knowledge, documentation |
| Text primary | `#e8e8e8` | Main text |
| Text secondary | `#8892b0` | Labels, secondary text |
### Fonts
| Use | Font | Fallback |
|---|---|---|
| Map labels | VT323 | monospace |
| CLI bar | Press Start 2P | VT323, monospace |
| Body text | IBM Plex Sans | Inter, sans-serif |
| Code | IBM Plex Mono | monospace |
## Planned Enhancements
- **Zoom-to-dimension**: scroll-zoom near a station gradually cross-fades into child dimension (no click needed)
- **Recursive dimensions**: every node can contain children forming a sub-metro-map, infinitely nestable
- **Right-click context menu**: canvas = "New node here", station = "Edit/Delete/Add child"
- **[+] FAB**: creates at current depth
- **Mobile**: list fallback with metro line colors preserved

View File

@@ -0,0 +1,109 @@
---
title: System Architecture
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [architecture, stack, docker, layers]
sources: [docker-compose.yml, composer.json, package.json, ai-service/app/main.py]
---
# System Architecture
## Stack
| Layer | Technology | Version |
|---|---|---|
| Backend | Laravel (PHP) | 13.0 / PHP 8.3+ |
| Frontend | Vue 3 + Inertia.js | Vue 3.5, Inertia 3 |
| Build | Vite | 8.0 |
| Styling | Tailwind CSS | 4.2 |
| Visualization | D3.js | 7.9 |
| Database | PostgreSQL + pgvector | 16 |
| Cache / Queue | Redis | alpine |
| AI Service | Python FastAPI | 0.1.0 |
| Auth | Laravel Fortify + Sanctum | Fortify 1.36, Sanctum 4.0 |
| Fonts | VT323, Press Start 2P, IBM Plex Mono | — |
## Architecture Principles
1. **Service-oriented** — domain logic lives in service classes (`app/Services/`), not controllers
2. **Event-driven** — status transitions go through transactional methods with audit logging
3. **API-first** — all functionality reachable via REST endpoints
4. **Audit trail** — all mutations logged to `audit_logs` table (append-only)
5. **AI content labeled** — AI-generated content marked and requires human confirmation
6. **Inertia SPA** — server-side routing (Laravel) with client-side rendering (Vue 3), no separate API layer needed for pages
## Docker Topology
```
┌─────────────────────────────────────────────────────────┐
│ nginx:alpine ─────────────────────────────→ :80 │
│ │ │
│ ▼ │
│ laravel-app (PHP 8.4-FPM) │
│ │ │
│ ├── laravel-worker (queue:work) │
│ ├── laravel-scheduler (cron) │
│ │ │
│ ┌───▼────────────┐ ┌─────────────────┐ │
│ │ postgresql:16 │ │ redis:alpine │ │
│ │ + pgvector │ │ cache/queue │ │
│ │ :5432 │ │ :6379 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ai-service (Python FastAPI) ──────────────→ :8000 │
│ └── connects to postgresql for embeddings │
└─────────────────────────────────────────────────────────┘
```
7 services total. All on `innovatieplatform` bridge network.
## Data Flow
### Page Rendering (Inertia)
```
Browser → nginx → PHP-FPM → Laravel Router → Controller
→ Service (business logic)
→ Inertia::render('Page', $data)
→ Vue component receives props
→ D3.js renders metro map canvas
```
### API Calls (Map data)
```
Vue component → axios GET /api/map/strategy
→ MapController::apiStrategy()
→ MapDataService::getStrategyMap()
→ Eloquent queries (Thema → Speerpunten → Projects)
→ JSON response {lines, nodes, connections, level}
→ D3 re-renders canvas
```
### AI Integration (planned)
```
CliBar.vue → POST /api/chat
→ Laravel proxy → ai-service:8000/api/chat
→ LangGraph agent → Anthropic Claude
→ RAG: pgvector similarity search on documents
→ Response with source attribution
→ CliBar displays with [AI] prefix
```
## Key Service Classes
| Service | LOC | Responsibility |
|---|---|---|
| `ProjectService` | 186 | Project CRUD, lifecycle transitions, park/stop, audit logging |
| `MapDataService` | 165 | Build metro map data structures (Level 1: strategy, Level 2: project) |
| `ThemaService` | 60 | Theme CRUD operations |
## Configuration
- **Session/Cache/Queue**: All Redis-backed (`config/session.php`, `config/cache.php`, `config/queue.php`)
- **Database**: PostgreSQL with pgvector extension for embedding vectors
- **Auth**: Fortify handles registration/login/password flows, Sanctum for API tokens
- **OPcache**: Production-optimized (`docker/php/opcache.ini`)
- **Gzip**: Enabled in nginx config

View File

@@ -0,0 +1,80 @@
---
title: AI Integration
created: 2026-04-08
updated: 2026-04-08
status: speculative
tags: [concept, ai, rag, langgraph, embeddings]
sources: [ai-service/app/main.py, ai-service/requirements.txt, docker-compose.yml]
---
# AI Integration
## Current State
The AI service is a **Python FastAPI stub** with placeholder endpoints. No actual AI processing is wired up yet.
### Implemented (stub only)
| Endpoint | Method | Status |
|---|---|---|
| `GET /health` | Health check | Working |
| `POST /api/chat` | Chat with context | Stub — returns placeholder text |
| `POST /api/summarize` | Generate summaries | Stub — returns placeholder text |
| `POST /api/search` | Semantic search | Stub — returns empty results |
### Request/Response Models (Pydantic)
```
ChatRequest: message, project_id?, conversation_history[]
ChatResponse: reply, project_id?
SummarizeRequest: content, project_id?, summary_type?
SummarizeResponse: summary, project_id?
SearchRequest: query, project_id?, limit?
SearchResponse: results[{id, content, score, metadata}], query
```
## Planned Architecture
```
Laravel App ↔ HTTP ↔ Python AI-Service (FastAPI)
├── LangGraph Orchestrator
│ ├── Router / Classifier
│ └── Agent graph (state machine)
├── Anthropic Claude (LLM)
├── pgvector (embeddings / similarity search)
└── Tools:
├── DB query (project data, commitments, phases)
├── Document retrieval (semantic search)
└── Embedding generation
```
## RAG Pipeline (planned)
### Sources
- Project descriptions and phase notes
- Documents (uploaded files, meeting notes)
- Lessons learned
- Decisions and their rationale
- Knowledge articles
### Embedding Strategy
- **Storage**: pgvector extension on PostgreSQL 16
- **Models**: Document and KennisArtikel already have `embedding` vector columns
- **Update triggers**: On document create/update, on project phase change
- **Chunking**: Per document type and size
### Agent Skills (from CLAUDE.md)
| Agent | Autonomy | Purpose |
|---|---|---|
| Project Assistant | Low | Answer questions about specific projects |
| Knowledge Assistant | Low | Search and surface knowledge articles |
| Document Assistant | Medium | Summarize, compare, extract from documents |
| System Tasks | High | Background indexing, embedding updates |
## Content Governance Rules
1. AI-generated content always labeled ("AI-suggestie", "Concept")
2. Human confirmation required before AI content gains system status
3. All AI interactions logged (request, response, tools used, sources cited)
4. Source attribution mandatory in AI responses
5. Confidence indicators when certainty is low

View File

@@ -0,0 +1,76 @@
---
title: Innovation Lifecycle
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [concept, lifecycle, phases, governance]
sources: [app/Enums/FaseType.php, app/Enums/ProjectStatus.php, app/Services/ProjectService.php]
---
# Innovation Lifecycle
Every innovation project at the R&D lab follows a 9-phase lifecycle, from initial signal to final evaluation. The platform enforces this via `FaseType` and `ProjectStatus` enums with transactional phase transitions.
## Phases
```
signaal → verkenning → concept → experiment → pilot → besluitvorming → overdracht_bouwen → overdracht_beheer → evaluatie
```
| # | Phase | Dutch | Purpose |
|---|---|---|---|
| 1 | Signal | Signaal | Initial idea or observation captured |
| 2 | Exploration | Verkenning | Research and feasibility assessment |
| 3 | Concept | Concept | Design a proposed solution |
| 4 | Experiment | Experiment | Small-scale test of the concept |
| 5 | Pilot | Pilot | Larger-scale test in production environment |
| 6 | Decision | Besluitvorming | Go/no-go decision by governance |
| 7 | Handover to Build | Overdracht bouwen | Transfer to development/implementation team |
| 8 | Handover to Operations | Overdracht beheer | Transfer to operational team for maintenance |
| 9 | Evaluation | Evaluatie | Post-handover review and lessons learned |
## Special Statuses
These are Project-only statuses (not lifecycle phases):
| Status | Purpose |
|---|---|
| Geparkeerd | Temporarily halted — can resume later |
| Gestopt | Permanently stopped — documented why |
| Afgerond | Successfully completed the full lifecycle |
## Phase Transition Logic
Phase transitions go through `ProjectService::transitionPhase()` in a database transaction:
1. Close current active phase (set status → `afgerond`, set `einddatum`)
2. Create new phase record (via `FaseType::tryFrom()` mapping)
3. Update project status
4. Write audit log entry with `from` and `to` states
Key constraint: `FaseType::tryFrom($newStatus->value)` — the first 9 ProjectStatus values map 1:1 to FaseType. Special statuses (geparkeerd, gestopt, afgerond) do not create new Fase records.
## Park and Stop
- **Park** (`ProjectService::park()`): closes active phase with reason in `opmerkingen`, sets status to `geparkeerd`
- **Stop** (`ProjectService::stop()`): same mechanics, sets status to `gestopt`
- Both log the transition reason in the audit trail
## Audit Trail
Every phase transition, park, or stop creates an `AuditLog` record:
```php
AuditLog::create([
'user_id' => Auth::id(),
'action' => "project.phase_transition",
'entity_type' => 'project',
'entity_id' => $project->id,
'payload' => ['from' => 'signaal', 'to' => 'verkenning'],
]);
```
## Metro Map Representation
- **Level 1**: Project status shown as station dot color (green=completed, cyan=active, yellow=parked, red=stopped)
- **Level 2**: Each lifecycle phase is a station on the project's "lifecycle" line, with active phase highlighted

34
wiki/index.md Normal file
View File

@@ -0,0 +1,34 @@
---
title: Wiki Index
created: 2026-04-08
updated: 2026-04-08
---
# Innovatieplatform Wiki Index
## Overview
- [Project Overview](overview.md) — current status, what works, what doesn't
- [Metrics Dashboard](metrics.md) — all numbers with provenance
- [Knowledge Graph](knowledge-graph.yaml) — structured data, machine-queryable
## Architecture
- [System Architecture](architecture/system-architecture.md) — layers, services, data flow
- [Domain Model](architecture/domain-model.md) — entities, relationships, lifecycle states
- [Metro Map UI](architecture/metro-map-ui.md) — D3 canvas, zoom dimensions, interaction model
## Core Concepts
- [Innovation Lifecycle](concepts/innovation-lifecycle.md) — phases from signaal to evaluatie
- [AI Integration](concepts/ai-integration.md) — RAG pipeline, LangGraph, agent skills
## Findings
<!-- Add findings pages — both proven AND disproven -->
## Sessions
- [2026-04-08 Wiki Setup](sessions/2026-04-08-wiki-setup.md) — initial wiki from template, codebase scan
## Not Yet Documented
- RBAC model and permission matrix
- Handover process (overdracht) details
- Budget/finance governance
- Student projects (Phase 2)
- Deployment pipeline / CI/CD

141
wiki/knowledge-graph.yaml Normal file
View File

@@ -0,0 +1,141 @@
# Innovatieplatform — Structured Knowledge Graph
# Single source of truth for numbers, metrics, and verifiable claims.
# Last updated: 2026-04-08
# ── TESTS ──
tests:
phpunit_feature:
count: 0
passing: 0
file: tests/Feature/
date: 2026-04-08
phpunit_unit:
count: 0
passing: 0
file: tests/Unit/
date: 2026-04-08
# ── METRICS ──
metrics:
backend_loc:
value: "~13,500"
source: "wc -l on PHP files excl. vendor"
date: 2026-04-08
frontend_loc:
value: "~3,900"
source: "wc -l on Vue/JS files excl. node_modules"
date: 2026-04-08
eloquent_models:
value: 21
source: "ls app/Models/"
date: 2026-04-08
enums:
value: 14
source: "ls app/Enums/"
date: 2026-04-08
migrations:
value: 27
source: "ls database/migrations/"
date: 2026-04-08
vue_components:
value: 15
source: "find resources/js -name '*.vue'"
date: 2026-04-08
docker_services:
value: 7
source: docker-compose.yml
date: 2026-04-08
git_commits:
value: 7
source: "git log --oneline"
date: 2026-04-08
# ── DISPROVEN CLAIMS ──
disproven: {}
# ── PERFORMANCE ──
performance: {}
# ── ARCHITECTURE ──
architecture:
backend: "Laravel 13.0 (PHP 8.3+)"
frontend: "Vue 3.5 + Inertia.js 3 + Vite 8"
database: "PostgreSQL 16 + pgvector"
ai_service: "Python FastAPI + LangGraph"
cache_queue: "Redis (cache, session, queue)"
styling: "Tailwind CSS 4.2"
visualization: "D3.js 7.9"
auth: "Laravel Fortify + Sanctum"
infrastructure: "Docker Compose (7 services)"
# ── DOMAIN MODEL ──
domain_model:
strategic_layer:
- Thema
- Speerpunt
- RoadmapItem
project_layer:
- Project
- Fase
- Risico
- Afhankelijkheid
commitment_layer:
- Commitment
- Actie
governance_layer:
- Besluit
- Budget
- Besteding
knowledge_layer:
- Document
- KennisArtikel
- LessonLearned
- Tag
handover_layer:
- Overdrachtsplan
- Criterium
- Acceptatie
auth_layer:
- User
- Role
- ProjectUser
system_layer:
- AuditLog
# ── LIFECYCLE PHASES ──
lifecycle_phases:
- signaal
- verkenning
- concept
- experiment
- pilot
- besluitvorming
- overdracht_bouwen
- overdracht_beheer
- evaluatie
# ── SPRINT STATUS ──
sprint_status:
sprint_0_scaffold: completed
sprint_1_foundation: completed
sprint_2_detail: not_started
# ── DATA SOURCES ──
data_sources: {}
# ── TIMELINE ──
timeline:
- date: 2026-04-01
desc: "Initial scaffold: Laravel 13, Vue 3, Docker, domain model"
- date: 2026-04-01
desc: "Style guide: metro map + retro-futurism aesthetic"
- date: 2026-04-02
desc: "Sprint 1: Auth, metro map canvas, services, retro UI"
- date: 2026-04-02
desc: "Fix Dutch model table names and enum values"
- date: 2026-04-02
desc: "Performance: OPcache, gzip, font subsetting, lazy loading"
- date: 2026-04-02
desc: "Logout button, metro label positioning fixes"
- date: 2026-04-08
desc: "Wiki initialized from wiki-template"

11
wiki/log.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: Wiki Log
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [log, history]
---
# Wiki Log
## [2026-04-08] init | Wiki initialized from wiki-template, populated with codebase scan data

55
wiki/metrics.md Normal file
View File

@@ -0,0 +1,55 @@
---
title: Metrics Dashboard
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [metrics, tests, performance]
sources: [phpunit.xml, composer.json, package.json]
---
# Metrics Dashboard
All numbers with provenance. Source of truth: `knowledge-graph.yaml`.
## Test Results
| Suite | Score | File | Date |
|---|---|---|---|
| PHPUnit Feature | 0/0 | tests/Feature/ | 2026-04-08 |
| PHPUnit Unit | 0/0 | tests/Unit/ | 2026-04-08 |
> No tests written yet. PHPUnit 12.5 is configured and ready.
## Codebase Size
| Metric | Value | Source |
|---|---|---|
| Backend LOC | ~13,500 | PHP files excl. vendor |
| Frontend LOC | ~3,900 | Vue/JS files excl. node_modules |
| Models | 21 | app/Models/ |
| Enums | 14 | app/Enums/ |
| Migrations | 27 | database/migrations/ |
| Vue components | 15 | resources/js/ |
| Docker services | 7 | docker-compose.yml |
## Build Dependencies
| Layer | Package Manager | Production | Dev |
|---|---|---|---|
| Backend | Composer | 5 (Laravel, Fortify, Sanctum, Inertia, Tinker) | 6 (Faker, Pail, Pint, Mockery, Collision, PHPUnit) |
| Frontend | npm | 8 (Vue, Inertia, D3, Pinia, VueUse, fonts) | 6 (Tailwind, Vite, Axios, Concurrently, Laravel-Vite) |
| AI Service | pip | FastAPI, LangGraph, LangChain, Anthropic, pgvector | — |
## Sprint Progress
| Sprint | Status | Key Deliverables |
|---|---|---|
| Sprint 0 — Scaffold | completed | Domain model, Docker, migrations, seeders, agent definitions |
| Sprint 1 — Foundation | completed | Auth (Fortify), metro map canvas (D3), services, retro UI |
| Sprint 2 — Detail & Interaction | not started | Project detail view, zoom-to-dimension, commitment UI |
## Disproven Claims
| Claim | Evidence For | Evidence Against | Date |
|---|---|---|---|
| (none yet) | — | — | — |

69
wiki/overview.md Normal file
View File

@@ -0,0 +1,69 @@
---
title: Project Overview
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [overview, status, roadmap]
sources: [CLAUDE.md, composer.json, package.json, docker-compose.yml]
---
# Innovatieplatform — R&D Lab Waterschap Brabantse Delta
Innovation governance platform supporting the full lifecycle of innovation trajectories — from signal to handover — with built-in AI support. The primary UI is a **zoomable metro map** with retro-futurism aesthetic (Commodore 64-inspired CLI bar, pixel accents, monospace typography).
## What Works
| Capability | Status | Evidence |
|---|---|---|
| Authentication (Fortify) | proven | Login, register, password reset, email verification all functional |
| Metro map canvas (D3.js) | proven | Zoomable SVG canvas with stations, lines, hover preview, breadcrumb |
| Project CRUD API | proven | REST endpoints for create, update, transition, park, stop, delete |
| Thema CRUD API | proven | REST endpoints for list, create, update |
| Domain model (21 models) | proven | All Eloquent models with relationships, 27 migrations |
| Service-oriented architecture | proven | ProjectService, MapDataService, ThemaService (411 LOC) |
| Docker Compose infrastructure | proven | 7 services: nginx, php-fpm, worker, scheduler, postgresql, redis, ai-service |
| Retro-futurism UI | proven | C64 CLI bar, VT323/Press Start 2P fonts, dark palette, glow effects |
| Demo data seeder | proven | 4 themes x 3 projects with lifecycle phases |
| Performance optimizations | proven | OPcache, gzip, font subsetting, lazy-loaded pages |
## What Doesn't Work (honestly)
| Capability | Status | Notes |
|---|---|---|
| Project detail pages | not started | Routes exist, page component missing |
| Commitment/action tracking UI | not started | Models + enums exist, no frontend |
| Document management | not started | Document model with embedding vector, no upload/display |
| AI chat integration | not started | Python FastAPI stub only, no LangGraph/RAG pipeline |
| Semantic search | not started | pgvector enabled, no embedding generation |
| RBAC UI | not started | Role/CheckRole middleware exist, no admin UI |
| Test suite | not started | PHPUnit 12.5 configured, 0 tests written |
| Roadmap visualization | not started | RoadmapItem model exists, no UI |
| Zoom-to-dimension transitions | not started | Canvas supports zoom, no cross-fade to child dimension |
| Right-click create/edit | not started | Planned interaction pattern, not implemented |
| Bilingual NL/EN | not started | All code in Dutch, no i18n framework |
## Current Scale
| Metric | Value |
|---|---|
| Backend LOC (excl. vendor) | ~13,500 |
| Frontend LOC (excl. node_modules) | ~3,900 |
| Eloquent models | 21 |
| Enums | 14 |
| Services | 3 |
| Controllers | 4 |
| Vue components/pages | 15 |
| Database migrations | 27 |
| Docker services | 7 |
| Git commits | 7 |
| Test coverage | 0% |
## Next Steps (Priority Order)
1. **Project detail view** — click station -> full project page with phases, commitments, documents
2. **Zoom-to-dimension** — scroll-zoom near station cross-fades into child metro map
3. **Commitment/action tracking** — UI for managing commitments with deadlines and owners
4. **Test suite** — PHPUnit feature tests for API + unit tests for services
5. **AI chat integration** — Wire Python FastAPI service with LangGraph orchestrator
6. **Document management** — Upload, link, embed, search documents
7. **RBAC admin** — Role assignment UI, permission management

View File

@@ -0,0 +1,41 @@
---
title: "Session: Wiki Setup"
created: 2026-04-08
updated: 2026-04-08
status: evolving
tags: [session, setup, wiki]
---
# 2026-04-08 — Wiki Setup
## Scope
Initialize wiki from wiki-template and populate with real codebase data.
## What Was Done
1. **Copied wiki-template** from `/home/znetsixe/wiki-template/` to `innovatieplatform/wiki/`
2. **Populated overview.md** with what works / what doesn't based on actual code scan
3. **Populated metrics.md** with codebase size, dependencies, sprint progress
4. **Populated knowledge-graph.yaml** with structured data: tests, metrics, architecture, domain model, lifecycle phases, timeline
5. **Created architecture pages**:
- `architecture/system-architecture.md` — stack, Docker topology, data flow, services
- `architecture/domain-model.md` — 21 models in 8 layers, relationships, enums, lifecycle
- `architecture/metro-map-ui.md` — D3 canvas, data contract, design system, planned enhancements
6. **Created concept pages**:
- `concepts/innovation-lifecycle.md` — 9 phases, transition logic, audit trail
- `concepts/ai-integration.md` — current stub state, planned RAG architecture
7. **Updated index.md** with all new pages
8. **Wired wiki into CLAUDE.md** for agent startup
## Key Numbers (from scan)
| Metric | Value |
|---|---|
| Backend LOC | ~13,500 |
| Frontend LOC | ~3,900 |
| Models | 21 |
| Enums | 14 |
| Migrations | 27 |
| Vue components | 15 |
| Tests | 0 |
| Git commits | 7 |

46
wiki/tools/lint.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
# Wiki health check — find issues
# Usage: ./wiki/tools/lint.sh
WIKI_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
echo "=== Wiki Health Check ==="
echo ""
echo "-- Page count --"
find "$WIKI_DIR" -name "*.md" -not -path "*/tools/*" | wc -l
echo " total pages"
echo ""
echo "-- Orphans (not linked from other pages) --"
for f in $(find "$WIKI_DIR" -name "*.md" -not -name "index.md" -not -name "log.md" -not -name "SCHEMA.md" -not -path "*/tools/*"); do
basename=$(basename "$f" .md)
refs=$(grep -rl --include="*.md" "$basename" "$WIKI_DIR" 2>/dev/null | grep -v "$f" | wc -l)
if [ "$refs" -eq 0 ]; then
echo " ORPHAN: $f"
fi
done
echo ""
echo "-- Status distribution --"
for status in proven disproven evolving speculative; do
count=$(grep -rl "status: $status" "$WIKI_DIR" --include="*.md" 2>/dev/null | wc -l)
echo " $status: $count"
done
echo ""
echo "-- Pages missing frontmatter --"
for f in $(find "$WIKI_DIR" -name "*.md" -not -name "SCHEMA.md" -not -path "*/tools/*"); do
if ! head -1 "$f" | grep -q "^---"; then
echo " NO FRONTMATTER: $f"
fi
done
echo ""
echo "-- Index completeness --"
indexed=$(grep -c '\[.*\](.*\.md)' "$WIKI_DIR/index.md" 2>/dev/null)
total=$(find "$WIKI_DIR" -name "*.md" -not -name "index.md" -not -name "log.md" -not -name "SCHEMA.md" -not -path "*/tools/*" | wc -l)
echo " Indexed: $indexed / Total: $total"
echo ""
echo "=== Done ==="

249
wiki/tools/query.py Normal file
View File

@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Wiki Knowledge Graph query tool.
Queryable interface over knowledge-graph.yaml + wiki pages.
Usable by both humans (CLI) and LLM agents (imported).
Usage:
python wiki/tools/query.py health # project health
python wiki/tools/query.py entity "search term" # everything about an entity
python wiki/tools/query.py metric "search term" # find metrics
python wiki/tools/query.py status "proven" # all pages with status
python wiki/tools/query.py test "test name" # test results
python wiki/tools/query.py search "keyword" # full-text search
python wiki/tools/query.py related "page-name" # pages linking to/from
python wiki/tools/query.py timeline # commit timeline
"""
import yaml
import os
import sys
import re
from pathlib import Path
WIKI_DIR = Path(__file__).parent.parent
GRAPH_PATH = WIKI_DIR / 'knowledge-graph.yaml'
def load_graph():
if not GRAPH_PATH.exists():
return {}
with open(GRAPH_PATH) as f:
return yaml.safe_load(f) or {}
def load_all_pages():
pages = {}
for md_path in WIKI_DIR.rglob('*.md'):
if 'tools' in str(md_path):
continue
rel = md_path.relative_to(WIKI_DIR)
content = md_path.read_text()
meta = {}
if content.startswith('---'):
parts = content.split('---', 2)
if len(parts) >= 3:
try:
meta = yaml.safe_load(parts[1]) or {}
except yaml.YAMLError:
pass
content = parts[2]
links = re.findall(r'\[\[([^\]]+)\]\]', content)
pages[str(rel)] = {
'path': str(rel), 'meta': meta, 'content': content,
'links': links, 'title': meta.get('title', str(rel)),
'status': meta.get('status', 'unknown'),
'tags': meta.get('tags', []),
}
return pages
def flatten_graph(graph, prefix=''):
items = []
if isinstance(graph, dict):
for k, v in graph.items():
path = f"{prefix}.{k}" if prefix else k
if isinstance(v, (dict, list)):
items.extend(flatten_graph(v, path))
else:
items.append((path, str(v)))
elif isinstance(graph, list):
for i, v in enumerate(graph):
path = f"{prefix}[{i}]"
if isinstance(v, (dict, list)):
items.extend(flatten_graph(v, path))
else:
items.append((path, str(v)))
return items
def cmd_health():
graph = load_graph()
pages = load_all_pages()
statuses = {}
for p in pages.values():
s = p['status']
statuses[s] = statuses.get(s, 0) + 1
tests = graph.get('tests', {})
total_pass = sum(t.get('passing', 0) for t in tests.values() if isinstance(t, dict))
total_count = sum(t.get('count', t.get('total', 0)) for t in tests.values() if isinstance(t, dict))
disproven = len(graph.get('disproven', {}))
timeline = len(graph.get('timeline', []))
# Count broken links
all_titles = set()
for p in pages.values():
all_titles.add(p['title'].lower())
all_titles.add(p['path'].lower().replace('.md', '').split('/')[-1])
broken = sum(1 for p in pages.values() for link in p['links']
if not any(link.lower().replace('-', ' ') in t or t in link.lower().replace('-', ' ')
for t in all_titles))
print(f"Wiki Health:\n")
print(f" Pages: {len(pages)}")
print(f" Statuses: {statuses}")
if total_count:
print(f" Tests: {total_pass}/{total_count} passing")
print(f" Disproven: {disproven} claims tracked")
print(f" Timeline: {timeline} commits")
print(f" Broken links: {broken}")
def cmd_entity(query):
graph = load_graph()
pages = load_all_pages()
q = query.lower()
print(f"Entity: '{query}'\n")
flat = flatten_graph(graph)
hits = [(p, v) for p, v in flat if q in p.lower() or q in v.lower()]
if hits:
print(" -- Knowledge Graph --")
for path, value in hits[:20]:
print(f" {path}: {value}")
print("\n -- Wiki Pages --")
for rel, page in sorted(pages.items()):
if q in page['content'].lower() or q in page['title'].lower():
lines = [l.strip() for l in page['content'].split('\n')
if q in l.lower() and l.strip()]
print(f" {rel} ({page['status']})")
for line in lines[:3]:
print(f" {line[:100]}")
def cmd_metric(query):
flat = flatten_graph(load_graph())
q = query.lower()
print(f"Metrics matching '{query}':\n")
found = 0
for path, value in flat:
if q in path.lower() or q in value.lower():
print(f" {path}: {value}")
found += 1
if not found:
print(" (no matches)")
def cmd_status(status):
pages = load_all_pages()
graph = load_graph()
print(f"Status: '{status}'\n")
for rel, page in sorted(pages.items()):
if page['status'] == status:
print(f" {page['title']} ({rel})")
if page['tags']:
print(f" tags: {page['tags']}")
if status == 'disproven' and 'disproven' in graph:
print("\n -- Disproven Claims --")
for name, claim in graph['disproven'].items():
print(f" {name}:")
for k, v in claim.items():
print(f" {k}: {v}")
def cmd_test(query):
tests = load_graph().get('tests', {})
q = query.lower()
print(f"Test results for '{query}':\n")
for name, suite in tests.items():
if q in name.lower() or q in str(suite).lower():
print(f" -- {name} --")
if isinstance(suite, dict):
for k, v in suite.items():
if isinstance(v, dict):
print(f" {k}: {v.get('passing', '?')}/{v.get('total', '?')}")
elif k in ('count', 'passing', 'accuracy', 'file', 'date'):
print(f" {k}: {v}")
elif k == 'results' and isinstance(v, list):
for r in v:
mark = '' if r.get('result') == 'pass' else ''
print(f" {mark} {r.get('test', '?')}")
def cmd_search(query):
flat = flatten_graph(load_graph())
pages = load_all_pages()
q = query.lower()
print(f"Search: '{query}'\n")
graph_hits = [(p, v) for p, v in flat if q in v.lower()]
if graph_hits:
print(f" -- Knowledge Graph ({len(graph_hits)} hits) --")
for p, v in graph_hits[:10]:
print(f" {p}: {v[:80]}")
page_hits = sorted(
[(page['content'].lower().count(q), rel, page['title'])
for rel, page in pages.items() if q in page['content'].lower()],
reverse=True)
if page_hits:
print(f"\n -- Wiki Pages ({len(page_hits)} pages) --")
for count, rel, title in page_hits:
print(f" {count:3d}x {title} ({rel})")
def cmd_related(page_name):
pages = load_all_pages()
q = page_name.lower().replace('-', ' ').replace('_', ' ')
print(f"Related to: '{page_name}'\n")
print(" -- Links TO --")
for rel, page in sorted(pages.items()):
for link in page['links']:
if q in link.lower().replace('-', ' '):
print(f" <- {page['title']} ({rel})")
break
print("\n -- Links FROM --")
for rel, page in pages.items():
if q in page['title'].lower().replace('-', ' '):
for link in page['links']:
print(f" -> [[{link}]]")
break
def cmd_timeline():
for entry in load_graph().get('timeline', []):
print(f" [{entry.get('date')}] {entry.get('commit', '?')}: {entry.get('desc', '?')}")
COMMANDS = {
'health': cmd_health, 'entity': cmd_entity, 'metric': cmd_metric,
'status': cmd_status, 'test': cmd_test, 'search': cmd_search,
'related': cmd_related, 'timeline': cmd_timeline,
}
if __name__ == '__main__':
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
print(f"Usage: query.py <{'|'.join(COMMANDS)}> [args]")
sys.exit(1)
cmd = sys.argv[1]
args = sys.argv[2:]
if cmd in ('timeline', 'health'):
COMMANDS[cmd]()
elif args:
COMMANDS[cmd](' '.join(args))
else:
print(f"Usage: query.py {cmd} <query>")

18
wiki/tools/search.sh Normal file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Search the wiki — usable by both humans and LLM agents
# Usage: ./wiki/tools/search.sh "query" [--files-only]
WIKI_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
QUERY="$1"
MODE="${2:---content}"
if [ -z "$QUERY" ]; then
echo "Usage: $0 <query> [--files-only]"
exit 1
fi
if [ "$MODE" = "--files-only" ]; then
grep -rl --include="*.md" --include="*.yaml" "$QUERY" "$WIKI_DIR" 2>/dev/null | sort
else
grep -rn --include="*.md" --include="*.yaml" --color=auto -i "$QUERY" "$WIKI_DIR" 2>/dev/null
fi