Skip to content

fix(workflows): bom_cost_rollup was written against entities that do not exist - #70

Merged
sauterbe merged 1 commit into
mainfrom
fix/bom-cost-rollup-real-entities
Aug 7, 2026
Merged

fix(workflows): bom_cost_rollup was written against entities that do not exist#70
sauterbe merged 1 commit into
mainfrom
fix/bom-cost-rollup-real-entities

Conversation

@sauterbe

@sauterbe sauterbe commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The non-recursive sibling of the two templates fixed in #69. It passed the raw-ERP gate because it was already business-entity shaped — it was still fiction against agentos_neo_xentral.

it used reality
a BillOfMaterials entity not one of the core's 50 entities. The bill is a section ON the product: bom.items, filled on a single read
Product filtered on isBom Product filters on number, status, name, project, ean, manufacturerNumber, variant.isMatrix, updatedAt — nothing else
purchasePrice, calculatedPurchasePrice the model has prices.purchase
.get('data') on a list removed by ADR-0002 — a list node's output IS the array

Better seeding, not just correct

There is no "is a bill of materials" filter — but the list payload already carries production.hasBillOfMaterials and prices.purchase per product. So the work list starts with the bills alone, and most components never need a read at all.

Measured on a real instance: 2 of 25 products are bills. In the fixtures the nested case dropped from 4 reads to 2, the diamond from 6 to 3.

Two failure modes the original did not survive

  • A bill containing itself looped until the run's time limit killed the job. It is now detected and logged.
  • A cycle poisoned everything above it. Anything built on a component we could not cost is now left untouched rather than written as 0. Overwriting a maintained purchase price with a number we know is wrong is worse than doing nothing.

Also: source: 'calculated' on the write — the one value that makes Xentral treat the number as a calculated purchase price rather than a manually maintained one.

Verified live against mvp

Dry-run: writes suppressed, reads real.

prd_62005 = 2 × prd_62004 @ 1.20  ->  2.4   (stored: 2.40)
prd_61989 = 1× + 4× + 2× @ 0.00   ->  0.0   (stored: 0.00)

Both writes idempotent on that instance — the numbers are confirmed against data neither I nor the fixtures invented.

Plus four fixture scenarios (nested, cycle, diamond, and a component outside the listed page), and validate_graph / check_workflow clean in both locales.

One thing this uncovered, fixed separately

The live run made the validator warn that page[size] "does nothing" and recommend query.page = {"size": N} instead. Measured: page[size] returns 100 rows, the recommended form returns 50. The validator was warning about the only form that works. Fixed in agent-os #107.

Not changed

The schedule ships without active: true, so it does not start itself on install. That is consistent across all six scheduled templates, so I left it alone rather than making this one the odd one out.

…not exist

It passed the raw-ERP gate because it was already `business-entity` shaped. It
was still fiction against `agentos_neo_xentral`:

- a `BillOfMaterials` entity — the core has 50 entities and that is not one of
  them. The bill of materials is a section ON the product, `bom.items`, filled
  on a single read.
- `Product` filtered on `isBom` — Product filters on number, status, name,
  project, ean, manufacturerNumber, variant.isMatrix and updatedAt. Nothing else.
- `purchasePrice` and `calculatedPurchasePrice` as flat fields — the model has
  `prices.purchase`.
- `.get('data')` on a list result, which ADR-0002 removed.

The rewrite reads the bill from the product and writes `prices.purchase` with
`source: 'calculated'` — the one value that makes Xentral treat the number as a
calculated purchase price rather than a manually maintained one.

**The seeding got better, not just correct.** There is no "is a bill of
materials" filter, but the list PAYLOAD already carries
`production.hasBillOfMaterials` and `prices.purchase` per product. So the work
list starts with the bills alone and most components never need a read at all.
Measured on a real instance: 2 of 25 products are bills, and in the fixtures the
nested case dropped from 4 reads to 2, the diamond from 6 to 3.

**Two failure modes the original did not survive.** A bill containing itself
looped until the run's time limit killed the job; it is now detected and logged.
And a cycle no longer poisons everything above it: anything built on a component
we could not cost is left untouched rather than written as 0. Overwriting a
maintained purchase price with a number we know is wrong is worse than doing
nothing.

Verified live against mvp (dry-run, writes suppressed, reads real):

    prd_62005 = 2 x prd_62004 @ 1.20  ->  2.4   (stored: 2.40)
    prd_61989 = 1x + 4x + 2x @ 0.00   ->  0.0   (stored: 0.00)

Both writes idempotent on that instance, so the numbers are confirmed against
data neither I nor the fixtures invented. Plus four fixture scenarios — nested,
cycle, diamond, and a component outside the listed page — and `validate_graph` /
`check_workflow` clean in both locales.
@sauterbe
sauterbe merged commit e136fa1 into main Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant