# LEDGER — Production: shared product search + line-grid column control

**Feature slug:** `production-grid-and-search`
**Started:** 2026-08-04 · **Install:** moonui2 · **Branch:** `hazemdev2` (both repos)
**Source plan:** `knowledge-base/plans/production-grid-and-search-unification.html`
(URL: `https://moonui2.elbaset.com/knowledge-base/plans/production-grid-and-search-unification.html`)

**Owner approval (verbatim):** «نفذ بكل توصياتك وخلص كل المراحل علي طول» — 2026-08-04.
That approves **every recommendation in §٩ of the analysis**, i.e.:
- Q1 → fix what is broken FIRST (Phase 0 before the column work).
- Q2 → BOM components take **TxColumnLayout over their own grid**, not the shared component.
- Q3 → column layout stays persisted **company-wide** (no per-user layer).
- Q4 → yes, the two shared-layer defects are fixed, and **before** the Production work.
- Q5 → production-order material grid stays **read-only** (not made editable).

**Owner's acceptance test:** open `…/app/factory/boms`, edit a BOM's components tab — the change
must actually persist after reopening; the product picker must return results for a production
user; and the components header must drag to reorder and drag to resize, with the layout
surviving a reload. Same drag/resize on MPS, consignment receipt, routing, CoA, and the
production-order material grid. R&D's product pickers must be the shared search.

---

## Decisions (settled — do NOT re-litigate)

1. **WP1 is fixed in the BACKEND, not by chaining three FE calls.** The FE already sends
   `components[]`/`operations[]` on `PUT /production/boms/{id}`; the server drops them. Making the
   FE call `PUT /boms/{id}/components` + `/operations` as well would be three non-atomic requests
   (header saved, components failed = corrupt state) and would fix only this one caller. One
   server-side fix covers the importer and every future caller. Standing rule: *fix in the shared
   layer, not per-screen.*
2. **Component/operation validation rules are extracted once and shared** between
   `UpdateBomRequest` and the existing `syncComponents`/`syncOperations`. Copy-pasting the rule
   array into a second place is a defect, not a shortcut.
3. **`show()` must load `components.substitutes.product`.** Without it, load→edit→save through the
   fixed path wipes every substitute, because the save replaces components wholesale.
4. **WP2 needs a forward BRIDGE MIGRATION, not only a seeder edit.** Role seeders are not re-run on
   existing installs, so a seeder-only change is dead code for every client already live.
5. **Column layout stays company-wide** (`core.document_settings`, `scope => company`). No per-user
   layer is built — it does not exist in the system and is out of scope.
6. **All Production DocConfig registry entries land in ONE WP (WP4).** `doc-config.service.ts` is a
   single shared file; four WPs editing it would conflict. WP5–WP8 only consume the keys.
7. **The production-order material grid stays read-only.** Making it editable would wake a save
   path that does not resolve ownership or apply waste — it would post customer-owned material to
   the factory's WIP. Column control only.
8. **`supply_source`, `owner_partner_id = 0`, issue-line `unit_id`, and costs are load-bearing.**
   No WP may default, coerce, or add an editor for them. See §٦ of the analysis.

## Serialization / DAG

**One writer per repo at a time.** BE and FE are different repos → one BE WP may run concurrently
with one FE WP, never two of the same repo.

```
BE lane:  WP1 ──► WP2 ──► WP9 ──► WP10 ──► WP11   (WP9/WP10/WP11 added mid-run from findings)
FE lane:  WP3 ──► WP4 ──► WP5 ──► WP6 ──► WP7 ──► WP8
                   │
                   └─ WP5..WP8 all depend on WP4 (registry keys + fixed resize)
```

## Baseline (pre-existing failures — do NOT blame yourself for these)

Recorded in `baseline.md` in this directory. Green = **zero NEW failures** vs that file.

## WP table

