From f884ba0b77016363f8863e6daf52066ec40b08da Mon Sep 17 00:00:00 2001 From: Tomasz Zajac Date: Fri, 25 Sep 2026 10:18:16 +0200 Subject: [PATCH] feat(hub): add UI mockups with low-fi wireframes to every blueprint Each of the seven Hub blueprints now carries three `mockup` nodes for its key user-facing screens, each with a hand-authored low-fi SVG wireframe in the same style the AI wireframe generator uses (grayscale, one accent). - illustrates -> the blueprint's own requirements (e.g. Checkout shows the provider-hosted card fields behind "No Card Data at Rest"; Case Review shows per-field extraction confidence below the review threshold) - presented-by -> the webapp / container that renders the screen - navigates-to -> the next screen in the same user flow - descriptions use the blueprint's template params, so they are substituted on import - placed in a new row below the ADRs; existing content is unchanged (the diff is purely additive) The Hub viewer (canvas / wiki / table) needed no changes. The blueprint import picker labelled groups from a hard-coded map, so mockups (and people) showed up as raw type ids; unknown types now fall back to the Hub theme's plural label. A new catalogue test checks every blueprint: mockups exist and each has a presented-by relation, mockup relations are valid per the governance metamodel, relation endpoints exist, and stored wireframes are already sanitised (sanitizeWireframeSvg leaves them unchanged). Co-Authored-By: Claude Opus 5.5 --- .../bp-ai-assistant-platform.radical | 94 ++++++++++++++++++ .../bp-analytical-data-platform.radical | 95 +++++++++++++++++++ hub/blueprint/bp-ecommerce-platform.radical | 95 +++++++++++++++++++ .../bp-fintech-ledger-platform.radical | 88 +++++++++++++++++ hub/blueprint/bp-marketplace-platform.radical | 81 ++++++++++++++++ .../bp-realtime-collaboration.radical | 95 +++++++++++++++++++ hub/blueprint/bp-saas-b2b-platform.radical | 95 +++++++++++++++++++ .../src/components/HubImportModal.tsx | 4 +- tests/hubCatalogue.test.ts | 33 +++++++ 9 files changed, 679 insertions(+), 1 deletion(-) diff --git a/hub/blueprint/bp-ai-assistant-platform.radical b/hub/blueprint/bp-ai-assistant-platform.radical index b3c52cf..0c4a045 100644 --- a/hub/blueprint/bp-ai-assistant-platform.radical +++ b/hub/blueprint/bp-ai-assistant-platform.radical @@ -404,6 +404,45 @@ "decision": "Keep the base model frozen and push knowledge updates through the retrieval index rather than fine-tuning on new content.", "consequences": "Knowledge freshness becomes index-refresh latency instead of training-cycle latency, but overall answer quality is now capped by retrieval quality, not just model quality.", "alternatives": "1. Periodic fine-tuning on new knowledge — keeps knowledge \"in\" the model, but the feedback loop is slow and risks catastrophic forgetting of earlier knowledge. 2. Retrieval only (chosen). 3. Retrieval plus light fine-tuning for tone and format only — a reasonable hybrid, at the cost of a second training pipeline. 4. Stuffing all knowledge into the prompt without retrieval — doesn't scale past a small, static document set." + }, + { + "id": "mockup-support-chat", + "type": "mockup", + "label": "Support Chat", + "description": "Customer-facing chat: the assistant answers with cited sources when it can ground the answer and hands off to a person when it cannot.", + "x": 0, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "chat widget", + "wireframe": "\n \n \n \n \n \n \n \n \n \n \n \n Support\n \n AI assistant\n \n \n \n \n \n \n \n \n Source: Returns policy §2\n \n \n \n Connecting you to a person —\n usually within a few minutes.\n \n Type a message…\n \n ➤\n" + }, + { + "id": "mockup-escalation-queue", + "type": "mockup", + "label": "Escalation Queue", + "description": "Human agents pick up escalated cases oldest-first; each case must be picked up within {{ESCALATION_SLA_MIN}} minutes.", + "x": 320, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/console/queue", + "wireframe": "\n \n \n \n Agent Console\n Knowledge\n My cases\n Queue (6)\n Escalated cases\n \n Pick up next\n \n Case\n Reason\n Waiting\n Confidence\n \n \n #4821\n Refund above auto-approve limit\n \n 12:40\n 62%\n \n #4826\n Low-confidence extraction\n \n 08:15\n 71%\n \n #4830\n Customer asked for a person\n \n 04:02\n —\n \n #4831\n \n \n 02:30\n \n \n #4833\n \n \n 00:48\n \n Oldest waiting first. Target pick-up time is shown on each case.\n \n Queue health\n Cases picked up within target today\n 70%\n \n \n" + }, + { + "id": "mockup-case-review", + "type": "mockup", + "label": "Case Review", + "description": "The assistant's proposed action with its confidence, per-field extraction confidence (below {{EXTRACTION_REVIEW_THRESHOLD_PCT}}% needs review) and the sources it used.", + "x": 640, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/console/cases/:id", + "wireframe": "\n \n \n \n Agent Console — Case #4821\n Back to queue\n \n \n \n \n \n \n \n \n \n \n \n \n \n Assistant proposal\n \n Refund €42.10 to the original card\n Confidence\n 62% — below threshold\n \n \n \n \n Extracted field\n Value\n Conf.\n \n Order id\n 98213\n 98%\n \n Amount\n €42.10\n \n 61%\n \n Order date\n 3 Mar\n 93%\n Sources: Returns policy §2 · Order 98213 · Payment log\n Decision, sources and scores go to the audit trail.\n \n Edit\n \n Approve & send\n" } ], "relations": [ @@ -571,6 +610,61 @@ "sourceId": "req-case-audit-trail", "targetId": "adr-confidence-gated-autonomy", "relationType": "traces-to" + }, + { + "id": "mockup-support-chat--req-escalation-pickup", + "sourceId": "mockup-support-chat", + "targetId": "req-escalation-pickup", + "relationType": "illustrates" + }, + { + "id": "mockup-support-chat--source-channel", + "sourceId": "mockup-support-chat", + "targetId": "source-channel", + "relationType": "presented-by" + }, + { + "id": "mockup-escalation-queue--req-escalation-pickup", + "sourceId": "mockup-escalation-queue", + "targetId": "req-escalation-pickup", + "relationType": "illustrates" + }, + { + "id": "mockup-escalation-queue--req-extraction-review", + "sourceId": "mockup-escalation-queue", + "targetId": "req-extraction-review", + "relationType": "illustrates" + }, + { + "id": "mockup-escalation-queue--agent-console", + "sourceId": "mockup-escalation-queue", + "targetId": "agent-console", + "relationType": "presented-by" + }, + { + "id": "mockup-escalation-queue--mockup-case-review", + "sourceId": "mockup-escalation-queue", + "targetId": "mockup-case-review", + "relationType": "navigates-to", + "label": "Pick up" + }, + { + "id": "mockup-case-review--req-extraction-review", + "sourceId": "mockup-case-review", + "targetId": "req-extraction-review", + "relationType": "illustrates" + }, + { + "id": "mockup-case-review--req-case-audit-trail", + "sourceId": "mockup-case-review", + "targetId": "req-case-audit-trail", + "relationType": "illustrates" + }, + { + "id": "mockup-case-review--agent-console", + "sourceId": "mockup-case-review", + "targetId": "agent-console", + "relationType": "presented-by" } ], "sequences": [ diff --git a/hub/blueprint/bp-analytical-data-platform.radical b/hub/blueprint/bp-analytical-data-platform.radical index 69bc63f..3398bba 100644 --- a/hub/blueprint/bp-analytical-data-platform.radical +++ b/hub/blueprint/bp-analytical-data-platform.radical @@ -369,6 +369,45 @@ "decision": "All curated-zone transformations are dbt models with at least one test and a description; Spark is reserved for raw ingestion and feature engineering.", "consequences": "Version control, testing and documentation by default; CI/CD required for model deployment; learning curve for SQL-only teams.", "alternatives": "1. Spark jobs only — flexible, no conventions for tests or lineage. 2. Stored procedures — vendor-coupled and hard to review." + }, + { + "id": "mockup-sales-dashboard", + "type": "mockup", + "label": "Sales Dashboard", + "description": "Self-service dashboard showing how fresh the data is; restricted columns are masked for users whose role does not allow them.", + "x": 0, + "y": 1534, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/dashboards/sales", + "wireframe": "\n \n \n \n BI\n Datasets\n Explore\n Dashboards\n Sales overview\n \n Region: EMEA ▾\n \n Last 30 days ▾\n \n Data as of 2 min ago\n \n Revenue\n €4.2M\n \n Orders\n 38,120\n \n Avg basket\n €110\n \n Revenue by day\n \n \n \n By channel\n \n \n \n \n \n \n Customer\n E-mail\n Lifetime value\n \n C-10293\n •••••• (restricted)\n €2,410\n \n C-10877\n •••••• (restricted)\n €1,980\n" + }, + { + "id": "mockup-dataset-lineage", + "type": "mockup", + "label": "Dataset & Lineage", + "description": "Catalogue page for a curated dataset with owner, classification and column-level lineage from source to dashboard.", + "x": 320, + "y": 1534, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/datasets/:name/lineage", + "wireframe": "\n \n \n \n Data Catalog\n Glossary\n Domains\n Search\n curated.orders\n \n Owner: Sales Data\n \n Confidential\n \n Contains PII\n Schema\n Lineage\n Quality\n \n \n \n raw.shop_orders\n order_id\n \n amount\n customer_id\n \n stg_orders\n order_id\n \n amount\n customer_id\n \n curated.orders\n order_id\n \n amount\n customer_id\n \n Sales overview\n order_id\n \n amount\n customer_id\n \n \n \n \n \n \n Column lineage for amount: raw.shop_orders.total → stg_orders.amount → curated.orders.amount\n \n Open in lineage API\n" + }, + { + "id": "mockup-quality-runs", + "type": "mockup", + "label": "Data Quality Runs", + "description": "Run results with the quality gate applied: failing records are quarantined, the affected models fail and the dataset owner is notified before anything reaches consumers.", + "x": 640, + "y": 1534, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/runs", + "wireframe": "\n \n \n \n Orchestrator\n Quarantine\n Runs\n Pipelines\n Transformation runs — today\n \n Model\n Checks\n Result\n Owner\n \n \n curated.orders\n 14 / 16\n \n failed — 132 quarantined\n notified ✓\n \n curated.customers\n 9 / 9\n \n passed\n —\n \n agg.daily_sales\n —\n \n skipped (upstream failed)\n —\n \n curated.products\n 11 / 11\n \n passed\n —\n \n Failed checks — curated.orders\n ✕ unique(order_id) — 132 duplicate rows\n ✕ relationships(customer_id → curated.customers) — 4 orphans\n Failing rows were quarantined; downstream models were not updated.\n \n Open quarantine\n \n Re-run\n" } ], "relations": [ @@ -506,6 +545,62 @@ "sourceId": "ff-quality", "targetId": "adr-dbt", "relationType": "implements" + }, + { + "id": "mockup-sales-dashboard--req-rls", + "sourceId": "mockup-sales-dashboard", + "targetId": "req-rls", + "relationType": "illustrates" + }, + { + "id": "mockup-sales-dashboard--req-freshness", + "sourceId": "mockup-sales-dashboard", + "targetId": "req-freshness", + "relationType": "illustrates" + }, + { + "id": "mockup-sales-dashboard--bi", + "sourceId": "mockup-sales-dashboard", + "targetId": "bi", + "relationType": "presented-by" + }, + { + "id": "mockup-sales-dashboard--mockup-dataset-lineage", + "sourceId": "mockup-sales-dashboard", + "targetId": "mockup-dataset-lineage", + "relationType": "navigates-to", + "label": "View dataset" + }, + { + "id": "mockup-dataset-lineage--req-lineage", + "sourceId": "mockup-dataset-lineage", + "targetId": "req-lineage", + "relationType": "illustrates" + }, + { + "id": "mockup-dataset-lineage--catalog", + "sourceId": "mockup-dataset-lineage", + "targetId": "catalog", + "relationType": "presented-by" + }, + { + "id": "mockup-dataset-lineage--mockup-quality-runs", + "sourceId": "mockup-dataset-lineage", + "targetId": "mockup-quality-runs", + "relationType": "navigates-to", + "label": "Quality" + }, + { + "id": "mockup-quality-runs--req-quality-gate", + "sourceId": "mockup-quality-runs", + "targetId": "req-quality-gate", + "relationType": "illustrates" + }, + { + "id": "mockup-quality-runs--orchestrator", + "sourceId": "mockup-quality-runs", + "targetId": "orchestrator", + "relationType": "presented-by" } ] } diff --git a/hub/blueprint/bp-ecommerce-platform.radical b/hub/blueprint/bp-ecommerce-platform.radical index 7eb5ed4..ec639df 100644 --- a/hub/blueprint/bp-ecommerce-platform.radical +++ b/hub/blueprint/bp-ecommerce-platform.radical @@ -492,6 +492,45 @@ "decision": "Each service owns its store; other services get data through APIs or events, never through the database.", "consequences": "Independent deployment and scaling; cross-service reports require projections or a data platform; joins across services are not possible in SQL.", "alternatives": "1. Shared database — simple joins, tight coupling. 2. Shared schema with per-service tables — coupling through migrations remains." + }, + { + "id": "mockup-product-page", + "type": "mockup", + "label": "Product Page", + "description": "Product detail with live availability from the catalogue; \"Add to cart\" leads into checkout.", + "x": 0, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/products/:slug", + "wireframe": "\n \n \n \n Shop\n Cart (2)\n Account\n Search\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Trail Running Shoe\n €129.00\n \n In stock — 12 left\n Size\n \n 38\n \n 39\n \n 40\n \n 41\n \n 42\n \n \n \n \n Add to cart\n You may also like\n \n \n \n \n \n \n \n \n \n \n \n \n" + }, + { + "id": "mockup-checkout", + "type": "mockup", + "label": "Checkout", + "description": "Final checkout step. Card fields are the provider's hosted fields, so card data never touches platform services; every step must answer within {{CHECKOUT_P95_MS}} ms at p95.", + "x": 320, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/checkout", + "wireframe": "\n \n \n \n Shop — Checkout\n Help\n \n 1 Address ✓\n \n 2 Shipping ✓\n \n 3 Payment\n Payment\n \n Card details — hosted by the payment provider\n Card number\n \n •••• •••• •••• ••••\n Expiry\n \n MM / YY\n CVC\n \n •••\n Card data goes straight to the provider — never through our servers.\n \n Pay €141.90\n ← Back to shipping\n \n Order summary\n \n \n \n \n \n \n \n \n \n \n \n Subtotal\n €129.00\n Shipping\n €4.90\n Tax\n €8.00\n Total\n €141.90\n" + }, + { + "id": "mockup-order-confirmation", + "type": "mockup", + "label": "Order Confirmation", + "description": "Shown once payment is captured. Refreshing or retrying never charges again — capture is keyed by the order id — and stock stays reserved for the order.", + "x": 640, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/orders/:id", + "wireframe": "\n \n \n \n Shop\n Cart (0)\n Account\n \n \n Thank you — order #10482 is placed\n Payment captured once · confirmation e-mail on its way\n \n Placed ✓\n \n Paid ✓\n \n Packed\n \n Shipped\n \n Delivered\n \n \n \n \n \n \n 1 ×\n €129.00\n \n \n \n \n \n 1 ×\n €4.90\n Items are reserved for you — nothing else can be sold against them.\n \n Track order\n \n Continue shopping\n" } ], "relations": [ @@ -715,6 +754,62 @@ "sourceId": "req-pci-scope", "targetId": "adr-hosted-payments", "relationType": "traces-to" + }, + { + "id": "mockup-product-page--storefront", + "sourceId": "mockup-product-page", + "targetId": "storefront", + "relationType": "presented-by" + }, + { + "id": "mockup-product-page--mockup-checkout", + "sourceId": "mockup-product-page", + "targetId": "mockup-checkout", + "relationType": "navigates-to", + "label": "Checkout" + }, + { + "id": "mockup-checkout--req-checkout-latency", + "sourceId": "mockup-checkout", + "targetId": "req-checkout-latency", + "relationType": "illustrates" + }, + { + "id": "mockup-checkout--req-pci-scope", + "sourceId": "mockup-checkout", + "targetId": "req-pci-scope", + "relationType": "illustrates" + }, + { + "id": "mockup-checkout--storefront", + "sourceId": "mockup-checkout", + "targetId": "storefront", + "relationType": "presented-by" + }, + { + "id": "mockup-checkout--mockup-order-confirmation", + "sourceId": "mockup-checkout", + "targetId": "mockup-order-confirmation", + "relationType": "navigates-to", + "label": "Pay" + }, + { + "id": "mockup-order-confirmation--req-payment-once", + "sourceId": "mockup-order-confirmation", + "targetId": "req-payment-once", + "relationType": "illustrates" + }, + { + "id": "mockup-order-confirmation--req-stock-reservation", + "sourceId": "mockup-order-confirmation", + "targetId": "req-stock-reservation", + "relationType": "illustrates" + }, + { + "id": "mockup-order-confirmation--storefront", + "sourceId": "mockup-order-confirmation", + "targetId": "storefront", + "relationType": "presented-by" } ] } diff --git a/hub/blueprint/bp-fintech-ledger-platform.radical b/hub/blueprint/bp-fintech-ledger-platform.radical index 307e04c..a63b174 100644 --- a/hub/blueprint/bp-fintech-ledger-platform.radical +++ b/hub/blueprint/bp-fintech-ledger-platform.radical @@ -392,6 +392,45 @@ "decision": "The internal journal is the source of truth for what the product shows the customer; the bank feed is an input reconciled against it, not the other way round.", "consequences": "A balance can update the instant a posting is accepted, before the bank settles — which means \"posted\" and \"reconciled\" must be visibly different states, or a customer or support agent will confuse a provisional balance with a settled one.", "alternatives": "1. Bank/processor as source of truth, product waits for their confirmation — correct by construction, but the customer's balance now lags the actual action by the settlement delay. 2. Internal ledger as source of truth (chosen). 3. Dual-write both, resolve conflicts on read — no clear authority, hardest of the three to reason about when they disagree." + }, + { + "id": "mockup-statement", + "type": "mockup", + "label": "Account Statement", + "description": "Customer statement generated strictly from balances replayed through a fixed cut-off; each line links to a dispute.", + "x": 0, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/accounts/:id/statement", + "wireframe": "\n \n \n \n Bank\n Help\n Statements\n Accounts\n Statement — March 2026\n Balances as of the cut-off: 31 Mar 2026, 23:59:59 UTC\n \n Opening balance\n €2,410.55\n \n Closing balance\n €1,982.13\n \n Date\n Description\n Amount\n \n \n 03 Mar\n Card — Green Grocer\n −42.10\n Dispute\n \n 07 Mar\n Salary — Acme Ltd\n +3,200.00\n Dispute\n \n 12 Mar\n Transfer to savings\n −500.00\n Dispute\n \n 18 Mar\n \n \n Dispute\n \n 24 Mar\n \n \n Dispute\n \n Download PDF\n" + }, + { + "id": "mockup-dispute", + "type": "mockup", + "label": "Dispute a Transaction", + "description": "Opens an investigation the moment it is submitted; the customer gets a resolution or explanation within {{DISPUTE_INVESTIGATION_DAYS}} days.", + "x": 320, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/disputes/new", + "wireframe": "\n \n \n \n Bank\n Help\n Statements\n Accounts\n Dispute a transaction\n \n 03 Mar 2026 · Card — Green Grocer · −€42.10\n What went wrong?\n \n I didn't make this payment\n \n \n I was charged the wrong amount\n \n I didn't receive the goods or service\n Tell us what happened\n \n Charged €42.10, receipt says €24.10…\n We'll open an investigation right away and reply within the investigation window.\n \n Cancel\n \n Submit dispute\n" + }, + { + "id": "mockup-reconciliation-exceptions", + "type": "mockup", + "label": "Reconciliation Exceptions", + "description": "Finance Ops queue of settlement events not matched within {{RECONCILIATION_LAG_MIN}} minutes. Corrections are compensating entries that must balance before they can be submitted — the journal itself is never edited.", + "x": 640, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/ops/exceptions", + "wireframe": "\n \n \n \n Finance Ops\n Reports\n Journal\n Exceptions (7)\n \n Unmatched 5\n \n Amount mismatch 2\n \n Older than 24 h 1\n \n Settlement ref\n Amount\n Age\n Status\n \n \n STL-88213\n 1,250.00\n 42 min\n \n unmatched\n \n STL-88207\n 980.40\n 1 h 10\n \n amount mismatch\n \n STL-88190\n 75.00\n 26 h\n \n older than 24 h\n \n STL-88188\n \n \n \n unmatched\n \n Compensating entry for STL-88213\n Debit account\n \n Suspense 1990\n \n \n 1,250.00\n Credit account\n \n Customer 4410-22\n \n \n 1,250.00\n \n Balanced ✓\n \n Submit for approval\n" } ], "relations": [ @@ -552,6 +591,55 @@ "sourceId": "adr-ledger-as-system-of-record", "targetId": "balance-projector", "relationType": "constrains" + }, + { + "id": "mockup-statement--req-statement-accuracy-cutoff", + "sourceId": "mockup-statement", + "targetId": "req-statement-accuracy-cutoff", + "relationType": "illustrates" + }, + { + "id": "mockup-statement--statement-service", + "sourceId": "mockup-statement", + "targetId": "statement-service", + "relationType": "presented-by" + }, + { + "id": "mockup-statement--mockup-dispute", + "sourceId": "mockup-statement", + "targetId": "mockup-dispute", + "relationType": "navigates-to", + "label": "Dispute" + }, + { + "id": "mockup-dispute--req-dispute-investigation-sla", + "sourceId": "mockup-dispute", + "targetId": "req-dispute-investigation-sla", + "relationType": "illustrates" + }, + { + "id": "mockup-dispute--statement-service", + "sourceId": "mockup-dispute", + "targetId": "statement-service", + "relationType": "presented-by" + }, + { + "id": "mockup-reconciliation-exceptions--req-reconciliation-freshness", + "sourceId": "mockup-reconciliation-exceptions", + "targetId": "req-reconciliation-freshness", + "relationType": "illustrates" + }, + { + "id": "mockup-reconciliation-exceptions--req-balanced-entry-enforcement", + "sourceId": "mockup-reconciliation-exceptions", + "targetId": "req-balanced-entry-enforcement", + "relationType": "illustrates" + }, + { + "id": "mockup-reconciliation-exceptions--finance-ops-console", + "sourceId": "mockup-reconciliation-exceptions", + "targetId": "finance-ops-console", + "relationType": "presented-by" } ], "sequences": [ diff --git a/hub/blueprint/bp-marketplace-platform.radical b/hub/blueprint/bp-marketplace-platform.radical index cc04e7f..abe62fc 100644 --- a/hub/blueprint/bp-marketplace-platform.radical +++ b/hub/blueprint/bp-marketplace-platform.radical @@ -414,6 +414,45 @@ "decision": "Suspend first on a high-confidence signal — hide from search, freeze payouts — then investigate and either reinstate or ban, rather than investigating first and acting after.", "consequences": "Buyers are protected as fast as the platform can detect a problem; the investigation SLA becomes a trust-critical metric in its own right, since it's now the only thing standing between a wrongly-flagged seller and lost income.", "alternatives": "1. Investigate first, act after — protects sellers from false positives, but a genuine bad actor keeps selling and collecting funds during the investigation window. 2. Suspend first, investigate after (chosen). 3. Community reporting only, no proactive detection — cheapest, but slowest and worst for buyer trust. 4. Permanent ban on first signal, no reinstatement path — fastest, but the harshest possible cost of a false positive." + }, + { + "id": "mockup-new-listing", + "type": "mockup", + "label": "New Listing", + "description": "Seller creates a listing; it goes into review and only appears in search once approved (within {{LISTING_REVIEW_SLA_HOURS}} h).", + "x": 0, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/sell/listings/new", + "wireframe": "\n \n \n \n Marketplace — Sell\n Payouts\n My listings\n Create a listing\n \n + Add photos\n up to 10\n Title\n \n Mid-century armchair\n Price\n \n € 180.00\n Category\n \n Furniture\n \n Description\n \n Condition, dimensions, pickup…\n \n Listings are reviewed against marketplace policy before they appear in search.\n You keep the price minus the platform commission.\n \n Save draft\n \n Submit for review\n" + }, + { + "id": "mockup-listing-review-queue", + "type": "mockup", + "label": "Listing Review Queue", + "description": "Trust & Safety queue sorted by time left on the review SLA. Rejecting a listing or suspending a seller removes it from search within {{SUPPRESSION_MINUTES}} minutes.", + "x": 320, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/trust/listings", + "wireframe": "\n \n \n \n Trust & Safety\n Disputes\n Sellers\n Listings (14)\n \n \n \n \n \n \n \n \n 3 h left\n \n \n \n \n \n \n 7 h left\n \n \n \n \n \n \n 11 h left\n \n \n \n \n \n \n 19 h left\n \n \n \n \n \n \n 22 h left\n \n \n \n Mid-century armchair\n €180 · seller VintageCo\n \n New seller\n Policy checks\n \n \n Photos show the actual item\n \n \n Price and description are consistent\n \n No prohibited category or keywords\n Suspending a seller removes all their listings from search.\n \n Reject…\n \n Suspend\n \n Approve\n" + }, + { + "id": "mockup-order-dispute", + "type": "mockup", + "label": "Order & Buyer Protection", + "description": "Buyer view of an order: funds stay held for {{DISPUTE_WINDOW_DAYS}} days after delivery, and opening a dispute freezes the payout.", + "x": 640, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/orders/:id", + "wireframe": "\n \n \n \n Marketplace\n Account\n Orders\n Search\n Order #58213\n Sold by VintageCo · delivered 2 Apr\n \n \n \n \n \n €180.00\n Where your money is\n \n Paid ✓\n \n Delivered ✓\n \n Held for buyer protection\n \n Released to seller\n Buyer protection window\n 3 of 7 days\n \n \n Something wrong? Opening a dispute freezes the payout until it is resolved.\n \n Open a dispute\n \n Confirm all good\n" } ], "relations": [ @@ -574,6 +613,48 @@ "sourceId": "adr-suspend-first-moderation", "targetId": "trust-safety-console", "relationType": "constrains" + }, + { + "id": "mockup-new-listing--req-listing-moderation", + "sourceId": "mockup-new-listing", + "targetId": "req-listing-moderation", + "relationType": "illustrates" + }, + { + "id": "mockup-new-listing--listings-service", + "sourceId": "mockup-new-listing", + "targetId": "listings-service", + "relationType": "presented-by" + }, + { + "id": "mockup-listing-review-queue--req-listing-moderation", + "sourceId": "mockup-listing-review-queue", + "targetId": "req-listing-moderation", + "relationType": "illustrates" + }, + { + "id": "mockup-listing-review-queue--req-delisting-suppression", + "sourceId": "mockup-listing-review-queue", + "targetId": "req-delisting-suppression", + "relationType": "illustrates" + }, + { + "id": "mockup-listing-review-queue--trust-safety-console", + "sourceId": "mockup-listing-review-queue", + "targetId": "trust-safety-console", + "relationType": "presented-by" + }, + { + "id": "mockup-order-dispute--req-dispute-window-hold", + "sourceId": "mockup-order-dispute", + "targetId": "req-dispute-window-hold", + "relationType": "illustrates" + }, + { + "id": "mockup-order-dispute--order-checkout-service", + "sourceId": "mockup-order-dispute", + "targetId": "order-checkout-service", + "relationType": "presented-by" } ], "sequences": [ diff --git a/hub/blueprint/bp-realtime-collaboration.radical b/hub/blueprint/bp-realtime-collaboration.radical index 299769e..571ab96 100644 --- a/hub/blueprint/bp-realtime-collaboration.radical +++ b/hub/blueprint/bp-realtime-collaboration.radical @@ -357,6 +357,45 @@ "decision": "Gateways hold no document state themselves; they publish and subscribe to operations through a shared broadcast bus, so any instance can serve any client.", "consequences": "Gateway instances scale and restart freely with no handoff logic, but every operation takes an extra hop through the bus, adding a small amount of latency to the common case.", "alternatives": "1. Sticky sessions routing all of a document's clients to one gateway instance — lower latency per operation, but that instance becomes a single point of failure and a hot-spot for popular documents. 2. Shared broadcast bus (chosen). 3. Peer-to-peer mesh between clients — no server bottleneck, but NAT traversal and offline persistence become much harder to get right." + }, + { + "id": "mockup-document-editor", + "type": "mockup", + "label": "Document Editor", + "description": "Everyone edits a local replica: changes apply instantly, keep working offline (up to {{OFFLINE_GRACE_HOURS}} h) and merge on reconnect; presence shows up to {{MAX_DOC_COLLABORATORS}} collaborators.", + "x": 0, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/d/:docId", + "wireframe": "\n \n \n \n Q3 Planning\n \n \n \n \n +3\n \n Share\n \n \n \n \n \n \n \n \n \n Offline — edits saved locally\n Q3 Planning\n \n \n \n Goals\n \n \n \n \n Ana\n Risks\n \n \n \n History\n" + }, + { + "id": "mockup-share-dialog", + "type": "mockup", + "label": "Share & Access", + "description": "Workspace admins manage who can open the document; removing someone disconnects their live session instead of waiting for it to expire.", + "x": 320, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/d/:docId/share", + "wireframe": "\n \n \n \n Share “Q3 Planning”\n \n Add people or groups\n \n Invite\n \n \n \n Owner\n \n \n \n \n Can edit\n \n Remove\n \n \n \n \n Can edit\n \n Remove\n \n \n \n \n Can view\n \n Remove\n Link access\n \n Only people in the workspace\n \n Removing someone disconnects their open session immediately.\n" + }, + { + "id": "mockup-version-history", + "type": "mockup", + "label": "Version History", + "description": "Browse and restore any earlier state of the document, reconstructed from snapshots plus the operation log.", + "x": 640, + "y": 2084, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/d/:docId/history", + "wireframe": "\n \n \n \n Q3 Planning — Version history\n Close\n Q3 Planning\n \n \n \n \n \n \n \n \n \n \n \n \n Today 14:02\n Ana, Ben\n \n Today 11:40\n Ben\n Yesterday 17:15\n Ana, Chris, +2\n Yesterday 09:03\n Chris\n 2 Apr 16:48\n Ana\n \n Restore this version\n" } ], "relations": [ @@ -496,6 +535,62 @@ "sourceId": "req-room-capacity", "targetId": "adr-stateless-gateway-fleet", "relationType": "traces-to" + }, + { + "id": "mockup-document-editor--req-offline-merge", + "sourceId": "mockup-document-editor", + "targetId": "req-offline-merge", + "relationType": "illustrates" + }, + { + "id": "mockup-document-editor--req-room-capacity", + "sourceId": "mockup-document-editor", + "targetId": "req-room-capacity", + "relationType": "illustrates" + }, + { + "id": "mockup-document-editor--editor-client", + "sourceId": "mockup-document-editor", + "targetId": "editor-client", + "relationType": "presented-by" + }, + { + "id": "mockup-document-editor--mockup-share-dialog", + "sourceId": "mockup-document-editor", + "targetId": "mockup-share-dialog", + "relationType": "navigates-to", + "label": "Share" + }, + { + "id": "mockup-document-editor--mockup-version-history", + "sourceId": "mockup-document-editor", + "targetId": "mockup-version-history", + "relationType": "navigates-to", + "label": "History" + }, + { + "id": "mockup-share-dialog--req-access-revocation", + "sourceId": "mockup-share-dialog", + "targetId": "req-access-revocation", + "relationType": "illustrates" + }, + { + "id": "mockup-share-dialog--editor-client", + "sourceId": "mockup-share-dialog", + "targetId": "editor-client", + "relationType": "presented-by" + }, + { + "id": "mockup-version-history--req-snapshot-recovery", + "sourceId": "mockup-version-history", + "targetId": "req-snapshot-recovery", + "relationType": "illustrates" + }, + { + "id": "mockup-version-history--editor-client", + "sourceId": "mockup-version-history", + "targetId": "editor-client", + "relationType": "presented-by" } ], "sequences": [ diff --git a/hub/blueprint/bp-saas-b2b-platform.radical b/hub/blueprint/bp-saas-b2b-platform.radical index a3123f4..6f84d19 100644 --- a/hub/blueprint/bp-saas-b2b-platform.radical +++ b/hub/blueprint/bp-saas-b2b-platform.radical @@ -440,6 +440,45 @@ "decision": "After a successful SSO handshake, the gateway issues its own short-lived, tenant-scoped JWT. The IdP is the source of truth for the identity; the platform never stores the tenant's SSO credentials.", "consequences": "A tenant revoking a user at their IdP takes effect on the token's next short expiry, not instantly, which is an accepted trade-off; token issuance adds one extra hop to first login.", "alternatives": "Storing IdP session cookies directly was rejected — it would tie the platform's session lifetime to each IdP's own cookie semantics and leak IdP-specific state into the app tier." + }, + { + "id": "mockup-signup", + "type": "mockup", + "label": "Sign Up", + "description": "Self-serve sign-up; the new tenant gets an isolated, ready-to-use workspace within {{PROVISION_MINUTES}} minutes.", + "x": 0, + "y": 2324, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/signup", + "wireframe": "\n \n \n \n \n Start your free trial\n Work e-mail\n \n jane@acme.com\n Company\n \n Acme Inc.\n Workspace URL\n \n acme.app.example\n \n Create workspace\n or\n \n Sign up with SSO\n Your workspace is ready in a couple of minutes.\n" + }, + { + "id": "mockup-workspace-dashboard", + "type": "mockup", + "label": "Workspace Dashboard", + "description": "Tenant home. When a payment fails the workspace turns read-only — data stays visible, writes are blocked — until billing is fixed.", + "x": 320, + "y": 2324, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/", + "wireframe": "\n \n \n \n Acme ▾\n Settings\n Reports\n Projects\n \n Payment failed — the workspace is read-only until billing is updated.\n Update billing\n \n Active projects\n 12\n \n Members\n 48\n \n API calls this month\n 812k\n Projects\n \n + New project\n \n Name\n Owner\n Updated\n \n \n \n 2 h ago\n \n \n \n yesterday\n \n \n \n 3 days ago\n \n \n \n last week\n \n \n \n last week\n" + }, + { + "id": "mockup-billing-usage", + "type": "mockup", + "label": "Billing & Usage", + "description": "Tenant admin view of plan, metered usage and invoices; usage figures are recorded exactly once and reconciled against source logs.", + "x": 640, + "y": 2324, + "width": 220, + "height": 190, + "collapsed": false, + "screen": "/settings/billing", + "wireframe": "\n \n \n \n Acme ▾\n Settings\n Reports\n Projects\n \n Profile\n Users\n Single sign-on\n \n Billing & usage\n Audit log\n Billing & usage\n \n Business plan\n €49 per seat / month · 48 seats\n \n Change plan\n API calls\n 812k / 1M\n \n \n Storage\n 38 / 100 GB\n \n \n Usage is metered per event and reconciled daily against source logs.\n \n Invoice\n Amount\n Status\n \n Mar 2026\n €2,352.00\n \n payment failed\n \n Feb 2026\n €2,254.00\n \n paid\n \n Update payment method\n" } ], "relations": [ @@ -561,6 +600,62 @@ "targetId": "email-provider", "label": "Sends transactional email", "technology": "HTTPS" + }, + { + "id": "mockup-signup--req-tenant-provisioning-time", + "sourceId": "mockup-signup", + "targetId": "req-tenant-provisioning-time", + "relationType": "illustrates" + }, + { + "id": "mockup-signup--webapp", + "sourceId": "mockup-signup", + "targetId": "webapp", + "relationType": "presented-by" + }, + { + "id": "mockup-signup--mockup-workspace-dashboard", + "sourceId": "mockup-signup", + "targetId": "mockup-workspace-dashboard", + "relationType": "navigates-to", + "label": "Create workspace" + }, + { + "id": "mockup-workspace-dashboard--req-subscription-enforcement", + "sourceId": "mockup-workspace-dashboard", + "targetId": "req-subscription-enforcement", + "relationType": "illustrates" + }, + { + "id": "mockup-workspace-dashboard--webapp", + "sourceId": "mockup-workspace-dashboard", + "targetId": "webapp", + "relationType": "presented-by" + }, + { + "id": "mockup-workspace-dashboard--mockup-billing-usage", + "sourceId": "mockup-workspace-dashboard", + "targetId": "mockup-billing-usage", + "relationType": "navigates-to", + "label": "Update billing" + }, + { + "id": "mockup-billing-usage--req-usage-metering-accuracy", + "sourceId": "mockup-billing-usage", + "targetId": "req-usage-metering-accuracy", + "relationType": "illustrates" + }, + { + "id": "mockup-billing-usage--req-subscription-enforcement", + "sourceId": "mockup-billing-usage", + "targetId": "req-subscription-enforcement", + "relationType": "illustrates" + }, + { + "id": "mockup-billing-usage--webapp", + "sourceId": "mockup-billing-usage", + "targetId": "webapp", + "relationType": "presented-by" } ] } diff --git a/src/renderer/src/components/HubImportModal.tsx b/src/renderer/src/components/HubImportModal.tsx index f8fc2d1..70d6c0a 100644 --- a/src/renderer/src/components/HubImportModal.tsx +++ b/src/renderer/src/components/HubImportModal.tsx @@ -5,6 +5,7 @@ import { useDiagramStore } from '../store/diagramStore' import type { C4Node, C4Relation, C4ElementType, DiagramSequence } from '../types/c4' import { NODE_SIZES } from '../types/c4' import { isParentAllowed } from '../types/metamodel' +import { nodeTypeTheme } from '../types/hubTheme' // ─── Props ────────────────────────────────────────────────────────────────── @@ -979,11 +980,12 @@ export function HubImportModal({ open, onClose, preselectedIds }: Props): React. component: 'Components', database: 'Databases', webapp: 'Web Apps', queue: 'Queues', domain: 'Domains', group: 'Groups', adr: 'ADRs', 'fitness-fn': 'Fitness Functions', requirement: 'Requirements', + person: 'People', } return Object.entries(grouped).filter(([type]) => type !== 'blueprint').map(([type, nodes]) => (
- {TYPE_LABELS_LOCAL[type] ?? type} ({nodes.length}) + {TYPE_LABELS_LOCAL[type] ?? nodeTypeTheme(type).plural} ({nodes.length})
{nodes.map((n) => { const nodeId = n.id as string diff --git a/tests/hubCatalogue.test.ts b/tests/hubCatalogue.test.ts index 164bae4..9f991f0 100644 --- a/tests/hubCatalogue.test.ts +++ b/tests/hubCatalogue.test.ts @@ -4,12 +4,16 @@ * - every top-level hub//.radical is valid (hub block, ids, refs) * - index.json summaries carry what the cards / drop-target check need * - doc ↔ concept mapping round-trips + * - blueprint mockups are wired up with metamodel-valid relations and + * carry wireframes that are already sanitised */ import { describe, it, expect } from 'vitest' import { resolve } from 'node:path' import { readCatalogue, validateCatalogue, buildIndex } from '../tools/hubCatalogue' import { conceptToDoc, docToConcept, summarize, type HubRadicalDoc } from '../src/renderer/src/hub/hubFormat' import { useDiagramStore } from '../src/renderer/src/store/diagramStore' +import { builtInGovernanceMetamodel } from '../src/renderer/src/types/metamodel' +import { sanitizeWireframeSvg } from '../src/renderer/src/ai/mockupWireframe' const HUB_DIR = resolve(__dirname, '../hub') @@ -80,3 +84,32 @@ describe('studio round-trip', () => { expect(useDiagramStore.getState().saveDiagram().hub).toBeUndefined() }) }) + +describe('blueprint mockups', () => { + const blueprints = readCatalogue(HUB_DIR).filter((e) => e.doc.hub.category === 'blueprint') + const mm = builtInGovernanceMetamodel() + + it.each(blueprints.map((e) => [e.file, e.doc] as const))('%s has wired-up mockups with sanitised wireframes', (_file, doc) => { + const byId = new Map(doc.nodes.map((n) => [String(n.id), n])) + const mockups = doc.nodes.filter((n) => n.type === 'mockup') + expect(mockups.length).toBeGreaterThan(0) + + for (const m of mockups) { + const wireframe = m.wireframe as string + expect(typeof m.screen).toBe('string') + expect(sanitizeWireframeSvg(wireframe)).toBe(wireframe) + const out = (doc.relations ?? []).filter((r) => r.sourceId === m.id) + expect(out.filter((r) => r.relationType === 'presented-by')).toHaveLength(1) + } + + for (const r of doc.relations ?? []) { + const src = byId.get(r.sourceId) + const tgt = byId.get(r.targetId) + expect(src, `${r.id}: missing source`).toBeDefined() + expect(tgt, `${r.id}: missing target`).toBeDefined() + if (src!.type !== 'mockup') continue + const pairs = mm.relationTypes[r.relationType ?? '']?.allowedPairs ?? [] + expect(pairs.some((p) => p.from === src!.type && p.to === tgt!.type), `${r.id}: ${r.relationType} ${src!.type} → ${tgt!.type}`).toBe(true) + } + }) +})