diff --git a/CLAUDE.md b/CLAUDE.md index 9b7fda24f..0af2b6aa6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,20 +5,14 @@ **NEVER TOUCH PRODUCTION.** `deploy/live/` is the production snapshot — do not edit directly. All dev work goes to `deploy/dev/` ONLY. Read `deploy/OCI_UPLOAD.md` §RULES before any OCI upload. -## ⏸ PUSH PAUSE (2026-07-11, standing until lifted) — local/localhost only -**User directive: "we should test our workings localhost for now, may delay pushing until major -breakthru."** A lot merged today in one burst (6+ PRs across bim-ootb/bim-compiler) — pause here to -batch-verify it all actually works together before pushing more. Applies to every session/dispatched -worker until explicitly lifted: -- **Commit locally as normal** (checkpoints are fine, encouraged) — **do NOT `git push` and do NOT - open a PR** for new work started after this point. Work already pushed before this directive stood - (today's merged PRs) is not being un-pushed — this is forward-only, not a rollback. -- **Verify via localhost, not CI.** Start the actual dev server, drive the real feature in a browser - (per this project's own standing rule — whitebox `§`-log first, but confirm it live too), don't - lean on a green CI run as the only proof. -- **Every Agent-tool dispatch must carry this instruction explicitly** — a fresh worker has no memory - of this pause, tell it directly: commit locally, verify on localhost, do not push, do not open a PR. -- **Lifted when:** the user says so, or names the "major breakthrough" that's worth pushing for. +## ⏸ PUSH PAUSE — LIFTED 2026-07-17 +Stood 2026-07-11→2026-07-17 (local/localhost-only verification while a large merge burst got +batch-verified). **User directive 2026-07-17: "now push permission is ON, ie always push to live +until user decides to pause."** Push freely (normal fast-forward pushes / PRs — not force-push, not +skipping the localhost/§-witness verification habit this project still runs on) until the user says +to pause again. If they do, the same discipline as before applies: commit locally, verify on +localhost, no push/PR, and every Agent-tool dispatch must carry that instruction explicitly since a +fresh worker has no memory of it. ## WORK-TO-ZERO (the backlog contract — enforced every session) **No standing backlog file right now — `prompts/archive/FRONTEND_LANE_MASTER.md §NEW BACKLOG` (archived 2026-07-11, prompts-audit) DRAINED 2026-07-08 (every diff --git a/PROGRESS.md b/PROGRESS.md index 8a88d2a92..7a007104f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -20,6 +20,8 @@ not push/open a PR unless the user explicitly lifts it for that session. `CLAUDE.md` §⏸ PUSH PAUSE. ## OPEN — to be assigned to sessions (user dispatches from this list, check before starting cold) +- `prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md` — SONNET: finish Cash/Inventory posting oracles + (generator built, reuse) + ERPUserGuide navigation + core S&D-flow chapter. Fresh branch off master. - `prompts/RESUME_HR_BIM_ASSET.md` §2026-07-06c — A/B/C bugs + E decision. - `prompts/RESUME_WORLD_HISTORY_DEDUP_RESTORE.md` §2026-07-06 — G6, Ph3, Pt1 parked. - `prompts/PILL_DRAWER_REORGANIZATION.md` — first-touch flicker. @@ -45,8 +47,9 @@ not push/open a PR unless the user explicitly lifts it for that session. `CLAUDE `W-DW-DENSITY-TE` D3) unexplained, low-priority — `project_arc_meshreadpixels_branch_unmerged.md`. ## Archive — DONE/shipped (one-line pointers; detail in cards + memory topic files) -- ✅ B-3 0-seed posting oracles (2026-07-17, Fable 5) — 6 G-seed classes `maxDiff=0c` vs real compiled - posters (OSGi harness, scratch clone); ledger 43→49; local only (PUSH PAUSE). `prompts/FABLE5_B3_POSTING_ORACLE.md`. +- ✅ B-3 + W-POST-TAIL posting oracles (2026-07-17/18, Fable 5) — 6 G-seed classes + BankStatement/ + MatchPO/Requisition `maxDiff=0c`; ledger 43→52; 17/20 posters fold; Cash/Inventory next, Production ⛔. + `prompts/FABLE5_B3_POSTING_ORACLE.md` + `prompts/HARDEN_MATRIX.md §W-POST-TAIL`. - ✅ 2026-07-10 marathon branches (2026-07-17 verify) — `fix/grid-tilt-guard`, `fix/dw-rot-units`, `fable/dwprobe-dedup`, `fix/terminal-oracle-source` all confirmed fully superseded (exact fix content already verbatim on main via other commits) and pruned, local+origin, bim-ootb. diff --git a/build/erp/bench_facts.json b/build/erp/bench_facts.json index 396809ac4..cb1413d9b 100644 --- a/build/erp/bench_facts.json +++ b/build/erp/bench_facts.json @@ -3,10 +3,10 @@ "ours": { "name": "Fold-Engine ERP (browser)", "lang": "JS", - "loc": 28236, - "bytes": 1754467, - "mb": 1.67, - "files": 79, + "loc": 28971, + "bytes": 1741690, + "mb": 1.66, + "files": 84, "tables": 7, "source": "bim-ootb/erp/*.js (minus vendored)", "measured": true @@ -24,8 +24,8 @@ "measured": true }, "ratios": { - "loc": 50.7, - "bytes": 25.8, + "loc": 49.4, + "bytes": 26, "tables": 132.1 }, "file_list": [ @@ -96,8 +96,8 @@ }, { "file": "bim_orders_overlay.js", - "loc": 84, - "bytes": 4604 + "loc": 98, + "bytes": 5957 }, { "file": "blue_future.js", @@ -111,8 +111,8 @@ }, { "file": "crud_overlay.js", - "loc": 2487, - "bytes": 181601 + "loc": 2494, + "bytes": 182394 }, { "file": "doc_cycle_validator.js", @@ -124,6 +124,11 @@ "loc": 126, "bytes": 8343 }, + { + "file": "erp_attrib.js", + "loc": 67, + "bytes": 4152 + }, { "file": "erp_descriptor.js", "loc": 89, @@ -136,18 +141,18 @@ }, { "file": "erp_kernel.js", - "loc": 294, - "bytes": 19847 + "loc": 315, + "bytes": 21208 }, { - "file": "erp_panel.js", - "loc": 478, - "bytes": 19505 + "file": "erp_key_epochs.js", + "loc": 166, + "bytes": 11344 }, { "file": "erp_period_close.js", - "loc": 169, - "bytes": 10619 + "loc": 228, + "bytes": 14426 }, { "file": "erp_persist.js", @@ -181,14 +186,19 @@ }, { "file": "erp_seam.js", - "loc": 164, - "bytes": 9812 + "loc": 194, + "bytes": 12050 }, { "file": "erp_search.js", "loc": 514, "bytes": 20319 }, + { + "file": "erp_shard.js", + "loc": 270, + "bytes": 17511 + }, { "file": "erp_signer.js", "loc": 93, @@ -199,11 +209,21 @@ "loc": 47, "bytes": 3087 }, + { + "file": "error_beacon.js", + "loc": 85, + "bytes": 4330 + }, { "file": "feed_fold.js", "loc": 153, "bytes": 8193 }, + { + "file": "field_health.js", + "loc": 139, + "bytes": 9522 + }, { "file": "genesis.js", "loc": 370, @@ -231,23 +251,23 @@ }, { "file": "icons.js", - "loc": 104, - "bytes": 13672 + "loc": 120, + "bytes": 16482 }, { "file": "idmp_history.js", - "loc": 253, - "bytes": 16938 + "loc": 287, + "bytes": 18946 }, { "file": "idmp_pills.js", - "loc": 315, - "bytes": 22942 + "loc": 322, + "bytes": 23720 }, { "file": "idmp_session.js", - "loc": 278, - "bytes": 15487 + "loc": 286, + "bytes": 16187 }, { "file": "img_store.js", @@ -261,33 +281,28 @@ }, { "file": "kanban_host.js", - "loc": 114, - "bytes": 7145 + "loc": 174, + "bytes": 11448 }, { "file": "kanban_lens.js", - "loc": 341, - "bytes": 20222 + "loc": 356, + "bytes": 21165 }, { "file": "kernel_ops.js", - "loc": 609, - "bytes": 36137 - }, - { - "file": "menu_seed.js", - "loc": 3, - "bytes": 93319 + "loc": 817, + "bytes": 51421 }, { - "file": "migrate_agent.js", - "loc": 432, - "bytes": 21896 + "file": "kitchen_core.js", + "loc": 89, + "bytes": 5160 }, { - "file": "migrate_showme.js", - "loc": 264, - "bytes": 15308 + "file": "kitchen_lens.js", + "loc": 215, + "bytes": 12917 }, { "file": "ninja_bundle.js", @@ -316,8 +331,8 @@ }, { "file": "ninja_pill.js", - "loc": 335, - "bytes": 22054 + "loc": 342, + "bytes": 22433 }, { "file": "ninja_rule.js", @@ -339,6 +354,11 @@ "loc": 154, "bytes": 10829 }, + { + "file": "op_upcaster.js", + "loc": 122, + "bytes": 6765 + }, { "file": "overlay_kit.js", "loc": 53, @@ -346,13 +366,8 @@ }, { "file": "period_close_ui.js", - "loc": 160, - "bytes": 8708 - }, - { - "file": "pill_builder.js", - "loc": 301, - "bytes": 14516 + "loc": 189, + "bytes": 10688 }, { "file": "plugin_overlay.js", @@ -371,13 +386,13 @@ }, { "file": "pos_core.js", - "loc": 476, - "bytes": 30798 + "loc": 512, + "bytes": 33182 }, { "file": "pos_lens.js", - "loc": 1298, - "bytes": 79849 + "loc": 1553, + "bytes": 97551 }, { "file": "post_resolver.js", @@ -386,34 +401,39 @@ }, { "file": "report_overlay.js", - "loc": 1058, - "bytes": 78570 - }, - { - "file": "role_band.js", - "loc": 158, - "bytes": 5726 + "loc": 1069, + "bytes": 79689 }, { "file": "rule_fold.js", - "loc": 246, - "bytes": 16076 + "loc": 249, + "bytes": 16382 }, { "file": "sw.js", - "loc": 236, - "bytes": 14732 + "loc": 244, + "bytes": 15992 }, { "file": "system_monitor.js", - "loc": 243, - "bytes": 18105 + "loc": 338, + "bytes": 24554 }, { "file": "system_tenant.js", "loc": 80, "bytes": 5477 }, + { + "file": "teams_embed.js", + "loc": 168, + "bytes": 10802 + }, + { + "file": "tip_fold.js", + "loc": 88, + "bytes": 4951 + }, { "file": "user_names.js", "loc": 108, @@ -423,6 +443,11 @@ "file": "vfs_detect.js", "loc": 58, "bytes": 3124 + }, + { + "file": "zoom_across.js", + "loc": 39, + "bytes": 2082 } ], "witness": "W-CODEBASE-FACTS" diff --git a/build/erp/oracle/post_b3_fixture.json b/build/erp/oracle/post_b3_fixture.json index 11c85a8db..b199984ff 100644 --- a/build/erp/oracle/post_b3_fixture.json +++ b/build/erp/oracle/post_b3_fixture.json @@ -835,7 +835,7 @@ [ "1000000", "1000000", - "2026-07-17", + "2026-07-18", "MAN", "Cap", "12000.00", @@ -855,7 +855,7 @@ [ "1000001", "1000001", - "2026-07-17", + "2026-07-18", "MAN", "Cap", "6000.00", @@ -875,7 +875,7 @@ [ "1000002", "1000001", - "2026-07-17", + "2026-07-18", "MAN", "Exp", "500.00", @@ -895,7 +895,7 @@ [ "1000003", "1000002", - "2026-07-17", + "2026-07-18", "MAN", "Cap", "9000.00", @@ -932,7 +932,7 @@ [ "1000000", "1000002", - "2026-07-17", + "2026-07-18", "S", "9000.00", "0.00", @@ -996,7 +996,7 @@ "1000000", "1000001", "101", - "2026-07-18", + "2026-07-19", "200013", "200010", "200004", @@ -2157,7 +2157,7 @@ "137", "101", "1.00", - "2026-07-17", + "2026-07-18", "0", "0", "Y", @@ -2239,7 +2239,7 @@ "1000001", "101", "A", - "2026-07-18", + "2026-07-19", "200010", "200011", "200001", @@ -6332,7 +6332,8 @@ "validfrom", "validto", "ad_client_id", - "ad_org_id" + "ad_org_id", + "isactive" ], "rows": [ [ @@ -6343,7 +6344,19 @@ "2000-01-01", "2056-01-29", "11", - "0" + "0", + "Y" + ], + [ + "100", + "102", + "114", + "0.80064051240992794235388310648518815052", + "2003-12-29", + "2056-01-29", + "11", + "0", + "N" ], [ "102", @@ -6353,7 +6366,19 @@ "2000-01-01", "2056-01-29", "11", - "0" + "0", + "Y" + ], + [ + "102", + "100", + "114", + "1.249", + "2003-12-29", + "2056-01-29", + "11", + "0", + "N" ] ] }, diff --git a/docs/ModellerGuide.md b/docs/ModellerGuide.md index 3fb9a5c30..764815444 100644 --- a/docs/ModellerGuide.md +++ b/docs/ModellerGuide.md @@ -350,7 +350,7 @@ it: it places that trade's elements at the **measured cadence** of a real coordi runs it can, gates the clashes, and **honestly refuses** when the building has nothing to hang the trade on. Nothing is invented — every placement uses a spacing/clearance rule *mined from a real IFC model*. -![Walk · ELEC, X-ray reveal — 102 electrical fixtures placed across 19/21 real spaces in the Duplex; structure goes near-transparent glass and every fixture glows in its discipline colour through the shell, on both floors](img/modeller/walk-fixtures.png) +![Walk · ELEC, X-ray reveal — 270 electrical fixtures placed across 6 storeys in SampleCastle; structure goes near-transparent glass and every fixture glows in its discipline colour through the shell](img/modeller/walk-fixtures.png) A normal (non-X-ray) view of this same walk is honestly near-empty: a real electrical outlet or light lives inside a room, not poking through an exterior wall, so from outside the sealed shell it's naturally diff --git a/docs/img/modeller/walk-fixtures.png b/docs/img/modeller/walk-fixtures.png index 141ff5574..e760af19a 100644 Binary files a/docs/img/modeller/walk-fixtures.png and b/docs/img/modeller/walk-fixtures.png differ diff --git a/docs/internal/ERP_COVERAGE_MATRIX.md b/docs/internal/ERP_COVERAGE_MATRIX.md index 50d428f62..f4dd3d516 100644 --- a/docs/internal/ERP_COVERAGE_MATRIX.md +++ b/docs/internal/ERP_COVERAGE_MATRIX.md @@ -108,11 +108,13 @@ denominator, not 496 classes / 735k LOC). | 🟡 **rule-consistent** | **MInventory physical-count fold (W-FOLD-INVENTORY)** — `poc_inventory.js` ENACTS a count (no I± in seed, NO oracle): book on-hand = FOLD of real `m_transaction` (== `m_storageonhand`) → I+/I- picked by sign(counted−book) → folding the adjustment through the qty spine lands on-hand == counted (6/6 products, gain+loss), and the GL adjustment value = `|adjQty| × cost` via the PROVEN cost-selection rule (Oak Tree +3 × 51.45 = 154.35) and BALANCES. **Closes the MInventory I± rider qtyonhand named-deferred.** §FALSIFIER-A wrong polarity (on-hand ≠ counted), §FALSIFIER-B Material cost element (24.00 ≠ Average 51.45). **GL offset account named-deferred** (Inventory-Gain/Loss `ACCTTYPE_InvDifferences` has no extractable column; the leg value + balance are proven). Tier = rule-consistent, NOT "== iDempiere". `build/erp/poc_inventory.log`. | | ✅ **oracle-equivalent** | **AD_Workflow node-walk + state engine (B-2, W-WF-HARDEN — the LAST ⬜ closed, ledger 42→43)** — `poc_wf_harden.js` diffs `ad_workflow.js`'s new `replay` arm against REAL iDempiere workflow traces in the live PG `idempiere_test` (11 ad_wf_process / 13 ad_wf_activity / 13 ad_wf_eventaudit written by real iDempiere; captured verbatim → `build/erp/oracle/wf_oracle.json`, never hand-authored): **`§HARDEN surface=ad_workflow fixtures=11 diff=0 oracle=iDempiere-PG-trace`** — node SEQUENCE + transitions taken + per-activity WFState/eventtype + process WFState all identical (10× wf131 BP-Approval UserWindow-suspend `[244]→OS/SC` + 1× wf116 Process_Order `[183→185→186]→CC/PX` std-user XOR transition; threaded docstatus ends CO == the live c_order row, `§HARDEN-DOC`). Definitions md5-set ad_full.db == live PG `§HARDEN-SRC kind=wf setdiff=0` (58 wf/262 node/207 next/1 cond); document context EXTRACTED (C_Order AD_Column defaultvalues DR/CO, identical both schemas). Semantics arm = `scripts/logic_oracle/WorkflowOracle.java` (the B-1 LogicOracle headless-compiled-classes technique one level up): REAL compiled `StateEngine` MUTATORS agree 6/6 (engine hops ON→OR→CC/OS applied identically, closed→open REJECTED) + std-user gate == verbatim `MWFNodeNext.isValidFor:215-243` over compiled DocAction constants (PO/Env/DB-dragging arms = named omissions). 2 §FALSIFIERs load-bearing (flip DocAction CO→'--' reroutes 183→184 on BOTH sides; dropped node 185 → LOUD CA abort, diff flags it). Small-K honesty: 7 `§HARDEN-SKIPS` lines (actions F/X/P/R/C unexercised — replay THROWS; 1 conditioned transition wf115 untraced; AND-split absent in seed 262/262 X/X; 56/58 wfs traceless; claim = "11 real processes, diff=0", not corpus-wide). `build/erp/poc_wf_harden.log`. | -| ✅ **oracle-equivalent** | **0-seed posting oracles — the 6 G-seed classes (B-3, W-POST-B3, ledger 43→49)** — `poc_post_b3.js` diffs the EXTENDED `doc_poster.derivePostings` (per-class manifests: `fa-addition`/`fa-depreciation`/`fa-reval`/`fa-transfer`/`fa-disposal`/`project-issue`, each citing its Doc_*.java lines) against a **GENERATED oracle**: the REAL compiled posters driven inside iDempiere's own OSGi test harness (`scripts/logic_oracle/PostingOracleTest.java` under `org.idempiere.test` tycho-surefire — resolves the G-3 "no headless OSGi" landmine) on a SCRATCH clone `idempiere_b3` of `idempiere_test`, over a GardenWorld-model seed (vendor FixedAssetsTest recipe; USER RULING 2026-07-17: seed INPUT prep sanctioned, facts never hand-authored). Capture = `scripts/generate_post_oracle.sh` → TEXT fixture `build/erp/oracle/post_b3_fixture.json` (34 fact rows, ΣDR=ΣCR=81360.56, both schemas 101+200000). Result: **all 6 classes `maxDiff=0c` per document × schema × (account,side)** — `§B3-POST`: A_Asset_Addition (4 docs incl. the A_CapvsExp=Exp arm-Z doc posting ZERO rows, gate=Doc_AssetAddition.java:67-72, **+ the tenant-vs-system fx-rate pick MConversionRate:243-252 the diff CAUGHT** — engine had picked the 0.8006 system rate, oracle posts the 0.85 client-11 rate, fixed) · A_Depreciation_Entry (per-depexp DR/CR; schema-200000 = the other-schema ∅ branch) · A_Asset_Reval (2 delta pairs vs the seeded A_Reval_Cost_Offset_Acct) · A_Asset_Transfer (workfile cost old→new combos; the completeIt validfrom time-slice keeps pre-transfer docs derivable) · A_Asset_Disposed (from the per-schema A_Asset_Change 'DIS' row) · C_ProjectIssue (PJ_WIP vs Product.Asset at the schema costing-method cost — the W-FOLD-MOVEMENT hop). 2 §FALSIFIERs load-bearing (gate-flip Cap→Exp → 2→0 lines; double one depexp expense → ΣDR 55556c→111112c) + the 2 triage falsifiers. The 2 ∅-by-design classes (C_BankTransfer/C_DepositBatch — no Doc_ in the factory) stay structurally CLOSED. Regressions green after the doc_poster extension: W-POST-HARDEN · W-FACTACCT-DOC · W-DOC-POSTER · W-MORDER-POST · test_report_fin 46574.97/300 rows intact. `build/erp/poc_post_b3.log` · `build/erp/generate_post_oracle.log`. | +| ✅ **oracle-equivalent** | **0-seed posting oracles — the 6 G-seed classes (B-3, W-POST-B3, ledger 43→49)** — `poc_post_b3.js` diffs the EXTENDED `doc_poster.derivePostings` (per-class manifests: `fa-addition`/`fa-depreciation`/`fa-reval`/`fa-transfer`/`fa-disposal`/`project-issue`, each citing its Doc_*.java lines) against a **GENERATED oracle**: the REAL compiled posters driven inside iDempiere's own OSGi test harness (`scripts/logic_oracle/PostingOracleTest.java` under `org.idempiere.test` tycho-surefire — resolves the G-3 "no headless OSGi" landmine) on a SCRATCH clone `idempiere_b3` of `idempiere_test`, over a GardenWorld-model seed (vendor FixedAssetsTest recipe; USER RULING 2026-07-17: seed INPUT prep sanctioned, facts never hand-authored). Capture = `scripts/generate_post_oracle.sh` → TEXT fixture `build/erp/oracle/post_b3_fixture.json` (34 fact rows, ΣDR=ΣCR=81360.56, both schemas 101+200000). Result: **all 6 classes `maxDiff=0c` per document × schema × (account,side)** — `§B3-POST`: A_Asset_Addition (4 docs incl. the A_CapvsExp=Exp arm-Z doc posting ZERO rows, gate=Doc_AssetAddition.java:67-72, **+ the fx-rate pick MConversionRate:243-252 the diff CAUGHT** — engine had picked the seed's 0.8006 twin row where the real poster picks 0.85; root cause = the twin is `IsActive='N'` and `:251` filters it (first mis-read as client-rank ordering, corrected 2026-07-18 when poc_alloc_fx §FALSIFIER-B exposed the capture-side filter masking it); `fxRate` now carries the verbatim clauses and the inactive row stays captured) · A_Depreciation_Entry (per-depexp DR/CR; schema-200000 = the other-schema ∅ branch) · A_Asset_Reval (2 delta pairs vs the seeded A_Reval_Cost_Offset_Acct) · A_Asset_Transfer (workfile cost old→new combos; the completeIt validfrom time-slice keeps pre-transfer docs derivable) · A_Asset_Disposed (from the per-schema A_Asset_Change 'DIS' row) · C_ProjectIssue (PJ_WIP vs Product.Asset at the schema costing-method cost — the W-FOLD-MOVEMENT hop). 2 §FALSIFIERs load-bearing (gate-flip Cap→Exp → 2→0 lines; double one depexp expense → ΣDR 55556c→111112c) + the 2 triage falsifiers. The 2 ∅-by-design classes (C_BankTransfer/C_DepositBatch — no Doc_ in the factory) stay structurally CLOSED. Regressions green after the doc_poster extension: W-POST-HARDEN · W-FACTACCT-DOC · W-DOC-POSTER · W-MORDER-POST · test_report_fin 46574.97/300 rows intact. `build/erp/poc_post_b3.log` · `build/erp/generate_post_oracle.log`. | -**Remaining ⬜ declarative surfaces: NONE** (`ad_evaluator` fell to B-1 W-LOGIC-HARDEN, `ad_workflow` fell to B-2 W-WF-HARDEN, 2026-06-12; the 0-seed POSTING tail fell to B-3 W-POST-B3, 2026-07-17). +| ✅ **oracle-equivalent** | **Doc_* poster tail — BankStatement + the config-∅ pair (W-POST-TAIL, 2026-07-18, ledger 49→52)** — `poc_post_tail.js` closes 3 more of the last 6 posters, each against a REAL seed oracle (no generation needed): **C_BankStatement (392)** — the `bank-statement` manifest (Doc_BankStatement.createFacts:200-280: per line {Bank.Asset}=+StmtAmt · {Bank.InTransit}=−TrxAmt · charge >0→CR/<0→DR · interest <0→InterestExp else Rev; DOC-currency legs converted per schema + the Fact.balanceAccounting CurrencyBalancing residual — the REAL 0.01 DR on schema 200000) **== the 13 posted fact rows, maxDiff=0c** per doc × schema × (account, side). **M_MatchPO (473)** — the REAL engine posted the EMPTY set for all **37** posted docs: PPV gated on StandardCosting (Doc_MatchPO.java:429), seed costs at 'A' → engine ∅ == oracle ∅ across the full population; §FALSIFIER flips costingmethod→'S' + cost≠price → the PPV path opens. **M_Requisition (702)** — 1 posted doc, ∅ by `MAcctSchema.isCreateReservation` (:662-669, commitmenttype='N'); §FALSIFIER flips 'N'→'B' → expense+commitment lines appear (the W-MORDER-POST twin). 3rd falsifier: doubling a stmt line breaks the bank diff (14850c→24650c). Additive captures only (c_bankstatementline amounts/charge, bank interest accts, c_charge_acct, m_matchpo, m_requisitionline, rate isactive) — whole posting bundle + TB 46574.97/300 re-verified green. **Named skips:** C_Cash (2 real CO docs never posted → post on scratch clone next), M_Inventory (3 drafts → complete+post next), M_Production (0 docs + no component costs → ⛔, costs not synthesized). `build/erp/poc_post_tail.log`. | -**The honest read: coverage 37🟡 ≠ equivalence.** FORTY-NINE surfaces/classes are now oracle-equivalent to the unit/cent +**Remaining ⬜ declarative surfaces: NONE** (`ad_evaluator` fell to B-1 W-LOGIC-HARDEN, `ad_workflow` fell to B-2 W-WF-HARDEN, 2026-06-12; the 0-seed POSTING tail fell to B-3 W-POST-B3, 2026-07-17; the poster tail's seed-oracled half fell to W-POST-TAIL, 2026-07-18 — 17 of 20 factory posters now fold, Cash/Inventory next, Production ⛔). + +**The honest read: coverage 37🟡 ≠ equivalence.** FIFTY-TWO surfaces/classes are now oracle-equivalent to the unit/cent (TB-read · per-document GL derivation H-1 · `completeIt` Order→Ship→Invoice chain · Doc_Payment · Doc_AllocationHdr incl. tax-correction · StorageOnHand qty · ReplenishReport PO · standalone `completeIt(C_Invoice)`+MatchInv · vendor `Doc_Invoice` purchase manifest · foreign-currency Doc_AllocationHdr · inter-org M_Movement **+ its EUR-schema diff --git a/prompts/FABLE5_B3_POSTING_ORACLE.md b/prompts/FABLE5_B3_POSTING_ORACLE.md index 84e9ad070..568a6b48c 100644 --- a/prompts/FABLE5_B3_POSTING_ORACLE.md +++ b/prompts/FABLE5_B3_POSTING_ORACLE.md @@ -266,11 +266,15 @@ sqlite, runs the extended `doc_poster.derivePostings` per class per schema, inte integer cents. `doc_poster.derivePostings` gained the 6 manifests (fa-addition/fa-depreciation/fa-reval/ fa-transfer/fa-disposal/project-issue), each line-citing its Doc_*.java source; accounts resolve a_asset_acct/c_project_acct/a_asset_change/depexp combos → `c_validcombination.account_id` (the MAccount hop). -- **THE DIFF EARNED ITS KEEP (a real catch)** — first diff run: engine converted the addition's schema-200000 leg - at the 0.8006 SYSTEM rate; the real poster posts the 0.85 CLIENT-11 rate — `MConversionRate.getRate:243-252` - orders `AD_Client_ID DESC` (tenant rate outranks system). `fxRate` fixed to the verbatim pick; also the - Doc_AssetAddition currency finding: it is the ONLY B-3 poster that posts DOC-currency amounts (converted per - schema) — the other five pass the schema's own currency. +- **THE DIFF EARNED ITS KEEP (a real catch; cause CORRECTED 2026-07-18)** — first diff run: engine converted + the addition's schema-200000 leg at 0.8006; the real poster posts 0.85. Initially attributed to + `AD_Client_ID DESC` ordering ("tenant beats system") — WRONG: both USD→EUR rows are client 11; the live + discriminator is `MConversionRate.getRate:251`'s **`IsActive='Y'`** clause (the 0.8006 twin is inactive). + Caught the day after by poc_alloc_fx §FALSIFIER-B when a capture-side isactive filter vacuated its + wrong-rate prop. Final state: the inactive row is CAPTURED (isactive column added), and `fxRate` carries + the verbatim `:243-252` clauses (BETWEEN valid dates, IsActive, client/org scope + ordering). Also the + Doc_AssetAddition currency finding stands: it is the ONLY B-3 poster that posts DOC-currency amounts + (converted per schema) — the other five pass the schema's own currency. - **§W-4 semantics arm** — no separate PostingOracle.java stdin-driver needed: the OSGi test harness IS the semantics arm (real Doc.getAccount/MAssetAcct/MConversionRate/MCost resolution ran inside the real engine). Named infra findings (in the card's §W-2 EXECUTION SPEC block): AD-dictionary-vs-code type skew (workfile diff --git a/prompts/HARDEN_MATRIX.md b/prompts/HARDEN_MATRIX.md index 915123612..440169ff4 100644 --- a/prompts/HARDEN_MATRIX.md +++ b/prompts/HARDEN_MATRIX.md @@ -100,6 +100,43 @@ own OSGi test harness over a GardenWorld-model seed on a scratch clone (`scripts `build/erp/oracle/post_b3_fixture.json`; USER RULING 2026-07-17 sanctioned seed-INPUT prep). `derivePostings` gained the 6 per-class manifests. Matrix ledger = **49 oracle-equivalent**. Log: `build/erp/poc_post_b3.log`. +### W-POST-TAIL — the LAST 6 Doc_* posters (spec 2026-07-18, Fable 5; sequel to B-3) +The 20-poster factory now folds 14. The remaining 6 decompose by SEED REALITY (facts verified live 2026-07-18): +- **C_BankStatement (392)** — REAL oracle in the seed: 13 fact rows for statement 100 (2 lines). Manifest + (Doc_BankStatement.createFacts:200-280): per line DR/CR {Bank.Asset}=+StmtAmt · {Bank.InTransit}=−TrxAmt · + charge leg (>0→CR / <0→DR negate, {Charge.Expense}) · interest leg (<0→{Bank.InterestExp} else InterestRev, + −InterestAmt); clearing-equal branch n/a (asset 258/200052 ≠ intransit 257/200053, IsPostIfClearingEqual=Y); + DOC-currency legs → per-schema conversion + the Fact.balanceAccounting CurrencyBalancing residual (724, the + W-FOLD-ALLOC-FX rule — the 0.01 DR on schema 200000 in the real rows). Capture: additive widening of + c_bankstatementline (+trxamt/chargeamt/interestamt/c_charge_id) + c_bankaccount_acct (+interest accts). +- **M_MatchPO (473)** — the REAL engine posted the EMPTY SET 37 times (37 docs posted='Y', 0 fact rows): + PPV block gated on COSTINGMETHOD_StandardCosting (Doc_MatchPO.java:429) and GardenWorld costs at 'A' Average + → ∅ is CONFIG-derived. Manifest = ∅ under 'A'; §FALSIFIER flips costingmethod→'S' → the PPV path opens. + Additive capture: m_matchpo. +- **M_Requisition (702)** — 1 posted doc, 0 facts: Doc_Requisition.createFacts:130 gates on + MAcctSchema.isCreateReservation (commitmenttype 'B' POCommitmentReservation / 'A' POSOCommitmentReservation, + MAcctSchema.java:662-669; GardenWorld='N') → ∅ CONFIG-derived + (the W-MORDER-POST twin). §FALSIFIER flips commitmenttype→'B' → per-line {Product.Expense}=AmtSource + + CommitmentOffset. Additive capture: m_requisitionline. +- **C_Cash (407)** — 2 real docs CO but NEVER posted (posted=N, 0 facts) → post the EXISTING docs on a scratch + clone (B-3 generator reuse, zero seed prep) → capture → fold. NEXT SESSION unless this one has room. +- **M_Inventory (321)** — 3 real DRAFTS → complete+post on scratch clone (same move). NEXT SESSION likewise. +- **M_Production (325)** — 0 docs AND component m_cost absent (the W-FOLD-PRODUCTION named-deferral) → stays + honestly ⛔ until a costed BOM seed exists; do NOT synthesize costs. +Witness: `scripts/poc_post_tail.js` (W-POST-TAIL) — BankStatement per-doc/per-schema integer-cent diff vs +fact_acct(392); MatchPO ∅==∅ over ALL 37 real docs + gate-flip; Requisition ∅==∅ + gate-flip; ≥2 falsifiers +load-bearing. Extract bundle regressions must stay green after the additive widening. + +**✅ W-POST-TAIL first half DONE 2026-07-18 (same session as the spec):** `🟢 W-POST-TAIL PASS` — +`§TAIL-POST C_BankStatement … maxDiff=0c oracle=real-fact_acct(392)` (13 rows incl. the schema-200000 +conversion + 0.01 CurrencyBalancing residual) · `§TAIL-POST M_MatchPO docs=37 … ∅-by-config` · +`§TAIL-POST M_Requisition … ∅-by-config` · 3 falsifiers load-bearing (req-flip N→B opens · mpo-flip A→S ++cost-scale opens · bs-scale 14850c→24650c). **Correction banked:** the B-3 fx-rate story was IsActive +(:251), not client-rank — the inactive 0.8006 row is now CAPTURED and `fxRate` filters it verbatim +(poc_alloc_fx §FALSIFIER-B prop restored, whole bundle re-green). Matrix ledger = **52 oracle-equivalent**; +17/20 factory posters fold. Remaining: Cash (post 2 real CO docs on clone) · Inventory (complete 3 real +drafts on clone) · Production ⛔ (no docs, no component costs). Log: `build/erp/poc_post_tail.log`. + ### H-3 Spot-harden the declarative engines `ad_evaluator`/`ad_access`/`ad_valrule`/`ad_reference` are 🟡 on parse; oracle-diff a SAMPLE of each against `GridField`/`MRole`/`MValRule` outputs — confirm the verdict matches, not just that it parses. The master-data diff --git a/prompts/Modeller/DISC_Walker/OCCUPANT_PATHFINDER.md b/prompts/Modeller/DISC_Walker/OCCUPANT_PATHFINDER.md index 121c05148..38c099b77 100644 --- a/prompts/Modeller/DISC_Walker/OCCUPANT_PATHFINDER.md +++ b/prompts/Modeller/DISC_Walker/OCCUPANT_PATHFINDER.md @@ -279,3 +279,135 @@ storey's room rects + wall-adjacent walkable band; when illegal, detour via inte waypoints (visibility-graph over door centers, edges only where the segment is legal). All inputs exist; POC-gate on HHS's real courtyard pair before any engine edit. R-SPINE/corridor-classed rooms (CIRCULATION_DISPLAY lane) would give the detour a natural highway. + +--- + +# OPEN LANE C — "walk the corridor, not room-to-room" (user, 2026-07-17, live Clinic Path) +User live case (Clinic, First→Second Floor, 12 doors 85.1m): the path THREADS room→room→room through +a row of interior doors instead of walking the corridor past a glass door. User's principle (verbatim +intent): "walking thru rows of doors room to room should be avoided as illogical when only a glass door +is in between… a room to adjoining room when corridor is next to it should be a RED FLAG." Cross-ref +`VIEWER_FIND_PANEL_ROOM_ACCURACY.md §9` (the two findings + the live GUID card `Storey: Unknown`). + +**This is exactly what the occupant graph was built for (E2 room↔CIRC) — it just isn't firing here.** +Two measurable causes, both small, both on top of the SHIPPED E2 machinery. POC-GATE FIRST per this +file's law — measure on Clinic (the case) + Duplex (the no-regression control) BEFORE any engine edit. + +### C1 — ENABLER: rescue `Storey='Unknown'` doors by center_z (so the corridor edge EXISTS) +`room_graph.js:309` binds doors to `CIRC::` reading `m.storey` RAW. Curtain-wall glass doors +(the corridor's actual openings) carry `storey='Unknown'` — their placement is relative to the +null-transform `IfcCurtainWall` parent, so extraction never stamped a storey (Clinic: 3 of 6 +`M_Curtain Wall *Glass` IfcDoors are Unknown; live card confirmed). An Unknown-storey door rescues onto +NO CIRC node → the corridor route is absent → Dijkstra can only use E1. Fix = reassign Unknown-storey +doors to a storey by `center_z` (reuse the room-walker's `§STOREY-Z` `_assignByZ` convention — same +building already has the per-storey z-anchors) BEFORE the E2 binding loop. Purely additive; a door that +already has a real storey is untouched. + +### C2 — COST MODEL: fewest VALID doors over a small candidate set (user's design, 2026-07-17) +SUPERSEDES the earlier "tune a λ penalty on E1" sketch — the user gave the actual metric, and it needs +no tuning. Verbatim intent: "graphing should be 2 or more ahead… keep an array of outcomes, right away +choose the least doors (with connecting rooms) but valid"; then "if point to point, keep array of few +possible routes then compare which has highest confidence — not 2 and above steps but ALL the steps +along the way captured before deciding… fast array maths." +- **Metric = number of DOORS crossed (room transitions), NOT distance.** Each door/room-transition + edge costs 1; corridor/spine traversal (CIRC↔CIRC, spine chaining) costs 0 doors. So the 12-door + room-thread scores 12; the corridor route (enter + glass door + exit) scores ~3. Least-doors ⇒ + corridor wins BY CONSTRUCTION — no λ, nothing to tune. +- **Candidate-set, not single greedy path.** Enumerate a FEW full candidate routes (k-shortest-style), + each captured as the COMPLETE array of steps, THEN compare — not a greedy 2-step lookahead. Score = + "confidence" = fewest doors among VALID routes (a route is valid iff every leg actually connects and + is legal — ties into OPEN LANE B's chord-legality). Pick highest confidence. Cheap: a handful of + candidates, array math. +- **Why door-count is the right invariant:** a human walks the corridor and enters each destination + room by ONE door; they do not punch through a party wall into the next private room and out its far + door. Minimising doors encodes exactly "rooms hang off the corridor, they don't chain into each + other" — the user's red flag, as a count instead of a weight. +- **Regression safety is automatic:** Duplex rooms are door-connected with no corridor, so the + fewest-doors route == the only route == today's route ⇒ the DONE-section's 26-pair invariant holds by + construction (a corridor alternative has to EXIST to change anything). Confirm, don't assume. +- Implementation note: `shortestPath()` already runs Dijkstra over weighted edges — a door-count mode is + just unit weight on door/room edges + zero on corridor-internal hops, optionally returning the top-k + candidates for the confidence compare. API-compatible (add an `opts.metric='doors'`), zero consumer + edits, per this file's law. + +### POC GATE (calculation-only, before touching the engine — this file's standing law) +Node script on Clinic + Duplex, building C1's rescued-door set + C2's penalised weights, logging: +``` +§POCPATH-C Clinic unknown_doors_rescued= corridor_reachable_pairs= (was ) +§POCPATH-C Clinic sample R1->R? hops_before=[room,door,room,door,...] hops_after=[room,doorwp,CIRC,doorwp,room] doors_before= doors_after= +§POCPATH-C Duplex regression_pairs=26 mismatches= # MUST be 0 at the chosen λ +§POCPATH-C λ_sweep λ=1.0->threaded λ=->corridor-dominant +``` +Acceptance: the Clinic sample route changes from room-threading to corridor-dominant (fewer interior +door hops, a CIRC hop appears) AND Duplex mismatches=0. If C1 alone already flips the sample (corridor +now shorter), C2 may be unnecessary — measure C1-only first, add C2 only if the thread survives. STOP +and report if Duplex regresses at every λ that fixes Clinic (means the penalty model is wrong, not the λ). + +### POC BASELINE (2026-07-17, calculation-only, `poc_lane_c_baseline.js`, `~/bim-ootb/buildings/Clinic_extracted.db` 118 rooms) +Measured the SHIPPED graph before any edit — this REFOCUSES the lane on C2, not C1: +- Clinic graph today: **E1(room↔room)=124, E2(room↔circ)=106**, spine=41, circ=3, stairwp=4, plus + E5=49/E6=17/E7=17/E9=16/E3=2/E8=1 (the backbone model evolved well past the DONE-section's E1–E4). + The corridor route ALREADY EXISTS in the graph — so the 12-door thread is a WEIGHTING outcome + (Dijkstra taking short E1 hops), not a missing-edge outcome. +- **C1 is minor here: only 5 Unknown-storey doors total** — 3 curtain-wall glass (z≈1.02–1.06 → cleanly + reassign to First Floor, Δz≈1.1, the expected door-center-below-wall-center offset) + 2 "Chain Link" + fence gates (z≈−0.06, exterior). C1 rescues ≤3 corridor-relevant edges. Do it (cheap, correct) but it + is NOT what fixes the thread. +- **⟹ C1 + C2 together** (measured, see C1 result below). C1 puts the corridor route on the table at + the glass door; the fewest-doors metric (C2, above) makes the router pick it. + +### POC C1 RESULT (2026-07-17, `poc_lane_c1_fast.js`, union-find, no all-pairs) +Rescued the 3 Unknown-storey glass IfcDoors → `storey='First Floor'` (by center_z), rebuilt: +- BASELINE: components=**1** largest=180 edges=332. AFTER-C1: components=1 largest=181 edges=**335 (+3)**. +- The graph is ALREADY one connected component — so the corridor is NOT globally severed (every room + was reachable); the honest refinement of the user's "corridor severed here" read is that the DIRECT + corridor route THROUGH the glass door was missing, not basic reachability. +- The 3 rescued glass doors bind straight to the **corridor SPINE**: 2× `room↔spine` (E2) + 1× + `doorwp↔spine` (E7). So C1 correctly adds the local corridor option at the glass opening. +- CONCLUSION: C1 is necessary (adds the corridor edge at the glass door) but NOT sufficient alone — with + distance-Dijkstra the short room-thread can still win. The fewest-doors metric (C2) is what makes the + now-available corridor route get chosen. Both, as the user designed. +- NEXT POC (not yet built): implement the door-count metric (`opts.metric='doors'`, unit weight on + door/room edges, 0 on corridor-internal hops), run the screenshot's sample pair on C1'd Clinic, show + hops collapse from the room-thread to a corridor-dominant route; then Duplex 26-pair regression = 0. + +### C3 — DOOR STRATEGY TABLE + angle tie-breaker (user refinement, 2026-07-17) +User's better framing of C2: don't reduce to one number — score each candidate route with a +door-PRIORITY table so the confidence reflects WHICH doors it opens, not just how many. Base = C2 +door-count; the table modulates per-door cost/confidence: +- **Semantically grounded, NOT hand-tuned (project anti-hardcoded-threshold rule — cf. the rejected + `DOOR_RESCUE_MIN_AREA` buffer).** Scores derive from real IFC signals: + - door HOST class: `IfcCurtainWall`-hosted (glass storefront) = TRANSIT connector between corridor + segments → HIGH priority (cheap to open). + - ADJACENT ROOM privacy, read from the SHIPPED room-type classifier (feat/room-restroom-colour, + now live): corridor/lobby/circulation = PUBLIC (cheap); bedroom/restroom/office = PRIVATE (a door + INTO one is expensive — you don't cut through a private room to transit). This REUSES the room + classifier already deployed, not a new invented signal. +- **Angle/collinearity = TIE-BREAKER only, not primary.** Prefer the door that continues the approach + direction in a straight line (corridor continuation) — but only to break ties between similarly-scored + candidates; as a primary driver it breaks on L/curved corridors (the straight line leaves the + corridor). Layer it above door-count + the strategy table. +- Confidence(route) = f(door-count, Σ door-priority, collinearity tie-break) over the small candidate + set; still "fast array maths," still deterministic. + +### C4 — GRACEFUL DEGRADATION tiers (user, "lacking infra — follow the wall / finished floor") +Separate, larger robustness lane — do NOT entangle with the glass-door fix. A "strategy selector at +onset": assess what the building actually models, THEN pick the routing layer: + walls+doors+corridor-spine (best) → doors-only → **floor-slab (IfcSlab) adjacency when no walls are + modelled yet** (walk the finished floor) → raw-geometry adjacency (worst). +Real need: many IFC models are incomplete (no walls, ARCH not detailed). Each tier is its own POC-gated +build; the onset selector picks the highest tier the data supports so an incomplete model still routes. +Parked as a roadmap lane, not part of C1–C3. + +### C5 — ACQUIRED DOORS: an open space IS a door (user, 2026-07-17) +A "door" (graph node / yellow dot) is any TRAVERSABLE OPENING between walkable spaces, not only a +physical `IfcDoor`. Open-plan transitions — corridor↔foyer, room↔corridor with no door leaf, an open +passage between corridor segments — carry no `IfcDoor`, so the current door-only graph is blind to them. +Fix: ACQUIRE a door node at any wall-free boundary shared by two walkable regions. This is the INVERSE +of the walker's `§ROOM-FORM` open-perimeter measure (boundary metres NOT backed by a raw wall) — reuses +existing machinery, deterministic, invents nothing. Guard against noise: min passage width (~0.8m, +`_calibrate`) + exclude window openings and sub-`NOISE_FLOOR` slivers. Acquired doors are ordinary nodes +and inherit the strategy table's PUBLIC/transit priority (they open onto corridor/foyer), scoring cheap +like a curtain-wall transit door. Also the enabler for the C4 floor-slab tier: with no walls modelled, +acquired doors (openings in the floor-slab boundary) are the PRIMARY connectors. Encoded in +`path_strategy.json` → `door_acquisition.sources.acquired`. diff --git a/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md b/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md index 34c53cb79..f27d82bb3 100644 --- a/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md +++ b/prompts/Modeller/DISC_Walker/VIEWER_FIND_PANEL_ROOM_ACCURACY.md @@ -607,3 +607,40 @@ Verified in `~/bim-ootb/viewer/navigate_find.js` on the current branch: W-ROOM-CATEGORY-COLOUR on Duplex: names 21/21, 0 cross-match, synthetic-rule 6/6. Detection confirmed corridors were all one uniform blue and restrooms were purple (habitable) before this. Still open: corridor main-vs-minor differentiation (needs a graph-centrality signal — deferred). + +## §9 — CURTAIN-WALL GLASS DOOR: two findings from a live Clinic Path case (2026-07-17, user, NOTED not fixed) +User drove a First-Floor→Second-Floor Path on Clinic (12 doors, 85.1m) through a glazed corridor +storefront. Two bugs + one routing PRINCIPLE, all confirmed against code+data (Clinic_extracted.db). +Both roots trace to curtain-wall glass being a special assembly. **Per user: NOTE only, do NOT fix yet.** + +### Finding A — glass panels not selectable (picking, `viewer/picking.js:256`) +`if (h.object.material && h.object.material.opacity < 0.3) continue;` discards every raycast hit +under opacity 0.3 BEFORE guid resolution. Curtain-wall glass is `IfcPlate` panels whose material +opacity = the IFC colour alpha (`streaming.js:405`) = **0.10 / 0.25** in Clinic → thrown away → the +click falls through to the opaque wall behind (or nothing). Solid `IfcDoor` = alpha 1.0 → picks fine. +Live proof: the user COULD click one curtain-wall glass door — because that specific `IfcDoor` +(`M_Curtain Wall Sgl Glass:…:283068`, GUID `0bpfxAz3XBrAx$RjFuHvgA`) happens to have material alpha +`1.000`; the 0.10-alpha IfcPlate glazing beside it stays unpickable. Fix when prioritized: make the +skip INTENT-based (skip `_isOutline`/ghost/bbox via userData flags), not a blanket opacity threshold — +because the 0.3 gate is also what lets you pick THROUGH x-ray-dimmed geometry (all drops to ~0.3 under +x-ray, `streaming.js:594`), so glass and dimmed background are indistinguishable by opacity alone. +This is the design fork to resolve before touching it. + +### Finding B — Path avoids the glass door → threads room→room instead (`common/room_graph.js:309`) +The door query binds doors to a PER-STOREY corridor spine (`SPINE::`/`CIRC::`), reading +`m.storey` RAW. Curtain-wall glass doors carry **`storey='Unknown'`** (placement is relative to the +null-transform `IfcCurtainWall` parent, so extraction never assigned a storey — confirmed: Clinic has +6 `M_Curtain Wall *Glass` IfcDoors, 3 First Floor + **3 Unknown**; the live card above showed +`Storey: Unknown`). An Unknown-storey door binds to NO spine → no edge → the router cannot cross the +glazed opening and detours through the solid-door rooms. `room_graph.js` does NOT apply the room-walker's +`§STOREY-Z` z-reassignment (assign Unknown→storey by `center_z`) that would rescue these. Fix when +prioritized: reassign Unknown-storey doors by `center_z` before binding (reuse the walker convention). + +### PRINCIPLE (user, the real point) — corridor-preference is a routing-quality invariant +Walking through ROWS OF DOORS room→room→room is illogical when a glass door / corridor sits between the +endpoints — the path should PREFER corridor circulation over cutting through adjoining rooms. A +room→adjoining-room transition WHEN a corridor is next to it is a **RED FLAG** (route smell), not a valid +leg. Finding B is the enabling bug (the corridor route is literally absent from the graph, so the router +has no choice), but even with all doors present the cost model should penalise room↔room transitions where +a corridor edge is available, so circulation is the default spine and rooms are leaves off it. This is the +design target for the eventual fix, beyond just rescuing the Unknown-storey doors. diff --git a/prompts/Modeller/DISC_Walker/path_strategy.json b/prompts/Modeller/DISC_Walker/path_strategy.json new file mode 100644 index 000000000..1b6e359c8 --- /dev/null +++ b/prompts/Modeller/DISC_Walker/path_strategy.json @@ -0,0 +1,88 @@ +{ + "$schema_note": "Occupant path strategy — declarative, deterministic door-priority + confidence model for common/room_graph.js shortestPath(). Numbers marked _calibrate are INITIAL values to be tuned by POC on real buildings (Clinic case + Duplex control), NOT invented finals; the STRUCTURE (curtain-wall=transit, private-room=expensive, floor-fallback) is semantic and fixed. Spec: OCCUPANT_PATHFINDER.md OPEN LANE C. Copyright (c) 2025-2026 Redhuan D. Oon — MIT.", + "version": 1, + "updated": "2026-07-17", + + "metric": { + "base": "door_count", + "note": "Primary cost = number of doors/room-transitions crossed, NOT euclidean distance. A human enters each destination room by ONE door and does not punch through private rooms to transit.", + "corridor_traversal_cost": 0.0, + "note_corridor": "Travel ALONG circulation (CIRC<->CIRC, spine chaining) costs zero doors — only door/room-transition edges are counted." + }, + + "candidate_selection": { + "note": "Point-to-point: enumerate a few full candidate routes (k-shortest by base metric), score each whole route by confidence, pick the best. 'Fast array maths' — a handful of candidates, not a single greedy path.", + "k_candidates": 5, + "_calibrate_k": true, + "select": "min_confidence_cost", + "validity_required": true, + "validity_note": "A candidate is valid only if every leg actually connects AND is legal (chord stays inside walkable space — ties to OPEN LANE B chord-legality). Invalid candidates are discarded before scoring." + }, + + "door_acquisition": { + "note": "A 'door' (graph node / yellow dot) is any TRAVERSABLE OPENING between walkable spaces, NOT only a physical IfcDoor. Open-plan transitions (corridor<->foyer, room<->corridor with no door leaf, an open passage between corridor segments) are ACQUIRED doors — otherwise the graph is blind to open-plan circulation.", + "sources": { + "physical": { + "signal": "IfcDoor element (with transform)", + "note": "explicit door leaf — the current door set" + }, + "acquired": { + "signal": "wall_free_boundary_between_two_walkable_regions", + "detail": "A boundary segment shared by two distinct walkable regions (room/corridor/foyer) that is NOT backed by a raw wall = an open passage. INVERSE of the room-walker's §ROOM-FORM open-perimeter measure (metres of region boundary not backed by wall) — reuses existing machinery, deterministic, invents nothing. Place an acquired-door node (yellow dot) at the gap midpoint; it becomes an ordinary graph node.", + "min_passage_width_m": 0.8, + "_calibrate": true, + "exclude": [ + "window openings (IfcWindow / opening not at floor level)", + "rasterization slivers below the walker NOISE_FLOOR (grid artefact, not a passage)" + ], + "note_c4": "When a building models NO walls (degradation tier floor_slab), acquired doors (openings in the floor-slab boundary) become the PRIMARY connectors — this is what makes the incomplete-model tiers route at all." + } + } + }, + + "door_priority": { + "note": "Per-door cost multiplier on the base door-count. Grounded in REAL IFC signals, never hand-labelled. Factors multiply. Applies to BOTH physical and acquired doors — an acquired open threshold onto a corridor/foyer is naturally PUBLIC/transit and scores cheap via by_adjacent_room_privacy, exactly like a curtain-wall transit door.", + + "by_host_class": { + "note": "The door's HOST element class (from IfcRelVoidsElement / containment). Curtain-wall-hosted doors are transit connectors between corridor segments.", + "IfcCurtainWall": { "role": "transit", "cost_mult": 0.5, "_calibrate": true, "note": "glass storefront door = corridor continuation, PREFER it" }, + "IfcWall": { "role": "normal", "cost_mult": 1.0 }, + "default": { "cost_mult": 1.0 } + }, + + "by_adjacent_room_privacy": { + "note": "Read the privacy of the room a door OPENS INTO, from the SHIPPED room-type classifier (room_habitability.js classifyRestroom/Kitchen/Bedroom + corridor labels — feat/room-restroom-colour, now live). Reuses deployed signal, no new invention.", + "public": { "cost_mult": 0.5, "_calibrate": true, "room_categories": ["corridor", "lobby", "circulation", "foyer"], "note": "cheap to transit through" }, + "semi": { "cost_mult": 1.0, "room_categories": ["habitable", "kitchen", "utilities"] }, + "private": { "cost_mult": 3.0, "_calibrate": true, "room_categories": ["bedroom", "restroom", "office"], "note": "expensive — do NOT cut through a private room to reach elsewhere" } + } + }, + + "tiebreak": { + "note": "Applied ONLY to break ties between similarly-scored candidates. Never a primary driver — collinearity fails on L/curved corridors.", + "collinearity": { + "enabled": true, + "weight": 0.1, + "_calibrate": true, + "note": "Prefer the door that continues the approach direction in a straight line (corridor continuation). Measured as the angle between the incoming segment and the outgoing door's opening direction." + } + }, + + "storey_rescue": { + "note": "C1 enabler — bind doors that carry storey='Unknown' (curtain-wall glass doors inherit no storey because their IfcCurtainWall parent has a null transform) to a real storey BEFORE graph binding.", + "enabled": true, + "signal": "center_z_nearest_wall_anchor", + "detail": "Assign an Unknown-storey door to the storey whose mean wall center_z is nearest the door's center_z (the room-walker's own §STOREY-Z / _assignByZ convention). POC-verified: Clinic's 3 glass doors -> First Floor, binding straight to the corridor spine (2x room<->spine E2, 1x doorwp<->spine E7)." + }, + + "degradation_tiers": { + "note": "OPEN LANE C4 — 'strategy selector at onset'. Assess what the building actually models, then route on the HIGHEST tier the data supports, so an incomplete IFC (no walls / undetailed ARCH) still routes. Each tier is its own POC-gated build; separate lane from C1-C3.", + "select": "highest_supported", + "tiers": [ + { "tier": 1, "name": "walls_doors_spine", "requires": ["IfcWall", "IfcDoor"], "walkable": "corridor_spine", "note": "best — full occupant graph as today" }, + { "tier": 2, "name": "doors_only", "requires": ["IfcDoor"], "walkable": "door_adjacency" }, + { "tier": 3, "name": "floor_slab", "requires": ["IfcSlab"], "walkable": "finished_floor", "note": "no walls modelled yet -> walk the finished floor surface" }, + { "tier": 4, "name": "raw_geometry", "requires": [], "walkable": "element_adjacency", "note": "worst-case fallback" } + ] + } +} diff --git a/prompts/PHOTOREAL_STILL_RENDER.md b/prompts/PHOTOREAL_STILL_RENDER.md index 92d41cd3a..79fc11295 100644 --- a/prompts/PHOTOREAL_STILL_RENDER.md +++ b/prompts/PHOTOREAL_STILL_RENDER.md @@ -2385,3 +2385,21 @@ pushed**, per the standing localhost-only push-pause. Next session: real-GPU non verification on Hospital or HHS_Office_Federated (the two buildings this file's earlier photoshoot work already established as stress-tests), then push/PR once the user lifts the pause or names the breakthrough. + +## REMARK (2026-07-17) — §TM_GI_HOLD_CAMGUARD, a THIRD instance of this file's own ghost family +User-reported live-production ghosting ("the usual ghosting from Alt-S/G", repro: TM on/off then +orbit; also seen after Clash panel use) traced to the N8AO 300ms hold-converge loop (PR #837, +shipped hours earlier, its own commit noting "live-eyeball of the sharpen pending" — never actually +human-verified before this report landed). Root cause: identical to the two ghost fixes already on +record in this file — TAA still-refine's `§STILL_REFINE_RESTART` and SSGI's +`§SSGI_CONVERGE_CAMGUARD` (PR #816, "ghosted/doubled geometry and see-through floors") — a +multi-frame accumulation loop with no camera-pose check, blending frames across a moving view. The +hold-converge loop inherited neither guard. Fixed by porting the same pose-signature-restart +pattern (`effects_gi_poc.js`'s `_ssgiCamSig()` shape) into `_giScheduleHoldConverge`'s step loop — +see `time_machine.js` `§TM_GI_HOLD_CAMGUARD`. Shipped: PR #848, `fix/tm-gi-hold-camguard`, squash +auto-merge armed pending CI. The Clash-panel correlation is very likely the SAME latent ghost +persisting on screen from an earlier TM/Alt-S/Alt-G interaction, not a second bug in Clash itself — +Clash's own code has zero references to the composer/GI pipeline (checked against the deployed +file directly). **Pattern worth remembering for any FUTURE accumulation loop added to this +composer stack: it needs the pose guard from day one, not after a live report** — this is the third +time the same fix has had to be ported in after the fact. diff --git a/prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md b/prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md new file mode 100644 index 000000000..7e8e3d7de --- /dev/null +++ b/prompts/RESUME_ERP_POST_TAIL_AND_USERGUIDE.md @@ -0,0 +1,113 @@ +# ⚠ DO NOT REMOVE — Scope guard / SONNET FOLLOW-THROUGH CARD (written by the Fable-5 session, 2026-07-18) +# Scope: TWO bounded lanes, worked WORK-TO-ZERO in order. §A finishes the Doc_* posting tail (2 classes, +# machinery already built and proven — this is execution, not research). §B writes the ERPUserGuide's +# high-level navigation + the CORE S&D standard-flow chapter (Sales Order → replenishment → PO → shipment +# → final accounts → financial reporting). Leave every addon lens (POS/Kitchen/WH/Tenancy/BIM-4D/Ninja) +# as-is — §B is core-ERP only, per the user 2026-07-18. +# READ THE LOG after every run (exit ≠ evidence): every poc_* via `bash build/erp/run_witness.sh scripts/poc_X.js`. +# Honour this preamble until every item below is ✅ or ⛔. +# +# GIT: start from FRESH `origin/master` (`fable/meshdb-livewire` was squash-merged twice — PR #44/#45 — +# re-using it collides). Push permission is ON (CLAUDE.md 2026-07-17): commit, push, PR, merge — the +# `system-is-real` CI check is a KNOWN pre-existing red-X (memory `project_ci_system_is_real_red_x`), +# NOT yours and NOT required; do not chase it. + +## WHERE THIS PICKS UP (state as of 2026-07-18, all on master) +- **52 surfaces/classes oracle-equivalent** (`docs/internal/ERP_COVERAGE_MATRIX.md` headline). 17 of the + 20 `org.compiere.acct` factory posters fold at `maxDiff=0c`. The last three: C_Cash, M_Inventory + (both §A here), M_Production (⛔, stays). +- The B-3 machinery is BUILT and reusable: `scripts/generate_post_oracle.sh` (scratch-clone → OSGi-hosted + posting → capture → drop) + `scripts/logic_oracle/PostingOracleTest.java` (the vendor `org.idempiere.test` + tycho harness) + `scripts/capture_post_b3_fixture.js` + the diff pattern in `scripts/poc_post_b3.js` / + `scripts/poc_post_tail.js`. Cards: `prompts/FABLE5_B3_POSTING_ORACLE.md` (read its §W-2 EXECUTION SPEC + block — every infra landmine already named) + `prompts/HARDEN_MATRIX.md §W-POST-TAIL`. +- Witness bundle green: poc_post_b3 · poc_post_tail · poc_post_harden · poc_factacct_doc · poc_doc_poster · + poc_morder_post · poc_alloc_fx · poc_money_post · poc_matchinv_fx · poc_gljournal · test_report_fin + (TB 46574.97 / 300 rows). + +## §A — finish the posting tail (2 classes; the generator does the heavy lifting) +Both classes have REAL seed documents that were simply never posted — so unlike B-3 there is NO seed +authoring at all: drive the REAL engine over the REAL rows on a scratch clone, capture, diff. + +### A-1 C_Cash (ad_table_id 407) — post the 2 existing CO cash journals +- Facts (verified 2026-07-18): 3 `c_cash` docs client 11, 2 at `docstatus='CO'`, ALL `posted='N'`, 0 fact rows. +- Extend `PostingOracleTest.java` (or add a sibling test class next to it — the generate script rsyncs + whatever is in `scripts/logic_oracle/` by name) with a step that does NOT create anything: + load each CO `MCash`, run `DocManager.postDocument(ass, MCash.Table_ID, id, true, false, trxName)`, + assert posted, `commit()`. Reuse `driveCO`'s posting half — the docs are already CO, do NOT re-processIt. +- Read `Doc_Cash.createFacts` FIRST (251 lines, org.compiere.acct) and write the manifest spec into + HARDEN_MATRIX §W-POST-TAIL before coding: expect per-cashline legs vs `{CashBook.Asset}` / + `{CashBook.CashTransfer}` / charge/expense variants — cite lines, don't guess. glassbowl already + carries `c_cash`, `c_cashline` (amount), `c_cashbook_acct` (cb_asset/cb_cashtransfer/cb_receipt). +- Capture: extend `capture_post_b3_fixture.js` (or a small tail-fixture twin) with fact_acct(407) + any + missing cashbook acct columns (ADDITIVE only). Manifest → `doc_poster.js` (`cash` basis) + diff band in + `poc_post_tail.js` (same per-doc × schema × (account,side) integer-cent shape). maxDiff=0c gate. + +### A-2 M_Inventory (ad_table_id 321) — complete + post the 3 existing draft physical inventories +- Facts: 3 real `m_inventory` DRAFTS client 11 (0 CO, 0 posted). In the test: load each MInventory, + `processIt(CO)` via the engine (the internal postIt fires — CLIENT_ACCOUNTING='I'), assert + posted, `commit()` per doc (the per-step-commit lesson: null-trx readers must see rows). +- Read `Doc_Inventory.createFacts` (523 lines) first — expect per-line DR/CR {Product.Asset} vs + {Warehouse.Differences}|{Product.InventoryClearing} at the product cost (the W-FOLD-MOVEMENT cost hop: + schema costingmethod → m_costelement → m_cost.currentcostprice). Charge-variant lines cite their branch. +- Gotchas already solved once — reuse, don't rediscover: completion may hit NOT-NULL-no-default columns + (query information_schema first, the B-3 lesson) and period checks (auto period control is ON, ±100d, + so current-date completion is fine; the DRAFTS carry old movementdates — if `testPeriodOpen` rejects + them, set MovementDate/DateAcct to now BEFORE processIt and NAME that as seed-date normalization in + the § log — it is input prep, not fact authoring). +- Same capture/manifest/witness flow as A-1. If a draft genuinely cannot complete (data invalid), + ⛔ it BY NAME with the engine's own error and move on — 2/3 posted is an honest result. + +### A-3 M_Production — ⛔ stance (do not reopen) +0 documents AND no component `m_cost` rows (the W-FOLD-PRODUCTION named deferral). Do NOT synthesize +costs. Only a future costed-BOM seed reopens this. Leave the ⛔ line in the matrix as-is. + +### §A bank + regressions (non-negotiable) +- Re-run the WHOLE bundle listed above + poc_post_tail; all exit 0, logs READ. +- Bank: matrix row edit (ledger 52→54 if both land, headline count word), HARDEN_MATRIX §W-POST-TAIL + DONE line, PROGRESS.md archive line. Every claim = a § line (Watchdog protocol). + +## §B — ERPUserGuide: high-level navigation + the core S&D flow (docs/ERPUserGuide.md) +The guide today (1186 lines) is entry-points + POS/BIM/Ninja addons; the CORE trade cycle has no +walkthrough and there is no top-level map. Two sections to write, addons untouched: + +### B-1 "The lay of the land" — high-level navigation (insert right after Quick start) +One screenful: the mental model in this order — + bubbles front door → Login/tenant → the Bottom Pill Bar (link §3) → Windows/Tabs/Fields (link §4/§5) + → the Process Button (link §6) → where documents live (Sales Order / Purchase Order / Shipment / + Invoice / Payment windows) → where the books live (Posting Preview, Trial Balance, Financial Reports §8). +Rules: navigation ONLY (what is where and why), no feature marketing, every claim points at an existing +section or a live surface; a small mermaid map is welcome (docs render it). Match the guide's existing +voice (`feedback_user_guide_quality_bar` memory: quality bar + one-session-for-related-guides). + +### B-2 "The standard flow — order to cash, procure to pay, books to reports" (new top-level section) +The S&D-standard cycle, told ONCE as a continuous story with the demo data, each step = do-it + what- +posted. Every step below is ALREADY live and oracle-proven — cite the witness in a footnote-style aside +so the guide inherits credibility without turning into a test report: + 1. **Sales Order** — create/complete (C_Order; completeIt fan-out W-FOLD-COMPLETE). + 2. **Shipment** — the generated M_InOut; on-hand drops (qty spine W-FOLD-QTYONHAND); COGS/Inventory GL + (W-FOLD-INOUTGL). + 3. **Customer Invoice → AR** — fact lines DR Receivable / CR Revenue+Tax (W-POST-HARDEN, Posting Preview + shows it live — W-DOC-POSTER). + 4. **Receipt & Allocation** — C_Payment then C_AllocationHdr incl. discount/write-off + VAT correction + (W-FOLD-PAYMENT / W-FOLD-ALLOC). + 5. **Replenishment** — on-hand fell → ReplenishReport suggests the PO (W-FOLD-REPLENISH; the POS §P-4 + section already demos it — LINK, don't duplicate). + 6. **Purchase Order → Receipt → Vendor Invoice → Match** — PO (commitment ∅ by config, W-MORDER-POST), + receipt, AP invoice DR InventoryClearing / CR V_Liability (W-FOLD-AP-INVOICE), M_MatchInv clearing + (W-FOLD-MATCHINV), M_MatchPO's honest ∅ under Average costing (W-POST-TAIL). + 7. **Final accounts** — GL Journal for the manual leg (W-FOLD-GLJOURNAL), bank statement reconcile + (W-POST-TAIL BankStatement), period close = the posted `fact_acct` journal, TB balances to the cent + (test_report_fin 46574.97). + 8. **Financial reporting** — link §8 (Balance Sheet / Income Statement / Cash Flow oracle-equivalent, + W-PA-REPORT) + NinjaExcel workbook. +- Fixed-asset & project postings (B-3's six classes) get ONE paragraph as "also in the books", not a + walkthrough — they are core-adjacent, the walkthrough stays the trade cycle. +- Verify navigation claims against the LIVE surface (localhost per `feedback_localhost_full_building_url_testing`), + not from memory; screenshots only where the guide already uses them. +### B-3 out of scope (explicit): POS/Kitchen/WH lenses, Tenancy/HR_BIM, 4D/5D scheduling, Ninja mode — + already documented; do not restructure them this session. + +## SESSION END +- Every §A/§B item ✅ or ⛔-with-the-one-question. Update THIS file's DONE appendix (§-lined), PROGRESS.md, + push everything (zero local-only commits), PR + merge per the git note at top. diff --git a/scripts/capture_post_b3_fixture.js b/scripts/capture_post_b3_fixture.js index 91ef360fe..fc12c49f8 100644 --- a/scripts/capture_post_b3_fixture.js +++ b/scripts/capture_post_b3_fixture.js @@ -171,11 +171,13 @@ var CAPTURE = { }, // Doc_AssetAddition posts in DOC currency (USD): schema-200000 (EUR) facts convert at the default // Spot rate — multiplyrate kept TEXT (exact NUMERIC, the W-ALLOC-FX precedent) + // isactive captured, NOT filtered — MConversionRate.getRate:251 filters IsActive='Y' itself and in + // this seed that clause (not client rank) is what excludes the 0.8006 twin row (corrected 2026-07-18) c_conversion_rate: { - cols: ['c_currency_id','c_currency_id_to','c_conversiontype_id','multiplyrate','validfrom','validto','ad_client_id','ad_org_id'], + cols: ['c_currency_id','c_currency_id_to','c_conversiontype_id','multiplyrate','validfrom','validto','ad_client_id','ad_org_id','isactive'], sql: "SELECT c_currency_id, c_currency_id_to, c_conversiontype_id, multiplyrate, validfrom::date, validto::date," + - " ad_client_id, ad_org_id" + - " FROM adempiere.c_conversion_rate WHERE ad_client_id IN (0,11) AND isactive='Y' ORDER BY c_currency_id, c_currency_id_to, validfrom" + " ad_client_id, ad_org_id, isactive" + + " FROM adempiere.c_conversion_rate WHERE ad_client_id IN (0,11) ORDER BY c_currency_id, c_currency_id_to, validfrom" }, c_conversiontype: { cols: ['c_conversiontype_id','isdefault'], diff --git a/scripts/doc_poster.js b/scripts/doc_poster.js index d93ef8015..a95b128b7 100644 --- a/scripts/doc_poster.js +++ b/scripts/doc_poster.js @@ -117,15 +117,16 @@ function assetAcctFor(db, assetId, schema, dateAcct) { [num(assetId), num(schema), String(dateAcct || '9999-12-31')]); } // MConversionRate.getRate:243-252 VERBATIM shape (default Spot type): date BETWEEN ValidFrom AND -// ValidTo, client/org-scoped, ORDER BY AD_Client_ID DESC, AD_Org_ID DESC, ValidFrom DESC — the -// TENANT rate outranks the system rate (this seed carries both, 0.85 vs 0.8006 — run-8 finding). +// ValidTo, IsActive='Y' (:251 — THE discriminator in this seed: the 0.8006 row is the SAME client 11 +// but INACTIVE; the B-3 run-8 "tenant-vs-system" reading was wrong, corrected 2026-07-18), +// client/org-scoped, ORDER BY AD_Client_ID DESC, AD_Org_ID DESC, ValidFrom DESC. function fxRate(db, curFrom, curTo, dateAcct, clientId, orgId) { if (num(curFrom) === num(curTo)) return 1; var r = getRow(db, "SELECT cr.multiplyrate AS rate FROM c_conversion_rate cr JOIN c_conversiontype ct" + " ON ct.c_conversiontype_id=cr.c_conversiontype_id AND ct.isdefault='Y'" + " WHERE cr.c_currency_id=? AND cr.c_currency_id_to=? AND ? BETWEEN cr.validfrom AND cr.validto" + - " AND cr.ad_client_id IN (0,?) AND cr.ad_org_id IN (0,?)" + + " AND cr.isactive='Y' AND cr.ad_client_id IN (0,?) AND cr.ad_org_id IN (0,?)" + " ORDER BY cr.ad_client_id DESC, cr.ad_org_id DESC, cr.validfrom DESC LIMIT 1", [num(curFrom), num(curTo), String(dateAcct || '9999-12-31'), num(clientId || 0), num(orgId || 0)]); return r ? Number(r.rate) : null; @@ -298,6 +299,118 @@ function deriveProjectIssue(db, id, schema) { return d; } +// ── W-POST-TAIL manifests (HARDEN_MATRIX.md §W-POST-TAIL, 2026-07-18) ─────────────────────────────── + +// Doc_BankStatement.createFacts:200-280 — per line (clearing accounts differ + IsPostIfClearingEqual=Y +// in this seed → the NORMAL branch): {Bank.Asset}=+StmtAmt · {Bank.InTransit}=−TrxAmt · charge leg +// (>0→CR, else DR .negate(); only when a charge account resolves and the amount ≠ 0 — Fact.createLine +// drops null-account/zero lines) · interest leg (<0→InterestExp else InterestRev, −InterestAmt). +// Legs post in DOC currency → per-schema conversion (fxRate) + the Fact.balanceAccounting +// CurrencyBalancing residual (c_acctschema_gl, the W-FOLD-ALLOC-FX rule). +function deriveBankStatement(db, id, schema) { + var hdr = getRow(db, 'SELECT * FROM c_bankstatement WHERE c_bankstatement_id=?', num(id)); + if (!hdr) return null; + var d = b3New(); + var lines = allRows(db, 'SELECT * FROM c_bankstatementline WHERE c_bankstatement_id=? ORDER BY c_bankstatementline_id', num(id)); + var ba = getRow(db, 'SELECT * FROM c_bankaccount_acct WHERE c_bankaccount_id=? AND c_acctschema_id=?', [num(hdr.c_bankaccount_id), num(schema)]); + if (!ba) { d.absent.push('c_bankaccount_acct#' + hdr.c_bankaccount_id + '/' + schema); return d; } + var as = schemaRow(db, schema); + var docCur = lines.length ? num(lines[0].c_currency_id) : (as ? num(as.c_currency_id) : 0); + var rate = fxRate(db, docCur, as ? as.c_currency_id : docCur, hdr.dateacct, hdr.ad_client_id, hdr.ad_org_id); + if (rate == null) { d.absent.push('fxrate#' + docCur + '->' + (as && as.c_currency_id)); return d; } + function conv(c) { return Math.round(c * rate); } + function leg(el, srcCents) { // signed source cents → DR (+) / CR (−) accounted cents + if (!el || srcCents === 0) return; + if (srcCents > 0) d.add('DR', el, conv(srcCents)); else d.add('CR', el, conv(-srcCents)); + } + var assetEl = elOf(db, vcAcct(db, ba.b_asset_acct), d.absent, '{Bank.Asset}'); + var transitEl = elOf(db, vcAcct(db, ba.b_intransit_acct), d.absent, '{Bank.InTransit}'); + lines.forEach(function (l) { + leg(assetEl, cents(l.stmtamt)); + leg(transitEl, -cents(l.trxamt)); + var chg = cents(l.chargeamt); + if (chg !== 0 && num(l.c_charge_id) > 0) { + var ch = getRow(db, 'SELECT ch_expense_acct AS acct FROM c_charge_acct WHERE c_charge_id=? AND c_acctschema_id=?', [num(l.c_charge_id), num(schema)]); + leg(elOf(db, vcAcct(db, ch && ch.acct), d.absent, '{Charge.Expense}'), -chg); // >0→CR / <0→DR + } + var intr = cents(l.interestamt); + if (intr !== 0) { + var col = intr < 0 ? 'b_interestexp_acct' : 'b_interestrev_acct'; + leg(elOf(db, vcAcct(db, ba[col]), d.absent, '{Bank.' + (intr < 0 ? 'InterestExp' : 'InterestRev') + '}'), -intr); + } + }); + // Fact.balanceAccounting — the per-doc accounted imbalance lands on the schema CurrencyBalancing acct + if (num(docCur) !== (as ? num(as.c_currency_id) : num(docCur))) { + var dr = 0, cr = 0; + Object.keys(d.by).forEach(function (k) { dr += d.by[k].dr; cr += d.by[k].cr; }); + if (dr !== cr) { + var gl = getRow(db, 'SELECT currencybalancing_acct AS acct FROM c_acctschema_gl WHERE c_acctschema_id=?', num(schema)); + var balEl = elOf(db, vcAcct(db, gl && gl.acct), d.absent, '{Schema.CurrencyBalancing}'); + if (balEl) d.add(dr < cr ? 'DR' : 'CR', balEl, Math.abs(dr - cr)); + } + } + return d; +} + +// Doc_MatchPO.createFacts:244-470 — the PPV pair posts ONLY under COSTINGMETHOD_StandardCosting +// (Doc_MatchPO.java:429); this seed costs at 'A' Average → the REAL engine posted the EMPTY set for +// all 37 docs (posted='Y', 0 fact rows — verified live 2026-07-18). ∅ is CONFIG-derived, not skipped: +// under 'S' the manifest opens the PPV path (poCost vs standard cost via m_cost — absent-token when +// the seed carries no standard-cost rows, which is itself the honest state). +function deriveMatchPO(db, id, schema) { + var hdr = getRow(db, 'SELECT * FROM m_matchpo WHERE m_matchpo_id=?', num(id)); + if (!hdr) return null; + var d = b3New(); + if (num(hdr.m_product_id) === 0 || Number(hdr.qty) === 0) return d; // :248-254 + if (num(hdr.m_inoutline_id) === 0) return d; // :275-282 no shipment match + var as = schemaRow(db, schema); + var method = as ? String(as.costingmethod) : ''; + // product-level override (m_product_category_acct costingmethod not captured — schema-level method, + // the same resolution the W-FOLD-MOVEMENT cost hop proved for this seed) + if (method !== 'S') return d; // :429 gate → ∅ under Average + var cost = getRow(db, + "SELECT c.currentcostprice AS p FROM m_cost c JOIN m_costelement e ON e.m_costelement_id=c.m_costelement_id" + + " AND e.costelementtype='M' AND e.costingmethod='S' WHERE c.m_product_id=? AND c.c_acctschema_id=?", + [num(hdr.m_product_id), num(schema)]); + if (!cost || Number(cost.p) === 0) { d.absent.push('{Product.StandardCost}#' + hdr.m_product_id); return d; } + var ol = getRow(db, 'SELECT priceactual FROM c_orderline WHERE c_orderline_id=?', num(hdr.c_orderline_id)); + if (!ol) { d.absent.push('c_orderline#' + hdr.c_orderline_id); return d; } + var ppv = Math.round((cents(ol.priceactual) - cents(cost.p)) * Number(hdr.qty)); + if (ppv !== 0) { + var pc = getRow(db, 'SELECT a.p_purchasepricevariance_acct AS acct FROM m_product p JOIN m_product_category_acct a ON a.m_product_category_id=p.m_product_category_id AND a.c_acctschema_id=? WHERE p.m_product_id=?', [num(schema), num(hdr.m_product_id)]); + var ppvEl = elOf(db, vcAcct(db, pc && pc.acct), d.absent, '{Product.PPV}'); + var offEl = elOf(db, vcAcct(db, null), d.absent, '{Schema.PPVOffset}'); // c_acctschema_gl ppvoffset not captured — named absent + if (ppvEl && offEl) { d.add(ppv > 0 ? 'DR' : 'CR', ppvEl, Math.abs(ppv)); d.add(ppv > 0 ? 'CR' : 'DR', offEl, Math.abs(ppv)); } + } + return d; +} + +// Doc_Requisition.createFacts:121-156 — posts ONLY under MAcctSchema.isCreateReservation +// (commitmenttype 'B'/'A', MAcctSchema.java:662-669); this seed = 'N' → the REAL engine posted ∅ for +// the 1 posted doc. Under the flip: per line DR {Product.Expense}=LineNetAmt + CR CommitmentOffset=Σ. +function deriveRequisition(db, id, schema) { + var hdr = getRow(db, 'SELECT * FROM m_requisition WHERE m_requisition_id=?', num(id)); + if (!hdr) return null; + var d = b3New(); + var as = schemaRow(db, schema); + var ct = as ? String(as.commitmenttype) : 'N'; + if (ct !== 'B' && ct !== 'A') return d; // isCreateReservation gate → ∅ + var lines = allRows(db, 'SELECT * FROM m_requisitionline WHERE m_requisition_id=? ORDER BY m_requisitionline_id', num(id)); + var total = 0; + lines.forEach(function (l) { + var amt = cents(l.linenetamt); + total += amt; + var pc = num(l.m_product_id) > 0 + ? getRow(db, 'SELECT a.p_expense_acct AS acct FROM m_product p JOIN m_product_category_acct a ON a.m_product_category_id=p.m_product_category_id AND a.c_acctschema_id=? WHERE p.m_product_id=?', [num(schema), num(l.m_product_id)]) + : getRow(db, 'SELECT ch_expense_acct AS acct FROM c_charge_acct WHERE c_charge_id=? AND c_acctschema_id=?', [num(l.c_charge_id), num(schema)]); + var el = elOf(db, vcAcct(db, pc && pc.acct), d.absent, '{Product.Expense}'); + if (el) d.add('DR', el, amt); + }); + var offEl = elOf(db, vcAcct(db, null), d.absent, '{Schema.CommitmentOffset}'); // not captured — named absent under the flip + if (offEl) d.add('CR', offEl, total); + return d; +} + function finish(d, basis) { if (!d) return { lines: [], balanced: false, sumDr: 0, sumCr: 0, absent: [], basis: 'none' }; var lines = Object.keys(d.by).map(function (k) { @@ -333,6 +446,10 @@ function derivePostings(db, recordRef, schema, R) { if (table === 'A_Asset_Transfer') return finish(deriveAssetTransfer(db, id, schema, recordRef.primarySchema), 'fa-transfer'); if (table === 'A_Asset_Disposed') return finish(deriveAssetDisposed(db, id, schema), 'fa-disposal'); if (table === 'C_ProjectIssue') return finish(deriveProjectIssue(db, id, schema), 'project-issue'); + // W-POST-TAIL classes (HARDEN_MATRIX.md §W-POST-TAIL) + if (table === 'C_BankStatement') return finish(deriveBankStatement(db, id, schema), 'bank-statement'); + if (table === 'M_MatchPO') return finish(deriveMatchPO(db, id, schema), 'matchpo'); + if (table === 'M_Requisition') return finish(deriveRequisition(db, id, schema), 'requisition'); return { lines: [], balanced: false, sumDr: 0, sumCr: 0, absent: [], basis: 'none' }; } diff --git a/scripts/extract_fact_acct.sh b/scripts/extract_fact_acct.sh index e1f310620..ccb339201 100644 --- a/scripts/extract_fact_acct.sh +++ b/scripts/extract_fact_acct.sh @@ -75,14 +75,19 @@ cap c_bp_customer_acct "c_bpartner_id,c_acctschema_id,c_receivable_acct" cap c_bp_vendor_acct "c_bpartner_id,c_acctschema_id,v_liability_acct" "c_bpartner_id INT,c_acctschema_id INT,v_liability_acct INT" # p_inventoryclearing_acct = the matched AP-invoice line DR (51400 Inventory Clearing, via product->category). ADDITIVE. # p_averagecostvariance_acct added ADDITIVELY for the M_MatchInv avg-cost IPV split (variance share for qty NOT on hand). -cap m_product_category_acct "m_product_category_id,c_acctschema_id,p_revenue_acct,p_cogs_acct,p_asset_acct,p_inventoryclearing_acct,p_averagecostvariance_acct" "m_product_category_id INT,c_acctschema_id INT,p_revenue_acct INT,p_cogs_acct INT,p_asset_acct INT,p_inventoryclearing_acct INT,p_averagecostvariance_acct INT" +# p_expense_acct + p_purchasepricevariance_acct added ADDITIVELY (W-POST-TAIL: Doc_Requisition expense +# lines under the reservation gate-flip; Doc_MatchPO PPV under the StandardCosting gate-flip). +cap m_product_category_acct "m_product_category_id,c_acctschema_id,p_revenue_acct,p_cogs_acct,p_asset_acct,p_inventoryclearing_acct,p_averagecostvariance_acct,p_expense_acct,p_purchasepricevariance_acct" "m_product_category_id INT,c_acctschema_id INT,p_revenue_acct INT,p_cogs_acct INT,p_asset_acct INT,p_inventoryclearing_acct INT,p_averagecostvariance_acct INT,p_expense_acct INT,p_purchasepricevariance_acct INT" # t_credit_acct = input-VAT (AP tax DR), mirrors t_due_acct (output VAT). ADDITIVE. cap c_tax_acct "c_tax_id,c_acctschema_id,t_due_acct,t_credit_acct" "c_tax_id INT,c_acctschema_id INT,t_due_acct INT,t_credit_acct INT" cap c_validcombination "c_validcombination_id,account_id" "c_validcombination_id INT,account_id INT" cap c_acctschema_default "c_acctschema_id" "c_acctschema_id INT" cap c_invoicetax "c_invoice_id,c_tax_id,taxamt" "c_invoice_id INT,c_tax_id INT,taxamt REAL" # Bank account acct-config: Doc_Payment posts a receipt DR {Bank.InTransit} / CR {Bank.UnallocatedCash} at payamt. -cap c_bankaccount_acct "c_bankaccount_id,c_acctschema_id,b_intransit_acct,b_unallocatedcash_acct,b_paymentselect_acct,b_asset_acct" "c_bankaccount_id INT,c_acctschema_id INT,b_intransit_acct INT,b_unallocatedcash_acct INT,b_paymentselect_acct INT,b_asset_acct INT" +# b_interestrev_acct/b_interestexp_acct added ADDITIVELY for W-POST-TAIL Doc_BankStatement interest legs. +# c_charge_acct added ADDITIVELY for the same fold's charge leg (line.getChargeAccount → ch_expense_acct). +cap c_charge_acct "c_charge_id,c_acctschema_id,ch_expense_acct" "c_charge_id INT,c_acctschema_id INT,ch_expense_acct INT" +cap c_bankaccount_acct "c_bankaccount_id,c_acctschema_id,b_intransit_acct,b_unallocatedcash_acct,b_paymentselect_acct,b_asset_acct,b_interestrev_acct,b_interestexp_acct" "c_bankaccount_id INT,c_acctschema_id INT,b_intransit_acct INT,b_unallocatedcash_acct INT,b_paymentselect_acct INT,b_asset_acct INT,b_interestrev_acct INT,b_interestexp_acct INT" echo "== capture inventory + cost oracle (F-1 shipment posting Doc_InOut + StorageOnHand spine, prompts/FOLD_MODEL_LOGIC.md §F-1 1b-ii / step-2) ==" # Doc_InOut posts DR {Product.Cogs} / CR {Product.Asset} at the product's current cost. The accounts @@ -147,9 +152,14 @@ sqlite3 "$DB" "DROP TABLE IF EXISTS c_cashline; CREATE TABLE c_cashline(c_cashli sqlite3 "$DB" ".mode csv" ".import /tmp/c_cashline.csv c_cashline" # FX allocation (schema-200000 EUR): the Spot conversion rate (multiplyrate, USD 100 -> EUR 102, type 114 IsDefault) # + the CurrencyBalancing account that absorbs the per-doc rounding imbalance. ADDITIVE (H-1 balance preserved). -PG "SELECT c_currency_id, c_currency_id_to, c_conversiontype_id, multiplyrate, validfrom, validto FROM adempiere.c_conversion_rate WHERE ad_client_id IN (0,11) ORDER BY c_currency_id, c_currency_id_to, validfrom;" > /tmp/c_conversion_rate.csv +# ad_client_id/ad_org_id/isactive added ADDITIVELY (W-POST-TAIL/B-3): MConversionRate.getRate:243-252 +# filters IsActive='Y' and orders AD_Client_ID DESC — in THIS seed the 0.8006 twin row is the same +# client 11 but INACTIVE, so IsActive is the live discriminator. The row is captured (not filtered) +# so the engine's own isactive clause does the work — and poc_alloc_fx §FALSIFIER-B keeps its +# wrong-rate prop. NON-INVENT: real columns, no row dropped. +PG "SELECT c_currency_id, c_currency_id_to, c_conversiontype_id, multiplyrate, validfrom, validto, ad_client_id, ad_org_id, isactive FROM adempiere.c_conversion_rate WHERE ad_client_id IN (0,11) ORDER BY c_currency_id, c_currency_id_to, validfrom;" > /tmp/c_conversion_rate.csv # multiplyrate kept as TEXT to preserve the exact PG NUMERIC decimal (REAL would float-drift the HALF_UP rounding). -sqlite3 "$DB" "DROP TABLE IF EXISTS c_conversion_rate; CREATE TABLE c_conversion_rate(c_currency_id INT, c_currency_id_to INT, c_conversiontype_id INT, multiplyrate TEXT, validfrom TEXT, validto TEXT);" +sqlite3 "$DB" "DROP TABLE IF EXISTS c_conversion_rate; CREATE TABLE c_conversion_rate(c_currency_id INT, c_currency_id_to INT, c_conversiontype_id INT, multiplyrate TEXT, validfrom TEXT, validto TEXT, ad_client_id INT, ad_org_id INT, isactive TEXT);" sqlite3 "$DB" ".mode csv" ".import /tmp/c_conversion_rate.csv c_conversion_rate" # intercompany due-to/from added ADDITIVELY for the inter-org M_Movement posting (schema-level GL accounts). cap c_acctschema_gl "c_acctschema_id,currencybalancing_acct,intercompanydueto_acct,intercompanyduefrom_acct" "c_acctschema_id INT,currencybalancing_acct INT,intercompanydueto_acct INT,intercompanyduefrom_acct INT" @@ -215,6 +225,24 @@ sqlite3 "$DB" ".mode csv" ".import /tmp/m_matchinv.csv m_matchinv" # NOTE: c_invoiceline is NOT re-captured here — the full table (incl. c_invoiceline_id/m_inoutline_id/qtyinvoiced # that completeInvoice reads) is already present from the base seed; a narrow re-cap would drop columns siblings need. +echo "== capture M_MatchPO + M_RequisitionLine (W-POST-TAIL — the config-gated-∅ posters' source rows) ==" +# m_matchpo: 37 docs ALL posted='Y' with ZERO fact rows — the REAL engine posted the empty set (PPV block +# gated on StandardCosting, Doc_MatchPO.java:429; GardenWorld costs at 'A'). Captured so the ∅==∅ diff runs +# over the REAL population, not a vacuous empty set. NON-INVENT: straight copy. +PG "SELECT m_matchpo_id, c_orderline_id, coalesce(m_inoutline_id,0), coalesce(c_invoiceline_id,0), + m_product_id, round(qty,2), posted, dateacct::date + FROM adempiere.m_matchpo WHERE ad_client_id=11 ORDER BY m_matchpo_id;" > /tmp/m_matchpo.csv +sqlite3 "$DB" "DROP TABLE IF EXISTS m_matchpo; CREATE TABLE m_matchpo(m_matchpo_id INT, c_orderline_id INT, m_inoutline_id INT, c_invoiceline_id INT, m_product_id INT, qty REAL, posted TEXT, dateacct TEXT);" +sqlite3 "$DB" ".mode csv" ".import /tmp/m_matchpo.csv m_matchpo" +# m_requisitionline: Doc_Requisition posts per-line {Product.Expense}=AmtSource + CommitmentOffset ONLY under +# isCreateReservation (MAcctSchema.java:662-669; GardenWorld commitmenttype='N' → ∅). Lines captured for the +# gate-flip §FALSIFIER manifest. NON-INVENT: real columns. +PG "SELECT m_requisitionline_id, m_requisition_id, coalesce(m_product_id,0), coalesce(c_charge_id,0), + round(linenetamt,2), round(qty,2) + FROM adempiere.m_requisitionline WHERE ad_client_id=11 ORDER BY m_requisitionline_id;" > /tmp/m_requisitionline.csv +sqlite3 "$DB" "DROP TABLE IF EXISTS m_requisitionline; CREATE TABLE m_requisitionline(m_requisitionline_id INT, m_requisition_id INT, m_product_id INT, c_charge_id INT, linenetamt REAL, qty REAL);" +sqlite3 "$DB" ".mode csv" ".import /tmp/m_requisitionline.csv m_requisitionline" + echo "== capture GL_Journal source (Doc_GLJournal posting — manual journal incl. inter-org balancing, fact_acct 224) ==" # A GL journal's lines post DIRECTLY as fact lines: amtacct = round(amtsource × currencyrate, 2) (the journal is # entered in the schema's own currency here, so rate=1). The non-trivial half is Fact.balanceAccounting: GardenWorld's @@ -301,9 +329,13 @@ PG "SELECT c_bankstatement_id, name, docstatus, docaction, processing, processed FROM adempiere.c_bankstatement WHERE ad_client_id=11 ORDER BY c_bankstatement_id;" > /tmp/c_bankstatement.csv sqlite3 "$DB" "DROP TABLE IF EXISTS c_bankstatement; CREATE TABLE c_bankstatement(c_bankstatement_id INT, name TEXT, docstatus TEXT, docaction TEXT, processing TEXT, processed TEXT, c_doctype_id INT, c_bankaccount_id INT, ad_org_id INT, ad_client_id INT, beginningbalance REAL, endingbalance REAL, statementdifference REAL, dateacct TEXT);" sqlite3 "$DB" ".mode csv" ".import /tmp/c_bankstatement.csv c_bankstatement" -PG "SELECT c_bankstatementline_id, c_bankstatement_id, round(stmtamt,2), c_payment_id +# trxamt/chargeamt/interestamt/c_charge_id/c_currency_id added ADDITIVELY for the W-POST-TAIL +# Doc_BankStatement fold (per-line BankAsset=+StmtAmt / InTransit=−TrxAmt / charge / interest legs, +# Doc_BankStatement.java:200-280). NON-INVENT: real columns. +PG "SELECT c_bankstatementline_id, c_bankstatement_id, round(stmtamt,2), c_payment_id, + round(trxamt,2), round(chargeamt,2), round(interestamt,2), coalesce(c_charge_id,0), c_currency_id FROM adempiere.c_bankstatementline WHERE ad_client_id=11 ORDER BY c_bankstatementline_id;" > /tmp/c_bankstatementline.csv -sqlite3 "$DB" "DROP TABLE IF EXISTS c_bankstatementline; CREATE TABLE c_bankstatementline(c_bankstatementline_id INT, c_bankstatement_id INT, stmtamt REAL, c_payment_id INT);" +sqlite3 "$DB" "DROP TABLE IF EXISTS c_bankstatementline; CREATE TABLE c_bankstatementline(c_bankstatementline_id INT, c_bankstatement_id INT, stmtamt REAL, c_payment_id INT, trxamt REAL, chargeamt REAL, interestamt REAL, c_charge_id INT, c_currency_id INT);" sqlite3 "$DB" ".mode csv" ".import /tmp/c_bankstatementline.csv c_bankstatementline" # m_rma: MRMA.beforeSave shipment-derives (BP/currency/IsSOTrx-match/SalesRep, MRMA.java:256-297); m_inout already captured FULL. PG "SELECT m_rma_id, documentno, docstatus, docaction, processing, processed, c_doctype_id, inout_id, diff --git a/scripts/poc_post_tail.js b/scripts/poc_post_tail.js new file mode 100644 index 000000000..7facf11f8 --- /dev/null +++ b/scripts/poc_post_tail.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node +// Copyright (c) 2025-2026 Redhuan D. Oon +// SPDX-License-Identifier: MIT +// ⚠ DO NOT REMOVE — Scope guard +// poc_post_tail.js — W-POST-TAIL (prompts/HARDEN_MATRIX.md §W-POST-TAIL): the LAST 6 Doc_* posters +// after B-3, worked by SEED REALITY (facts verified live 2026-07-18): +// · C_BankStatement (392): REAL oracle IN the seed — 13 fact rows for statement 100. derivePostings' +// bank-statement manifest == fact_acct(392) per document × schema × (account, side), INTEGER CENTS, +// maxDiff=0c — incl. the schema-200000 conversion + the 0.01 CurrencyBalancing residual. +// · M_MatchPO (473): the REAL engine posted the EMPTY SET for all 37 docs (posted='Y', 0 fact rows) — +// the PPV block is gated on StandardCosting (Doc_MatchPO.java:429), this seed costs at 'A'. The +// ∅==∅ diff runs over the REAL 37-doc population; §FALSIFIER flips costingmethod → the gate OPENS. +// · M_Requisition (702): 1 posted doc, 0 facts — Doc_Requisition gates on isCreateReservation +// (MAcctSchema.java:662-669, commitmenttype 'B'/'A'; seed='N'). ∅==∅ + gate-flip → lines appear. +// · C_Cash / M_Inventory / M_Production: NOT claimed here — named next-session (§TAIL-SKIPS): Cash +// needs the existing CO docs POSTED on a scratch clone (B-3 generator reuse), Inventory needs its +// 3 drafts completed there, Production has 0 docs + no component costs (W-FOLD-PRODUCTION deferral). +// NON-INVENT: oracle = build/erp/glassbowl_data.db (real client-11 rows, extract_fact_acct.sh); the +// gate flips happen on a THROWAWAY in-memory copy, never the oracle. READ THE LOG; exit ≠ evidence. +// Run: bash build/erp/run_witness.sh scripts/poc_post_tail.js → build/erp/poc_post_tail.log +'use strict'; +var path = require('path'); +var Database = require('better-sqlite3'); +var DP = require('./doc_poster'); + +var ORACLE = path.join(__dirname, '..', 'build', 'erp', 'glassbowl_data.db'); +var fails = 0; +function verdict(ok, label, detail) { if (!ok) fails++; console.log(' ' + (ok ? '🟢' : '🔴') + ' ' + label + (detail ? ' — ' + detail : '')); } +function cents(x) { return Math.round(Number(x || 0) * 100); } + +console.log('═══ W-POST-TAIL — the last Doc_* posters vs the REAL seed oracle (HARDEN_MATRIX §W-POST-TAIL) ═══\n'); + +var db = new Database(ORACLE, { readonly: true }); +var SCHEMAS = db.prepare('SELECT c_acctschema_id s FROM c_acctschema ORDER BY 1').all().map(function (r) { return r.s; }); + +// ── 1. C_BankStatement — the real 13-row oracle, per doc × schema × (account, side) ──────────────── +console.log('── C_BankStatement (392) — REAL fact oracle, maxDiff=0c gate ──'); +var stmts = db.prepare("SELECT c_bankstatement_id id FROM c_bankstatement WHERE docstatus='CO' ORDER BY 1").all(); +var bsMax = 0, bsCells = 0, bsOracleRows = db.prepare('SELECT count(*) c FROM fact_acct WHERE ad_table_id=392').get().c; +stmts.forEach(function (s) { + SCHEMAS.forEach(function (schema) { + var want = {}; + db.prepare('SELECT * FROM fact_acct WHERE ad_table_id=392 AND record_id=? AND c_acctschema_id=?').all(s.id, schema) + .forEach(function (f) { + var k = String(f.account_id); + if (!want[k]) want[k] = { dr: 0, cr: 0 }; + want[k].dr += cents(f.amtacctdr); want[k].cr += cents(f.amtacctcr); + }); + var got = {}; + var r = DP.derivePostings(db, { table: 'C_BankStatement', id: s.id }, schema); + r.lines.forEach(function (l) { + var k = String(l.account_id); + if (!got[k]) got[k] = { dr: 0, cr: 0 }; + got[k].dr += cents(l.amtacctdr); got[k].cr += cents(l.amtacctcr); + }); + Object.keys(want).concat(Object.keys(got)).forEach(function (k) { + var w = want[k] || { dr: 0, cr: 0 }, o = got[k] || { dr: 0, cr: 0 }; + var dd = Math.max(Math.abs(w.dr - o.dr), Math.abs(w.cr - o.cr)); + if (dd > bsMax) bsMax = dd; + if (dd !== 0) console.log(' 🔴 diff stmt=' + s.id + ' schema=' + schema + ' account=' + k + + ' oracle=' + w.dr + '/' + w.cr + 'c engine=' + o.dr + '/' + o.cr + 'c'); + }); + bsCells++; + }); +}); +console.log('§TAIL-POST class=C_BankStatement docs=' + stmts.length + ' cells=' + bsCells + + ' oracle_rows=' + bsOracleRows + ' maxDiff=' + bsMax + 'c oracle=real-fact_acct(392)'); +verdict(bsMax === 0 && bsOracleRows > 0, 'C_BankStatement derivePostings == the REAL posted journal (maxDiff=0c, ' + bsOracleRows + ' oracle rows)'); + +// ── 2. M_MatchPO — the engine-posted EMPTY set, over the REAL 37-doc population ──────────────────── +console.log('\n── M_MatchPO (473) — config-gated ∅ over the REAL posted population ──'); +var mpos = db.prepare("SELECT m_matchpo_id id FROM m_matchpo WHERE posted='Y' ORDER BY 1").all(); +var mpoFacts = db.prepare('SELECT count(*) c FROM fact_acct WHERE ad_table_id=473').get().c; +var mpoNonEmpty = 0; +mpos.forEach(function (m) { + SCHEMAS.forEach(function (schema) { + var r = DP.derivePostings(db, { table: 'M_MatchPO', id: m.id }, schema); + if (r.lines.length !== 0) mpoNonEmpty++; + }); +}); +console.log('§TAIL-POST class=M_MatchPO docs=' + mpos.length + ' oracle_rows=' + mpoFacts + + ' engine_nonempty=' + mpoNonEmpty + ' maxDiff=0c basis=∅-by-config (Doc_MatchPO.java:429, costingmethod=A)'); +verdict(mpos.length === 37 && mpoFacts === 0 && mpoNonEmpty === 0, + 'M_MatchPO: engine ∅ == the REAL engine\'s ∅ across all 37 posted docs (StandardCosting gate closed under Average)'); + +// ── 3. M_Requisition — same shape, the W-MORDER-POST twin ────────────────────────────────────────── +console.log('\n── M_Requisition (702) — config-gated ∅ (isCreateReservation, seed commitmenttype=N) ──'); +var reqs = db.prepare("SELECT m_requisition_id id FROM m_requisition WHERE docstatus='CO' ORDER BY 1").all(); +var reqFacts = db.prepare('SELECT count(*) c FROM fact_acct WHERE ad_table_id=702').get().c; +var reqNonEmpty = 0; +reqs.forEach(function (q) { + SCHEMAS.forEach(function (schema) { + var r = DP.derivePostings(db, { table: 'M_Requisition', id: q.id }, schema); + if (r.lines.length !== 0) reqNonEmpty++; + }); +}); +console.log('§TAIL-POST class=M_Requisition docs=' + reqs.length + ' oracle_rows=' + reqFacts + + ' engine_nonempty=' + reqNonEmpty + ' maxDiff=0c basis=∅-by-config (MAcctSchema.java:662-669, commitmenttype=N)'); +verdict(reqs.length === 1 && reqFacts === 0 && reqNonEmpty === 0, + 'M_Requisition: engine ∅ == the REAL engine\'s ∅ (reservation gate closed)'); + +// ── 4. §FALSIFIERS (load-bearing) on a THROWAWAY copy ────────────────────────────────────────────── +console.log('\n── §FALSIFIERS (in-memory copy; the oracle db stays read-only) ──'); +var mem = new Database(':memory:'); +['c_acctschema', 'c_acctschema_gl', 'c_bankstatement', 'c_bankstatementline', 'c_bankaccount_acct', + 'c_charge_acct', 'c_validcombination', 'c_elementvalue', 'c_conversion_rate', 'c_conversiontype', + 'm_matchpo', 'm_requisition', 'm_requisitionline', 'm_product', 'm_product_category_acct', 'm_cost', + 'm_costelement', 'c_orderline'].forEach(function (t) { + var rows = db.prepare('SELECT * FROM ' + t).all(); + if (!rows.length) { mem.prepare('CREATE TABLE ' + t + ' (dummy)').run(); return; } + var cols = Object.keys(rows[0]); + mem.prepare('CREATE TABLE ' + t + ' (' + cols.join(',') + ')').run(); + var ins = mem.prepare('INSERT INTO ' + t + ' VALUES (' + cols.map(function () { return '?'; }).join(',') + ')'); + rows.forEach(function (r) { ins.run(cols.map(function (c) { return r[c]; })); }); +}); + +// (a) requisition gate-flip: commitmenttype N→B → the manifest OPENS (lines / named-absent offset) +mem.prepare("UPDATE c_acctschema SET commitmenttype='B' WHERE c_acctschema_id=101").run(); +var reqFlip = DP.derivePostings(mem, { table: 'M_Requisition', id: reqs[0].id }, 101); +mem.prepare("UPDATE c_acctschema SET commitmenttype='N' WHERE c_acctschema_id=101").run(); +var okA = reqFlip.lines.length > 0 || reqFlip.absent.length > 0; +verdict(okA, '§FALSIFIER requisition gate-flip N→B: manifest OPENS (zero is CONFIG-derived, not hardcoded)', + 'lines=' + reqFlip.lines.length + ' absent=[' + reqFlip.absent.join(',') + ']'); +console.log('§TAIL-FALSIFIER req-flip commitmenttype N→B lines=' + reqFlip.lines.length + ' absent=' + reqFlip.absent.length); + +// (b) matchpo gate-flip: costingmethod A→S opens the PPV path. On the unmodified copy the first +// matched doc's PPV computes to exactly 0 (price == standard cost — a REAL zero, observed), so the +// flip ALSO halves the standard cost: a price≠cost variance MUST now surface (lines or the named +// PPVOffset absence) — proving the :429 gate and the PPV arithmetic are both live. +var mpoDoc = mem.prepare('SELECT m_matchpo_id id, m_product_id p FROM m_matchpo WHERE m_inoutline_id>0 AND m_product_id>0 AND qty>0 ORDER BY 1 LIMIT 1').get(); +mem.prepare("UPDATE c_acctschema SET costingmethod='S' WHERE c_acctschema_id=101").run(); +mem.prepare("UPDATE m_cost SET currentcostprice=currentcostprice/2 WHERE m_product_id=? AND c_acctschema_id=101" + + " AND m_costelement_id IN (SELECT m_costelement_id FROM m_costelement WHERE costingmethod='S')").run(mpoDoc.p); +var mpoFlip = DP.derivePostings(mem, { table: 'M_MatchPO', id: mpoDoc.id }, 101); +mem.prepare("UPDATE c_acctschema SET costingmethod='A' WHERE c_acctschema_id=101").run(); +var okB = mpoFlip.lines.length > 0 || mpoFlip.absent.length > 0; +verdict(okB, '§FALSIFIER matchpo gate-flip A→S (+cost≠price): the PPV path opens (Doc_MatchPO.java:429 gate is live)', + 'doc=' + mpoDoc.id + ' lines=' + mpoFlip.lines.length + ' absent=[' + mpoFlip.absent.join(',') + ']'); +console.log('§TAIL-FALSIFIER mpo-flip costingmethod A→S doc=' + mpoDoc.id + ' lines=' + mpoFlip.lines.length + ' absent=' + mpoFlip.absent.length); + +// (c) scale ONE bank-statement line → maxDiff≠0 vs the real oracle (the diff is load-bearing) +mem.prepare('UPDATE c_bankstatementline SET stmtamt=stmtamt*2 WHERE c_bankstatementline_id=100').run(); +var scaled = DP.derivePostings(mem, { table: 'C_BankStatement', id: 100 }, 101); +var wantDr = 0; +db.prepare('SELECT * FROM fact_acct WHERE ad_table_id=392 AND record_id=100 AND c_acctschema_id=101').all() + .forEach(function (f) { wantDr += cents(f.amtacctdr); }); +var gotDr = 0; scaled.lines.forEach(function (l) { gotDr += cents(l.amtacctdr); }); +verdict(gotDr !== wantDr, '§FALSIFIER scale-one-line: doubling stmtamt breaks the bank-statement diff (metric load-bearing)', + 'oracle ΣDR=' + wantDr + 'c scaled ΣDR=' + gotDr + 'c'); +console.log('§TAIL-FALSIFIER bs-scale line=100 oracleΣDR=' + wantDr + 'c scaledΣDR=' + gotDr + 'c (must differ)'); +mem.close(); + +// ── 5. named skips (the honest remainder) ────────────────────────────────────────────────────────── +console.log('\n§TAIL-SKIPS C_Cash (407): 2 real CO docs NEVER posted (posted=N, 0 facts) → post them on a scratch'); +console.log('§TAIL-SKIPS clone via the B-3 generator (zero seed prep) — NEXT session, not claimed here.'); +console.log('§TAIL-SKIPS M_Inventory (321): 3 real DRAFTS → complete+post on the clone — NEXT session.'); +console.log('§TAIL-SKIPS M_Production (325): 0 docs + component m_cost absent (W-FOLD-PRODUCTION deferral) → ⛔'); +console.log('§TAIL-SKIPS until a costed BOM seed exists; costs are NOT synthesized.'); + +db.close(); +console.log('\n' + (fails === 0 ? '🟢 W-POST-TAIL PASS' : '🔴 W-POST-TAIL FAIL (' + fails + ')') + + ' — BankStatement folds the REAL journal to the cent; MatchPO(37)+Requisition ∅==∅ config-derived; 3 named skips.'); +process.exit(fails === 0 ? 0 : 1);