| WP | Scope | Repo | Depends | Migration | Status | Commits / result |
|----|-------|------|---------|-----------|--------|------------------|
| WP1 | `PUT /boms/{id}` persists `components[]`/`operations[]`; shared line rules; `show()` loads substitutes | BE | — | no | ✅ | `513cc5697` · 5F→6P red/green · 622P/10F scoped |
| WP2 | `core.products.view` granted to Production roles — seeder + bridge migration | BE | WP1 | **yes** | ✅ | `27519dbe6` · 403→200 red/green · migration ran on dev |
| WP9 | **[added mid-run]** BOM line rules company-scoped — cross-tenant `exists:` hole | BE | WP2 | no | ✅ | `be1f6b4ed` · 7F(all 200!)→8P · 640P/10F |
| WP10 | **[added mid-run]** Production roles get the LOOKUP permissions their screens call (units/warehouses/partners) | BE | WP9 | **yes** | ✅ | `36b54271e` · 403→200 ×3 · 654P/10F · migration read back from DB |
| WP11 | **[added mid-run]** `ProductionOrderController` — 4 unscoped FKs (tenant isolation) | BE | WP10 | no | ✅ | `c209bdebd` · 5F(all 200)→7P · 661P/10F |
| WP3 | R&D product pickers → shared `<app-product-search>` (kills full-catalog preload) | FE | — | — | ✅ | `c03c989d1` · build+tsc green · reviewed |
| WP4 | Shared layer: dialog-safe resize in `TransactionLineItemsComponent`; handles gated on the WRITE permission; persist failure surfaced + rolled back; **all Production doc keys registered** | FE | WP3 | — | ✅ | `1574011b8` · build+tsc green · reviewed |
| WP5 | BOM components grid → `TxColumnLayout` (`production.bom`) | FE | WP4 | — | ✅ | `a8c20cad5` · build+tsc green · reviewed |
| WP6 | MPS lines → column control (`production.mps`) — **`TxColumnLayout`, not the shared component** | FE | WP5 | — | ✅ | `433347abe` · build+tsc green · reviewed |
| WP7 | Consignment receipt lines → column control (`production.consignment`) — **`TxColumnLayout`** | FE | WP6 | — | ✅ | `20684e86b` · build+tsc green · reviewed |
| WP8 | Routing ops + CoA params + order material grid → `TxColumnLayout` | FE | WP7 | — | ✅ | `571188f1a` · build+tsc green · reviewed |

## Deferrals

**D1 — `TxCellType` has no `date` cell.** Discovered by WP6. The shared grid's cell types are
`product | select | number | percent | computed | action | text`. Any document line with a date
column therefore cannot use `TransactionLineItemsComponent` without adding a `@case ('date')` to a
template **12 live screens render**.

*Decision for this run: do NOT grow `TxCellType`.* The user-visible outcome is identical either way
— same drag, same resize, same company-wide persistence — and both affected Production screens (MPS,
consignment) already use the shared `<app-product-search>` for their product cell, so the full
conversion would buy the owner nothing he could see, in exchange for touching the component that 12
working screens render. Both took `TxColumnLayout` instead.

*Recommendation if this comes back:* adding `date` is purely additive (no existing screen uses the
string `'date'`, so no existing behaviour changes) and it unblocks the whole class of date-bearing
document. Worth doing as its own small, separately-reviewed change — not smuggled inside a
Production WP. **Owner decision, not mine to take silently.**

---

**D3 — `lines.*.product_variant_id` in `issueMaterials()` has NO `exists` rule at all.** Found by
WP11's sweep. On a raw product line with no order-material row, `IssueMaterials::resolveLines()`
takes the request value verbatim onto `inventory_issue_items.product_variant_id` — a genuine,
narrower cross-tenant reference on the same endpoint WP11 just hardened. Left open **because it
needs a decision, not a pattern**: validate-and-scope it, or force the variant to match the resolved
product. Applying WP11's fix blindly would pick one of those silently. **Owner's call.**

---

