Skip to content

Commit 1bd3223

Browse files
Merge pull request #53 from programmerShinobi/feat/phase2g-advisor-export-footer-polish
[feat](advisor): Master Blueprint — Scenario Gallery + Custom Wizard + honest Step-3 stepper + UI polish + README
2 parents b184476 + 2ef4e65 commit 1bd3223

19 files changed

Lines changed: 1290 additions & 92 deletions

DECISIONS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,36 @@ below is content or presentation, never scoring.
317317
the light theme (elegant), automatically light in dark contexts via `currentColor` (in-app) and
318318
an SVG-embedded `prefers-color-scheme` rule (favicon), so it is never invisible. PWA PNG icons
319319
keep a solid tile (maskable requirement): black compass on a soft white tile.
320+
321+
## Custom Architecture Wizard + honest Step-3 analysis stepper (Master Blueprint, 2026-07-18)
322+
323+
Two decisions taken with the owner (AskUserQuestion) while executing the "Master Blueprint" overhaul.
324+
Both are chosen precisely to preserve the Zero-Mismatch invariant.
325+
326+
- **The Custom Wizard maps onto the FROZEN engine — one scoring model, not two.** The guided builder
327+
captures four universal variables (Primary System Goal · Domain/Industry · Hard Constraints
328+
[budget/team/timeline] · prioritized NFRs) but does **not** score them itself. A pure, unit-tested
329+
bridge (`src/lib/customWizard.ts``wizardToLevels`) derives the frozen model's 14 factor levels
330+
from the answers; the same `rank()` does the scoring. A **parallel 4-variable model was rejected**
331+
— it would duplicate decision logic, sit outside the frozen guards, and create two sources of truth
332+
to keep in sync (the exact long-term debt the blueprint forbids). The wizard **schema is typed,
333+
injectable config** (`src/config/customWizard.ts`): new options/questions inject there and the UI
334+
+ mapping iterate it — no component edits (Blueprint Phase 1.1). Combine rule: ordered override
335+
from a **moderate baseline** (all factors = 1), Goal → Domain → NFRs → Constraints, so an explicit
336+
hard constraint always wins and a vague/empty wizard still yields a valid, balanced recommendation
337+
(Phase 3.2 resilience). Every nudge is validated against the model by a unit test.
338+
- **The Step-3 "processing" state is HONEST — it never fakes latency.** The blueprint asked for a
339+
"Progressive Terminal Readout"; the engine computes in <1 ms, and this file already records that
340+
the prototype's *artificial* skeleton-on-recompute was omitted as misleading. Reconciliation
341+
(`AnalysisStepper.tsx`): on an **explicit analyze action** (applying a preset card or the wizard)
342+
it briefly surfaces the **real** pipeline stages the engine actually runs — *derive QA weights →
343+
score 21 options → check anti-patterns → rank dimensions* — as a developer-centric terminal
344+
readout, then reveals the result. It is **skippable**, and under `prefers-reduced-motion` it
345+
renders nothing (instant). **Live factor edits stay instant** (no stepper) — the reveal is reserved
346+
for the "run a scenario" gesture. So this *extends* the honesty stance (real stages, real values)
347+
rather than reversing it; a full theatrical delay was explicitly declined.
348+
- **The Scenario Card Gallery supersedes the preset dropdown** (Blueprint Phase 1.3): searchable,
349+
tag-filterable cards with a **dominant, dashed, terminal-styled "Build custom system"** card.
350+
Preset tags (`src/config/presetTags.ts`) are **pure UI metadata** kept out of `presets.ts`, so the
351+
frozen preset-level guards are untouched. The wizard modal is **lazy-loaded**, keeping the initial
352+
Advisor bundle under budget.

README.md

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,32 @@ managed — is decided early, hard to reverse, and disproportionately shapes the
4444
quality. In practice these decisions are too often made by trend rather than by an explicit
4545
trade-off analysis.
4646

