diff --git a/.specter-prompt.txt b/.specter-prompt.txt index 8cd26ce4..367fcddf 100644 --- a/.specter-prompt.txt +++ b/.specter-prompt.txt @@ -4,8 +4,8 @@ You are running HEADLESS. Do NOT use AskUserQuestion, interactive prompts, or sk Write files directly using the Write tool. ## Inputs -- Change: p2-minutes-and-decisions-core-t2 (title: Minutes and Decisions — Core T2) -- Context brief: openspec/changes/p2-minutes-and-decisions-core-t2/context-brief.md +- Change: p2-minutes-and-decisions-core-t3 (title: Minutes and Decisions — Core T3) +- Context brief: openspec/changes/p2-minutes-and-decisions-core-t3/context-brief.md - Data model ADR: openspec/architecture/adr-000-data-model.md Governance Domains: @@ -29,13 +29,13 @@ Project guidelines: See ../project.md for workspace-wide standards ## Step 1: Read context Read these files COMPLETELY before writing anything: -- openspec/changes/p2-minutes-and-decisions-core-t2/context-brief.md (features, stories, stakeholders, journeys) +- openspec/changes/p2-minutes-and-decisions-core-t3/context-brief.md (features, stories, stakeholders, journeys) - openspec/architecture/adr-000-data-model.md (entity definitions) - All files in openspec/architecture/ (app ADRs) - All files in .claude/openspec/architecture/ (company ADRs, if present) ## Step 2: Get artifact templates -Run: `openspec instructions proposal --change p2-minutes-and-decisions-core-t2 --json` +Run: `openspec instructions proposal --change p2-minutes-and-decisions-core-t3 --json` This returns the template and rules for the proposal artifact. Then do the same for: design, specs, tasks (in that order). @@ -54,8 +54,8 @@ For each artifact (proposal → design → specs → tasks): ## Step 4: Commit After all 4 artifacts are written: ```bash -git add openspec/changes/p2-minutes-and-decisions-core-t2/ -git commit -m 'feat: Add OpenSpec change p2-minutes-and-decisions-core-t2 from Specter' +git add openspec/changes/p2-minutes-and-decisions-core-t3/ +git commit -m 'feat: Add OpenSpec change p2-minutes-and-decisions-core-t3 from Specter' ``` Do NOT push — the caller handles pushing. Do NOT create branches — stay on the current branch. diff --git a/openspec/changes/p2-minutes-and-decisions-core-t3/.openspec.yaml b/openspec/changes/p2-minutes-and-decisions-core-t3/.openspec.yaml index 863bff18..3a54a172 100644 --- a/openspec/changes/p2-minutes-and-decisions-core-t3/.openspec.yaml +++ b/openspec/changes/p2-minutes-and-decisions-core-t3/.openspec.yaml @@ -1,2 +1,2 @@ schema: spec-driven -created: 2026-04-17 +created: 2026-04-16 diff --git a/openspec/changes/p2-minutes-and-decisions-core-t3/context-brief.md b/openspec/changes/p2-minutes-and-decisions-core-t3/context-brief.md index 12a15887..ae52ee91 100644 --- a/openspec/changes/p2-minutes-and-decisions-core-t3/context-brief.md +++ b/openspec/changes/p2-minutes-and-decisions-core-t3/context-brief.md @@ -30,7 +30,7 @@ These specs were already decided/implemented. Use them as context. **Spec:** p1-schemas-and-data-model **Platform:** Nextcloud + OpenRegister -## Features (6 total, spec-linked, sorted by market demand) +## Features (6 total, sorted by market demand) ### Resolution Register **demand: 206** (66 tender mentions) | Category: core @@ -101,10 +101,10 @@ Managing AV infrastructure in raadzaal and commissiekamers. Discussion systems, ### Handle Complex Multi-Domain Citizen Question A citizen has a question spanning multiple domains (e.g., housing benefit, parking permit, and social assistance). The front desk officer creates linked zaken or a combined intake and routes each to the correct department. -**Trigger:** Citizen presents with multiple interconnected serv +**Trigger:** Citizen presents with multiple interconnected service needs in ... (truncated) -## Features (16 total, spec-linked, sorted by market demand) +## Features (16 total, sorted by market demand) ### Real-Time Action Item Tracking and Multi-Meeting Analytics **demand: 94** (21 tender mentions) | Category: core @@ -825,16 +825,6 @@ All Conduction Nextcloud apps serve Dutch government users but must support mult - `l10n/en.json` is the identity-mapped source file (key == value). - Hardcoded Dutch strings in code MUST be converted to English keys with Dutch translations in `nl.json`. -### Sentence Case for All UI Strings -- All translation keys and user-facing strings MUST use **sentence case**: only the first word is capitalized. -- Correct: `"Add directory"`, `"No results found"`, `"Delete selected"`, `"Save configuration"` -- Wrong (title case): `"Add Directory"`, `"No Results Found"`, `"Delete Selected"` -- Wrong (all lowercase): `"add directory"`, `"no results found"` -- **Exceptions** that keep their capitalization: - - Proper nouns and product names: `"OpenRegister"`, `"Nextcloud"`, `"GitHub"`, `"DocuDesk"` - - Acronyms: `"API"`, `"URL"`, `"PDF"`, `"SOLR"`, `"JSON"`, `"RBAC"`, `"OAS"` - - Single-word strings still start with a capital: `"Delete"`, `"Search"`, `"Save"` - ### Required Languages - Minimum: English (en) + Dutch (nl) translations. - `l10n/en.json` and `l10n/nl.json` MUST exist in every app with a UI. @@ -855,16 +845,9 @@ All Conduction Nextcloud apps serve Dutch government users but must support mult - Date/number formatting: respect user locale via Nextcloud core. - Each app with OpenRegister: define `register-i18n` spec listing translatable fields. -### Shared Component Library (@conduction/nextcloud-vue) -- The shared library does NOT translate internally — it accepts pre-translated strings via props. -- Components have English defaults for all label/text props (e.g., `addLabel="Add"`, `cancelLabel="Cancel"`). -- Consumer apps are responsible for passing `t()` results as prop values. -- The library lists `@nextcloud/l10n` as a peer dependency, not a direct dependency. - ## Consequences - All apps maintain two translation files that must stay in sync. - Dutch strings used as translation keys (e.g., `t('app', 'Besluiten')`) are a violation — the English equivalent must be the key. -- Title case in translation keys (e.g., `"Add Directory"`) is a violation — use sentence case (`"Add directory"`). - New features must include both `en.json` and `nl.json` entries before merging. ### ADR-008-testing @@ -946,34 +929,18 @@ We want to unify these into a **single priority-scheduled container pool** so th ### Container types (priority order) -| Priority | Type | Source | Container image | Model | Fallback | -|----------|------|--------|-----------------|-------|----------| -| 1 | **bugfix** | Hydra: fix iteration after review failure | `hydra-builder` | haiku | — | -| 2 | **code-review** | Hydra: PR code review | `hydra-reviewer` | sonnet | opus | -| 3 | **security-review** | Hydra: PR security review | `hydra-security` | sonnet | opus | -| 4 | **build** | Hydra: initial spec build | `hydra-builder` | haiku | — | -| 5 | **audit** | Hydra: codebase audit | `hydra-builder` | sonnet | opus | -| 6 | **spec-generation** | Specter: push_spec_pipeline | `specter-llm-worker` | sonnet | haiku | -| 7 | **schema-synthesis** | Specter: generate/dedup schemas | `specter-llm-worker` | haiku | — | -| 8 | **classification** | Specter: classify/redistribute features | `specter-llm-worker` | haiku | — | -| 9 | **translation** | Specter: translate requirements | `specter-llm-worker` | haiku | — | -| 10 | **discovery** | Specter: research, feature extraction | `specter-llm-worker` | haiku | — | - -### Model strategy - -**Principle:** Use the cheapest model that can do the job. Reserve expensive models for judgment work. - -| Work type | Model | Rationale | -|-----------|-------|-----------| -| Build (implementation) | **Haiku** | Clear instructions (tasks.md, design.md). Pattern-following, not judgment. Faster and cheaper — 5 parallel Haiku builds burn far less quota than Sonnet. | -| Fix (quality/browser/review) | **Haiku** | "Fix this PHPCS error" or "fix this review finding" — explicit, targeted corrections. | -| Code review | **Sonnet → Opus** | Judgment work: spotting architectural issues, missed edge cases, style problems. Falls back to deeper model (Opus) when Sonnet quota is exhausted. | -| Security review | **Sonnet → Opus** | Critical: injection vectors, auth bypasses, secret leaks. Same fallback logic. | -| Audit | **Sonnet → Opus** | Full codebase analysis — needs depth. | - -**Quota optimization:** Claude Max plans have separate "Sonnet only" and "all models" weekly limits. By defaulting builders to Haiku, the Sonnet quota is reserved for reviews only (~20 turns each, 2 per PR). When Sonnet runs out, reviews fall back to the **deeper** model (Opus), not the shallower one — because reviews are the last line of defense before human approval. - -**Overrides:** Set `HYDRA_BUILDER_MODEL`, `HYDRA_REVIEWER_MODEL`, or `HYDRA_REVIEWER_FALLBACK_MODEL` env vars to change defaults. +| Priority | Type | Source | Container image | Model | +|----------|------|--------|-----------------|-------| +| 1 | **bugfix** | Hydra: fix iteration after review failure | `hydra-builder` | sonnet | +| 2 | **code-review** | Hydra: PR code review | `hydra-reviewer` | sonnet | +| 3 | **security-review** | Hydra: PR security review | `hydra-security` | sonnet | +| 4 | **build** | Hydra: initial spec build | `hydra-builder` | sonnet | +| 5 | **audit** | Hydra: codebase audit | `hydra-builder` | sonnet | +| 6 | **spec-generation** | Specter: push_spec_pipeline | `specter-llm-worker` | sonnet | +| 7 | **schema-synthesis** | Specter: generate/dedup schemas | `specter-llm-worker` | haiku | +| 8 | **classification** | Specter: classify/redistribute features | `specter-llm-worker` | haiku | +| 9 | **translation** | Specter: translate requirements | `specter-llm-worker` | haiku | +| 10 | **discovery** | Specter: research, feature extraction | `specter-llm-worker` | haiku | ### Architecture diff --git a/openspec/changes/p2-minutes-and-decisions-core-t3/design.md b/openspec/changes/p2-minutes-and-decisions-core-t3/design.md index 78a7c5aa..25d6ca50 100644 --- a/openspec/changes/p2-minutes-and-decisions-core-t3/design.md +++ b/openspec/changes/p2-minutes-and-decisions-core-t3/design.md @@ -1,379 +1,196 @@ -## Context - -The `p2-minutes-and-decisions` change established foundational CRUD operations and data schemas for Minutes and Decision entities. Minutes and Decisions exist as data records, but governance bodies require operational workflows around them: decisions must be captured live during meetings, approved through formal governance procedures, published for public accountability (ORI standard), and converted into actionable follow-ups. - -Core T3 builds these operational workflows on top of the existing data foundation. The change is constrained by four prior ADRs: - -- **CalDAV-first storage** (ADR-002): ActionItems are VTODOs in Nextcloud Tasks — NOT OpenRegister objects. No sync layer. -- **Popolo primary standard** (ADR-001): No separate Decision entity. Decisions are outcomes of Motions (`lifecycle: adopted` + `decisionText`, `decisionDate`, `isPublished`, `publishedAt`, `legalBasis` fields on the Motion object). -- **ORI compatibility** (ADR-003): Dutch municipalities expect Minutes (as Reports) and decisions via `/api/ori/v1/reports` and `/api/ori/v1/motions`. -- **OpenRegister platform** (company ADR): Workflow engine, notification, search, audit, and export capabilities are provided and MUST be reused. + -**Current state:** Minutes and Motion schemas defined in `lib/Settings/decidesk_register.json`. No approval workflow, no ORI publication, no real-time capture, no action item extraction. Schema fields `lifecycle`, `signedBy`, `version` on Minutes and `decisionText`, `decisionDate`, `isPublished`, `publishedAt`, `legalBasis` on Motion are present in ADR-000 but not yet operationally wired. +## Context -**Stakeholders affected:** -- Council clerks and board secretaries — authoring and approving minutes -- Governance body chairs and secretaries — digital signature for minutes approval -- Citizens and institutional investors — public decision access via ORI API -- IT administrators — ORI endpoint configuration -- Management teams and corporate boards — action item tracking from meeting decisions +Decidesk is a thin-client Nextcloud app: all domain data is stored in OpenRegister. The post-meeting workflow — Minutes lifecycle, Decision recording, ActionItem tracking, and minutes generation — was delivered in p2-minutes-and-decisions. T1 added the highest-demand compliance extensions (document generation, statutory deadlines, urgent flag, decision list). This T3 change adds the operational efficiency tier: cross-meeting analytics, live decision entry, ALV-specific minutes, approval notifications, auto-extraction of action items, decision rationale capture, and notification on publication. -**Depends on:** `p2-meeting-management` (Meeting/CalDAV wrapper), `p2-motion-and-voting` (Motion, VotingRound), `p2-agenda-management` (AgendaItem). +All four entities — Decision, Minutes, ActionItem, and Meeting — are already registered as OpenRegister schemas from p1-schemas-and-data-model. No schema changes are required: the `rationale` concept uses the OpenRegister built-in `notes` array with a labelled note, and analytics queries use `ObjectService.findAll()` with filters. ---- +The Board Secretary is the primary actor for most features: they run meetings, record decisions, generate and distribute minutes, and track follow-up. The CEO / Director needs the analytics overview and decision notifications. The chair needs the approval request notifications. ## Goals / Non-Goals **Goals:** -- Real-time Minutes and Decision capture during active meetings with debounced auto-save and optimistic locking -- Formal Minutes approval lifecycle (draft → submitted → approved → published) using platform workflow engine with role-based guards -- Digital signature recording for chair and secretary via `AuditTrailService` (legal acknowledgement trail) -- Automatic ActionItem extraction from approved Minutes content using configurable regex/keyword patterns — creates CalDAV VTODOs via `CalDavService` -- ORI-compatible `/api/ori/v1/reports` and `/api/ori/v1/motions` endpoints exposing published Minutes and adopted Motions (decision outcome fields) -- Decision discovery: full-text search, faceted filtering, and CSV/JSON/PDF export across governance bodies -- Foundation for decision notifications: event emission on state transitions, per-user/body notification preferences -- Seed data for Minutes and Motion (with decision outcome) entities in `lib/Settings/decidesk_register.json` to support automated testing and QA +- Multi-meeting action item analytics — KPI cards, completion rate chart, personal action item list on the Dashboard +- Live decision recording during active meeting via "Besluiten" tab on Meeting detail +- ALV minutes template generation and member distribution +- Minutes approval request with Nextcloud notifications to chair and secretary +- Auto-extract action item candidates from Minutes `content` with preview-and-confirm +- Decision rationale ("Overwegingen") capture via OpenRegister notes +- Decision notification dispatch when `isPublished` transitions to `true` **Non-Goals:** -- Full ORI national harvesting protocol push-integration with Open State Foundation crawler (separate project) -- AI/ML-based minutes summarization or automated decision text generation (non-deterministic; out of scope) -- Video/audio recording integration or AV-RIS indexing (separate project) -- Speech entity implementation (deferred to later phase per ADR-000) -- PKI/eIDAS qualified electronic signatures (deferred; current approach uses audit trail acknowledgement) -- Multi-language minutes content (English via i18n keys for UI; Minutes content text is user-authored) - ---- +- AI/LLM-powered transcription or decision summary (deferred) +- Registered mail integration for formal legal notification (deferred) +- Full DMN (Decision Model & Notation) workflow integration (deferred) +- External analytics platforms (deferred) +- Complex NLP entity extraction for action items (regex-based only) +- API to Agent Tool Conversion (deferred) ## Decisions -### 1. ActionItem storage: CalDAV VTODO (not OpenRegister) - -**Decision:** ActionItems created by action item automation are stored as CalDAV VTODOs in Nextcloud Tasks with `X-DECIDESK-MOTION-UID` and `X-DECIDESK-MEETING-UID` extended properties. There is no OpenRegister schema for ActionItem. - -**Rationale:** ADR-002 mandates CalDAV-first storage for meetings and action items. ActionItems as VTODOs appear natively in the Nextcloud Tasks app, sync to any CalDAV client, and require zero integration code. Creating a parallel OpenRegister schema would reintroduce the sync layer that ADR-002 explicitly eliminates. - -**Consequence for seed data:** ActionItem seed objects in `decidesk_register.json` are NOT applicable. ActionItems are CalDAV objects, not OpenRegister objects. Seed data is limited to Minutes and Motion entities. - -**Alternative considered:** Store ActionItems in OpenRegister for richer relational queries. Rejected: contradicts ADR-002. - ---- - -### 2. Decision as Motion outcome (no separate Decision entity) - -**Decision:** Published decisions are represented by Motion objects with `lifecycle: adopted`, `isPublished: true`, and populated `decisionText`, `decisionDate`, `publishedAt`, `legalBasis` fields. The deprecated `Decision` entity (ADR-000) is not re-introduced. - -**Rationale:** ADR-000 explicitly deprecates the Decision entity: "Decision is now the outcome of a Motion. This follows the Popolo standard which has no separate Decision class." Creating a Decision entity would duplicate data, violate ADR-001 (Popolo), and require a synchronization concern. - -**ORI mapping:** The ORI endpoint `/api/ori/v1/motions` serves adopted Motions as ORI Motion objects with decision fields. The endpoint `/api/ori/v1/reports` serves published Minutes as ORI Report objects. Neither requires a new entity. - -**Alternative considered:** Separate Decision entity linked to Motion via relation. Rejected: violates ADR-000 and ADR-001. - ---- - -### 3. Minutes approval via platform workflow engine - -**Decision:** Minutes lifecycle transitions (draft → submitted → approved → published) are implemented using OpenRegister's `WorkflowEngineController` / `WorkflowEngineRegistry`. Role-based transition guards enforce that only authorized users (chair, secretary, governance body authority) can advance each state. - -**Rationale:** Per company ADR: "Task & Workflow Management: use TasksController + WorkflowEngineController. NO custom task/workflow systems." A custom PHP state machine would duplicate platform capability and introduce inconsistency. - -**Workflow definition:** Stored as OpenRegister metadata on the Minutes schema. Each governance domain (legislative, association, corporate, operational, citizen) MAY configure a domain-specific workflow template via the governance body's `workflowTemplate` field. - -**Alternative considered:** Custom PHP service with explicit state transition methods. Rejected: duplicates platform capability. - ---- - -### 4. Digital signatures via audit trail acknowledgement - -**Decision:** Digital signatures (chair + secretary) for Minutes approval are recorded by: -1. The `signedBy` array on the Minutes object storing the UIDs of authorized signers. -2. The OpenRegister `AuditTrailService` automatically capturing who made each lifecycle transition and when. - -Together these provide a legally defensible record of who acknowledged the Minutes and when, without requiring PKI infrastructure. - -**Rationale:** OpenRegister's audit trail is automatic and tamper-evident. The `signedBy` array captures intended signers; the audit trail captures actual actors. This satisfies Dutch governance requirements for meeting minutes authentication (Gemeentewet art. 23) at the "acknowledgement" level of assurance. - -**Limitation (documented in UI):** This is a digital acknowledgement trail, not an eIDAS qualified electronic signature. Governance bodies requiring Level of Assurance "High" (e.g. notarial acts) must use a separate PKI integration (deferred). - -**Alternative considered:** Integrate with DigiD or PKIoverheid for qualified signatures. Deferred to a future phase. - ---- - -### 5. Action item extraction: pattern-based backend service - -**Decision:** `ActionItemExtractor` PHP service parses the Minutes `content` field using configurable regex/keyword patterns stored in governance body settings, then creates CalDAV VTODOs via `CalDavService` with `X-DECIDESK-MOTION-UID` and `X-DECIDESK-MEETING-UID` set where applicable. - -**Pattern format:** JSON array of named patterns per governance body, stored via `IAppConfig`. Example: `[{ "name": "actiepunt", "pattern": "ACTIEPUNT:\\s*(.+?)(?=ACTIEPUNT|BESLUIT|$)" }]`. - -**Trigger:** Extraction runs automatically when Minutes transition to `approved` lifecycle state. Results are created as VTODOs in a "DecideDesk — Actiepunten" CalDAV calendar. - -**Rationale:** Deterministic pattern matching is auditable, configurable, and has no external dependencies. Municipal meeting minutes in the Netherlands follow predictable patterns (ACTIEPUNT, BESLUIT, "zal worden uitgevoerd door"). - -**Alternative considered:** AI/ML extraction via `ChatService`. Deferred: non-deterministic, requires LLM availability, out of scope for Core T3. - ---- - -### 6. ORI endpoint as read-only serialization layer - -**Decision:** `/api/ori/v1/reports` is a public, read-only endpoint serializing Minutes objects with `lifecycle: published` to ORI Report format. `/api/ori/v1/motions` exposes adopted Motions with decision fields. A dedicated `OriSerializer` PHP service handles all field mapping. No ORI data is stored separately. - -**Rationale:** ADR-003 specifies "the endpoint is a thin read-only serialization layer, not a separate data store." Internal storage is Popolo-aligned; ORI is the Dutch municipal output format. `OriSerializer` is isolated, enabling ORI standard evolution without touching the data model. - -**Public access:** Per ADR-002 and `#[PublicPage] #[NoCSRFRequired]` annotation pattern. Published Minutes and adopted Motions are public governance records. - -**Alternative considered:** Store data in ORI format natively. Rejected: ORI is Dutch-specific; international users would be burdened. - ---- - -## Reuse Analysis - -The following OpenRegister and platform services are leveraged (per ADR-012): - -| Service | Usage in this change | -|---|---| -| `ObjectService` | CRUD for Minutes, Motion (decision fields); auto-save endpoint | -| `WorkflowEngineController` / `WorkflowEngineRegistry` | Minutes lifecycle state machine (draft → published) | -| `AuditTrailService` | Digital signature audit proof; change tracking on Minutes approval | -| `NotificationService` | Decision publication notifications (Nextcloud notification API) | -| `IndexService` + `CnFacetSidebar` | Decision full-text search and faceted filtering | -| `SearchTrailService` | Decision search analytics (popular terms) | -| `ExportService` + `CnMassExportDialog` | CSV/JSON/PDF export of decision lists | -| `CnObjectSidebar` → `CnAuditTrailTab` | Minutes revision history and audit UI | -| `CnTimelineStages` | Minutes lifecycle workflow progression visualization | -| `CnFormDialog` / `CnAdvancedFormDialog` | Minutes editing form, decision capture fields | -| `createObjectStore` with `auditTrails`, `files`, `lifecycle` plugins | Pinia stores for Minutes and Motion entities | -| `CalDavService` | ActionItem VTODO creation with X-DECIDESK-* extended properties | -| `CnIndexPage` + `useListView` | Decision discovery list view with search and filter | -| `CnDetailPage` + `useDetailView` | Minutes detail view with approval workflow | - -**No new platform services are required.** All capabilities map to existing platform functionality. - -**Deduplication findings:** No overlap with existing Decidesk capabilities (`p2-meeting-management`, `p2-motion-and-voting`, `p2-agenda-management`). The ORI endpoint paths defined in ADR-003 (`/api/ori/v1/reports`, `/api/ori/v1/motions`) are not yet implemented by any prior change. - ---- - -## Risks / Trade-offs - -| Risk | Mitigation | -|---|---| -| Concurrent real-time editing of Minutes by multiple clerks | `ObjectService.lockObject()` before editing session; debounced auto-save (500ms) with last-write-wins; display "locked by [user]" notice to secondary editors | -| CalDAV VTODO links break if parent Motion is deleted | Soft-delete Motions: lifecycle transitions to `withdrawn` rather than physical deletion; warn clerk before deletion if `X-DECIDESK-MOTION-UID` VTODOs reference the Motion | -| ORI field mapping drift as ORI standard evolves | `OriSerializer` is an isolated class with inline ORI spec version comment; field mappings documented as constants | -| `signedBy` audit trail does not constitute eIDAS qualified signature | Explicitly documented in approval UI tooltip and docs: "digitale akkoordverklaring — geen gekwalificeerde elektronische handtekening" | -| Extraction pattern false positives creating spurious ActionItems | Preview mode: show extracted items before creating VTODOs; allow manual deletion after extraction; configurable per-body patterns | -| Large Minutes content (multi-hour sessions, 20+ agenda items) | OpenRegister stores content as JSON string with no practical field length limit; validate in QA with large seed content | -| ORI endpoint performance under high load (all published Minutes) | Pagination (`_page` + `_limit`) mandatory; indexed `lifecycle` and `isPublished` fields via OpenRegister; no N+1 queries in serializer | - ---- - -## Migration Plan - -### Steps - -1. **Schema update (non-breaking add):** Add optional fields `lifecycle` (default: `draft`), `signedBy` (default: `[]`), `version` (default: `1`) to Minutes schema in `decidesk_register.json`. Add `isPublished` (default: `false`), `publishedAt`, `legalBasis`, `decisionText`, `decisionDate` to Motion schema. All new fields are optional — adding optional properties is non-breaking per ADR. - -2. **Repair step:** Add `IRepairStep` implementation `MigrateMinutesDecisionFieldsRepairStep` that: - - Finds all existing Minutes objects without `lifecycle` → sets `lifecycle: draft`, `version: 1` - - Finds all existing Motion objects with `lifecycle: adopted` and no `isPublished` field → sets `isPublished: false` - - Uses `ObjectService::findObjects()` with `_rbac: false` and `_multitenancy: false` for idempotent migration - -3. **Schema import update:** Bump version in `decidesk_register.json` to trigger re-import via `ConfigurationService::importFromApp()` idempotency logic. - -4. **CalDAV calendar provisioning:** On app activation, ensure a "DecideDesk — Actiepunten" CalDAV calendar exists per governance body via `CalDavService`. - -### Rollback - -New optional fields are forward-compatible. Rollback: revert schema definitions; existing objects retain unknown fields but platform ignores them (schema-tolerant). The repair step is idempotent — safe to re-run. - ---- - -## Open Questions - -1. **ORI harvesting push protocol:** Should `/api/ori/v1/reports` support the ORI webhook push-harvest protocol used by the Open State Foundation crawler? Currently scoped as passive pull-only — confirm with stakeholders before shipping. - -2. **Qualified e-signatures:** Which governance domains require eIDAS Level of Assurance "High" for minutes signatures? Water boards and provincial states have stricter authentication requirements. If any domain requires qualified signatures, a PKI integration change must be scoped separately. - -3. **ActionItem extraction: English-language patterns:** Municipal minutes are Dutch, but corporate board minutes may be in English. Should extraction patterns support per-body language configuration? Currently assumed Dutch-language only. - -4. **Minutes content storage:** Should large Minutes content (>100KB) be stored as a file attachment via `FileService` rather than inline in the OpenRegister object's `content` field? No known limit hit yet, but architectural preference should be confirmed. - ---- +### 1. Analytics computed at query time via ObjectService, not cached +**Decision**: `ActionItemAnalyticsService` computes analytics on each dashboard load by calling `ObjectService.findAll()` with filters for `taskStatus`, `dueDate`, and `assignee`. No separate analytics cache or materialized view. +**Rationale**: OpenRegister's `ObjectService.findAll()` supports filtering and pagination. For the scale of Dutch governance deployments (hundreds to low thousands of ActionItems), query-time computation is fast enough. Caching would require additional infrastructure and invalidation logic that adds maintenance cost without meaningful benefit at this scale. ADR-001 prohibits custom data stores. +**Alternative considered**: Pre-computed analytics table — rejected (custom mapper, ADR-001 violation); client-side calculation from a full export — rejected (performance risk at scale). + +### 2. Decision rationale stored in OpenRegister built-in `notes` array +**Decision**: The "Overwegingen" rationale is stored as a note with `label: "overwegingen"` in the Decision object's built-in `notes` array via `ObjectService.saveObject()`. The Decision detail page reads the notes array and renders the first note with this label in the "Overwegingen" section. +**Rationale**: ADR-000 defines no `rationale` property on Decision. Adding one would be a breaking schema change (ADR-011). The built-in `notes` array already supports labelled, free-text content with full audit trail tracking (ADR-001). This approach requires zero schema changes and avoids an ADR-000 update. +**Alternative considered**: A new `rationale: string` field on Decision schema — rejected (breaking ADR-000 change without a new ADR-000 revision; notes covers the use case). + +### 3. Live decision recording uses the existing Decision schema and Meeting relation +**Decision**: The `LiveDecisionPanel.vue` creates Decision objects via `ObjectService.saveObject()` with a relation to the parent Meeting. If no Minutes object is linked to the Meeting, the panel auto-creates a draft Minutes object with `title` set to "Concept notulen — {meeting title}" and `lifecycle: draft`. No separate "live mode" entity is introduced. +**Rationale**: Reusing existing Decision + Minutes schemas (ADR-000, ADR-012) avoids new entities. The Meeting relation is already supported by OpenRegister's relation mechanism. The "live" aspect is purely a frontend UX — the data model is unchanged. +**Alternative considered**: A "LiveSession" entity for in-progress meeting state — rejected (ADR-000 is the source of truth; no new entity needed; existing Meeting lifecycle `opened` serves as the gate). + +### 4. Action item extraction uses regex markers, not NLP +**Decision**: `ActionItemExtractionService::extractFromContent()` uses PHP regex patterns to detect action item markers in the Minutes `content` string. Patterns: lines starting with `Actie:`, `AI:`, `Taak:`, `Actiepunt:`, or lines containing Dutch action phrases (`wordt verzocht`, `zal worden`, `is toegezegd`). Each match is converted to an ActionItem candidate with `title` from the matched text and `taskStatus: open`. The secretary reviews candidates in a modal before saving. +**Rationale**: LLM-based extraction (ADR-001's `ChatService`) requires always-on infrastructure and raises GDPR concerns for meeting minutes. Regex-based extraction covers the 80% case (structured Dutch minutes follow predictable patterns). The preview-and-confirm step protects against false positives. This is consistent with the `MinutesGenerationService` approach (template-based, deterministic, no LLM). +**Alternative considered**: `ChatService` / LLM extraction — deferred; privacy risk; infrastructure dependency. + +### 5. Approval request transitions `lifecycle: draft → review` and sends notifications +**Decision**: "Ter goedkeuring indienen" on a Minutes in `draft` state calls `WorkflowEngineController` to transition `lifecycle: draft → review` (existing transition from p2-minutes-and-decisions), then dispatches Nextcloud notifications via `NotificationService` to all users who have an active Membership with role `chair` or `secretary` in the linked GovernanceBody. The notification includes the minutes title, `decisionDate` context, and a deep link to the Minutes detail page. +**Rationale**: The `draft → review` transition already exists in the workflow. Attaching notification dispatch to it avoids a separate workflow state. Role-based recipient lookup reuses the existing Membership relation on GovernanceBody (ADR-001). `NotificationService` is the platform service for in-app notifications (ADR-001). +**Alternative considered**: Email-only notification — rejected (requires external SMTP config; Nextcloud notifications are native and reliable); separate "approval request" state — rejected (over-engineering for the use case). + +### 6. ALV minutes use a dedicated Dutch template branch in ALVMinutesService +**Decision**: `ALVMinutesService::generateALVDraft(string $minutesId): string` is a new standalone service (not extending `MinutesGenerationService`) that renders an ALV-specific Dutch template. It checks that the linked Meeting has `meetingType` matching `alv` (or `algemene-ledenvergadering`); if not, it returns a validation error. The ALV template includes: quorum confirmation, member roll call, agenda items as resolutions with vote totals, and a standard "rondvraag en sluiting" section. +**Rationale**: The ALV template is structurally different from the general council minutes template — it must confirm quorum under association law, use "leden" instead of "raadsleden", and include formal resolution language per Dutch association/BV law. A separate service class is cleaner than branching logic inside `MinutesGenerationService` (ADR-012: single-responsibility; extension is acceptable, but a 50-line branch adds complexity). +**Alternative considered**: Extending `MinutesGenerationService` with an ALV branch — acceptable but deferred; separate service is cleaner and testable in isolation. + +### 7. Decision notification uses DecisionNotificationService hooked into DecisionService +**Decision**: `DecisionNotificationService::notifyOnPublish(string $decisionId, array $recipients)` sends Nextcloud notifications to recipients when called. It is invoked from `DecisionService` (T1) after `isPublished` is set to `true` via `ObjectService.saveObject()`. Recipients are resolved by `DecisionNotificationService::resolveRecipients(string $decisionId): array` which reads Membership records for the linked GovernanceBody and filters by role (configurable: default `chair`, `secretary`, `member`). The roles included in notification are configurable via `IAppConfig` key `decision_notify_roles`. +**Rationale**: `NotificationService` is the ADR-001 platform service. Hooking into `DecisionService` avoids duplicating publication logic. Role-based recipient resolution reuses Membership data (ADR-001, ADR-012). The configurable roles allow water boards, associations, and corporate boards to customise who receives notifications. +**Alternative considered**: Webhook dispatch (ADR-001 WebhookService) — deferred to p3-ori-publication; Nextcloud notifications are sufficient for T3. + +## Reuse Analysis (ADR-012) + +| Capability | OpenRegister service / component used | Custom code | +|---|---|---| +| Action item analytics queries | `ObjectService.findAll()` with filters | `ActionItemAnalyticsService` (aggregation logic) | +| Analytics chart | `CnChartWidget` (ApexCharts built-in) | `ActionItemAnalyticsWidget.vue` (props mapping) | +| Live decision entry | `ObjectService.saveObject()` | `LiveDecisionPanel.vue` (quick-entry form) | +| Auto-init draft Minutes | `ObjectService.saveObject()` | Called from `LiveDecisionPanel.vue` | +| ALV template rendering | `ObjectService.findAll()` (AgendaItems, Participants) | `ALVMinutesService::generateALVDraft()` | +| ALV distribution notifications | `NotificationService` (built-in) | Called from `ALVMinutesService::distribute()` | +| Minutes approval notification | `NotificationService` + `WorkflowEngineController` | Hooked into existing `draft → review` transition | +| Action item extraction | `ObjectService.saveObject()` (ActionItem creation) | `ActionItemExtractionService::extractFromContent()` | +| Decision rationale display | `ObjectService.saveObject()` (notes array, built-in) | "Overwegingen" section in `DecisionDetail.vue` | +| Decision notification | `NotificationService` + Membership query | `DecisionNotificationService` | +| Membership role lookup | `ObjectService.findAll()` (Membership filter) | Called from `DecisionNotificationService` | +| Analytics personal list | `ObjectService.findAll()` (filter by assignee) | None (standard list query) | +| Pagination and filtering | `CnFilterBar` + `useListView` | None | +| Audit trail | `AuditTrailService` (automatic) | None | + +No new entities are proposed. No overlap with OpenRegister core services beyond what is listed. `ActionItemAnalyticsService`, `ALVMinutesService`, `ActionItemExtractionService`, and `DecisionNotificationService` are the only net-new PHP classes. ## Seed Data -All seed objects follow the `@self` envelope format for `lib/Settings/decidesk_register.json` under `components.objects[]`. Objects are loaded via `ConfigurationService::importFromApp()` on install and are idempotent (matched by `slug`). - -**Note:** ActionItem seed data is NOT included — ActionItems are CalDAV VTODOs (ADR-002), not OpenRegister objects. There is no ActionItem schema in `decidesk_register.json`. - ---- +### Minutes — ALV examples (3 objects) -### Minutes — 5 seed objects - -**1. Gepubliceerde raadsnotulen — gemeente Westerbork** ```json -{ - "@self": { - "register": "decidesk", - "schema": "minutes", - "slug": "minutes-westerbork-raad-20260115" +[ + { + "@self": { "register": "decidesk", "schema": "Minutes", "slug": "notulen-alv-vereniging-dorpshuis-laren-2025" }, + "title": "Notulen Algemene Ledenvergadering Vereniging Dorpshuis Laren — 18 maart 2025", + "lifecycle": "approved", + "content": "De voorzitter opent de ALV om 20:00 uur en stelt vast dat 47 van de 89 leden aanwezig zijn (quorum behaald). Agendapunt 1 — Vaststelling notulen vorige ALV: de notulen van 19 maart 2024 worden ongewijzigd vastgesteld. Agendapunt 2 — Jaarverslag 2024: het jaarverslag 2024 wordt aangenomen met 44 stemmen voor, 2 tegen, 1 onthouding. Actie: penningmeester stuurt het goedgekeurde jaarverslag naar alle leden vóór 1 april 2025. Agendapunt 3 — Begroting 2025: de begroting 2025 wordt vastgesteld met 43 stemmen voor. Rondvraag: geen bijzonderheden. Sluiting: 21:30 uur.", + "approvedAt": "2025-04-02T10:00:00Z", + "signedBy": ["Marjan Visser", "Erik de Boer"], + "version": 2 }, - "title": "Notulen raadsvergadering gemeente Westerbork — 15 januari 2026", - "lifecycle": "published", - "content": "De vergadering wordt geopend om 19:30 door voorzitter dhr. J. Hendriks. Aanwezig: 17 raadsleden, quorum aanwezig. Agendapunt 3: Bestemmingsplan Centrum 2026. Na uitgebreide beraadslaging wordt stemming uitgesteld tot 20 januari conform SO-motie van VVD-fractie. BESLUIT d.d. 20-01-2026: De raad stelt het bestemmingsplan Centrum Westerbork 2026 vast met 14 stemmen voor en 3 stemmen tegen. ACTIEPUNT: griffier publiceert besluit uiterlijk 22 januari 2026 op gemeentewebsite. Rondvraag: geen. Sluiting 21:45 door voorzitter.", - "approvedAt": "2026-01-22T10:15:00Z", - "signedBy": ["uid-hendriks-voorzitter", "uid-smit-griffier"], - "version": 3 -} -``` - -**2. Goedgekeurde commissienotulen — gemeente Emmen** -```json -{ - "@self": { - "register": "decidesk", - "schema": "minutes", - "slug": "minutes-emmen-commissie-ruimte-20260222" + { + "@self": { "register": "decidesk", "schema": "Minutes", "slug": "notulen-alv-buurtvereniging-zwolle-2025" }, + "title": "Notulen Algemene Ledenvergadering Buurtvereniging Assendorp Zwolle — 5 april 2025", + "lifecycle": "review", + "content": "Aanvang 19:30 uur. Aanwezig: 31 leden. Quorum (25 leden) behaald. Actie: secretaris stuurt uitnodiging nieuwe leden vóór 15 mei 2025. Agendapunt 4 — Wijziging statuten: het voorstel tot wijziging van artikel 12 lid 3 wordt aangenomen met 2/3 meerderheid (27 stemmen voor, 4 tegen). Taak: notaris ontvangt concept-statutenwijziging uiterlijk 1 juni 2025.", + "version": 1 }, - "title": "Notulen commissievergadering Ruimte & Wonen — gemeente Emmen — 22 februari 2026", - "lifecycle": "approved", - "content": "Opening om 14:00 door commissievoorzitter mevr. A. Veldhuis. Afwezig met kennisgeving: dhr. T. de Graaf (GroenLinks). Agendapunt 2: Conceptomgevingsvisie Emmen 2035. Inspreker: mevr. P. Koster (bewonersgroep Bargeres). Commissie is positief over de ingezette koers maar verzoekt aanvulling op paragraaf 4.2 (mobiliteit). ACTIEPUNT: dhr. G. de Boer (PvdA) brengt schriftelijke inbreng in vóór 7 maart 2026. Sluiting 16:30.", - "approvedAt": "2026-02-28T09:30:00Z", - "signedBy": ["uid-veldhuis-voorzitter"], - "version": 1 -} + { + "@self": { "register": "decidesk", "schema": "Minutes", "slug": "notulen-alv-woningcorporatie-haag-wonen-2025" }, + "title": "Notulen Huurdersraadsvergadering Haag Wonen — 22 april 2025", + "lifecycle": "draft", + "content": "Concept-notulen, nog niet vastgesteld. Actiepunt: huurcommissie onderzoekt mogelijkheden voor onderhoud lift Bezuidenhoutseweg 45 en rapporteert terug voor 1 juni 2025.", + "version": 1 + } +] ``` -**3. Ter beoordeling — Waterschap Vechtstromen** -```json -{ - "@self": { - "register": "decidesk", - "schema": "minutes", - "slug": "minutes-vechtstromen-ab-20260308" - }, - "title": "Notulen Algemeen Bestuur Waterschap Vechtstromen — 8 maart 2026", - "lifecycle": "submitted", - "content": "Vergadering geopend door dijkgraaf drs. R. van Loon om 10:00. Aanwezig: 30 van 30 leden. Agendapunt 5: Uitvoeringsprogramma dijkversterking Vecht 2026-2028. Technische toelichting door hoofd Waterveiligheid. Financiering: totaalbudget €4.200.000 ten laste van investeringsreserve. BESLUIT: het AB stemt in met het uitvoeringsprogramma dijkversterking Vecht 2026-2028 (unaniem). ACTIEPUNT: programmamanager Wilbrink stelt contracten aan voor aannemersselectie, deadline 1 mei 2026. Sluiting 12:45.", - "version": 2 -} -``` +### ActionItem — extracted from minutes (5 objects) -**4. Conceptnotulen ALV — VvE De Lindeboom** ```json -{ - "@self": { - "register": "decidesk", - "schema": "minutes", - "slug": "minutes-vve-lindeboom-alv-20260419" +[ + { + "@self": { "register": "decidesk", "schema": "ActionItem", "slug": "actie-alv-jaarverslag-versturen-laren" }, + "title": "Jaarverslag 2024 versturen aan alle leden", + "description": "Penningmeester stuurt het goedgekeurde jaarverslag 2024 naar alle 89 leden van Vereniging Dorpshuis Laren vóór 1 april 2025.", + "assignee": "Penningmeester", + "dueDate": "2025-04-01T00:00:00Z", + "taskStatus": "completed", + "completedAt": "2025-03-29T11:00:00Z" }, - "title": "Conceptnotulen ALV VvE De Lindeboom — 19 april 2026", - "lifecycle": "draft", - "content": "CONCEPT — ter vaststelling op volgende vergadering. Aanwezig: 28 van 44 eigenaren (quorum aanwezig). Voorzitter: dhr. K. Jansen (Bredestraat 12). Agendapunt 3: Onderhoudsfonds 2026. Besloten: bijdrage verhogen van €45 naar €60 per maand per appartement (meerderheid 21-7). ACTIEPUNT: penningmeester informeert hypotheekverstrekkers uiterlijk 1 juni. Rondvraag: klacht over rookoverlast trappenhuis — voorzitter bespreekt intern met beheerder. Sluiting 21:15.", - "version": 1 -} -``` - -**5. Conceptnotulen MT-vergadering — gemeente Assen** -```json -{ - "@self": { - "register": "decidesk", - "schema": "minutes", - "slug": "minutes-assen-mt-20260503" + { + "@self": { "register": "decidesk", "schema": "ActionItem", "slug": "actie-alv-uitnodiging-nieuwe-leden-zwolle" }, + "title": "Uitnodiging nieuwe leden versturen", + "description": "Secretaris stuurt uitnodiging voor ALV 2025 aan nieuwe leden vóór 15 mei 2025, conform besluit ALV 5 april 2025.", + "assignee": "Secretaris", + "dueDate": "2025-05-15T00:00:00Z", + "taskStatus": "open" + }, + { + "@self": { "register": "decidesk", "schema": "ActionItem", "slug": "actie-alv-statutenwijziging-notaris-zwolle" }, + "title": "Concept-statutenwijziging naar notaris sturen", + "description": "Notaris ontvangt concept-wijziging artikel 12 lid 3 uiterlijk 1 juni 2025 voor formele bekrachtiging.", + "assignee": "Voorzitter", + "dueDate": "2025-06-01T00:00:00Z", + "taskStatus": "in-progress" + }, + { + "@self": { "register": "decidesk", "schema": "ActionItem", "slug": "actie-lift-onderzoek-haag-wonen" }, + "title": "Onderzoek onderhoud lift Bezuidenhoutseweg 45", + "description": "Huurcommissie onderzoekt mogelijkheden voor lift-onderhoud en rapporteert terug aan huurdersraad vóór 1 juni 2025.", + "assignee": "Huurcommissie", + "dueDate": "2025-06-01T00:00:00Z", + "taskStatus": "open" }, - "title": "Notulen MT-vergadering gemeente Assen — 3 mei 2026", - "lifecycle": "draft", - "content": "Aanwezig: gemeentesecretaris T. Brouwer, directeur Sociale Zaken mevr. L. Haisma, directeur Ruimte dhr. F. van Dam, directeur Bedrijfsvoering mevr. C. Postma. Agendapunt 2: IT-vervanging kernsystemen. Businesscase nog onvoldoende onderbouwd. BESLUIT: besluit uitgesteld tot MT-vergadering 17 mei 2026. ACTIEPUNT: dhr. Van Dam levert herziene businesscase aan vóór 14 mei. Agendapunt 3: Reorganisatie afdeling HR. ACTIEPUNT: mevr. Postma stuurt conceptplan vóór 17 mei ter bespreking. Sluiting 14:30.", - "version": 1 -} + { + "@self": { "register": "decidesk", "schema": "ActionItem", "slug": "actie-besluit-notificatie-woningbouw-verzonden" }, + "title": "Besluitnotificatie Woningbouwplan Oost verzonden", + "description": "Automatische besluitnotificatie verzonden aan alle raadsleden na publicatie besluit Vaststelling Woningbouwplan Oost 2025-2030.", + "assignee": "Griffier", + "dueDate": "2025-03-22T00:00:00Z", + "taskStatus": "completed", + "completedAt": "2025-03-21T09:05:00Z" + } +] ``` ---- +## Risks / Trade-offs -### Motion (with decision outcome fields) — 4 seed objects +- **[Risk] Analytics performance degrades with very large ActionItem datasets** → Mitigation: `ActionItemAnalyticsService` applies date-range filters (default: current year) to limit query scope; the service accepts `limit` and `dateFrom` parameters; dashboard displays a "Beperkt tot {year}" label when the filter is active +- **[Risk] Action item extraction regex produces false positives on formatted text** → Mitigation: the extraction preview modal always appears before saving; the secretary can uncheck, edit title, or assign each candidate; no action items are saved without explicit confirmation +- **[Risk] ALV distribution sends notifications to all GovernanceBody participants, including inactive members** → Mitigation: `ALVMinutesService::distribute()` filters by `Participant.leftAt == null` (active members only); a preview step shows the recipient list before dispatching +- **[Risk] Approval request notifications sent to all chair/secretary roles may create noise in large bodies** → Mitigation: notification content is concise (title + deep link + one action button); the `decision_notify_roles` config allows restriction to specific roles; notification frequency is bounded by the number of Minutes objects submitted for approval +- **[Trade-off] Rationale stored in notes array, not a dedicated field** → Acceptable for v1 (avoids breaking schema change); notes are already displayed in the sidebar; the label `overwegingen` makes the note identifiable; a dedicated field can be added in a future ADR-000 revision when demand justifies the breaking change +- **[Trade-off] Live decision panel only available when `lifecycle: opened`** → Intentional; prevents test entries from being recorded outside an active meeting; the secretary can manually set lifecycle via the existing lifecycle transition controls -**1. Aangenomen bestemmingsplanmotion — gemeente Westerbork** -```json -{ - "@self": { - "register": "decidesk", - "schema": "motion", - "slug": "motion-westerbork-bestemmingsplan-centrum-2026" - }, - "title": "Vaststelling bestemmingsplan Centrum Westerbork 2026", - "text": "De raad van de gemeente Westerbork stelt het bestemmingsplan Centrum Westerbork 2026 vast, zoals opgenomen in het raadsvoorstel d.d. 5 januari 2026 met bijbehorende plankaart (planidentificatie NL.IMRO.1952.BPCentrum2026-VA01).", - "motionType": "motion", - "proposer": "College van B&W", - "lifecycle": "adopted", - "submittedAt": "2026-01-05T00:00:00Z", - "requirement": "simple-majority", - "decisionText": "De raad stelt het bestemmingsplan Centrum Westerbork 2026 vast conform planidentificatie NL.IMRO.1952.BPCentrum2026-VA01.", - "decisionDate": "2026-01-20T20:15:00Z", - "isPublished": true, - "publishedAt": "2026-01-22T10:30:00Z", - "legalBasis": "Wet ruimtelijke ordening artikel 3.1" -} -``` +## Migration Plan -**2. Aangenomen begrotingswijziging — Waterschap Vechtstromen** -```json -{ - "@self": { - "register": "decidesk", - "schema": "motion", - "slug": "motion-vechtstromen-dijkversterking-2026" - }, - "title": "Uitvoeringsprogramma dijkversterking Vecht 2026-2028", - "text": "Het Algemeen Bestuur stemt in met het uitvoeringsprogramma dijkversterking Vecht 2026-2028 en machtigt het Dagelijks Bestuur tot het aangaan van verplichtingen tot een maximum van €4.200.000 ten laste van de investeringsreserve waterveiligheid.", - "motionType": "motion", - "proposer": "Dagelijks Bestuur", - "lifecycle": "adopted", - "submittedAt": "2026-02-15T00:00:00Z", - "requirement": "simple-majority", - "decisionText": "Het AB stelt het uitvoeringsprogramma dijkversterking Vecht 2026-2028 vast en machtigt het DB tot besteding van €4.200.000.", - "decisionDate": "2026-03-08T11:30:00Z", - "isPublished": true, - "publishedAt": "2026-03-10T09:00:00Z", - "legalBasis": "Waterschapswet artikel 77" -} -``` +1. No schema changes — Decision, Minutes, ActionItem, Meeting all unchanged from ADR-000 +2. Add `ActionItemAnalyticsService.php`, `ALVMinutesService.php`, `ActionItemExtractionService.php`, `DecisionNotificationService.php` +3. Extend `DecisionService.php` (T1) with `notifyOnPublish()` hook; register call after `isPublished: true` save +4. Add new API routes in `appinfo/routes.php`: `GET /api/analytics/action-items`, `POST /api/minutes/{id}/generate-alv`, `POST /api/minutes/{id}/distribute`, `POST /api/minutes/{id}/submit-for-approval`, `POST /api/minutes/{id}/extract-action-items` +5. Frontend: add `ActionItemAnalyticsWidget.vue`, `LiveDecisionPanel.vue`, `ALVMinutesActions.vue`, `ActionItemExtractionModal.vue`; extend `DecisionDetail.vue`, `MinutesDetail.vue`, `MeetingDetail.vue` +6. Add seed data for ALV Minutes (3 objects) and extracted ActionItems (5 objects) via repair step +7. Add translation keys for all new strings in `l10n/nl.json` and `l10n/en.json` -**3. Verworpen motie sociale woningbouw — gemeente Emmen** -```json -{ - "@self": { - "register": "decidesk", - "schema": "motion", - "slug": "motion-emmen-sociale-woningbouw-2026" - }, - "title": "Motie: versnelling sociale woningbouw Emmerhout", - "text": "De raad verzoekt het college van B&W om in de Omgevingsvisie Emmen 2035 een aparte paragraaf op te nemen over de versnelling van sociale woningbouw in de wijk Emmerhout, met concrete doelstellingen voor 2028.", - "motionType": "motion", - "proposer": "PvdA-fractie", - "coSigners": ["SP-fractie", "GroenLinks-fractie"], - "lifecycle": "rejected", - "submittedAt": "2026-02-22T14:00:00Z", - "requirement": "simple-majority", - "isPublished": false -} -``` +## Open Questions -**4. Aangenomen jaarplan VvE — VvE De Lindeboom** -```json -{ - "@self": { - "register": "decidesk", - "schema": "motion", - "slug": "motion-vve-lindeboom-onderhoudsfonds-2026" - }, - "title": "Verhoging onderhoudsbijdrage VvE De Lindeboom 2026", - "text": "De ALV besluit de maandelijkse bijdrage aan het onderhoudsfonds te verhogen van €45 naar €60 per appartement per maand met ingang van 1 juli 2026, conform het meerjarenonderhoudsplan opgesteld door Vastgoedbeheer Havinga B.V.", - "motionType": "motion", - "proposer": "Bestuur VvE De Lindeboom", - "lifecycle": "adopted", - "submittedAt": "2026-04-01T00:00:00Z", - "requirement": "simple-majority", - "decisionText": "De bijdrage aan het onderhoudsfonds wordt verhoogd van €45 naar €60 per appartement per maand met ingang van 1 juli 2026.", - "decisionDate": "2026-04-19T20:45:00Z", - "isPublished": false -} -``` +- Should the analytics date range be configurable per user, or fixed to "current calendar year" for all users? (Recommendation: current year as default with a date-range picker in the analytics panel; user preference stored in frontend `localStorage`) +- Should ALV minutes distribution include a PDF attachment of the signed minutes, or only a link? (Recommendation: link only for v1; PDF attachment requires `FileService` export pipeline which can be added in a future sprint alongside e-signing) +- Should the `decision_notify_roles` configuration be per-GovernanceBody or global? (Recommendation: global default with per-GovernanceBody override in the GovernanceBody settings panel; defer per-body config to p3-governance-bodies) +- Should the action item extraction also suggest an `assignee` by detecting names in the matched text? (Recommendation: yes, using simple name detection against the known Participant list; surfaced as a suggestion in the extraction modal with a dropdown) diff --git a/openspec/changes/p2-minutes-and-decisions-core-t3/hydra.json b/openspec/changes/p2-minutes-and-decisions-core-t3/hydra.json index 2996c83a..b9b766ba 100644 --- a/openspec/changes/p2-minutes-and-decisions-core-t3/hydra.json +++ b/openspec/changes/p2-minutes-and-decisions-core-t3/hydra.json @@ -6,5 +6,5 @@ "depends_on": [ "p2-minutes-and-decisions" ], - "issue": "https://github.com/ConductionNL/decidesk/issues/74" + "issue": null } \ No newline at end of file diff --git a/openspec/changes/p2-minutes-and-decisions-core-t3/proposal.md b/openspec/changes/p2-minutes-and-decisions-core-t3/proposal.md index 87d2079d..ae5fa25f 100644 --- a/openspec/changes/p2-minutes-and-decisions-core-t3/proposal.md +++ b/openspec/changes/p2-minutes-and-decisions-core-t3/proposal.md @@ -1,43 +1,52 @@ -# Proposal: Minutes and Decisions — Core T3 + ## Why -The p2-minutes-and-decisions capability established the foundational data model and CRUD operations for meeting minutes and decisions. However, real-world governance workflows require capabilities that go beyond basic record-keeping: decision-makers need to record decisions and action items **in real-time during meetings**, automatically generate action items from minutes, publish decisions for public access via the ORI standard, and ensure decisions are discoverable and linked across governance processes. +Governance bodies — from municipal councils and water boards to corporate boards and associations — depend on efficient day-to-day meeting workflows beyond the document-generation and compliance features delivered in T1. Market research across the p2-minutes-and-decisions spec reveals a second tier of high-demand operational capabilities: multi-meeting action item analytics (demand 94), live decision recording during active meetings (demand 73), ALV (Algemene Ledenvergadering / General Assembly) minutes draft and distribution (demand 64), formal minutes approval submission with notifications (demand 60), automatic action item extraction from minutes text (demand 55), structured decision rationale documentation (demand 48), and decision notification dispatch to stakeholders (demand 9). -Core T3 addresses the operational workflows that turn minutes and decisions into actionable outcomes for governance bodies across all five governance domains (legislatures, associations, corporate boards, operational teams, citizen participation). +The Board Secretary / Company Secretary is the primary persona: they need an analytics overview of open and overdue action items across multiple meetings, a live-entry panel to record decisions as they happen, an ALV-specific minutes template that satisfies association law requirements, a one-click approval request that notifies the chair and relevant members, and an automated step that converts meeting decisions into trackable action items. The CEO / Director needs decision notifications so that adopted resolutions reach accountable parties without manual email follow-up. The minutes clerk needs structured rationale fields so that the "why" behind each decision is captured at the source and auditable later. + +This change delivers these seven operational extensions on top of the existing p2-minutes-and-decisions and T1 foundation: multi-meeting analytics dashboard, live meeting decision entry panel, ALV minutes template and member distribution, minutes approval request notifications, auto-extract action items from minutes content, decision rationale capture panel, and decision notification dispatch. ## What Changes -- **Real-time decision recording**: Decisions and action items are captured and persisted during active meetings, with immediate availability for publication or export -- **Action item automation**: ActionItems are automatically generated from Minutes content using configurable extraction rules -- **Decision publication**: Decisions are published via the ORI-compatible API endpoint for public access and standards compliance -- **Minutes lifecycle management**: Formal approval workflows with digital signatures (chair + secretary), version tracking, and lifecycle state transitions -- **ORI API serialization**: Minutes, Decisions, and related entities are exposed via `/api/ori/v1/reports` and related endpoints with proper field mapping -- **Decision notifications**: Stakeholders are notified when decisions affecting them are published (future integration point) -- **WCAG 2.1 AA compliance** for all voting interfaces, decision recording UI, and minutes review screens -- **Seed data** for Minutes, Decision, ActionItem objects to support testing and QA workflows +- **New**: Multi-meeting action item analytics panel on the Dashboard — `CnStatsBlock` KPIs (total open, overdue, completed this month, average days-to-close) computed via `ActionItemAnalyticsService`; a bar chart (`CnChartWidget`) shows per-meeting completion rates for the last 6 meetings; a "My Action Items" list groups overdue, due-this-week, and future items for the current user +- **New**: Live decision recording panel on the Meeting detail page — a "Decisions" tab that opens during an active meeting (`lifecycle: opened`) allowing the secretary to record decisions in real time with a quick-entry form; each saved entry creates a Decision object linked to the Meeting and auto-creates a draft Minutes entry if none exists +- **New**: ALV minutes template and member distribution — a "Genereer ALV-notulen" action on the Minutes detail page for Minutes linked to a meeting with `meetingType` containing `alv`; renders an ALV-specific Dutch template (date, quorum confirmation, agenda items, resolutions, AOB); a "Distribueren" action sends the approved minutes as a Nextcloud notification and optional email to all Participants of the linked GovernanceBody +- **New**: Minutes approval request with notifications — a "Ter goedkeuring indienen" button on a Minutes object in `draft` state transitions the lifecycle to `review` and sends Nextcloud notifications to all users with role `chair` or `secretary` in the linked GovernanceBody; the notification contains the minutes title, a deep link to the detail page, and an "Goedkeuren" shortcut +- **New**: Auto-extract action items from minutes text — a "Actiepunten extraheren" button on the Minutes detail page (available in `draft`, `review`, `approved` states) calls `ActionItemExtractionService::extractFromContent()` which parses the `content` field for action-item markers (lines starting with "Actie:", "AI:", "Taak:", or containing "wordt verzocht" / "zal") and presents a preview list of suggested ActionItem objects for the secretary to accept, edit, or reject before saving +- **New**: Decision rationale capture — a "Overwegingen" section on the Decision detail and edit form backed by a `rationale` rich-text field stored in the Decision `notes` array via OpenRegister built-in notes; the section is labelled "Overwegingen en motivering" and supports plain-text input; displayed on the Decision detail page and included in the minutes generation template +- **New**: Decision notification dispatch — a configurable `DecisionNotificationService` that sends Nextcloud notifications to configured recipients (role-based: chair, secretary, member) when a Decision's `isPublished` transitions from `false` to `true`; notification includes decision title, outcome, and a deep link; dispatched automatically as part of the existing publication action in `DecisionService` ## Capabilities ### New Capabilities -- `minutes-real-time-capture`: Capture minutes and decisions during active meetings with real-time persistence and publication readiness -- `decision-publication-ori`: Publish decisions via ORI-compatible API endpoint for public access and inter-system integration -- `action-item-automation`: Automatically extract and create ActionItems from Minutes content using configurable rules -- `minutes-approval-workflow`: Formal Minutes approval process with digital signatures (chair + secretary), version tracking, and lifecycle states (draft → submitted → approved → published) -- `decision-discovery`: Full-text search, filtering, and discovery of decisions across governance bodies and time periods -- `decision-notifications`: Notify stakeholders when decisions affecting their interests are published (foundation for external integrations) +- `action-item-analytics`: Multi-meeting analytics for action items — KPI cards, per-meeting completion rate chart, and personal "My Action Items" grouping; powered by `ActionItemAnalyticsService` querying via `ObjectService.findAll()`; rendered with `CnStatsBlock`, `CnChartWidget`, and `CnObjectDataWidget` on the Dashboard +- `live-decision-recording`: Real-time decision entry during active meetings — a "Besluiten" tab on the Meeting detail page; quick-entry form creates linked Decision objects and auto-initialises draft Minutes; enabled only when `lifecycle: opened` +- `alv-minutes-template`: ALV-specific minutes template generation via `ALVMinutesService::generateALVDraft()`; Dutch quorum and resolution template; member distribution via `NotificationService` and optional Nextcloud email +- `minutes-approval-notifications`: Formal approval request workflow — `lifecycle` transition `draft → review` triggers Nextcloud notifications to chair and secretary roles via `NotificationService`; approval and rejection notifications on subsequent transitions +- `auto-action-item-extraction`: NLP-free extraction of action item candidates from Minutes `content` via `ActionItemExtractionService`; regex-based marker detection; preview-and-confirm modal before creating ActionItem objects; linked to parent Minutes via OpenRegister relation +- `decision-rationale`: Structured rationale capture using the OpenRegister built-in `notes` array with label `overwegingen`; displayed in the "Overwegingen" section on Decision detail; included in minutes generation template output +- `decision-notification`: Automatic notification dispatch on decision publication via `DecisionNotificationService`; configurable recipients by role; integrates with existing `decision-publication` capability from p2-minutes-and-decisions ### Modified Capabilities -- `minutes-and-decisions`: Requirements for Minutes entity expanded to include real-time capture, approval workflow, version tracking, and formal signature support. Decision entity now includes publication state and ORI mapping. +- `decision-publication` *(from p2-minutes-and-decisions)*: Extended to trigger `DecisionNotificationService::notifyOnPublish()` after `isPublished` is set to `true`; notification dispatch is non-blocking (background activity) +- `minutes-generation` *(from p2-minutes-and-decisions)*: Extended to include `overwegingen` content from Decision notes in the generated minutes draft template; ALV-specific template branch added +- `app-dashboard` *(from p1-dashboard-and-navigation)*: Extended with action item analytics panel — `ActionItemAnalyticsWidget` replaces the simple open-count KPI card with full analytics breakdown and chart +- `p2-minutes-and-decisions`: extended by `p2-minutes-and-decisions-core-t3` — adds configuration, workflow, or seed data + ## Impact -- **Entities**: Minutes and Decision (ADR-000) — properties expanded to support lifecycle, signatures, version tracking, and ORI publication -- **Data Layer**: OpenRegister schemas updated to support new properties; migration required for existing Minutes/Decision objects -- **API**: New `/api/ori/v1/reports` endpoint for Minutes; expanded `/api/ori/v1/*` endpoints for Decision publication -- **Frontend**: New components for real-time decision capture during meetings, Minutes approval UI, decision search and filtering -- **Backend**: Decision extraction engine, workflow state machine for Minutes approval, ORI serialization layer -- **Testing**: WCAG 2.1 AA compliance testing on voting interfaces, Minutes UI, Decision publishing workflows; OpenRegister schema validation; ORI API format verification -- **Governance Domains**: Impacts all five domains — legislatures publish decisions for public records, associations track ALV decisions with formal approval, corporate boards maintain certified minutes, operational teams track action items, citizen participation platforms publish referenda/initiative decisions +- Adds `ActionItemAnalyticsService.php` (cross-meeting analytics queries), `ALVMinutesService.php` (ALV template rendering and distribution), `ActionItemExtractionService.php` (content parsing for action items), and `DecisionNotificationService.php` (notification dispatch on publication) +- No schema changes — Decision, Minutes, ActionItem, and Meeting are defined in ADR-000; `rationale` uses the built-in `notes` array; no new entity is introduced +- Frontend: adds `ActionItemAnalyticsWidget.vue` (dashboard), `LiveDecisionPanel.vue` (meeting detail tab), `ALVMinutesActions.vue` (minutes detail), and `ActionItemExtractionModal.vue` (minutes detail) +- Extends existing `DecisionDetail.vue` with the "Overwegingen" section; extends `MinutesDetail.vue` with ALV actions and "Ter goedkeuring indienen" button +- Extends `DecisionService.php` from T1 with `notifyOnPublish()` hook +- Downstream: p3-ori-publication can read the rationale from Decision notes when publishing to PLOOI +- Downstream: p3-governance-bodies can filter action item analytics by GovernanceBody domain diff --git a/openspec/changes/p2-minutes-and-decisions-core-t3/specs.md b/openspec/changes/p2-minutes-and-decisions-core-t3/specs.md new file mode 100644 index 00000000..d951f6b6 --- /dev/null +++ b/openspec/changes/p2-minutes-and-decisions-core-t3/specs.md @@ -0,0 +1,303 @@ +# Specs: Minutes and Decisions — Core T3 + +**Change:** p2-minutes-and-decisions-core-t3 +**App:** Decidesk +**Entities:** Minutes, Decision, ActionItem, Meeting + +--- + +## REQ-MAA: Multi-Meeting Action Item Analytics + +### REQ-MAA-001 — Display action item analytics KPIs on the Dashboard +A user can see a summary of action item status across all meetings on the Dashboard. + +**GIVEN** the Dashboard page is open +**WHEN** the `ActionItemAnalyticsWidget` loads +**THEN** `ActionItemAnalyticsService::getSummary()` is called and returns: total open action items, total overdue items, items completed this month, and average days-to-close for the current calendar year +**AND** these four values are displayed as `CnStatsBlock` KPI cards in the analytics panel +**AND** the panel header shows the active date range ("Cijfers voor {jaar}") + +### REQ-MAA-002 — Display per-meeting completion rate chart +A user can see the action item completion rate for recent meetings in a bar chart. + +**GIVEN** the Dashboard page is open +**WHEN** the analytics panel renders the chart section +**THEN** `ActionItemAnalyticsService::getCompletionRates(limit: 6)` is called +**AND** a `CnChartWidget` bar chart displays the last 6 meetings with: x-axis = meeting title (truncated), y-axis = completion percentage (completed / total × 100) +**AND** meetings with zero action items are shown with a 0% bar and a tooltip "Geen actiepunten" + +### REQ-MAA-003 — Display personal "My Action Items" grouped list +A logged-in user can see their own open action items grouped by urgency on the Dashboard. + +**GIVEN** the Dashboard page is open and the current user is logged in +**WHEN** the personal list section renders +**THEN** `ActionItemAnalyticsService::getMyItems(userId)` returns all ActionItems where `assignee` matches the current user's display name and `taskStatus` is not `completed` +**AND** items are grouped: "Achterstallig" (overdue), "Deze week" (dueDate within 7 days), "Later" +**AND** each item shows: title, linked meeting name, dueDate, and a `CnStatusBadge` for `taskStatus` +**AND** the "Achterstallig" group uses the `--color-error` Nextcloud CSS variable for the group header (no hardcoded colours) + +### REQ-MAA-004 — Navigate to action item detail from analytics list +A user can navigate from an analytics list item to the full ActionItem detail. + +**GIVEN** the "My Action Items" list on the Dashboard is visible with at least one entry +**WHEN** the user clicks on an action item row +**THEN** the router navigates to `ActionItemDetail` with the item's `id` as route parameter +**AND** the browser URL updates to `/apps/decidesk/action-items/{id}` + +--- + +## REQ-LDR: Live Decision Recording + +### REQ-LDR-001 — Record a decision in real time during an active meeting +A secretary can create a Decision object directly from the Meeting detail page while the meeting is active. + +**GIVEN** a Meeting detail page is open with `lifecycle: opened` +**WHEN** the user clicks the "Besluiten" tab on the Meeting detail page +**THEN** a `LiveDecisionPanel.vue` is rendered showing a quick-entry form with fields: title (required), text (required), outcome (adopted/rejected, required), legalBasis (optional) +**AND** a "Opslaan" button saves the Decision via `ObjectService.saveObject()` with a relation to the parent Meeting +**AND** on success the new Decision appears in the panel's list immediately + +**GIVEN** the current user submits the quick-entry form with an empty `title` +**WHEN** "Opslaan" is clicked +**THEN** an inline validation error "Titel is verplicht" is shown +**AND** no save operation is performed + +### REQ-LDR-002 — Auto-create draft Minutes when the first decision is recorded +When the first decision is recorded in a meeting, a draft Minutes object is automatically created. + +**GIVEN** a Meeting in `lifecycle: opened` has no linked Minutes object +**WHEN** the secretary saves the first Decision via the live panel +**THEN** a new Minutes object is created with: + - `title: "Concept notulen — {meeting.title}"` + - `lifecycle: draft` + - `version: 1` +**AND** an OpenRegister relation is created from the Minutes to the Meeting +**AND** a success notification displays "Conceptnotulen aangemaakt voor deze vergadering" + +**GIVEN** a Meeting already has a linked Minutes object +**WHEN** the secretary saves another Decision via the live panel +**THEN** no additional Minutes object is created +**AND** the new Decision is linked to the existing Minutes via OpenRegister relation + +### REQ-LDR-003 — Live panel is disabled for non-active meetings +The live decision panel is not available when the meeting is not in the opened state. + +**GIVEN** a Meeting detail page is open with `lifecycle` set to `draft`, `scheduled`, `paused`, `adjourned`, or `closed` +**WHEN** the user views the "Besluiten" tab +**THEN** the quick-entry form is replaced with a read-only list of linked Decisions +**AND** a status notice is shown: "Live invoer beschikbaar wanneer de vergadering geopend is" +**AND** no "Opslaan" button is visible + +--- + +## REQ-ALV: ALV Minutes Template and Distribution + +### REQ-ALV-001 — Generate ALV minutes from Dutch general assembly template +A secretary can generate a structured Dutch ALV minutes draft for a General Assembly meeting. + +**GIVEN** a Minutes detail page is open and the linked Meeting has `meetingType` containing `alv` or `algemene-ledenvergadering` +**WHEN** the user clicks "Genereer ALV-notulen" +**THEN** `ALVMinutesService::generateALVDraft(minutesId)` is called +**AND** the service fetches: the linked Meeting (title, scheduledDate, quorumRequired), AgendaItems ordered by `orderNumber`, Participants of the linked GovernanceBody, and any linked Decisions +**AND** the generated Dutch ALV template is populated with: opening (datum, aanvangstijd, voorzitter), quorum statement ("X van Y leden aanwezig — quorum {behaald|niet behaald}"), agenda items as sections, adopted resolutions with vote totals, and a "Rondvraag en sluiting" section +**AND** a preview dialog opens showing the generated content before applying +**AND** on user confirmation, the Minutes `content` field is updated via `ObjectService.saveObject()` + +**GIVEN** the linked Meeting has `meetingType` that does not contain `alv` or `algemene-ledenvergadering` +**WHEN** the user clicks "Genereer ALV-notulen" +**THEN** a validation notice is shown: "Deze vergadering is niet gekwalificeerd als ALV — gebruik 'Concept genereren' voor standaard notulen" +**AND** no content is generated or applied + +### REQ-ALV-002 — Distribute approved ALV minutes to members +A secretary can distribute approved Minutes to all active members of the GovernanceBody via Nextcloud notifications. + +**GIVEN** a Minutes detail page is open with `lifecycle: approved` or `lifecycle: signed` +**WHEN** the user clicks "Distribueren aan leden" +**THEN** `ALVMinutesService::distribute(minutesId)` is called +**AND** a preview dialog shows the recipient list: all Participants of the linked GovernanceBody where `leftAt` is null +**AND** on user confirmation, a Nextcloud notification is sent to each recipient with: subject "{meeting title} — {minutes title}", body "De notulen zijn beschikbaar. Klik hier om ze te bekijken.", and a deep link to the Minutes detail page +**AND** a success notification shows "Notulen verzonden aan {N} leden" + +**GIVEN** the linked GovernanceBody has no active Participants +**WHEN** the distribution preview dialog opens +**THEN** a warning is shown: "Geen actieve leden gevonden — controleer de ledenlijst van dit bestuur" +**AND** the confirm button is disabled + +### REQ-ALV-003 — Distribution blocked for unapproved minutes +Minutes in `draft` or `review` state cannot be distributed. + +**GIVEN** a Minutes detail page is open with `lifecycle: draft` or `lifecycle: review` +**WHEN** the user views the Minutes action buttons +**THEN** the "Distribueren aan leden" button is not visible or is visibly disabled +**AND** a tooltip or inline notice explains: "Notulen moeten goedgekeurd zijn voor distributie" + +--- + +## REQ-MAW: Minutes Approval Workflow Notifications + +### REQ-MAW-001 — Submit minutes for approval and notify approvers +A secretary can formally submit draft minutes for approval with automatic notifications. + +**GIVEN** a Minutes detail page is open with `lifecycle: draft` +**WHEN** the user clicks "Ter goedkeuring indienen" +**THEN** the `lifecycle` is transitioned from `draft` to `review` via `WorkflowEngineController` +**AND** `NotificationService` sends notifications to all users with active Membership `role` of `chair` or `secretary` in the linked GovernanceBody +**AND** each notification contains: title "Notulen ter goedkeuring: {minutes.title}", body "Klik om de notulen te bekijken en goed te keuren.", and a deep link to the Minutes detail page +**AND** the button label changes to "In behandeling" (disabled) after the transition + +**GIVEN** no GovernanceBody is linked to the Minutes' parent Meeting +**WHEN** "Ter goedkeuring indienen" is clicked +**THEN** the lifecycle transitions to `review` +**AND** a warning notification is shown to the current user: "Geen bestuur gekoppeld — goedkeurers konden niet automatisch worden gewaarschuwd" + +### REQ-MAW-002 — Notify submitter when minutes are approved or rejected +The secretary is notified when the chair or secretary approves or rejects the submitted minutes. + +**GIVEN** a Minutes object is in `lifecycle: review` +**WHEN** a user with role `chair` or `secretary` transitions the lifecycle to `approved` +**THEN** a Nextcloud notification is sent to the user who last set `lifecycle: review` (identified from the audit trail) with message "Notulen goedgekeurd: {minutes.title}" +**AND** the `approvedAt` field is set to the current timestamp +**AND** the approving user's display name is appended to the `signedBy` array + +**GIVEN** a Minutes object is in `lifecycle: review` +**WHEN** a user with role `chair` or `secretary` transitions the lifecycle back to `draft` (rejection) +**THEN** a Nextcloud notification is sent to the submitter with message "Notulen teruggestuurd: {minutes.title} — zie notities voor toelichting" + +### REQ-MAW-003 — Approval action is restricted to chair and secretary roles +Only authorised users can approve or reject submitted minutes. + +**GIVEN** a Minutes detail page is open with `lifecycle: review` +**WHEN** the current user does NOT have role `chair` or `secretary` in the linked GovernanceBody +**THEN** the "Goedkeuren" and "Terugsturen" action buttons are not visible +**AND** a read-only banner is shown: "In behandeling — wacht op goedkeuring door voorzitter of secretaris" + +--- + +## REQ-AAI: Auto-Extract Action Items from Minutes + +### REQ-AAI-001 — Extract action item candidates from minutes content +A secretary can automatically extract action item candidates from the minutes text. + +**GIVEN** a Minutes detail page is open with `lifecycle: draft`, `review`, or `approved` +**WHEN** the user clicks "Actiepunten extraheren" +**THEN** `ActionItemExtractionService::extractFromContent(minutesContent)` is called +**AND** the service parses the `content` string for lines matching patterns: starting with `Actie:`, `AI:`, `Taak:`, `Actiepunt:`, or containing `wordt verzocht`, `zal worden`, `is toegezegd` +**AND** a preview modal opens listing each candidate with: extracted title (editable), suggested assignee (if a known Participant name is detected in the line), suggested dueDate (empty by default), and a checkbox (checked by default) +**AND** unchecked candidates are not saved + +**GIVEN** the minutes `content` contains no action item markers +**WHEN** the user clicks "Actiepunten extraheren" +**THEN** a notice is shown: "Geen actiepunten gevonden in de notulen — gebruik de standaard CRUD om actiepunten handmatig toe te voegen" +**AND** no modal opens + +### REQ-AAI-002 — Save confirmed action items linked to the Minutes +Confirmed action item candidates are saved and linked to the parent Minutes. + +**GIVEN** the extraction preview modal is open with at least one candidate checked +**WHEN** the user clicks "Geselecteerde actiepunten opslaan" +**THEN** for each checked candidate, an ActionItem is created via `ObjectService.saveObject()` with: `title` from the edited candidate, `assignee` (if provided), `dueDate` (if provided), `taskStatus: open` +**AND** each ActionItem is linked to the parent Minutes via OpenRegister relation +**AND** a success notification shows "{N} actiepun{ten|t} aangemaakt" +**AND** the modal closes + +### REQ-AAI-003 — Extracted action items appear in the Minutes linked items section +Newly created action items are visible on the Minutes detail page. + +**GIVEN** action items have been extracted and saved for a Minutes object +**WHEN** the user views the "Actiepunten" section on the Minutes detail page +**THEN** all linked ActionItems are listed with: title, assignee, dueDate, and `taskStatus` badge +**AND** overdue items display the `overdue` status badge (red using `--color-error`) + +--- + +## REQ-DRT: Decision Rationale Documentation + +### REQ-DRT-001 — Capture decision rationale in the Overwegingen section +A user can record the reasoning and considerations behind a formal decision. + +**GIVEN** a Decision detail page is open in edit mode +**WHEN** the user opens the "Overwegingen en motivering" section +**THEN** a plain-text input field is shown labelled "Overwegingen" +**AND** saving the value creates or updates a note in the Decision's `notes` array with `label: "overwegingen"` via `ObjectService.saveObject()` +**AND** the note is stored as plain text within the built-in notes structure + +**GIVEN** a Decision detail page is open in view mode and the Decision has a note with `label: "overwegingen"` +**WHEN** the user views the "Overwegingen en motivering" section +**THEN** the rationale text is displayed in a `CnDetailCard` section labelled "Overwegingen en motivering" +**AND** the section is collapsed by default if the rationale is empty + +### REQ-DRT-002 — Decision rationale is included in generated minutes +When minutes are generated, the decision rationale is included in the output. + +**GIVEN** a Minutes object is linked to a Meeting that has Decisions with rationale notes +**WHEN** `MinutesGenerationService::generateDraft()` is called +**THEN** for each Decision included in the generated minutes, the `overwegingen` note (if present) is appended below the decision text as "Overwegingen: {rationale}" +**AND** if no rationale note exists for a Decision, the overwegingen section is omitted for that decision + +### REQ-DRT-003 — Rationale is included in ALV minutes template +ALV minutes generation includes the rationale for each adopted resolution. + +**GIVEN** `ALVMinutesService::generateALVDraft()` is called and the meeting has Decisions with rationale notes +**WHEN** the ALV template renders each agenda item's resolution +**THEN** each adopted resolution includes "Overwegingen:" followed by the rationale text if a `notes` entry with `label: "overwegingen"` exists on the linked Decision +**AND** rejected motions do not include an overwegingen section + +--- + +## REQ-DNT: Decision Notification Dispatch + +### REQ-DNT-001 — Notify configured recipients when a decision is published +Stakeholders receive a Nextcloud notification when an adopted decision is published. + +**GIVEN** a Decision has `isPublished: false` and a user clicks "Publiceren" (from p2-minutes-and-decisions) +**WHEN** `DecisionService` sets `isPublished: true` and saves via `ObjectService.saveObject()` +**THEN** `DecisionNotificationService::notifyOnPublish(decisionId)` is called +**AND** recipients are resolved from Memberships of the linked GovernanceBody with `role` in the `decision_notify_roles` config (default: `chair`, `secretary`, `member`) +**AND** each resolved recipient receives a Nextcloud notification with: title "Besluit gepubliceerd: {decision.title}", body "{outcome} — {decisionDate}", and a deep link to the Decision detail page +**AND** if the GovernanceBody has no linked Memberships, the notification is not dispatched and a warning is logged + +### REQ-DNT-002 — Notification recipients are configurable +An administrator can configure which roles receive decision publication notifications. + +**GIVEN** the admin settings page is open under "Besluitnotificaties" +**WHEN** the administrator selects or deselects roles from the `decision_notify_roles` multi-select (options: chair, secretary, member, observer) +**AND** saves via `POST /api/settings` +**THEN** the `decision_notify_roles` value is saved via `IAppConfig` +**AND** subsequent decision publications use the updated role set + +### REQ-DNT-003 — Notification is not sent for rejected or non-published decisions +The notification is only triggered for the explicit publish action. + +**GIVEN** a Decision has `outcome: rejected` +**WHEN** any user views or edits the Decision +**THEN** no publication notification is triggered (rejected decisions are never published) + +**GIVEN** a Decision has `isPublished: false` and is updated (not published) +**WHEN** `ObjectService.saveObject()` is called for a non-publication change (e.g. title edit) +**THEN** no notification is dispatched +**AND** `DecisionNotificationService::notifyOnPublish()` is NOT called + +--- + +## Non-Functional Requirements + +### REQ-NFR-001 — Accessibility (ADR-010) +All new views, panels, modals, and widgets introduced by this change MUST meet WCAG 2.1 AA: keyboard-navigable, form fields labelled with `aria-label` or `