**D2 — Consignment receipt: the Save button stays disabled while you fill in lines.** Found by WP7
in passing; **independently confirmed by the orchestrator**, not taken on the agent's word:
`canSubmitReceipt` is a `computed()` over `receiptLines()` (consignment.component.ts:288-291), but
the six line cells bind `[(ngModel)]="line.<field>"`, which mutates the row object **in place** and
never re-`set`s the signal. The signal's reference is unchanged, so the computed never
re-evaluates. An operator who picks customer + warehouse first and fills the lines last is left
with a **disabled "Save receipt" button** until they happen to add or delete a line.

*Entirely pre-existing.* This run neither caused it nor widened it — which is precisely why it was
NOT fixed here, unlike WP9. The rule applied consistently across this run: **fix what we made worse,
report what we merely found.**

*Recommended fix, if the owner wants it:* convert the six cells from `[(ngModel)]="line.x"` to the
`[ngModel]` + `patchLine()` shape — the exact pattern MPS already uses, landed and proven in
`433347abe` on this same branch. Mechanical, one file, low risk. **Owner's call.**

## Log

- 2026-08-04 — Phase A: ledger + all 8 briefs written; `pest Modules/Production` baseline capture started.
- 2026-08-04 13:37 — WP1 (BE) and WP3 (FE) dispatched in parallel — different repos, so no shared writer.
- 2026-08-04 — **Verified in code before dispatch** (so no implementer re-derives it):
  `UpdateBomRequest` declares neither `components` nor `operations`; `BomController::update()` at
  line 128 calls `$bom->update($request->validated())`; `bom.service.ts` has **no** sync method;
  `BomController::show()` loads `components.product`/`components.unit` but **not**
  `components.substitutes`; `ProductionRoleSeeder` is **absent** from `moonstack.updater.seeders`
  (so a seeder-only permission grant reaches fresh installs only → bridge migration is mandatory);
  `TransactionLineItemsComponent` line ~402 resolves the `<th>` via
  `elRef.nativeElement.querySelector('.tx-lines-table')` (null inside a dialog) while
  `TxColumnLayout` already uses `closest('th')`; `DocConfigService.persist()` line ~414 swallows the
  error with `.subscribe({ error: () => {} })`; `PermissionService.hasPermission()` is a **prefix**
  match, so `core.settings.view` passes the `canReorder` gate on both mechanisms.

- 2026-08-04 13:56 — **WP3 ✅** `c03c989d1`. Both R&D pickers are `<app-product-search>`; the
  full-catalogue `listAll()` preload is gone. Build + `tsc --noEmit` green. Reviewed by the
  orchestrator against the brief: value shape stays `number | null` (`onChange` emits
  `selected.id`), so `product_id` still leaves as an id in `RndProjectRequest` and
  `RndAmortScheduleRequest`. The agent's one open concern — R&D binds with `[ngModel]` rather than
  `formControlName`, a first for this component — was checked and cleared: `writeValue()`
  (product-search.component.ts:421) resolves the id through `productService.getById()` and sets the
  display label, so editing an existing project shows the product name, not an empty box. The
  sibling `p-select`s in the same dialogs also carry no `name` attribute, so there is no parent
  `<form>` for ngModel to complain about. No i18n keys added.
  *Note:* `writeValue`'s `getById` is gated by `core.products.view` — the same permission WP2 grants.
  Until WP2 lands, this picker is empty for production roles, exactly as before.

