|
1 | 1 | # crm — rung 7 of the [application ladder](../LADDER.md) |
2 | 2 |
|
3 | | -**Status: 7a under construction** (green-lit 2026-08-28 by direct decision, |
4 | | -ahead of a formal ladder-wide findings-scoreboard review — see |
5 | | -[`../LADDER.md`](../LADDER.md) for the program's general post-rung-4 gate). |
6 | | -This README remains the design record; the rung's defining framework |
7 | | -question (runtime custom fields) was already answered by the standalone |
8 | | -**extension-bag spike** (complete — see |
| 3 | +**Status: server-side complete, no client.** Build order steps 1–10 and |
| 4 | +7b's three follow-on items are built and tested (`src/`, `include/`, |
| 5 | +`tests/`). crm is the only built rung with no `gui/` and no `gui_wasm/`, so the |
| 6 | +parts of the definition of done that name a rendering client are not met. |
| 7 | +Two smaller items also fall short of what this README asks for: lists ship |
| 8 | +unpaginated, and the deleted-field lifecycle has one of its three arrival |
| 9 | +paths under test. All three are set out in |
| 10 | +[What is not built](#what-is-not-built), and the definition of done below is |
| 11 | +annotated bullet by bullet. |
| 12 | + |
| 13 | +Construction was green-lit 2026-08-28 by direct decision, ahead of a formal |
| 14 | +ladder-wide findings-scoreboard review — see [`../LADDER.md`](../LADDER.md) |
| 15 | +for the program's general post-rung-4 gate. This README remains the design |
| 16 | +record; the rung's defining framework question (runtime custom fields) was |
| 17 | +already answered by the standalone **extension-bag spike** (complete — see |
9 | 18 | [`EXTENSION-BAG-SPIKE.md`](EXTENSION-BAG-SPIKE.md): yes, reachable today with |
10 | 19 | no framework change). A mini-Salesforce: accounts, contacts, leads, |
11 | 20 | opportunities in a pipeline, quotes with exact pricing, per-field |
12 | 21 | permissions, field-level audit history — and, as the endgame, runtime custom |
13 | 22 | fields. This rung tests whether morph can carry *metadata-driven* production |
14 | 23 | business software, the defining property of the Salesforce/SAP class. |
15 | 24 |
|
16 | | -Per review, the rung is split: **7a** = steps 1–8 (a conventional CRM on |
| 25 | +Per review, the rung was split: **7a** = steps 1–8 (a conventional CRM on |
17 | 26 | compiled types), **7b** = steps 9–10 (runtime custom fields), with an |
18 | 27 | explicit **go/no-go gate** between them — the extension-bag question has a |
19 | | -different risk profile, and a negative answer must not stall the ladder. |
| 28 | +different risk profile, and a negative answer must not stall the ladder. The |
| 29 | +gate was passed on the spike's answer, and both halves are built. |
20 | 30 |
|
21 | 31 | ## Reference implementations |
22 | 32 |
|
@@ -590,11 +600,67 @@ execute → warnings + confirmation token → re-execute; and **record merge** |
590 | 600 | instances — two attached handler sets, one survivor), the hardest |
591 | 601 | journal + instance-directory interaction in the ladder. |
592 | 602 |
|
| 603 | +## What is not built |
| 604 | + |
| 605 | +Three things this README asks for that the rung does not currently have. |
| 606 | +Each is listed with why it was left, so the gap is a decision on the record |
| 607 | +rather than an omission a reader has to discover. |
| 608 | + |
| 609 | +- **No client — no `gui/`, no `gui_wasm/`.** Every other built rung ships a |
| 610 | + `gui/`; pastebin, bookmarks and polls also ship a `gui_wasm/`. The |
| 611 | + server-side halves of the form story *are* built and tested — |
| 612 | + `crm::gui::crmSchemasJson()` serves every form action's schema, and |
| 613 | + `crm::gui::updateAccountSchemaJsonFor()` shapes it per caller — but |
| 614 | + nothing consumes them. Two "expected strain points" below therefore stay |
| 615 | + open, because only a renderer can confront them: the **explicit-submit / |
| 616 | + presenter-gated form mode** (which that section requires *before any form |
| 617 | + ships*), and the **child-table control for quote lines**. The recursive |
| 618 | + line-item validation those lines also need does exist, server-side, in |
| 619 | + `QuoteModel` (`test_quote_model.cpp`). |
| 620 | +- **Lists are unpaginated.** Step 1 asks for filters/pagination and the |
| 621 | + strain points make keyset-cursor lists the ladder idiom, with a test for |
| 622 | + cursor stability while another client renames or deletes rows mid-walk. |
| 623 | + `ListAccounts`' own doc comment (`dto/account_dto.hpp`) records the |
| 624 | + decision taken instead: no rung has a cursor convention yet, and step 1 |
| 625 | + stays consistent with the ladder rather than inventing the first one |
| 626 | + alone. Filters ship; pagination and its stability test do not. |
| 627 | +- **The deleted-field lifecycle has one of three arrival paths under |
| 628 | + test.** The strain points name three ways a write naming a just-deleted |
| 629 | + custom field can arrive: a stale open form, a queued offline edit, and |
| 630 | + journal replay of an old payload. All three would pass through the single |
| 631 | + enforcement point `validateCustomFields()`, and the live-submit path is |
| 632 | + tested ("Submitting a deleted field's key is rejected, not silently |
| 633 | + dropped or stored"). The other two are not exercised. The offline path is |
| 634 | + structurally unreachable today — the outbox carries only |
| 635 | + `QueuedOpportunityUpdate` and custom fields exist only on `Account` — and |
| 636 | + replay is worth a test of its own rather than an argument, because §8 |
| 637 | + already records that replay mutates rows directly instead of going |
| 638 | + through `execute()`. |
| 639 | + |
| 640 | +The two review-added features in the **"7-later" bucket** (duplicate |
| 641 | +detection on create, record merge) are deferred by the delivery review |
| 642 | +itself, not by this build, and neither gates 7a or 7b. |
| 643 | + |
593 | 644 | ## Definition of done |
594 | 645 |
|
595 | | -- A rep works a lead → conversion → opportunity → quote → won, entirely on |
596 | | - generated forms, on desktop and WASM, local and remote. |
597 | | -- A second user with a restricted role sees the same records with fields |
598 | | - hidden/read-only, enforced server-side. |
599 | | -- An admin adds a custom field at runtime; existing clients render it on |
600 | | - next schema fetch; its values persist, validate, and journal. |
| 646 | +Current state per bullet. The rung is server-complete; the two bullets that |
| 647 | +name a rendering client are unmet for the reason above. |
| 648 | + |
| 649 | +- **Not met** — no client. *A rep works a lead → conversion → opportunity → |
| 650 | + quote → won, entirely on generated forms, on desktop and WASM, local and |
| 651 | + remote.* Every action behind that flow is built and tested end to end |
| 652 | + (`test_lead_model.cpp`, `test_convert_lead.cpp`, `test_pipeline.cpp`, |
| 653 | + `test_quote_model.cpp`), and the forms are served; there is no renderer to |
| 654 | + work them on, on either platform. |
| 655 | +- **Met.** *A second user with a restricted role sees the same records with |
| 656 | + fields hidden/read-only, enforced server-side.* The served schema is |
| 657 | + shaped per caller (`test_authz_schema.cpp`) **and** the field is re-checked |
| 658 | + independently on write, so a client that ignores `x-readonly` is still |
| 659 | + refused (`test_account_authz.cpp`); history is redacted on the same terms |
| 660 | + (`test_account_history.cpp`). |
| 661 | +- **Partly met.** *An admin adds a custom field at runtime; existing clients |
| 662 | + render it on next schema fetch; its values persist, validate, and journal.* |
| 663 | + The schema grows to carry the new field, and its values persist, validate |
| 664 | + and journal (`test_custom_fields.cpp`, `test_custom_fields_7b.cpp`). The |
| 665 | + render-on-next-fetch half is untested, for the same reason as the first |
| 666 | + bullet. |
0 commit comments