File size: 46,650 Bytes
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcdb685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
dcdb685
c14ceee
e1b3e71
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
91de770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1b3e71
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1b3e71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91de770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcdb685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1b3e71
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
dcdb685
 
 
 
 
c14ceee
 
 
dcdb685
 
c14ceee
 
 
91de770
 
 
 
c14ceee
e1b3e71
dcdb685
 
 
 
 
 
c14ceee
 
dcdb685
 
c14ceee
 
 
dcdb685
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
e1b3e71
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
 
 
 
91de770
c14ceee
91de770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
dcdb685
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcdb685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
dcdb685
 
 
 
 
c14ceee
dcdb685
 
 
c14ceee
dcdb685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1b3e71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
"""modules/product_data.py β€” the PRODUCT table's pool (wave 15 item 9/10, contract C-TOPIC).

The second object on the table-page factory: same grid, same engine, same permission wall β€” the
only difference is the field schema and the identity. `modules/customer_data.pool()` is the
template and this deliberately mirrors its signature and its row shape.

β›” THREE DECISIONS THIS FILE HAD TO MAKE, EACH RECORDED BECAUSE A LATER READER WILL WONDER.

1. **THE IDENTITY IS A SKU CODE, WHICH IS A STRING, AND THE GRID WANTS AN INTEGER `pid`.**
   Cohort membership, `allowed_pids`, the measure channel and `rows_from_pool` all key on an
   integer. So each row carries BOTH: `pid` (a stable CRC32 of the code, so the same SKU gets the
   same id on every pull and across processes β€” never an enumeration index, which would reshuffle
   whenever the catalogue changes) and `code`, the real business key, as a visible column.
   `_assert_no_pid_collision` fails the BUILD rather than the read: two SKUs sharing a pid would
   silently merge in every downstream set operation, and a loud build failure is the only version
   of that anyone would notice.

2. **THE SCOPE RULE β€” inventory columns are CONSOLIDATED and are therefore OMITTED for a
   BU-scoped caller.** `products.directory(t, team_id)` is brand-shaped; `inventory.sku_inventory`
   is explicitly NOT (its own docstring: "on-hand stock is one physical warehouse, not
   brand-tagged"). Joining them for a Fisch-only user would put BU-shaped revenue beside
   company-wide stock IN THE SAME ROW β€” the mixed-scope value defect wave 15's amendment 3 exists
   for, arriving through a different door. The honest options were "omit" or "label the columns
   company-wide"; omit is the fail-closed one, and a column that is absent asks a question,
   whereas a column that is silently company-wide answers one wrongly.

3. **WHAT IS NOT HERE, NAMED RATHER THAN QUIETLY MISSING.** R4 lists ~20 fields. Vendor and
   COUNTRY are not in Odoo at all β€” they live in the inventory WORKBOOK
   ([[odoo-vendor-country-origin]]) and need a loader this module deliberately does not invent.
   Margin % comes from `modules/pricing.table`, which is a heavier build (channel-rate cost
   allocation) and is left for the wave that needs it. `validate()` reconciles what SHIPS; it
   does not pretend to cover columns that are absent.

4. **THE POOL IS CATALOGUE-FIRST, WITH REVENUE LEFT-JOINED** (wave 29, owner item 22 / R12,
   2026-08-11). It was `for r in products.directory(...)` β€” and `directory()`'s row set IS the
   union of two revenue read-groups, so **a SKU that never sold could not exist** and the grid
   showed **2,717** of **5,875** active products. Three things that will be re-derived otherwise:

     Β· β›” THE CAUSE IS A JOIN, NOT A LIMIT. No row cap exists on this path. Removing the date
       window from `directory()` would ALSO be wrong twice: it breaks four SKU-health metrics
       that legitimately want a sales window, and it lands at 3,327 (all-time-sold), because
       ~2,550 active SKUs have never sold in wholesale scope at all.
     · ⭐ REVENUE ON A NEVER-SOLD ROW IS **BLANK, NOT $0** (R12). A row that appears in the
       revenue universe carries measured numbers INCLUDING a real 0.0 β€” it sold last year and
       not this one, and that zero is a measurement. A row that appears in NO revenue read
       carries `None`, which the wire keeps (`aios_grid._round` passes None through) and the
       client renders as an empty cell. Blank is an admission; zero is a measurement.
     · ⚠ A BU-SCOPED CALLER NOW SEES THE WHOLE CATALOGUE, with ITS OWN revenue and blanks where
       that BU never sold. That is not decision 2's mixed-scope defect: the catalogue is the ROW
       UNIVERSE, not a company-wide VALUE sitting beside a BU-shaped one. `product.product`
       carries no team, so a catalogue cannot be BU-shaped at all β€” which is exactly why the two
       sources are JOINED rather than merged.
"""
import json
import zlib
from pathlib import Path

import core.odoo as O
import core.periods as P
import core.shared_overlay as shared_overlay
import core.table_store as table_store
import modules.products as products