- 2026-08-04 14:12 — **WP4 ✅** `1574011b8`. Build + `tsc --noEmit` green.
  Reviewed against the brief by the orchestrator:
  * Dialog resize now resolves the `<th>` via `closest('th')` — the fix `TxColumnLayout` already had.
  * Both mechanisms now gate on the exact-match `can('core.settings.manage')`, not the prefix.
  * `DocConfigService.persist()` takes a `previous` snapshot from **all four** mutators, restores it
    on error and raises a toast — verified in the diff, not taken on trust.
  * The 12 pre-existing registry entries are byte-identical (old array body is an exact string
    prefix of the new one).
  **Accepted deviation (outside the three named files):** a root `MessageService` in `app.config.ts`
  plus one app-level `<p-toast />` in `App`. PrimeNG's `MessageService` has no `providedIn`, and
  every screen provides its own — so a ROOT service had no channel to the user at all, which is
  almost certainly *why* the error was swallowed in the first place. Component-level providers still
  shadow the root one, so no existing screen changes and no double toasts (each `<p-toast>` binds to
  the instance it injects). Judged necessary rather than scope creep.
  **Registry corrections the agent made from the code (code wins over my brief's table):** 10 of 12
  `production.routing` keys (`sequence`→`operation_no`, `production_center_id`→`work_center_id`,
  `setup_time`→`setup_time_minutes`, `run_time`→`run_time_per_unit`, `cavities`→`cavity_count`,
  `cycle_time`→`cycle_time_seconds`, `wait_time`→`queue_time_minutes`, `move_time`→`move_time_minutes`,
  `is_inspection`→`inspection_required`, `is_critical`→`critical_operation`) and 2
  `production.consignment` keys (`declared_cost`→`declared_unit_cost`,
  `production_date`→`manufacturing_date`). **Target correction:** `production.order_issue` is the
  **Issue Materials dialog** grid, not the read-only Materials tab the analysis named. Both
  corrections were propagated into the WP5 and WP8 briefs as verbatim key lists.
  **Open, tracked to the end of the run:** all 7 Production docs were registered `wired: true`, so
  the Documents settings screen shows them as fully enabled before WP5–WP8 wire the grids. Every one
  of the 7 is wired by WP5/WP6/WP7/WP8 — **re-verify at Phase C** that none is left claiming to be
  wired when it is not. i18n: 8 keys added per file, 0 removed, 0 changed.

- 2026-08-04 14:31 — **WP5 ✅** `a8c20cad5` — the screen the owner named. Build + `tsc` green.
  Both BOM tabs (components + operations) now carry `TxColumnLayout`. Reviewed:
  * `git show --numstat` on `boms.component.ts` reports **45 insertions, 0 deletions** — the save
    path (`onSave`/`addComponent`/`addOperation`) is provably untouched, so "the payload is
    identical" is demonstrated, not asserted. The save reads the FormArray, never the DOM.
  * The substitutes expander's `colspan` is now derived from the visible column count, so hiding a
    column cannot skew the expander row — a failure mode neither my analysis nor the brief caught.
  * Hidden columns keep their `FormControl` values (Angular's `_cleanUpControl` does not reset), so
    a hidden `supply_source` cannot blank into `'own'`. `supply_source` logic untouched.
  * Registry keys matched the row model exactly — no registry edit needed, confirming WP4's
    verification pass.
  Agent concerns, adjudicated: (a) the operations grid's pre-existing `min-width: 1080px` exceeds
  its default column sum (~880px), so a saved width there is honoured proportionally rather than to
  the pixel until the columns exceed 1080 — **cosmetic, ops tab only; the components grid sums to
  1332px so its widths are exact**; accepted, recorded here rather than fixed, since changing a
  pre-existing min-width is outside this WP. (b) ops-tab inputs now fill their column (CSS only,
  markup untouched) — intentional, mirrors the reference grids. (c) **No CHANGELOG entry yet** — the
  CHANGELOG lives in the BE repo (`docs/moonstack/CHANGELOG.md`); FE WPs cannot write it. Phase C
  must add the capability bullets there for the whole feature. Neither i18n file was touched.

- 2026-08-04 14:45 — **WP6 ✅** `433347abe`. Build + `tsc` green. **Deviation, and it was the right
  call:** the agent took the brief's documented escape hatch on the first blocker and used
  `TxColumnLayout` rather than converting to `TransactionLineItemsComponent`. Reason: MPS's `period`
  is a native `<input type="date">` on a **locked, always-visible** column, and `TxCellType` has no
  date case — rendering it as `text` would drop the picker and let free text reach `period` in the
  payload. See Deferral D1. All 6 acceptance criteria still met; the signals→FormArray cost the
  brief priced turned out not to be the binding constraint at all.
  Payload equivalence proven twice over: `interface LineRow` + `addLine`/`removeLine`/`patchLine` +
  **`save()` in full** + `blankLine`/`lineRowsFrom` extracted from HEAD and from the working tree —
  106 lines each, **identical sha256** `30eeccb5…`; plus a control-name comparison of the template's
  binding surface (same 4 `[ngModel]` reads, same 4 `patchLine` writes, `type="date"` 3→3,
  `[disabled]="isReadOnly()"` 7→7). All 18 references to the lines signal live in the two MPS files —
  no external reader. Registry needed no correction.
  Concerns: not browser-verified (no deploy); below 760px the grid scrolls horizontally instead of
  reflowing — same trade the BOM grids made; no CHANGELOG entry (Phase C owns that).
- 2026-08-04 14:47 — **WP7 brief rewritten before dispatch.** Consignment carries TWO date columns
  (`expiry_date`, `manufacturing_date` — confirmed at `consignment.component.html:381` and `:390`),
  so it would hit D1 identically. Rather than let it burn a discovery cycle and re-take the same
  decision, the brief now mandates `TxColumnLayout` up front and points at `433347abe` (MPS,
  `ngModel` signal rows) as the closest reference. The "if it doesn't fit" escape hatch was replaced
  with a hard prohibition on touching the shared component.

- 2026-08-04 15:03 — **WP1 ✅** `513cc5697` — the defect the owner's own screen was suffering from.
  **Red-before-green delivered: 5 failed / 1 passed (15 assertions) → 6 passed (36 assertions).**
  Scoped `pest Modules/Production`: **622 passed / 10 failed**, all 10 pre-existing and unrelated
  (ConsignmentFoundation ×1, CostAiAnomalyVariance ×3, ProductionVariance ×6 — the agent re-verified
  those 6 fail identically with its edits stashed, rather than assuming). **This is the new baseline
  for every later BE WP.**
  Line rules are now shared by `UpdateBomRequest` + both sync methods via
  `Http\Requests\Concerns\BomLineRules` — one source, as Decision 2 required. `show()` now
  eager-loads the substitutes, closing the round trip. **No FE change was needed**: the agent
  verified `boms.component.ts` already sends a payload that validates and already reads
  `data.substitutes` back.
- 2026-08-04 15:05 — **WP9 added to the plan** (was not in the analysis). WP1 reported, and the
  orchestrator independently confirmed in the code, that `BomLineRules` leaves three foreign keys
  **unscoped by company**: `components.*.product_id` (line 31), `components.*.unit_id` (line 34),
  `operations.*.production_center_id` (line 63) — so a caller can attach another tenant's product,
  unit or production centre to their own BOM.
  Two facts make it a defect rather than a judgement call: the **substitute** product on line 50 IS
  company-scoped, and `StoreBomRequest` (create) scopes all of them (`:45,:48`). Creating a BOM is
  protected; editing one is not — inside the same feature.
  It is pre-existing (always reachable via `PUT /boms/{bom}/components`) but WP1 **widened its
  reach** to the ordinary Save button, and WP1 correctly left it alone because its own criterion 6
  required sync behaviour stay byte-identical. That constraint does not bind WP9.
  Not deferred: this is a tenant-isolation hole in a path this run just made more reachable.

- 2026-08-04 15:19 — **WP7 ✅** `20684e86b`. Build + `tsc` green. `TxColumnLayout` as the rewritten
  brief mandated — no discovery cycle wasted re-deriving the D1 decision.
  Payload equivalence proven two ways: a 123-line brace-balanced extract of the full state+payload
  path hashes identically before/after (`f327f9a4…`), plus a 1:1 control-name comparison across all
  six bindings. Registry `production.consignment` needed no correction — all 7 keys matched the real
  `ReceiptLineForm`. The `owner_partner_id = 0` sentinel guard turned out to be moot here: the field
  does not exist anywhere on this screen, its model, or its service — the agent checked rather than
  assumed, which is the right answer to a guard that does not apply.
  Concerns: not browser-verified; the two `p-datepicker` overlays now sit in a `<td>` with
  `overflow: hidden` but use `appendTo="body"` so should be immune (worth one look after deploy);
  narrow screens scroll horizontally instead of squashing. **Plus the pre-existing defect recorded
  as Deferral D2.** Committed with `SKIP_CHANGELOG=1`, as the sibling FE commits did — Phase C owns
  the bilingual entry.

- 2026-08-04 15:36 — **WP8 ✅** `571188f1a`. Build + `tsc` green. **All six FE packages are done.**
  The hard constraint held: the Issue Materials dialog's editability is unchanged, and the agent
  proved it three ways rather than asserting it — a control census over the whole 47 KB
  `production-orders.component.html` (identical HEAD vs working tree, so a new control ANYWHERE
  would have shown), the same census over the dialog region alone (identical down to the
  `[disabled]="saving()"` count, 8/8), and `buildIssueLines`/`submitIssueMaterials` byte-identical.
  `unit_id` is still plain text — no unit picker appeared.
  The `owner_partner_id = 0` sentinel guard was actually *checked*, not assumed: PrimeNG's
  `Select.clear()` calls `updateModel(null, …)`, so a blank emits `null`, and `submitIssueMaterials`
  skips on `!= null`. `0` is unreachable. Hiding the column is stricter still — the picker never
  renders, so the row keeps `buildIssueLines`' `null`.
  Payload equivalence per screen by the now-established method: routing `95cca2b9…`, CoA
  `36bd7e57…`, orders `32f1d63d…` — each extracted brace-balanced from HEAD and the working tree and
  hashed, all three identical. All 26 registry keys re-verified against `OperationRow` /
  `newParameterGroup()` / `IssueLineRow` — zero corrections needed, which closes the loop on WP4's
  verification pass (the routing keys WP4 corrected were right).
  Concerns: nothing browser-verified (routing's 12 fixed-width columns and the order-issue batch
  expander are the two spots worth a click-through after deploy); CoA and order-issue scroll
  horizontally instead of squashing on narrow screens. **CHANGELOG is now due — Phase C.**

- 2026-08-04 16:00 — **WP2 ✅** `27519dbe6`. **Red-before-green: `production_supervisor` on
  `GET /api/core/products` 403 → 200.** New file 10 passed. Scoped: Production **632 P / 10 F**
  (baseline 622/10 + the 10 new tests → zero new failures); Core **719 P / 10 F** — no Core baseline
  existed, so the agent proved all 10 pre-existing by stashing the change and re-running the 8
  affected files, including the three *role* tests where a false negative would have mattered most.
  Granted to 10 roles, view only. `warehouse_storekeeper` turned out **not** to be the judgement call
  my brief assumed: it holds `inventory.receipts.create`/`issues.create`, and both stock-receipts and
  stock-issues mount the shared picker — without it a storekeeper cannot add a line at all.
  `machine_operator` correctly excluded (terminal-only; an existing test pins it at 3 permissions).
  Bridge `2026_08_04_100000_production_roles_products_view_bridge` ran on `moonui2_dev_be` via
  `migrate --force`; read back from the DB: 10 roles `view=YES`, `writes=0`; re-running twice left
  counts unchanged.
- 2026-08-04 16:02 — **WP10 added to the plan.** WP2's report flagged a risk — "if a factory screen
  resolves units/categories via a separate gated endpoint that's a distinct 403 this WP doesn't
  cover". **The orchestrator checked instead of filing it, and it is real and worse than products
  was.** Querying `moonui2_dev_be`: **zero** of the 8 Production roles hold **any** of
  `core.units.view`, `inventory.warehouses.view`, `core.partners.view`, `core.branches.view`,
  `accounting.tax-rates.view`, `core.users.view`, `core.product-categories.view`,
  `core.unit-groups.view`.
  **`core.units.view` is blocking**: `boms.component.ts:241` calls `unitService.listAll()` →
  `GET /core/units`, and `unit_id` is `Validators.required` on the BOM header **and every component
  line** (`:233`, `:344`, `:386`). For a production user the units dropdown is empty and **the BOM
  cannot be saved at all** — precisely what WP1 + WP5 just made work for everyone else. Shipping
  WP2 alone would have been a half-fix that still 403s.
  Screen→service map (derived, not guessed): units → boms/orders/toll · warehouses →
  consignment/orders/toll/cost-reports · partners → consignment/orders/toll · branches →
  factory-dashboard · tax-rates → orders · users → variances.
  WP10 grants the **first three** (the ones this feature's own screens need to function) and
  **deliberately holds back** branches / tax-rates / users — they belong to screens outside this
  feature and cross into org-structure, Accounting and user-directory surfaces. Those are reported
  for the owner, not granted silently. **This is an authorization widening and must be called out
  prominently in the final report.**

- 2026-08-04 16:33 — **WP9 ✅** `be1f6b4ed`. **Red-before-green, and the red run is the headline:
  7 failed / 1 passed — every cross-tenant write returned 200.** After: 8 passed / 37 assertions.
  `ProductionBomUpdateLinesTest` still 6 P / 36 A. `pest Modules/Production` **640 P / 10 F** vs the
  632/10 baseline — +8 is exactly the new file, zero new failures (the agent re-confirmed the 10 by
  running those three files alone → 10 failed, rather than inferring).
- 2026-08-04 16:35 — **WP11 added to the plan.** WP9's sweep found four MORE unscoped `exists:`
  rules, all in `ProductionOrderController`: `:695` `warehouse_id` and `:703` `lines.*.product_id`
  and `:731` `lines.*.unit_id` in `issueMaterials()`, and `:1006` `warehouse_id` in receive-FG.
  **Confirmed by the orchestrator by reading the file, not on the agent's word.**
  `:695` is the one that matters most, and not for severity — for the comment above it. Line 690
  reads *"below so a line can never reference another company's warehouse or business partner"*,
  `$companyId` is resolved on `:691` and applied to the per-line rules — and **not** to this
  top-level `warehouse_id`. The file documents a guarantee it does not provide, so anyone auditing
  the invariant would conclude it holds. (WP9 notes ISS-2026-9166/WP4 missed it.)
  **Why this is fixed rather than filed**, given D2 was filed: this run's rule is *fix what we made
  worse, report what we merely found* — and **tenant isolation is the stated exception**. These are
  cross-tenant WRITE paths, the fix pattern is already landed and proven in `be1f6b4ed`, and one of
  them is guarded by a comment that is actively false. WP11's criterion 4 requires the comment be
  made true, not just the code.

- 2026-08-04 17:20 — **WP10 ✅** `36b54271e`. **Red-before-green ×3:** as `production_supervisor`,
  `GET /api/core/units`, `/api/inventory/warehouses`, `/api/core/partners` all 403 → all 200 — and
  the agent kept WP2's products test passing alongside in the red run, which proves the harness
  rather than the absence of a harness. New file 9F/5P → **14 passed**. Production 640→**654 P /
  10 F** (+14 = exactly the new tests); Core **719 P / 10 F, identical**.
  The per-permission role derivation the brief demanded **collapsed to the same 10 roles** WP2 used,
  and the agent explained why rather than just asserting it: every seeded Production role except
  `machine_operator` holds `boms.view`, `orders.view` or `toll_contracts.view`, and `/factory/orders`
  alone calls all three lookups. `machine_operator` still exactly 3 permissions, its pinning test
  green. View only — no create/update/delete, and no partner freeze/unfreeze.
  Bridge `2026_08_04_110000_production_roles_lookup_views_bridge` ran on `moonui2_dev_be`
  (`migrate --force`, batch 265). **Read back from the DB, not the seeder:** each of the 10 roles
  +3 exactly (supervisor 21→24, quality_inspector 6→9), `machine_operator` unchanged, no write keys,
  no held-back keys. Idempotency proven on the real DB: `role_has_permissions` 3056 → 3056 after
  re-running the grant twice.
  Held back and now **pinned by a test** so a future bulk grant has to be deliberate:
  `core.branches.view`, `accounting.tax-rates.view`, `core.users.view`.
  **Accepted 2-line edit to WP2's test file:** its "a client's custom extra permission survives the
  additive grant" test used `core.partners.view` as the stand-in — which WP10 now seeds, making the
  assertion vacuous. Swapped for a synthetic key no seeder grants. WP2's behaviour under test is
  unchanged and still passes. Correct catch; a silently-vacuous regression test is worse than none.
  Also verified: **no** Production FE screen references `product-categories`, so that permission was
  rightly out of scope.

- 2026-08-04 17:55 — **WP11 ✅** `c209bdebd`. **Red run: 5 failed / 2 passed — all four cross-tenant
  references returned 200.** After: 7 passed / 25 assertions. `pest Modules/Production` **661 P /
  10 F** = 654 + 7, same 10 pre-existing failures by name. Sweep complete: `grep "exists:"` over
  `Modules/Production/app/Http/Controllers/` now returns **zero** unscoped hits; the three remaining
  `Rule::exists` are correctly scoped through a company-owned parent.
  **The judgement call worth recording:** criterion 4 asked the agent to verify the line-690 comment
  becomes true rather than assume it. It **enumerated every key in the validate array and traced
  each one**, found that it does NOT become literally true — `product_variant_id` has no `exists`
  rule at all — and therefore **refused to restore the sweeping wording**, writing a comment that
  states what each key is scoped by and names `product_variant_id` as the explicit exception. That
  is exactly right: restoring a comforting comment would have recreated the original defect, which
  was a false guarantee, not a missing one. New finding recorded as Deferral D3.

---

## Phase C — closeout (2026-08-04 18:10)

- **All 11 WPs ✅.** Three (WP9, WP10, WP11) were added mid-run from findings, none from scope creep:
  each was either a hole this work widened or a tenant-isolation defect.
- **WP4's tracked concern is resolved:** all 7 Production doc keys registered `wired: true` are now
  consumed by a feature file (`production.bom`, `bom_operation`, `mps`, `consignment`, `routing`,
  `coa` ×3 files, `order_issue`) — verified by grep, so nothing claims to be wired that is not.
- **CHANGELOG** `718468f09`: the six FE packages landed in the Angular repo, which has no CHANGELOG,
  so their user-facing capabilities had no release note. Added three bilingual bullets (column
  control on the factory grids · dialog resize + the permission/rollback fix · R&D search).
  Verified: 28 `[Unreleased]` bullets, **0** missing `{{ar}}`, 96 released sections intact.
- **Built and deployed to `/app`** — bundle `main-SUX2FPMR.js`, `apiUrl` confirmed still the moonui2
  dev API, `chown -R moonui2:moonui2`. Per the standing rule that the owner tests on live `/app`.
- **KB updated:** `topics/production.md` (`updated: 2026-08-04`, full section + Decisions + Open) and
  a new `INDEX.md` entry. `RESUME.md` written as the end-state record.
- **Nothing pushed, nothing merged.** `/fullpush` is the owner's step.
- **Nothing browser-verified** — every acceptance criterion was met by code path, test, or build.
  The owner's pass on `/app` is the remaining gate.