47-
**Architecture Advisor** is a planned, fully client-side web app that turns that decision into
48-
a transparent pipeline:
49-
50-
```
51-
PROJECT FACTORS ─► QUALITY-ATTRIBUTE PRIORITIES ─► ARCHITECTURE FIT ─► ANALYSIS
52-
(drivers & (a weighted "utility tree" of (how well each option (trade-offs, risks,
53-
constraints) quality attributes, grounded satisfies the sensitivity, fitness
54-
in ISO/IEC 25010:2023) prioritized QAs across functions, ADR/report)
55-
5 orthogonal dimensions)
47+
**Architecture Advisor** is a fully client-side web app that turns that decision into a
48+
transparent pipeline:
49+
50+
```mermaid
51+
flowchart LR
52+
F["PROJECT FACTORS<br/><i>drivers &amp; constraints</i>"]
53+
--> Q["QUALITY-ATTRIBUTE PRIORITIES<br/><i>weighted utility tree,<br/>ISO/IEC 25010:2023</i>"]
54+
--> A["ARCHITECTURE FIT<br/><i>how well each option satisfies<br/>the priorities, across 5 dimensions</i>"]
55+
--> O["ANALYSIS &amp; OUTPUT<br/><i>trade-offs, risks, sensitivity,<br/>fitness functions, ADR / report</i>"]
5656
```
5757

5858
You answer a handful of questions about your project; the tool recommends an architecture
5959
across five dimensions, ranks the alternatives, and — critically — **shows the full
6060
calculation**: which factor raised which quality attribute, and how that produced the score.
6161
Experts get auditable numbers and editable weights; newcomers get plain-language explanations.
6262

63+
**Two ways in.** Start from a **Scenario Card Gallery** — searchable, tag-filterable cards for
64+
ten pre-built scenarios (startup MVP, regulated fintech, high-traffic e-commerce, IoT streaming,
65+
legacy modernization, …) — or open the **Custom Architecture Wizard** (the dominant dashed
66+
"Build custom system" card): a foolproof guided builder that captures four universal variables
67+
(Primary System Goal · Domain/Industry · Hard Constraints · prioritized NFRs) and maps them onto
68+
the **same frozen scoring engine** — there is exactly one scoring model, never a parallel one.
69+
Running a scenario briefly surfaces the engine's **real** analysis stages as a developer terminal
70+
readout (weights → score 21 options → anti-patterns → rank), then reveals the result — no fake
71+
latency, and instant under `prefers-reduced-motion`.
72+
6373
It adapts established methods — **ISO/IEC 25010:2023**, **ATAM**, **Attribute-Driven Design**,
6474
and **evolutionary-architecture fitness functions** — into an interactive tool, and is honest
6575
about uncertainty: scores are *tunable heuristics, not facts*.
@@ -100,6 +110,42 @@ see the [content rollout plan](docs/03-blueprint/content-rollout-plan.md).
100110
> [evolution roadmap](docs/01-discovery-and-planning/discovery-and-planning.md#15-versioning-policy--evolution-roadmap)
101111
> for what's deferred beyond v1.0.
102112
113+
## Workflow — the immutable four-step journey
114+
115+
Every scenario flows through the same four steps; **Step 3 is the engine** (the "professional
116+
analysis"). The flow never changes — only the inputs do.
117+
118+
```mermaid
119+
flowchart LR
120+
subgraph S1["① Scenario &amp; factors"]
121+
G["Scenario Card Gallery<br/>(search · tags)"]
122+
W["Custom Wizard<br/>(4 variables)"]
123+
F["14 project factors"]
124+
G --> F
125+
W -->|"wizardToLevels()"| F
126+
end
127+
subgraph S2["② Quality priorities"]
128+
QA["Derived ISO/IEC 25010<br/>weights (editable)"]
129+
end
130+
subgraph S3["③ Professional analysis"]
131+
E["Frozen scoring engine<br/>+ honest analysis stepper"]
132+
end
133+
subgraph S4["④ Strategic output"]
134+
R["Recommendation · radar ·<br/>sensitivity · ADR / report / share"]
135+
end
136+
S1 --> S2 --> S3 --> S4
137+
```
138+
139+
- **① Scenario & factors** — pick a preset card, build a custom system with the wizard, or set the
140+
14 factors directly. The wizard and presets both resolve to the same 14 factor levels.
141+
- **② Quality priorities** — the engine derives the twelve ISO/IEC 25010 quality-attribute weights;
142+
experts can pin/override any weight (the rest re-balance around it).
143+
- **③ Professional analysis** — the frozen engine scores all 21 architectures across the five
144+
dimensions. A brief, skippable terminal stepper surfaces the real pipeline stages, then the
145+
ranked result appears. **Live factor edits recompute instantly** (no stepper).
146+
- **④ Strategic output** — the recommendation with a trade-off radar, sensitivity, anti-pattern
147+
warnings and migration paths; export an ADR (MADR), a full report, CSV/JSON, or a share link.
148+
103149
## Run it locally
104150

105151
> **Prerequisite:** Node **24** (LTS) — the version is pinned in [`.nvmrc`](.nvmrc) and used by all
@@ -128,7 +174,7 @@ traceable:
128174
| 2 | [Requirement Analysis](docs/02-requirement-analysis/) | [SRS](docs/02-requirement-analysis/software-requirements-specification.md) | 🔬 In progress |
129175
| 3 | [Blueprint (Design)](docs/03-blueprint/) | [Design spec](docs/03-blueprint/design-specification.md) + [Model Data Sheet](docs/03-blueprint/model-data-sheet.md) + [Architecture Reader](docs/03-blueprint/architecture-reader.md) + [Content Rollout Plan](docs/03-blueprint/content-rollout-plan.md) + [UI prototype](docs/03-blueprint/prototype/index.html) | 🔬 In progress |
130176
| 4 | [Development](docs/04-development/) | Source code (`src/`, scoring engine, components) | ✅ v1.0 implemented |
131-
| 5 | [Testing / QA](docs/05-testing-qa/) | [Test plan](docs/05-testing-qa/test-plan.md)99 Vitest + Playwright E2E + 3 model guards + content guard; CI gates size/audit; 14/16 AC automated | 🔬 In progress |
177+
| 5 | [Testing / QA](docs/05-testing-qa/) | [Test plan](docs/05-testing-qa/test-plan.md)127 Vitest + 14 Playwright E2E + 6 guards (model / docs / app-config / content / bundle-size / SEO); CI gates size/axe; 14/16 AC automated | 🔬 In progress |
132178
| 6 | [Deployment / Release](docs/06-deployment/) | [Live on GitHub Pages](https://programmershinobi.github.io/architecture-advisor/) via `deploy.yml` (CI/CD) | ✅ Live |
133179
| 7 | [Maintenance & Iteration](docs/07-maintenance/) | [Changelog](CHANGELOG.md), Dependabot, issue/PR templates, [security policy](SECURITY.md) | 🔄 Ongoing |
134180

@@ -169,6 +215,82 @@ The canonical statement is [SRS §2.3 — Operating Environment](docs/02-require
169215
5. **Actionable & shareable** — export an ADR (MADR) and a full report; share via URL.
170216
6. **Open & evolving** — community-built, improving across versions.
171217

218+
## Extending — scenarios, the Custom Wizard & the Step-3 UI
219+
220+
Everything below is **typed, injectable config** — you add data, not logic. The frozen scoring
221+
model (`src/config/{factors,dimensions,qualityAttributes,factorQaMatrix}.ts`) is verified by
222+
`npm run verify:model` and must not change; new *scenarios* never touch it.
223+
224+
### Add a preset scenario
225+
226+
Presets are strictly-typed `Preset[]`. Append one to [`src/config/presets.ts`](src/config/presets.ts)
227+
and give it filter tags in [`src/config/presetTags.ts`](src/config/presetTags.ts) — the Card Gallery
228+
picks it up automatically. Helper presets are `calibrated: false`; the five ratified ones stay
229+
`true` and are bit-pinned by the guards.
230+
231+
```ts
232+
// src/config/presets.ts
233+
{
234+
id: 'edge-cdn',
235+
label: { en: 'Edge / CDN app', id: 'Aplikasi edge / CDN' },
236+
description: { en: 'Globally distributed, latency-critical, cache-heavy.', id: '' },
237+
levels: levels([1, 2, 1, 1, 2, 2, 1, 1, 2, 1, 0, 1, 0, 2]), // 14 factors, values 0–2
238+
calibrated: false,
239+
},
240+
241+
// src/config/presetTags.ts
242+
export const PRESET_TAGS = { /**/ 'edge-cdn': ['high-scale', 'realtime'] };
243+
```
244+
245+
A unit test (`src/config/presets.test.ts`) pins each helper preset's engine outcome, so a bad
246+
scenario fails the build rather than silently drifting.
247+
248+
### Add / modify a Custom Wizard question
249+
250+
The wizard is driven entirely by [`src/config/customWizard.ts`](src/config/customWizard.ts). Each
251+
option declares the factor **nudges** it applies; the pure bridge
252+
[`src/lib/customWizard.ts`](src/lib/customWizard.ts) combines them (ordered override from a moderate
253+
baseline, hard constraints last) into the 14 factor levels the frozen engine scores. **No component
254+
edits are needed**`CustomWizard.tsx` iterates this config.
255+
256+
```ts
257+
// src/config/customWizard.ts → add an option to the "goal" question
258+
{
259+
id: 'batch-etl',
260+
label: { en: 'Run heavy batch / ETL', id: 'Jalankan batch / ETL berat' },
261+
hint: { en: 'Scheduled data pipelines at volume', id: '' },
262+
levels: { dataVolume: 2, async: 2, scale: 2, realtime: 0 }, // only real factor ids, 0–2
263+
},
264+
```
265+
266+
`wizardToLevels()` is unit-tested (`src/lib/customWizard.test.ts`): every nudge must reference a
267+
real factor id at a valid level, and the output is always a complete, valid `Levels` object — even
268+
when the user answers nothing (it falls back to a balanced baseline; the engine can never crash).
269+
270+
### Modify the Step-3 terminal UI (analysis stepper)
271+
272+
The developer-centric readout lives in
273+
[`src/components/advisor/AnalysisStepper.tsx`](src/components/advisor/AnalysisStepper.tsx). The
274+
stages are dict keys — edit the `STAGES` array and their strings in
275+
[`src/i18n/dict.ts`](src/i18n/dict.ts) (`analysis.run.*`). Keep the stages **honest** (they name
276+
real pipeline steps) and the total duration short; the component already renders nothing under
277+
`prefers-reduced-motion`. It is triggered by `analysisRun` in `App.tsx`, which increments on an
278+
explicit analyze action (preset/wizard apply) — never on a live factor edit.
279+
280+
### Component map (Advisor tab)
281+
282+
| Area | Component | Notes |
283+
|---|---|---|
284+
| Scenario gallery + wizard entry | [`components/advisor/PresetBar.tsx`](src/components/advisor/PresetBar.tsx) | search, tag filters, dominant custom card |
285+
| Custom wizard (lazy modal) | [`components/advisor/CustomWizard.tsx`](src/components/advisor/CustomWizard.tsx) | iterates the wizard config |
286+
| Wizard → engine bridge (pure) | [`lib/customWizard.ts`](src/lib/customWizard.ts) | `wizardToLevels()` — the only mapping |
287+
| ① Project factors | [`components/advisor/FactorInputs.tsx`](src/components/advisor/FactorInputs.tsx) · `FactorField.tsx` | 14 factors, per-level examples |
288+
| ② Quality priorities + adjuster | [`components/advisor/PrioritiesCard.tsx`](src/components/advisor/PrioritiesCard.tsx) · `QaOverridePanel.tsx` | derived + editable weights |
289+
| ③ Analysis stepper | [`components/advisor/AnalysisStepper.tsx`](src/components/advisor/AnalysisStepper.tsx) | honest terminal readout |
290+
| ③ Recommendation | `DimensionCards.tsx` · `DimensionDetail.tsx` · `RadarPanel.tsx` | ranked result + radar |
291+
| ④ Export / share | [`components/chrome/Toolbar.tsx`](src/components/chrome/Toolbar.tsx) | ADR / report / CSV / JSON / share |
292+
| Pure scoring engine (frozen) | [`lib/scoring.ts`](src/lib/scoring.ts) | `rank()` — the single source of truth |
293+
172294
## Contributing
173295

174296
Contributions are welcome — code, documentation, translations, and model review. Start with

docs/05-testing-qa/test-plan.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,8 @@ Pure client-side, no backend/accounts/secrets — the surface is the browser and
241241
## 9. Performance verification (L7)
242242

243243
- [x] **Bundle budget:** **CI-gated**`npm run size` ([`scripts/check-bundle-size.mjs`](../../scripts/check-bundle-size.mjs))
244-
asserts gzip **initial JS ≤120kB** (first load) and **total JS ≤260kB** (raised 200→260 for full Insights bilingualisation 2026-07-15 — EN+ID content is lazy; NFR cap 300) / CSS ≤25kB (currently ~114 initial /
245-
~237 total / ~23 CSS with React 19 on Vite 8). The guard reads the **real initial set from `dist/index.html`** (entry
244+
asserts gzip **initial JS ≤120kB** (first load) and **total JS ≤260kB** (raised 200→260 for full Insights bilingualisation 2026-07-15 — EN+ID content is lazy; NFR cap 300) / **CSS ≤27kB** (raised 25→27 for Fase 2g UI/UX polish 2026-07-18; NFR ceiling ~30kB) (currently ~117 initial /
245+
~250 total / ~25 CSS with React 19 on Vite 8). The guard reads the **real initial set from `dist/index.html`** (entry
246246
script + modulepreloads), so lazy views (Manual/Guide, Insights) **and their shared async chunks** (e.g.
247247
`readerContent`, which powers the data-driven Catalog) are correctly excluded from the first load — none can be
248248
silently mis-counted. No chart/diagram/markdown library ships — all visuals are hand-built SVG and Markdown is

e2e/a11y.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,5 @@ test('controls are keyboard-operable', async ({ page }) => {
8080
await page.getByRole('button', { name: 'EN', exact: true }).focus();
8181
await page.keyboard.press('Enter');
8282
await page.getByRole('button', { name: 'Advisor', exact: true }).click(); // default view is now Home
83-
await expect(page.getByRole('button', { name: 'Busy online shop' })).toBeVisible();
83+
await expect(page.getByRole('button', { name: /Busy online shop/ })).toBeVisible();
8484
});

e2e/share.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test('share link round-trips the scenario via a deep link', async ({ page, conte
1111
await page.goto(APP);
1212
await page.getByRole('button', { name: 'EN', exact: true }).click();
1313
await page.getByRole('button', { name: 'Advisor', exact: true }).click(); // default view is now Home
14-
await page.getByRole('button', { name: 'Busy online shop' }).click();
14+
await page.getByRole('button', { name: /Busy online shop/ }).click();
1515
const shared = (await page.getByText(VERDICT).textContent())?.trim();
1616

1717
// Share copies the deep link to the clipboard

e2e/smoke.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ test('the four-step flow loads and the recommendation recomputes on a preset', a
2222
await expect(verdict).toBeVisible();
2323
const before = (await verdict.textContent())?.trim();
2424

25-
// applying a preset instantly recomputes the recommendation (AC-2, real browser)
26-
await page.getByRole('button', { name: 'Busy online shop' }).click();
25+
// applying a preset instantly recomputes the recommendation (AC-2, real browser).
26+
// Presets are cards in the Scenario Card Gallery (Blueprint Phase 1.3) — clicked directly.
27+
await page.getByRole('button', { name: /Busy online shop/ }).click();
2728
await expect(page.getByText(VERDICT)).not.toHaveText(before ?? '');
2829
});
2930

scripts/check-bundle-size.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { gzipSync } from 'node:zlib';
1818
// budgets deliberately (with a note) if the app grows.
1919
const JS_INITIAL_BUDGET_KB = 120;
2020
const JS_TOTAL_BUDGET_KB = 260; // raised 200→260 for full Insights bilingualisation 2026-07-15 (6 datasets + 18 article bodies now carry EN+ID; all in the lazy content chunk — initial budget untouched; NFR cap is 300)
21-
const CSS_BUDGET_KB = 25;
21+
const CSS_BUDGET_KB = 27; // raised 25→27 for Fase 2g UI/UX polish 2026-07-18 (preset dropdown, uniform export buttons, 2×2 step-rail wrap grid, footer stacking, modern app-bar controls); still well under the ~30kB NFR ceiling
2222

2323
const dir = 'dist/assets';
2424
if (!existsSync(dir)) {

src/App.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ describe('App integration', () => {
1919
it('AC-2: applying a preset instantly recomputes the recommendation', () => {
2020
renderWithI18n(<App />, 'en');
2121
const before = screen.getByText(VERDICT).textContent;
22-
fireEvent.click(screen.getByRole('button', { name: 'Busy online shop' }));
22+
// Presets are cards in the Scenario Card Gallery (Blueprint Phase 1.3) — clicked directly.
23+
fireEvent.click(screen.getByRole('button', { name: /Busy online shop/ }));
2324
expect(screen.getByText(VERDICT).textContent).not.toBe(before);
2425
});
2526

0 commit comments

Comments
 (0)