#: POOL-ROW KEYS that exist ONLY on a consolidated pull. See decision 2.
#: `cover_gap_d` / `buy_now` (wave 17) join it because both are computed FROM `dos`, and a buy
#: signal built on a stock number the caller cannot see would be a recommendation nobody could
#: check ([[no-unverifiable-aggregates]]).
#:
#: ⚠ ROW KEYS, NOT FIELDS, and the distinction started mattering on 2026-08-03. Two readers:
#: `verify_perm_scope` asserts the shape of a POOL ROW against this, and `routes_products`
#: narrows the FIELD list with it. Since owner item 5 `buy_now` is a row key with no Field β€”
#: computed as `validate()`'s oracle, projected onto no wire β€” so it belongs here for the first
#: reader and is inert for the second. Removing it would stop the scope gate proving that a
#: BU-scoped pull withholds it.
#:
#: ⭐⭐ OWNER RULING 2026-08-11 β€” THIS SET IS NOW EMPTY, AND THAT IS THE POINT. Verbatim:
#: *"Just scope any inventory with Sales from Fisch, leave the rest."* Withholding the whole
#: inventory block from a BU-scoped reader meant a Fisch salesperson could not see whether
#: anything was IN STOCK, which is the first question they ask. The ruling splits the block by
#: what a business unit can actually shape:
#:
#:   * `on_hand` / `unit_cost` / `inv_value` β€” ONE physical warehouse, no Fisch shelf and no
#:     Royal shelf. Served UNSCOPED to everybody, identical in both pulls. ("leave the rest")
#:   * `qty_ltm` / `dos` / `stock_bucket` / `cover_gap_d` / `buy_now` β€” all SALES-derived, so a
#:     BU-scoped pull recomputes them from THAT unit's LTM units. ("scope any inventory with
#:     Sales from Fisch")
#:
#: This deliberately relaxes decision 2's "never a company-wide value beside a BU-shaped one":
#: stock is not a value a BU can own, and a column that is absent for a Fisch reader asks a
#: question they cannot answer anywhere else in the product. `verify_perm_scope` no longer
#: asserts the columns are WITHHELD β€” it asserts the split above, which is a stronger claim and
#: cannot pass on an empty scoped payload (the shape the old rule and an outage share).
CONSOLIDATED_ONLY = ()

#: β›” POOL-ROW KEYS THAT DELIBERATELY HAVE NO FIELD β€” they must never reach a browser.
#:
#: `rows_from_pool` projects strictly through the field contract, so "no Field" already means
#: "no cell on the wire". This tuple makes that a CHECKED fact rather than a consequence nobody
#: is watching: `verify_perm_scope` asserts every member is absent from BOTH product contracts,
#: and that every OTHER `CONSOLIDATED_ONLY` key is present in the consolidated one.
#:
#: Written because the two tuples silently diverged the moment owner item 5 retired `buy_now`'s
#: Field while keeping its computation, and the gate β€” which was asserting a FIELD property from
#: a ROW-key list β€” went red with no way to tell a deliberate divergence from a dropped column.
UNSHIPPED_ROW_KEYS = ("buy_now",)

#: Wave 17 (owner item 13, ruling R3) β€” the SUPPLIER MASTER, from the curated mastersheet map
#: `procurement_suppliers.json` (3,963 SKUs; supplier on 3,624, lead time on 3,563). NOT Odoo:
#: the owner confirmed this data came from the Inventory System Mastersheet, which is why
#: `modules/product_data`'s header said Vendor/Country were "NOT HERE, named rather than
#: quietly missing" and needed "a loader this module deliberately does not invent". This is
#: that loader.
#:
#: β›” THESE ARE CONTRACT COLUMNS, NOT USER-CREATED FIELDS, AND THE REASON IS A PERMISSION FACT.
#: A user-created field and its values live in the PER-USER strata (`table_store.workspace`
#: reads `store.get(key)[username]`); only VIEWS are shared. So a shared "Buy list" view that
#: filtered on a user-created column would, for every OTHER account, name a column that does not
#: exist β€” and an unknown column is an INACTIVE condition in the tri-state engine, which IGNORES
#: it and therefore WIDENS. The buy list would silently show the whole catalogue to everyone but
#: its author. Contract columns are identical for every reader, so the view means one thing.
#:
#: ⭐⭐ 2026-08-12 (W30-T36) β€” THE OWNER'S ASK IS NOW DELIVERED, AND THE PARAGRAPH ABOVE IS WHY IT
#: TOOK THREE WAVES. Owner: *"turn this Excel sheet into a User created Field that we can edit."*
#: It was parked because a per-user column silently WIDENS a shared view β€” not because editing was
#: hard. Wave 29's `core/shared_overlay.py` (whose header quotes this very comment) is the stratum
#: that removes the objection: **one value per (row, column) for the whole tenant**, so the column
#: still means ONE thing to every reader and a shared view still filters honestly.
#: β‡’ The four columns below are now `source: "overlay"` + `shared: true` in the canonical
#: contract, their values live in `<TABLE_KEY>__shared`, and the master map is what SEEDS an
#: unedited cell rather than what freezes it. See `SHARED_KEYS` and `_ProductTableStore`.
_SUPPLIER_MAP_PATH = Path(__file__).resolve().parent.parent / "procurement_suppliers.json"
_SUPPLIER_CACHE = {}


def supplier_master():
    """`{code: {supplier, lead_days, origin_country, first_cost}}`, read once per process.

    Degrades to `{}` when the file is unreadable, matching `_inventory_by_code`: a product table
    that will not render because a master map is missing is a worse failure than one with blank
    supplier columns.
    """
    if _SUPPLIER_CACHE:
        return _SUPPLIER_CACHE
    try:
        raw = json.loads(_SUPPLIER_MAP_PATH.read_text(encoding="utf-8"))
    except Exception:
        return {}
    for code, meta in (raw or {}).items():
        if not isinstance(meta, dict):
            continue
        lead = meta.get("lead")
        _SUPPLIER_CACHE[str(code)] = {
            "supplier": (meta.get("vendor") or "") or None,
            "lead_days": int(lead) if isinstance(lead, (int, float)) else None,
            "origin_country": (meta.get("country") or "") or None,
            "first_cost": meta.get("first_cost"),
        }
    return _SUPPLIER_CACHE

#: Wave 16 C-TOPIC β€” the PRODUCT table's OWN workspace bucket. β›” Never the customer one:
#: product pids are CRC32 hashes of SKU codes and customer pids are Odoo partner ids, so in a
#: SHARED overlay bucket a hash collision would silently write a product note onto somebody's
#: customer (or the reverse). Separate store keys make that structurally impossible, which is
#: the whole reason the table-page factory exists ("a new table object gets its own
#: table_store.make('<its>_table_workspace')").
TABLE_KEY = 'product_table_workspace'

_GRID_FIELDS_PATH = Path(__file__).resolve().parent.parent / 'aios_grid_fields.json'
_SHARED_KEYS = None


def SHARED_KEYS():
    """The product columns whose values are TENANT-WIDE β€” derived from the canonical contract's
    own `shared: true`, never typed out a second time.

    β›” IT DELIBERATELY DOES NOT SWALLOW A READ FAILURE. Degrading to `()` would send a shared
    write into the per-user stratum with nothing going wrong anywhere β€” the widening defect
    reappearing silently, which is the one outcome this whole mechanism exists to prevent. If the
    canonical contract is unreadable the product grid cannot render at all (`pd_fields` parses the
    same file with no guard), so a raise here costs nothing that was still working.
    """
    global _SHARED_KEYS
    if _SHARED_KEYS is None:
        doc = json.loads(_GRID_FIELDS_PATH.read_text(encoding='utf-8'))
        _SHARED_KEYS = tuple(f['key'] for f in (doc.get('product_data') or {}).get('fields') or []
                             if f.get('shared'))
    return _SHARED_KEYS


class _ProductTableStore(table_store.TableStore):
    """The product workspace, with the SHARED columns routed to the tenant-wide stratum.

    ⭐⭐ THIS SUBCLASS IS THE WHOLE OF W30-T36's WRITE PATH, AND THE REASON IT LIVES HERE RATHER
    THAN AT A ROUTE IS THAT **THE BROWSER NEVER CALLS `PATCH /products/{pid}`** β€” measured, zero
    call sites in `aios-web/web/src`. A cell edit travels `POST /grid/events` β†’ `grid_events.
    handle_one` β†’ `_tops(ctx).patch_overlay(...)`, and `_tops` returns `ctx.table`, which
    `routes_grid._ctx` sets to `TABLE_OPS` for the product scope. So this object IS the seam both
    doors pass through; intercepting at either route would have left the other one writing a
    per-user value that only its author could see.

    ⚠ `st=self.st`, NEVER the module default. The shared stratum must resolve to the SAME store
    handle as the per-user one it sits beside β€” `_tops`' own comment explains that a split, where
    one side is tenant-scoped and the other is not, is worse than a stated residency error because
    a user's value would vanish the moment they saved it. Reading `self.st` means both strata move
    together the day that singleton gains a tenant handle.
    """

    def patch_overlay(self, username, pid, updates):
        clean = dict(updates or {})
        if not clean:
            return
        keys = set(SHARED_KEYS())
        shared = {k: v for k, v in clean.items() if k in keys}
        personal = {k: v for k, v in clean.items() if k not in keys}
        if shared:
            shared_overlay.put_cells(TABLE_KEY, pid, shared, st=self.st)
        if personal:
            super().patch_overlay(username, pid, personal)


TABLE_OPS = _ProductTableStore(TABLE_KEY)


def shared_cells(pids, st=None):
    """`{"<pid>": {key: value}}` for the SHARED columns of the rows named by `pids`.

    ⚠ `pids` is required and positional all the way down β€” `shared_overlay.cells` refuses to serve
    "everything" by design, and the caller here always holds an already-scoped pool.
    """
    return shared_overlay.cells(TABLE_KEY, pids, st=st if st is not None else TABLE_OPS.st)


def sku_pid(code):
    """A stable integer id for a SKU code. CRC32, masked to 31 bits so it is always positive and
    always JSON-safe. Stable across processes and pulls, which an enumeration index is not."""
    return zlib.crc32(str(code).encode("utf-8")) & 0x7FFFFFFF


def _assert_no_pid_collision(rows):
    """Two SKUs sharing a pid would MERGE in every set operation downstream β€” cohort membership,
    allowed_pids, the measure channel β€” and nothing would report it. Fail the build instead."""
    seen = {}
    for r in rows:
        prior = seen.get(r["pid"])
        if prior is not None and prior != r["code"]:
            raise ValueError(
                f"product_data: pid collision β€” {prior!r} and {r['code']!r} both hash to "
                f"{r['pid']}. Downstream set operations would merge them silently; widen the id "
                f"before shipping this catalogue.")
        seen[r["pid"]] = r["code"]


def _inventory_by_code(t):
    """`{code: {...}}` from the inventory module, or `{}` if it cannot be read.

    Degrades to empty rather than raising, matching `customer_data._pool_build`'s treatment of
    its own slow families: a product table that will not render because inventory is momentarily
    unreachable is a worse failure than one with blank stock columns.
    """
    try:
        import modules.inventory as inventory
        return inventory.sku_inventory(t=t) or {}
    except Exception:
        return {}


def _bu_ltm_share(t, team_id):
    """`{code: 0.0..1.0}` β€” this unit's SHARE of the SKU's last-twelve-months units.

    ⭐ OWNER 2026-08-11: *"scope any inventory with Sales from Fisch"*. The velocity half of the
    inventory block has to be re-shaped by a BU fact, and this is that fact.

    β›” A SHARE, NOT THE UNIT COUNT ITSELF β€” AND THE REASON IS A MEASURED IMPOSSIBILITY. My first
    version returned `products._sku_rev(...)['qty']` and used it directly as the scoped `qty_ltm`,
    leaving the consolidated column on `inventory.sku_inventory`'s own figure. Two readers of one
    question ([[one-question-two-normalizers]]): they disagree, and on 5 SKUs of 5,871 the live
    check found **Fisch's LTM units EXCEEDING the company's** β€” a subset larger than its superset,
    which no reader could explain and no reconciliation could survive.

    Both halves of the ratio come from the SAME read here, so the share is in [0, 1] by
    construction and the scoped figure can never exceed the consolidated one. It also leaves the
    consolidated column exactly as it was β€” the Inventory page and the Product grid still agree
    about company-wide units, which is what "leave the rest" asked for.

    `all_qty == 0` implies `bu_qty == 0` (same reader), so a share of 0 is the honest answer for
    "this unit never sold it": `_bucket` turns that into 'No recent sales', not zero cover.

    Degrades to `{}` on any failure, matching `_inventory_by_code` β€” and a missing code then takes
    the `_DEFAULT_SHARE` below rather than a silent 0.
    """
    try:
        f, to = P.ltm(t)
        allq = {code: (r.get('qty') or 0.0)
                for code, r in (products._sku_rev(f, to, None) or {}).items()}
        buq = {code: (r.get('qty') or 0.0)
               for code, r in (products._sku_rev(f, to, team_id) or {}).items()}
    except Exception:
        return {}
    out = {}
    for code, total in allq.items():
        out[code] = min(1.0, max(0.0, (buq.get(code, 0.0) / total))) if total > 0 else 0.0
    return out


