Research & Evidence
This guide was written against the live module source, not from intent. This page records what was verified, and how.
Primary sources read
| Claim area | Verified against |
|---|---|
| Module identity, permissions, menus | module.json, config/module_config.php, config/permissions.php, config/menu.php + default_menu.php |
| Workflow rules | config/statuses.php (the single state-map source) + libraries/Law_office_workflow.php (acceptance guard, close guard, verified immutability, supersede chain) |
| Conflict search behavior | libraries/Law_office_conflicts.php (four passes, ≤20 terms, involvement freezing) + helpers/law_office_helper.php (normalization, accent folding, SOUNDEX, token matching) |
| Visibility & confidentiality | law_office_matter_visibility_sql() and its use in the matters/docket/deadlines/tasks/events models; intake scoping in Law_intakes_model; law_access_logs writers |
| Docket & sweep | controllers/Law_office_docket.php, jobs/DeadlineReminderSweepJob.php (ladder, dedupe marker, writes-before-notify) |
| Documents pipeline | libraries/Law_office_documents.php (CityBPLO hybrid: move_temp_file_smart() + central general_files row + module linkage) |
| APIs | config/api_routes.php / euapi_routes.php / api_scope.php, both controllers, and the regenerated OpenAPI specs |
What the database harnesses proved
Each build phase closed with a throwaway seeder harness run through the full CI stack against the real database (then deleted). Highlights of what passed:
- Visibility (14 checks) — an outsider's list excludes a restricted matter; team member, lead and bypass-holder see it; direct access denied to the outsider; a permitted restricted view writes the access log.
- Conflicts (17 checks) — a withdrawn intake's party still hits a later search; “Gerardo Yap” finds “Gerardo M. Yap” (token pass — added when the initial run missed it); dispositions cannot be edited or deleted; acceptance blocked until a clearing disposition exists; latest disposition wins.
- Sweep (12 checks, real
cron:runtwice) — each ladder rung fires exactly once; second run is a no-op; a restricted matter's hearings and deadlines are invisible to outsiders in the calendar and register while its own team's reminders still fire. - Work product (24 checks) — task lifecycle with the completer stamp; the restricted-matter rule holds for cross-matter task reads; document store/download/remove round-trip (S3 mode); the exactly-one-owner rule refuses both-owner and no-owner rows; the source-document dropdown contract.
- Demo seeder — load (37 rows) → idempotent re-run (0) →
--remove(37 soft-deleted) → re-run restores the same rows.
Standing test suite
php erpat module:test LawOffice — 7 classes / 45 tests pinning: the manifest and its
version↔CHANGELOG agreement, the frozen 25-key permission catalog, live-menu↔default-menu lockstep,
nav descriptions, route→controller resolution, the module gate in every controller, the widgets pair,
the API sidecars and self-only EU identity, the visibility fragment's four arms and int-cast safety,
all three workflow state maps, and the conflict normalization primitives (including accent folding —
José/Jose must share one corpus key, a bug found and fixed by this very test).
The schema-drift guard (added after two shipped bugs)
Two defects reached users that no linter or behavioral test could see, because both were code referencing a database column the migrations never create:
| Version | Defect | How it presented |
|---|---|---|
| 0.1.7 | The Matter Portfolio report grouped by law_practice_areas.title; that table's label column is name. | The report crashed outright. |
| 0.1.8 | The Integration API read court; matters store court_name + court_branch. The demo seeder had the same typo. | Silent. The API returned an empty venue for every litigation matter, and the seeder's schema-drift filter quietly discarded the key. |
The second was found by executing every report, dashboard, widget and API query against a live
database — something the unit suite cannot do. That runtime harness is preserved (documentation-only)
under specs/harnesses/ with instructions for re-running it after any query change; its last
run was 58 checks, 0 failures.
The class is now closed statically by LawOfficeSchemaContractTest, which reads the module's
own migrations for the authoritative column list and asserts that every SQL column reference, every
demo-seeder write key and every API projection property actually exists. It was mutation-tested:
each of the three shipped defects was deliberately re-introduced and confirmed caught — the first attempt
missed the seeder case (the scanner only read inline arrays, while the seeder builds rows in a variable),
which is precisely why a new guard is not trusted until it has been made to fail.
The pre-sign-off security audit
Before release sign-off the module was audited across six dimensions (column drift, language keys, controller→view contracts, undefined global functions, visibility leakage, permission-gate coverage), with every candidate finding independently re-examined by a reviewer instructed to refute it. Result: 8 confirmed, 3 refuted.
The important find was a class no amount of query-execution testing could reach, because the queries were all correct — the flaw was in who was authorized. Five write endpoints (matter tasks, matter parties, docket events, deadlines, intake parties) checked permission against the parent id sent by the browser while saving to the row id sent by the browser. Since the data layer updates by primary key alone, a user with access to a single matter could have overwritten — and moved — records belonging to matters or confidential intakes they could not see, including restricted ones. Updates now resolve the parent from the stored record; matter-scoped rows are additionally pinned so they cannot be re-parented, while docket rows (where moving between matters is a legitimate action) require access to both ends.
Two disclosure defects were fixed alongside: the Parties registry's "Matters" count was not visibility-scoped (a party appearing only in a restricted matter showed a count while their matter tab showed none — the mismatch revealed the hidden matter), and the confidential-intake list reported the firm-wide intake total to users who could only see their own.
Three further candidates were correctly refuted on evidence rather than accepted, which matters as much: a NOT-NULL concern that cannot occur because strict mode is disabled platform-wide and the display path is guarded, and two count-disclosure claims for tables whose totals nothing in the interface ever renders.
Every defect found across versions 0.1.7–0.1.9 — eleven in total — was then deliberately re-introduced one at a time to confirm the test suite catches it. The first version of the authorization guard failed that exercise (four of five bugs slipped past, because its code parser desynchronized on an apostrophe inside a comment); it was rebuilt on PHP's own tokenizer and now self-tests its parser. A guard that has never been made to fail is not a guard.
Design precedents reused (not reinvented)
| Precedent | Reused for |
|---|---|
modules/Barangay KP deadline sweep + notification bridge | The reminder job shape and the module-owned bell/email delivery (zero core edits). |
modules/CityBPLO document pipeline | Upload/download/delete with the central general_files registry. |
modules/Community / OrganizationHub API conventions | Route fragments, scope sidecar, module-gate 404, projection shaping. |
Core DataTableTrait | Server-side listings and model-pushed API pagination. |
Full plan & status trail
The complete implementation plan (77 tasks, phases A–G) and the per-phase status log — including every
deviation and its rationale — live in modules/LawOffice/specs/law-office-implementation-plan.md
and law-office-status.md.