diff --git a/.superpowers/sdd/2026-08-20-plan-xer-p6-lezer/task-X6-wiring-report.md b/.superpowers/sdd/2026-08-20-plan-xer-p6-lezer/task-X6-wiring-report.md new file mode 100644 index 000000000..c0d30445c --- /dev/null +++ b/.superpowers/sdd/2026-08-20-plan-xer-p6-lezer/task-X6-wiring-report.md @@ -0,0 +1,130 @@ +# X6 — resources en toewijzingen + +## Afbakening + +Deze wijziging bedraadt uitsluitend de actuele X6-kern in de XER-leesroute. RSRC, RSRCRATE, +ROLERATE, RSRCCURVDATA en TASKRSRC worden één keer uit de geparste tabelset gelezen. X4b- +meerdocumentselectie en baseline-uitsluiting blijven ongewijzigd; X5 SCHEDOPTIONS blijft +ongewijzigd. Er is geen X8-wiring en er is geen X9-IFC-, recovery- of exportserialisatieclaim. +De retained XER-metadata is uitsluitend in-memory bronbewijs voor een latere X9-overdracht. + +## Gewijzigde bestanden + +- `src/services/xer/xerResourceTypes.ts`: retained bronvormen, fouten en read-context. +- `src/services/xer/xerResourceCurves.ts`: 21 rauwe curvepunten en niet-destructieve best-fit. +- `src/services/xer/xerResourceAssignments.ts`: lineaire TASKRSRC-partitionering en projectview. +- `src/services/xer/xerResources.ts`: bestandsbrede resource-/rol-/ratecatalogus en mutable + projectmaterialisatie. +- `src/services/xer/xerReader.ts`: catalogus/index éénmaal maken en per X4b-project projecteren. +- `src/services/xer/xerTables.ts`: rows en cells na parsing runtime-bevriezen. +- `src/services/xer/xerMultiProject.ts`: catalogus/raw rows referentie-identiek herstellen na + de bestaande mutable projectclone. +- `src/services/importTypes.ts`: optionele retained X6-metadata onder `xer`. +- `tests/planning/check-xer-resources.ts`: X6-contract voor calendar/rates/curves, + task.resourceIds, X4b-projectfiltering, baselineretentie, catalogusalias en clone-isolatie. +- `tests/planning/check-xer-resource-corpus.ts`: onafhankelijke bytescan met anonieme hashes, + twee publieke corpuspins en parser-/kernperformance/heap-poorten. +- `tests/planning/run.sh`: beide X6-checks éénmaal buiten de tijdzonematrix; de corpuscheck met + `--expose-gc`. +- `tests/planning/check-xer-schedule-options.ts`, `src/utils/wbs.ts` en + `src/state/slices/librarySlice.ts`: type- en instrumentatieaanpassingen die nodig waren nu + parserrows bevroren zijn; geen nieuwe X5- of library-semantiek. + +## Invarianten en mutatiebewijs + +- Eén `parseXerTables()` voedt één immutable catalogus; de catalogus deelt de ruwe `XerRow` + referenties met iedere projectview. +- TASKRSRC wordt éénmaal naar `proj_id` gepartitioneerd. Een project materialiseert alleen zijn + rijen; ongescope en baseline-rijen blijven uitsluitend in de catalogus voor X9. +- Per project worden resources gekloond en assignments nieuw geprojecteerd. De X4b-test muteert + P1-resource en -assignment en bewijst dat P2 ongewijzigd blijft, terwijl de catalogus gedeeld + en bevroren blijft. +- Arbeidsfractie blijft `1 = 100%`; materiaal per uur wordt met de resourcekalender naar per dag + omgerekend. De tijdelijke productiemutatie `rawRate * 100` maakte de verse X6-bundel rood met + exitcode 1: `0,5 -> 50` en de onafhankelijke P2-waarde `2 -> 200`. De regel is daarna met + `apply_patch` exact hersteld; de verse contracttest is weer groen. + +## Onafhankelijke orakels en performance + +De corpuscheck importeert geen productiemodule voor zijn eerste meting. Hij scant de XER-bytes +zelf, telt tabellen/typen en vergelijkt de anonieme SHA-16-pins voor Roads en rehab-2. Daarna +controleert hij de productiecatalogus en lineaire projectprojectie tegen die onafhankelijke scan. + +De geslaagde rerun mat: + +| corpuspin | parser | parserheap | X6-kern | kernheap | +| --- | ---: | ---: | ---: | ---: | +| `a2ef7b35c00d8cf8` | 81,6 ms | 20.391.440 B | 59,6 ms | 2.919.512 B | +| `2c1dce175b9f0781` | 977,5 ms | 243.986.592 B | 778,9 ms | 41.203.632 B | + +Beide bleven onder de vastgelegde grenzen. De bronlog staat lokaal in +`task-X6-planning-rerun.log`; die log is bewust genegeerd en niet onderdeel van de commit. + +## Uitgevoerde poorten + +- Gerichte X6-contracttest na herstel: exitcode 0, 7 checks. +- Gerichte X6-corpuscheck met openbare `OPS_XER_CORPUS`: exitcode 0, 14 checks. +- `npm run typecheck`: exitcode 0. +- Volledige `npm run test:planning` met `OPS_XER_CORPUS` en `OPS_P6_COMPARISON`: exitcode 0, + 560/560 en UTC, America/New_York, Pacific/Midway, Pacific/Auckland en Atlantic/Azores groen. +- Volledige `npm run verify` met dezelfde publieke corpuscontext: exitcode 0; inclusief lint, + planning/library/MCP/dev-server, examples, docs, i18n, cycles en audit (0 vulnerabilities). +- `git diff --check`: exitcode 0. + +## X9-overdracht + +X9 moet de retained `xer.resources`-catalogus en projectassignment-view door het +documentcontract, IFC-writer/-reader en recovery-bronserialisatie voeren. Deze X6-commit bewaart +alleen de in-memory raw rows met referentie-identiteit; zij claimt expliciet geen save/reload- +round-trip, export of MCP-leespaddocumentatie. + +## Reviewfix X6 — 2026-08-25 + +De bestandsbrede catalogus is nu runtime-diep bevroren: nested resources, sourceprojecties, +rate-entiteiten/-tuples, curve-tuples en issues zijn immutable. De doorloop werkt in-place en +stopt bij de reeds bevroren parserrijen, dus raw `XerRow`- en celidentiteit wordt nooit gekloond. +Documentprojecties blijven bewust mutable en geïsoleerd. + +`duplicateDocument()` kloont XER-metadata gericht: catalogus plus raw rows worden gedeeld, maar +projectassignmentbronnen (entity, quantities, costs, rawCurves) en issues zijn nieuwe objecten. +Hiermee wordt een catalogus met 52.640 retained TASKRSRC-rijen niet meer JSON-gekloneerd. + +De corpuspoort start met een directe byte-/tabelscanner voor presence/tellingen, exacte rates en +ingangsdatums, resourcekalenders, role-only assignments, 21 curvepunten/best-fit, projectpartitie +en raw-rowidentiteit. Zij rapporteert afzonderlijk parser, catalogusbouw, materialisatie en de +volledige `readXER`-route. Heap is een live-delta vanaf één nulmeting per corpuscase, omdat alle +resultaten voor het onafhankelijke orakel terecht live blijven. + +Laatste openbare corpusrun: Roads 90,6 / 6,6 / 72,4 / 246,1 ms; rehab-2 1.079,2 / 13,1 / +1.066,6 / 2.707,8 ms (parser / catalogusbouw / materialisatie / end-to-end), 40 checks, +exitcode 0. Rode mutaties (ieder exitcode 1, direct hersteld): rate ×100, verkeerde kalender, +20 i.p.v. 21 curvepunten, resourceIds-fan-out weg, projectmisrouting en catalogusfilter met +baseline/unscoped-verlies. De voorafgaande RED-tests bewezen ook shallow freeze en rawmetadata- +klonen bij duplicateDocument(). + +## Herreviewfix X6 — 2026-08-25 (twee Important-restpunten) + +De runtime-freeze is nu ook statisch afgedwongen. `XerReadonly` is de ene canonieke, +tuple-behoudende diep-readonlycompositie voor de catalogusgrens; `XerResourceCatalog` exposeert +alle resources, identiteiten, rows, rates, entities, kostentuples, curve-tuples, issues en raw +rows als readonly. De bestaande `Xer*`-bronvormen blijven de expliciet mutabele, +documentgebonden projectie beschrijven. De compile-time contracttest zet `@ts-expect-error` op +catalogusmutaties; wanneer een daarvan weer compileert wordt die directive ongebruikt en faalt +de typecheck. Dezelfde proef bevestigt dat entity, assignedRole, quantities, costs en rawCurves +in een `XerResourceReadResult` wél mutabel blijven. + +Materialisatie kloont naast resources nu ook catalogusidentiteiten, issues en bronwrappers voor +resources, roles, rates en curves. Alleen `rawRow` blijft per ontwerp referentie-identiek aan de +catalogus/parserrij. De X6-contracttest vergelijkt voor resource-, role- en +assignmentprojecties afzonderlijk de objectidentiteit met catalogus/peerprojectie en vervolgens +de isolatie na mutatie. Een tijdelijke vervanging van `structuredClone(catalog.resources)` door +de catalogusreferentie gaf exitcode 1 met twee gerichte contractverschillen (identiteit en +“gematerialiseerde projectie is niet mutable”), zonder stacktrace; de tijdelijke mutatie is +direct hersteld. + +Zelf gereproduceerd op `50799884`: vóór de fix waren acht `@ts-expect-error`-directives +ongebruikt (`npm run typecheck`, exitcode 2), dus catalogusmutaties compileerden. Dezelfde +tijdelijke materialisatieclone-regressie beëindigde de oude test met een ongehanteerde +`TypeError` en exitcode 1. Na de fix waren de gerichte resource-, duplicateDocument- en +openbare corpuschecks respectievelijk groen met 38, 7 en 40 checks (alle exitcode 0); de +volledige poorten staan onder de commit vastgelegd. diff --git a/CLAUDE.md b/CLAUDE.md index 483408bf6..9d2271257 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,7 @@ npm run test:library # los: bibliotheek/IFC/i18n-checks npm run test:mcp # los: MCP-tools npm run test:dev-server # los: node:test-units + integratietest van de dev-serverpoort/-locks npm run test:browser # los: echte gebruikersflows in Chromium via Playwright +npm run test:browser:x11 # lokaal headed; vereist OPS_XER_CORPUS + desktopdisplay, vervangt de corpusloze CI-poort niet npm run verify:examples # los: de gebundelde voorbeelden laden/rekenen door zoals verwacht npm run verify:docs # los: in-app gidsen — nl+en hard vereist, overige 12 talen indien aanwezig npm run verify:i18n # los: ontbrekende vertaalsleutels t.o.v. nl (CLDR-pluralcategorieën meegerekend) @@ -114,12 +115,57 @@ die histogram, overallocatie, nivelleerder (`ResourceLeveler.ts`) en bezettingso (1) een opgeslagen contour (gekoppeld aan de toewijzing via `TaskTimephasedContour.resourceId`, `matchContoursToAssignments`) ⇒ data, zonder de hele-eenheden-afronding; (2) `ResourceAssignment. curveValues` (de exacte 21-punts P6-/MSPDI-curve, `CONTOUR_SHAPE_VALUES`-vorm) ⇒ eveneens data; -(3) anders de bestaande `distributeUnits`-formule, byte-identiek. De engine raakt **geen taakdatum**: +(3) opgeslagen werk (`ResourceAssignment.remainingWorkMinutes` [+ `actualWorkMinutes`], taaktypes- +etappe) ⇒ als data met de curvevorm over de duur gespreid; (4) anders de bestaande `distributeUnits`- +formule, byte-identiek. De engine raakt **geen taakdatum**: de CPM-datums van een import blijven bij laag 3/4 van de Z8-beslistabel en `splitGaps` — de fidelity-poort bewaakt dat. Een duurwijziging (`taskSlice.updateTask`, `createMcpTransactions`, `taskEditPlan`) herschaalt de contour én de importsplits proportioneel via `taskDefaults.ts`'s -`rescaleTaskContours` (actuals blijven, `mspTaskType === 'FIXED_WORK'` houdt het werk vast); een -datum-/kalender-/toewijzingswijziging raakt de as niet. `src/services/contourIo.ts` is de adapterlaag: +`rescaleTaskContours` (actuals blijven; werkbehoud volgt de effectieve werkregel via +`workRuleApply.ts`'s `contourKeepsWork` — zonder eigen `workRule` geldt nog `mspTaskType === +'FIXED_WORK'`); een datum-/kalender-/toewijzingswijziging raakt de as niet. **Werkregels +(taaktypes-etappe, spec `docs/superpowers/specs/2026-09-04-spec-taaktypes-opgeslagen-werk.md`, in +aanbouw):** `Task.workRule` ∈ FIXED_DURATION_RATE (standaard, het gedrag van vandaag) | +FIXED_DURATION_WORK | FIXED_WORK | FIXED_RATE, anders `Project.defaultWorkRule`. De pure kern +`src/engine/work/workTriangle.ts` werkt op de RESTERENDE toestand (werk = restduur × inzet; W en I +opgeslagen, R afgeleid en naar boven afgerond op hele dagen/minuten); de brug +`src/engine/work/workRuleApply.ts` (`captureTriangle` vóór de mutatie → `settle…` erna) is op vier +plekken bedraad: `taskSlice.updateTask`/`setTaskWorkRule`, `resourceSlice.assignResource`/ +`updateAssignment`/`unassignResource`/`moveAssignment`/`removeResource`/`setAssignmentWork`, +`gridTransaction.ts` en de MCP-tweeling in `createMcpTransactions.ts`. Een duur die uit de driehoek +komt (inzet/werk/resource erbij-eraf onder FIXED_WORK/FIXED_RATE) zet `scheduleStale` en loopt door +`settleDurationAftermath` (contour + importsplits herschalen, Z8-venster en bevroren walks wissen), +precies als een duurbewerking; op een gestarte taak wordt de rest dan expliciet geschreven +(`remainingTime`/`remainingMinutes`) zodat niets drift. Een voortgangsbewerking is géén duurbewerking +(de poort is de totale werkduur). Verandert het restwerk van een toewijzing mét contour, dan zakt de +contourhoogte mee (`reconcileContourWork`, "vorm blijft, hoogte zakt"). Materiaalresources sturen +de duur nooit. Een **kalenderwissel** (taak-/projectkalender of andere uren per dag in een kalender) +verandert de slotgrootte en loopt daarna óók door de regel (`applySlotChange`/`settleCalendarChange`: +Vast werk/Vaste inzet ⇒ duur; Vaste duur en werk ⇒ inzet; standaard ⇒ werk volgt, byte-identiek); +de contour-as herschaalt daarbij van de oude naar de nieuwe werkminuten (ook zonder dagverandering), +en een project-/kalenderwijziging die duren verandert meldt hoeveel (`notifyWorkRuleDurationsChanged`). +Zes aanroepers delen `captureCalendarChange` → mutatie → `settleCalendarChange` (store, raster — als +EIGEN stap vóór de rest van de paste —, MCP-tweeling, projectkalender, kalenderinhoud); de contour- +hoogte wordt daarin tegen het werkelijke werk per toewijzing verzoend, niet tegen een regelvlag. +Drie randpaden die de slot óók kunnen wijzigen (`setCalendar`, `resolveDeviation`, de `workTime`- +verwijdering in de MCP-kalendertool) zijn bewust NIET bedraad — zie `docs/TODO.md`. Een +duurbewerking op een taak met EXPLICIETE restduur schuift die rest mee met Δ, geklemd op 0 +(`carryRemainingThroughDurationEdit`). Schrijft de brug de rest expliciet — Δ-regel of kalenderwissel +op een gestarte taak — dan volgt `completion` daaruit als 1 − rest ÷ duur (`syncCompletionToRemaining`, +dezelfde formule als een restbewerking in het raster; eigenaarsbesluit 2026-09-06), zodat Gantt-balk, +solver en rapportage één waarheid delen — eigenaarsbesluiten 2026-09-05/06, spec §6.4/§6.5, meetlat +32–36. Regressie: `tests/planning/check-work-triangle.ts` (kern + meetlat +`work-triangle-cases.json`), `check-work-rule-mapping.ts` (MSP/P6/XER-vertaling) en +`check-work-rule-store.ts` (store/raster/MCP). Via de MCP-bridge: `planner_update_tasks`/`planner_add_tasks` +`fields.workRule`, `planner_manage_assignments` `update.remainingWorkMinutes` en `planner_update_project` +`defaultWorkRule` (`tests/mcp/cases-work-rule.ts`). UI: zichtbaar wanneer de instelling **Toon +taaktypes** (`ui.showTaskTypes`, `ops-showTaskTypes`, default uit) aan staat óf het document zelf +taaktypedata draagt (`taskTypesVisible` in `DOCUMENT_FIELDS`, afgeleid bij laden via +`hasTaskTypeData`, met één melding per document — `taskTypesNotice.ts`); selector `taskTypesUnlocked` +(`src/engine/work/taskTypesVisibility.ts`). Dan: `TaskWorkRuleField` in paneel en dialoog, de kolom +**Werk (rest)** met slotjes in `TaskAssignmentsSection`, en de rasterkolommen `task.workRule` en +`assignment.remainingWork` (alleen `available` wanneer ontsloten; `TaskColumnContext.taskTypesUnlocked`). +Gids: `public/docs/{nl,en}/gids-taaktypes.md`; browserspec `tests/browser/work-rule.spec.ts`. `src/services/contourIo.ts` is de adapterlaag: MSPDI `` (Type 1/2, per werkdag) en P6 `` + `` + de `PlannedCurve`/`RemainingCurve`/`ActualCurve`-spreidingsstrings (`"werkuren:periodeuren;…"`, MPXJ `TimephasedHelper`) round-trippen daar doorheen — let op: P6's `` is dus GEEN @@ -231,10 +277,10 @@ bedrading — álle `@tauri-apps/*`-imports dynamisch achter `isTauri()`, zodat bouwen), `dispatcher.ts`, `schemaValidate.ts` (schema's worden in de dispatcher afgedwongen, óók binnen `planner_batch` — een draaiboek mag de poort niet omzeilen), `toolRegistry.ts`/`toolIndex.ts`, `staleGuard.ts` (`ensureFreshSchedule`), `backup.ts` (AI-backups per document in `appDataDir`, -`MAX_PER_DOC = 10`) en `activityLog.ts` (ring-buffer achter het AI-activiteitenpaneel). De 39 +`MAX_PER_DOC = 10`) en `activityLog.ts` (ring-buffer achter het AI-activiteitenpaneel). De 40 `planner_*`-tools staan in `src/services/mcp/tools/` (taken, relaties, resources, kalender, project, -baselines, documenten/bestanden, leestools, en `planner_batch` als transactionele executor met -temp-id-resolutie). +baselines, documenten/bestanden, leestools, XER/P6-bronprovenance, en `planner_batch` als +transactionele executor met temp-id-resolutie). Veiligheid is state, geen conventie: `ui.aiMode` (de hele AI-tab en bridge verschijnen pas hierdoor), `ui.aiPaused`, `ui.aiReadOnly` en `ui.aiServerStatus` leven in `uiSlice`; de per-request `McpContext` diff --git a/README.md b/README.md index 279c067b2..7bed101be 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ src/ extensions/ # Extensiesysteem (types, api, loader, service) i18n/ # Vertalingen, 14 talen × 4 namespaces hooks/ types/ utils/ styles/ -public/docs/ # In-app handleiding: 32 artikelen × 14 talen (voedt ook de wiki) +public/docs/ # In-app handleiding: 34 artikelen × 14 talen (voedt ook de wiki) examples/ # Voorbeeldplanningen in IFC tests/ # planning · library · mcp · dev-server · browser src-tauri/ # De Rust-schil (dun: precies één native command) diff --git a/docs/TODO.md b/docs/TODO.md index ed0cc6ed3..435f02020 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -350,9 +350,87 @@ deze lijst verwijderd — wat klaar is, staat in de changelog en git-historie. - [ ] **Bewerken-meetlat tegen MS Project.** De herschalingsregel (proportioneel, actuals blijven, FIXED_WORK houdt werk) volgt MSP's gedocumenteerde gedrag maar is niet tegen MSP zelf gemeten — de taaktypes-spec noemt die meetlat als de duurste post van de vervolgetappe. + +### Taaktypes / opgeslagen werk — in aanbouw (spec 2026-09-04, bouw 2026-09-05) + +> Ontwerp: `docs/superpowers/specs/2026-09-04-spec-taaktypes-opgeslagen-werk.md` (opvolger van de +> spec van 2026-08-18). Bouwt op de branch `claude/contour-engine-planner-mnrsy3` (PR #101), die +> gestapeld is op de XER-branch en pas ná die PR merget. Stappen 1–7 staan erin; 8 (afronding docs) +> volgt — zie spec §10 voor de stand per stap. De gids `gids-taaktypes` bestaat in nl+en; de +> twaalf vertalingen volgen in de maandelijkse ronde. +> Eigenaarsbesluiten 1–7 (2026-09-04) en 8–10 (2026-09-05) staan daar in §3. + +- [x] **Duurbewerking op een taak met expliciete `remainingTime`/`remainingMinutes` — besloten + 2026-09-05:** de rest schuift mee met Δ, geklemd op 0; `completion` volgt daaruit (besluit + 2026-09-06, optie a — `syncCompletionToRemaining`; `carryRemainingThroughDurationEdit`, + store/raster/MCP). Bron: Microsoft [M5]. +- [x] **Kalenderwissel op een taak met vastgelegd werk (K2) — besloten 2026-09-05:** een kalenderwissel + verandert de slotgrootte en daarna beslist de werkregel (Vast werk/Vaste inzet ⇒ duur; Vaste + duur en werk ⇒ inzet; standaard ⇒ werk volgt, byte-identiek). Gebouwd (spec §6.4, meetlat + 32–34, `settleCalendarChange`); melding bij een project-/kalenderwijziging die duren verandert. +- [ ] **MS Project-meting van K2 en de Δ-regel (§6.4/§6.5):** beide zijn *documented* voor de richting + en *reasoned* voor de OPS-werkdagen; wie MS Project heeft, meet cases 32–36 plus "duur wijzigen + op een taak met ingevoerde resterende duur" en noteert de uren. +- [ ] **K2 niet bedraad op drie randpaden (review G9, 2026-09-05):** `projectSlice.setCalendar` + (vervangt de hele projectkalender; geen UI-aanroeper meer, wel API-oppervlak), + `librarySlice.resolveDeviation(ref, 'company')` (neemt poolwaarden incl. `hoursPerDay` over) en + de `workTime`-verwijdering ná `draft.updateCalendar` in `calendarResourceTools.ts` wijzigen de + slot buiten `settleCalendarChange` om. Bedraden zodra een van die paden weer een UI-ingang + krijgt; tot dan volgt de werkregel daar niet. Daarnaast (G10): `updateCalendar`/ + `setProjectCalendar` wissen het Z8-venster alleen wanneer de regel de duur wijzigt, terwijl + `setTaskCalendar` dat bij elke kalenderwissel doet — zelfde trigger, ander gedrag. +- [x] **`completion` ↔ expliciete rest (review F4) — besloten 2026-09-06, optie a:** zodra de brug de + rest expliciet schrijft (Δ-regel én kalenderwissel) wordt `completion` herrekend als + 1 − rest ÷ duur, dezelfde formule als een restbewerking in het raster; Gantt-balk, solver en + rapportage delen daarmee één waarheid (spec §6.5, laatste punt; `syncCompletionToRemaining`). +- [ ] **Crashherstel ontsluit zonder melding (review K2, 2026-09-05).** `restoreDocuments` leidt + `taskTypesVisible` correct af (`payloadFromImport`) maar loopt niet langs `applyLoadedProject`, + waar de eenmalige melding zit — na herstel verschijnen de bedieningselementen zonder uitleg. + Bewust gelaten: herstel is dezelfde gebruiker in (meestal) dezelfde sessie. Meenemen zodra + `restoreDocuments` andere laadmeldingen krijgt. +- [ ] **Werkinvoer ≤ 0 in het paneel weigert stil** (review K6a): rode rand via `aria-invalid`, geen + melding — zelfde conventie als de inzetinvoer (`isValidUnits`). +- [ ] **B1c-koppelpunt (`origin/t3code/b1c-etappe3`, gezien 2026-09-05):** die branch wist bij elke + as-verzettende bewerking de nivelleergaten (`clearLevelingGaps`, B1c-plan3 taak 3). Een duur die + uit de werkdriehoek komt (`afterTriangleDurationChange` in `resourceSlice.ts`/ + `createMcpTransactions.ts`, en het assignment-set-pad in `gridTransaction.ts` — sinds de + reviewronde één plek: `workRuleApply.ts`'s `settleDurationAftermath`) hoort dat ná de merge + óók te doen — één regel toe te voegen op de kant die als tweede merget. Geen inhoudelijke overlap + verder: B1c raakt inzet noch werk (besluit 7 blijft). + +- [x] **Beslispunten 8–10 genomen (2026-09-05)**, vastgelegd in spec §3.3: 8 = optie B (vier types in + het menu, bewaard `effortDriven` stuurt alleen de twee MSP-afwijkende cellen); 9 = drie optionele + werkvelden per toewijzing; 10 = de vereenvoudiging "elke toewijzing loopt over de hele restduur" + is voor deze etappe geaccepteerd — zie het vervolgpunt hieronder. +- [ ] **Per-toewijzing-spanne (vervolg op beslispunt 10).** MS Project en P6 laten de ene resource op + een taak eerder klaar zijn dan de andere; OPS laat elke toewijzing over de hele restduur lopen + (spec §6.2: verhoog je op een vast-werk-taak de inzet van één resource, dan wordt de taak korter + en gaat de ándere resource dunner over die kortere duur in plaats van eerder klaar te zijn). + `ResourceAssignment.workWindowStart/Finish` bestaat al, round-tript door IFC + (`OPS_TimephasedWindow`) en het extensiecontract, maar geen lezer vult het en geen solverstap + leest het. Activeren raakt `assignmentDayUnits` (histogram/nivelleerder/bezetting), de + renderer (balk per toewijzing?) en de MSPDI-/P6-exports (per-assignment start/finish). +- [ ] **MSP-meetlat: 36 bewerkingen** (spec §9) meten in MS Project (en P6) zodra iemand het heeft; + tot dan draagt elke case `evidence: 'documented' | 'reasoned' | 'decided'` in `work-triangle-cases.json`. +- [ ] **Telling `mspTaskType × effortDriven` over de `OPS_MPP_CRAWL`-set** (216 bestanden): bepaalt + hoe vaak beslispunt 8 in de praktijk speelt. Het corpus is niet in de repo. +- [ ] **Nivelleerder-optie "inzet verlagen bij vast werk"** (eigenaarsbesluit 7-B, 2026-09-04) als + geavanceerde optie naast het verschuiven; de verdeler raakt nu nooit inzet of werk. +- [ ] **% werk gereed** (MSP % Work Complete) naast de duurgebaseerde `completion`. +- [ ] **Projectstandaard-werkregel in de UI** (projectwizard/projectinfo); het veld bestaat sinds + bouwstap 1 en is via `planner_update_project` (`defaultWorkRule`) zetbaar; de gids noemt dat. +- [ ] **P6-optie "preserve existing assignments"** bij resource erbij: OPS volgt altijd de + synchronisatietabel ("recalculate"); de preserve-variant is een instelling voor later. - [ ] **Uur-modus-dagslot is een benadering.** De engine deelt de as in slots van `hoursPerDay × 60`; een werkdag met afwijkende bandlengte (korte vrijdag) telt daardoor als een deel-slot — dezelfde benadering als `enumerateTaskWorkDays`, dus consistent, maar geen echte per-dag-bandtelling. +- [ ] **XER-opgeslagen werk (`target_qty`/`remain_qty`/`act_reg_qty`) blijft bewust in het + bronarchief.** Geen nieuw modelveld vanuit XER deze etappe — de taaktypes-etappe definieert het + eersteklasveld op `ResourceAssignment`; XER zet het pas dán over, en uitsluitend wanneer + `target_qty` afwijkt van `target_drtn_hr_cnt × target_qty_per_hr` (anders blijft het veld + afwezig, byte-identiek). Meetlatbestanden: `HarbourPointe_AssistedLiving` (98 afwijkende rijen, + factor 3), `Harbour Point DCP-03` (factor 4; `remain_qty` zonder resttarief), `p6_torture_test_v1` + (duur 0 met werk); resttarief wijkt in `rehab-2` in 27,5% van de rijen af. ### Solver/presentatie — resterende punten (2026-07-20) diff --git a/docs/extensions.md b/docs/extensions.md index b3cd0690e..7a32c16c8 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -38,10 +38,12 @@ Gebruik `currentColor` voor `fill`/`stroke` zodat het icoon met het thema meekle | `ribbon` | **hard** — ontbreekt ⇒ `api.ui.addRibbonButton` gooit | Een knop in de ribbon plaatsen. | | `backstage` | **warn** (overgangsregime) — ontbreekt ⇒ `api.importers.*` werkt nog, maar logt een waarschuwing | Een importer registreren (verschijnt in Bestand → Importeren). | | `pdf-fonts` | **hard** — ontbreekt ⇒ `api.pdfFonts.register` gooit | Een font-provider registreren voor de vector-PDF-export (bv. CJK-glyf-bytes). | +| `importSource` | **hard, default-deny** — ontbreekt ⇒ `api.data.getImportSourceInfo`/`getImportSourceChunk`/`getImportSourceCatalogPage` gooien vóórdat er ook maar één byte gelezen wordt | De **volledige oorspronkelijke bronbytes** van een geïmporteerd bestand (vandaag: XER) lezen, inclusief velden die de importlaag bewust niet in het projectmodel materialiseert. Zie de aparte paragraaf verderop. | | `filesystem` | informatief | Geen API-oppervlak; puur getoonde intentie bij installatie — **geen** sandbox-garantie (extensie-code heeft technisch gewoon toegang). | | `network` | informatief | Idem — getoonde intentie, geen technische grens. | -`data.*`, `settings.*`, `assets.*` en `ui.showNotification` zijn **kern-API**: altijd beschikbaar, geen permissie nodig. +`data.*` is verder **kern-API** — behalve de drie `getImportSource*`-methoden hierboven — net als +`settings.*`, `assets.*` en `ui.showNotification`: altijd beschikbaar, geen permissie nodig. De afdwinging is gecentraliseerd in `src/extensions/permissions.ts` (één tabel pad → permissie). @@ -203,7 +205,7 @@ module.exports = { | Onderdeel | Functies | |---|---| | `api.importers` | `register(def)`, `unregister(id)` | -| `api.data` | `getProject/getCalendar/getTasks/getSequences/getResources/getAssignments`, `addTask`, `updateTask`, `addSequence`, `loadProject(result)`, `recalculate()`, `batch(fn)` | +| `api.data` | `getProject/getCalendar/getTasks/getSequences/getResources/getAssignments`, `getImportSourceInfo/getImportSourceChunk/getImportSourceCatalogPage` (permissie `importSource`, zie hieronder), `addTask`, `updateTask`, `addSequence`, `loadProject(result)`, `recalculate()`, `batch(fn)` | | `api.events` | `on/off/emit` (permissie `events`) | | `api.ui` | `addRibbonButton(reg)` (permissie `ribbon`), `showNotification(msg, type?)` | | `api.settings` | `get(key, default)`, `set(key, value)` — per extensie geprefixt in localStorage | @@ -261,6 +263,80 @@ module.exports = { }; ```` +### Read-only XER-bronroute (permissie `importSource`, `apiVersion` ≥ 1.1) + +Naast de gemapte `data.*`-DTO's (afgeleid, genormaliseerd, altijd beschikbaar) kan een extensie met +de permissie `importSource` ook bij de **oorspronkelijke, ongewijzigde brondata** van het huidige +document — vandaag alleen voor een geopend `.xer`-bestand (Primavera P6). Zonder deze permissie +gooien alle drie de methoden vóórdat er ook maar één byte gelezen wordt; er lekt dus niets via een +gedeeltelijke aanroep of een foutpad. Deze drie methoden bestaan sinds contractversie `1.1.0` (zie +*Twee versievelden, twee vragen* hierboven) — declareer `"apiVersion": "1.1"` of hoger in je +manifest als je erop rekent; een host ouder dan 1.1 kent de methoden simpelweg niet. + +**Waarom een aparte permissie en geen kern-API.** De rest van `api.data.*` levert het interne +projectmodel: taken, kalender, relaties — precies wat de importer ervan gemaakt heeft. De +bronroute geeft de **volledige originele bytes en tabellen** terug, inclusief kolommen die de +importlaag bewust nooit in het projectmodel materialiseert (audit-/herkomstvelden als +`create_user`/`update_date`, kosten, review-/locatievelden, ongebruikte UDF's, …). Dat is een +wezenlijk grotere blootstelling dan "de app leest dit bestand" — elke geïnstalleerde extensie zou +anders, ook zonder enige andere permissie, de rauwe brontekst van elk geopend project kunnen lezen +en doorsturen. Vandaar: default-deny, expliciet gedeclareerd in `manifest.json`. + +```js +// manifest.json → "permissions": ["importSource"] +const info = api.data.getImportSourceInfo(); // null buiten een XER-document +if (info) { + console.log(info.sourceFormat, info.archive.byteLength, info.catalogs.taskSourceRows.totalRows); +} +``` + +- **`getImportSourceInfo()`** → `ExtImportSourceInfo | null`. Een kleine, samengestelde samenvatting + (bronformaat, archief-identiteit inclusief `sha256`/`byteLength`/`chunkCount`, getalnotatie, + diagnostiek-tellingen, het importrapport, de schedule-options-herkomst en catalogustellingen). + Geen record-inhoud. **`null`** wanneer het actieve document geen retained XER-bron heeft (elk + niet-XER-document, of een XER-document van vóór deze functie). +- **`getImportSourceChunk(index)`** → `Uint8Array | null`. Eén losse, verse kopie van een stuk van + de oorspronkelijke bestandsbytes (`archive.chunkSize`/`archive.chunkCount` uit `getImportSourceInfo()` + geven de indeling). Concateneer alle chunks 0..`chunkCount - 1` in volgorde om de **exacte** + oorspronkelijke bytes te reconstrueren — vergelijk de `sha256` uit `getImportSourceInfo()` om dat + te bevestigen. Een ongeldige index (negatief, fractioneel, of buiten bereik) gooit een + `RangeError`; buiten een XER-document levert de methode `null`. +- **`getImportSourceCatalogPage(collection, options?)`** → `ExtImportSourceCatalogPage | null`. + Gepagineerde, per record gekopieerde toegang tot de retained brontabellen (task-bronrijen, + resource-/rol-/tarief-/curve-/toewijzingsrijen, activiteitscodes, custom-field-definities, + UDF-waarden, schedule-options-bronrijen, …) — zie `ExtImportSourceCollection` in `extTypes.ts` + voor de volledige lijst. `options.offset` (default 0) en `options.limit` (default 100, **maximaal + 500 per pagina**) sturen de paginering; een niet-safe-integer of negatieve waarde gooit een + `RangeError`, net als een onbekende `collection`. Een `offset` voorbij het einde van de collectie + gooit **niet** — hij wordt gecanoniseerd naar `total` en levert zo een lege, geldige laatste + pagina (`items: []`) in plaats van een fout of een numeriek onveilige slice. Buiten een + XER-document levert de methode `null`. + +**Documentbinding, selector en documentdrift tijdens pagineren.** Alle drie de methoden werken op +het **actieve document**: bij het wisselen van document (`switchDocument`) volgen ze automatisch +mee naar de bronroute (of het ontbreken daarvan) van het nieuw actieve document. `info.selector`/ +`info.sourceProjectId` identificeert welk P6-project binnen het (mogelijk multi-project) +XER-bestand dit document vertegenwoordigt — een `.xer`-bestand kan meerdere documenten openen (één +per project), en elk document draagt zijn eigen bronselector. + +Dat "automatisch meevolgen" is handig voor een enkele aanroep, maar een **risico bij pagineren**: +pagineren is per definitie meerdere aanroepen na elkaar, en er is geen paginasessie die aan één +document vastzit. Wisselt de gebruiker tussen twee `getImportSourceCatalogPage`-aanroepen van +document (`switchDocument`), dan levert de tweede aanroep zonder verdere maatregelen gewoon een +pagina van het **nieuwe** actieve document — bijvoorbeeld een lege pagina omdat dat project minder +records heeft, wat een naïeve extensie laat concluderen "klaar" terwijl in werkelijkheid twee +projecten door elkaar zijn gehaald. Geef daarom `options.expectedSourceProjectId` mee met het +`sourceProjectId` dat je van een eerdere aanroep kreeg: wijkt de actieve bronselector af (ook als +het actieve document inmiddels helemaal geen XER-bron meer heeft), dan gooit de aanroep een +`ExtImportSourceDriftError` in plaats van stilzwijgend door te gaan. Zonder deze optie is er **geen** +driftbewaking. Dezelfde risicoklasse geldt in mindere mate voor het na elkaar opvragen van meerdere +`getImportSourceChunk`-indexen om de bronbytes te reconstrueren — vergelijk daar `sourceProjectId` +(of de `sha256`) tussen chunks als je niet zeker weet dat het document niet wisselt. + +**Verse kopieën, geen aliasing.** Zoals de rest van `api.data.*` levert elke aanroep een nieuwe, +onafhankelijke kopie: muteren van een teruggegeven `info`, cataloguspagina-item of chunk raakt het +retained archief niet, en twee aanroepen na elkaar geven nooit hetzelfde object terug. + ### Datacontract: de `Ext*`-typen Alles wat via `api.data.*`, de importer-handlers en `sdk.factory.*` de extensie in- en uitgaat, gebruikt **stabiele extensie-typen** (`ExtProject`, `ExtCalendar`, `ExtTask`, `ExtTaskTime`, `ExtSequence`, `ExtResource`, `ExtAssignment`, `ExtImportResult`; gedefinieerd in `src/extensions/extTypes.ts`). Dit is het **publieke contract** — bewust losgekoppeld van het interne domeinmodel, zodat een interne refactor jouw extensie niet breekt. @@ -343,6 +419,6 @@ Bij een los `.js`-bestand mag het manifest als commentaarblok bovenaan: ## Beperkingen -- Er is geen JavaScript-sandbox: extensie-code draait via `new Function(...)` en heeft toegang tot `window`, `document` en `fetch`. Permissies worden hard afgedwongen voor `ribbon`/`events`, in warn-modus voor `backstage`, en zijn voor `filesystem`/`network` puur informatief (geen technische grens). Installeer alleen extensies die je vertrouwt. +- Er is geen JavaScript-sandbox: extensie-code draait via `new Function(...)` en heeft toegang tot `window`, `document` en `fetch`. Permissies worden hard afgedwongen (default-deny) voor `ribbon`/`events`/`pdf-fonts`/`importSource`, in warn-modus voor `backstage`, en zijn voor `filesystem`/`network` puur informatief (geen technische grens). Installeer alleen extensies die je vertrouwt. - Objecten uit `api.data.get*()` zijn **verse, muteerbare `Ext*`-kopieën** — muteren raakt de store niet; schrijf terug via de muterende API-functies. - Het `@manifest`-commentaarblok in een los .js-bestand moet een plat JSON-object zijn (geen geneste objecten). diff --git a/docs/superpowers/README.md b/docs/superpowers/README.md index fba39a9c7..88f3ec66b 100644 --- a/docs/superpowers/README.md +++ b/docs/superpowers/README.md @@ -67,6 +67,7 @@ Wél verplaatst, omdat ze nergens meer bij horen: | document | status | |---|---| | `HANDOFF-2026-08-14-roadmap.md` | **actief** — wat er nog op de roadmap staat, met peildatum en afhankelijkheden | +| `specs/2026-09-04-spec-taaktypes-opgeslagen-werk.md` | **gebouwd (stappen 1–7, 2026-09-05; stap 8 = afronding docs)** — taaktypes/werkregels, opgeslagen werk per toewijzing en effort-driven bewerken; opvolger van `specs/2026-08-18-spec-taaktypes-effort-driven.md`. Bevat de MSP/P6-documentatievergelijking, de regeltabel, de meetlat (31 bewerkingen), alle eigenaarsbesluiten (1–10) en per stap de status en de verwerkte reviewbevindingen (§10). Code: `src/engine/work/` (kern, brug, zichtbaarheid), bedrading in de slices/het raster/de MCP-tweeling, UI in `TaskWorkRuleField`/`TaskAssignmentsSection`, gids `gids-taaktypes`. Open punten staan in `docs/TODO.md`. | | `werkdagen-as-ontwerp.md` | naslag; aangehaald vanuit `timeAxis.ts`, `workdayAxis.ts` en `check-workday-axis.ts` | | `verticale-drag-ontwerp.md`, `verticale-drag-ontwerp-B.md` | naslag; twee varianten van hetzelfde ontwerp | | `modulariteit-audit.md`, `prestatie-modulariteit-audit.md` | de audits waar de P-bevindingen uit komen; aangehaald vanuit testkoppen | diff --git a/docs/superpowers/plans/2026-08-20-plan-xer-p6-lezer.md b/docs/superpowers/plans/2026-08-20-plan-xer-p6-lezer.md new file mode 100644 index 000000000..60858bd8b --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-plan-xer-p6-lezer.md @@ -0,0 +1,589 @@ +# Etappe 2 — Primavera XER lezen, getrouw aan P6 op vier assen + +*Levend etappeplan. Concept 2026-08-20 na drie verkenningsrondes; herwerkt dezelfde dag na de +hyperkritische planreview (verdict: herwerken — alle blokkerende en zware punten zijn in deze +versie verwerkt; de review mat vrijwel alle corpusgetallen zelf na en de gecorrigeerde cijfers +hieronder zijn de zijne). Eigenaar van dit document is de orkestrator; besluiten en bevindingen +worden hier bijgeschreven zoals bij `2026-08-17-plan-mpp-nul-afwijkingen.md`. Overgenomen +2026-09-04 door de Claude-hoofdsessie na het einde van de Codex-thread; besluiten X-O6 t/m X-O8 +en de laagbijstelling stammen uit die overname.* + +## §1 Doel + +**Open Planner Studio opent Primavera XER-bestanden (.xer) native, en is daarbij getrouw aan +P6's eigen opgeslagen rekenuitvoer op vier assen: early start, early finish, late start en late +finish — plus de totale en vrije float.** Over alle leesbare, door P6 doorgerekende bestanden +van het XER-corpus geldt na import + herberekening (`runCPM`): exact nul afwijkingen, per as +geteld over de taken waar die as meetbaar is. De baseline bestaat uitsluitend uit nullen, zonder +één reason-pin — het `GOAL_ZERO_DEVIATIONS`-model van etappe 1, uitgebreid met per as een +afwijkings- én een meetbaar-teller. + +**Granulariteit (planreview B1 — dit beslist alles):** 98,9% van de orakeldatums draagt een +echte tijd (73.408 van 74.212 gemeten datumwaarden staan niet op middernacht; 08:00, 16:00 en +17:00 domineren). De meetlat is dus **minuut-exact**, wat betekent dat élk orakelbestand in +uurmodus gelezen moet worden — anders landt elke taak in `sameday`, en sameday moet nul zijn. +De kalenderdecoder en de uurmodus-promotie (X3) zijn daarmee niet een tussenstap maar de +kritieke taak van de etappe. + +**Float-precisie (planreview V2), als formule:** P6 slaat float op in uren +(`total_float_hr_cnt`/`free_float_hr_cnt`; 70 van de ±18.400 gevulde waarden zijn fractioneel, +dus "hele uren" is geen geldige aanname). Wij rekenen float in werkdagen. De vergelijking is: +`ons_float_in_minuten === round(p6_float_uren × 60)`, waarbij onze werkdag→minuten-omrekening +loopt via de taak-effectieve kalender — dezelfde uren-per-dag-bron als de duren, inclusief de +afleiding-uit-weekuren wanneer P6's eigen uren-per-dag-veld leeg is (dat is het hóófdpad: in +het rijkste corpusbestand is dat veld voor alle 124 kalenders leeg). Exact, geen tolerantie; +blijkt een klasse fractionele gevallen structureel onbeslisbaar, dan is dat een X-O3-escalatie, +geen stille afronding. + +**Wat er níét in deze etappe zit**: XER schríjven (export) — eigen latere etappe (MPXJ's +`PrimaveraXERFileWriter` als referentie; TODO-registratie in X12). De taaktypes/effort-driven- +mótor blijft de aparte etappe uit `2026-08-18-spec-taaktypes-effort-driven.md`; P6's duration- +en activiteitstypes worden hier wél gelezen en bewaard als data. X0 legt daarbij vast dat de +latere motor-etappe `mspTaskType` en de nieuwe P6-typevelden naar één interne superset mapt — +twee opslagvelden nu, één rekenmodel straks, geen twee eilanden. + +## §2 Waarom dit de juiste volgende stap is + +- `docs/TODO.md` (issue #17): *"Primavera XER import/export — tekstformaat, native in TS + haalbaar (geen JVM); samen met ons bestaande PMXML dekt dit de P6-wereld. Hoogste + interop-prioriteit."* In de bouwpraktijk is de .xer vaak het enige dat een aannemer krijgt. +- Het fundament ligt er: de format-registry maakt een `.xer`-entry een patroonvolgend haakje + (lazy chunk, dialoogfilters, i18n-fouten — het .mpp-stramien). De solver leest + `schedulingOptions` volledig; `mppReader.ts` en `mspdiReader.ts` vullen ze al gedeeltelijk — + XER's SCHEDOPTIONS-tabel wordt de derde vuller en de eerste met P6's eigen + instellingsbegrippen (planreview F1: het plan claimde eerder "de eerste" — onjuist). +- Het corpus is publiek: 93 crawl-bestanden (P6 5.0 t/m 24.x). **Eerlijke telling van de + meetmassa (planreview B3/licht):** 18.489 taken dragen de vier datum-assen; 17.963 dragen + álle zes assen. Let op de duplicaten (her-check-meting): byte-dedup (md5) houdt 84 unieke + bestanden over met 17.600 orakeltaken in 23 unieke orakelbestanden — maar het gróótste + duplicaat vangt een inhoudshash juist níét: de twee Hotel-bestanden (samen 47% van het + orakel) dragen dezelfde vier projecten en dezelfde 4.236 taken in nét verschillende bytes. + De pinning dedupliceert daarom tweeledig: byte-hash voor de exacte kopieën (twee Harbour + Point-bestanden staan er 4× in, drie 2×) én een schema-vingerafdruk (proj_id-set + + taakcodes + orakelwaarden) die het Hotel-paar als bekend duplicaat markeert. Het unieke + orakel is dan 13.383 taken over 22 bestanden — nog altijd ~4× het .mpp-corpus, zonder + bedrijfsdata: bestandsnamen mogen gewoon in tests en commits. + +## §3 De twee meetlatten + +1. **Corpus-orakel (bulk).** P6 bewaart zijn laatste rekenuitvoer per taak in de TASK-tabel. + Een eigen `xerGroundTruth` leest die velden met een **onafhankelijke, minimale tabelscan** — + bewust een tweede parser naast de echte lezer (F7-les uit etappe 1: gedeelde veldkaarten + zijn common-mode; hier blijft zelfs de tokenizer gescheiden, en de "veldkaart" is per + bestand de eigen `%F`-regel). Statussemantiek hoort bij de meetlat: voltooide taken meten op + `act_`-datums; per as geldt een meetbaar-aantal naast het afwijkingental, want de dekking is + scheef (her-check-meting, gevulde cellen over de 60 orakelbestanden: ES/EF in 48 + bestanden, LS/LF in 36, TF ±18.400 cellen, FF ±18.000 — er bestaan bestanden met alleen + float en geen datums, en andersom). + **Beide tellers worden gepind** (planreview M1): een lezer die stilletjes minder gaat meten + maakt de suite net zo rood als een lezer die fout meet. + Daarnaast rapporteert (buiten de nul-poort) een zevende as: `driving_path_flag` — P6's eigen + kritiek/driving-oordeel, gevuld in 18.321 cellen over 30 bestanden, de goedkoopste externe + toets op onze kritiek-padlogica die er bestaat (planreview M3). Promotie van die as tot + poort is een expliciet latere afweging. +2. **P6-geverifieerde scenario's (scherp).** De `p6-comparison`-map bevat 13 kleine cases met + per case ES/EF/LS/LF/TF/FF zoals **echt P6 23.12** ze produceerde. Alleen de `*_p6`-kolommen + zijn meetlat — mét hun tijden. De `*_engine`-kolommen en de PASS-oordelen van dat raamwerk + zijn géén referentie: hun "PASS" is geveld ná een normalisatie die de tijd volledig laat + vallen én een exclusief-vs-inclusief-finish-conventie overbrugt (gemeten in case 01: + `EF_engine=2026-01-12` vs `EF_p6=2026-01-09 17:00` telt daar als gelijk), en hun README + meldt zelf dat de engine op deze capture is nagefit (planreview M4). Wij nemen uitsluitend + P6's kolom over, onvertaald, en bouwen daar `cases-p6-verified.json` uit (herkomst: alleen + de data; de engine-code van dat project is geen referentie). De twee cases die het raamwerk + buiten zijn matrix hield (fractional-lag, dangling-relationship) worden gedocumenteerde + niet-in-P6-reproduceerbaar-randgevallen, geen poort. + +Niet-orakel-bestanden (de ongerekende generator-fixtures onder `xer-corpus/cases/` — planreview +M5: die dragen géén orakelvelden, hun TASK-header stopt bij constraints en actuals — de +robuustheidsbestanden en het 8-byte-DROID-skelet) tellen niet in de fidelity-poort maar in de +**parser-poort**: leesbaar-of-nette-typed-fout, nooit een crash of een stil half project. + +## §4 Harde regels (geërfd uit etappe 1, plus de XER-eigen) + +1. **Het opgeslagen antwoord is meetlat, nooit uitkomst — als WHITELIST (planreview M2, + dichtgemaakt na de her-check).** De lezer mag uit de TASK-tabel uitsluitend deze + invoervelden importeren: identiteit/structuur (`task_id`, `task_code`, `task_name`, + `wbs_id`, `clndr_id`, `proj_id`, `guid`, `rsrc_id`), `status_code`, `task_type`, + `duration_type`, `priority_type`; voortgang: `complete_pct_type`, `complete_pct`, + `phys_complete_pct` (dé kolom voor de CP_Phys-taken, 18.607 gevulde cellen), + `act_start_date`/`act_end_date`; duren: `target_drtn_hr_cnt`/`remain_drtn_hr_cnt`; + geplande datums: `target_start_date`/`target_end_date`; werk-hoeveelheden (als data): + `target_work_qty`/`remain_work_qty`/`act_work_qty`/`act_this_per_work_qty` en de + `*_equip_qty`-familie; constraints: `cstr_type`/`cstr_date` (+`2`); + `suspend_date`/`resume_date`; `task_notes` (échte notitiedata, X8); `constraint_type` + (dialect-alias van `cstr_type` in de ProjectLens-bestanden); en `expect_end_date` + (uitsluitend samen met de bijbehorende SCHEDOPTIONS-vlag, zie X5). **Alles wat niet op + deze lijst staat is verboden terrein voor de lezer** — expliciet inclusief álle rekenuitvoer die er als gewone velden uitziet: + `restart_date`/`reend_date` (15.328/15.329 gevulde cellen), + `rem_late_start_date`/`rem_late_end_date` (10.193 elk), `driving_path_flag`, + `float_path`(`_order`), + `external_early_start_date`/`external_late_end_date` (die onder X-O1 relevant lijken maar + uitvoer zijn), `old_restart_date`/`old_reend_date`/`old_remain_drtn_hr_cnt`, + `crt_path_num`, `critical_drtn_hr_cnt`, `act_drtn_hr_cnt` en + `plan_start_date`/`plan_end_date`. Naast deze twee lijsten is er een + **derde bak: "genegeerd — geen planningsdata"** (delta-check: 32 corpuskolommen vielen + buiten beide lijsten en de poortregel was daarmee op dag één onvervulbaar): audittrail + (`create_date`/`update_date`/`create_user`/`update_user`), vlaggen + (`rev_fdbk_flag`/`lock_plan_flag`/`auto_compute_act_flag`), kosten + (`remain_cost`/`plan_cost`/`act_cost`), review/locatie + (`review_type`/`review_end_date`/`location_id`), `est_wt`, `tmpl_guid`, + `target_qty_per_hr`/`act_reg_qty`/`act_ot_qty`, `plan_start_date`/`plan_end_date`-achtige + restvelden voor zover niet al verboden, de pseudo-XER-kolommen uit bestanden die X2 toch + weigert, en de lege kolomnaam die een afsluitende tab op een `%F`-regel oplevert + (tokenizer-nota in X2). Voorbehoud: blijkt tijdens de bouw dat een bak-3-veld tóch + planningsinvoer draagt (kandidaten: `est_wt`, `review_type`, `tmpl_guid` — op naam + ingedeeld, niet doorgelezen), dan verhuist het expliciet naar bak 1, nooit stilzwijgend. + Naast deze drie bakken is er een **vierde bak: "uitsluitend weergave/meetlat, nooit + solverinvoer"** (X-O7, §5): `early_start_date`, `early_end_date`, `late_start_date`, + `late_end_date`, `total_float_hr_cnt`, `free_float_hr_cnt`. De lezer draagt ze in een apart, + waardedragend contractveld (werknaam `ImportResult.recordedTimes`), nooit in `Task.time` en + nooit in de solverinvoer — mutatiebewijs in X12-stijl: gemuteerde opgeslagen uitvoer + verplaatst de solve niet, maar verplaatst de weergavemodus (het issue-#63-mechanisme van + X-O7.3) wél. + **Een corpus-%F-kolom die in géén van de vier bakken staat is een X0-poortfout, geen vrije + keuze.** De X12-sluiproute-scan grept tegen de whitelist. Drie afgekeurde pogingen in + etappe 1 zijn het precedent. +7. **De enum-tokenregel (delta-check — twee mechanismen, niet één):** (a) hoofdletter- + varianten van bekende tokens worden case-insensitief gematcht (het corpus draagt + `RCAL_SUCCESSOR` naast `rcal_Successor` en `TT_mile` naast `TT_Mile`); (b) een token dat + ook ná case-fold onbekend is — zoals `ST_TotalFloat`, een PMXML-vorm in een XER die géén + case-variant van de XER-tokens is — wordt een **gerapporteerde** terugval naar de default, + nooit een stille. Implementatie in X4a/X5 (de semantische lagen; de X2-tokenizer kent geen + enums), elk met een mutatiebewijs: case-fold uit ⇒ de `RCAL_SUCCESSOR`-fixture ROOD; + rapportage uit ⇒ de `ST_TotalFloat`-fixture ROOD. +2. **De veld-als-signaal-regel** (etappe-1-registratie): veld-aanwezigheid op `Task` ís + semantiek-signaal. De XER-lezer zet een bestaand veld alleen als de P6-betekenis aantoonbaar + identiek is; elke afwijkende semantiek wordt een **bron-vlag** naar het O6-patroon (default + uit ⇒ byte-identiek, uitsluitend door de betreffende lezer gezet). Verwachte kandidaten: + de lag-kalender, retained logic vs. progress override, de Z10/Z11-relatieregels, en de + suspend/resume-semantiek (X7). +3. **Corpus is publiek**; bedrijfs-XER-bestanden zouden hash-only zijn, maar het corpus bevat + ze niet. +4. **MPXJ (LGPL-2.1) uitsluitend lezen-om-te-begrijpen**; onafhankelijk herimplementeren, + herkomstvermelding per bestand. Zelfde regel voor het cpp-cpm-engine-raamwerk: alleen de + `*_p6`-data is meetlat, hun code is geen referentie. +5. **Exitcode is de poort, nooit de tail**; blast-radius meten vóór verbreden; regels op de + invoer; diagnose op bladniveau; elke nieuwe decodeerregel een corpusloze fixture naast zijn + corpuspin; élke taak minstens één mutatiebewijs in zijn acceptatie (planreview: X4/X5/X6 + misten dat in het concept — hersteld hieronder). +6. **Reviewpijplijn**: verse Sonnet-implementer per taak → review via de + `hyperkritische-review`-skill (tier 2/Opus voor motor-, meetlat- en grammatica-werk, tier + 1/Sonnet voor mechanisch werk) → fixronde bij dezelfde implementer → her-check bij dezelfde + reviewer; [BEVESTIGD]/[VERMOED]-labels blijven intact in de doorgeleiding. Mergen één taak + per keer met de tellers vóór/ná in het merge-commit. + +## §5 Openstaande eigenaarsbesluiten + +- **X-O1 — BESLOTEN (eigenaar, 2026-08-20): álles wordt geïmporteerd.** De leidende regel: + wie het bestand hier opent, ziet hetzelfde als in Primavera. Elk project in het bestand + wordt een eigen document in het bestaande multi-documentmodel; het project met de meeste + taken wordt het actieve tabblad (de export-vlag discrimineert niet — gemeten 15/15 en 4/4 + 'Y' — en "het eerste project" draagt in de Hotel-bestanden nul taken). Cross-project- + relaties worden `externalLinks` tussen de geopende documenten — let wel (her-check): + cross-project-relaties komen in het corpus exact nul keer voor, dus deze tak krijgt een + synthetische fixture en `externalLinks` is data, geen solverinvoer. De openingsmelding + benoemt hoeveel projecten er geopend zijn. **Lege projecten** (gemeten: 3 van de 15 in het + OZB-bestand en 2 van de 4 in het Hotel-bestand dragen géén taken) worden overgeslagen en + geteld in de melding — geen lege tabbladen (orkestratorbesluit, eigenaar kan overrulen). + Uitzondering: een project dat door een ánder project in hetzelfde bestand als baseline + wordt aangewezen (X-O2) opent níét als los document — in Primavera is een baseline ook + geen open project. **Begrenzing (her-check)**: die uitsluiting mag de verzameling nooit + leegmaken — bij wederzijdse of zelfverwijzing, of wanneer álle projecten als baseline zijn + aangewezen, opent alles alsnog als gewoon document mét melding; en de + meeste-taken-heuristiek voor het actieve tabblad telt uitsluitend de daadwerkelijk + geopende projecten. Gevolg voor de meetlat: het volledige orakel is bereikbaar; de + fidelity meet per project en pint per bestand de som. +- **X-O2 — BESLOTEN (eigenaar, 2026-08-20): baselines blijven gewoon bewaard.** Een + gekoppeld baselineproject (`sum_base_proj_id` → aanwezige PROJECT-rij) wordt als + OPS-baseline op het hoofdproject gematerialiseerd — **verplichte deliverable van de + etappe** (blijft buiten de nul-poort: baselines dragen geen orakelvelden voor de vier + assen). Dangling-tak verplicht: 9 van de 10 gemeten corpuskoppelingen verwijzen naar een + proj_id dat níét in het bestand zit — dangling wordt genegeerd én geteld in de + openingsmelding, nooit een crash of stil half project. `stack_data_center_baseline.xer` is + de positieve testcase. +- **X-O3 — BESLOTEN (eigenaar, 2026-08-20, conform voorstel).** TF/FF tellen volwaardig mee, met de formule uit + §1 en per as de dubbele teller (afwijkingen + meetbaar). Legt de residu-iteratie een + principieel P6-float-definitieverschil bloot dat niet via `schedulingOptions` te vangen is, + dan is dat een eigenaarsbeslispunt — nooit stilzwijgend versoepelen. +- **X-O4 — BESLOTEN (eigenaar, 2026-08-20, conform voorstel; gecorrigeerd na planreview).** Het corpus bevat géén enkel BOM-dragend bestand — de + BOM-tak is een formaliteit, **de heuristiek draagt alles**: geldige-UTF-8-toets over het + hele bestand, bij falen Windows-1252 (gemeten: 22 bestanden met high-bit-bytes, 12 daarvan + geen geldige UTF-8). Voor kleine bestanden is de toets principieel onbeslisbaar (3 + high-bit-bytes kunnen toevallig geldige UTF-8 zijn); daarom is de vermelding van de gemaakte + keuze in de openingsmelding **de eigenlijke mitigatie**, geen extraatje. Geen + gebruikersinstelling in deze etappe. +- **X-O5 — BESLOTEN (eigenaar, 2026-08-20): getalnotatie uit CURRTYPE, conform voorstel, + mét documentatie-eis.** De decimaal/duizend-tekens komen uit het bestand zelf en zijn niet + altijd letterlijk: naast `.`/`,` bestaan symbolische tokens (`ds_Period`, `dg_Comma`). + Regel: bekende tokens decoderen; ontbrekende CURRTYPE-tabel (62 van de 93 bestanden!) ⇒ + default punt-decimaal; een aantoonbaar komma-decimaal-bestand zonder CURRTYPE ⇒ typed fout + ("dit bestand kan ik niet betrouwbaar lezen") boven een stil verkeerd geparsed getal. Dit + raakt élk getal — duren en floats incluis — en krijgt een eigen fixture-batterij in X2. + **Documentatie-eis (eigenaar)**: dit gedrag wordt op drie plekken vastgelegd — een + docblok-uitleg bij de CURRTYPE-tweepas in `xerTables.ts` (X2), de typed-foutmelding zelf in + alle 14 talen (X2 werpt de getypeerde foutcode, X4a mapt hem naar de vertaalde tekst — + exact het bestaande `mppCode`-patroon), en een eigen paragraaf in de .xer-gids (X10) die in + gebruikerstaal uitlegt wat het bestand wel/niet zegt over zijn getalnotatie en wanneer de + app weigert te gokken. `verify:docs` bewaakt de gidsparagraaf zoals altijd. +- **X-O6 — BESLOTEN (eigenaar, 2026-09-04): main wordt gemergd in de etappebranch, geen + rebase.** De commit-identiteiten van de branch blijven intact (reviewrapporten en het + X11-browserbewijs pinnen erop). Alle vier de openstaande zijbranches + (`codex/xer-mcp-read`, `codex/xer-ext-read`, `codex/xer-x12-v2-rebased`, + `codex/xer-schedoptions-provenance`) blijven onderdeel van deze etappe. +- **X-O7 — BESLOTEN (eigenaar, 2026-09-04): P6-getrouwheid in drie lagen, bijgesteld na de + kalibratiemeting van dezelfde dag.** Primavera is per definitie de referentie voor wat een + XER-project toont. + 1. *Primavera-gedrag nabouwen* — **het zwaartepunt na de bijstelling**: conventieverschillen + (bv. LS/LF van voltooide taken = werkelijke datums) worden nagebouwd achter een bron-vlag + die uitsluitend de XER-lezer zet (O6-patroon); de motor verandert niet voor IFC/MSP- + projecten. Dossier `rehab-2.xer` (14.812 cellen) is hierbij motorsemantiek, geen + ontbrekende invoer: (i) late zijde + float van voltooide activiteiten, 5.620 cellen — P6 + rekent voltooide activiteiten door tot de statusdatum en geeft ze echte float; (ii) late + zijde van niet-gestarte activiteiten, 7.056 cellen — wij systematisch later, 140 + verschillende delta's, anker klopt. Samen 69% van het corpustotaal (18.398 gemeten + cellen), beide achter een XER-bron-vlag. Echte eigen rekenfouten die daarbij boven komen + worden voor alle projecten gefixt, met eigen test. + 2. *Ontbrekende instellingen afleiden* — **vervalt** (kalibratiemeting 2026-09-04, meting, + geen aanname): 384 combinaties van échte P6-instellingen over 34 orakelbestanden gaven een + beste denkbare winst van 1.033 van 18.398 cellen (5,6%), **0 bestanden met een uniek + optimum**, en op 2 van 9 valideerbare bestanden sprak de afleiding de gedeclareerde + SCHEDOPTIONS tegen. De vaste defaults uit §X5(b) blijven de weg; kalibratie wordt niet + gebouwd. + 3. *Opgeslagen datums tonen* (issue #63-mechanisme): voor XER staat "Datums zoals + opgeslagen" standaard aan zodra er restverschillen zijn; de melding noemt het aantal + afwijkende taken en die taken krijgen een markering in tabel en eigenschappenpaneel. + Bewerken of F5 verlaat de modus zoals nu. Vereist een §4.1-uitbreiding — het laag-3- + besluit: de zes orakelkolommen (`early_start_date`, `early_end_date`, `late_start_date`, + `late_end_date`, `total_float_hr_cnt`, `free_float_hr_cnt`) verhuizen uit de verboden bak + naar de vierde bak "uitsluitend weergave/meetlat, nooit solverinvoer" (zie §4.1); uren → + werkdagen via de taak-effectieve kalender, geen `?? rec.start`/`?? 0`-terugval voor XER + maar een "niet vastgelegd"-representatie. Dekking gemeten: 18.388 van 18.398 cellen. + Poort: nul verschil na laag 1 voor bestanden mét SCHEDOPTIONS; de rest per bestand gepind met + aantal en oorzaak. Meetbaar-tellers blijven gepind (M1). +- **X-O8 — VERVALLEN (eigenaar, 2026-09-04, na de kalibratiemeting): afgeleide instellingen + toepassen en melden.** Was bedoeld voor laag 2 van X-O7 (ontbrekende SCHEDOPTIONS afleiden + uit een begrensd rooster van échte P6-instellingen, met een opening die de afgeleide waarden + toont en linkt naar de projectinstellingen). Doordat die laag verviel — geen enkel bestand + met een uniek optimum — is X-O8 zonder object en vervalt eveneens. + +### Afspraken met de etappe taaktypes / opgeslagen werk (2026-09-04) + +- **Volgorde**: de taaktypes-etappe start op een main waar XER al in zit; de tweede merge + (origin/main ná f16bfff7, incl. contour-engine PR #95) volgt direct na de eerste. +- **Curves**: na de tweede merge vervangt `normalizeCurveValues` + `matchCurveValues` + (`contourEngine.ts`) de eigen `bestFitXerCurve`, met terugval op `P6_NAME_TO_CURVE` op naam; + de 21 punten gaan naar `ResourceAssignment.curveValues`. Corpusfeit: RSRCCURV bestaat niet, + alleen RSRCCURVDATA (kolommen `pct_usage_0`..`pct_usage_20`, lineair); `curv_id` is + corpusbreed 2× gevuld. Het pad hoeft correct te zijn, niet rijk. +- **Taaktype**: `Task.p6DurationType` (pset `OPS_P6Progress`, property `DurationType`) is de + opslag waar de taaktypes-spec naar verwijst; de spec definieert alleen de vertaling van + `mspTaskType`+`effortDriven` naar dezelfde vier keuzes. Het neutrale documentveld bouwt de + taaktypes-etappe, niet XER. `p6xmlReader` leest `` nog niet: meenemen zodat + beide P6-paden gelijk zijn. Corpusfeit: `DT_FixedRate` 0× in het corpus, `DT_FixedQty` 153× + in 2 bestanden. +- **Opgeslagen werk**: geen nieuw modelveld vanuit XER. De spec definieert het eersteklasveld + op `ResourceAssignment`; XER zet het daarna over uit het bronarchief — alleen wanneer + `target_qty` afwijkt van `target_drtn_hr_cnt × target_qty_per_hr`, anders blijft het veld + afwezig (byte-identiek). Meetlat: HarbourPointe_AssistedLiving (98 afwijkende rijen, factor + 3), Harbour Point DCP-03 (factor 4; `remain_qty` zonder resttarief), p6_torture_test_v1 + (duur 0 met werk), plus 263 rijen zonder `target_qty_per_hr` in 5 bestanden. Corpusbreed: 176 + afwijkende werkrijen in 5 bestanden; het resttarief wijkt af in 27,5% van rehab-2. +- **Additief blijven**: `types/task.ts`, `types/resource.ts`, `taskSlice`, `resourceSlice`, + `documentContract`, `ifcPsets`, `taskColumnRegistry`, `fieldCoverage`. TODO-registratie in de + sectie "Contour-engine (2026-09)". + +- **X-O7 — VASTGELEGD (2026-09-05, met één genoteerde uitzondering): een fidelity-stap mag geen + as slechter maken.** De regel bestaat om te voorkomen dat een totaalcijfer gekocht wordt door + fouten van de ene as naar de andere te verschuiven. Hij verbiedt níét elke stap die een + *compensatiefout* blootlegt. + + **Uitzondering, etappe 7b (weekend-klemherstel in `xerCalendarData.ts`).** Deze stap verbetert + ls/lf/tf/ff met 1.426 cellen en verslechtert es/ef met 449 (es 618→940, ef 813→940 op + `rehab-2.xer`; 344 resp. 327 taken nieuw fout, 281× één en 56× twee werkdagen te laat, vrijwel + alle op de 842-kalendergroep). De uitzondering is toegestaan omdat de BRON de kalender bevestigt + en de rest als dossier is geregistreerd: + + 1. P6 zet nul ES/EF/LS/LF ín alle negen gereconstrueerde blokken tegen 811 in de drie dagen + eromheen, en die 811 zit volledig op de vier blokken binnen de projectperiode + (167/214/178/252) terwijl de overige vijf 0 ín én 0 in de rand hebben en dus niets bewijzen; + 2. P6's eigen opgeslagen vensters tellen op de OUDE kalender 10,00 werkdagen voor een taak van 7 + (`V3109400`) en 24,00 voor een taak van 21 (`V3109300`) — intern inconsistent — en op de + gereconstrueerde kalender exact 7,00 en 21,00. + + Daaruit volgt dat een deel van de eerdere es-treffers een compensatiefout was tussen een te korte + kalender en een te vroeg startanker. Zulke fidelity is niet beschermenswaardig, en een regel die + deze stap blokkeert maakt de volgorde onmogelijk: het ankergat is niet te diagnosticeren zolang de + kalender de fout in de vensterlengte verstopt. **Een geschonden planregel zonder geschreven + uitzondering is een tijdbom voor de volgende reviewer — vandaar deze notitie.** + +## §6 Banen en taken + +Vier banen, elk een eigen worktree (`.claude/worktrees/xer-{meetlat,lezer,motor,data}`). +X-nummering; volgorde binnen een baan is dwingend, banen parallel na X0/X1. + +### SERIEEL VOORAF + +**X0 — Typen, harness-skelet, superset-registratie.** Task-/ImportResult-velden als +compile-gedekte typen: P6-duration-type en -activiteitstype als eigen opgeslagen velden naast +`mspTaskType` (veld-als-signaal geldt ook voor typen), mét de vastgelegde afspraak dat de +latere taaktypes-etappe beide naar één interne superset mapt; suspend/resume-herkomstvlag +(X7); corpusscan-tooling (`OPS_XER_CORPUS`). Geen registry-entry (die komt bij X4). +**Acceptatie**: typecheck-poorten; lege-lezer-run produceert een lege maar welgevormde +baseline; mutatiebewijs: een veld uit de typelijst verwijderen ⇒ compile-fout in het +harness-skelet. + +**X1 — De meetlat éérst (baan M).** Eerst de meetkern eerlijk maken (planreview F2): +`measureFidelity` roept vandaag hard `solveMppBytes`/`scanGroundTruthTasks` aan — X1 begint +met het uitfactoriseren van het formaat-agnostische deel (`classify()`, de rijvorm, de +delta-administratie) naar een gedeelde kern, mutatie-bewezen byte-identiek voor de bestaande +.mpp-suite. Daarop: `xerGroundTruth.ts` (onafhankelijke %T/%F/%R-scan, zes assen + status + +act-datums + `driving_path_flag`, eigen encoding-afhandeling), `xerFidelity.ts`, +`check-xer-fidelity.ts` met **per-project-meting binnen het bestand** (X-O1: één bestand +kan meerdere projecten dragen; de grondwaarheidsscan leest álle taken, een opgelost document +draagt er één project van — de meetkern krijgt dus een expliciete per-project-lus in plaats +van de bestaande alles-of-niets-assertie op de taaktelling) en per-bestand-pinning van de +som, gededupliceerd per §2 (byte-hash + schema-vingerafdruk), per as afwijkings- én +meetbaar-tellers, +`OPS_XER_FIDELITY_REPORT`-modi, reason-verplichting bij elke niet-nul-pin. Plus de +p6-comparison-extractie naar `cases-p6-verified.json`: uitsluitend de `*_p6`-kolommen, mét +tijden, met een generator-script en een herkomstkop die de M4-voorbehouden documenteert. +**Acceptatie**: mutatie-bewezen (meetlatveld verleggen ⇒ rood; meetbaar-teller verlagen ⇒ +rood); de .mpp-suite draait byte-identiek op de uitgefactoriseerde kern. + +### BAAN F — formaat en lezer + +**X2 — XER-grammatica.** `src/services/xer/xerTables.ts`: ERMHDR (versie + veld 9 = +default-valuta), %T/%F/%R/%E, tabs zonder escaping, `""`-quotes, DEL-DEL-multiline in +notitievelden (incl. BOM/NUL-strip — herkomst MPXJ `NotesHelper`), de lege-eerste-token- +continuatieregel, onbekende tabellen overslaan, en de **CURRTYPE-tweepas** conform X-O5 +(inclusief token-decodering en de geen-CURRTYPE-default). Encoding per X-O4. Fout-tolerantie +als bewuste keuze: kapotte rijen verzamelen in een import-rapportstructuur (geen stille skip, +geen harde crash); de openingsmelding (X10) toont het aantal. De enum-tokenregel +(§4.7) geldt in de semantische lagen (X4a/X5), niet hier — de tokenizer kent geen enums; +X2 levert alleen de rauwe tokens plus de lege-kolomnaam-afhandeling (afsluitende tab). **Failure-mode-model +(gecorrigeerd, planreview)**: de echte poort is *verplichte P6-kolommen ontbreken ⇒ typed +fout* — het corpus bevat namelijk pseudo-XER-bestanden mét `%F`-headers maar niet-P6-kolommen +(`p6xer-basic.xer`: `Task_ID`/`Start_Date`/`Duration`); die mogen nooit als leeg-maar-geldig +project openen. **Acceptatie**: de robuustheids- en `kedular-*`-bestanden gepind op hun +verwachte rapportinhoud (NB: `p6xer-encodings.xer` is géén kapot bestand — geldige UTF-8 met +verzonnen tabelnamen; pin hem als "onbekende tabellen overgeslagen"); synthetische fixtures +per grammaticaregel; mutatiebewijs: CURRTYPE-tweepas uitschakelen ⇒ het komma-decimaal-fixture +ROOD. + +**X3 — De kalenderdecoder (DE KRITIEKE TAAK, zie §1-granulariteit).** +`src/services/xer/xerCalendarData.ts`: de structured-text-grammatica +(`(nr||naam(veld|waarde|…)(kinderen…))`, DEL-DEL-gescheiden) als eigen tokenizer; DaysOfWeek +(dag 1-7, s/f-uurblokken, 24-uurs én AM/PM-notatie), Exceptions (`d|n` = dagen sinds +1899-12-30, mét of zónder afwijkende uren), lege kalender ⇒ P6-default ma-vr 08:00-16:00, +`base_clndr_id`-hiërarchie in een tweede pas, `clndr_type`, en de uren-per-periode-velden met +de afleiding-uit-weekuren als **hoofdpad** (gemeten: in `rehab-2.xer` is `day_hr_cnt` voor +alle 124 kalenders leeg). **De XER-uurmodus-discriminator wordt hier expliciet uitgeschreven +en apart geaccepteerd (planreview V1)**: de bestaande promotieregels (a) >1 band/dag, +(b) middernacht-wrap, (b2) ≥1440 min dekken een één-bands-kalender 08:00-16:00 niet, terwijl +het orakel wél 16:00-tijden draagt — de XER-lezer krijgt een eigen (c)-anker +("XER-kalenderbanden dragen kloktijden ⇒ promoveerbaar"), met blast-radius-meting over het +corpus en een pin op het aantal gepromoveerde kalenders per bestand. **Acceptatie**: +corpusloze fixtures per grammatica-element (incl. AM/PM en de epoch-conversie); corpuspin op +het 124-kalender-monster; pariteitstest tegen de P6-XML-kalenderroute op een equivalent paar — +waarvoor `parseP6StandardWorkWeek` (nu niet-geëxporteerd, `p6xmlReader.ts:96`) geëxporteerd +wordt of de toets via `readP6XML` loopt; mutatiebewijs: de weekuren-afleiding uitschakelen ⇒ +de rehab-pin ROOD. + +**X4a — Kern-mapping + registry-entry (enkelproject).** `src/services/xer/xerReader.ts`, +eerst voor het geval één (niet-leeg) project — dit deblokkeert baan S en D. De PROJECT-rij +van dat ene project hoort hier (delta-check: die viel bij de knip tussen wal en schip): +projectnaam, datadatum, projectkalender-verwijzing en default-valuta; de *selectie* uit +meerdere projecten is X4b. PROJWBS (sorteren op `(parent_wbs_id, seq_num)`; +**WBS-rijen worden verzameltaken in onze bestaande structuur, nooit extra bladtaken — de +taaktelling moet 1:1 op het orakel passen, anders klapt elke meting** — planreview V8), TASK +(statussen; milestones uit het activiteitstype, **case-insensitief** — het corpus bevat +`TT_mile`/`TT_finmile`-kleine-lettervarianten; `TT_LOE` → `isHammock`; `TT_Rsrc` (2 corpusrijen) +en `TT_WBS` (1 rij) worden **als data gelezen, als gewone taak gepland en in de melding +genoemd** — geen eigen rekenmodel deze etappe, wél elk een synthetische fixture; duration- en +activiteitstype als opgeslagen data), TASKPRED (`PR_*` én de prefixloze variant uit 3 echte +bestanden — 5 dragers, waarvan 2 pseudo-XER die X2 weigert), constraints (`CS_*` incl. mandatory → `hard`), `ExternalRelation` voor +cross-project-randen. Format-registry-entry (`kind: 'text'`, lazy chunk, `canBeSaveTarget` +blijft IFC-only), i18n-foutmeldingen 14 talen. **Acceptatie**: eerste fidelity-nulmeting +draait en pint; `crawl-xer/p6diff-baseline.xer` (8 taken, alle zes assen) en +`crawl-xer-extra/p6difftool/sample-target.xer` exact op de datum-assen (planreview M5: de +`xer-corpus/cases/*`-fixtures dragen géén orakel en zijn parser-poort, geen fidelity-poort); +mutatiebewijzen: de prefixloze-`PR_`-tak uit ⇒ de **3** echte bestanden met een prefixloos +`pred_type` ROOD (her-check: 5 dragers waarvan 2 pseudo-XER die X2 weigert); de +case-insensitieve typematch uit ⇒ de kleine-letter-fixture ROOD. + +**X4b — Meervoudige import en baselines (X-O1 + X-O2 — dit ís de X-O2-taak).** Het +meervoudig-`ImportResult`-pad in de open-pijplijn (het eerste formaat dat één bestand tot +meerdere documenten opent): projectselectie en lege-project-regel per X-O1, +baseline-materialisatie per X-O2 (gekoppeld baselineproject → OPS-baseline op het +hoofdproject; dangling genegeerd + geteld; de begrenzingsregel dat de uitsluiting de +verzameling nooit leegmaakt, incl. cyclus- en zelfverwijzing), `externalLinks` met +synthetische fixture (nul corpusdekking), en een open-tijd-meting op het +15-projecten-bestand. **Acceptatie**: `stack_data_center_baseline.xer` levert één document +mét OPS-baseline; het OZB-bestand opent 12 documenten (3 lege overgeslagen en gemeld, 9 +dangling-baselines gemeld); mutatiebewijzen: de begrenzingsregel uit ⇒ de +zelfverwijzings-fixture opent niets ⇒ ROOD; baseline-materialisatie uit ⇒ de +stack-case ROOD. + +### BAAN S — motor en semantiek + +**X5 — SCHEDOPTIONS en de defaults-vraag.** Twee helften, beide verplicht: +*(a) de tabel lezen* — `sched_calendar_on_relationship_lag` → `lagCalendar` (P6-default: +predecessor), retained logic/progress override → `progressMode`, critical-definitie en +float-modus; **elke kolom uit de corpus-union wordt belegd als gemapt / genegeerd-met-reden / +TODO** (planreview V7 — o.a. `sched_use_project_end_date_for_float` en +`sched_lag_early_start_flag` raken float en hebben geen tegenhanger in `SchedulingOptions`; +die worden op zijn minst geregistreerd), en de mapping verhoudt zich expliciet tot wat +`mppReader`/`mspdiReader` al zetten én tot de `OPS_SchedulingOptions`-IFC-round-trip. +*(b) de defaults-paragraaf (planreview B2 — het meerderheidsgeval!)*: 36 van de 60 +orakelbestanden hebben géén SCHEDOPTIONS, waaronder `rehab-2.xer` (60% van het orakel). Voor +die populatie geldt een expliciet vastgelegde default-set, gefundeerd op **de gemeten +meerderheid in de SCHEDOPTIONS-dragende bestanden** (her-check: `sched_float_type` = `FT_FF` +in 41/50 rijen ⇒ finish float — een échte gedragsomslag t.o.v. onze 'smallest'-huisdefault; +retained logic 48/50 aan; open-eindes-niet-kritiek en lag-op-voorgangerskalender sporen met +onze defaults), per default blast-radius-gemeten over het corpus en gepind. **Acceptatie**: de +in-progress/retained-logic- en completed-successor-cases uit `cases-p6-verified.json` groen; +de 36-zonder-SCHEDOPTIONS-populatie per default-keuze gemeten en gepind; mutatiebewijs: +`lagCalendar` naar successor forceren ⇒ de multi-kalender-case ROOD. + +**X6 — Resources en toewijzingen** *(parallel aan X5 — geen afhankelijkheid, planreview §5)*. +RSRC/RSRCRATE/TASKRSRC: rollen-vs-resources-ID-naamruimten, units-schalen (1.000.000- en +×100-conventies per veld — herkomst MPXJ, onafhankelijk geverifieerd tegen corpuswaarden), +resourcekalender-koppeling. Curves-dossier klein en afgebakend: 2 corpusrijen met `curv_id` +plus RSRCCURVDATA (21-punts verdeling) — best-fit naar onze curve-typen, met de rauwe 21 +punten als opgeslagen data (eigenaarsprincipe; `timephasedContours`-patroon). **Acceptatie**: +corpuspins op `Roads_Project_TEC.xer` en `rehab-2.xer`; geen datumbeweging op bestanden zonder +resources; mutatiebewijs: de units-schaal op ×100 zetten ⇒ de resourcecase ROOD. + +**X7 — Suspend/resume en voortgang.** P6's suspend/resume ↔ `TaskTime.stop`/`resume`: het +veld bestaat, maar de solver-semantiek eromheen is de MSP-conventie uit etappe 1 — eerst meten +(22 corpustaken met suspend), dan per verschil een bron-vlag (O6-patroon). Voortgang per +`complete_pct_type` met **een expliciet criterium per variant (planreview V5)**: `CP_Drtn` +(16.813 taken — percentage stuurt restduur), `CP_Phys` (1.492 — percentage stuurt de datums +NIET; restduur komt uit `remain_drtn_hr_cnt`) en `CP_Units` (8 — idem, units-gedreven; als +data gelezen, gedrag gelijk aan CP_Phys deze etappe, geregistreerd). `expect_end_date` (246 +cellen, 3 bestanden) uitsluitend gehonoreerd wanneer `sched_use_expect_end_flag` het zegt +(planreview V6) — anders als data bewaard. **Acceptatie**: het out-of-sequence-scenario uit de +P6-geverifieerde cases groen; de suspend-dragende bestanden gepind; mutatiebewijs op elke +nieuwe vlag-tak én op de CP_Phys-scheiding (percentage wijzigen ⇒ datums bewegen NIET). + +### BAAN D — data en randen + +**X8 — Activity codes, UDF's, notities.** ACTVTYPE/ACTVCODE/TASKACTV → `activityCodeTypes` +(119.878 corpus-koppelingen — prestatie meten op `rehab-2.xer` en het Hotel-schema); +UDFTYPE/UDFVALUE → `customFieldDefs`; memo-tabellen → taaknotities (DEL-DEL uit X2). +**Acceptatie**: IFC-round-trip aangetoond; tellingen gepind; mutatiebewijs: de +TASKACTV-koppeltabel overslaan ⇒ de telling-pin ROOD. + +**X9 — Documentcontract, round-trip, exportranden én de P6-XML-drift.** Alle nieuwe velden +door documentcontract en IFC-round-trip; exportranden warnen (patroon etappe 1); +`moveProject`-verdicten; MCP-leeskant conform het etappe-1-besluit. **Nieuw (planreview V3)**: +na deze etappe leest een `.xer` méér P6-data dan onze eigen `.xml`-lezer (activity codes, +UDF's, typen, schedulingOptions) — die asymmetrie wordt niet stil: een geregistreerd besluit +plus TODO ("p6xmlReader bijtrekken tot pariteit") én een pariteits-smoketest die de asymmetrie +expliciet documenteert in plaats van hem te laten verrassen. **Acceptatie**: het +Z14-mutatiestramien (property weg ⇒ rood op precies dat veld; byte-identieke examples). + +**X10 — Melding en gidsen.** Openingsmelding naar het Z16-model: echte tellingen (kapotte +rijen; geopende projecten, overgeslagen lege projecten en als baseline gematerialiseerde +projecten (X-O1/X-O2); dangling baselines; encoding-keuze bij niet-ASCII (X-O4)), severity +info, 14 talen, CLDR-pluralen. Gidsen (nl+en): "Primavera P6 +(.xer) openen" naar het model van de .mpp-gids — elke claim code-/testverwezen, mét de +X-O5-getalnotatie-paragraaf (de derde documentatieplek uit dat besluit), en eerlijk over wat +(nog) niet meekomt (TT_Rsrc/TT_WBS-rekenmodel, curves-als-verdeling, P6-XML-asymmetrie — +multi-document komt per X-O1 juist wél mee en staat in het geopende-projecten-deel van de +melding en de gids). **Acceptatie**: de Z16-mutatiebewijzen (melding/i18n/manifest). + +### SERIEEL — afronding + +**X11 — Browser-gebruikstest** (aparte agent, tier 1; mag parallel aan X12's residu-iteratie): +de dossierselectie openen (`p6diff-baseline`, `rehab-2.xer`, multi-kalender, negatieve float, +het torture-bestand, en het 15-projecten-bestand als multi-document-stresstest: 12 documenten +in één keer — raakt de auto-save (één recovery-snapshot per document), de documenttabbalk bij +12+ tabbladen en de Ctrl/⌘ 1–9-navigatie die maar tot negen reikt), IFC-opslaan/heropenen met +veldbehoud, +F5-stabiliteit, meldingen en gidslinks, taalwissel, undo/documentwissel — het Z18-draaiboek, +plus: blijft de app vlot op het 119k-koppelingen-bestand. + +**X12 — Residu naar nul en de eindpoort.** Detail-rapportage per as → classificeren op +bladniveau → echte fout fixen met bewijs, of escaleren (X-O3); "pinnen met reden" bestaat niet +als uitweg. Daarna `GOAL_ZERO_DEVIATIONS_XER` aan: nul op alle afwijkingstellers, per as +`gemetenExact === meetbaar` én het **gepinde meetbaar-aantal** zelf (planreview M1 — een as +die stil naar nul meetbaar zakt is rood), reason-verbod, en de tweeledige dedup-bewaking (byte-hash + schema-vingerafdruk, §2). +TODO-registraties (XER-export; p6xml-pariteit; driving-path-as als poortkandidaat). Hyperkritische Opus-eindreview over de volledige etappe-diff, inclusief de +whitelist-sluiproute-scan van §4.1. + +## §7 Parallellisering + +``` +X0 ─ X1 (serieel; X1 bevat de meetkern-uitfactorisering + per-project-lus) + ├── baan F: X2 → X3 → X4a → X4b ─┐ X3 = kritieke taak (uurmodus beslist alles) + ├── baan S: (na X4a) X5 ─┐ │ + │ (na X4a) X6 ─┤ → X7 ─┤ + ├── baan D: (na X4a) X8 → X9 ──────┤ + │ (na X9 én X4b) X10 ──┤ + └──────────────── X11 ∥ X12 (X11 mag parallel aan de residu-iteratie) +``` +X4b (meervoudige import) loopt parallel aan de banen S en D; X10 wacht op X9 én X4b (de +meldingstellingen over geopende/lege/baseline-projecten bestaan pas met X4b), en X11 wacht +op X4b voor de 12-documenten-stresstest. +X5 en X6 zijn onderling onafhankelijk en lopen parallel; X7 wacht op beide (voortgang leunt op +actuals-invarianten én toewijzingen). De meetlat (X1) staat vóór alles — wie eerst bouwt en +dan meet, meet zijn eigen aannames. + +## §8 Risico's, eerlijk benoemd + +1. **De float-assen zijn onontgonnen terrein**, en het defaults-gat maakt het scherper: voor + 60% van de orakelbestanden (36 van 60, incl. de grootste massa) bepalen ónze + default-aannames de late datums en de float. X5(b) is daarom geen administratie maar een + meetprogramma. Negatieve float zit in **zes** bestanden (45/6/6/3/2/1 taken — planreview + B4; het concept zei twee), waarvan vijf zonder SCHEDOPTIONS — dat wordt een dossier. +2. **`clndr_data` is de bytepuzzel én de kritieke taak** — de uurmodus-promotie (§1) en de + uren-per-dag-afleiding (hoofdpad, niet randgeval) bepalen de meetbaarheid van élk bestand. +3. **Encoding en getalnotatie zitten ín het bestand** (X-O4/X-O5); fouten hier zijn stil. + Vandaar eigen fixture-batterijen in X2 en de meldings-mitigatie. +4. **Schaal**: `rehab-2.xer` (6.976 orakeltaken, 52.640 toewijzingen, 81k code-koppelingen) is + ~2× het grootste bestand dat de app ooit las; X11 meet het expliciet. +5. **Scope**: multi-document-import en baseline-materialisatie zijn per eigenaarsbesluit + onderdeel van de etappe geworden (X-O1/X-O2) — dat is de bewuste verzwaring; XER-export, + het TT_Rsrc-rekenmodel en p6xml-pariteit blijven begrensde vervolgtrajecten. De goal is + lezen-getrouw-op-vier-assen; de multi-documentroute is er de gebruikerszichtbare helft van. + +## §9 Dossiers uit de etappe + +### 7b-4 — forward-anker na gereconstrueerde kalenderblokken + +**Status:** geregistreerd, niet gebouwd. Ontstaan bij etappe 7b (weekend-klemherstel), her-check +2026-09-05. + +**Wat er staat.** Op `rehab-2.xer` verschuiven 344 ES- en 327 EF-cellen van goed naar fout +(es 618→940, ef 813→940), 343 daarvan op de 842-kalendergroep; 281× één werkdag te laat, 56× twee, +2× zes en 3× elf. Ze lopen van 2008-11 t/m 2010-03, dus vanaf direct ná het oktoberblok. + +**Wat het NIET is.** Geen kalenderfout. Gemeten op de gereconstrueerde kalender telt ons ES→EF-venster +voor alle vijf de getraceerde taken exact evenveel werkminuten als dat van P6: + +| taak | duur | P6-venster op de OUDE kalender | op de NIEUWE kalender | ons venster (nieuw) | +|---|---|---|---|---| +| `V3109400` | 7 d | 10,00 werkdagen | 7,00 | 7,00 | +| `V3109300` | 21 d | 24,00 werkdagen | 21,00 | 21,00 | +| `V3109420` | 7 d | 7,00 | 7,00 | 7,00 | +| `V3109220` | 3 d | 3,00 | 3,00 | 3,00 | +| `V3109480` | 7 d | 7,00 | 7,00 | 7,00 | + +De duurwandeling klopt dus; alleen het STARTANKER ligt één tot twee werkdagen te laat. Concreet: +P6 zet `V3109400` op 2008-12-04 → 12-24, wij op 12-06 → 12-25 — hetzelfde venster van zeven +werkdagen, twee werkdagen naar rechts. + +**Waar te beginnen.** Die vijf taken, en de vraag wat P6 aan de forwardkant doet dat wij niet doen +zodra een keten een meerdaags vrij blok kruist (kandidaten: retained-logic/voortgangssemantiek rond +de statusdatum, of de `ownAnchor`-vloer voor taken met een voorganger vlak vóór een blok). + +**n=1-basis.** Het weekend-klemherstel zelf is afgeleid uit één bestand van 93 +(`crawl-xer-extra/jailaff-xer-splitter/rehab-2.xer`, P6 6.0-export, kalenderdata gedeeld door 119 van +de 124 kalenders). De reconstructie wordt binnen dat bestand door de bron bevestigd (zie de twee +metingen bij X-O7 hierboven). Wat er níét is: een tweede bestand. MPXJ, de referentie-implementatie, +kent het verschijnsel niet en leest de `Exceptions`-lijst letterlijk +(`TableContextReader.processCalendarExceptions`). De poort is daarom bewust record-lokaal en +conservatief — drie eisen op het record zelf, corpusloos gepind in `check-xer-calendar-data.ts` +sectie 22a–22g. Duikt er een tweede bestand op, dan bevestigt of ontkracht dat de regel; het mag er +niet stil op meeliften. diff --git a/docs/superpowers/specs/2026-09-04-spec-taaktypes-opgeslagen-werk.md b/docs/superpowers/specs/2026-09-04-spec-taaktypes-opgeslagen-werk.md new file mode 100644 index 000000000..46b3eb607 --- /dev/null +++ b/docs/superpowers/specs/2026-09-04-spec-taaktypes-opgeslagen-werk.md @@ -0,0 +1,767 @@ +# Taaktypes, opgeslagen werk en effort-driven plannen — ontwerp + +*Ontwerp, 2026-09-04. Opvolger van het voorstel +[`2026-08-18-spec-taaktypes-effort-driven.md`](2026-08-18-spec-taaktypes-effort-driven.md) +("ontwerp vóór bouw"). Status: **in aanbouw op de branch `claude/contour-engine-planner-mnrsy3` +(PR #101, gestapeld op de XER-branch): stappen 1 t/m 7 zijn gebouwd (2026-09-05); stap 6 is +daarin voor de store/raster/MCP-kant meegenomen (resource erbij/eraf + contourhoogte, besluit 3); +stap 8 (afronding docs) volgt.** +De contour-engine, de contour-UI en de fasen-editor (2026-09) zijn geleverd en worden hier als +bestaand fundament gebruikt.* + +## 0. Leeswijzer + +Elke bewering in dit document draagt één van deze labels, zodat een bouwer ziet waar hij op kan +leunen en waar niet: + +| label | betekenis | +|---|---| +| **ZEKER** | staat letterlijk in de documentatie van Microsoft of Oracle (bron in §13), of in onze eigen code | +| **GEMETEN** | geteld op een corpus (het XER-corpus van de XER-sessie, §11) | +| **AFGELEID** | volgt met een korte redenering uit ZEKER/GEMETEN feiten; de redenering staat erbij | +| **BEREDENEERD** | ontwerpkeuze zonder bron; de meetlat (§9) moet dit later tegen MS Project toetsen | +| **ONBEKEND** | niet gevonden in documentatie en niet gemeten; expliciet open | +| **BESLOTEN** | eigenaarsbesluit (§3) | + +De eigenaar vroeg om "beredeneren + grondig onderzoek in de documentatie van MS Project en P6" als +bewijs, omdat er niemand met MS Project beschikbaar is om te meten (besluit 4). Dat is de reden dat +§2 zo uitgebreid is en dat §9 een concrete lijst bewerkingen bevat die een latere meting kan +afwerken. + +## 1. Waar dit over gaat + +Eén rekensom: **werk = duur × inzet.** Ken je er twee, dan ligt de derde vast. Het karakter van een +planningspakket wordt bepaald door de vraag welke van de drie *beschermd* is wanneer de gebruiker +aan een van de andere twee draait. + +Open Planner Studio beschermt vandaag altijd duur en inzet: dat zijn de opgeslagen velden +(`scheduleDuration`/`durationMinutes` op de taak, `unitsPerDay` op de toewijzing), en werk wordt er +elke keer uit afgeleid — "werk = duur × unitsPerDay, altijd afgeleid, nooit opgeslagen" staat +letterlijk in `src/types/resource.ts` (ZEKER). In de terminologie van P6 is dat **Fixed Duration & +Units/Time**; in die van MS Project **Fixed Duration, niet effort-driven** (AFGELEID uit de tabellen +in §2.1 en §2.2: bij een duurwijziging beweegt het werk mee, bij een inzetwijziging beweegt het werk +mee, een resource erbij voegt werk toe). + +Deze etappe maakt die keuze per taak instelbaar en maakt werk een echte, opgeslagen grootheid. De +kernregels uit het voorstel van 2026-08-18 blijven staan: standaard verandert er niets; de +aan-knop stuurt alleen de weergave en nooit de berekening; de semantiek zit in het document. + +## 2. Wat MS Project en P6 doen + +### 2.1 MS Project + +**De drie taaktypes en de rekentabel** (ZEKER, bron [M1], [M4]): + +| taaktype | inzet gewijzigd | duur gewijzigd | werk gewijzigd | +|---|---|---|---| +| Fixed Units | duur herberekend | werk herberekend | duur herberekend | +| Fixed Work | duur herberekend | inzet herberekend | duur herberekend | +| Fixed Duration | werk herberekend | werk herberekend | inzet herberekend | + +**Standaardtype** is Fixed Units (ZEKER, [M4]: "Fixed Units (default)"); de gebruiker kan dat in de +projectopties wijzigen. + +**Effort-driven** (ZEKER; bron per punt): + +- Effort-driven gaat uitsluitend over het **toevoegen of verwijderen van resources** ([M2]): + "Project lengthens or shortens the duration of the task based on the number of resources that are + assigned to it, but Project does not change the total work for the task." Bij Fixed Units verkort + een resource erbij de duur; bij Fixed Duration daalt de inzet van elke resource ([M2]). +- Effort-driven werkt pas **ná de eerste toewijzing** ([M3]): "If you assign multiple resources at + the same time, the duration doesn't change from your original estimate." De eerste toewijzing(en) + zetten het werk; daarna houdt effort-driven dat werk vast. +- **Fixed Work is altijd effort-driven** en dat vinkje is daar niet te wijzigen ([M1]): "Project + doesn't consider fixed work tasks to have flexible work values and are therefore always + effort-driven." +- Samenvattingstaken en ingevoegde projecten kunnen niet effort-driven zijn ([M2]). +- Standaardwaarde van het vinkje voor nieuwe taken: de Microsoft-artikelen die ik vond noemen alleen + dát er een optie "New tasks are effort driven" bestaat, niet wat hij standaard is ([M2], [M3]). + Secundaire bronnen (trainingsmateriaal, [M9]) zeggen "standaard uit" voor 2010/2013/2016. Ik + markeer dit als **AFGELEID uit secundaire bron**, niet ZEKER. + +**Werk, verricht werk en restwerk** (ZEKER, [M5], [M6], [M7], [M8]): + +- Taakwerk = som van het werk van alle toewijzingen. Toewijzingswerk = spanne × inzet + ("The span of an assignment is multiplied by the assignment units to calculate the amount of + work": 100 % op één dag = 8 uur, 50 % = 4 uur, 200 % = 16 uur). +- Restwerk = werk − verricht werk; restduur = duur − verrichte duur. Wie de restduur wijzigt, houdt de + verrichte duur vast en verandert de totale duur ("Project changes duration to match the sum of + the remaining duration and actual duration and leaves the actual duration unchanged"). +- Wie taakwerk of verricht werk op taakniveau invoert, laat Project dat "onder de toegewezen + resources verdelen"; **hoe** die verdeling gaat (evenredig aan inzet? aan bestaand werk?) staat er + niet bij — ONBEKEND. Voor tijdgefaseerde invoer zegt [M7] wel "in the same proportion of work + scheduled for each assigned resource at that point in time". +- Wat een wijziging van de *totale* duur op een lopende taak met verrichte duur doet, staat niet + letterlijk in [M6]/[M8]. AFGELEID: verricht werk en verrichte duur zijn feiten die geen enkele + formule in de artikelen aanpast; de enige velden die de formules laten bewegen zijn duur, restduur, + restwerk en % gereed. Dit is precies de vraag die aan het eind van het vorige gesprek open bleef + ("houdt MS Project het rest- of het totaalwerk vast bij een duurwijziging op een lopende + Fixed-Work-taak"): documentatie geeft er geen uitsluitsel over; §5 kiest "de driehoek werkt op het + restant" en §9 (meting 16–18) legt dat vast als te toetsen. + +### 2.2 Primavera P6 + +**De vier duration types en hun formules** (definities ZEKER uit [P1]; de twee +Fixed-Duration-formules ZEKER uit [P2]; voor de andere twee types geeft geen van beide pagina's +een formule — de regel daar is AFGELEID uit de algemene identiteit in de inleiding van [P3], +"Duration = Units ÷ (Resource Units ÷ Time)"): + +| duration type | wat vastligt | formule | +|---|---|---| +| Fixed Duration & Units/Time | duur en inzet per tijdseenheid | Remaining Units = Units/Time × Remaining Duration (ZEKER, [P2]) | +| Fixed Duration & Units | duur en totaal werk | Units/Time = Remaining Units / Remaining Duration (ZEKER, [P2]) | +| Fixed Units/Time | inzet per tijdseenheid | Duration = Units / (Units/Time) (AFGELEID uit [P3]) | +| Fixed Units | totaal werk | idem (AFGELEID uit [P3]) | + +Let op de woorden in de twee gedocumenteerde formules: **Remaining** Units en **Remaining** +Duration. Dat P6 de driehoek op het resterende deel rekent is daarmee ZEKER voor de twee +Fixed-Duration-types en **AFGELEID** voor Fixed Units/Time en Fixed Units: [P2] zegt daar niets +over rest versus totaal. De redenering: P6 houdt per toewijzing Actual, Remaining en At Completion +apart bij en herrekent Remaining of At Completion uit elkaar ([P5]), dus een regel die op het +totaal zou werken zou verrichte uren moeten herschrijven — wat geen van de opties in [P5] doet. +Dit is de basis van de keuze in §5 en staat daarom in §9 (cases 16–18) als te meten. + +**De synchronisatietabel** (ZEKER, [P3] — Oracle's eigen overzicht van wat er per type verandert): + +| duration type | inzet (units) gewijzigd | duur gewijzigd | inzet/tijd gewijzigd | resource erbij zonder werk | extra resource erbij | +|---|---|---|---|---|---| +| Fixed Units/Time | duur | werk | duur | werk | duur | +| Fixed Duration & Units/Time | inzet/tijd | werk | werk | werk | werk | +| Fixed Units | duur | inzet/tijd | duur | werk | duur | +| Fixed Duration & Units | inzet/tijd | inzet/tijd | werk | werk | inzet/tijd van elke resource | + +(In P6-termen: "Units" = totaal werk in uren, "Units/Time" = inzet per tijdseenheid. Ik schrijf hier +werk en inzet/tijd om verwarring met OPS' `unitsPerDay` te voorkomen.) + +**Overige P6-regels** (ZEKER): + +- Het duration type doet pas iets zodra er minstens één resource op de activiteit staat ([P2]: + "The duration type applies only when you have at least one resource assigned to the activity"). +- Bij Start- en Finish-mijlpalen is het veld uitgeschakeld ([P1]). +- Projectoptie bij het toevoegen van resources ([P4]): "Preserve the Units, Duration, and Units/Time + for existing assignments" óf "Recalculate the Units, Duration, and Units/Time for existing + assignments based on the activity Duration Type". De laatste kolom van de synchronisatietabel + geldt dus alleen bij de tweede instelling. +- Projectoptie bij actuals ([P5]): "Add actual to remaining" (At Completion Units = Remaining Units + + Actual Units) óf "Subtract actual from at completion" (Remaining Units = At Completion Units − + Actual Units). AFGELEID uit die twee formules: verricht werk is in beide varianten invoer en wordt + door geen van beide herschreven. +- Standaard duration type voor nieuwe activiteiten: niet gevonden in de geraadpleegde pagina's — + ONBEKEND uit documentatie. GEMETEN (§11): 89 % van de activiteiten in het XER-corpus staat op + `DT_FixedDUR2` (Fixed Duration & Units). + +### 2.3 Waar ze verschillen — en de consequentie voor besluit 1 + +Zet je de tabellen van §2.1 en §2.2 naast elkaar, dan zijn drie MS Project-combinaties exact gelijk +aan een P6-type, en twee niet (AFGELEID, cel voor cel uit [M1] en [P3]): + +| MS Project | P6 | overeenkomst | +|---|---|---| +| Fixed Units, effort-driven | Fixed Units/Time | alle vijf kolommen gelijk | +| Fixed Work (altijd effort-driven) | Fixed Units | alle vijf kolommen gelijk | +| Fixed Duration, niet effort-driven | Fixed Duration & Units/Time | alle vijf kolommen gelijk | +| Fixed Duration, effort-driven | Fixed Duration & Units | **verschil bij duur gewijzigd**: MSP herberekent werk ([M1], rij Fixed Duration), P6 herberekent inzet/tijd ([P3]) | +| Fixed Units, niet effort-driven | — | **geen P6-tegenhanger**: MSP combineert hier *inzet gewijzigd ⇒ duur herberekend* met *resource erbij ⇒ werk groeit, duur blijft*. In [P3] gaat de eerste eigenschap alleen samen met de twee types die bij een extra resource juist de duur verkorten (Fixed Units/Time, Fixed Units); de twee types die de duur laten staan herberekenen bij een inzetwijziging het werk | + +Besluit 1 (menukaart = de vier P6-types, geen los vinkje) dekt dus drie van de vijf +MS Project-gedragingen exact. De twee andere zijn geen exotische gevallen: "Fixed Units, niet +effort-driven" is de fabrieksinstelling van MS Project (Fixed Units is ZEKER standaard, het vinkje +staat volgens secundaire bronnen standaard uit), dus vermoedelijk het meest voorkomende type in +.mpp-bestanden — GEMETEN is dat niet (het .mpp-fidelity-corpus is hier niet beschikbaar; een telling +van `mspTaskType × effortDriven` over de `OPS_MPP_CRAWL`-set is in §12 als TODO opgenomen). + +Dit is een nieuw feit ten opzichte van het gesprek van 2026-09-04 en vraagt om een aanvulling op +besluit 1. Het staat in §3 als **beslispunt 8** met drie opties en een advies; de bouwvolgorde (§10) +zet het gedrag "resource erbij/eraf" bewust als aparte, late stap zodat de rest niet wacht. + +## 3. Besluiten + +### 3.1 Al vastgelegd (2026-08-18, voorstel + eigenaarsbesluit in het mpp-plan) + +1. De volledige motor komt er, **opt-in**; standaard blijft alles zoals nu. +2. De knop stuurt **alleen de weergave, nooit de berekening**; taaktypes zijn documentdata. +3. Een bestand met taaktypedata **ontsluit de weergave automatisch** voor dat document, met melding. +4. Het **eigenschappenpaneel** krijgt de instelling; een tabelkolom mag mee zodra het kolommensysteem + dat draagt (de tabelrevisie van 2026-08-24 bestaat inmiddels — ZEKER, `taskColumnRegistry.ts`). +5. Semantiek werkt **alleen op bewerkingen**, nooit bij het openen/herberekenen van een bestand + (anders verschuiven de gepinde importdatums). +6. Import **bewaart** MSP's velden (gedaan: `Task.mspTaskType`, `Task.effortDriven`, pset + `OPS_MspTaskType`; alleen de `.mpp`-lezer vult ze — ZEKER, `mppReader.ts`; de MSPDI-lezer leest + ``/`` op taakniveau vandaag **niet**, ZEKER, `mspdiReader.ts`). +7. De **contour-engine** hoort bij deze etappe (gedaan, 2026-09). +8. **Hele-eenheden-afronding** alleen op het formulepad, nooit op opgeslagen dagwaarden (gedaan). +9. Het interne model is **neutraal** tussen MSP en P6 (superset). +10. De **bewerken-meetlat** wordt vóór de motorbouw ontworpen (→ §9). +11. Geen tijdstip; geen gebruikersvraag die dit voorrang geeft. + +### 3.2 Genomen op 2026-09-04 + +| nr | besluit | +|---|---| +| 1 | Menukaart = **P6-vorm: vier types, geen los effort-driven-vinkje**; intern een superset die MSP dekt (zie beslispunt 8 voor de twee gevallen die niet passen). | +| 2 | **Een typewissel verandert geen enkel getal**; alleen welke hoek voortaan beschermd is. Bij wissel naar een type dat werk beschermt wordt het huidige werk vastgelegd zoals het is. | +| 3 | Resource toevoegen op een gecontoureerde taak met vast werk: **vorm blijft, hoogte zakt evenredig; de nieuwe resource is vlak; de duur wordt korter.** "Volg MS Project, meting bepaalt details." | +| 4 | Bewijs = **beredeneren + grondig documentatieonderzoek**, elke detailregel expliciet; meting tegen MSP zelf in de TODO met een bewerkingenlijst. | +| 5 | **Aan-knop = app-instelling** ("Toon taaktypes"), plus automatische ontsluiting per document (3.1-3). | +| 6 | Reikwijdte: **alleen gewone bladtaken op werktijd, en uurtaken.** | +| 7 | **Nivelleerder blijft alleen verschuiven**; "inzet verlagen bij vast werk" komt in de TODO als geavanceerde optie. | + +### 3.3 Genomen op 2026-09-05 (beslispunten 8–10) + +| nr | besluit | +|---|---| +| 8 | **Optie B**: vier types in het menu; het bewaarde `Task.effortDriven` stuurt alleen de twee cellen waar MS Project van P6 afwijkt (§5). Taken die in OPS worden aangemaakt krijgen het veld nooit. | +| 9 | **Drie optionele werkvelden per toewijzing** (begroot / verricht / resterend); de driehoek werkt op het restant, begroot blijft referentie (§4.3). | +| 10 | **De vereenvoudiging "elke toewijzing loopt over de hele restduur" is geaccepteerd** voor deze etappe; de per-toewijzing-spanne staat in `docs/TODO.md` als vervolg (§6.2, §12). | + +De afwegingen achter de drie punten staan hieronder bewaard zoals ze aan de eigenaar zijn +voorgelegd. + +**Beslispunt 8 — de twee MS Project-gedragingen die niet in de vier P6-types passen (§2.3).** + +| optie | wat het betekent | gevolg | +|---|---|---| +| A. Vier types, punt | Een MSP-taak "Fixed Units, niet effort-driven" wordt Fixed Units/Time en "Fixed Duration, effort-driven" wordt Fixed Duration & Units. | Eenvoudigst. Maar een geïmporteerde MSP-standaardtaak krijgt na "resource erbij" een kortere duur waar MSP de duur zou laten staan, en een effort-driven Fixed-Duration-taak houdt bij een duurwijziging het werk vast waar MSP het werk laat meegroeien. Dat is de "onverwacht verschuivende planning" die de UX-opgave juist wil vermijden. | +| B. Vier types in het menu, `effortDriven` blijft bewaarde invoer die alleen de afwijkende kolommen stuurt **(advies)** | Het bestaande veld `Task.effortDriven` blijft staan zoals het nu al round-tript. De regeltabel (§5) leest het op precies de twee cellen waar MSP afwijkt: Fixed Units/Time + `effortDriven === false` ⇒ resource erbij/eraf verandert werk in plaats van duur; Fixed Duration & Units + `effortDriven === true` ⇒ duur gewijzigd verandert werk in plaats van inzet. Taken die de gebruiker in OPS aanmaakt krijgen het veld nooit en volgen zuiver P6. | Het menu blijft vier keuzes (besluit 1 intact), MSP-bestanden gedragen zich als in MSP, en de afwijking is zichtbaar te maken als bijschrift ("uit MS Project: niet effort-driven") in plaats van als vinkje. Kost één extra rij in de regeltabel en twee testgevallen. | +| C. Zes types | Het menu toont de volledige unie (vier P6 + twee MSP). | Volledig, maar precies de "MSP-verwarring" die de eigenaar niet wil importeren; verworpen door besluit 1. | + +Gekozen: B. De rekenkern van bouwstap 3 implementeert die twee cellen al (`workTriangle.ts`, +`TriangleState.effortDriven`; cases wt-05, wt-05b en wt-11b). + +**Beslispunt 9 — drie optionele werkvelden per toewijzing (§4.3).** Aanbeveling uit de corpusscan +(GEMETEN: het resttarief `remain_qty_per_hr` wijkt in vijf bestanden structureel af van het +begrote tarief, tot 100 % van de rijen), uitgelegd aan de eigenaar op 2026-09-04 met het +metselwerkvoorbeeld (160 uur begroot, 80 verricht, 120 resterend). Gekozen: de drie velden. + +**Beslispunt 10 — meerdere toewijzingen en de taakduur (§6.2).** OPS kent geen duur per toewijzing; +het ontwerp kiest de vereenvoudiging "elke toewijzing loopt over de hele restduur van de taak" en +legt de per-toewijzing-spanne in de TODO. Dat wijkt af van MSP en P6, waar toewijzingen een eigen +spanne hebben. Gekozen: accepteren, mét de TODO-notitie. + +## 4. Datamodel + +### 4.1 De werkregel per taak + +Een nieuw optioneel taakveld, neutraal genoemd zodat het niet met `taskType` (OPS-domeinklasse), +`mspTaskType` (MSP-import) of `durationType` (WORKTIME/ELAPSEDTIME) botst: + +```ts +/** Welke hoeken van werk = duur × inzet beschermd zijn bij een BEWERKING (taaktypes-etappe). + * Afwezig ⇒ de projectstandaard (`Project.defaultWorkRule`), en als die ook ontbreekt + * FIXED_DURATION_RATE — het gedrag van vandaag, byte-identiek. Geen enkele solverstap leest dit + * veld; het werkt uitsluitend in de bewerkingslaag (§5). */ +export type WorkRule = + | 'FIXED_DURATION_RATE' // P6 Fixed Duration & Units/Time · MSP Fixed Duration, niet effort-driven · vandaag + | 'FIXED_DURATION_WORK' // P6 Fixed Duration & Units · MSP Fixed Duration, effort-driven (zie beslispunt 8) + | 'FIXED_WORK' // P6 Fixed Units · MSP Fixed Work + | 'FIXED_RATE'; // P6 Fixed Units/Time · MSP Fixed Units, effort-driven (niet effort-driven: zie beslispunt 8) +``` + +Gebruikersnamen (nl): *Vaste duur en inzet*, *Vaste duur en werk*, *Vast werk*, *Vaste inzet*. Elk +met een bijschrift van één zin in het paneel ("Bij een duurwijziging beweegt het werk mee" enz.). + +"Rate" staat voor `unitsPerDay` (inzet per werkdag), P6's units/time. Ik vermijd "units" omdat P6 +er totaal werk mee bedoelt en MSP inzet — precies de verwarring die het neutrale model moet +wegnemen. + +`Project.defaultWorkRule?: WorkRule` is de projectstandaard (het "taaktype als projecteigenschap" +uit het eigenaarsbesluit van 2026-08-18). Nieuwe taken krijgen **geen** eigen veld; ze volgen de +projectstandaard totdat de gebruiker per taak kiest. Afwezig ⇒ FIXED_DURATION_RATE. + +Beide velden horen in `DOCUMENT_FIELDS` (documentcontract) — anders overleven ze geen documentwissel, +undo of crashherstel (ZEKER, `documentContract.ts`). + +### 4.2 Vertaling van de importvelden + +De bestaande importvelden blijven **onaangeraakt** staan (het `manuallyScheduled`-precedent O3: één +klasse taakvelden, altijd round-trip). De werkregel wordt er bij import uit **afgeleid en apart +opgeslagen**, zodat een latere typewissel de herkomst niet vernietigt: + +| bron | invoer | `workRule` | +|---|---|---| +| MSP (.mpp, MSPDI) | Fixed Units + effort-driven | FIXED_RATE | +| | Fixed Units, niet effort-driven | FIXED_RATE **+ `effortDriven: false` blijft staan** (beslispunt 8) | +| | Fixed Duration, niet effort-driven | FIXED_DURATION_RATE | +| | Fixed Duration + effort-driven | FIXED_DURATION_WORK **+ `effortDriven: true` blijft staan** (beslispunt 8) | +| | Fixed Work | FIXED_WORK | +| P6 XML | `DurationType` = Fixed Duration and Units/Time | FIXED_DURATION_RATE | +| | Fixed Duration and Units | FIXED_DURATION_WORK | +| | Fixed Units | FIXED_WORK | +| | Fixed Units/Time | FIXED_RATE | +| XER (XER-branch) | `DT_FixedDrtn` | FIXED_DURATION_RATE | +| | `DT_FixedDUR2` | FIXED_DURATION_WORK | +| | `DT_FixedQty` | FIXED_WORK | +| | `DT_FixedRate` | FIXED_RATE | +| | `DT_FixedDUR` (niet-standaard, 24 activiteiten in p6difftool-fixtures, GEMETEN) | de XER-lezer zelf valt bij een onbekend token gerapporteerd terug op de projectstandaard (`PROJECT.def_duration_type`, XER-etappeplan §4.7) en zet dát als `p6DurationType`; de werkregel volgt die standaard. Het rauwe token blijft in het bronarchief. (Bijgewerkt 2026-09-05 na de merge met de XER-branch; de oude regel "geen werkregel" is daarmee vervallen.) | + +De P6 XML-labels zijn geverifieerd tegen de P6 EPPM REST-documentatie van het Activity-object +(ZEKER; dezelfde enum als PMXML) en de XER-token↔label-paren tegen Oracle's XER Import/Export Data +Map Guide (TASK.duration_type): `DT_FixedDrtn` = "Fixed Duration and Units/Time", `DT_FixedDUR2` = +"Fixed Duration and Units". Let op: de P6 XML-lezer van de XER-branch had die twee labels +verwisseld; bouwstap 2 heeft dat gecorrigeerd (`p6xmlReader.ts`, `check-xer-p6xml-parity.ts`). +De XER-tokens komen van de XER-sessie (ZEKER voor hun branch). `mppReader.ts`'s `MSP_TASK_TYPE_VALUES` gebruikt de volgorde 0/1/2 = +Fixed Units/Fixed Duration/Fixed Work (ZEKER); dat MSPDI's task-level `` dezelfde nummering +draagt is een externe bewering die de repo nergens vastlegt — te verifiëren tegen MPXJ's +`MSPDIReader` in stap 2 (ONBEKEND tot dan). + +Omgekeerd bij export: `workRule` → MSPDI `` + `` volgens dezelfde tabel, waarbij +een bewaard `effortDriven` wint; P6 XML `` 1-op-1 (een MSP-afwijking uit beslispunt 8 +gaat daar verloren — één waarschuwing, zoals de ELAPSEDTIME-waarschuwing in de CSV-export); XER +schrijft de app niet. + +### 4.3 Werk per toewijzing: drie optionele velden + +```ts +export interface ResourceAssignment { + // … bestaande velden … + /** OPTIONEEL — begroot werk in minuten (MSP Work / P6 Planned Units / XER target_qty). */ + plannedWorkMinutes?: number; + /** OPTIONEEL — verricht werk in minuten (MSP Actual Work / P6 Actual Units / XER act_reg_qty + act_ot_qty). */ + actualWorkMinutes?: number; + /** OPTIONEEL — resterend werk in minuten (MSP Remaining Work / P6 Remaining Units / XER remain_qty). */ + remainingWorkMinutes?: number; +} +``` + +**De regel "afwezig ⇒ afgeleid zoals nu":** + +| veld | afwezig ⇒ | wanneer geschreven | +|---|---|---| +| `remainingWorkMinutes` | restduur van de taak (werkdagen × `hoursPerDay × 60`, of `remainingMinutes` in uurmodus) × `unitsPerDay`; met een contour: de som van de `remaining`-periodes | (a) door een bewerking onder een werkbeschermende regel (§5), (b) bij een expliciete werkinvoer van de gebruiker, (c) bij import wanneer de bron een waarde levert die van de afleiding afwijkt | +| `actualWorkMinutes` | som van de `actual`-periodes van de contour; zonder contour 0 | (b), (c); nooit door een planningsbewerking (verricht werk is een feit) | +| `plannedWorkMinutes` | `actual + remaining` | (b), (c), en bij een typewissel naar een werkbeschermende regel (besluit 2: "vastleggen zoals het is") | + +Waarom drie en niet één (AFGELEID uit §11): in het XER-corpus wijkt het resttarief structureel af +van het begrote tarief (rehab-2: 14.459 van 52.640 toewijzingen; DCP-03 As-Built 47/47; Baseline +45/45). Met één werkveld zou "wat was begroot" verloren gaan zodra het restant wordt herschat, en +dat is precies het getal waar een planner later op wordt afgerekend. De driehoek werkt op het +**resterende** deel (P6: ZEKER, de formules in §2.2 gebruiken Remaining; MSP: AFGELEID, §2.1); +`plannedWorkMinutes` is referentie en wordt door de driehoek nooit herschreven. + +**Waar de tijdgefaseerde contour zich toe verhoudt.** De contour (`Task.timephasedContours`) is de +*vorm*, de werkvelden zijn de *totalen*. Regel: staan ze allebei, dan moet de som van de +`remaining`-periodes gelijk zijn aan `remainingWorkMinutes` (idem actual). Een bewerking die het +restwerk wijzigt herschaalt de contour proportioneel (de bestaande `rescaleContourForDuration`: die heeft een +parameter `taskType?: MspTaskType` en leidt daar intern `keepWork` uit af voor `'FIXED_WORK'` — +ZEKER; die parameter gaat de werkregel dragen, dus verbreden naar `WorkRule` of een tweede +parameter erbij, te kiezen in stap 3). Wijkt de som bij import af van het veld +(bron inconsistent), dan winnen de velden voor de totalen en wordt de contour bij de eerste +bewerking op de velden herschaald — nooit bij het openen (3.1-5). BEREDENEERD. + +**Materiaalresources** vallen buiten de driehoek: hun "werk" is een hoeveelheid (P6 Unit of +Measure, MSP Material Label — ZEKER `Resource.unitOfMeasure`), geen tijd. Alleen LABOR, EQUIPMENT, +CREW en SUBCONTRACTOR sturen de duur. BEREDENEERD; MSP's tabel spreekt alleen over work resources. + +### 4.4 Round-trips + +| kanaal | taak | toewijzing | opmerking | +|---|---|---|---| +| **IFC** (native) | nieuwe pset `OPS_WorkRule` (property `WorkRule`, IFCLABEL) naast het bestaande `OPS_MspTaskType`; `Project.defaultWorkRule` in `OPS_SchedulingOptions` | JSON-pset `OPS_AssignmentWork` per taak (zelfde `writeTimephasedMeta`-vorm als `OPS_Timephased`, per toewijzing via `resourceId`, door `remapContourResourceIds`-achtige guid-remap) | route: `docs/ifc-round-trip.md` — writer, reader, fixture én canon-tabel in `check-ifc-roundtrip`; de laatste twee zijn compile-afgedwongen | +| **MSPDI** | ``, `` (nu niet gelezen én niet geschreven — ZEKER, `mspdiReader.ts`/`mspdiWriter.ts`; stap 2 bouwt beide richtingen) | ``, ``, `` op `` én de sommen op `` | de writer schrijft `` vandaag al afgeleid (ZEKER, regel 652); wordt: veld als aanwezig, anders afgeleid | +| **P6 XML** | `` | ``, ``, `` (uren) en `` = actual + remaining | lezer/writer lezen/schrijven nu alleen `PlannedUnitsPerTime` (ZEKER) | +| **XER** (alleen lezen, XER-branch) | `TASK.duration_type` → `p6DurationType` (bestaat op de XER-branch, niet op main — zie stap 0) + `workRule` (nieuw) | `TASKRSRC.target_qty`, `act_reg_qty + act_ot_qty`, `remain_qty` → de drie velden — **alleen wanneer `target_qty` afwijkt van duur × `target_qty_per_hr`** (afspraak met de XER-sessie); tot dan blijft alles in het bronarchief `OPS_XerSourceArchive` | GEMETEN: 176 van 61.618 rijen wijken >1 % af; 263 rijen hebben werk zonder tarief | +| **CSV** | geen kolom (CSV kent geen toewijzingen — ZEKER, `csvWriter.ts` negeert `_assignments`) | — | bewust buiten scope | +| **MCP** | leestools tonen `workRule`/velden; `planner_update_task` krijgt `workRule`; werkinvoer per toewijzing via `planner_update_assignment` (bestaand contract uitbreiden) | | route: `docs/recepten/mcp-tool.md`; `cases-toolregistry.ts` vangt een vergeten registratie | +| **Extensies** | `ExtTask.workRule` alleen-lezen erbij (zoals `mspTaskType` er nu al staat — ZEKER `extTypes.ts`) | | | + +## 5. De regeltabel + +Symbolen: **R** = resterende duur van de taak (werkdagen, of minuten in uurmodus), **I** = inzet +per werkdag van een toewijzing (`unitsPerDay`), **W** = resterend werk van die toewijzing. +Verricht werk en verrichte duur staan in geen enkele cel: ze zijn feiten (§2.1, §2.2 — P6 ZEKER, +MSP AFGELEID). Alles hieronder werkt op het restant. + +| bewerking | FIXED_DURATION_RATE (vandaag) | FIXED_DURATION_WORK | FIXED_WORK | FIXED_RATE | +|---|---|---|---|---| +| **R gewijzigd** | W = R × I | I = W / R | I = W / R | W = R × I | +| **I gewijzigd** | W = R × I | W = R × I | R = W / I | R = W / I | +| **W gewijzigd** | I = W / R | I = W / R | R = W / I | R = W / I | +| **resource erbij** (met inzet I_n) | nieuwe W_n = R × I_n; rest ongemoeid | totaal W blijft; verdeeld naar rato van inzet; elke I_i = W_i / R | totaal W blijft; verdeeld naar rato van inzet; R = max_i(W_i / I_i) | totaal W blijft; verdeeld naar rato van inzet; R = max_i(W_i / I_i); **bij bewaard `effortDriven: false`: als FIXED_DURATION_RATE** (beslispunt 8-B) | +| **resource eraf** | W_n vervalt; rest ongemoeid | totaal W blijft; opnieuw verdeeld; I_i = W_i / R | totaal W blijft; opnieuw verdeeld; R = max_i(W_i / I_i) | totaal W blijft; opnieuw verdeeld; R = max_i(W_i / I_i); **bij `effortDriven: false`: W_n vervalt, R blijft** | +| **typewissel** | geen getal verandert (besluit 2) | idem + W vastleggen | idem + W vastleggen | geen getal verandert | +| **R gewijzigd, bewaard `effortDriven: true`** | — | **W = R × I** (MSP Fixed Duration + effort-driven, beslispunt 8-B) | — | — | + +Bronnen per kolom: FIXED_DURATION_RATE, FIXED_RATE en FIXED_WORK: rijen 1–3 ZEKER ([M1] en +[P3] stemmen overeen). Rij 4 (erbij) ZEKER uit [P3]'s kolom "add additional resources" en [M2]. +Rij 5 (eraf): [P3] heeft géén kolom voor verwijderen; [M2] dekt de drie effort-driven regels +("when you assign or remove people from a task"), dus ZEKER voor FIXED_RATE, FIXED_WORK en +FIXED_DURATION_WORK, en **AFGELEID** voor FIXED_DURATION_RATE (spiegelbeeld van rij 4). +FIXED_DURATION_WORK: rij 1 ZEKER uit [P3] (P6-lezing; de MSP-lezing staat in de laatste rij), +rij 2–3 ZEKER, rij 4 ZEKER ([P3]: "Units/Time of each resource"; [M2]: "decreases individual +resource unit values"). + +Drie regels die de tabel eenduidig maken (alle drie BEREDENEERD; §9 toetst ze): + +- **De verdeelsleutel** bij "totaal W blijft; verdeeld" is in geen van beide documentaties benoemd + (ONBEKEND). Het ontwerp verdeelt *naar rato van de inzet zoals die bij de bewerking geldt*: I_n + zoals ingevoerd voor de nieuwe toewijzing, de huidige I_i voor de bestaande — dus vóórdat de + regel zelf de inzet herrekent, anders is de sleutel circulair. Bij twee gelijke resources geeft + dat de helft-helft-uitkomst die beide handleidingen als voorbeeld geven (cases 10, 15, 30). +- **De taakduur bij meerdere toewijzingen** is R = max_i(W_i / I_i) over de werkresources (§6.2). + Direct ná een evenredige herverdeling is dat gelijk aan W / ΣI (elke toewijzing heeft dan dezelfde + W_i / I_i); heeft de gebruiker per toewijzing eigen werk ingevoerd, dan bepaalt de zwaarste + toewijzing de duur (case 29). "R = W / ΣI" mag dus nooit als losse formule worden gebouwd. +- **Na afronding** (§6.1) zijn W en I de opgeslagen grootheden en is R afgeleid en naar boven + afgerond; een volgende bewerking rekent altijd vanuit de exacte W en I, nooit terug vanuit de + afgeronde R (case 31). + +"Resource erbij" op een taak **zonder** toewijzingen: de nieuwe toewijzing krijgt W = R × I; geen +enkele regel wordt toegepast (P6 ZEKER: het type doet pas iets vanaf één resource; MSP ZEKER: de +eerste toewijzing zet het werk). + +## 6. Detailregels + +### 6.1 Afronding + +- Werk wordt in **minuten** opgeslagen, als getal; geen afronding op hele dagen of hele uren + (besluit 4 en het contour-precedent: een fractie is data). Weergave rondt op twee decimalen uur. +- **Dagtaken** hebben een hele-dagen-invariant (`scheduleDuration` is een geheel aantal werkdagen — + ZEKER, `TaskTime`-docblok). Levert een regel R = W / I een fractie op, dan wordt R **naar boven op + hele dagen** afgerond en blijft W exact staan; de laatste dag is dan niet vol. Dat vraagt één + aanpassing in `assignmentDayUnits` (ResourceLoad.ts): een vierde bron vóór de formule — is + `remainingWorkMinutes` aanwezig, dan is het te verdelen totaal het RESTwerk **plus het verrichte + deel** (`actualWorkMinutes` als de bron 'm gaf, anders afgeleid als verrichte duur × inzet — + reviewbevinding B3 op bouwstap 4: de driehoek schrijft alleen het restveld, en een typewissel + op een half gedane taak mag de belasting niet halveren, besluit 2), gespreid met de curvevorm + over de hele duur; niet `unitsPerDay × durationDays`. Zonder die aanpassing toont het histogram + op zo'n taak te veel werk. BEREDENEERD; MSP staat fractionele dagen toe (0,5 d) en heeft dit + probleem niet. +- **Wat na afronding leidend is**: na case 4 geldt R = 3 d, I = 1,0 + 1,0 en W = 20 + 20 u, terwijl + R × ΣI = 48 u. De identiteit werk = duur × inzet geldt dan alleen nog voor de exacte, niet-afgeronde + R. Regel: onder de werkbeschermende regels zijn **W en I opgeslagen en is R afgeleid**; elke + volgende bewerking (case 31) rekent uit de exacte W en I en nooit terug uit de afgeronde R, zodat + herhaald bewerken niet drift. Onder de inzetbeschermende regels blijft W = R × I met de hele-dagen-R + die de gebruiker zelf koos — het gedrag van vandaag. +- **Uurtaken**: R in hele minuten, naar boven; W exact. +- I wordt op twee decimalen opgeslagen zoals nu in de UI (ZEKER `isValidUnits`-guard: > 0). + +### 6.2 Meerdere toewijzingen + +OPS kent geen duur per toewijzing: elke toewijzing loopt over de hele taakduur (ZEKER, het +datamodel; `workWindowStart/Finish` bestaat, round-tript al door IFC (`OPS_TimephasedWindow`) en +het extensiecontract, maar geen lezer vult het en geen solverstap leest het — de latere activering +is dus geen groen veld). Regel in deze etappe (beslispunt +10): **R van de taak = max over de werkresources van W_i / I_i**, en elke toewijzing loopt over die +R. Gevolg: wijzigt de gebruiker I van één toewijzing op een FIXED_WORK-taak, dan verandert R via die +toewijzing en krijgen de andere toewijzingen I_j = W_j / R (hun werk blijft, dunner gespreid). MSP +en P6 laten de andere toewijzingen hun eigen kortere spanne houden. Dit is een bewuste, zichtbare +vereenvoudiging; de per-toewijzing-spanne staat in de TODO (§12). + +Een taakniveau-invoer van werk (kolom "Werk" op de taak) wordt **naar rato van bestaand restwerk** +over de toewijzingen verdeeld (BEREDENEERD; MSP zegt alleen "verdeeld onder de resources", [M5]). + +### 6.3 Contouren + +- **R gewijzigd**: de bestaande `rescaleContourForDuration` — as proportioneel, gaten mee, actuals + blijven; `keepWork` = de werkregel is FIXED_DURATION_WORK of FIXED_WORK (nu: `mspTaskType === + 'FIXED_WORK'`). ZEKER dat het mechanisme bestaat; de aansturing wisselt van veld. +- **I gewijzigd** op een gecontoureerde toewijzing onder een werkbeschermende regel: de contour is + data en de inzet is er een afgeleide van (de dialoog toont I per fase). De regel wordt: de hele + contour schaalt in hoogte met I_nieuw / I_oud; onder FIXED_WORK verandert daardoor R (§5) en volgt + daarna de as-herschaling. BEREDENEERD. +- **Resource erbij** op een gecontoureerde taak met vast werk: besluit 3 — bestaande contouren + houden hun vorm, hoogte × (W_i,nieuw / W_i,oud); de nieuwe toewijzing is vlak (geen contour) over de + nieuwe R; R = max_i(W_i / I_i) (§5). Voor FIXED_DURATION_WORK hetzelfde zonder duurwijziging. +- **Resource eraf**: contour van de verwijderde toewijzing vervalt met de toewijzing (nu al zo). + +### 6.4 Kalenders en tijdmodus + +De taakkalender bepaalt R in werkdagen/-minuten; de resourcekalender bepaalt op welke dagen het +histogram het werk legt (`taskWorkDayIsos`, ZEKER). Een werkregel verandert niets aan welke dagen +werkdagen zijn. ELAPSEDTIME-taken zijn buiten scope (besluit 6): hun "duur" is kloktijd en werk = +duur × inzet heeft er geen betekenis. + +**Kalenderwissel (eigenaarsbesluit 2026-09-05, K2 — vervangt de eerdere "ongewijzigd"-regel).** +Een andere kalender (taakkalender, projectkalender of de inhoud van een kalender) verandert de +SLOTgrootte: werkminuten per werkdag. De restduur in dagen blijft, het werk van vóór de wissel is +het anker, en daarna beslist de regel van de taak (`workTriangle.ts`'s `applySlotChange`, brug +`settleCalendarChange`): +- FIXED_DURATION_RATE (standaard): duur en inzet blijven, het werk volgt — het gedrag van vandaag; + zonder werkveld byte-identiek, een aanwezig veld wordt R' × I (meetlat 34). +- FIXED_DURATION_WORK: duur en werk blijven, de inzet wordt W / R' (meetlat 33). +- FIXED_WORK en FIXED_RATE: werk en inzet blijven, R = max(W / I) in de nieuwe slot, naar boven op + hele dagen — minder uren per dag maakt de taak langer (meetlat 32). Dat verschuift dus wél de + planning; een project- of kalenderwijziging die duren verandert, meldt hoeveel taken + (`notifications.workRuleDurationsChanged`). +Uurtaken hebben geen slotafhankelijke duur en blijven ongemoeid. Bewijs: MSP rekent +Duration = Work ÷ (Units × Hours per day) en houdt onder Fixed Work het werk vast [M2]/[M4] +(documented), maar MSP's "dag" is een vaste omrekenfactor (Opties → Uren per dag) en geen +kalenderwerkdag — de vertaling naar OPS-werkdagen is beredeneerd, niet gemeten. + +Vier randregels (reviewronde 2026-09-05, F3–F6; alle BEREDENEERD): +- FIXED_RATE legt bij de slotwissel — net als bij een inzetbewerking (§4.3) — géén werkveld vast: + het anker is rekeninvoer voor R, en een afgerond R laat dan geen opgeslagen W achter die van + R × I afwijkt (meetlat 35). Meerdere toewijzingen: R = max(W_i / I_i) (meetlat 36). +- Beslispunt 8-B (`effortDriven`) speelt bij een slotwissel niet: die uitzondering geldt een + DUURbewerking in dagen, en de slotwissel laat de dagen juist staan. Een MSP-import met Fixed + Duration + effort-driven krijgt hier dus de P6-lezing (inzet = W / R'), anders dan MSP zelf zou + doen — bewust, en niet gemeten. +- De contour-as leeft op werkminuten (§6.3), dus zij wordt na de wissel herschaald van de OUDE + werkminuten (dagen × oude slot) naar de nieuwe — óók wanneer de regel de dagen niet wijzigt + (Vaste duur): dezelfde dagen zijn in de nieuwe slot een andere hoeveelheid werk. De hoogte volgt + niet een regelconstante maar de toewijzing zelf: elke contour met een opgeslagen werkveld wordt + op precies dat werk gezet (`reconcileContourWork`), zonder werkveld is R' × I het werk. Taken + buiten de regel (besluit 6: mijlpaal, verzameltaak, hangmat, ELAPSEDTIME) blijven bij een + kalenderwissel byte-identiek, inclusief hun contour-as — dat wijkt bewust af van de Δ-rest-regel + (§6.5), die als duur-identiteit wél op ELAPSEDTIME werkt. +- Een gestarte taak: de nieuwe duur is verricht + nieuwe rest, de rest wordt expliciet + geschreven en `completion` volgt daaruit (verricht ÷ nieuwe duur), precies zoals bij een + duurbewerking (§6.5, B2 en het besluit van 2026-09-06). Dat gebeurt hier zónder + gebruikersbewerking, bij elke project- of kalenderwijziging — het percentage van een gestarte + taak kan dus veranderen door een kalenderwijziging. + +### 6.5 Actuals + +- `completion`, `actualStart/Finish`, `remainingTime/Minutes` blijven zoals ze zijn: OPS' + voortgang is duurgebaseerd (P6 Duration % Complete-achtig). Een werkpercentage (MSP % Work + Complete) komt er in deze etappe niet (TODO). +- Bij een bewerking op een gestarte taak is R de **restduur** (`remainingTime`/`remainingMinutes`, + anders duur × (1 − completion) — de bestaande afleiding in de solver, ZEKER `CPMSolver.ts`) en W + het restwerk. Verrichte duur en verricht werk bewegen niet. **Terugschrijven op een gestarte taak + (bouwstap 4, reviewbevinding B2):** de nieuwe duur is verricht + nieuwe rest, en de rest wordt dan + EXPLICIET geschreven (`remainingTime` in dagmodus, `remainingMinutes` in uurmodus) — anders zou de + solver de rest opnieuw afleiden als nieuwe duur × (1 − completion), het verrichte deel mee + verschuiven en een heen-en-weer-bewerking driften (case 31). `completion` volgt uit de + geschreven rest (besluit 2026-09-06, zie het laatste punt hieronder). +- **Een voortgangsbewerking is geen duurbewerking** (reviewbevinding B1): `completion` of + `remainingTime` wijzigen verandert de rest maar niet de duur, en raakt de driehoek niet; de poort + is de TOTALE werkduur van de taak (`settleDurationEdit` vergelijkt die met de momentopname). +- **Duurbewerking bij een EXPLICIETE restduur (eigenaarsbesluit 2026-09-05):** het verrichte deel is + een feit, dus wat de gebruiker aan de duur toevoegt of afhaalt landt in de rest — rest = max(0, + rest + Δ), in dagen (`remainingTime`) of minuten (`remainingMinutes`); `completion` volgt uit + de nieuwe rest (`carryRemainingThroughDurationEdit`, in store, raster en MCP vóór de + driehoekstap). Bron: + Microsoft, Remaining Duration = Duration − Actual Duration [M5] (documented voor de identiteit; + de Δ-richting bij een duurbewerking is daaruit afgeleid, niet gemeten). Zonder expliciet restveld + wordt de rest al uit duur × (1 − completion) afgeleid en schuift hij vanzelf mee — maar let op + (ZEKER, `taskMutationRules.ts`'s `applyProgressInvariants` en `importNormalize.ts`): elke + voortgangsinvoer en elke import schrijft `remainingTime` expliciet, dus in de praktijk geldt de + Δ-regel voor vrijwel elke gestarte taak. Reikwijdte + (reviewbevinding F7, AFGELEID): dit is een duur-identiteit en geen driehoeksregel, dus zij geldt + óók op ELAPSEDTIME-taken; alleen verzameltaken, hangmatten en mijlpalen (geen eigen bewerkbare + duur) blijven buiten schot. +- **`completion` volgt de expliciete rest (reviewbevinding F4; eigenaarsbesluit 2026-09-06, optie a).** + Zodra de brug de rest expliciet schrijft (Δ-regel hierboven, B2, en de kalenderwissel in §6.4) + wordt `completion` herrekend als 1 − rest ÷ duur — dezelfde formule en randafspraken als een + restbewerking in het taakraster (`taskEditPlan.ts`, route `task-progress`: duur 0 ⇒ 100 %, + percentage > 0 zet een ontbrekende `actualStart`, percentage < 1 wist `actualFinish`), in + `workRuleApply.ts`'s `syncCompletionToRemaining`. Voorbeeld: 10 d op 50 % met rest 5 wordt na + een kalenderwissel naar 6 u/dag onder Vast werk 12 d met rest 7 en 41,7 % (5 d verricht) — + Gantt-balk, solver en rapportage zeggen hetzelfde; terug naar 8 u/dag geeft weer 10 d, rest 5, + 50 %. Gevolg van de klem op 0: een duur die onder het verrichte deel wordt gekort, geeft rest 0 + en dus 100 %. Verworpen alternatieven: (b) renderer en rapportage op de rest laten leunen + (groter, twee lezingen blijven bestaan); (c) laten (twee waarheden in één bestand). Bewijs: de + richting (percentage = verrichte duur ÷ duur) is Microsofts definitie van % Complete + (documented, niet in deze sessie opnieuw geverifieerd); de OPS-uitwerking is beredeneerd. +- Het `actual`-deel van een contour telt als `actualWorkMinutes` wanneer dat veld afwezig is. +- Afsluiten op 100 %: restwerk 0, begroot blijft; heropenen laat de velden staan. + +### 6.6 Nivelleerder, histogram, bezetting + +- Nivelleerder: verschuiven blijft de enige ingreep (besluit 7); hij leest via `assignmentDayUnits` + automatisch het opgeslagen restwerk (§6.1) en verandert nooit I of W. "Inzet verlagen om binnen + capaciteit te blijven" → TODO, geavanceerde optie. +- Histogram/overallocatie/bezettingsoverzicht: geen eigen code; ze delen `assignmentDayUnits` + (ZEKER) en krijgen de vierde bron uit §6.1 gratis mee. + +### 6.7 Wat niet mag + +- Een regel mag nooit een negatief of nul-restant opleveren: I ≤ 0 of R ≤ 0 als uitkomst ⇒ de + bewerking wordt geweigerd met behoud van de oude waarden (dezelfde "weigeren-met-behoud"-conventie + als `updateAssignment`'s `isValidUnits`, ZEKER). +- Een bewerking onder een werkregel is **één undo-stap**, ook als ze drie velden en een contour + raakt (`runtime.beginUndoable` één keer; §9 meting 28). +- Nooit een getal wijzigen bij openen, herberekenen (F5), documentwissel of typewissel. + +## 7. UI + +**Instelling "Toon taaktypes"** (`ops-showTaskTypes`, boolean, standaard uit) via +`settingsRegistry.ts` + `saveShowTaskTypes` + `SettingsPanelContent` (de drie vaste plekken — +ZEKER, `docs/recepten/instelling.md`). De instelling stuurt alleen zichtbaarheid. + +**Automatische ontsluiting per document** (3.1-3): bij het laden leidt de app een niet-gepersisteerd +documentveld `taskTypesVisible` af (rol `none` in de snapshot; in `DOCUMENT_FIELDS`, `fresh: false`) +uit "minstens één taak heeft `workRule`, `mspTaskType` of `p6DurationType`, of minstens één +toewijzing heeft een werkveld". Is dat zo, dan zijn de bedieningselementen voor dát document zichtbaar +ongeacht de app-instelling, met één informatieve melding via het bestaande meldingenkanaal en een +`helpArticleId` naar de gids (patroon `notifyTimephasedLoss`, ZEKER). + +**Eigenschappenpaneel** (`TaskPropertiesPanel`, sectie Planning): een keuzelijst *Werkregel* met de +vier keuzes en het bijschrift; daaronder een regel "Beschermd: duur en inzet" in gewone woorden. In +de toewijzingstabel (`TaskAssignmentsSection`) een kolom **Werk (rest)** in uren, bewerkbaar, en een +slotje in de kop van de kolommen die de huidige regel beschermt — dat is de "bescherming zichtbaar +maken"-opgave uit het voorstel. Een taak met een bewaard `effortDriven` dat afwijkt (beslispunt 8-B) +toont een bijschrift "uit MS Project: (niet) effort-driven", geen vinkje. + +**Taakraster**: `task.workRule` als bewerkbare enum-kolom (naast het bestaande alleen-lezen +`task.mspTaskType`, ZEKER `taskColumnRegistry.ts` regel 603) en `assignment.work` als tokens-kolom +in de categorie *resources*, met dezelfde `assignment-set`-transactie als `assignment.unitsPerDay`. +Kolommen verschijnen alleen in de kolomkiezer wanneer de weergave ontsloten is. + +**Taakdialoog**: dezelfde secties als het paneel (ze delen `task-sections/`, ZEKER). + +## 8. Reikwijdte + +In scope: gewone bladtaken op werktijd, in dag- en uurmodus (besluit 6). Buiten scope, gedrag +ongewijzigd en geïmporteerd type bewaard: ELAPSEDTIME-taken, hangmatten, mijlpalen (P6 schakelt het +veld daar zelf uit — ZEKER), samenvattingstaken (MSP: nooit effort-driven — ZEKER), taken zonder +toewijzingen (regel doet niets — ZEKER), materiaalresources (§4.3). + +Alle toewijzingsroutes lopen door de driehoek — ook **verplaatsen** (`moveAssignment` = eraf bij de +oude taak + erbij bij de nieuwe) en **een resource verwijderen** (`removeResource` = eraf op elke +taak waar hij stond); reviewbevinding B4 op bouwstap 4. Buiten de driehoek blijft alleen de +kalender (§6.4) — met het gevolg dat na een kalenderwissel op een taak met vastgelegd werk W niet +meer bij I × R past (het histogram toont dan I' = W / R'): open beslispunt in `docs/TODO.md`. + +## 9. De meetlat: bewerkingen om later tegen MS Project te toetsen + +Vorm: een data-gedreven case-bestand `tests/planning/work-triangle-cases.json` (gebouwd; de naam +mist bewust het `cases-`-prefix, want `run.sh` globt `cases-*.json` de CPM-harnas en een vaste +batterij-inventaris in — ZEKER), met per case een resterende uitgangssituatie, één of meer +bewerkingen, de verwachte uitkomst en een veld `evidence: 'documented' | 'reasoned' | 'decided' +| 'measured'`. `tests/planning/check-work-triangle.ts` draait de cases tegen de pure +bewerkmodule (§10 stap 3) — géén UI; de drie store-gebonden cases (22, 23, 28) telt hij en +slaat hij over. Zodra iemand een +meting in MS Project (of P6) heeft gedaan, wordt `evidence` `measured` en de verwachting eventueel +gecorrigeerd. Uitgangssituatie tenzij anders vermeld: dagtaak, 8 uur/dag, 5 werkdagen, één +werkresource op inzet 1,0 ⇒ werk 40 uur, niets verricht. + +| nr | regel | bewerking | verwacht | bewijs | +|---|---|---|---|---| +| 1 | FIXED_RATE | duur → 10 d | werk 80 u, inzet 1,0 | documented [M1] | +| 2 | FIXED_RATE | inzet → 0,5 | duur 10 d, werk 40 u | documented | +| 3 | FIXED_RATE | werk → 80 u | duur 10 d | documented | +| 4 | FIXED_RATE | tweede resource 1,0 erbij | duur 2,5 d in MSP; OPS: 3 d met halve laatste dag, werk 20 + 20 u | documented [M2] + reasoned (§6.1) | +| 5 | FIXED_RATE + `effortDriven:false` | tweede resource erbij | duur 5 d, werk 40 + 40 u | documented [M2] (beslispunt 8) | +| 6 | FIXED_DURATION_RATE | duur → 10 d | werk 80 u | documented | +| 7 | FIXED_DURATION_RATE | inzet → 0,5 | werk 20 u | documented | +| 8 | FIXED_DURATION_RATE | werk → 80 u | inzet 2,0 | documented | +| 9 | FIXED_DURATION_RATE | tweede resource erbij | duur 5 d, werk 40 + 40 u | documented [P3] | +| 10 | FIXED_DURATION_WORK | tweede resource 1,0 erbij | duur 5 d, inzet 0,5 + 0,5, werk 20 + 20 u | documented [M2]/[P3]; verdeelsleutel reasoned | +| 11 | FIXED_DURATION_WORK | duur → 10 d | inzet 0,5, werk 40 u (P6) — **MSP effort-driven Fixed Duration: werk 80 u** | documented, beide lezingen (beslispunt 8) | +| 12 | FIXED_WORK | duur → 10 d | inzet 0,5, werk 40 u | documented [M1] | +| 13 | FIXED_WORK | inzet → 0,5 | duur 10 d | documented | +| 14 | FIXED_WORK | werk → 80 u | duur 10 d | documented | +| 15 | FIXED_WORK | tweede resource 1,0 erbij | duur 2,5 d (OPS 3 d), werk 20 + 20 u | documented; verdeelsleutel reasoned | +| 16 | FIXED_RATE, 10 d, 40 % gereed (32 u verricht) | duur → 15 d | verricht 4 d/32 u ongewijzigd; rest 11 d, restwerk 88 u; totaal 120 u | reasoned (§2.1 laatste punt) | +| 17 | FIXED_WORK, 10 d, 40 % gereed | duur → 12 d | rest 8 d, restwerk 48 u ⇒ inzet 0,75; verricht ongewijzigd | reasoned | +| 18 | FIXED_DURATION_RATE, 10 d, 40 % | duur → 12 d | rest 8 d, restwerk 64 u, totaal 96 u | reasoned | +| 19 | FIXED_WORK, twee resources 1,0 (werk 20 + 20 u, 2,5 d) | één resource eraf | duur 5 d, werk 40 u op de blijver | documented [M2] | +| 20 | FIXED_DURATION_WORK, twee resources 0,5 | één eraf | blijver inzet 1,0, duur 5 d | documented | +| 21 | FIXED_RATE | wissel → FIXED_WORK, daarna duur → 10 d | na wissel niets gewijzigd; na duur: inzet 0,5 | besloten (2) + documented | +| 22 | FIXED_WORK, contour vooraan belast | duur → 10 d | as ×2, hoogte ×0,5, werk 40 u; actual-periodes onaangeraakt | reasoned (bestaande `rescaleContourForDuration`) | +| 23 | FIXED_WORK, contour vooraan belast | tweede resource erbij | vorm blijft, hoogte ×0,5, nieuwe resource vlak, duur korter | besloten (3) | +| 24 | FIXED_WORK, uurtaak 16 u, inzet 1,0 | inzet → 2,0 | duur 8 u (480 min) | documented | +| 25 | FIXED_WORK + materiaalresource 100 stuks | duur → 10 d | materiaal ongemoeid; werkresource inzet 0,5 | reasoned (§4.3) | +| 26 | elk type, geen toewijzingen | duur → 10 d | alleen de duur; geen veld geschreven | documented [P2] (P6); reasoned (MSP-lezingen) | +| 27 | FIXED_WORK | inzet → 0 | geweigerd, niets gewijzigd | bestaande guard | +| 28 | FIXED_WORK | inzet → 0,5, dan undo | duur, inzet, werk en contour in één stap terug | ontwerpregel §6.7 | +| 29 | FIXED_WORK, twee resources 1,0 met per toewijzing ingevoerd werk 40 u en 8 u | duur → 10 d | inzet 0,5 en 0,1; werk ongewijzigd — en omgekeerd: inzet van de eerste → 2,0 ⇒ R = max(40/16, 8/8) = 2,5 d (OPS 3 d), tweede toewijzing loopt over die 3 d: I = 8/(3 × 8) ≈ 0,33 | reasoned (§5, R = max_i) | +| 30 | FIXED_WORK, één resource 1,0 (40 u) | tweede resource erbij met inzet 0,5 | verdeling 1,0 : 0,5 ⇒ werk 26,67 + 13,33 u; R = 40/12 ≈ 3,33 d (OPS 4 d) | reasoned (verdeelsleutel §5) | +| 31 | FIXED_WORK, één resource 1,0 (40 u) | inzet → 0,6, daarna inzet → 1,0 | eerst R = ⌈40/4,8⌉ = 9 d, werk 40 u; daarna R = 5 d exact (niet 9 × 8 × 1,0 = 72 u) | reasoned (§6.1, W leidend) | +| 32 | FIXED_WORK, 4 d op 8 u/dag, één resource 1,0 (32 u) | taakkalender → 6 u/dag | werk 32 u, inzet 1,0, R = 32/6 = 5,33 ⇒ 6 d | reasoned (K2; [M2]/[M4] documented voor "werk vast, duur herrekend", OPS-dagen beredeneerd) | +| 33 | FIXED_DURATION_WORK, 4 d op 8 u/dag, één resource 1,0 (32 u) | taakkalender → 6 u/dag | R = 4 d, werk 32 u, inzet 32/24 = 1,33 | reasoned (K2; [P1] Units/Time = Remaining Units / Remaining Duration) | +| 34 | FIXED_DURATION_RATE, 4 d op 8 u/dag, resource a 1,0 met veld 32 u, resource b 1,0 zonder veld | taakkalender → 6 u/dag | R = 4 d, inzet 1,0; a wordt 24 u, b blijft veldloos (afgeleid 24 u) | reasoned (K2; het gedrag van vandaag) | +| 35 | FIXED_RATE, 4 d op 8 u/dag, één resource 1,0 zonder veld | taakkalender → 6 u/dag | R = ⌈32/6⌉ = 6 d, inzet 1,0, GEEN werkveld (afgeleid 36 u) | reasoned (F5; §4.3 afwezig blijft afwezig) | +| 36 | FIXED_WORK, 4 d op 8 u/dag, resource a 1,0 (32 u) + resource b 0,5 (8 u) | taakkalender → 6 u/dag | R = max(32/1, 8/0,5) = 32 u ÷ 6 = 5,33 ⇒ 6 d; beide werkvelden blijven | reasoned (F11; §6.2) | + +Deze lijst (36 bewerkingen) gaat ook naar `docs/TODO.md` als "MSP-meetlat", zodat de eerstvolgende persoon met +MS Project weet wat er te meten valt. Voor 32–36 en de Δ-regel uit §6.5 geldt bovendien dat MS Project +hier niet in werkdagen rekent; wie meet, noteert de uren. + +## 10. Bouwvolgorde — apart verifieerbare stappen + +**Stap 0 — XER merget eerst.** De XER-branch heeft een tweede merge nodig (46 overlappende +bestanden met main sinds PR #95 en drie andere PR's). Deze etappe start pas op een main **mét** XER +erin, zodat `Task.p6DurationType`, de pset `OPS_P6Progress` en het bronarchief er zijn en er niet +twee keer aan `formatRegistry.ts`, `ifcPsets.ts` en de contour-adapters wordt getrokken. + +| stap | levert | poort | +|---|---|---| +| 1 | `WorkRule`, `Task.workRule`, `Project.defaultWorkRule`, de drie werkvelden; `DOCUMENT_FIELDS`; IFC-psets `OPS_WorkRule` + `OPS_AssignmentWork`; ext-contract alleen-lezen. **Geen gedrag.** | `check-ifc-roundtrip` (fixture + canon), `check-document-contract`, `verify:cycles`; elk bestaand bestand byte-identiek | +| 2 | Importvertaling (§4.2) voor .mpp, MSPDI (incl. ``/`` lezen), P6 XML, XER; export MSPDI/P6 XML met de werkvelden; waarschuwingen | fidelity-poort blijft 0 (`GOAL_ZERO_DEVIATIONS`); `check-mpp-*`, `check-adapters-*`; nieuwe fixtures per bron | +| 3 **(gebouwd 2026-09-05)** | Pure module `src/engine/work/workTriangle.ts`: `applyDurationEdit`, `applyUnitsEdit`, `applyWorkEdit`, `applyTaskWorkEdit`, `applyAssignmentAdded`, `applyAssignmentRemoved`, `applyRuleChange` — invoer: de RESTERENDE toestand (`TriangleState`: regel, bewaard `effortDriven`, restduur in werkminuten, per toewijzing inzet + optioneel restwerk + `drivesDuration`), uitvoer: een nieuwe toestand of een weigering met reden; nooit een store. `WorkRule` in `src/types/workRule.ts` (verhuist in stap 1 naar `task.ts`). Plus `work-triangle-cases.json` (§9) | `tests/planning/check-work-triangle.ts`: 332 checks, 39 cases (21 documented, 16 reasoned, 2 decided, 0 measured; bij een mengvorm telt het zwakste bewijs) plus eigenschappen: geen veld geschreven onder de inzetregels, typewissel in alle 16 richtingen getalvrij, weigering bij restduur ≤ 0 of niet-eindige inzet (§6.7), `effortDriven` zonder effect buiten de twee 8-B-cellen | +| 4 **(gebouwd 2026-09-05)** | Brug `src/engine/work/workRuleApply.ts` (`captureTriangle` → kernstap → `applyTriangleResult`; `settleDurationEdit`/`settleUnitsEdit`/`planWorkEdit`+`commitTrianglePlan`/`settleAssignmentAdded`/`settleAssignmentRemoved`/`settleAssignmentPlan`/`settleRuleChange`; `contourKeepsWork`; `reconcileContourWork` = besluit 3 "vorm blijft, hoogte zakt"). Bedraad in `taskSlice.updateTask` + nieuw `setTaskWorkRule`, `resourceSlice.assignResource`/`updateAssignment`/`unassignResource` + nieuw `setAssignmentWork`, `gridTransaction.ts` (celduur én de assignment-set-cel, via `AssignmentSettleOp`), `createMcpTransactions.ts` (alle tweelingen + `setAssignmentWork`/`setTaskWorkRule` op de draft), `rescaleTaskContours(…, keepWork)` uit de effectieve regel, `assignmentDayUnits` derde bron = opgeslagen werk (§6.1). Een duur die uit de driehoek komt zet `scheduleStale`, herschaalt contour + importsplits en wist het Z8-venster — precies als een duurbewerking; onder FIXED_DURATION_RATE zonder werkvelden byte-identiek | `tests/planning/check-work-rule-store.ts` (95 checks: store, raster, MCP, vierde bron, meetlat 22/23/28, voortgang > 0, uurmodus, FIXED_RATE + 8-B, materiaal in het raster, datums-zoals-opgeslagen, verplaatsen/resource verwijderen), bestaande planning- en MCP-suites groen | +| 5 **(gebouwd 2026-09-05)** | Instelling `ui.showTaskTypes` (`ops-showTaskTypes`, `settingsRegistry` + `saveShowTaskTypes` + `SettingsPanelContent`, tabblad Tijdlijn); documentveld `taskTypesVisible` (`DOCUMENT_FIELDS`, rol `none`, afgeleid in `payloadFromImport` via `hasTaskTypeData` — taak met `workRule`/`mspTaskType`/`p6DurationType`, projectstandaard, of toewijzing met werkveld — en gezet door `setTaskWorkRule`/`setAssignmentWork`/rasterbewerkingen); één melding per document (`taskTypesNotice.ts`, `notifications.taskTypesUnlocked`, `helpArticleId` → `gids-taaktypes`); selector `taskTypesUnlocked`. UI: `TaskWorkRuleField` (paneel + dialoog: keuzelijst met projectstandaard, "Beschermd: …", MSP-bijschrift 8-B), kolom **Werk (rest)** + slotjes in `TaskAssignmentsSection`, raster `task.workRule` (bewerkbare enum, `available` alleen ontsloten; typewissel legt werk vast in `gridTransaction.ts`) en `assignment.remainingWork` (tokens `naam: uren`, assignment-set via `settleWorkEdit`), `TaskColumnContext.taskTypesUnlocked`; i18n 14 locales; gids `gids-taaktypes` nl+en (12 vertalingen volgen maandelijks) Reviewronde (2026-09-05) verwerkt: B1 cross-task plakken behoudt het werk (`validateAssignmentTokens`), B2 de werkcel vergelijkt met de getoonde waarde (niet-bewerkte toewijzingen krijgen geen expliciet werk), B3 typewissel in paneel en dialoog via `setTaskWorkRule` (geen `scheduleStale`, "datums zoals opgeslagen" blijft), B4 dialoog commit de regel direct en Opslaan stuurt de duur alleen mee als de gebruiker die wijzigde, K1 meldingsgate gewist bij `newProject`, K3 elk schrijfpad (ook MCP) ontsluit, K4 werkkolom alleen waar de regel werkt, K5 werkinvoer commit op Enter/blur, K6 contour-restsom, aria-labels met resourcenaam, sectiekop; `add` in `planner_manage_assignments` kent `remainingWorkMinutes` (batch zonder tempId voor toewijzingen). K2 (crashherstel zonder melding) en K6a in de TODO | `verify:i18n`, `verify:docs`, `check-work-rule-store` secties (n)+(o), browserspec `tests/browser/work-rule.spec.ts` (keuzelijst, werk typen toets-voor-toets, inzet, undo, dialoogpad, ontsluiting), `cases-work-rule.ts` (10) | +| 6 | Resource erbij/eraf (§5 rij 4–5) inclusief contourregels (§6.3) — **vereist beslispunt 8** (genomen: 8-B). **Store/raster/MCP-kant gebouwd in stap 4** (`settleAssignmentAdded`/`Removed`, `reconcileContourWork`); wat rest is de UI-kant (stap 5) | cases 4, 5, 9, 10, 15, 19, 20, 23, 29, 30 (kern) + `check-work-rule-store` (b15–b21, d5–d9, e6–e7, f3–f4) | +| 7 **(gebouwd 2026-09-05)** | Geen nieuwe tool (39 blijft 39) maar drie bestaande tools uitgebreid: `planner_update_tasks`/`planner_add_tasks` `fields.workRule` (enum \| null, via `draft.setTaskWorkRule` ná de duurpatch — een gelijktijdige `duration` wordt onder de OUDE regel verwerkt, `taskFields.ts`), `planner_manage_assignments` `update.remainingWorkMinutes` (> 0, via `draft.setAssignmentWork`; alleen op taken waar `workRuleApplies`), `planner_update_project` `defaultWorkRule` (enum \| null, `delete` bij null); `planner_get_project_info` toont `defaultWorkRule`, `planner_get_task` toonde `workRule` + werkvelden al sinds stap 1. `REJECT_HINTS` voor `plannedWorkMinutes`/`actualWorkMinutes`/`remainingWorkMinutes` als taakveld | `tests/mcp/cases-work-rule.ts` (9 cases via de echte dispatch, incl. `planner_batch` = één undo-stap), `cases-toolregistry`/`cases-schemavalidatie` groen | +| 8 | CLAUDE.md-sectie, TODO-afvinking, `docs/superpowers/README.md` | `verify:docs` | + +Stap 1–3 zijn onafhankelijk van elkaar te reviewen en veranderen niets aan wat de gebruiker ziet. + +## 11. Corpusbewijs (GEMETEN — XER-sessie, 84 unieke bestanden, aangeleverd 2026-09-04) + +| meting | uitkomst | wat het voor dit ontwerp betekent | +|---|---|---| +| activiteiten per duration type | DT_FixedDUR2 15.978 · DT_FixedDrtn 1.773 · DT_FixedQty 153 (2 bestanden) · DT_FixedRate 0 · niet-standaard DT_FixedDUR 24 | bouwvolgorde binnen stap 3: FIXED_DURATION_WORK → FIXED_DURATION_RATE → FIXED_WORK → FIXED_RATE (minst getest in de praktijk); de vandaag hardgecodeerde regel is de op één na meest voorkomende in P6-bestanden | +| `target_qty` wijkt >1 % af van duur × tarief | 176 van 61.618 rijen (4 met duur 0); 263 rijen met werk zonder tarief; in 5 bestanden (HarbourPointe 98/417, DCP-03 As-Built 37/47, DCP-03 Baseline 35/45, p6_torture_test 5/45, testXer 1/90) | "afwezig ⇒ afgeleid" is voor 99,7 % van de rijen verliesvrij; de XER-afspraak "alleen overzetten bij afwijking" is dus goedkoop | +| toewijzingen met een curve | 2 rijen, 1 bestand | curves + werkregels: lage prioriteit, wel correct (§6.3) | +| verricht werk op DT_FixedQty | 1 rij | case 17 (verricht werk op een vast-werk-taak) heeft géén corpusdekking — puur beredeneerd | +| `remain_qty_per_hr` ≠ `target_qty_per_hr` | rehab-2 14.459/52.640 · DCP-03 As-Built 47/47 · Baseline 45/45 · Roads 71/3.575 · HarbourPointe 14/417 | het resttarief is een eigen grootheid ⇒ drie werkvelden (beslispunt 9) | + +Het corpus staat bij de eigenaar (`~/open-aec/voor claude/testdata-crawl`, env `OPS_XER_CORPUS`), +niet in de repo; de tabellen staan daar in `/tmp/xer-overname/corpusscan-werk-2.md`. Een telling +van `mspTaskType × effortDriven` over de .mpp-crawl (216 bestanden) ontbreekt nog (§12). + +## 12. Wat dit ontwerp bewust laat liggen (→ `docs/TODO.md`) + +- **MSP-meetlat**: de 31 bewerkingen uit §9 meten in MS Project (en P6) zodra iemand het heeft. +- **Telling `mspTaskType × effortDriven`** over de `OPS_MPP_CRAWL`-set: bepaalt hoe vaak beslispunt + 8 in de praktijk speelt. +- **Nivelleerder-optie "inzet verlagen"** (besluit 7-B) onder een geavanceerde optie. +- **Per-toewijzing-spanne** (`workWindowStart/Finish` activeren) — heft de vereenvoudiging van + §6.2 op. +- **% werk gereed** (MSP % Work Complete) naast de duurgebaseerde `completion`. +- **Projectstandaard-werkregel in de UI** (projectwizard/projectinfo): stap 1 slaat het veld op, + de UI ervoor is klein maar niet in deze etappe. +- **CSV**: toewijzingen en werk in CSV — pas wanneer CSV toewijzingen kent. +- **P6-optie "preserve existing assignments"** ([P4]): OPS kiest altijd "recalculate" (de + synchronisatietabel); de "preserve"-variant is een instelling voor later. + +## 13. Bronnen + +MS Project (Microsoft Support, geraadpleegd 2026-09-04): + +- [M1] *Change the task type for more accurate scheduling* — + https://support.microsoft.com/en-us/office/change-the-task-type-for-more-accurate-scheduling-b0b969ad-45bc-4e9e-8967-435587548a72 +- [M2] *Change the effort driven setting for task types* — + https://support.microsoft.com/en-us/office/change-the-effort-driven-setting-for-task-types-18efd7c7-d146-4b06-bdbe-b6a11564bdf3 +- [M3] *The duration or work value changed when I assigned a resource* — + https://support.microsoft.com/en-au/office/the-duration-or-work-value-changed-when-i-assigned-a-resource-a3573268-f613-419d-b78f-2516255c7432 +- [M4] *Type fields* — https://support.microsoft.com/en-us/project/type-fields +- [M5] *Remaining Duration (task field)* — https://support.microsoft.com/en-us/project/remaining-duration-task-field + (geraadpleegd 2026-09-05: "Remaining Duration = Duration − Actual Duration"; een wijziging van de + restduur laat Project de duur herrekenen als rest + werkelijke duur) +- [M5] *Work fields* — https://support.microsoft.com/en-us/project/work-fields +- [M6] *Remaining Work fields* — https://support.microsoft.com/en-us/project/remaining-work-fields +- [M7] *Actual Work fields* — https://support.microsoft.com/en-US/project/actual-work-fields +- [M8] *Remaining Duration (task field)* — https://support.microsoft.com/en-us/project/remaining-duration-task-field +- [M9] secundair: NCDOT-trainingsdocument *Microsoft Project 2016 – Before You Plan Your First + Project* (stelt "New tasks are effort driven" standaard uit) — + https://connect.ncdot.gov/projects/Project-Management/TrainingDocs/MicrosoftProject2016-BeforeYouPlanYourFirstProject.pdf +- Ook geraadpleegd zonder aanvullende regel: *Effort Driven (task field)*, *Duration (task field)*, + *Work Contour field* (eerder, contour-etappe). + +Primavera P6 (Oracle Help Center, geraadpleegd 2026-09-04): + +- [P1] *Define activity duration types* (P6 Professional 24) — + https://docs.oracle.com/cd/F88968_01/English/User_Guides/p6_pro_user/define_activity_duration_types.htm +- [P2] *About Duration Types* (P6 EPPM Help 24, met de Remaining-formules) — + https://docs.oracle.com/cd/F88966_01/p6help/en/6631.htm +- [P3] *Synchronizing activity duration, units, and resource units/time* (de synchronisatietabel) — + https://docs.oracle.com/cd/F88968_01/English/User_Guides/p6_pro_user/synchronizing_activity_duration_units_and_resource_units_time.htm +- [P4] *Select calculation options for resource and role assignments* — + https://docs.oracle.com/cd/G48902_01/English/User_Guides/p6_pro_user/select_calculation_options_for_resource_and_role_assignments.htm +- [P5] *Calculations Tab of the Project Preferences Dialog Box* (P6 EPPM Help 23) — + https://docs.oracle.com/cd/F74773_01/p6help/en/91690.htm +- Eerder (contour-etappe): *Future period bucket planning*, *The Resource Usage Spreadsheet*, + *Editing Period Values for Assignments*. + +Eigen code (ZEKER-verwijzingen): `src/types/task.ts`, `src/types/resource.ts`, +`src/engine/contour/contourEngine.ts` (`rescaleContourForDuration`), `src/utils/taskDefaults.ts` +(`rescaleTaskContours`), `src/engine/scheduler/ResourceLoad.ts` (`assignmentDayUnits`, +`taskWorkDayIsos`), `src/state/slices/resourceSlice.ts`, `src/state/slices/taskSlice.ts`, +`src/services/mpp/mppReader.ts`, `src/services/msproject/mspdi{Reader,Writer}.ts`, +`src/services/p6/p6xml{Reader,Writer}.ts`, `src/services/csv/csvWriter.ts`, +`src/engine/taskGrid/taskColumnRegistry.ts`, `src/state/documentContract.ts`, +`docs/ifc-round-trip.md`, `docs/recepten/instelling.md`, `tests/planning/cases-progress.json`. diff --git a/docs/wiki/Extensions-Authoring.md b/docs/wiki/Extensions-Authoring.md index a09ab0935..e9c2832e3 100644 --- a/docs/wiki/Extensions-Authoring.md +++ b/docs/wiki/Extensions-Authoring.md @@ -30,13 +30,15 @@ Categories: `Import/Export`, `Planning`, `Reporting`, `Utility`, `Fonts`, `Other | `ribbon` | **hard** — missing ⇒ `api.ui.addRibbonButton` throws | Add a button to the ribbon. | | `backstage` | **warn** — missing ⇒ `api.importers.*` still works, but logs a warning | Register an importer (appears under File → Import). | | `pdf-fonts` | **hard** — missing ⇒ `api.pdfFonts.register` throws | Register a font provider for the vector PDF export (e.g. CJK glyph bytes). | +| `importSource` | **hard, default-deny** — missing ⇒ `api.data.getImportSourceInfo`/`getImportSourceChunk`/`getImportSourceCatalogPage` throw before a single byte is read | Read the **full original source bytes** of an imported file (today: XER), including fields the import layer deliberately never materializes into the project model. See the section below. | | `filesystem` | informational | No API surface; a declared intent shown at install time — **no** sandbox guarantee. | | `network` | informational | Likewise — declared intent, not a technical boundary. | -`data.*`, `settings.*`, `assets.*` and `ui.showNotification` are **core API**: always available, no -permission required. Enforcement is centralized in `src/extensions/permissions.ts`. `minAppVersion` is -also enforced: on an older app the extension refuses to activate. Unknown permissions are filtered out -with a warning in the debug terminal. +`data.*` is otherwise **core API** — except for the three `getImportSource*` methods above — same +as `settings.*`, `assets.*` and `ui.showNotification`: always available, no permission required. +Enforcement is centralized in `src/extensions/permissions.ts`. `minAppVersion` is also enforced: on +an older app the extension refuses to activate. Unknown permissions are filtered out with a warning +in the debug terminal. ## main.js @@ -75,7 +77,7 @@ module.exports = { | Area | Functions | |---|---| | `api.importers` | `register(def)`, `unregister(id)` | -| `api.data` | `getProject/getCalendar/getTasks/getSequences/getResources/getAssignments`, `addTask`, `updateTask`, `addSequence`, `loadProject(result)`, `recalculate()` | +| `api.data` | `getProject/getCalendar/getTasks/getSequences/getResources/getAssignments`, `getImportSourceInfo/getImportSourceChunk/getImportSourceCatalogPage` (permission `importSource`, see below), `addTask`, `updateTask`, `addSequence`, `loadProject(result)`, `recalculate()`, `batch(fn)` | | `api.events` | `on/off/emit` (permission `events`) | | `api.ui` | `addRibbonButton(reg)` (permission `ribbon`), `showNotification(msg, type?)` | | `api.settings` | `get(key, default)`, `set(key, value)` — prefixed per extension in localStorage | @@ -110,6 +112,65 @@ module.exports = { }; ``` +### Read-only XER source route (permission `importSource`, `apiVersion` ≥ 1.1) + +Besides the mapped `data.*` DTOs (derived, normalized, always available), an extension holding the +`importSource` permission can also reach the **original, unmodified source data** of the current +document — today only for an opened `.xer` file (Primavera P6). Without this permission all three +methods throw before a single byte is read; nothing leaks through a partial call or an error path. +These three methods exist since contract version `1.1.0` — declare `"apiVersion": "1.1"` or higher +if you rely on them; an older host simply doesn't have them. + +**Why a separate permission instead of core API.** The rest of `api.data.*` exposes the internal +project model — tasks, calendar, relations — exactly what the importer made of it. The source route +returns the **full original bytes and tables**, including columns the import layer deliberately +never materializes into the project model (provenance fields such as `create_user`/`update_date`, +costs, review/location fields, unused UDFs, …). That is a materially larger exposure than "the app +reads this file" — every installed extension would otherwise be able to read and forward the raw +source text of every opened project, even without any other permission. Hence: default-deny, +explicitly declared in `manifest.json`. + +```js +// manifest.json → "permissions": ["importSource"] +const info = api.data.getImportSourceInfo(); // null outside an XER document +if (info) { + console.log(info.sourceFormat, info.archive.byteLength, info.catalogs.taskSourceRows.totalRows); +} +``` + +- **`getImportSourceInfo()`** → a small summary (source format, archive identity including + `sha256`/`byteLength`/`chunkCount`, number formatting, diagnostics counts, the import report, the + schedule-options provenance and catalog counts). No record contents. **`null`** when the active + document has no retained XER source (any non-XER document). +- **`getImportSourceChunk(index)`** → a fresh copy of one piece of the original file bytes. + Concatenate all chunks `0..chunkCount - 1` in order to reconstruct the **exact** original bytes — + compare against the `sha256` from `getImportSourceInfo()` to confirm. An invalid index throws a + `RangeError`; outside an XER document the method returns `null`. +- **`getImportSourceCatalogPage(collection, options?)`** → paginated, per-record-copied access to + the retained source tables (task source rows, resource/role/rate/curve/assignment rows, activity + codes, custom field definitions, UDF values, schedule-options source rows, …). `options.offset` + (default 0) and `options.limit` (default 100, **maximum 500 per page**) drive pagination; an + invalid value throws a `RangeError`. An `offset` past the end of the collection does not throw — + it is canonicalized to `total`, yielding an empty but valid last page. + +All three methods are bound to the **active document**: switching documents follows the source +route (or its absence) of the newly active document automatically. Every call returns a fresh, +independent copy — mutating a returned `info`, page item or chunk never touches the retained +archive. + +**Document drift while paginating.** "Follows automatically" is convenient for a single call, but a +**risk when paginating**: pagination is inherently multiple calls over time, and there is no +per-document page session. If the user switches documents (`switchDocument`) between two +`getImportSourceCatalogPage` calls, the second call simply returns a page from the **new** active +document — possibly an empty page that a naive extension reads as "done", while it actually just +mixed two projects together. Pass `options.expectedSourceProjectId` with the `sourceProjectId` you +got from an earlier call: if the active source selector no longer matches (including when the +active document no longer has any XER source at all), the call throws an +`ExtImportSourceDriftError` instead of silently continuing. Without this option there is **no** +drift protection. The same risk applies, to a lesser extent, to fetching a sequence of +`getImportSourceChunk` indices to reconstruct the source bytes — compare `sourceProjectId` (or the +`sha256`) between chunks if you cannot guarantee the document won't switch. + ### Data contract: the `Ext*` types Everything crossing the extension boundary via `api.data.*`, importer handlers and `sdk.factory.*` uses @@ -180,8 +241,9 @@ For a standalone `.js` file the manifest may be a comment block at the top: ## Limitations - The sandbox is light: extension code runs via `new Function(...)` and has access to `window`, - `document` and `fetch`. Permissions are enforced hard for `ribbon`/`events`, in warn mode for - `backstage`, and are purely informational for `filesystem`/`network`. Only install extensions you trust. + `document` and `fetch`. Permissions are enforced hard (default-deny) for `ribbon`/`events`/ + `pdf-fonts`/`importSource`, in warn mode for `backstage`, and are purely informational for + `filesystem`/`network`. Only install extensions you trust. - Objects from `api.data.get*()` are fresh, mutable `Ext*` copies — mutating them does not touch the store; write back via the mutating API functions. - The `@manifest` comment block in a standalone `.js` file must be a flat JSON object (no nested objects). diff --git a/docs/wiki/Features.md b/docs/wiki/Features.md index 1e97025f9..04e24ded0 100644 --- a/docs/wiki/Features.md +++ b/docs/wiki/Features.md @@ -29,6 +29,11 @@ bridge and automatic updates. (from the library, project-only, or orphaned) and a library/project view toggle. See [Resource libraries](docs://gids-resourcebibliotheken) in the manual. - **Assignments** — assign resources to tasks, with time-phased max-units availability. +- **Task types and work** — a work rule per task (fixed duration and units, fixed duration and + work, fixed work, fixed units — the MS Project task types and P6 duration types) decides which + corner of work = remaining duration × units moves when you edit another; remaining work per + assignment is editable in hours. Hidden by default; a file that already carries task types shows + them. See [Task types and work](docs://gids-taaktypes) in the manual. - **Histogram & leveling** — a resource histogram plus automatic leveling options, including leveling priority per task and leveling within slack only. - **Occupancy overview** — for multiple open projects drawing from the same library, a diff --git a/docs/xer-recovery-guardrails.md b/docs/xer-recovery-guardrails.md new file mode 100644 index 000000000..0a1d95f82 --- /dev/null +++ b/docs/xer-recovery-guardrails.md @@ -0,0 +1,45 @@ +# XER-bronarchief en crashherstel — technische vangrails + +Deze notitie beschrijft de opslaggrens van de X9-implementatie. Het doel is niet om de actuele +machineprestaties als productspecificatie vast te leggen, maar om te voorkomen dat een latere +optimalisatie opnieuw alle open documenten serialiseert of een half gecommitte herstelset kan +publiceren. + +## Harde, machine-onafhankelijke grenzen + +- De recoverydelta vergelijkt de volledige `IFCSaveSource` via `sameIFCSource`. `isDirty`, het + actieve tabblad en bestandspaden zijn manifestmetadata; ze zijn geen inhoudsrevisie. +- Eén inhoudsbewerking serialiseert precies één keer met `writeIFC` en levert precies één volledige + IFC-upsert. De overige open documenten houden hun bestaande snapshot. +- Een actieve-documentwissel is metadata-only: nul IFC-upserts, één manifestcommit. +- Tauri gebruikt manifestversie 3. Nieuwe documentinhoud krijgt een immutable generatienaam. Eerst + worden de volledige IFC-generaties via temp+rename gepubliceerd; daarna is de atomaire rename van + het manifest het commitpunt; oude eigen generaties worden pas daarna opgeruimd. +- De webbackend schrijft document-upserts, manifest en verwijderingen in één strikte IndexedDB- + `readwrite`-transactie. Een fout mag de persisted basis van de delta-tracker niet bevorderen. +- Recoverymanifesten van versie 1 en 2 blijven leesbaar. Schema-1 en schema-2 XER-bronarchieven + blijven eveneens leesbaar; schema 2 wordt in een koud proces via + `readIFCWithXerReconstruction` uit uitsluitend de opgeslagen bronbytes herbouwd. +- De OZB-corpusfixture opent en herstelt twaalf niet-lege documenten. Eén edit herschrijft daarvan + slechts één IFC-snapshot. De rehab-fixture herstelt haar bronbytes checksum-exact. + +Deze grenzen worden afgedwongen door `check-recovery-delta.ts`, +`measure-xer-recovery-write-amplification.ts`, `check-recovery-isolation.ts`, +`check-xer-archive-cold-read.ts` en `check-xer-archive-recovery-corpus.ts`. + +## Informatieve schaalmeting + +Op 2026-08-28 gaf één Linux/Node 22-run de volgende waarnemingen: + +- rehab-2, zelfstandige compacte IFC-ronde: bron 18.592.333 bytes, IFC 50.212.986 tekens, + 25,4 s walltime en 1.559.372 KiB peak RSS; +- rehab-2, volledige recoveryronde: 74,1 s walltime en 2.829.496 KiB peak RSS, met één document- + upsert en één manifest-put voor de edit; +- OZB, twaalf documenten: gezamenlijk 4.630.032 IFC-tekens, 4,8 s walltime en 315.400 KiB peak RSS, + eveneens met één document-upsert en één manifest-put. + +Deze tijd- en RSS-cijfers zijn bewust **geen pass/fail-drempels**. CPU, beschikbare RAM, garbage +collection, kernel/page-cache en CI-host verschillen te sterk. De corpuscheck eist wel dat de +metingen positief en eindig zijn, zodat een kapotte of overgeslagen probe niet groen kan lijken. +Regressies worden primair op de structurele schrijfvermenigvuldiging en checksum-exact herstel +gepoord; tijd en RSS blijven zichtbaar voor trendvergelijking. diff --git a/package.json b/package.json index baa2e1b7b..b2ff221ce 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:mcp": "bash tests/mcp/run.sh", "test:dev-server": "node --test tests/dev-server/*.test.mjs && bash tests/dev-server/integration.sh", "test:browser": "node scripts/run-browser-tests.mjs", + "test:browser:x11": "node tests/browser/x11-xer-evidence.mjs", "verify": "npm run typecheck && npm run lint && npm test && npm run verify:examples && npm run verify:docs && npm run verify:i18n && npm run verify:store-boundaries && npm run verify:gantt-boundaries && npm run verify:cycles", "preview": "vite preview", "tauri": "tauri", diff --git a/public/docs/ar/gids-import-export.md b/public/docs/ar/gids-import-export.md index 64f13e4b4..a29266b70 100644 --- a/public/docs/ar/gids-import-export.md +++ b/public/docs/ar/gids-import-export.md @@ -61,6 +61,8 @@ MSPDI أغنى بكثير من CSV: تُرافق الموارد والتخصيص ملف `.mpp` (الصيغة الأصلية لـ Microsoft Project، من Project 2010 إلى 2021) يسلك مسارًا منفصلاً: هذا الاستيراد **للقراءة فقط** — لا يوجد تصدير بصيغة `.mpp`، لذا فإن إعادة التصدير إلى MS Project يمر عبر MSPDI XML. راجع دليل [فتح MS Project (.mpp)](docs://gids-msproject-import) لمعرفة ما يُنقَل وما هي القيود. +ملف `.xer` هو صيغة تبادل Primavera P6. يُستورد مباشرة ويُحفظ كملف IFC بعد التحرير؛ راجع [فتح Primavera P6 (.xer)](docs://gids-xer-import). + ## استيراد الإضافات إلى جانب الصيغ الثابتة أعلاه، يمكن للإضافات المثبَّتة إضافة أدوات استيراد خاصة بها — مثلًا لصيغة غير مدعومة افتراضيًا. تظهر تلك تحت **Backstage ← استيراد**، كل منها باسمه ووصفه وامتدادات الملفات المطابقة؛ بدون أي إضافات استيراد مثبَّتة، يكون ذلك القسم فارغًا. تحقّق من **Backstage ← الإضافات** لرؤية ما هو متاح. diff --git a/public/docs/de/gids-import-export.md b/public/docs/de/gids-import-export.md index 8f885a134..aba784b4f 100644 --- a/public/docs/de/gids-import-export.md +++ b/public/docs/de/gids-import-export.md @@ -61,6 +61,8 @@ Diese Warnungen sind keine Schlamperei — sie sind eine bewusste, ausdrücklich Eine `.mpp`-Datei (das native Microsoft-Project-Format, Project 2010 bis 2021) ist ein eigener Weg: Dieser Import ist **nur lesend** — es gibt keinen `.mpp`-Export, ein Re-Export nach MS Project läuft daher über MSPDI-XML. Siehe die Anleitung [MS Project (.mpp) öffnen](docs://gids-msproject-import) für das, was mitkommt, und die Einschränkungen. +Eine `.xer`-Datei ist das Austauschformat von Primavera P6. Sie wird direkt importiert und nach einer Bearbeitung als IFC gespeichert; siehe [Primavera P6 (.xer) öffnen](docs://gids-xer-import). + ## Erweiterungs-Importer Über die festen Formate hinaus können installierte Erweiterungen eigene Importer hinzufügen — zum Beispiel für ein Format, das standardmäßig nicht unterstützt wird. Diese erscheinen unter **Backstage → Importieren**, jeweils mit eigenem Namen, Beschreibung und passenden Dateierweiterungen; ohne installierte Import-Erweiterungen ist dieser Abschnitt leer. Prüfen Sie **Backstage → Erweiterungen**, um zu sehen, was verfügbar ist. diff --git a/public/docs/en/gids-import-export.md b/public/docs/en/gids-import-export.md index 72abad341..40743390b 100644 --- a/public/docs/en/gids-import-export.md +++ b/public/docs/en/gids-import-export.md @@ -127,11 +127,17 @@ then shows exactly which items were dropped or simplified, and how many. ## Importing -**File → Open** (or **Backstage → Open**) accepts `.ifc`, `.csv`, `.xml` and `.mpp` files. For an +**File → Open** (or **Backstage → Open**) accepts `.ifc`, `.csv`, `.xml`, `.mpp` and `.xer` files. For an `.xml` file, the app detects on its own whether it's a Primavera P6 or an MS Project file, based on -the content. As described above: a CSV or P6 import produces a project **without baselines** (there +the content. As described above: a CSV or Primavera P6 XML import produces a project **without baselines** (there weren't any in the source), while IFC and MSPDI bring baselines along. +A `.xer` file is Primavera P6's own exchange format. The app reads it directly but does not write +`.xer` back: after editing, save as IFC. One XER can contain several current projects and baseline +projects; current projects open as separate documents and matching baselines remain attached to +their project. See [Opening Primavera P6 (.xer)](docs://gids-xer-import) for project selection, +text encoding, P6 number notation and retained source data. + A `.mpp` file (Microsoft Project's native format, Project 2010 through 2021) is a separate path: that import is **read-only** — there is no `.mpp` export, so exporting back to MS Project runs through MSPDI XML. See the guide [Opening MS Project (.mpp)](docs://gids-msproject-import) for @@ -153,7 +159,7 @@ section is empty. Check **Backstage → Extensions** to see what's available. ## Further reading -- Baselines only come along via IFC and MS Project XML, not via CSV or P6 — read the guide +- Baselines only come along via IFC and MS Project XML, not via CSV or Primavera P6 XML — read the guide [Baselines & progress](docs://gids-baselines-voortgang) for how to record a baseline. - Resources, assignments and loading curves — read the guide [Resources, histogram & leveling](docs://gids-resources-histogram) for how those are built before diff --git a/public/docs/en/gids-taaktypes.md b/public/docs/en/gids-taaktypes.md new file mode 100644 index 000000000..be3b764cd --- /dev/null +++ b/public/docs/en/gids-taaktypes.md @@ -0,0 +1,36 @@ +# Task types and work: fixed duration, fixed work or fixed units + +A task with resources has three numbers that belong together: the **remaining duration** (how many working days are left), the **units** per resource (units per working day, 1 = one person full-time) and the **work** (hours). Work = remaining duration × units. Change one of them and another must move. Which one moves is decided by the task's **work rule** — MS Project calls it the *task type* plus *effort-driven*, Primavera P6 the *duration type*. + +## Making it visible + +By default Open Planner Studio keeps duration and units and lets the work follow — exactly how the app has always scheduled. The work rule and the remaining work are then hidden. + +- **Setting**: turn on *Show task types (work rules)* under Settings (⚙, the Settings tab or Backstage → Settings). The work rule then appears in the properties panel and the task dialog, the *Work (rem.)* column in the assignment table, and the *Work rule* and *Remaining work* columns in the grid's column picker. +- **Automatic**: when you open a file that already contains task types (an `.mpp`, MSPDI, P6 or XER file with task types, or a work rule set earlier in this app), those controls are shown for that document regardless of the setting. The app tells you once. + +## The four work rules + +- **Fixed duration and units** (default; MS Project *Fixed Duration*, not effort-driven; P6 *Fixed Duration & Units/Time*): duration and units stay, work follows. Adding a resource does not change the duration. +- **Fixed duration and work** (P6 *Fixed Duration & Units*): duration and work stay, units follow. A second resource shares the work and lowers everyone's units. +- **Fixed work** (MS Project *Fixed Work*; P6 *Fixed Units*): the work stays. More units, or an extra resource, shortens the task; removing a resource lengthens it. +- **Fixed units** (MS Project *Fixed Units*, effort-driven; P6 *Fixed Units/Time*): the units stay. More work lengthens the task; an extra resource shares the work and shortens it. + +Switching the rule alone changes no number. Below the list the panel says in plain words what the chosen rule protects, and in the assignment table the protected column carries a lock. + +## Entering work + +In the assignment table the *Work (rem.)* column shows the remaining work in hours: stored work from the file, or otherwise remaining duration × units. Type a new number and the work rule decides what moves: under *Fixed work* or *Fixed units* the task gets longer or shorter (the schedule is then stale until you recalculate), under the two fixed-duration rules the units change. Material resources never drive the duration. + +In the grid the *Work rule* (list) and *Remaining work* (`name: hours; name: hours`) columns work the same way, also when pasting across several tasks. + +## Good to know + +- The rule works on the **remaining** part of a started task: actual duration and actual work never move. +- A day task keeps whole days: if work ÷ units yields half a day, the duration is rounded up and the work stays exact. +- Adding or removing a resource, also via *Move to…* or deleting a resource, follows the same rule. +- A **different calendar** (for the task, for the project, or different hours per day inside the calendar) changes the working hours per day; the work rule then decides. Under *Fixed work* and *Fixed units* a task gets longer when people work fewer hours per day (32 hours at 6 hours per day = 6 days). Under *Fixed duration and work* the units rise. Under the default rule nothing changes from before: duration and units stay, work follows. When a project or calendar change alters task durations, the app tells you how many. +- A **duration change on a started task** keeps the completed part (every progress entry records the remaining duration): what you add to or take from the duration goes to the remaining duration (never below zero). The percent complete is then recomputed as completed divided by the new duration, so the progress bar and the remaining duration agree. The same happens when a calendar change alters the duration of a started task. +- Every edit is one undo step. +- The project default work rule (for tasks without their own choice) can be set through the AI assistant; a UI for it will follow. +- Milestones, summary tasks, hammocks and elapsed-time tasks have no work rule. diff --git a/public/docs/en/gids-xer-import.md b/public/docs/en/gids-xer-import.md new file mode 100644 index 000000000..14b8cbea0 --- /dev/null +++ b/public/docs/en/gids-xer-import.md @@ -0,0 +1,62 @@ +# Opening Primavera P6 (.xer) + +A `.xer` file is Primavera P6's exchange format. Open Planner Studio can open it directly; no P6 XML conversion or external converter is required. This guide explains what the import does, which data remains available, and where the current P6 model has limits. + +## What you'll learn here + +- How one XER file can open several project documents. +- How current projects, empty projects and baseline projects are handled. +- Which calendar, resource, progress and metadata information is read. +- How text encoding and P6 number notation are determined safely. +- What saving as IFC means and which P6 features do not yet have their own scheduling model. + +## Opening and documents + +Open a `.xer` file through **File → Open** or **Ctrl+O**. One export can contain several P6 projects. Open Planner Studio opens every non-empty current project as a separate document; the document with the most activities becomes active. Empty projects do not create a pointless tab. + +After one file action, one informational notification appears, even when many documents open. It reports the actual projects found and opened, empty projects, baselines and any fallbacks. A later XER file action receives its own notification. + +A P6 baseline project is not opened as a separate schedulable document. When it belongs to an open current project, it is retained as that document's baseline. A baseline reference whose target is not present in the file is not invented: it stays out of the baseline collection and is counted in the notification. A self-reference, a cycle of baseline references, or a selection that would otherwise open no document safely reverses the exclusion: those projects then open as ordinary current documents. This prevents a silently empty screen and makes the fallback visible. + +Relations between two different P6 projects are retained as external source links. The app does not schedule them as ordinary relations, because every opened document is an independent schedule. + +## What comes from P6 + +The import reads, among other things: + +- **Projects, WBS, activities and milestones**, including P6 activity and duration types. +- **Relations, lags, constraints, progress and actuals**, plus P6 suspend/resume dates where the source file contains them. +- **Project and resource calendars**, working times and exceptions. Hours and clock times remain properties of the calendar rather than a project-wide guess. +- **Resources, rates and assignments**. +- **Activity codes, UDFs and notes**, including their source structure and activity links. + +The raw P6 source data that Open Planner Studio reads remains part of the document. It survives tab switching, undo, recovery and saving. That is different from claiming that every P6 feature already has an equivalent editing or scheduling model: when such a model is missing, source data is retained rather than silently discarded. + +## Text encoding and numbers + +XER does not reliably declare its text encoding in the file. A UTF BOM is followed; without one, the reader uses valid UTF-8 and otherwise falls back to Windows-1252. If that non-ASCII choice is needed, the opening notification states the encoding used. The app does not guess individual rows or describe them as "skipped". + +P6 can store decimal and thousands separators in the `CURRTYPE` table, either as literal characters or symbolic tokens such as `ds_Period` and `dg_Comma`. That notation is read before durations, work and float are converted. If `CURRTYPE` is absent, a dot is the safe default. If a value looks like a comma decimal while this source information is absent, import stops with a specific error instead of opening a potentially wrong schedule. + +## Saving and exchange + +An XER import is an **import**, not an XER editor or XER exporter. When you save afterwards, Open Planner Studio writes an IFC file. IFC is the app's native project file and retains the XER source data alongside the data the app uses. The original `.xer` file is never silently overwritten. + +For exchange with Primavera, use the existing **Primavera P6 XML** export. It is a different format with its own limits; see [Import/export](docs://gids-import-export). Keep the IFC file as well when you want to reopen an edited project later. + +## Limits that stay visible + +Some P6 concepts are already retained but do not yet have a fully equivalent scheduling model: + +- **`TT_Rsrc`** (resource-dependent activity) and **`TT_WBS`** are retained as P6 source types. The solver does not yet have a separate P6 scheduling mode for these types. +- A P6 resource curve with 21 points is retained as source distribution. A recognisable shape can be mapped to the nearest built-in curve for the histogram, but the original 21-point shape is not yet recalculated after an edit. +- The existing **P6 XML** reader and this XER reader do not yet cover the same full field set. XER can therefore contain data that P6 XML in the app does not yet read or write. + +These limits do not remove source data from the IFC project file. When XER-specific source data is present and you export to CSV, MS Project XML or Primavera P6 XML, that source information cannot fit completely in the target format. After a successful export, one informational notification appears with a link to this guide. If you cancel the export or saving fails, that notification does not appear. Exporting to IFC retains the XER source data; the other exports include only the data their own format supports. The original `.xer` file is not overwritten. + +## Further reading + +- [Calendars & hour planning](docs://gids-kalenders-uren) explains how working times and exceptions shape a schedule. +- [Resources, histogram & leveling](docs://gids-resources-histogram) covers resources, assignments and loading in Open Planner Studio. +- [Baselines & progress](docs://gids-baselines-voortgang) explains how to use baselines after import. +- [Import/export](docs://gids-import-export) compares IFC, CSV, MS Project XML and Primavera P6 XML. diff --git a/public/docs/es/gids-import-export.md b/public/docs/es/gids-import-export.md index cba64bc4a..324cf4e73 100644 --- a/public/docs/es/gids-import-export.md +++ b/public/docs/es/gids-import-export.md @@ -101,6 +101,8 @@ importación es **de solo lectura** — no existe una exportación `.mpp`, así pasa por MSPDI XML. Consulte la guía [Abrir MS Project (.mpp)](docs://gids-msproject-import) para saber qué se conserva y cuáles son las limitaciones. +Un archivo `.xer` es el formato de intercambio de Primavera P6. Se importa directamente y, después de editarlo, se guarda como IFC; consulta [Abrir Primavera P6 (.xer)](docs://gids-xer-import). + ## Importadores de extensiones Más allá de los formatos fijos anteriores, las extensiones instaladas pueden añadir sus propios importadores — por ejemplo para un diff --git a/public/docs/fa/gids-import-export.md b/public/docs/fa/gids-import-export.md index f550204dc..98b3ef9d6 100644 --- a/public/docs/fa/gids-import-export.md +++ b/public/docs/fa/gids-import-export.md @@ -99,6 +99,8 @@ MSPDI به‌طور قابل‌توجهی غنی‌تر از CSV است: منا MS Project از راه MSPDI XML انجام می‌شود. برای دیدن اینکه چه چیزی همراه می‌آید و محدودیت‌ها چیستند، به راهنمای [باز کردن MS Project (.mpp)](docs://gids-msproject-import) مراجعه کنید. +فایل `.xer` قالب تبادل Primavera P6 است. مستقیماً وارد می‌شود و پس از ویرایش به‌صورت IFC ذخیره می‌شود؛ [باز کردن Primavera P6 (.xer)](docs://gids-xer-import) را ببینید. + ## درون‌ریزکننده‌های افزونه‌ای جدا از قالب‌های ثابت بالا، افزونه‌های نصب‌شده می‌توانند درون‌ریزکننده‌های خودشان را اضافه کنند — برای مثال برای diff --git a/public/docs/fr/gids-import-export.md b/public/docs/fr/gids-import-export.md index c8933ed50..9f422ed84 100644 --- a/public/docs/fr/gids-import-export.md +++ b/public/docs/fr/gids-import-export.md @@ -61,6 +61,8 @@ Ces avertissements ne sont pas de la négligence — c'est un choix délibéré Un fichier `.mpp` (le format natif de Microsoft Project, Project 2010 à 2021) suit un chemin séparé : cet import est **en lecture seule** — il n'existe pas d'export `.mpp`, donc réexporter vers MS Project passe par MSPDI XML. Voir le guide [Ouvrir MS Project (.mpp)](docs://gids-msproject-import) pour savoir ce qui est conservé et quelles sont les limites. +Un fichier `.xer` est le format d'échange de Primavera P6. Il est importé directement et, après modification, enregistré au format IFC ; voir [Ouvrir Primavera P6 (.xer)](docs://gids-xer-import). + ## Importateurs d'extension Au-delà des formats fixes ci-dessus, les extensions installées peuvent ajouter leurs propres importateurs — par exemple pour un format qui n'est pas pris en charge par défaut. Ceux-ci apparaissent sous **Backstage → Importer**, chacun avec son propre nom, sa description et ses extensions de fichier correspondantes ; sans extension d'import installée, cette section est vide. Consultez **Backstage → Extensions** pour voir ce qui est disponible. diff --git a/public/docs/it/gids-import-export.md b/public/docs/it/gids-import-export.md index 347c6a04d..d1c7334c4 100644 --- a/public/docs/it/gids-import-export.md +++ b/public/docs/it/gids-import-export.md @@ -108,6 +108,8 @@ riesportare verso MS Project passa per MSPDI XML. Vedi la guida [Aprire MS Project (.mpp)](docs://gids-msproject-import) per sapere cosa viene incluso e quali sono i limiti. +Un file `.xer` è il formato di scambio di Primavera P6. Viene importato direttamente e, dopo la modifica, salvato come IFC; vedi [Aprire Primavera P6 (.xer)](docs://gids-xer-import). + ## Importatori tramite estensioni Oltre ai formati fissi sopra descritti, le estensioni installate possono aggiungere i propri diff --git a/public/docs/ja/gids-import-export.md b/public/docs/ja/gids-import-export.md index be67ff9e8..a78b8ddc7 100644 --- a/public/docs/ja/gids-import-export.md +++ b/public/docs/ja/gids-import-export.md @@ -61,6 +61,8 @@ MSPDI と同種のトレードオフがあり、いくつか P6 固有の癖も `.mpp` ファイル(Microsoft Project のネイティブ形式、Project 2010〜2021)は別の経路をたどります。このインポートは**読み取り専用**です —`.mpp` のエクスポートは存在しないため、MS Project への再エクスポートは MSPDI XML 経由になります。何が引き継がれ、どのような制限があるかについては、ガイド[MS Project(.mpp)を開く](docs://gids-msproject-import)を参照してください。 +`.xer` は Primavera P6 の交換形式です。直接インポートし、編集後は IFC として保存します。詳しくは[Primavera P6(.xer)を開く](docs://gids-xer-import)をご覧ください。 + ## 拡張機能インポーター 上記の固定形式に加えて、インストールされた拡張機能は独自のインポーターを追加できます — 例えば既定ではサポートされていない形式のためです。それらは **Backstage → インポート** の下に、それぞれ名前、説明、対応するファイル拡張子とともに表示されます。インポート用の拡張機能が1つもインストールされていない場合、そのセクションは空です。利用可能な拡張機能については **Backstage → 拡張機能** を確認してください。 diff --git a/public/docs/ko/gids-import-export.md b/public/docs/ko/gids-import-export.md index 890f1576d..cfbdab07b 100644 --- a/public/docs/ko/gids-import-export.md +++ b/public/docs/ko/gids-import-export.md @@ -98,6 +98,8 @@ MSPDI와 같은 종류의 트레이드오프이며, P6 특유의 몇 가지 특 내보내려면 MSPDI XML을 거칩니다. 무엇이 함께 오고 어떤 제한이 있는지는 [MS Project(.mpp) 열기](docs://gids-msproject-import) 가이드를 참고하세요. +`.xer`은 Primavera P6의 교환 형식입니다. 직접 가져오며 편집 후 IFC로 저장합니다. [Primavera P6(.xer) 열기](docs://gids-xer-import)를 참조하세요. + ## 확장 프로그램 가져오기 도구 위의 고정된 형식 외에도, 설치된 확장 프로그램은 기본적으로 지원되지 않는 형식 등을 위해 자체 diff --git a/public/docs/manifest.json b/public/docs/manifest.json index 1afee11ab..f31fca9aa 100644 --- a/public/docs/manifest.json +++ b/public/docs/manifest.json @@ -210,6 +210,26 @@ "layer": "gidsen", "cluster": "import-export" }, + { + "id": "gids-taaktypes", + "title": { + "nl": "Taaktypes en werk", + "en": "Task types and work", + "de": "Vorgangsarten und Arbeit", + "fr": "Types de tâche et travail", + "es": "Tipos de tarea y trabajo", + "zh": "任务类型与工时", + "it": "Tipi di attività e lavoro", + "pt": "Tipos de tarefa e trabalho", + "pl": "Typy zadań i praca", + "tr": "Görev türleri ve iş", + "ar": "أنواع المهام والعمل", + "ja": "タスクの種類と作業", + "ko": "작업 유형과 작업량", + "fa": "انواع کار و کار" + }, + "layer": "gidsen" + }, { "id": "gids-msproject-import", "title": { @@ -231,6 +251,27 @@ "layer": "gidsen", "cluster": "msproject-import" }, + { + "id": "gids-xer-import", + "title": { + "nl": "Primavera P6 (.xer) openen", + "en": "Opening Primavera P6 (.xer)", + "de": "Primavera P6 (.xer) öffnen", + "fr": "Ouvrir Primavera P6 (.xer)", + "es": "Abrir Primavera P6 (.xer)", + "zh": "打开 Primavera P6(.xer)", + "it": "Aprire Primavera P6 (.xer)", + "pt": "Abrir Primavera P6 (.xer)", + "pl": "Otwieranie Primavera P6 (.xer)", + "tr": "Primavera P6 (.xer) dosyasını açma", + "ar": "فتح Primavera P6 (.xer)", + "ja": "Primavera P6(.xer)を開く", + "ko": "Primavera P6(.xer) 열기", + "fa": "باز کردن Primavera P6 (.xer)" + }, + "layer": "gidsen", + "cluster": "xer-import" + }, { "id": "datums-zoals-opgeslagen", "title": { diff --git a/public/docs/nl/gids-import-export.md b/public/docs/nl/gids-import-export.md index 580f34cc3..d6952a02a 100644 --- a/public/docs/nl/gids-import-export.md +++ b/public/docs/nl/gids-import-export.md @@ -128,12 +128,19 @@ ontwikkelaarsconsole toont dan exact welke items zijn weggelaten of vereenvoudig ## Importeren -**Bestand → Openen** (of **Backstage → Openen**) accepteert `.ifc`-, `.csv`-, `.xml`- en -`.mpp`-bestanden. Bij een `.xml`-bestand herkent de app zelf of het een Primavera P6- of een MS -Project-bestand is, aan de hand van de inhoud. Zoals hierboven beschreven: een CSV- of P6-import +**Bestand → Openen** (of **Backstage → Openen**) accepteert `.ifc`-, `.csv`-, `.xml`-, `.mpp`- en +`.xer`-bestanden. Bij een `.xml`-bestand herkent de app zelf of het een Primavera P6- of een MS +Project-bestand is, aan de hand van de inhoud. Zoals hierboven beschreven: een CSV- of Primavera P6 XML-import levert een project op **zonder baselines** (die stonden er niet in), terwijl IFC en MSPDI baselines wél meebrengen. +Een `.xer`-bestand is Primavera P6's eigen uitwisselingsformaat. De app leest het rechtstreeks, +maar schrijft geen `.xer` terug: na een bewerking sla je op als IFC. Eén XER kan meerdere huidige +projecten en baselineprojecten bevatten; de huidige projecten openen als afzonderlijke documenten +en bijbehorende baselines blijven aan hun project gekoppeld. Zie +[Primavera P6 (.xer) openen](docs://gids-xer-import) voor de projectselectie, tekencodering, +P6-getalnotatie en de bewaarde brondata. + Een `.mpp`-bestand (het native Microsoft Project-formaat, Project 2010 t/m 2021) is een aparte route: die import is **alleen-lezen** — er bestaat geen `.mpp`-export, dus terugexporteren naar MS Project loopt via MSPDI-XML. Zie de gids [MS Project (.mpp) openen](docs://gids-msproject-import) @@ -157,7 +164,7 @@ extensies beschikbaar zijn. ## Verder lezen -- Baselines gaan alleen mee via IFC en MS Project XML, niet via CSV of P6 — lees de gids +- Baselines gaan alleen mee via IFC en MS Project XML, niet via CSV of Primavera P6 XML — lees de gids [Baselines & voortgang](docs://gids-baselines-voortgang) voor hoe je een baseline vastlegt. - Resources, toewijzingen en belastingscurves — lees de gids [Resources, histogram & nivellering](docs://gids-resources-histogram) voor hoe die tot stand komen diff --git a/public/docs/nl/gids-taaktypes.md b/public/docs/nl/gids-taaktypes.md new file mode 100644 index 000000000..927740f0c --- /dev/null +++ b/public/docs/nl/gids-taaktypes.md @@ -0,0 +1,36 @@ +# Taaktypes en werk: vaste duur, vast werk of vaste inzet + +Een taak met resources heeft drie getallen die bij elkaar horen: de **restduur** (hoeveel werkdagen er nog zijn), de **inzet** per resource (eenheden per werkdag, 1 = één persoon voltijds) en het **werk** (uren). Werk = restduur × inzet. Verandert er één, dan moet een ander getal meebewegen. Welk getal dat is, bepaalt de **werkregel** van de taak — in MS Project heet dat het *taaktype* plus *effort-driven*, in Primavera P6 het *duration type*. + +## Zichtbaar maken + +Standaard houdt Open Planner Studio duur en inzet vast en volgt het werk — precies zoals de app altijd al plande. De werkregel en het resterende werk worden dan niet getoond. + +- **Instelling**: zet *Toon taaktypes (werkregels)* aan onder Instellingen (⚙, het tabblad Instellingen of Backstage → Instellingen). Dan verschijnen de werkregel in het eigenschappenpaneel en de taakdialoog, de kolom *Werk (rest)* in de toewijzingstabel en de kolommen *Werkregel* en *Resterend werk* in de kolomkiezer van het raster. +- **Automatisch**: opent u een bestand dat al taaktypes bevat (een `.mpp`, MSPDI-, P6- of XER-bestand met taaktypes, of een eerder in deze app gezette werkregel), dan zijn die bedieningselementen voor dát document zichtbaar, ongeacht de instelling. De app meldt dat één keer. + +## De vier werkregels + +- **Vaste duur en inzet** (standaard; MS Project *Fixed Duration*, niet effort-driven; P6 *Fixed Duration & Units/Time*): duur en inzet blijven staan, het werk volgt. Een resource erbij verandert de duur niet. +- **Vaste duur en werk** (P6 *Fixed Duration & Units*): duur en werk blijven staan, de inzet volgt. Een tweede resource verdeelt het werk en verlaagt ieders inzet. +- **Vast werk** (MS Project *Fixed Work*; P6 *Fixed Units*): het werk blijft staan. Meer inzet, of een resource erbij, maakt de taak korter; een resource eraf maakt haar langer. +- **Vaste inzet** (MS Project *Fixed Units*, effort-driven; P6 *Fixed Units/Time*): de inzet blijft staan. Meer werk maakt de taak langer; een resource erbij verdeelt het werk en maakt haar korter. + +Alleen de regel wisselen verandert geen enkel getal. Onder de keuzelijst staat in gewone woorden wat de gekozen regel beschermt, en in de toewijzingstabel draagt de beschermde kolom een slotje. + +## Werk invoeren + +In de toewijzingstabel toont de kolom *Werk (rest)* het resterende werk in uren: opgeslagen werk uit het bestand, of anders restduur × inzet. Typ een nieuw getal en de werkregel bepaalt wat meebeweegt: onder *Vast werk* of *Vaste inzet* wordt de taak langer of korter (de planning is dan verouderd tot u opnieuw berekent), onder de twee vaste-duur-regels verandert de inzet. Materiaalresources tellen niet mee voor de duur. + +In het raster werken de kolommen *Werkregel* (keuzelijst) en *Resterend werk* (`naam: uren; naam: uren`) op dezelfde manier, ook bij plakken over meerdere taken. + +## Wat u moet weten + +- De regel werkt op het **resterende** deel van een gestarte taak: verrichte duur en verricht werk bewegen nooit. +- Een dagtaak houdt hele dagen: levert werk ÷ inzet een halve dag op, dan wordt de duur naar boven afgerond en blijft het werk exact staan. +- Een resource erbij of eraf, ook via *Verplaats naar…* of het verwijderen van een resource, volgt dezelfde regel. +- Een **andere kalender** (voor de taak, voor het project, of andere uren per dag in de kalender zelf) verandert het aantal werkuren per dag; daarna beslist de werkregel. Onder *Vast werk* en *Vaste inzet* wordt een taak langer als de mensen minder uren per dag maken (32 uur op 6 uur per dag = 6 dagen). Onder *Vaste duur en werk* stijgt de inzet. Onder de standaardregel blijft alles zoals voorheen: duur en inzet blijven, het werk volgt. Verandert een project- of kalenderwijziging de duur van taken, dan meldt de app hoeveel. +- Een **duurwijziging op een gestarte taak** laat het verrichte deel staan (elke voortgangsinvoer legt de resterende duur vast): wat u aan de duur toevoegt of afhaalt, komt bij de resterende duur (nooit onder nul). Het percentage gereed wordt daarna opnieuw berekend als verricht gedeeld door de nieuwe duur, zodat de voortgangsbalk en de resterende duur hetzelfde zeggen. Hetzelfde gebeurt wanneer een kalenderwijziging de duur van een gestarte taak verandert. +- Elke bewerking is één stap ongedaan te maken. +- De projectstandaard-werkregel (voor taken zonder eigen keuze) is via de AI-assistent te zetten; een UI daarvoor volgt. +- Mijlpalen, verzameltaken, hangmatten en taken op doorlooptijd hebben geen werkregel. diff --git a/public/docs/nl/gids-xer-import.md b/public/docs/nl/gids-xer-import.md new file mode 100644 index 000000000..b6d69f1f5 --- /dev/null +++ b/public/docs/nl/gids-xer-import.md @@ -0,0 +1,62 @@ +# Primavera P6 (.xer) openen + +Een `.xer`-bestand is het uitwisselingsformaat van Primavera P6. Open Planner Studio kan zo'n bestand rechtstreeks openen; een tussenstap via P6 XML of een externe omzetter is niet nodig. Deze gids legt uit wat de import doet, welke gegevens bewaard blijven en waar de grenzen van het huidige P6-model liggen. + +## Wat je hier leert + +- Hoe één XER-bestand meerdere projectdocumenten kan openen. +- Hoe huidige projecten, lege projecten en baselineprojecten worden behandeld. +- Welke kalender-, resource-, voortgangs- en metadata-informatie wordt ingelezen. +- Hoe tekencodering en de P6-getalnotatie veilig worden bepaald. +- Wat opslaan als IFC betekent en welke P6-functies nog geen eigen rekenmodel hebben. + +## Openen en documenten + +Open een `.xer`-bestand via **Bestand → Openen** of **Ctrl+O**. Eén export kan meerdere P6-projecten bevatten. Open Planner Studio opent ieder niet-leeg huidig project als een afzonderlijk document; het document met de meeste activiteiten wordt actief. Lege projecten krijgen geen zinloos tabblad. + +Na één bestandsactie verschijnt één informatieve melding, ook wanneer er veel documenten openen. Die melding noemt de werkelijk gevonden en geopende projecten, lege projecten, baselines en eventuele terugvallen. Bij een volgende XER-bestandsactie krijg je opnieuw één eigen melding. + +Een P6-baselineproject wordt niet als los, planbaar document geopend. Als het bij een geopend huidig project hoort, wordt het als baseline bij dat document bewaard. Een verwijzing naar een baseline die niet in het bestand staat wordt niet verzonnen: die blijft buiten de baselineverzameling en wordt in de melding geteld. Een zelfverwijzing, een kring van baselineverwijzingen of een selectie die anders geen enkel document zou openen, schakelt de uitsluiting veilig terug: de betrokken projecten openen dan gewoon als huidige documenten. Dat beschermt je tegen een stil leeg scherm en maakt de terugval zichtbaar. + +Relaties tussen twee verschillende P6-projecten worden als externe bronlinks bewaard. De app rekent ze niet door als gewone relaties, omdat elk geopend document een zelfstandige planning is. + +## Wat er uit P6 meekomt + +De import leest onder meer: + +- **Projecten, WBS, activiteiten en mijlpalen**, inclusief P6-activiteits- en duurtypen. +- **Relaties, lags, constraints, voortgang en actuals**, plus P6-suspend/resume-datums waar ze in het bronbestand staan. +- **Project- en resourcekalenders**, werktijden en uitzonderingen. Uren en kloktijden blijven daarbij gegevens van de kalender in plaats van een projectbrede gok. +- **Resources, tarieven en toewijzingen**. +- **Activity codes, UDF's en notities**, inclusief hun bronstructuur en koppelingen aan activiteiten. + +De rauwe P6-brongegevens die Open Planner Studio leest, blijven onderdeel van het document. Ze reizen mee door tabwissels, undo, herstel en opslaan. Dat is iets anders dan beloven dat iedere P6-functie al een gelijkwaardig bewerk- of rekenmodel heeft: waar zo'n motor ontbreekt, bewaren we de brondata in plaats van haar stil weg te gooien. + +## Tekencodering en getallen + +XER noemt zijn tekencodering niet betrouwbaar in het bestand. Een UTF-BOM wordt gevolgd; zonder BOM gebruikt de lezer geldige UTF-8 en valt hij anders terug op Windows-1252. Is zo'n niet-ASCII-keuze nodig, dan staat de gebruikte codering in de openingsmelding. De app probeert geen regels te raden of als "overgeslagen" voor te stellen. + +P6 kan de decimaal- en duizendtallenscheiding in de `CURRTYPE`-tabel vastleggen, zowel als letterlijk teken als met symbolische tokens zoals `ds_Period` en `dg_Comma`. Die notatie wordt gelezen vóór duren, werk en float worden omgezet. Ontbreekt `CURRTYPE`, dan is punt de veilige standaard. Lijkt een waarde een komma-decimaal terwijl die broninformatie ontbreekt, dan stopt de import met een gerichte fout in plaats van een mogelijk verkeerd schema te openen. + +## Opslaan en uitwisselen + +Een XER-import is een **import**, geen XER-editor of XER-exporter. Wanneer je daarna opslaat, schrijft Open Planner Studio een IFC-bestand. Dat IFC is het eigen projectbestand en bewaart de gelezen XER-brondata naast de gegevens waarmee de app werkt. Het oorspronkelijke `.xer`-bestand wordt nooit stil overschreven. + +Voor uitwisseling naar Primavera bestaat de bestaande **Primavera P6 XML**-export. Dat is een ander formaat met eigen beperkingen; zie [Im-/export](docs://gids-import-export). Bewaar daarom altijd ook het IFC-bestand wanneer je een bewerkt project later opnieuw wilt openen. + +## Grenzen die zichtbaar blijven + +Een paar P6-begrippen zijn al opgeslagen, maar hebben nog geen volledig gelijkwaardig rekenmodel: + +- **`TT_Rsrc`** (resource-dependent activity) en **`TT_WBS`** worden als P6-brontype bewaard. De solver heeft nog geen afzonderlijke P6-rekenmodus voor deze typen. +- Een P6-resourcecurve met 21 punten wordt als bronverdeling bewaard. Een herkenbare vorm kan voor het histogram naar de dichtstbijzijnde ingebouwde curve worden vertaald, maar de oorspronkelijke 21-puntsvorm wordt na een bewerking nog niet opnieuw berekend. +- De bestaande **P6 XML**-lezer en deze XER-lezer hebben nog niet dezelfde volledige veldendekking. XER kan daarom gegevens bevatten die P6 XML in de app nog niet leest of schrijft. + +Deze grenzen verwijderen geen brongegevens uit het IFC-projectbestand. Als XER-specifieke brondata aanwezig is en je exporteert naar CSV, MS Project XML of Primavera P6 XML, past die broninformatie niet volledig in het doelformaat. Na een geslaagde export verschijnt daarom één informatieve melding met een link naar deze gids. Annuleer je de export of mislukt het opslaan, dan verschijnt die melding niet. De export naar IFC bewaart de XER-brondata; de andere exports nemen alleen de gegevens mee die hun eigen formaat ondersteunt. Het oorspronkelijke `.xer`-bestand wordt niet overschreven. + +## Verder lezen + +- [Kalenders & uren-planning](docs://gids-kalenders-uren) legt uit hoe werktijden en uitzonderingen de planning sturen. +- [Resources, histogram & nivellering](docs://gids-resources-histogram) behandelt resources, toewijzingen en belasting in Open Planner Studio. +- [Baselines & voortgang](docs://gids-baselines-voortgang) legt het gebruik van baselines na import uit. +- [Im-/export](docs://gids-import-export) vergelijkt IFC, CSV, MS Project XML en Primavera P6 XML. diff --git a/public/docs/pl/gids-import-export.md b/public/docs/pl/gids-import-export.md index eb38a4599..b47a88b7c 100644 --- a/public/docs/pl/gids-import-export.md +++ b/public/docs/pl/gids-import-export.md @@ -101,6 +101,8 @@ Plik `.mpp` (natywny format Microsoft Project, Project 2010–2021) to osobna ś MSPDI XML. Zobacz przewodnik [Otwieranie MS Project (.mpp)](docs://gids-msproject-import), aby dowiedzieć się, co jest przenoszone i jakie są ograniczenia. +Plik `.xer` jest formatem wymiany Primavera P6. Jest importowany bezpośrednio, a po edycji zapisywany jako IFC; zobacz [Otwieranie Primavera P6 (.xer)](docs://gids-xer-import). + ## Importery z rozszerzeń Poza powyższymi stałymi formatami, zainstalowane rozszerzenia mogą dodawać własne importery — na przykład dla diff --git a/public/docs/pt/gids-import-export.md b/public/docs/pt/gids-import-export.md index 42e6287cc..81da5a4a8 100644 --- a/public/docs/pt/gids-import-export.md +++ b/public/docs/pt/gids-import-export.md @@ -103,6 +103,8 @@ para o MS Project passa pelo MSPDI XML. Veja o guia [Abrir o MS Project (.mpp)](docs://gids-msproject-import) para saber o que é trazido e quais são as limitações. +Um ficheiro `.xer` é o formato de intercâmbio do Primavera P6. É importado diretamente e, depois de editado, guardado como IFC; consulte [Abrir o Primavera P6 (.xer)](docs://gids-xer-import). + ## Importadores de extensões Além dos formatos fixos acima, as extensões instaladas podem adicionar os seus próprios importadores — por exemplo para um diff --git a/public/docs/tr/gids-import-export.md b/public/docs/tr/gids-import-export.md index 7dbb5c326..eeeb9424f 100644 --- a/public/docs/tr/gids-import-export.md +++ b/public/docs/tr/gids-import-export.md @@ -61,6 +61,8 @@ Bu uyarılar özensizlik değildir — kasıtlı, açık bir seçimdir: düşür Bir `.mpp` dosyası (Microsoft Project'in yerel biçimi, Project 2010–2021) ayrı bir yol izler: bu içe aktarma **salt okunurdur** — bir `.mpp` dışa aktarımı yoktur, bu yüzden MS Project'e yeniden dışa aktarma MSPDI XML üzerinden yapılır. Neyin geldiğini ve sınırlamaların neler olduğunu görmek için [MS Project (.mpp) dosyasını açma](docs://gids-msproject-import) kılavuzuna bakın. +`.xer` dosyası Primavera P6'nın değişim biçimidir. Doğrudan içe aktarılır ve düzenlemeden sonra IFC olarak kaydedilir; bkz. [Primavera P6 (.xer) dosyasını açma](docs://gids-xer-import). + ## Uzantı içe aktarıcıları Yukarıdaki sabit biçimlerin ötesinde, yüklü uzantılar kendi içe aktarıcılarını ekleyebilir — örneğin varsayılan olarak desteklenmeyen bir biçim için. Bunlar **Backstage → İçe aktar** altında, her biri kendi adı, açıklaması ve eşleşen dosya uzantılarıyla görünür; hiçbir içe aktarma uzantısı yüklü değilken bu bölüm boştur. Nelerin mevcut olduğunu görmek için **Backstage → Uzantılar**'ı kontrol edin. diff --git a/public/docs/zh/gids-import-export.md b/public/docs/zh/gids-import-export.md index 385ac2d57..413d1b280 100644 --- a/public/docs/zh/gids-import-export.md +++ b/public/docs/zh/gids-import-export.md @@ -61,6 +61,8 @@ MSPDI 比 CSV 丰富得多:资源、分配(包括其负荷曲线)、日历 `.mpp` 文件(Microsoft Project 的原生格式,Project 2010 至 2021)走的是另一条路径:该导入是**只读**的——不存在 `.mpp` 导出格式,因此重新导出到 MS Project 要走 MSPDI XML。参见指南[打开 MS Project(.mpp)](docs://gids-msproject-import)了解都保留了哪些内容以及有哪些限制。 +`.xer` 是 Primavera P6 的交换格式。应用可直接导入它;编辑后保存为 IFC。请参阅[打开 Primavera P6(.xer)](docs://gids-xer-import)。 + ## 扩展导入器 除了上述固定格式外,已安装的扩展可以添加自己的导入器——例如针对默认不支持的格式。它们会显示在 **Backstage → 导入**下,各自带有自己的名称、描述和匹配的文件扩展名;未安装任何导入扩展时,该部分为空。查看 **Backstage → 扩展**了解有哪些可用。 diff --git a/scripts/README.md b/scripts/README.md index 046c84e36..ce8a980a7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -43,6 +43,13 @@ aangeroepen: - `example-resources.ts` — de resourcepool - `example-topologies.json` — de relatienetwerken (117 kB data, geen code) +## Meetlatdata genereren + +`node scripts/generate-p6-verified-cases.mjs ` schrijft +`tests/planning/cases-p6-verified.json` opnieuw uit de publieke P6 23.12-capture. Alleen +`activity_code` en de `*_p6`-kolommen komen mee; kloktijden en actual-suffixen blijven onvertaald. +De `*_engine`-kolommen en PASS-oordelen zijn uitdrukkelijk geen brondata voor deze generator. + ## Release en publicatie | script | aangeroepen door | doet | diff --git a/scripts/generate-p6-verified-cases.mjs b/scripts/generate-p6-verified-cases.mjs new file mode 100644 index 000000000..2f990e8c2 --- /dev/null +++ b/scripts/generate-p6-verified-cases.mjs @@ -0,0 +1,144 @@ +#!/usr/bin/env node + +/** + * Extraheert uitsluitend de door P6 23.12 vastgelegde kolommen uit het publieke + * cpp-cpm-engine-vergelijkingsraamwerk. De enginekolommen en verdicts worden niet ingelezen in het + * resultaat: hun vergelijking gooide kloktijden weg, normaliseerde exclusieve/inclusieve finishes + * en de bronengine is na de capture op deze cases nagefit. Ze zijn daarom geen onafhankelijke + * meetlat voor Open Planner Studio. + * + * Gebruik: + * node scripts/generate-p6-verified-cases.mjs [uitvoer-json] + * + * De bronmap mag ook via OPS_P6_COMPARISON worden gezet. Er staat bewust geen lokaal corpuspad in + * dit script. + */ + +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const sourceRoot = process.argv[2] ?? process.env.OPS_P6_COMPARISON; +const outputPath = process.argv[3] + ?? fileURLToPath(new URL('../tests/planning/cases-p6-verified.json', import.meta.url)); + +if (!sourceRoot || !existsSync(join(sourceRoot, 'cases'))) { + console.error('Geef een bestaande p6-comparison-map via argument 1 of OPS_P6_COMPARISON.'); + process.exit(1); +} + +function parseCsv(text) { + const rows = []; + let row = []; + let cell = ''; + let quoted = false; + for (let index = 0; index < text.length; index++) { + const char = text[index]; + if (quoted) { + if (char === '"' && text[index + 1] === '"') { + cell += '"'; + index++; + } else if (char === '"') { + quoted = false; + } else { + cell += char; + } + continue; + } + if (char === '"') quoted = true; + else if (char === ',') { + row.push(cell); + cell = ''; + } else if (char === '\n') { + row.push(cell.endsWith('\r') ? cell.slice(0, -1) : cell); + rows.push(row); + row = []; + cell = ''; + } else { + cell += char; + } + } + if (cell.length > 0 || row.length > 0) { + row.push(cell); + rows.push(row); + } + return rows; +} + +const selectedColumns = ['activity_code', 'ES_p6', 'EF_p6', 'LS_p6', 'LF_p6', 'TF_p6', 'FF_p6']; +const expectedCases = { + '01-fs-chain': { digest: '9d06cd88cfdce11a33d853ca5410088d9ab0a1a82aa384237147cd9a08ac70fb', activities: ['A', 'B', 'C'] }, + '02-ss-with-lag': { digest: '7719cb13f2bfd5297dc5302f9f2ab672376d7b6e7cafcbfa5c52cbbf64c45cae', activities: ['A', 'B'] }, + '03-ff-with-lag': { digest: 'bb8f76a2bdb926bdd0e7177f28c94da117cd8c55b110882f84ae958ed028d2da', activities: ['A', 'B'] }, + '04-sf-edge-case': { digest: 'be04f8ccd8430726635b87654b2039d64fb70779f12562f260bdf3e72d3eb654', activities: ['A', 'B'] }, + '05-negative-float': { digest: '463f4ea2ee5e973340d82b26e2adbf0305b0815161f8b78dc074f381140d22b1', activities: ['A', 'B'] }, + '06-multiple-calendars': { digest: '8623f28be05dfcbee390746c5ece5aacfb5c1501edbee3e74ffee92ac1730432', activities: ['A', 'B'] }, + '07-ontario-holidays': { digest: '23ec9703eab963f8f1300ecdc7bfadd902668314d3008e172048a7adb5042026', activities: ['A'] }, + '08-in-progress-retained-logic': { digest: '67c9811d83cdeace67fe7b410aa59b8550ae30cee287d6ee2bda7cd4347e255d', activities: ['A', 'B'] }, + '09-completed-successor': { digest: '0307a19fb51a61f1ad7b6445cbd6f2a20618ee4408912bc2cd320aca6ed1416b', activities: ['A', 'B'] }, + '10-out-of-sequence-progress': { digest: '2f598578d4539a95ea80698ab2979fd28a3cb007d8fe09e679a1173f00f5c5f6', activities: ['A', 'B'] }, + '11-mandatory-start-finish': { digest: 'b4353c7fcdfff89ae1740cfa1e85a5c72d7c9493a53eea37a5cab246b828453b', activities: ['A', 'B'] }, + '12-snet-fnlt': { digest: '42480cf535150559b1ed5ed11d5e7604ee3c6ae1d666b9a555ab84b4936f0ee6', activities: ['A', 'B'] }, + '13-alap': { digest: '6ed931a167215ec05a8410c0751a02e429026b62410f0fed4f8768746f6a37bc', activities: ['A', 'B', 'C'] }, +}; +const expectedCaseIds = Object.keys(expectedCases); +const caseDirs = readdirSync(join(sourceRoot, 'cases'), { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .sort(); +if (JSON.stringify(caseDirs) !== JSON.stringify(expectedCaseIds)) { + throw new Error(`onverwachte caseset: verwacht ${expectedCaseIds.join(', ')}, kreeg ${caseDirs.join(', ')}`); +} + +const cases = caseDirs.map(id => { + const csvPath = join(sourceRoot, 'cases', id, 'comparison.csv'); + if (!existsSync(csvPath)) throw new Error(`${id}: comparison.csv ontbreekt`); + const sourceBytes = readFileSync(csvPath); + const digest = createHash('sha256').update(sourceBytes).digest('hex'); + if (digest !== expectedCases[id].digest) { + throw new Error(`${id}: bron-SHA-256 verwacht ${expectedCases[id].digest}, kreeg ${digest}`); + } + const [header, ...rows] = parseCsv(sourceBytes.toString('utf-8')); + const indexes = selectedColumns.map(column => { + const matches = header.map((value, index) => value === column ? index : -1).filter(index => index >= 0); + if (matches.length !== 1) throw new Error(`${id}: kolom ${column} moet exact eenmaal voorkomen`); + return matches[0]; + }); + const activities = rows + .filter(row => row.some(cellValue => cellValue !== '')) + .map((row, rowIndex) => { + const activity = Object.fromEntries(selectedColumns.map((column, index) => [column, row[indexes[index]] ?? ''])); + for (const field of ['ES_p6', 'EF_p6', 'LS_p6', 'LF_p6']) { + if (activity[field] !== '' && !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?: A)?$/.test(activity[field])) { + throw new Error(`${id}/rij ${rowIndex + 2}/${field}: ongeldige P6-datum ${JSON.stringify(activity[field])}`); + } + } + for (const field of ['TF_p6', 'FF_p6']) { + if (activity[field] !== '' && !/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/.test(activity[field])) { + throw new Error(`${id}/rij ${rowIndex + 2}/${field}: ongeldige P6-float ${JSON.stringify(activity[field])}`); + } + } + return activity; + }); + const activityCodes = activities.map(activity => activity.activity_code); + if (new Set(activityCodes).size !== activityCodes.length) throw new Error(`${id}: dubbele activity_code`); + if (JSON.stringify(activityCodes) !== JSON.stringify(expectedCases[id].activities)) { + throw new Error(`${id}: activiteitset verwacht ${expectedCases[id].activities.join(', ')}, kreeg ${activityCodes.join(', ')}`); + } + return { id, sourceSha256: digest, activities }; +}); + +const output = { + provenance: { + repository: 'https://github.com/danafitkowski/cpp-cpm-engine.git', + sourceCommit: 'c279a5c4ff204ba763a6f9726aa6383574b50475', + capture: 'Menselijke P6 23.12-capture van 2026-08-11; het primaire capturesheet is niet meegecommit.', + scope: 'Uitsluitend activity_code en de onvertaalde *_p6-kolommen zijn overgenomen; kloktijden en actual-suffixen blijven staan.', + warning: 'De *_engine-kolommen en PASS-oordelen zijn geen meetlat: daarbij is tijd weggegooid, het exclusief/inclusief-finishverschil genormaliseerd en de bronengine na deze capture nagefit.', + }, + cases, +}; + +writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`, 'utf-8'); +console.log(`P6-vergelijkingsdata geschreven: ${cases.length} cases naar ${outputPath}`); diff --git a/scripts/i18n-diff.mjs b/scripts/i18n-diff.mjs index 3a165ad01..7cb5d1a84 100644 --- a/scripts/i18n-diff.mjs +++ b/scripts/i18n-diff.mjs @@ -84,11 +84,37 @@ const baseFiles = fs.readdirSync(path.join(localesDir, base)).filter((f) => f.en let totalMissing = 0; const report = {}; +const basePluralReport = {}; for (const file of baseFiles) { const baseData = JSON.parse(fs.readFileSync(path.join(localesDir, base, file), 'utf8')); const basePaths = collectPaths(baseData); + // De bronlocale is óók een echte locale. Zonder deze check kan iemand bijvoorbeeld nl/_other + // verwijderen: de 13 doelvertalingen vergelijken dan nog steeds met de verminkte bron en de + // poort blijft ten onrechte groen. Verzamel pluralen per stam, zodat alle CLDR-vormen van het + // Nederlands precies eenmaal vereist zijn, net als verderop voor elke doelvertaling. + const foundByStem = new Map(); + for (const p of basePaths) { + const match = p.match(PLURAL_SUFFIX); + if (!match) continue; + const stem = p.slice(0, -match[0].length); + const found = foundByStem.get(stem) || new Set(); + found.add(match[1]); + foundByStem.set(stem, found); + } + const baseCategories = categoriesFor(base); + for (const [stem, found] of foundByStem) { + const missing = [...baseCategories].filter(category => !found.has(category)); + const extra = [...found].filter(category => !baseCategories.has(category)); + if (!missing.length && !extra.length) continue; + const problems = []; + if (missing.length) problems.push(`${stem}: ontbreekt ${missing.join(', ')}`); + if (extra.length) problems.push(`${stem}: overbodig ${extra.join(', ')}`); + basePluralReport[file] = [...(basePluralReport[file] || []), ...problems]; + totalMissing += missing.length + extra.length; + } + for (const locale of others) { const expected = expectedPathsFor(basePaths, locale); const filePath = path.join(localesDir, locale, file); @@ -111,10 +137,19 @@ for (const file of baseFiles) { const jsonMode = process.argv.includes('--json'); if (jsonMode) { // Rapportagemodus: bedoeld om doorgesluisd te worden, dus geen exitcode-poort. - console.log(JSON.stringify(report, null, 2)); + console.log(JSON.stringify({ ...(Object.keys(basePluralReport).length ? { [base]: basePluralReport } : {}), ...report }, null, 2)); process.exit(0); } +if (Object.keys(basePluralReport).length) { + const count = Object.values(basePluralReport).reduce((total, paths) => total + paths.length, 0); + console.log(`\n=== ${base}: ${count} ongeldige CLDR-pluralvorm(en) ===`); + for (const [file, paths] of Object.entries(basePluralReport)) { + console.log(` ${file}:`); + for (const problem of paths) console.log(` - ${problem}`); + } +} + for (const locale of others) { const files = report[locale]; const count = files ? Object.values(files).reduce((a, b) => a + b.length, 0) : 0; diff --git a/src/App.tsx b/src/App.tsx index 747aae154..1cdf9d57d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,7 @@ import { StructureLockedNotice } from '@/components/layout/StructureLockedNotice import { DependencyModeNotice } from '@/components/layout/DependencyModeNotice'; import { RecordedDatesNotice } from '@/components/layout/RecordedDatesNotice'; import { NotificationHost } from '@/components/layout/NotificationHost'; +import { DOCUMENT_TABPANEL_ID, documentTabId } from '@/components/layout/DocumentChrome/documentTabNavigation'; // Code-splitting (pakket E2): componenten die pas achter een `ui.show*`-vlag, een ribbontab of een // overlay renderen worden lazy geladen, zodat hun code niet in de eager first-load-bundel zit maar @@ -104,6 +105,7 @@ function AppContent() { const uiFontFamily = useAppStore(s => s.ui.uiFontFamily); const uiFontScale = useAppStore(s => s.ui.uiFontScale); const documentChromeStyle = useAppStore(s => s.ui.documentChromeStyle); + const activeDocumentId = useAppStore(s => s.activeDocumentId); // Recovery-restore bij opstarten (Tauri én web): detectie + RecoveryDialog-callbacks; levert ook @@ -276,6 +278,11 @@ function AppContent() {
{isFullPanel ? ( // Full panel views (Table, IFC, Report) — eigen kaart diff --git a/src/components/backstage/Backstage.tsx b/src/components/backstage/Backstage.tsx index bd1d878bc..517d9072a 100644 --- a/src/components/backstage/Backstage.tsx +++ b/src/components/backstage/Backstage.tsx @@ -266,7 +266,7 @@ function ExamplesSection() { const res = await fetch(`${import.meta.env.BASE_URL}examples/${ex.file}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const content = await res.text(); - openExampleFromString(content, ex.name, buildImportLabels(tCommon)); + await openExampleFromString(content, ex.name, buildImportLabels(tCommon)); // Showcase-voorbeelden delen één demo-resourcebibliotheek (issue #19, user-verzoek). De // laadgrens heeft al gerekend; het linken herleidt zelf de resourcebelasting opnieuw. if (ex.category === 'showcase') applyDemoLibraryToShowcaseProject(); diff --git a/src/components/backstage/HelpPanel.tsx b/src/components/backstage/HelpPanel.tsx index 2839e4b25..3ca8dd0f2 100644 --- a/src/components/backstage/HelpPanel.tsx +++ b/src/components/backstage/HelpPanel.tsx @@ -185,7 +185,7 @@ export function HelpPanel() { const res = await fetch(`${import.meta.env.BASE_URL}examples/${file}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const content = await res.text(); - openExampleFromString(content, file, buildImportLabels(tCommon)); + await openExampleFromString(content, file, buildImportLabels(tCommon)); // Showcase-voorbeelden delen één demo-resourcebibliotheek (issue #19, user-verzoek): zelfde // volgorde als Backstage → Voorbeelden (`ExamplesSection.handleOpen`). Deze aanroeper kent // alleen de bestandsnaam (geen manifest-`category`) — de showcase-bestanden dragen allemaal het diff --git a/src/components/canvas/ContextMenu.tsx b/src/components/canvas/ContextMenu.tsx index 3b74e163a..fdfe699d9 100644 --- a/src/components/canvas/ContextMenu.tsx +++ b/src/components/canvas/ContextMenu.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useClickOutside } from '@/hooks/useClickOutside'; import { Task } from '@/types/task'; import type { WorkCalendar } from '@/types/calendar'; +import { isSummaryTask } from '@/utils/taskHierarchy'; export interface ContextMenuGroupInfo { key: string; @@ -129,7 +130,7 @@ export function ContextMenu({ // (anders valt de flyout goeddeels buiten beeld). const flipSub = pos.left > window.innerWidth - 380; - const isSummary = task ? task.childIds.length > 0 : false; + const isSummary = isSummaryTask(task ?? undefined); const closeAll = () => { setOpenSub(null); onClose(); }; return ( diff --git a/src/components/dialogs/PoolImportDialog.tsx b/src/components/dialogs/PoolImportDialog.tsx index 2da921700..8157d604a 100644 --- a/src/components/dialogs/PoolImportDialog.tsx +++ b/src/components/dialogs/PoolImportDialog.tsx @@ -92,7 +92,7 @@ export function PoolImportDialog() { const res = await openFileDialog([{ name: 'IFC', extensions: ['ifc'] }]); if (!res) return; try { - const pool = readPoolIFC(res.content); + const pool = await readPoolIFC(res.content); setImported(pool); // Voorselectie (issue #19, kern van de fix): gedelegeerd aan de PURE `resolvePoolImportPreselection` // (critreview F1 — direct headless testbaar, spiegelt de reviewer zijn eigen diff --git a/src/components/dialogs/TaskDialog.tsx b/src/components/dialogs/TaskDialog.tsx index 35a564f21..ade65fe0c 100644 --- a/src/components/dialogs/TaskDialog.tsx +++ b/src/components/dialogs/TaskDialog.tsx @@ -19,6 +19,7 @@ import { TaskProgressFields } from '@/components/task-sections/TaskProgressField import { TaskCpmResultSection } from '@/components/task-sections/TaskCpmResultSection'; import { TaskDependenciesSection } from '@/components/task-sections/TaskDependenciesSection'; import { TaskAssignmentsSection } from '@/components/task-sections/TaskAssignmentsSection'; +import { TaskWorkRuleField } from '@/components/task-sections/TaskWorkRuleField'; import { TaskCodesFieldsSection } from '@/components/task-sections/TaskCodesFieldsSection'; import { getPersonalTaskTypes } from '@/services/taskTypes/personalTaskTypes'; import { TaskDurationField } from '@/components/task-sections/TaskDurationField'; @@ -45,6 +46,7 @@ export function TaskDialog() { const setUI = useAppStore(s => s.setUI); const addTask = useAppStore(s => s.addTask); const updateTask = useAppStore(s => s.updateTask); + const setTaskWorkRule = useAppStore(s => s.setTaskWorkRule); const moveTask = useAppStore(s => s.moveTask); const project = useAppStore(s => s.project); const constructionMode = useAppStore(s => s.ui.constructionMode); @@ -69,6 +71,7 @@ export function TaskDialog() { // store) i.p.v. `draft.time`, zodat een eventuele CPM-herberekening tijdens het open staan van de // dialoog niet wordt teruggedraaid door een verouderde draft-snapshot. const [startDate, setStartDate] = useState(''); + const initialDurationRef = useRef<{ unit: 'days' | 'hours'; scheduleDuration: number; durationMinutes?: number } | null>(null); const calendars = useAppStore(s => s.calendars); const projectCal = useAppStore(s => s.calendar); const nameInputRef = useRef(null); @@ -93,6 +96,9 @@ export function TaskDialog() { if (editingTask) { setDraft({ ...editingTask, time: { ...editingTask.time } }); + initialDurationRef.current = { + unit: editingTask.time.durationUnit, scheduleDuration: editingTask.time.scheduleDuration, durationMinutes: editingTask.time.durationMinutes, + }; // Toon de berekende start (consistent met tabel/Gantt); scheduleStart is de geplande anker. setStartDate(editingTask.time.earlyStart || editingTask.time.scheduleStart); } else { @@ -127,11 +133,21 @@ export function TaskDialog() { // teruggedraaid. Voortgangs-velden (completion/actualStart/actualFinish) komen WEL uit de // draft — dat zijn de enige `time`-subvelden die deze dialoog-sessie zelf muteert buiten de // hieronder-berekende schedule-ankervelden. + // Review B4 (taaktypes): de duur ALLEEN uit de draft wanneer de gebruiker hem in deze sessie + // wijzigde — anders zou Opslaan een duur die de werkdriehoek intussen via de toewijzingssectie + // veranderde stil terugdraaien. + const initial = initialDurationRef.current; + const durationTouched = !initial + || draft.time.durationUnit !== initial.unit + || draft.time.scheduleDuration !== initial.scheduleDuration + || draft.time.durationMinutes !== initial.durationMinutes; const time = { ...editingTask.time, - durationUnit: draft.time.durationUnit, - scheduleDuration: draft.time.scheduleDuration, - durationMinutes: draft.time.durationUnit === 'hours' ? draft.time.durationMinutes : undefined, + ...(durationTouched ? { + durationUnit: draft.time.durationUnit, + scheduleDuration: draft.time.scheduleDuration, + durationMinutes: draft.time.durationUnit === 'hours' ? draft.time.durationMinutes : undefined, + } : {}), completion: draft.time.completion, actualStart: draft.time.actualStart, actualFinish: draft.time.actualFinish, @@ -178,6 +194,7 @@ export function TaskDialog() { wbsCode: draft.wbsCode, taskType: draft.taskType, customTaskTypeId: draft.customTaskTypeId, + workRule: draft.workRule, isMilestone: draft.isMilestone, parentId: draft.parentId || null, calendarId: draft.calendarId, @@ -297,6 +314,18 @@ export function TaskDialog() {
+ {/* Taaktypes-etappe (spec §7): zelfde veld als het paneel; commit op Opslaan via `workRule` + in de updateTask-/addTask-patch (de store legt het werk vast, K1). */} + { + // Review B4: op een bestaande taak direct committen (zoals de toewijzingssectie, die óók + // rechtstreeks op de store werkt) zodat werk/inzet in dezelfde dialoog met de gekozen + // regel rekenen; de draft spiegelt. Een nieuwe taak houdt 'm in de draft tot Opslaan. + onChange(patch); + if (editingTask) setTaskWorkRule(editingTask.id, patch.workRule); + }} + /> diff --git a/src/components/layout/DocumentChrome/CloseDocumentDialog.tsx b/src/components/layout/DocumentChrome/CloseDocumentDialog.tsx index 56b77a303..7abbbb8d4 100644 --- a/src/components/layout/DocumentChrome/CloseDocumentDialog.tsx +++ b/src/components/layout/DocumentChrome/CloseDocumentDialog.tsx @@ -1,6 +1,13 @@ +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppStore } from '@/state/appStore'; import { documentTitle } from '@/utils/documents'; +import { CloseDocumentDialogControl } from './CloseDocumentDialogControl'; +import { + createCloseDocumentActionGate, + createCloseDocumentDialogActions, + type CloseDocumentActionGate, +} from './closeDocumentActions'; /** * Sluit-bevestiging met drie keuzes bij een document met niet-opgeslagen @@ -21,7 +28,24 @@ export function CloseDocumentDialog() { const setUI = useAppStore((s) => s.setUI); const closeDocument = useAppStore((s) => s.closeDocument); const switchDocument = useAppStore((s) => s.switchDocument); - const saveFile = useAppStore((s) => s.saveFile); + const saveFileForDocument = useAppStore((s) => s.saveFileForDocument); + const openerRef = useRef(null); + const cancelButtonRef = useRef(null); + const actionGateRef = useRef(null); + const [savePending, setSavePending] = useState(false); + if (!actionGateRef.current) actionGateRef.current = createCloseDocumentActionGate(); + + useEffect(() => { + if (!pendingId) { + actionGateRef.current = createCloseDocumentActionGate(); + setSavePending(false); + return; + } + if (actionGateRef.current?.started) return; + const activeElement = document.activeElement; + openerRef.current = activeElement instanceof HTMLElement ? activeElement : null; + cancelButtonRef.current?.focus(); + }, [pendingId]); if (!pendingId) return null; @@ -30,55 +54,34 @@ export function CloseDocumentDialog() { const fp = pendingId === activeId ? filePath : entry?.payload?.filePath ?? null; const name = documentTitle(fp, proj?.name ?? '') || t('project.untitled'); - const cancel = () => setUI({ pendingCloseDocId: null }); - const dontSave = () => { closeDocument(pendingId); setUI({ pendingCloseDocId: null }); }; - const save = async () => { - if (pendingId !== useAppStore.getState().activeDocumentId) switchDocument(pendingId); - try { - await saveFile(); - // Alleen sluiten als het opslaan ook echt lukte (bij een geannuleerde 'Opslaan als…' of een - // schrijffout blijft isDirty staan → document open laten, geen werk verliezen). - if (!useAppStore.getState().isDirty) closeDocument(pendingId); - } finally { - // Ongeacht de afloop moet de bevestiging weg, anders blokkeert een mislukte opslag de hele - // app. Blijft nodig óók nu `saveFile` zelf vangt — verdedigingslinie tegen een throw hogerop. - setUI({ pendingCloseDocId: null }); - } + const restoreOpenerFocus = () => { + const opener = openerRef.current; + if (opener?.isConnected) opener.focus({ preventScroll: true }); }; + const actions = createCloseDocumentDialogActions({ + gate: actionGateRef.current, + pendingId, + getActiveDocumentId: () => useAppStore.getState().activeDocumentId, + switchDocument, + closeDocument, + saveFile: () => saveFileForDocument(pendingId), + clearPending: () => { setUI({ pendingCloseDocId: null }); }, + restoreOpenerFocus, + onSavePendingChange: setSavePending, + }); return ( -
-
e.stopPropagation()} - style={{ - width: 420, maxWidth: '90%', background: 'var(--theme-surface-elevated)', - border: '1px solid var(--theme-border)', borderRadius: 'var(--radius-lg)', - boxShadow: 'var(--shadow-pop)', padding: 20, - }} - > -

- {t('documents.closeTitle')} -

-

- {t('documents.closeBody', { name })} -

-
- - - -
-
-
+ { void actions.save(); }} + /> ); } diff --git a/src/components/layout/DocumentChrome/CloseDocumentDialogControl.tsx b/src/components/layout/DocumentChrome/CloseDocumentDialogControl.tsx new file mode 100644 index 000000000..4f4cb96a0 --- /dev/null +++ b/src/components/layout/DocumentChrome/CloseDocumentDialogControl.tsx @@ -0,0 +1,87 @@ +import type { MouseEvent, Ref } from 'react'; + +interface CloseDocumentDialogControlProps { + title: string; + body: string; + cancelLabel: string; + discardLabel: string; + saveLabel: string; + busy: boolean; + cancelButtonRef: Ref; + onCancel: () => void; + onDiscard: () => void; + onSave: () => void; +} + +/** Prop-gedreven dialooglaag, zodat de echte backdrop- en knopwiring gericht uitvoerbaar is. */ +export function CloseDocumentDialogControl({ + title, + body, + cancelLabel, + discardLabel, + saveLabel, + busy, + cancelButtonRef, + onCancel, + onDiscard, + onSave, +}: CloseDocumentDialogControlProps) { + return ( +
+
) => event.stopPropagation()} + style={{ + width: 420, maxWidth: '90%', background: 'var(--theme-surface-elevated)', + border: '1px solid var(--theme-border)', borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-pop)', padding: 20, + }} + > +

+ {title} +

+

+ {body} +

+
+ + + +
+
+
+ ); +} diff --git a/src/components/layout/DocumentChrome/DocumentChrome.css b/src/components/layout/DocumentChrome/DocumentChrome.css index c03eccfff..e1b973fc3 100644 --- a/src/components/layout/DocumentChrome/DocumentChrome.css +++ b/src/components/layout/DocumentChrome/DocumentChrome.css @@ -23,15 +23,25 @@ padding: 0 6px; gap: 3px; } .ops-tabstrip-menu { align-self: center; width: 30px; height: 26px; margin-right: 2px; } +.ops-tabstrip-scroll-button { align-self: center; width: 24px; height: 26px; flex: none; } +.ops-tabstrip-viewport { + align-self: stretch; flex: 1 1 auto; min-width: 0; overflow-x: auto; overflow-y: hidden; + scrollbar-width: thin; scroll-behavior: smooth; +} +.ops-tabstrip-tabs { display: flex; align-items: stretch; gap: 3px; min-width: max-content; height: 100%; } +.ops-tab-group { + position: relative; align-self: flex-end; flex: none; min-width: 158px; max-width: 230px; height: 100%; +} .ops-tab { - position: relative; display: flex; align-items: center; gap: 8px; - padding: 0 8px 0 11px; min-width: 158px; max-width: 230px; align-self: flex-end; + position: relative; display: flex; align-items: center; gap: 8px; flex: none; + width: 100%; padding: 0 32px 0 11px; min-width: 0; max-width: none; align-self: flex-end; height: 31px; border-radius: 7px 7px 0 0; border: 1px solid transparent; - border-bottom: none; background: transparent; cursor: pointer; overflow: hidden; + border-bottom: none; background: transparent; cursor: pointer; overflow: hidden; outline: none; font-family: var(--font-body); } .ops-tab:hover { background: var(--theme-hover); } .ops-tab.active { background: var(--theme-ribbon-content-bg); border-color: var(--theme-border); } +.ops-tab:focus-visible { box-shadow: inset 0 0 0 2px var(--theme-accent); } .ops-tab.active::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--theme-accent); } @@ -43,11 +53,12 @@ .ops-tab.active .ops-tab-name { font-weight: 600; color: var(--theme-text); } .ops-dirty-dot { width: 7px; height: 7px; border-radius: 9999px; flex: none; background: var(--theme-warning-text); } .ops-tab-close { + position: absolute; right: 7px; bottom: 6px; z-index: 1; width: 18px; height: 18px; flex: none; display: flex; align-items: center; justify-content: center; background: transparent; border: none; border-radius: 5px; cursor: pointer; color: var(--theme-text-muted); } .ops-tab-close:hover { background: var(--theme-hover); color: var(--theme-text); } -.ops-tabstrip-add { align-self: flex-end; width: 30px; height: 31px; border-radius: 7px 7px 0 0; } +.ops-tabstrip-add { align-self: flex-end; width: 30px; height: 31px; border-radius: 7px 7px 0 0; flex: none; } /* ---------- B: Projectbalk (rail) ---------- */ .ops-rail { diff --git a/src/components/layout/DocumentChrome/DocumentTabBar.tsx b/src/components/layout/DocumentChrome/DocumentTabBar.tsx index 3f638e980..e030636a7 100644 --- a/src/components/layout/DocumentChrome/DocumentTabBar.tsx +++ b/src/components/layout/DocumentChrome/DocumentTabBar.tsx @@ -1,13 +1,52 @@ +import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { Menu, Plus, X } from 'lucide-react'; +import { Menu, Plus } from 'lucide-react'; +import { useAppStore } from '@/state/appStore'; import { useDocumentCards, useDocumentActions } from './useDocumentCards'; +import { DocumentTabList } from './DocumentTabList'; +import { + documentTabCloseFocusTarget, + focusDocumentTab, + revealDocumentTab, +} from './documentTabNavigation'; +import { documentTabFocusTargetOutsideOverview } from './projectOverviewFocus'; import './DocumentChrome.css'; /** A · Documenttabs — horizontale tabstrip onder het lint. */ export function DocumentTabBar() { - const { t } = useTranslation('common'); + const { t, i18n } = useTranslation('common'); const cards = useDocumentCards(); const { switchTo, closeWithGuard, chooseNewOrOpenProject, openOverview } = useDocumentActions(); + const projectOverviewOpen = useAppStore((state) => state.ui.showProjectOverview); + const tabRefs = useRef(new Map()); + const previousDocumentIds = useRef(cards.map(card => card.id)); + const activeId = cards.find(card => card.isActive)?.id; + const direction = i18n.dir() === 'rtl' ? 'rtl' : 'ltr'; + + // Focus verschuift uitsluitend na een echte verwijdering uit de gecommitteerde kaartenlijst. Dit + // dekt schoon sluiten én de twee effectieve dirty-routes (opslaan/niet opslaan), ook wanneer een + // sluiting vanuit het projectoverzicht komt; annuleren laat de bestaande focus ongemoeid. + useLayoutEffect(() => { + const currentDocumentIds = cards.map(card => card.id); + const requestedDocumentId = previousDocumentIds.current.find(id => !currentDocumentIds.includes(id)) ?? null; + const focusTarget = documentTabFocusTargetOutsideOverview( + projectOverviewOpen, + documentTabCloseFocusTarget(previousDocumentIds.current, cards, requestedDocumentId), + ); + previousDocumentIds.current = currentDocumentIds; + if (focusTarget) tabRefs.current.get(focusTarget)?.focus({ preventScroll: true }); + }, [cards, projectOverviewOpen]); + + // Iedere route kan een document activeren; alleen hier kennen we de horizontale viewport. + useEffect(() => { + if (!activeId) return; + const tab = tabRefs.current.get(activeId); + if (tab) revealDocumentTab(tab); + }, [activeId]); + + const focusTab = useCallback((id: string) => { + focusDocumentTab(tabRefs.current, id); + }, []); return (
@@ -19,28 +58,21 @@ export function DocumentTabBar() { - {cards.map((card) => ( -
switchTo(card.id)} - data-ops-tab={card.id} - data-testid="document-tab" - > - - {card.title} - {card.isDirty && } - -
- ))} +
+ { + if (element) tabRefs.current.set(documentId, element); + else tabRefs.current.delete(documentId); + }} + switchTo={switchTo} + focusTab={focusTab} + closeWithGuard={closeWithGuard} + /> +
+ +
+ ); +} diff --git a/src/components/layout/DocumentChrome/DocumentTabList.tsx b/src/components/layout/DocumentChrome/DocumentTabList.tsx new file mode 100644 index 000000000..fa8721d98 --- /dev/null +++ b/src/components/layout/DocumentChrome/DocumentTabList.tsx @@ -0,0 +1,50 @@ +import type { Ref } from 'react'; +import type { DocumentCard } from './useDocumentCards'; +import { DocumentTabControl } from './DocumentTabControl'; +import { + handleDocumentTabKeyDown, + type DocumentTabDirection, +} from './documentTabNavigation'; + +interface DocumentTabListProps { + cards: DocumentCard[]; + direction: DocumentTabDirection; + tablistLabel: string; + closeLabel: string; + tabRef: (documentId: string, element: HTMLButtonElement | null) => void; + switchTo: (documentId: string) => void; + focusTab: (documentId: string) => void; + closeWithGuard: (card: Pick) => void; +} + +/** De werkelijk gebruikte, prop-gedreven eventroute voor alle zichtbare documenttabs. */ +export function DocumentTabList({ + cards, + direction, + tablistLabel, + closeLabel, + tabRef, + switchTo, + focusTab, + closeWithGuard, +}: DocumentTabListProps) { + const documentIds = cards.map(card => card.id); + return ( +
+ {cards.map((card, index) => ( + { tabRef(card.id, element); }) as Ref} + onSelect={(event) => { switchTo(card.id); event.currentTarget.focus(); }} + onKeyDown={(event) => { + handleDocumentTabKeyDown(event, documentIds, card.id, direction, { switchTo, focusTab }); + }} + onClose={(event) => { event.stopPropagation(); closeWithGuard(card); }} + /> + ))} +
+ ); +} diff --git a/src/components/layout/DocumentChrome/ProjectOverview.tsx b/src/components/layout/DocumentChrome/ProjectOverview.tsx index b22563c80..2c1929d0b 100644 --- a/src/components/layout/DocumentChrome/ProjectOverview.tsx +++ b/src/components/layout/DocumentChrome/ProjectOverview.tsx @@ -1,7 +1,9 @@ +import { useLayoutEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Plus, X } from 'lucide-react'; import { useAppStore } from '@/state/appStore'; import { useDocumentCards, useDocumentActions, type DocumentCard } from './useDocumentCards'; +import { projectOverviewCloseButtonId, projectOverviewCloseFocusTarget } from './projectOverviewFocus'; import './DocumentChrome.css'; /** @@ -14,6 +16,37 @@ export function ProjectOverview() { const open = useAppStore((s) => s.ui.showProjectOverview); const cards = useDocumentCards(); const { switchTo, closeWithGuard, chooseNewOrOpenProject, closeOverview } = useDocumentActions(); + const closeButtonRefs = useRef(new Map()); + const previousDocumentIds = useRef(cards.map(card => card.id)); + const wasOpen = useRef(false); + + useLayoutEffect(() => { + const currentDocumentIds = cards.map(card => card.id); + if (!open) { + previousDocumentIds.current = currentDocumentIds; + wasOpen.current = false; + return; + } + + if (!wasOpen.current) { + wasOpen.current = true; + const initialTarget = cards.find(card => card.isActive)?.id ?? currentDocumentIds[0] ?? null; + if (initialTarget) closeButtonRefs.current.get(initialTarget)?.focus({ preventScroll: true }); + previousDocumentIds.current = currentDocumentIds; + return; + } + + const removedDocumentId = previousDocumentIds.current.find( + documentId => !currentDocumentIds.includes(documentId), + ) ?? null; + const focusTarget = projectOverviewCloseFocusTarget( + previousDocumentIds.current, + currentDocumentIds, + removedDocumentId, + ); + previousDocumentIds.current = currentDocumentIds; + if (focusTarget) closeButtonRefs.current.get(focusTarget)?.focus({ preventScroll: true }); + }, [cards, open]); if (!open) return null; @@ -98,8 +131,14 @@ export function ProjectOverview() { )}