#: What a SKU absent from the LTM sales read is worth to a business unit. ZERO β€” it did not sell
#: in anybody's book over the window, so no unit can claim its velocity. Named rather than
#: inlined so the choice is visible: the alternative (1.0, "assume it is all ours") would print
#: company-wide cover on a BU grid, which is the mixed-scope defect this whole rule avoids.
_DEFAULT_SHARE = 0.0


def _rescope_inventory(e, share):
    """`(qty_ltm, dos, bucket)` recomputed for ONE business unit's sales rate.

    The formulas are `inventory.sku_inventory`'s, applied to a BU-shaped numerator β€” NOT a second
    idea of what days-of-supply means. `_bucket` is imported from there for the same reason: two
    copies of a threshold table is how the Product grid and the Inventory page start disagreeing
    about which SKUs are dead.

    ⚠ `on_hand` is whatever the warehouse holds, unscoped β€” so a Fisch reader's `dos` answers
    "how long does ALL our stock last at Fisch's rate", which is the question a Fisch salesperson
    actually has. It is deliberately NOT a pro-rated share of the shelf: there is no such shelf,
    and inventing one would put a number on screen that no Odoo query could reproduce.
    """
    on_hand = e.get("on_hand")
    if on_hand is None:
        return None, None, None          # no inventory row for this SKU: blank, never zero
    qty = float(e.get("qty_ltm") or 0.0) * float(share or 0.0)
    daily = qty / 365.0
    if daily > 0:
        dos_raw = on_hand / daily
    else:
        dos_raw = float('inf') if on_hand > 0 else 0.0
    try:
        import modules.inventory as inventory
        bucket = inventory._bucket(dos_raw, on_hand, qty)
    except Exception:
        bucket = None
    return qty, (None if dos_raw == float('inf') else round(float(dos_raw), 0)), bucket


def _catalogue_by_code():
    """`{code: {'product', 'category'}}` β€” the CATALOGUE universe this pool is built from.

    β›” UNLIKE `_inventory_by_code`, THIS ONE RAISES, and the asymmetry is the whole point.
    Inventory degrades to `{}` because a product table with blank stock columns beats one that
    will not render. The CATALOGUE is not a column β€” it is the ROW SET. A catalogue read that
    failed quietly would drop the grid straight back to the sold-only 2,717 with every gate
    green and nothing on screen saying so, which is the defect this seam exists to end
    ([[gate-can-report-green-on-nothing]]). `routes_products._pool_for` already turns the raise
    into a 503 that names the cause, so the loud failure has somewhere honest to land.

    It exists as a `pd`-level function rather than an inline `products.catalogue()` call for the
    same reason `_inventory_by_code` does: it is the seam `verify_perm_scope`'s section H stubs
    to build a pool without Odoo.
    """
    return products.catalogue()


def _pricelist_by_code():
    """`({code: {price_*: price}}, report)` from `products.pricelist_by_code`, or `({}, …)`.

    A SEAM for the same two reasons `_inventory_by_code` is one: it degrades rather than raises
    (these are columns, not the row set), and `verify_perm_scope`'s section H stubs it to build a
    pool without Odoo. β›” Unstubbed there, section H would reach live Odoo through the back door
    and the whole file would stop being runnable offline.
    """
    try:
        return products.pricelist_by_code()
    except Exception as e:
        return {}, {"error": f"{type(e).__name__}: {str(e)[:200]}"}


def pool(team_id=None, t=None):
    """One row per ACTIVE product β€” the PRODUCT analogue of `customer_data.pool`.

    CATALOGUE-FIRST, REVENUE LEFT-JOINED (R12 β€” see decision 4 in the module header). The row set
    is `products.catalogue()`; `products.directory()` supplies the revenue columns for the SKUs
    that sold in its window and contributes NO rows of its own.

    `team_id` shapes the revenue columns exactly as it does for customers (`products.directory`
    passes it into `_sku_rev`), which is why `core.perm_scope.derive_pool_scope` must keep
    driving it rather than a post-filter deciding the BU. It does NOT shape the row set: a
    catalogue has no team.
    """
    t = t or P.today()
    consolidated = team_id is None
    # ⭐ OWNER 2026-08-11: read inventory on EVERY pull, not just a consolidated one. The stock
    # itself is company-wide; only the sales-derived half is re-scoped, by `_rescope_inventory`.
    inv = _inventory_by_code(t)
    bu_share = {} if consolidated else _bu_ltm_share(t, team_id)
    sup = supplier_master()
    prices, _price_report = _pricelist_by_code()
    cat = _catalogue_by_code()
    # The LEFT side of the join, indexed by the same code key. β›” A code here that the catalogue
    # does not carry belongs to a product no ACTIVE record claims β€” archived, and R12 keeps those
    # out. `validate()` asserts that of Odoo rather than assuming it, and reports the revenue
    # that therefore sits outside the grid (MEASURED 2026-08-11: 3 codes, $0.00 YTD / $294.50 LY).
    rev = {r["code"]: r for r in products.directory(t=t, team_id=team_id)}

    rows = []
    for code, meta in cat.items():
        r = rev.get(code)
        row = {
            "pid": sku_pid(code),
            "code": code,
            # ⭐ ONE source for the name and the category, the CATALOGUE β€” not the sale line's
            # m2o. For a re-SKUed code the line's name can be the ARCHIVED record's; the active
            # record's `display_name` is the current truth, and it is the same string for every
            # SKU that is not re-SKUed. `directory()` derives the category identically.
            "product": meta.get("product") or code,
            "category": meta.get("category") or "(uncategorized)",
            # ⭐ R12 β€” BLANK, NEVER $0, on a SKU no revenue read ever saw. A row that IS in `rev`
            # keeps its measured numbers including a real 0.0 (it sold last year, not this one).
            "rev_ytd": r.get("rev_ytd", 0.0) if r else None,
            "rev_ly": r.get("rev_ly", 0.0) if r else None,
            "yoy_pct": r.get("yoy_pct") if r else None,
            "qty_ytd": r.get("qty_ytd", 0.0) if r else None,
            "orders_ytd": r.get("orders_ytd", 0) if r else None,
        }
        # ⭐ WAVE 30 W30-T34 β€” the PRICELIST stratum, one column per declared list. Catalogue
        # data like the supplier block, so it rides every pull, scoped or not: a price book is
        # not a thing a business unit owns a slice of, and a Fisch reader who cannot see the
        # Fisch price is exactly who this column is for.
        #
        # β›” `None`, NEVER 0 AND NEVER A FALLBACK, for the 1,883 SKUs no list prices. That is
        # W29-T52's own negative control: the single `3_global` rule computes over `list_price`,
        # which is 1.00 on 5,817 of 5,871 products, so the fallback is not a cheaper answer β€”
        # it is a wrong one wearing a currency sign.
        p = prices.get(code) or {}
        for _col, _name in products.PRICELIST_COLUMNS:
            row[_col] = p.get(_col)
        # Wave 17 R3 β€” the supplier master, on every pull (it is catalogue data, not stock).
        s = sup.get(code) or {}
        row.update({
            "supplier": s.get("supplier") or "(none)",
            "lead_days": s.get("lead_days"),
            "origin_country": s.get("origin_country") or "(none)",
            "first_cost": s.get("first_cost"),
        })
        e = inv.get(code) or {}
        if consolidated:
            qty_ltm, dos, bucket = e.get("qty_ltm"), e.get("dos"), e.get("bucket")
        else:
            qty_ltm, dos, bucket = _rescope_inventory(e, bu_share.get(code, _DEFAULT_SHARE))
        row.update({
            # UNSCOPED on purpose (owner 2026-08-11): one warehouse, no per-brand shelf.
            "on_hand": e.get("on_hand"),
            "unit_cost": e.get("unit_cost"),
            "inv_value": e.get("inv_value"),
            # SCOPED: these three are functions of how fast THIS unit sells the SKU.
            "qty_ltm": qty_ltm,
            "dos": dos,
            "stock_bucket": bucket,
        })
        # THE BUY TRIGGER (R3), ported from `modules/procurement`'s lead-time-cover rule:
        # buy when the shelf runs out before a reorder could land.
        #
        # ⚠ BLANK, NOT "OK", WHEN EITHER INPUT IS MISSING. 339 SKUs have no supplier and 400
        # no lead time; `dos` is null for anything that never sells through. "We don't know"
        # and "you're fine" are different sentences, and only one of them is safe to print
        # beside a purchasing decision.
        #
        # β›” `buy_now` SHIPS NOWHERE ANY MORE, and is computed anyway. OWNER 2026-08-03: it
        # is not a preset field β€” it is a formula over the two columns beside it, and the
        # platform has a formula field type for exactly that. Its Field is gone from
        # `aios_grid_fields.json`, and `rows_from_pool` projects rows STRICTLY through that
        # contract, so no Field means no cell on the wire. Nothing renders this key.
        #
        # It stays computed because deleting it would delete the PROOF. `validate()` below
        # reconciles the formula's predicate against this one, row for row, which is the only
        # thing that makes "the same exact figures" a claim rather than an assertion β€” and
        # this repo has the scar already (wave 17: archiving `ar` "would have DELETED the
        # proof"). Two comparisons per SKU is what that costs.
        #
        # ⭐ READS THE SCOPED `dos`, NOT `e['dos']`. On a Fisch pull the cover gap must answer
        # "does the shelf outlast a reorder AT FISCH'S RATE" β€” reading the consolidated figure
        # here would print a buy signal computed from both units' velocity beside a days-of-supply
        # computed from one, and the two columns would disagree on the same row.
        lead = s.get("lead_days")
        if isinstance(dos, (int, float)) and isinstance(lead, (int, float)) and lead > 0:
            row["cover_gap_d"] = int(round(dos - lead))
            row["buy_now"] = "Buy now" if dos < lead else "OK"
        else:
            row["cover_gap_d"] = None
            row["buy_now"] = None
        rows.append(row)

    # `or 0.0` reads a BLANK as zero FOR SORTING ONLY β€” never for the cell. A never-sold SKU
    # settles among the zero-revenue ones at the bottom, which is where it belongs; the stored
    # value stays None so nothing downstream can mistake "we never saw a sale" for "we measured
    # nothing sold".
    rows.sort(key=lambda r: -(r["rev_ytd"] or 0.0))
    _assert_no_pid_collision(rows)
    return rows


#: ⭐ THE BUY SIGNAL, AS THE FORMULA FIELD EVALUATES IT (owner, 2026-08-03).
#:
#: The owner's ruling was that "Buy signal" is not a preset field β€” it is a formula over two
#: columns the product table already carries, and the platform has a formula field type for it.
#: The formula, verbatim, is what `_seed_wave17.BUY_SIGNAL_FORMULA` creates and what the JSON
#: contract's `_product_removed_buy_now` note records:
#:
#:     IF({lead_days} > 0, IF({dos} < {lead_days}, "Buy now", "OK"), "")
#:
#: This function is a PORT of how `customer-grid/formulaEngine.ts` evaluates that tree, not a
#: restatement of the business rule β€” that is the whole point, because the two could drift and
#: `validate()` is where the drift must show. Three engine behaviours it reproduces exactly:
#:
#:   Β· a `ref` to a missing/non-numeric cell is None (`case "ref"` returns null for anything
#:     that is not a finite number or a string);
#:   Β· `cmp` returns BLANK unless BOTH sides read as numbers β€” it never coerces a blank to 0,
#:     which is the difference between this and a filter engine's `toNum(null) === 0`;
#:   Β· `IF` with a non-boolean condition returns BLANK ("no truthiness"), so a blank comparison
#:     propagates out as a blank cell rather than taking the false branch.
BUY_SIGNAL_FORMULA = 'IF({lead_days} > 0, IF({dos} < {lead_days}, "Buy now", "OK"), "")'


def _buy_signal_formula(row):
    """Evaluate `BUY_SIGNAL_FORMULA` over one pool row -> 'Buy now' | 'OK' | '' (blank)."""
    def num(v):
        # The engine's `ref` + `asNumber`: booleans are not values here, and a non-finite
        # number is null. `isinstance(True, int)` is True in Python, so bool is excluded first.
        if isinstance(v, bool) or not isinstance(v, (int, float)):
            return None
        return v if v == v and v not in (float('inf'), float('-inf')) else None

    lead, dos = num(row.get("lead_days")), num(row.get("dos"))
    if lead is None:                 # `{lead_days} > 0` is blank -> IF(blank, …) is blank
        return ""
    if not lead > 0:
        return ""                    # the formula's own else-branch
    if dos is None:                  # `{dos} < {lead_days}` is blank -> IF(blank, …) is blank
        return ""
    return "Buy now" if dos < lead else "OK"


def validate(team_id=None, t=None):
    """Reconcile what SHIPS to an independent aggregate β€” the platform's own rule that a number
    which does not tie to Odoo does not ship.

    β›”β›” THE POPULATION LEG IS NEW, AND IT IS HERE BECAUSE THIS FUNCTION USED TO BE SELF-SEALING
    (wave 29, contract C8). It reconciled Ξ£ per-SKU YTD revenue against `products._sku_rev` β€” the
    same function `pool()` built its rows from. BOTH SIDES CAME FROM THE JOIN, so the oracle could
    not see a missing row, and a grid holding 2,717 of 5,875 active products passed for a wave.
    No gate anywhere asserted a pool row COUNT against an independent Odoo `search_count` either.
    That is the leg below, and it is the control that would have caught it
    ([[no-unverifiable-aggregates]]).

    ⚠ The revenue leg was NOT deleted with it β€” it answers a different question ("did the join
    lose money?") and is still the right shape for that one. What changed is that it now has to
    account for revenue belonging to codes NO ACTIVE PRODUCT CARRIES, because R12 keeps archived
    products out of the grid; the total is decomposed rather than compared loosely, so a growing
    "outside the grid" figure shows up as a number rather than as slack in a tolerance.

    Still deliberately NOT covered, and said rather than implied: the inventory columns are a
    different module's reconciliation, and they are absent on a scoped pull anyway.
    """
    t = t or P.today()
    rows = pool(team_id=team_id, t=t)

    # ⭐⭐ THE POPULATION, AGAINST AN ORACLE THAT CANNOT SEE OUR JOIN. A bare Odoo count of active
    # products, asked fresh β€” not a `len()` over anything this module or `products.catalogue()`
    # built. NC: drop one row from `pool()` and this goes red; that is the whole point of it.
    n_active = products.catalogue_count()
    checks = [{
        "check": "the pool holds one row per ACTIVE product (R12) β€” row count == an INDEPENDENT "
                 "Odoo search_count('product.product', active=True)",
        "ours": len(rows), "theirs": n_active, "ok": len(rows) == n_active,
    }]

    yf, yt = P.ytd(t)
    sku_rev = products._sku_rev(yf, yt, team_id) or {}
    in_pool = {r["code"] for r in rows}
    outside = {c: v for c, v in sku_rev.items() if c not in in_pool}
    ours = round(sum(r["rev_ytd"] or 0.0 for r in rows), 2)
    theirs = round(sum(v.get("rev", 0.0) for v in sku_rev.values()), 2)
    dropped = round(sum(v.get("rev", 0.0) for v in outside.values()), 2)
    checks.append({
        "check": "Ξ£ per-SKU YTD revenue in the grid + Ξ£ YTD revenue of codes no ACTIVE product "
                 "carries == modules.products._sku_rev over the same window",
        "ours": round(ours + dropped, 2), "theirs": theirs,
        "ok": abs((ours + dropped) - theirs) < 0.01,
        "detail": {"in_the_grid": ours, "outside_the_grid": dropped,
                   "codes_outside": len(outside)},
    })
    # β›” AND THE EXCLUSION IS ASSERTED, NOT ASSUMED. "Everything I dropped was archived" is
    # trivially true when checked against the dict that did the dropping β€” that is the
    # self-sealing shape all over again. So it is asked of ODOO. A code here that a live ACTIVE
    # product carries means `products.catalogue()` missed a row that has revenue, which is the
    # 2,717 defect returning in a smaller costume.
    # ⚠ `pid:N` keys are not `default_code`s and are skipped: an UNCODED active product is in the
    # catalogue under its own `pid:N` key and therefore cannot be in `outside` at all.
    coded_outside = sorted(c for c in outside if not str(c).startswith("pid:"))
    still_active = (O.get_odoo().search_count(
        'product.product', [('active', '=', True), ('default_code', 'in', coded_outside)])
        if coded_outside else 0)
    checks.append({
        "check": "every SKU with revenue but NO grid row is genuinely ARCHIVED (R12: archived "
                 "stay out) β€” asked of Odoo, never of the catalogue that did the dropping",
        "ours": still_active, "theirs": 0, "ok": still_active == 0,
        "detail": {"codes": coded_outside[:10], "n_codes": len(outside)},
    })
    # ── ⭐⭐ WAVE 30 W30-T34 β€” THE PRICELIST COLUMNS, AGAINST ORACLES THAT CANNOT SEE OUR JOIN ──
    #
    # Three legs, because the column can fail in three different ways and only one of them is a
    # count. `products.pricelist_by_code` builds a `{code: {col: price}}` map by expanding each
    # rule over the products it names, IN PYTHON β€” that expansion is the fragile part, so every
    # leg below re-asks ODOO instead of re-reading the map.
    price_report = _pricelist_by_code()[1]
    _pl_rows = O.search_read('product.pricelist', [], ['id', 'name'])
    _pl_id = {}
    for _p in _pl_rows:
        _pl_id.setdefault(str(_p.get('name') or '').strip(), _p['id'])
    _today = P.today().isoformat()

    _targets_memo = {}

    def _rule_targets(list_name):
        """(variant_ids, template_ids) a pricelist prices today β€” read FRESH from Odoo.

        Memoised for the LIFE OF THIS CALL only: legs 1 and 2 both need all three lists, and
        without this `validate()` makes six identical round trips instead of three (measured:
        ~48s of the run). Deliberately NOT an `lru_cache` β€” an oracle that survives the process
        is an oracle reading yesterday's Odoo.
        """
        if list_name in _targets_memo:
            return _targets_memo[list_name]
        var, tmpl = set(), set()
        plid = _pl_id.get(list_name)
        if plid is None:
            return var, tmpl
        for r in O.search_read(
                'product.pricelist.item',
                [('pricelist_id', '=', plid), ('compute_price', '=', 'fixed'),
                 ('applied_on', 'in', ['0_product_variant', '1_product'])],
                ['product_id', 'product_tmpl_id', 'applied_on', 'fixed_price',
                 'date_start', 'date_end']):
            ds, de = str(r.get('date_start') or '')[:10], str(r.get('date_end') or '')[:10]
            if (ds and ds > _today) or (de and de < _today):
                continue
            fp = r.get('fixed_price')
            if not isinstance(fp, (int, float)) or fp <= 0:
                continue
            if r.get('applied_on') == '0_product_variant' and r.get('product_id'):
                var.add(O.m2o_id(r['product_id']))
            elif r.get('product_tmpl_id'):
                tmpl.add(O.m2o_id(r['product_tmpl_id']))
        _targets_memo[list_name] = (var, tmpl)
        return var, tmpl

    # LEG 1 β€” COVERAGE PER LIST. Ours: cells we filled. Theirs: a bare Odoo `search_count` of
    # ACTIVE products a fresh read of that list's rules reaches. The rule set is shared (it IS
    # the data) but the EXPANSION is not, and the expansion is what breaks.
    for _col, _name in products.PRICELIST_COLUMNS:
        _var, _tmpl = _rule_targets(_name)
        _dom = [('active', '=', True), '|', ('id', 'in', sorted(_var)),
                ('product_tmpl_id', 'in', sorted(_tmpl))]
        theirs = O.get_odoo().search_count('product.product', _dom) if (_var or _tmpl) else 0
        ours = sum(1 for r in rows if isinstance(r.get(_col), (int, float)))
        checks.append({
            "check": f"{_name} pricelist: SKUs priced in the grid == an INDEPENDENT Odoo count "
                     f"of ACTIVE products its date-valid fixed rules reach",
            "ours": ours, "theirs": theirs, "ok": ours == theirs,
            "detail": {"column": _col, "variant_rules": len(_var), "template_rules": len(_tmpl)},
        })

    # LEG 2 β€” β›” THE TICKET'S OWN NEGATIVE CONTROL, AS A LEG. A SKU with no specific item must
    # render BLANK. Asked of ODOO, never of the map that produced the blank β€” "everything I left
    # empty was genuinely unpriced" is trivially true when checked against the dict that emptied
    # it, which is the self-sealing shape the population leg above exists to end.
    _unpriced = [r for r in rows
                 if not any(isinstance(r.get(c), (int, float))
                            for c, _n in products.PRICELIST_COLUMNS)]
    _codes = {r["code"] for r in _unpriced}
    _stray = 0
    if _codes:
        # Resolve those codes back to Odoo ids and ask whether ANY declared list prices them.
        _ids, _tmpls = set(), set()
        for _p in O.search_read('product.product',
                                [('active', '=', True),
                                 ('default_code', 'in', sorted(c for c in _codes
                                                               if not c.startswith("pid:")))],
                                ['id', 'product_tmpl_id']):
            _ids.add(_p['id'])
            _tmpls.add(O.m2o_id(_p.get('product_tmpl_id')))
        for _col, _name in products.PRICELIST_COLUMNS:
            _v, _t = _rule_targets(_name)
            _stray += len((_v & _ids) | ({t for t in _tmpls if t in _t}))
    checks.append({
        "check": "every SKU rendering a BLANK price is genuinely unpriced on all three declared "
                 "lists (W29-T52's NC: no fallback dressed as a price) β€” asked of Odoo",
        "ours": _stray, "theirs": 0, "ok": _stray == 0,
        "detail": {"blank_skus": len(_unpriced), "priced_skus": len(rows) - len(_unpriced)},
    })

    # LEG 3 β€” ⭐ R6's SECOND SENTENCE, AS A NUMBER. *"If there is lag or it can't be done, you
    # need to explicitly tell me why and recommend a fix."* Everything the reader cannot see is
    # counted here rather than dropped. The ASSERTION is the one thing that must never be true β€”
    # a price cell that is present and not a positive number, i.e. a "free" SKU β€” while the
    # rest rides `detail` so it is reported without reddening an honest day's data.
    _bad = [r["code"] for r in rows
            for c, _n in products.PRICELIST_COLUMNS
            if r.get(c) is not None and not (isinstance(r.get(c), (int, float)) and r[c] > 0)]
    # β›” MEASURED 2026-08-12 AND NOT FIXABLE FROM THIS FENCE: `aios_grid.rows_from_pool` sends
    # every non-text field through `_round(v)` = `round(v)` with no ndigits, i.e. to a whole
    # dollar. 52.1% of fixed prices carry cents and the median relative error is 5.26%, so this
    # column ties to the cent HERE and ships rounded. Reported, never silently enforced.
    _cents = sum(1 for r in rows for c, _n in products.PRICELIST_COLUMNS
                 if isinstance(r.get(c), (int, float)) and abs(r[c] - round(r[c])) > 1e-9)
    _to_zero = sum(1 for r in rows for c, _n in products.PRICELIST_COLUMNS
                   if isinstance(r.get(c), (int, float)) and round(r[c]) == 0)
    checks.append({
        "check": "no price cell is present-but-not-a-positive-number (a 0 would read as FREE; "
                 "an unset rule must be blank), and what the reader cannot see is COUNTED",
        "ours": len(_bad), "theirs": 0, "ok": not _bad,
        "detail": {"bad_cells": _bad[:10], "reader_report": price_report,
                   "wire_rounding_loses_cents_on": _cents,
                   "wire_rounding_to_zero_dollars": _to_zero},
    })

    # WAVE 17 R3 β€” the BUY SIGNAL must be a total, exact partition of the catalogue, and every
    # member of it must be re-derivable from the two columns beside it. A signal somebody buys
    # stock on cannot be "mostly right": the failure that matters is a row that says OK because
    # an input was missing, so the blank leg is checked as hard as the other two.
    if team_id is None:                      # consolidated only β€” the inputs exist only there
        buy = [r for r in rows if r.get("buy_now") == "Buy now"]
        ok_rows = [r for r in rows if r.get("buy_now") == "OK"]
        blank = [r for r in rows if r.get("buy_now") is None]
        mis = sum(1 for r in buy if not (r["dos"] < r["lead_days"]))
        mis += sum(1 for r in ok_rows if not (r["dos"] >= r["lead_days"]))
        # A blank must be UNKNOWN β€” never a row we could have answered and quietly did not.
        mis += sum(1 for r in blank
                   if isinstance(r.get("dos"), (int, float))
                   and isinstance(r.get("lead_days"), (int, float)) and r["lead_days"] > 0)
        checks.append({
            "check": "Buy signal partitions the catalogue (buy + ok + unknown == rows, none "
                     "misclassified)",
            "ours": len(buy) + len(ok_rows) + len(blank) - mis, "theirs": len(rows),
            "ok": mis == 0 and len(buy) + len(ok_rows) + len(blank) == len(rows),
            "detail": {"buy_now": len(buy), "ok": len(ok_rows), "unknown": len(blank),
                       "misclassified": mis},
        })
        # ⭐ OWNER 2026-08-03 β€” "use the formula fields to come up to the same EXACT figures".
        #
        # This is that sentence, as a check. The retired preset column and the formula that
        # replaces it must agree on every SKU, in all three states, or the replacement is not a
        # replacement. `_buy_signal_formula` is a line-by-line port of the client formula
        # engine's evaluation of
        #
        #     IF({lead_days} > 0, IF({dos} < {lead_days}, "Buy now", "OK"), "")
        #
        # including the part that is easy to get wrong: a comparison against a BLANK is blank,
        # never a coerced 0. (The client engine's `cmp` returns null unless both sides read as
        # numbers, and `IF` refuses a non-boolean condition β€” so a missing `dos` yields "" and
        # not "Buy now". A filter engine would have said `0 < 30` and swept in every SKU that
        # never sells through; the formula engine does not, and this check is what holds it.)
        #
        # ⚠ It compares SETS OF SKUs, not counts. Two different partitions can share a shape.
        disagree = sorted(r["code"] for r in rows
                          if (r.get("buy_now") or "") != _buy_signal_formula(r))
        checks.append({
            "check": 'Buy signal as a FORMULA field == the retired preset column, per SKU '
                     '(IF({lead_days} > 0, IF({dos} < {lead_days}, "Buy now", "OK"), ""))',
            "ours": len(rows) - len(disagree), "theirs": len(rows),
            "ok": not disagree,
            "detail": {"disagreeing_skus": disagree[:10], "n_disagree": len(disagree)},
        })
        # The other half of "the same figures": the SHARED Buy list view can no longer filter on
        # the retired column, and its replacement conditions must select the same SKUs. They are
        # `dos isNotEmpty AND lead_days isNotEmpty AND lead_days > 0 AND dos < lead_days`
        # (_seed_wave17.views), so this reproduces exactly that conjunction.
        #
        # β›” THE NEGATIVE CONTROL IS WHY THIS IS NOT `cover_gap_d < 0`, which reads like the
        # obvious filter and is WRONG: `cover_gap_d` is `int(round(dos - lead))`, so a genuine
        # gap of -0.4 days rounds to 0 and that SKU drops off a buy list it belongs on.
        view_rows = {r["code"] for r in rows
                     if isinstance(r.get("dos"), (int, float))
                     and isinstance(r.get("lead_days"), (int, float))
                     and r["lead_days"] > 0 and r["dos"] < r["lead_days"]}
        signal_rows = {r["code"] for r in buy}
        rounding_would_miss = sorted(
            c for c in signal_rows
            if next((r for r in rows if r["code"] == c), {}).get("cover_gap_d") == 0)
        checks.append({
            "check": "Buy list view conditions (dos/lead_days, no retired column) select "
                     "exactly the Buy-now SKUs",
            "ours": len(view_rows), "theirs": len(signal_rows),
            "ok": view_rows == signal_rows,
            "detail": {"only_in_view": sorted(view_rows - signal_rows)[:10],
                       "only_in_signal": sorted(signal_rows - view_rows)[:10],
                       # Reported, not asserted: how many SKUs a `cover_gap_d < 0` filter would
                       # have silently dropped. 0 today does not make that filter correct.
                       "cover_gap_rounds_to_zero": len(rounding_would_miss)},
        })
    return checks