From d319847875b3b3eeff2c4ca4c43fd7dda12f8b40 Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Thu, 30 Jul 2026 20:37:00 +0200 Subject: [PATCH 1/6] build(#577): add the UI-complexity measurement instrument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instrument #577's Preact decision rests on, plus the committed manifest that makes acceptance criterion 4 (domain logic vs framework-like rendering plumbing) auditable rather than asserted. Counts run over esbuild-transformed source, never raw text. Two naive instruments both lie about this repository: - Physical LOC. The five shell-plumbing modules total 1791 physical lines but ~709 lines of code; 989 are comment-only, because each invariant is documented next to the review bug that found it. Any arm written at normal comment density "wins" on physical LOC while containing more code. - Regex counts of manual DOM mutation. Run over raw source they match the prose *inside* those comments — a first pass reported 37 "mutation sites" in app-shell.ts, and a comment-only file scores above zero. They also go syntactically invisible under a vDOM, so a component arm scores ~0 by construction. esbuild does the stripping because it is the repo's only build tool and it handles what a hand-rolled scanner does not: `//` inside a string, `\/\/` inside a regex literal, a `*`-prefixed line inside a template literal, a trailing `code; // note`. Re-printing also normalizes formatting, so an arm cannot win by collapsing statements onto fewer lines. ONE canonical manifest is measured against every evaluation state, via `--manifest`, and a manifest entry whose file is absent from the current checkout is reported as `absent` rather than skipped or thrown on. This is load-bearing, not a convenience: the two most decision-relevant facts in the whole comparison are a file APPEARING (the control adds the inspector) and a file DISAPPEARING (the treatment deletes the vanilla shell). Measuring each state against its own manifest would make both vanish from the comparison instead of showing up in it. For the same reason the manifest test pins the path set instead of asserting filesystem existence — an existence check fails on exactly the states where absence is the result. Metric tiering is emitted in the JSON (METRIC_TIERS). Two independent plan reviews found that ten unranked numbers let almost any outcome support either recommendation, so each explanatory metric carries the reason it may not be voted with: minified bytes ignore tree shaking and complexity moved into a dependency; lcov BRF/FNF fall when mechanisms move into Preact, which is externalized rather than eliminated complexity. `nonCodeLines` is deliberately NOT called "comment-only lines": esbuild re-prints the code, so physical - blank - normalized is not a comment total. It is source non-blank lines carrying no code after normalization — comments plus formatting-only lines. Naming it a comment count would be a measurement error in the metric the decision rests on. Unmatched lcov records report as null, never as a zero record: "no coverage data" and "no branches" are different claims. Tests include a sabotage suite, and a structural guard asserting the runner counts stripped code rather than raw source — the lib is pure by design, so that wiring is otherwise unguarded and swapping it back would leave every other test green. Verified falsifiable: sabotaging it turns the suite red. Part of #577. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XLuZUHRbTCFKDJf2Zf8nP1 --- build/ui-complexity-lib.mjs | 378 +++++++++++++++++++ build/ui-complexity-manifest.json | 92 +++++ build/ui-complexity-report.mjs | 189 ++++++++++ tests/unit/ui-complexity-report.test.js | 459 ++++++++++++++++++++++++ 4 files changed, 1118 insertions(+) create mode 100644 build/ui-complexity-lib.mjs create mode 100644 build/ui-complexity-manifest.json create mode 100644 build/ui-complexity-report.mjs create mode 100644 tests/unit/ui-complexity-report.test.js diff --git a/build/ui-complexity-lib.mjs b/build/ui-complexity-lib.mjs new file mode 100644 index 00000000..c31e79c6 --- /dev/null +++ b/build/ui-complexity-lib.mjs @@ -0,0 +1,378 @@ +// UI-complexity report — pure half (issue #577). +// +// The measurement instrument the Preact evaluation (#577) decides on. Split +// lib/runner exactly like size-report-lib.mjs / size-report.mjs so every +// counting rule below is unit-testable without touching the filesystem or +// esbuild. +// +// WHY THIS EXISTS, and why it is not a LOC counter: +// +// #577's decision principle asks whether a completed migration would REDUCE, +// not merely reorganize, production UI code and lifecycle complexity. Two +// naive instruments both lie about this repository: +// +// 1. Physical LOC. The five shell-plumbing modules total 1791 physical lines +// but only ~709 lines of code — 989 are comment-only, because this repo +// documents each invariant next to the review bug that found it. Any arm +// written at normal comment density "wins" on physical LOC while +// containing more code. +// 2. Regex counts of manual DOM mutation (`.hidden =`, `.dataset.`, +// `.replaceChildren(`). Three of those four families become +// SYNTACTICALLY INVISIBLE under a vDOM whether or not the underlying +// number of presentation decisions changed — a component arm scores ~0 by +// construction. Worse, run over raw source they also match the prose IN +// the comments, which is how a first pass at this measured 37 +// "mutation sites" in app-shell.ts. +// +// So: every count here runs over ESBUILD-TRANSFORMED source, never raw text. +// esbuild is the repo's only build tool, it strips comments correctly where a +// hand-rolled scanner does not (`//` inside a string, `\/\/` inside a regex +// literal, a `*`-prefixed line inside a template literal, a trailing +// `code; // note`), and it re-prints the code — which normalizes formatting, so +// an arm cannot win by collapsing statements onto fewer lines. See +// `tests/unit/ui-complexity-report.test.js` for the sabotage cases that hold +// this claim. +// +// METRIC TIERING IS PART OF THE OUTPUT (`METRIC_TIERS` below). Two independent +// plan reviews of #577 found that a report carrying ten unranked numbers lets +// almost any outcome support either recommendation. The tier each metric sits +// in is therefore emitted in the JSON, so a demoted metric cannot be quietly +// promoted to a deciding one when the report is written. + +export const COMPLEXITY_SCHEMA_VERSION = 1; + +/** + * Which metrics may decide #577, and which may only explain it. + * + * `deciding` mirrors the four evidence classes in the approved plan. Three of + * them are NOT computable from source text and are therefore recorded here as + * declared-but-external: behavioural parity comes from the e2e suite, the + * artifact numbers come from `build/size-report.mjs`, and change amplification + * comes from the frozen controlled-change experiment. This instrument owns only + * `ownedProductionCode`. + * + * `explanatory` metrics are reported per file and per class but never voted + * with. Each carries the reason it was demoted, so the report cannot silently + * re-promote it: + * - `minifiedBytes` ignores cross-module tree shaking, shared helper + * generation, and complexity moved INTO a dependency; + * - `lcovBranches`/`lcovFunctions` fall when mechanisms move into Preact — + * that is externalized, not eliminated, complexity; + * - `lifecycleSites` is the vDOM-invisible family described above. + */ +export const METRIC_TIERS = { + deciding: { + ownedProductionCode: { owner: 'this instrument' }, + parity: { owner: 'tests/e2e/shell-parity.spec.js' }, + artifact: { owner: 'build/size-report.mjs' }, + changeAmplification: { owner: 'frozen controlled-change experiment' }, + cleanupObligations: { owner: 'leak invariant + manual obligation count' }, + }, + explanatory: { + minifiedBytes: { demotedBecause: 'ignores tree shaking, shared helpers, and complexity moved into a dependency' }, + lcovBranches: { demotedBecause: 'falls when mechanisms move into Preact — externalized, not eliminated' }, + lcovFunctions: { demotedBecause: 'falls when mechanisms move into Preact — externalized, not eliminated' }, + lifecycleSites: { demotedBecause: 'hidden/dataset/replaceChildren go syntactically invisible under a vDOM' }, + }, +}; + +/** The three classes a manifest entry may declare. `domain` is preserved by + * both arms, `island` stays imperative under any render model (hard rule 5 — + * signals coordinate, they do not own mousemove), and only `plumbing` is + * genuinely in play. Mislabelling here is a wrong decision, not a wrong + * number, which is why the manifest is committed and the report names every + * file in it. */ +export const FILE_CLASSES = ['domain', 'plumbing', 'island']; + +/** + * Count the code-bearing lines of already-comment-stripped, re-printed source. + * + * Takes esbuild's `transform().code`, NOT raw source — the caller owns that + * step so this stays pure. Blank lines are excluded; a trailing newline does + * not count as a line. + */ +export function countNormalizedLines(transformedCode) { + const lines = String(transformedCode).split('\n'); + let normalized = 0; + let blank = 0; + for (const line of lines) { + if (line.trim() === '') blank += 1; + else normalized += 1; + } + return { normalized, blank }; +} + +/** + * Occurrence counts (not line hits) of the lifecycle families #577's + * Measurements section names, over comment-stripped code. + * + * A first pass at this used `rg -c`, which counts matching LINES — so a line + * touching three of these families scored 1, and a comment discussing one + * scored 1 with no code at all. Both are fixed here: the caller passes stripped + * code, and every family is counted with a global regex. + */ +export function countLifecycleSites(strippedCode) { + const code = String(strippedCode); + const count = (re) => (code.match(re) || []).length; + return { + domMutation: count(/\.(?:hidden|textContent|value)\s*=|\.dataset\.[A-Za-z]/g) + + count(/\.(?:replaceChildren|append|setAttribute|removeAttribute)\(/g) + + count(/\.style\.[A-Za-z]+\s*=|\.classList\.[A-Za-z]+\(/g), + listener: count(/\.(?:add|remove)EventListener\(/g), + effect: count(/\b(?:effect|computed|untracked|batch)\(/g), + disposal: count(/\bdispose\w*\b/g), + focus: count(/\.focus\(\)/g), + }; +} + +/** + * Parse the `BRF`/`BRH`/`FNF`/`FNH` totals out of an lcov report, keyed by the + * `SF:` path exactly as lcov wrote it. + * + * Explanatory only (see `METRIC_TIERS`). Reported as "repo-owned executable + * test surface" rather than as a complexity verdict: a framework migration can + * cut these counts by moving mechanisms into Preact, which is a real reduction + * in code the project owns but not automatically a reduction in total + * lifecycle complexity. + */ +export function parseLcov(text) { + const byFile = {}; + let current = null; + for (const rawLine of String(text).split('\n')) { + const line = rawLine.trim(); + if (line.startsWith('SF:')) { + current = line.slice(3); + byFile[current] = { branchesFound: 0, branchesHit: 0, functionsFound: 0, functionsHit: 0 }; + continue; + } + if (current === null) continue; + if (line.startsWith('BRF:')) byFile[current].branchesFound = Number(line.slice(4)) || 0; + else if (line.startsWith('BRH:')) byFile[current].branchesHit = Number(line.slice(4)) || 0; + else if (line.startsWith('FNF:')) byFile[current].functionsFound = Number(line.slice(4)) || 0; + else if (line.startsWith('FNH:')) byFile[current].functionsHit = Number(line.slice(4)) || 0; + else if (line === 'end_of_record') current = null; + } + return byFile; +} + +/** + * Resolve a manifest path against lcov's own key set. + * + * lcov paths are absolute in this repo's coverage output while the manifest is + * repo-relative, so a plain lookup misses every file and would silently report + * zero branches everywhere — indistinguishable from "this file has no + * branches". Matching on a path suffix keeps that failure loud instead: an + * unmatched file is reported as `null`, never as 0. + */ +export function lookupLcov(lcovByFile, relPath) { + if (Object.prototype.hasOwnProperty.call(lcovByFile, relPath)) return lcovByFile[relPath]; + const suffix = '/' + relPath; + for (const key of Object.keys(lcovByFile)) { + if (key.endsWith(suffix)) return lcovByFile[key]; + } + return null; +} + +/** + * Assemble the report from per-file measurements the runner has already taken. + * + * `entries` items: `{ path, class, sliceItem, normalizedLines, + * sourceBlankLines, physicalLines, minifiedBytes, lifecycle, lcov }`. Totals + * are grouped by + * class, because criterion 4 of #577 requires the report to distinguish + * product-specific domain logic from framework-like rendering plumbing — an + * undifferentiated grand total cannot do that. + */ +export function buildComplexityReport({ label, entries, environment = null }) { + const files = [...entries].sort((a, b) => a.path.localeCompare(b.path)); + for (const entry of files) { + if (!FILE_CLASSES.includes(entry.class)) { + throw new Error(`ui-complexity: file "${entry.path}" has unknown class "${entry.class}" (expected one of ${FILE_CLASSES.join(', ')})`); + } + } + const emptyTotals = () => ({ + files: 0, + // A manifest entry whose file does not exist in THIS state. Central to the + // method: all three evaluation states are measured against ONE canonical + // manifest, so the set is identical and the totals are comparable. S1 adds + // `right-inspector.ts` (absent in S0) and S2 DELETES `app-shell.ts` (absent + // there) — if each state used its own manifest, S1's cost and S2's deletion + // would both vanish from the comparison instead of showing up in it. + absentFiles: 0, + physicalLines: 0, + normalizedLines: 0, + nonCodeLines: 0, + minifiedBytes: 0, + lifecycle: { domMutation: 0, listener: 0, effect: 0, disposal: 0, focus: 0 }, + lcovBranchesFound: 0, + lcovFunctionsFound: 0, + lcovUnmatchedFiles: 0, + }); + const byClass = {}; + const overall = emptyTotals(); + for (const cls of FILE_CLASSES) byClass[cls] = emptyTotals(); + + for (const entry of files) { + for (const bucket of [byClass[entry.class], overall]) { + if (entry.absent) { + bucket.absentFiles += 1; + continue; + } + bucket.files += 1; + bucket.physicalLines += entry.physicalLines; + bucket.normalizedLines += entry.normalizedLines; + // NOT called "comment-only": esbuild RE-PRINTS the code, so its line + // count is not the source's code-line count and + // `physical - blank - normalized` is not a comment total. What it IS, + // exactly: source non-blank lines carrying no code after normalization — + // comments PLUS formatting-only lines (a lone `)`, a multi-line call + // esbuild re-joins). That is still the number a raw-LOC comparison would + // have wrongly credited as complexity, which is why it is reported; it is + // just not a comment count, and naming it one would be a measurement + // error in the metric this whole decision rests on. + bucket.nonCodeLines += entry.physicalLines - entry.sourceBlankLines - entry.normalizedLines; + bucket.minifiedBytes += entry.minifiedBytes; + for (const key of Object.keys(bucket.lifecycle)) bucket.lifecycle[key] += entry.lifecycle[key]; + if (entry.lcov) { + bucket.lcovBranchesFound += entry.lcov.branchesFound; + bucket.lcovFunctionsFound += entry.lcov.functionsFound; + } else { + bucket.lcovUnmatchedFiles += 1; + } + } + } + + return { + schemaVersion: COMPLEXITY_SCHEMA_VERSION, + label: label || null, + metricTiers: METRIC_TIERS, + environment, + // The deciding metric this instrument owns. Named explicitly so the report + // cannot substitute a different number for it. + ownedProductionCode: { + plumbingNormalizedLines: byClass.plumbing.normalizedLines, + islandNormalizedLines: byClass.island.normalizedLines, + domainNormalizedLines: byClass.domain.normalizedLines, + }, + byClass, + overall, + files, + }; +} + +/** Absolute + percentage delta between two reports, for the same metric set. + * `null` percentage when the base is 0 — a 0 → n change has no meaningful + * percentage and printing `Infinity%` in a decision document is worse than + * printing nothing. */ +export function computeComplexityDelta(current, base) { + const abs = current - base; + return { current, base, abs, pct: base === 0 ? null : (abs / base) * 100 }; +} + +/** Diff two whole reports, per class and overall. */ +export function diffComplexityReports(current, base) { + const out = { byClass: {}, overall: {} }; + const metrics = ['physicalLines', 'normalizedLines', 'nonCodeLines', 'minifiedBytes']; + for (const cls of FILE_CLASSES) { + out.byClass[cls] = {}; + for (const metric of metrics) { + out.byClass[cls][metric] = computeComplexityDelta(current.byClass[cls][metric], base.byClass[cls][metric]); + } + } + for (const metric of metrics) { + out.overall[metric] = computeComplexityDelta(current.overall[metric], base.overall[metric]); + } + return out; +} + +function formatPct(pct) { + if (pct === null) return 'n/a'; + const sign = pct > 0 ? '+' : ''; + return `${sign}${pct.toFixed(1)}%`; +} + +function formatSigned(n) { + return (n > 0 ? '+' : '') + String(n); +} + +/** Human-readable report. Deliberately leads with the tiering and with the + * physical-vs-normalized gap, because those two facts are what stop a reader + * from misreading the table underneath them. */ +export function renderComplexityMarkdown(report, deltas = null) { + const lines = []; + lines.push(`# UI complexity report${report.label ? ` — ${report.label}` : ''}`); + lines.push(''); + lines.push(`Schema version ${report.schemaVersion}. Counts run over esbuild-transformed`); + lines.push('source (comments stripped, formatting normalized), never raw text.'); + lines.push(''); + lines.push('**Deciding metric owned here:** `ownedProductionCode` — normalized lines by class.'); + lines.push('Every other number below is explanatory and must not be voted with:'); + for (const [metric, meta] of Object.entries(report.metricTiers.explanatory)) { + lines.push(`- \`${metric}\` — ${meta.demotedBecause}`); + } + lines.push(''); + lines.push('## Owned production code (deciding)'); + lines.push(''); + lines.push('| Class | Files | Normalized (code) | Physical | Non-code |'); + lines.push('|---|---:|---:|---:|---:|'); + for (const cls of FILE_CLASSES) { + const t = report.byClass[cls]; + if (t.files === 0) continue; + lines.push(`| ${cls} | ${t.files} | **${t.normalizedLines}** | ${t.physicalLines} | ${t.nonCodeLines} |`); + } + const o = report.overall; + lines.push(`| **total** | **${o.files}** | **${o.normalizedLines}** | **${o.physicalLines}** | **${o.nonCodeLines}** |`); + lines.push(''); + lines.push(`Physical LOC overstates code by **${o.nonCodeLines}** non-code lines`); + lines.push('(comments plus formatting-only lines). A raw-LOC comparison would have credited'); + lines.push('those as complexity — which is the specific error this instrument exists to prevent.'); + lines.push(''); + lines.push('## Explanatory metrics'); + lines.push(''); + lines.push('| File | Class | Code | Minified B | DOM | Listeners | Effects | Disposal | focus() | lcov BRF/FNF |'); + lines.push('|---|---|---:|---:|---:|---:|---:|---:|---:|---|'); + for (const f of report.files) { + if (f.absent) { + // Explicitly present as a row rather than omitted: "this state does not + // have this file" is a measurement, and for S2 it is THE measurement. + lines.push(`| \`${f.path}\` | ${f.class} | _absent_ | — | — | — | — | — | — | — |`); + continue; + } + const lc = f.lcov ? `${f.lcov.branchesFound}/${f.lcov.functionsFound}` : '_unmatched_'; + lines.push(`| \`${f.path}\` | ${f.class} | ${f.normalizedLines} | ${f.minifiedBytes} | ${f.lifecycle.domMutation} | ${f.lifecycle.listener} | ${f.lifecycle.effect} | ${f.lifecycle.disposal} | ${f.lifecycle.focus} | ${lc} |`); + } + if (report.overall.absentFiles > 0) { + lines.push(''); + lines.push(`> ${report.overall.absentFiles} manifest file(s) absent in this state — measured against the one canonical`); + lines.push('> manifest so every state covers an identical file set (a deletion must show up, not vanish).'); + } + if (o.lcovUnmatchedFiles > 0) { + lines.push(''); + lines.push(`> ${o.lcovUnmatchedFiles} file(s) had no lcov record — reported as unmatched, never as zero branches.`); + } + if (deltas) { + lines.push(''); + lines.push('## Delta vs base'); + lines.push(''); + lines.push('| Class | Normalized (code) | Δ | Minified B | Δ |'); + lines.push('|---|---:|---:|---:|---:|'); + for (const cls of FILE_CLASSES) { + const n = deltas.byClass[cls].normalizedLines; + const m = deltas.byClass[cls].minifiedBytes; + if (n.base === 0 && n.current === 0) continue; + lines.push(`| ${cls} | ${n.current} | ${formatSigned(n.abs)} (${formatPct(n.pct)}) | ${m.current} | ${formatSigned(m.abs)} (${formatPct(m.pct)}) |`); + } + const n = deltas.overall.normalizedLines; + lines.push(`| **total** | **${n.current}** | **${formatSigned(n.abs)} (${formatPct(n.pct)})** | ${deltas.overall.minifiedBytes.current} | ${formatSigned(deltas.overall.minifiedBytes.abs)} |`); + } + if (report.environment) { + lines.push(''); + lines.push('## Environment'); + lines.push(''); + for (const [key, value] of Object.entries(report.environment)) { + lines.push(`- **${key}**: \`${value}\``); + } + } + lines.push(''); + return lines.join('\n'); +} diff --git a/build/ui-complexity-manifest.json b/build/ui-complexity-manifest.json new file mode 100644 index 00000000..1d74e493 --- /dev/null +++ b/build/ui-complexity-manifest.json @@ -0,0 +1,92 @@ +{ + "_comment": [ + "The auditable file set for #577's UI-complexity measurement. Acceptance", + "criterion 4 requires the report to distinguish product-specific domain logic", + "from framework-like rendering plumbing, so THIS FILE is the artifact that", + "claim rests on — a wrong `class` here is a wrong decision, not a wrong", + "number. Every file the report names comes from here and nowhere else.", + "", + "Classes:", + " domain — preserved verbatim by both the vanilla control and the Preact", + " treatment. Any diff in these files during the evaluation is a", + " finding against #577's own 'preserve proven non-rendering", + " logic' constraint, not a licence.", + " plumbing — genuinely replaceable by a component model. This is the only", + " class the treatment may claim a reduction in.", + " island — stays imperative under ANY render model (CLAUDE.md hard rule 5:", + " signals coordinate state, they do not own every mousemove).", + " Counted so the report cannot present island code as a Preact", + " win or a Preact cost.", + "", + "The `sliceItem` field maps each file to the numbered item in #577's", + "'Evaluation scope' list it covers, so a reader can check the slice is", + "actually covered rather than taking the report's word for it.", + "", + "Classification rationale for the three non-obvious calls (a first pass at", + "this plan mislabelled all three, which would have overstated the", + "replaceable baseline by roughly 1000 physical lines):", + " - left-nav-separator.ts is an ISLAND, not plumbing: it is pointer/keyboard", + " mechanics plus resize-session bookkeeping. Only its thin ARIA/paint glue", + " is plumbing, and that is not separable at file granularity.", + " - nav-sections.ts and sidebar-upper.ts are DOMAIN shared verbatim: they are", + " NAV_SECTION_META plus persistent-host construction that BOTH arms need.", + " Only a ~10-line showSection is replaceable." + ], + "files": [ + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching" + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation" + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations" + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher" + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics" + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free" + }, + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam" + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host" + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes" + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)" + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector" + } + ] +} diff --git a/build/ui-complexity-report.mjs b/build/ui-complexity-report.mjs new file mode 100644 index 00000000..b91dbb1a --- /dev/null +++ b/build/ui-complexity-report.mjs @@ -0,0 +1,189 @@ +// UI-complexity report runner (issue #577). +// +// Measures the file set in `build/ui-complexity-manifest.json` and emits a +// machine-readable JSON report plus a human-readable Markdown one, with +// optional deltas against a prior report. The pure counting rules live in +// `ui-complexity-lib.mjs` (unit-tested, including sabotage cases); this file +// owns only filesystem access, the esbuild call, and environment capture. +// +// Usage: node build/ui-complexity-report.mjs [--out ] [--base ] +// [--label ] [--manifest ] +// --out output directory (default: complexity-report/) +// --base a prior ui-complexity-report.json to diff against +// --label a name for this measurement (e.g. baseline / control / treatment) +// --manifest manifest to measure (default: build/ui-complexity-manifest.json) +// +// `--manifest` exists so ONE canonical manifest can be run against several +// checked-out states. Every evaluation state must be measured over an IDENTICAL +// file set or the totals are not comparable — and the two most decision-relevant +// facts are precisely a file appearing (S1 adds the inspector) and a file +// disappearing (S2 deletes the vanilla shell). A manifest entry whose file is +// absent from the current checkout is reported as `absent`, never skipped, so a +// deletion shows up in the comparison instead of vanishing from it. +// +// Reporting only — it reads source and never writes into src/. +// +// WHY esbuild AND NOT A REGEX: see ui-complexity-lib.mjs's header. Short +// version: every count runs over comment-stripped, re-printed source, because +// counting raw text both credits this repo's very high comment density as +// complexity and matches the prose inside those comments. +// +// Environment capture is deliberately part of the report rather than a footnote: +// #577's acceptance criterion 5 requires REPRODUCIBLE measurements, and a git +// tag preserves source, not a build environment. The captured commit SHA is the +// resolved one, never a tag name (tags can be moved). + +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { createHash } from 'node:crypto'; +import * as esbuild from 'esbuild'; +import { + buildComplexityReport, + countLifecycleSites, + countNormalizedLines, + diffComplexityReports, + lookupLcov, + parseLcov, + renderComplexityMarkdown, +} from './ui-complexity-lib.mjs'; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, '..'); + +function parseArgs(argv) { + const args = { + out: 'complexity-report', base: null, label: null, + manifest: 'build/ui-complexity-manifest.json', + }; + for (let i = 0; i < argv.length; i += 1) { + if (argv[i] === '--out') args.out = argv[i + 1], i += 1; + else if (argv[i] === '--base') args.base = argv[i + 1], i += 1; + else if (argv[i] === '--label') args.label = argv[i + 1], i += 1; + else if (argv[i] === '--manifest') args.manifest = argv[i + 1], i += 1; + } + return args; +} + +async function readJson(path) { + return JSON.parse(await readFile(path, 'utf8')); +} + +/** Best-effort `git`/tool version capture. A missing value is recorded as + * 'unavailable' rather than omitted, so a report can never look like it + * captured an environment it did not. */ +function capture(command, args) { + try { + return execFileSync(command, args, { cwd: root, encoding: 'utf8' }).trim(); + } catch { + return 'unavailable'; + } +} + +async function captureEnvironment() { + let lockHash = 'unavailable'; + try { + const lock = await readFile(resolve(root, 'package-lock.json')); + lockHash = 'sha256:' + createHash('sha256').update(lock).digest('hex').slice(0, 16); + } catch { /* recorded as unavailable */ } + return { + // Resolved SHA, never a tag name — an annotated tag can be moved unless + // repository policy prevents it, so the SHA is what actually pins a state. + commit: capture('git', ['rev-parse', 'HEAD']), + commitDirty: capture('git', ['status', '--porcelain']) === '' ? 'clean' : 'DIRTY', + node: process.version, + esbuild: esbuild.version, + platform: `${process.platform}-${process.arch}`, + packageLock: lockHash, + }; +} + +async function measureFile(entry, lcovByFile) { + const abs = resolve(root, entry.path); + let source; + try { + source = await readFile(abs, 'utf8'); + } catch { + // Absent in THIS state. Recorded, not skipped — see the `--manifest` note in + // the header: for the treatment state, the absence of the vanilla shell IS + // the headline measurement. + return { path: entry.path, class: entry.class, sliceItem: entry.sliceItem || null, absent: true }; + } + const sourceLines = source.split('\n'); + // A trailing newline yields a final empty element that is not a line. + const physicalLines = sourceLines.length > 0 && sourceLines[sourceLines.length - 1] === '' + ? sourceLines.length - 1 + : sourceLines.length; + let sourceBlankLines = 0; + for (let i = 0; i < physicalLines; i += 1) { + if (sourceLines[i].trim() === '') sourceBlankLines += 1; + } + + const loader = entry.path.endsWith('.ts') ? 'ts' : 'js'; + const stripped = await esbuild.transform(source, { loader }); + const minified = await esbuild.transform(source, { loader, minify: true }); + const { normalized } = countNormalizedLines(stripped.code); + + return { + path: entry.path, + class: entry.class, + sliceItem: entry.sliceItem || null, + physicalLines, + sourceBlankLines, + normalizedLines: normalized, + minifiedBytes: Buffer.byteLength(minified.code, 'utf8'), + // Counted over STRIPPED code, so a comment discussing `.hidden =` no longer + // inflates the number the way a raw-source regex did. + lifecycle: countLifecycleSites(stripped.code), + lcov: lookupLcov(lcovByFile, entry.path), + }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const manifest = await readJson(resolve(process.cwd(), args.manifest)); + + let lcovByFile = {}; + try { + lcovByFile = parseLcov(await readFile(resolve(root, 'coverage/lcov.info'), 'utf8')); + } catch { + // No coverage run yet. Every file then reports lcov as unmatched, which the + // report prints as `_unmatched_` — never as zero branches, since "no data" + // and "no branches" are different claims. + } + + const entries = []; + for (const entry of manifest.files) entries.push(await measureFile(entry, lcovByFile)); + + const report = buildComplexityReport({ + label: args.label, + entries, + environment: await captureEnvironment(), + }); + + const base = args.base ? await readJson(resolve(process.cwd(), args.base)) : null; + const deltas = base ? diffComplexityReports(report, base) : null; + + const outDir = resolve(process.cwd(), args.out); + await mkdir(outDir, { recursive: true }); + await writeFile(resolve(outDir, 'ui-complexity-report.json'), JSON.stringify(report, null, 2) + '\n'); + await writeFile(resolve(outDir, 'ui-complexity-report.md'), renderComplexityMarkdown(report, deltas)); + + const o = report.overall; + console.log(`ui-complexity report -> ${args.out}/`); + console.log(` ${o.files} files: ${o.normalizedLines} code lines (${o.physicalLines} physical, ${o.nonCodeLines} non-code)`); + console.log(` plumbing ${report.byClass.plumbing.normalizedLines} | domain ${report.byClass.domain.normalizedLines} | island ${report.byClass.island.normalizedLines}`); + if (o.absentFiles > 0) console.log(` ${o.absentFiles} manifest file(s) absent in this state`); + if (deltas) { + const n = deltas.overall.normalizedLines; + console.log(` Δ code lines vs base: ${n.abs > 0 ? '+' : ''}${n.abs}`); + } else { + console.log(' (no base report — deltas omitted)'); + } + if (report.environment.commitDirty === 'DIRTY') { + console.log(' WARNING: working tree is dirty — this measurement is not reproducible from the recorded commit.'); + } +} + +await main(); diff --git a/tests/unit/ui-complexity-report.test.js b/tests/unit/ui-complexity-report.test.js new file mode 100644 index 00000000..f1cf2874 --- /dev/null +++ b/tests/unit/ui-complexity-report.test.js @@ -0,0 +1,459 @@ +// Tests for the #577 UI-complexity measurement instrument. +// +// Two jobs. The ordinary one is covering the pure counting rules in +// `build/ui-complexity-lib.mjs`. The important one is the SABOTAGE suite: the +// instrument's whole claim to being decision-grade is that it counts code and +// not this repository's very heavy comment convention, so there are tests here +// that FAIL if comment-stripping regresses. That mirrors the convention in +// `tests/unit/typography-contract.test.js` — a gate is only evidence if it can +// be shown to fail. +// +// `build/` sits outside the coverage `include` glob (`src/**/*.{js,ts}`), so +// these tests are not driven by the per-file floor; they exist because a +// measurement instrument nobody tested is not reproducible, which is #577's +// acceptance criterion 5. + +import { describe, expect, it } from 'vitest'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import * as esbuild from 'esbuild'; +import { + COMPLEXITY_SCHEMA_VERSION, + FILE_CLASSES, + METRIC_TIERS, + buildComplexityReport, + computeComplexityDelta, + countLifecycleSites, + countNormalizedLines, + diffComplexityReports, + lookupLcov, + parseLcov, + renderComplexityMarkdown, +} from '../../build/ui-complexity-lib.mjs'; + +const repoRoot = resolve(import.meta.dirname, '../..'); + +/** Build a manifest-shaped entry with sane defaults so each test only states + * the fields it cares about. */ +function entry(overrides = {}) { + return { + path: 'src/ui/x.ts', + class: 'plumbing', + sliceItem: '1', + physicalLines: 10, + sourceBlankLines: 2, + normalizedLines: 5, + minifiedBytes: 100, + lifecycle: { domMutation: 0, listener: 0, effect: 0, disposal: 0, focus: 0 }, + lcov: { branchesFound: 4, branchesHit: 4, functionsFound: 2, functionsHit: 2 }, + ...overrides, + }; +} + +describe('countNormalizedLines', () => { + it('counts non-blank lines and ignores a trailing newline', () => { + expect(countNormalizedLines('a\nb\n')).toEqual({ normalized: 2, blank: 1 }); + }); + + it('does not count whitespace-only lines as code', () => { + expect(countNormalizedLines('a\n \n\t\nb')).toEqual({ normalized: 2, blank: 2 }); + }); + + it('reports zero for empty input rather than throwing', () => { + expect(countNormalizedLines('')).toEqual({ normalized: 0, blank: 1 }); + }); +}); + +describe('countLifecycleSites', () => { + it('counts OCCURRENCES, not matching lines', () => { + // The first implementation of this measurement used `rg -c`, which counts + // LINES — so this single line scored 1 instead of 3 and every per-file + // number in the report was an undercount. + const code = 'el.hidden = true; other.hidden = false; third.hidden = true;'; + expect(countLifecycleSites(code).domMutation).toBe(3); + }); + + it('counts each DOM-mutation family the issue names', () => { + const code = [ + 'a.hidden = true;', + 'b.dataset.navMode = "wide";', + 'c.textContent = "x";', + 'd.replaceChildren(e);', + 'f.setAttribute("aria-labelledby", g);', + 'h.removeAttribute("aria-labelledby");', + 'i.style.width = "1px";', + 'j.classList.add("k");', + ].join('\n'); + expect(countLifecycleSites(code).domMutation).toBe(8); + }); + + it('separates listeners, effects, disposal and focus', () => { + const code = [ + 'el.addEventListener("keydown", f);', + 'el.removeEventListener("keydown", f);', + 'disposers.push(effect(() => { untracked(() => s.value); }));', + 'batch(() => {});', + 'const c = computed(() => 1);', + 'thing.dispose();', + 'button.focus();', + ].join('\n'); + const sites = countLifecycleSites(code); + expect(sites.listener).toBe(2); + expect(sites.effect).toBe(4); + expect(sites.focus).toBe(1); + expect(sites.disposal).toBeGreaterThanOrEqual(2); + }); +}); + +// --------------------------------------------------------------------------- +// SABOTAGE SUITE — these are the tests that make the instrument defensible. +// --------------------------------------------------------------------------- +describe('comment stripping (sabotage cases)', () => { + /** Exactly what the runner does: esbuild-transform, then count. */ + async function measure(source, loader = 'ts') { + const stripped = await esbuild.transform(source, { loader }); + return { + code: stripped.code, + lines: countNormalizedLines(stripped.code).normalized, + sites: countLifecycleSites(stripped.code), + }; + } + + it('does not count comment-only lines as code', async () => { + const source = [ + '// one', + '// two', + '/* three', + ' four */', + 'const a = 1;', + ].join('\n'); + expect((await measure(source)).lines).toBe(1); + }); + + it('does not count DOM mutations that appear only inside a comment', async () => { + // This is the concrete bug the first pass at this measurement had: run over + // RAW source, a regex matched the prose in this repo's rationale comments, + // reporting 37 "mutation sites" in app-shell.ts where the stripped code has + // a different, smaller number. + const source = [ + '// Every `hidden`/text toggle below is set UNCONDITIONALLY, and', + '// el.dataset.navMode is written here too, plus a.replaceChildren(b).', + 'const noop = 1;', + ].join('\n'); + const { sites } = await measure(source); + expect(sites.domMutation).toBe(0); + }); + + it('keeps a "//" that lives inside a string literal', async () => { + const source = 'const url = "http://example.com//path";'; + const { code, lines } = await measure(source); + expect(lines).toBe(1); + expect(code).toContain('//path'); + }); + + it('keeps an escaped "//" inside a regex literal', async () => { + // A hand-rolled scanner tracking only quotes treats the `\/\/` here as the + // start of a line comment and silently drops the rest of the line. + const source = 'const re = /https:\\/\\/x/g;\nel.hidden = true;'; + const { lines, sites } = await measure(source); + expect(lines).toBe(2); + expect(sites.domMutation).toBe(1); + }); + + it('keeps a "*"-prefixed line inside a template literal', async () => { + // A scanner that skips lines whose trimmed form starts with `*` (to handle + // jsdoc continuation) eats real code here. + const source = 'const t = `\n * not a comment\n`;\nel.focus();'; + const { code, sites } = await measure(source); + expect(code).toContain('not a comment'); + expect(sites.focus).toBe(1); + }); + + it('strips a trailing comment without dropping the code before it', async () => { + const source = 'el.hidden = true; // rationale here mentions .dataset.x'; + const { lines, sites } = await measure(source); + expect(lines).toBe(1); + expect(sites.domMutation).toBe(1); + }); + + it('normalizes formatting, so collapsing lines cannot win', async () => { + // Without normalization an arm could halve its "LOC" purely by reformatting. + const spread = 'function f(\n a,\n b\n) {\n return a + b;\n}'; + const dense = 'function f(a, b) { return a + b; }'; + expect((await measure(spread)).lines).toBe((await measure(dense)).lines); + }); + + it('counting raw source really would inflate the numbers (the bug is reachable)', async () => { + // Guards against this suite becoming vacuous. Every test above asserts that + // STRIPPED code counts correctly — but esbuild's stripping cannot regress, + // so on its own that proves nothing about our choice to strip. This one + // pins the actual delta: the same input scores higher un-stripped, so the + // strip step is load-bearing rather than decorative. + const source = [ + '// sets el.hidden = true and writes el.dataset.navMode', + 'const noop = 1;', + ].join('\n'); + const raw = countLifecycleSites(source).domMutation; + const stripped = countLifecycleSites((await esbuild.transform(source, { loader: 'ts' })).code).domMutation; + expect(raw).toBeGreaterThan(0); + expect(stripped).toBe(0); + }); + + it('the runner counts stripped code, not raw source', async () => { + // A structural guard on the one decision that makes every number in the + // report trustworthy. The lib is pure by design (no fs, no esbuild), so the + // strip-then-count wiring lives in the runner and is otherwise unguarded: + // swapping `stripped.code` for `source` there would leave all 30+ tests + // above green while silently restoring the comment-inflated counts. + const runner = await readFile(resolve(repoRoot, 'build/ui-complexity-report.mjs'), 'utf8'); + expect(runner).toMatch(/countLifecycleSites\(\s*stripped\.code\s*\)/); + expect(runner).toMatch(/countNormalizedLines\(\s*stripped\.code\s*\)/); + expect(runner).not.toMatch(/countLifecycleSites\(\s*source\s*\)/); + }); +}); + +describe('parseLcov / lookupLcov', () => { + const lcov = [ + 'SF:/abs/repo/src/ui/app-shell.ts', + 'FNF:39', + 'FNH:38', + 'BRF:90', + 'BRH:85', + 'end_of_record', + 'SF:/abs/repo/src/ui/left-rail.ts', + 'FNF:6', + 'FNH:6', + 'BRF:2', + 'BRH:2', + 'end_of_record', + ].join('\n'); + + it('parses the found/hit totals per file', () => { + const parsed = parseLcov(lcov); + expect(parsed['/abs/repo/src/ui/app-shell.ts']).toEqual({ + branchesFound: 90, branchesHit: 85, functionsFound: 39, functionsHit: 38, + }); + }); + + it('resolves a repo-relative manifest path against an absolute lcov key', () => { + // lcov writes absolute paths here while the manifest is repo-relative; a + // plain lookup misses every file and would report zero branches everywhere. + const parsed = parseLcov(lcov); + expect(lookupLcov(parsed, 'src/ui/left-rail.ts').functionsFound).toBe(6); + }); + + it('returns null — never a zero record — for a file with no lcov data', () => { + // "No coverage data" and "no branches" are different claims, and conflating + // them would let a missing measurement read as a perfect one. + expect(lookupLcov(parseLcov(lcov), 'src/ui/absent.ts')).toBeNull(); + }); + + it('does not match a path that merely shares a filename suffix', () => { + const parsed = parseLcov('SF:/abs/repo/src/ui/rail.ts\nBRF:1\nend_of_record'); + expect(lookupLcov(parsed, 'src/ui/left-rail.ts')).toBeNull(); + }); +}); + +describe('buildComplexityReport', () => { + it('rejects an unknown file class instead of silently bucketing it', () => { + // The manifest is acceptance criterion 4's auditable artifact, so a typo in + // a class name has to be loud. + expect(() => buildComplexityReport({ entries: [entry({ class: 'plumbling' })] })) + .toThrow(/unknown class "plumbling"/); + }); + + it('totals separately per class so domain is never credited to plumbing', () => { + const report = buildComplexityReport({ + entries: [ + entry({ path: 'a.ts', class: 'plumbing', normalizedLines: 10 }), + entry({ path: 'b.ts', class: 'domain', normalizedLines: 20 }), + entry({ path: 'c.ts', class: 'island', normalizedLines: 30 }), + ], + }); + expect(report.byClass.plumbing.normalizedLines).toBe(10); + expect(report.byClass.domain.normalizedLines).toBe(20); + expect(report.byClass.island.normalizedLines).toBe(30); + expect(report.overall.normalizedLines).toBe(60); + }); + + it('exposes the deciding metric under its own name', () => { + const report = buildComplexityReport({ + entries: [entry({ class: 'plumbing', normalizedLines: 7 })], + }); + expect(report.ownedProductionCode.plumbingNormalizedLines).toBe(7); + }); + + it('derives non-code lines as physical minus blank minus code', () => { + const report = buildComplexityReport({ + entries: [entry({ physicalLines: 100, sourceBlankLines: 10, normalizedLines: 30 })], + }); + expect(report.overall.nonCodeLines).toBe(60); + }); + + it('records an absent file without counting it as zero-sized code', () => { + // The comparability rule: one canonical manifest across every state, so a + // file that disappears is reported as absent rather than silently dropped — + // for the treatment state, that absence IS the headline result. + const report = buildComplexityReport({ + entries: [ + entry({ path: 'kept.ts', normalizedLines: 40 }), + { path: 'deleted.ts', class: 'plumbing', absent: true }, + ], + }); + expect(report.overall.absentFiles).toBe(1); + expect(report.overall.files).toBe(1); + expect(report.overall.normalizedLines).toBe(40); + }); + + it('marks an absent file in the rendered report instead of omitting the row', () => { + const md = renderComplexityMarkdown(buildComplexityReport({ + entries: [{ path: 'deleted.ts', class: 'plumbing', absent: true }], + })); + expect(md).toContain('deleted.ts'); + expect(md).toContain('_absent_'); + expect(md).toMatch(/a deletion must show up, not vanish/); + }); + + it('still validates the class of an absent file', () => { + expect(() => buildComplexityReport({ entries: [{ path: 'x.ts', class: 'bogus', absent: true }] })) + .toThrow(/unknown class/); + }); + + it('counts files with no lcov record as unmatched', () => { + const report = buildComplexityReport({ entries: [entry({ lcov: null })] }); + expect(report.overall.lcovUnmatchedFiles).toBe(1); + expect(report.overall.lcovBranchesFound).toBe(0); + }); + + it('emits the metric tiering, so a demoted metric cannot be quietly promoted', () => { + const report = buildComplexityReport({ entries: [entry()] }); + expect(report.metricTiers).toBe(METRIC_TIERS); + expect(report.metricTiers.explanatory.lcovBranches.demotedBecause).toMatch(/externalized/); + expect(Object.keys(report.metricTiers.deciding)).toContain('changeAmplification'); + }); + + it('sorts files deterministically so two reports diff cleanly', () => { + const report = buildComplexityReport({ + entries: [entry({ path: 'z.ts' }), entry({ path: 'a.ts' })], + }); + expect(report.files.map((f) => f.path)).toEqual(['a.ts', 'z.ts']); + }); + + it('stamps the schema version', () => { + expect(buildComplexityReport({ entries: [entry()] }).schemaVersion).toBe(COMPLEXITY_SCHEMA_VERSION); + }); +}); + +describe('deltas', () => { + it('computes absolute and percentage change', () => { + expect(computeComplexityDelta(120, 100)).toEqual({ current: 120, base: 100, abs: 20, pct: 20 }); + }); + + it('reports a null percentage against a zero base rather than Infinity', () => { + // Printing `Infinity%` in a decision document is worse than printing nothing. + expect(computeComplexityDelta(5, 0).pct).toBeNull(); + }); + + it('diffs per class and overall', () => { + const mk = (n) => buildComplexityReport({ entries: [entry({ normalizedLines: n })] }); + const deltas = diffComplexityReports(mk(80), mk(100)); + expect(deltas.byClass.plumbing.normalizedLines.abs).toBe(-20); + expect(deltas.overall.normalizedLines.abs).toBe(-20); + }); +}); + +describe('renderComplexityMarkdown', () => { + it('states the demotion reason for every explanatory metric', () => { + const md = renderComplexityMarkdown(buildComplexityReport({ entries: [entry()] })); + for (const metric of Object.keys(METRIC_TIERS.explanatory)) { + expect(md).toContain(`\`${metric}\``); + } + expect(md).toMatch(/must not be voted with/); + }); + + it('flags unmatched lcov files instead of printing a zero', () => { + const md = renderComplexityMarkdown(buildComplexityReport({ entries: [entry({ lcov: null })] })); + expect(md).toContain('_unmatched_'); + expect(md).toMatch(/never as zero branches/); + }); + + it('renders a delta section only when a base is supplied', () => { + const report = buildComplexityReport({ entries: [entry()] }); + expect(renderComplexityMarkdown(report)).not.toContain('Delta vs base'); + expect(renderComplexityMarkdown(report, diffComplexityReports(report, report))) + .toContain('Delta vs base'); + }); + + it('renders the captured environment when present', () => { + const report = buildComplexityReport({ entries: [entry()], environment: { commit: 'abc123' } }); + expect(renderComplexityMarkdown(report)).toContain('abc123'); + }); +}); + +describe('the committed manifest', () => { + /** The measured file set, pinned. + * + * Deliberately a PINNED LIST rather than a filesystem-existence check. The + * same manifest is measured against every evaluation state, and a file is + * legitimately absent in some of them — the inspector does not exist in S0, + * and the vanilla shell does not exist in S2, which is the single most + * decision-relevant fact the whole report carries. An existence assertion + * would therefore fail on the states it matters most on. + * + * A pin still catches what actually needs catching: a rename or an accidental + * drop silently shrinks the measured baseline, and that is exactly the kind of + * undetectable bias this instrument exists to prevent. Changing the set now + * requires consciously changing this list. */ + const PINNED_PATHS = [ + 'src/application/left-nav.ts', + 'src/core/left-nav-layout.ts', + 'src/ui/app-shell.ts', + 'src/ui/drawer.ts', + 'src/ui/left-nav-separator.ts', + 'src/ui/left-rail.ts', + 'src/ui/nav-sections.ts', + 'src/ui/right-inspector.ts', + 'src/ui/sidebar-upper.ts', + 'src/ui/splitters.ts', + 'src/ui/workbench/workbench-shell.ts', + ]; + + async function loadManifest() { + return JSON.parse(await readFile(resolve(repoRoot, 'build/ui-complexity-manifest.json'), 'utf8')); + } + + it('measures exactly the pinned file set', async () => { + const manifest = await loadManifest(); + expect([...manifest.files.map((f) => f.path)].sort()).toEqual([...PINNED_PATHS].sort()); + }); + + it('declares a known class for every file, and no duplicate paths', async () => { + const manifest = await loadManifest(); + const seen = new Set(); + for (const file of manifest.files) { + expect(FILE_CLASSES, `${file.path} class`).toContain(file.class); + expect(seen.has(file.path), `${file.path} duplicated`).toBe(false); + seen.add(file.path); + } + }); + + it('classifies the three calls a first pass got wrong', async () => { + // These were mislabelled as replaceable plumbing in an early draft, which + // overstated the baseline the Preact arm had to beat by ~1000 physical lines. + const byPath = Object.fromEntries((await loadManifest()).files.map((f) => [f.path, f.class])); + expect(byPath['src/ui/left-nav-separator.ts']).toBe('island'); + expect(byPath['src/ui/nav-sections.ts']).toBe('domain'); + expect(byPath['src/ui/sidebar-upper.ts']).toBe('domain'); + }); + + it('covers every numbered item of #577\'s evaluation scope that has code today', async () => { + // Items 1-3, 5 and 6 exist on main. Item 4 (the right inspector) is what the + // control adds, and item 7 (CodeMirror) sits behind the EditorPort seam and + // is measured as an island via its adapter, not as shell plumbing. + const manifest = JSON.parse(await readFile(resolve(repoRoot, 'build/ui-complexity-manifest.json'), 'utf8')); + const covered = manifest.files.map((f) => f.sliceItem || '').join(' '); + for (const item of ['1', '2', '3', '5', '6']) { + expect(covered, `slice item ${item}`).toContain(item); + } + }); +}); From 45d08c8b509f35297053d86d3e92aad988fca35b Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Thu, 30 Jul 2026 20:40:44 +0200 Subject: [PATCH 2/6] docs(#577): pin the S0 and S1 measurement evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Derived evidence committed on the branch that will merge, not left only on the evaluation refs. ADR-0001's Preact addendum is currently the ONLY trace of the `spike/preact-schema` branch it cites — that branch is gone from `origin` — and this is the mistake that loses. Numbers a reader cannot re-derive are not reproducible measurements (acceptance criterion 5). Both states measured over the ONE canonical 11-file manifest committed here alongside them, from clean trees, with the instrument identical in both (it lands complete in S0 for exactly that reason). Each report embeds its own environment capture: resolved commit SHA (never a tag name — tags can be moved), lockfile hash, Node and esbuild versions, platform, and a clean/dirty flag. Headline: the vanilla control costs +147 code lines (plumbing +143, island +4, domain 0) and +4098 raw / +1028 gzip artifact bytes. That is what a #488-shaped right inspector costs under the current architecture, and it is the number the Preact treatment has to beat on the same feature. The esbuild metafiles are deliberately NOT committed yet (~360 KB each). They land with the final report, where the S2 metafile's per-input attribution is what actually PROVES the vanilla shell was deleted rather than shipped alongside. Part of #577. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XLuZUHRbTCFKDJf2Zf8nP1 --- docs/design/577/canonical-manifest.json | 92 +++++ docs/design/577/s0/bundle-size-report.json | 386 +++++++++++++++++++ docs/design/577/s0/ui-complexity-report.json | 352 +++++++++++++++++ docs/design/577/s1/bundle-size-report.json | 386 +++++++++++++++++++ docs/design/577/s1/ui-complexity-report.json | 363 +++++++++++++++++ 5 files changed, 1579 insertions(+) create mode 100644 docs/design/577/canonical-manifest.json create mode 100644 docs/design/577/s0/bundle-size-report.json create mode 100644 docs/design/577/s0/ui-complexity-report.json create mode 100644 docs/design/577/s1/bundle-size-report.json create mode 100644 docs/design/577/s1/ui-complexity-report.json diff --git a/docs/design/577/canonical-manifest.json b/docs/design/577/canonical-manifest.json new file mode 100644 index 00000000..1d74e493 --- /dev/null +++ b/docs/design/577/canonical-manifest.json @@ -0,0 +1,92 @@ +{ + "_comment": [ + "The auditable file set for #577's UI-complexity measurement. Acceptance", + "criterion 4 requires the report to distinguish product-specific domain logic", + "from framework-like rendering plumbing, so THIS FILE is the artifact that", + "claim rests on — a wrong `class` here is a wrong decision, not a wrong", + "number. Every file the report names comes from here and nowhere else.", + "", + "Classes:", + " domain — preserved verbatim by both the vanilla control and the Preact", + " treatment. Any diff in these files during the evaluation is a", + " finding against #577's own 'preserve proven non-rendering", + " logic' constraint, not a licence.", + " plumbing — genuinely replaceable by a component model. This is the only", + " class the treatment may claim a reduction in.", + " island — stays imperative under ANY render model (CLAUDE.md hard rule 5:", + " signals coordinate state, they do not own every mousemove).", + " Counted so the report cannot present island code as a Preact", + " win or a Preact cost.", + "", + "The `sliceItem` field maps each file to the numbered item in #577's", + "'Evaluation scope' list it covers, so a reader can check the slice is", + "actually covered rather than taking the report's word for it.", + "", + "Classification rationale for the three non-obvious calls (a first pass at", + "this plan mislabelled all three, which would have overstated the", + "replaceable baseline by roughly 1000 physical lines):", + " - left-nav-separator.ts is an ISLAND, not plumbing: it is pointer/keyboard", + " mechanics plus resize-session bookkeeping. Only its thin ARIA/paint glue", + " is plumbing, and that is not separable at file granularity.", + " - nav-sections.ts and sidebar-upper.ts are DOMAIN shared verbatim: they are", + " NAV_SECTION_META plus persistent-host construction that BOTH arms need.", + " Only a ~10-line showSection is replaceable." + ], + "files": [ + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching" + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation" + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations" + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher" + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics" + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free" + }, + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam" + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host" + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes" + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)" + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector" + } + ] +} diff --git a/docs/design/577/s0/bundle-size-report.json b/docs/design/577/s0/bundle-size-report.json new file mode 100644 index 00000000..4fc2a468 --- /dev/null +++ b/docs/design/577/s0/bundle-size-report.json @@ -0,0 +1,386 @@ +{ + "schemaVersion": 1, + "artifact": { + "raw": 2126521, + "gzip": 623736, + "brotli": 527859 + }, + "js": { + "raw": 1868626, + "gzip": 508303, + "brotli": 419075 + }, + "css": { + "raw": 237839, + "gzip": 109628, + "brotli": 105834 + }, + "totalOutputBytes": 1868430, + "entryPoints": [ + { + "file": "main.js", + "entryPoint": "src/main.ts", + "bytes": 1868623 + } + ], + "ownership": { + "project": { + "bytes": 629424, + "pct": 33.687320370578504 + }, + "generated": { + "bytes": 457367, + "pct": 24.47867996125089 + }, + "external": { + "bytes": 781639, + "pct": 41.8339996681706 + }, + "other": { + "bytes": 0, + "pct": 0 + } + }, + "packages": [ + { + "name": "chart.js", + "bytes": 197174, + "pct": 10.55292411275777 + }, + { + "name": "@codemirror/view", + "bytes": 186268, + "pct": 9.969225499483523 + }, + { + "name": "date-fns", + "bytes": 47998, + "pct": 2.5688947405040596 + }, + { + "name": "@codemirror/state", + "bytes": 46747, + "pct": 2.5019401315542993 + }, + { + "name": "marked", + "bytes": 41843, + "pct": 2.2394737828016034 + }, + { + "name": "@dagrejs/dagre", + "bytes": 40422, + "pct": 2.163420625873059 + }, + { + "name": "@codemirror/lang-sql", + "bytes": 32138, + "pct": 1.720053734953945 + }, + { + "name": "@codemirror/autocomplete", + "bytes": 31522, + "pct": 1.6870848787484678 + }, + { + "name": "@lezer/lr", + "bytes": 26552, + "pct": 1.421086152545185 + }, + { + "name": "@codemirror/language", + "bytes": 24486, + "pct": 1.3105120341677237 + }, + { + "name": "@codemirror/commands", + "bytes": 23371, + "pct": 1.250836263600991 + }, + { + "name": "@lezer/common", + "bytes": 20388, + "pct": 1.091183507008558 + }, + { + "name": "@codemirror/search", + "bytes": 18472, + "pct": 0.9886375192006123 + }, + { + "name": "@lezer/xml", + "bytes": 8569, + "pct": 0.45862033900119353 + }, + { + "name": "@kurkle/color", + "bytes": 7597, + "pct": 0.40659805291073253 + }, + { + "name": "@lezer/highlight", + "bytes": 7178, + "pct": 0.38417280818655236 + }, + { + "name": "@codemirror/lang-xml", + "bytes": 5975, + "pct": 0.31978720101903735 + }, + { + "name": "@preact/signals-core", + "bytes": 4713, + "pct": 0.25224386249417957 + }, + { + "name": "@marijn/find-cluster-break", + "bytes": 2332, + "pct": 0.1248106699207356 + }, + { + "name": "style-mod", + "bytes": 2202, + "pct": 0.11785295676048875 + }, + { + "name": "chartjs-adapter-date-fns", + "bytes": 1688, + "pct": 0.09034322934228202 + }, + { + "name": "@lezer/json", + "bytes": 1599, + "pct": 0.08557987187103612 + }, + { + "name": "w3c-keyname", + "bytes": 1520, + "pct": 0.08135172310442457 + }, + { + "name": "crelt", + "bytes": 611, + "pct": 0.03270125185316014 + }, + { + "name": "@codemirror/lang-json", + "bytes": 274, + "pct": 0.014664718506981797 + } + ], + "topModules": [ + { + "path": "src/generated/json-schema-validators.js", + "bytes": 214595, + "pct": 11.485311197101309, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "src/generated/example-dashboards.ts", + "bytes": 187686, + "pct": 10.045118093800678, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "node_modules/@codemirror/view/dist/index.js", + "bytes": 186268, + "pct": 9.969225499483523, + "owner": "external", + "group": "@codemirror/view" + }, + { + "path": "node_modules/chart.js/dist/chart.js", + "bytes": 165504, + "pct": 8.857918145180713, + "owner": "external", + "group": "chart.js" + }, + { + "path": "src/generated/json-schemas.js", + "bytes": 55086, + "pct": 2.9482506703489024, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "node_modules/@codemirror/state/dist/index.js", + "bytes": 46747, + "pct": 2.5019401315542993, + "owner": "external", + "group": "@codemirror/state" + }, + { + "path": "node_modules/marked/lib/marked.esm.js", + "bytes": 41843, + "pct": 2.2394737828016034, + "owner": "external", + "group": "marked" + }, + { + "path": "src/ui/app.ts", + "bytes": 41314, + "pct": 2.211161242326445, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@dagrejs/dagre/dist/dagre.esm.js", + "bytes": 40422, + "pct": 2.163420625873059, + "owner": "external", + "group": "@dagrejs/dagre" + }, + { + "path": "src/ui/dashboard.ts", + "bytes": 35268, + "pct": 1.8875740595045039, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@codemirror/lang-sql/dist/index.js", + "bytes": 32138, + "pct": 1.720053734953945, + "owner": "external", + "group": "@codemirror/lang-sql" + }, + { + "path": "node_modules/chart.js/dist/chunks/helpers.dataset.js", + "bytes": 31641, + "pct": 1.693453862333617, + "owner": "external", + "group": "chart.js" + }, + { + "path": "node_modules/@codemirror/autocomplete/dist/index.js", + "bytes": 31522, + "pct": 1.6870848787484678, + "owner": "external", + "group": "@codemirror/autocomplete" + }, + { + "path": "node_modules/@lezer/lr/dist/index.js", + "bytes": 26552, + "pct": 1.421086152545185, + "owner": "external", + "group": "@lezer/lr" + }, + { + "path": "node_modules/@codemirror/language/dist/index.js", + "bytes": 24486, + "pct": 1.3105120341677237, + "owner": "external", + "group": "@codemirror/language" + }, + { + "path": "node_modules/@codemirror/commands/dist/index.js", + "bytes": 23371, + "pct": 1.250836263600991, + "owner": "external", + "group": "@codemirror/commands" + }, + { + "path": "node_modules/@lezer/common/dist/index.js", + "bytes": 20388, + "pct": 1.091183507008558, + "owner": "external", + "group": "@lezer/common" + }, + { + "path": "src/ui/results.ts", + "bytes": 18828, + "pct": 1.0076909490855959, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@codemirror/search/dist/index.js", + "bytes": 18472, + "pct": 0.9886375192006123, + "owner": "external", + "group": "@codemirror/search" + }, + { + "path": "src/dashboard/application/dashboard-viewer-session.ts", + "bytes": 15288, + "pct": 0.8182270676450282, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/dashboard-tree.ts", + "bytes": 13899, + "pct": 0.7438865785713139, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/file-menu.ts", + "bytes": 12919, + "pct": 0.6914361255171454, + "owner": "project", + "group": "src" + }, + { + "path": "src/core/chart-data.ts", + "bytes": 12028, + "pct": 0.6437490299342229, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/explain-graph.ts", + "bytes": 11980, + "pct": 0.6411800281519778, + "owner": "project", + "group": "src" + }, + { + "path": "src/state.ts", + "bytes": 10933, + "pct": 0.585143676776759, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/shortcuts.ts", + "bytes": 10307, + "pct": 0.5516396118666475, + "owner": "project", + "group": "src" + }, + { + "path": "src/net/ch-client.ts", + "bytes": 9860, + "pct": 0.5277157827694909, + "owner": "project", + "group": "src" + }, + { + "path": "src/dashboard/model/workspace-semantics.ts", + "bytes": 8712, + "pct": 0.46627382347746504, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@lezer/xml/dist/index.js", + "bytes": 8569, + "pct": 0.45862033900119353, + "owner": "external", + "group": "@lezer/xml" + }, + { + "path": "src/core/spec-schema.ts", + "bytes": 8272, + "pct": 0.44272464047355264, + "owner": "project", + "group": "src" + } + ], + "notes": [ + "Percentages are of raw contributed output bytes (metafile bytesInOutput); gzip/Brotli are measured per whole artifact only — compression is not additive across modules." + ] +} \ No newline at end of file diff --git a/docs/design/577/s0/ui-complexity-report.json b/docs/design/577/s0/ui-complexity-report.json new file mode 100644 index 00000000..ec16be85 --- /dev/null +++ b/docs/design/577/s0/ui-complexity-report.json @@ -0,0 +1,352 @@ +{ + "schemaVersion": 1, + "label": "s0-baseline", + "metricTiers": { + "deciding": { + "ownedProductionCode": { + "owner": "this instrument" + }, + "parity": { + "owner": "tests/e2e/shell-parity.spec.js" + }, + "artifact": { + "owner": "build/size-report.mjs" + }, + "changeAmplification": { + "owner": "frozen controlled-change experiment" + }, + "cleanupObligations": { + "owner": "leak invariant + manual obligation count" + } + }, + "explanatory": { + "minifiedBytes": { + "demotedBecause": "ignores tree shaking, shared helpers, and complexity moved into a dependency" + }, + "lcovBranches": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lcovFunctions": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lifecycleSites": { + "demotedBecause": "hidden/dataset/replaceChildren go syntactically invisible under a vDOM" + } + } + }, + "environment": { + "commit": "d319847875b3b3eeff2c4ca4c43fd7dda12f8b40", + "commitDirty": "clean", + "node": "v25.9.0", + "esbuild": "0.28.1", + "platform": "darwin-arm64", + "packageLock": "sha256:9042ec0421903e38" + }, + "ownedProductionCode": { + "plumbingNormalizedLines": 599, + "islandNormalizedLines": 199, + "domainNormalizedLines": 341 + }, + "byClass": { + "domain": { + "files": 4, + "absentFiles": 0, + "physicalLines": 1367, + "normalizedLines": 341, + "nonCodeLines": 953, + "minifiedBytes": 8984, + "lifecycle": { + "domMutation": 10, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcovBranchesFound": 150, + "lcovFunctionsFound": 44, + "lcovUnmatchedFiles": 0 + }, + "plumbing": { + "files": 4, + "absentFiles": 1, + "physicalLines": 1470, + "normalizedLines": 599, + "nonCodeLines": 817, + "minifiedBytes": 15143, + "lifecycle": { + "domMutation": 60, + "listener": 7, + "effect": 17, + "disposal": 34, + "focus": 7 + }, + "lcovBranchesFound": 144, + "lcovFunctionsFound": 81, + "lcovUnmatchedFiles": 0 + }, + "island": { + "files": 2, + "absentFiles": 0, + "physicalLines": 590, + "normalizedLines": 199, + "nonCodeLines": 350, + "minifiedBytes": 4156, + "lifecycle": { + "domMutation": 14, + "listener": 18, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcovBranchesFound": 48, + "lcovFunctionsFound": 27, + "lcovUnmatchedFiles": 0 + } + }, + "overall": { + "files": 10, + "absentFiles": 1, + "physicalLines": 3427, + "normalizedLines": 1139, + "nonCodeLines": 2120, + "minifiedBytes": 28283, + "lifecycle": { + "domMutation": 84, + "listener": 25, + "effect": 21, + "disposal": 38, + "focus": 7 + }, + "lcovBranchesFound": 342, + "lcovFunctionsFound": 152, + "lcovUnmatchedFiles": 0 + }, + "files": [ + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam", + "physicalLines": 175, + "sourceBlankLines": 8, + "normalizedLines": 48, + "minifiedBytes": 951, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 8, + "branchesHit": 8, + "functionsFound": 7, + "functionsHit": 7 + } + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free", + "physicalLines": 853, + "sourceBlankLines": 39, + "normalizedLines": 150, + "minifiedBytes": 4836, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 124, + "branchesHit": 124, + "functionsFound": 23, + "functionsHit": 23 + } + }, + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching", + "physicalLines": 880, + "sourceBlankLines": 30, + "normalizedLines": 328, + "minifiedBytes": 7603, + "lifecycle": { + "domMutation": 39, + "listener": 3, + "effect": 14, + "disposal": 22, + "focus": 4 + }, + "lcov": { + "branchesFound": 90, + "branchesHit": 90, + "functionsFound": 39, + "functionsHit": 39 + } + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes", + "physicalLines": 144, + "sourceBlankLines": 7, + "normalizedLines": 56, + "minifiedBytes": 993, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 11, + "branchesHit": 11, + "functionsFound": 10, + "functionsHit": 10 + } + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics", + "physicalLines": 443, + "sourceBlankLines": 29, + "normalizedLines": 157, + "minifiedBytes": 3170, + "lifecycle": { + "domMutation": 12, + "listener": 14, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcov": { + "branchesFound": 28, + "branchesHit": 28, + "functionsFound": 21, + "functionsHit": 21 + } + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation", + "physicalLines": 129, + "sourceBlankLines": 8, + "normalizedLines": 51, + "minifiedBytes": 775, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 1, + "disposal": 6, + "focus": 3 + }, + "lcov": { + "branchesFound": 2, + "branchesHit": 2, + "functionsFound": 6, + "functionsHit": 6 + } + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations", + "physicalLines": 202, + "sourceBlankLines": 13, + "normalizedLines": 69, + "minifiedBytes": 1464, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 2, + "branchesHit": 2, + "functionsFound": 8, + "functionsHit": 8 + } + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)", + "absent": true + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher", + "physicalLines": 137, + "sourceBlankLines": 13, + "normalizedLines": 74, + "minifiedBytes": 1733, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 16, + "branchesHit": 15, + "functionsFound": 6, + "functionsHit": 6 + } + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector", + "physicalLines": 147, + "sourceBlankLines": 12, + "normalizedLines": 42, + "minifiedBytes": 986, + "lifecycle": { + "domMutation": 2, + "listener": 4, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 20, + "branchesHit": 20, + "functionsFound": 6, + "functionsHit": 6 + } + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host", + "physicalLines": 317, + "sourceBlankLines": 9, + "normalizedLines": 164, + "minifiedBytes": 5772, + "lifecycle": { + "domMutation": 17, + "listener": 4, + "effect": 2, + "disposal": 6, + "focus": 0 + }, + "lcov": { + "branchesFound": 41, + "branchesHit": 40, + "functionsFound": 26, + "functionsHit": 26 + } + } + ] +} diff --git a/docs/design/577/s1/bundle-size-report.json b/docs/design/577/s1/bundle-size-report.json new file mode 100644 index 00000000..8476b8bb --- /dev/null +++ b/docs/design/577/s1/bundle-size-report.json @@ -0,0 +1,386 @@ +{ + "schemaVersion": 1, + "artifact": { + "raw": 2130619, + "gzip": 624764, + "brotli": 528747 + }, + "js": { + "raw": 1870835, + "gzip": 509126, + "brotli": 419745 + }, + "css": { + "raw": 239728, + "gzip": 109836, + "brotli": 105960 + }, + "totalOutputBytes": 1870639, + "entryPoints": [ + { + "file": "main.js", + "entryPoint": "src/main.ts", + "bytes": 1870832 + } + ], + "ownership": { + "project": { + "bytes": 631633, + "pct": 33.7656276812362 + }, + "generated": { + "bytes": 457367, + "pct": 24.449773580044038 + }, + "external": { + "bytes": 781639, + "pct": 41.784598738719765 + }, + "other": { + "bytes": 0, + "pct": 0 + } + }, + "packages": [ + { + "name": "chart.js", + "bytes": 197174, + "pct": 10.540462376760026 + }, + { + "name": "@codemirror/view", + "bytes": 186268, + "pct": 9.957453041447334 + }, + { + "name": "date-fns", + "bytes": 47998, + "pct": 2.5658611843332677 + }, + { + "name": "@codemirror/state", + "bytes": 46747, + "pct": 2.498985640735599 + }, + { + "name": "marked", + "bytes": 41843, + "pct": 2.236829233219237 + }, + { + "name": "@dagrejs/dagre", + "bytes": 40422, + "pct": 2.160865885935234 + }, + { + "name": "@codemirror/lang-sql", + "bytes": 32138, + "pct": 1.718022558067056 + }, + { + "name": "@codemirror/autocomplete", + "bytes": 31522, + "pct": 1.6850926341212817 + }, + { + "name": "@lezer/lr", + "bytes": 26552, + "pct": 1.419408020467872 + }, + { + "name": "@codemirror/language", + "bytes": 24486, + "pct": 1.3089644768445434 + }, + { + "name": "@codemirror/commands", + "bytes": 23371, + "pct": 1.2493591761959415 + }, + { + "name": "@lezer/common", + "bytes": 20388, + "pct": 1.089894950335153 + }, + { + "name": "@codemirror/search", + "bytes": 18472, + "pct": 0.9874700570232953 + }, + { + "name": "@lezer/xml", + "bytes": 8569, + "pct": 0.4580787634599728 + }, + { + "name": "@kurkle/color", + "bytes": 7597, + "pct": 0.40611790944164 + }, + { + "name": "@lezer/highlight", + "bytes": 7178, + "pct": 0.38371914623826403 + }, + { + "name": "@codemirror/lang-xml", + "bytes": 5975, + "pct": 0.3194095707402657 + }, + { + "name": "@preact/signals-core", + "bytes": 4713, + "pct": 0.25194599278642216 + }, + { + "name": "@marijn/find-cluster-break", + "bytes": 2332, + "pct": 0.12466328350900416 + }, + { + "name": "style-mod", + "bytes": 2202, + "pct": 0.11771378657239584 + }, + { + "name": "chartjs-adapter-date-fns", + "bytes": 1688, + "pct": 0.09023654483842151 + }, + { + "name": "@lezer/json", + "bytes": 1599, + "pct": 0.085478812320282 + }, + { + "name": "w3c-keyname", + "bytes": 1520, + "pct": 0.08125565648957389 + }, + { + "name": "crelt", + "bytes": 611, + "pct": 0.032662635602058974 + }, + { + "name": "@codemirror/lang-json", + "bytes": 274, + "pct": 0.014647401235620555 + } + ], + "topModules": [ + { + "path": "src/generated/json-schema-validators.js", + "bytes": 214595, + "pct": 11.471748423934281, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "src/generated/example-dashboards.ts", + "bytes": 187686, + "pct": 10.033256015725108, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "node_modules/@codemirror/view/dist/index.js", + "bytes": 186268, + "pct": 9.957453041447334, + "owner": "external", + "group": "@codemirror/view" + }, + { + "path": "node_modules/chart.js/dist/chart.js", + "bytes": 165504, + "pct": 8.84745800766476, + "owner": "external", + "group": "chart.js" + }, + { + "path": "src/generated/json-schemas.js", + "bytes": 55086, + "pct": 2.944769140384649, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "node_modules/@codemirror/state/dist/index.js", + "bytes": 46747, + "pct": 2.498985640735599, + "owner": "external", + "group": "@codemirror/state" + }, + { + "path": "node_modules/marked/lib/marked.esm.js", + "bytes": 41843, + "pct": 2.236829233219237, + "owner": "external", + "group": "marked" + }, + { + "path": "src/ui/app.ts", + "bytes": 41314, + "pct": 2.2085501264541154, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@dagrejs/dagre/dist/dagre.esm.js", + "bytes": 40422, + "pct": 2.160865885935234, + "owner": "external", + "group": "@dagrejs/dagre" + }, + { + "path": "src/ui/dashboard.ts", + "bytes": 35268, + "pct": 1.8853450612330866, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@codemirror/lang-sql/dist/index.js", + "bytes": 32138, + "pct": 1.718022558067056, + "owner": "external", + "group": "@codemirror/lang-sql" + }, + { + "path": "node_modules/chart.js/dist/chunks/helpers.dataset.js", + "bytes": 31641, + "pct": 1.6914540967017153, + "owner": "external", + "group": "chart.js" + }, + { + "path": "node_modules/@codemirror/autocomplete/dist/index.js", + "bytes": 31522, + "pct": 1.6850926341212817, + "owner": "external", + "group": "@codemirror/autocomplete" + }, + { + "path": "node_modules/@lezer/lr/dist/index.js", + "bytes": 26552, + "pct": 1.419408020467872, + "owner": "external", + "group": "@lezer/lr" + }, + { + "path": "node_modules/@codemirror/language/dist/index.js", + "bytes": 24486, + "pct": 1.3089644768445434, + "owner": "external", + "group": "@codemirror/language" + }, + { + "path": "node_modules/@codemirror/commands/dist/index.js", + "bytes": 23371, + "pct": 1.2493591761959415, + "owner": "external", + "group": "@codemirror/commands" + }, + { + "path": "node_modules/@lezer/common/dist/index.js", + "bytes": 20388, + "pct": 1.089894950335153, + "owner": "external", + "group": "@lezer/common" + }, + { + "path": "src/ui/results.ts", + "bytes": 18828, + "pct": 1.0065009870958534, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@codemirror/search/dist/index.js", + "bytes": 18472, + "pct": 0.9874700570232953, + "owner": "external", + "group": "@codemirror/search" + }, + { + "path": "src/dashboard/application/dashboard-viewer-session.ts", + "bytes": 15288, + "pct": 0.8172608397451353, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/dashboard-tree.ts", + "bytes": 13899, + "pct": 0.7430081378609127, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/file-menu.ts", + "bytes": 12919, + "pct": 0.6906196224926349, + "owner": "project", + "group": "src" + }, + { + "path": "src/core/chart-data.ts", + "bytes": 12028, + "pct": 0.6429888396424965, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/explain-graph.ts", + "bytes": 11980, + "pct": 0.6404228715428257, + "owner": "project", + "group": "src" + }, + { + "path": "src/state.ts", + "bytes": 11032, + "pct": 0.5897450015743284, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/shortcuts.ts", + "bytes": 10307, + "pct": 0.5509881917355514, + "owner": "project", + "group": "src" + }, + { + "path": "src/net/ch-client.ts", + "bytes": 9860, + "pct": 0.5270926138073675, + "owner": "project", + "group": "src" + }, + { + "path": "src/dashboard/model/workspace-semantics.ts", + "bytes": 8712, + "pct": 0.4657232100902419, + "owner": "project", + "group": "src" + }, + { + "path": "node_modules/@lezer/xml/dist/index.js", + "bytes": 8569, + "pct": 0.4580787634599728, + "owner": "external", + "group": "@lezer/xml" + }, + { + "path": "src/core/spec-schema.ts", + "bytes": 8272, + "pct": 0.44220183584325995, + "owner": "project", + "group": "src" + } + ], + "notes": [ + "Percentages are of raw contributed output bytes (metafile bytesInOutput); gzip/Brotli are measured per whole artifact only — compression is not additive across modules." + ] +} \ No newline at end of file diff --git a/docs/design/577/s1/ui-complexity-report.json b/docs/design/577/s1/ui-complexity-report.json new file mode 100644 index 00000000..d28797cb --- /dev/null +++ b/docs/design/577/s1/ui-complexity-report.json @@ -0,0 +1,363 @@ +{ + "schemaVersion": 1, + "label": "s1-control", + "metricTiers": { + "deciding": { + "ownedProductionCode": { + "owner": "this instrument" + }, + "parity": { + "owner": "tests/e2e/shell-parity.spec.js" + }, + "artifact": { + "owner": "build/size-report.mjs" + }, + "changeAmplification": { + "owner": "frozen controlled-change experiment" + }, + "cleanupObligations": { + "owner": "leak invariant + manual obligation count" + } + }, + "explanatory": { + "minifiedBytes": { + "demotedBecause": "ignores tree shaking, shared helpers, and complexity moved into a dependency" + }, + "lcovBranches": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lcovFunctions": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lifecycleSites": { + "demotedBecause": "hidden/dataset/replaceChildren go syntactically invisible under a vDOM" + } + } + }, + "environment": { + "commit": "7e7c8c4b6d8c5853d4e59dc599c5705f8e9cacab", + "commitDirty": "clean", + "node": "v25.9.0", + "esbuild": "0.28.1", + "platform": "darwin-arm64", + "packageLock": "sha256:9042ec0421903e38" + }, + "ownedProductionCode": { + "plumbingNormalizedLines": 742, + "islandNormalizedLines": 203, + "domainNormalizedLines": 341 + }, + "byClass": { + "domain": { + "files": 4, + "absentFiles": 0, + "physicalLines": 1367, + "normalizedLines": 341, + "nonCodeLines": 953, + "minifiedBytes": 8984, + "lifecycle": { + "domMutation": 10, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcovBranchesFound": 150, + "lcovFunctionsFound": 44, + "lcovUnmatchedFiles": 0 + }, + "plumbing": { + "files": 5, + "absentFiles": 0, + "physicalLines": 1766, + "normalizedLines": 742, + "nonCodeLines": 949, + "minifiedBytes": 17444, + "lifecycle": { + "domMutation": 72, + "listener": 7, + "effect": 17, + "disposal": 36, + "focus": 8 + }, + "lcovBranchesFound": 144, + "lcovFunctionsFound": 81, + "lcovUnmatchedFiles": 1 + }, + "island": { + "files": 2, + "absentFiles": 0, + "physicalLines": 603, + "normalizedLines": 203, + "nonCodeLines": 359, + "minifiedBytes": 4269, + "lifecycle": { + "domMutation": 14, + "listener": 18, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcovBranchesFound": 48, + "lcovFunctionsFound": 27, + "lcovUnmatchedFiles": 0 + } + }, + "overall": { + "files": 11, + "absentFiles": 0, + "physicalLines": 3736, + "normalizedLines": 1286, + "nonCodeLines": 2261, + "minifiedBytes": 30697, + "lifecycle": { + "domMutation": 96, + "listener": 25, + "effect": 21, + "disposal": 40, + "focus": 8 + }, + "lcovBranchesFound": 342, + "lcovFunctionsFound": 152, + "lcovUnmatchedFiles": 1 + }, + "files": [ + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam", + "physicalLines": 175, + "sourceBlankLines": 8, + "normalizedLines": 48, + "minifiedBytes": 951, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 8, + "branchesHit": 8, + "functionsFound": 7, + "functionsHit": 7 + } + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free", + "physicalLines": 853, + "sourceBlankLines": 39, + "normalizedLines": 150, + "minifiedBytes": 4836, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 124, + "branchesHit": 124, + "functionsFound": 23, + "functionsHit": 23 + } + }, + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching", + "physicalLines": 905, + "sourceBlankLines": 31, + "normalizedLines": 340, + "minifiedBytes": 7856, + "lifecycle": { + "domMutation": 41, + "listener": 3, + "effect": 14, + "disposal": 23, + "focus": 4 + }, + "lcov": { + "branchesFound": 90, + "branchesHit": 90, + "functionsFound": 39, + "functionsHit": 39 + } + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes", + "physicalLines": 144, + "sourceBlankLines": 7, + "normalizedLines": 56, + "minifiedBytes": 993, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 11, + "branchesHit": 11, + "functionsFound": 10, + "functionsHit": 10 + } + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics", + "physicalLines": 443, + "sourceBlankLines": 29, + "normalizedLines": 157, + "minifiedBytes": 3170, + "lifecycle": { + "domMutation": 12, + "listener": 14, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcov": { + "branchesFound": 28, + "branchesHit": 28, + "functionsFound": 21, + "functionsHit": 21 + } + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation", + "physicalLines": 129, + "sourceBlankLines": 8, + "normalizedLines": 51, + "minifiedBytes": 775, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 1, + "disposal": 6, + "focus": 3 + }, + "lcov": { + "branchesFound": 2, + "branchesHit": 2, + "functionsFound": 6, + "functionsHit": 6 + } + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations", + "physicalLines": 202, + "sourceBlankLines": 13, + "normalizedLines": 69, + "minifiedBytes": 1464, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 2, + "branchesHit": 2, + "functionsFound": 8, + "functionsHit": 8 + } + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)", + "physicalLines": 271, + "sourceBlankLines": 20, + "normalizedLines": 131, + "minifiedBytes": 2048, + "lifecycle": { + "domMutation": 10, + "listener": 0, + "effect": 0, + "disposal": 1, + "focus": 1 + }, + "lcov": null + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher", + "physicalLines": 137, + "sourceBlankLines": 13, + "normalizedLines": 74, + "minifiedBytes": 1733, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 16, + "branchesHit": 15, + "functionsFound": 6, + "functionsHit": 6 + } + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector", + "physicalLines": 160, + "sourceBlankLines": 12, + "normalizedLines": 46, + "minifiedBytes": 1099, + "lifecycle": { + "domMutation": 2, + "listener": 4, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": { + "branchesFound": 20, + "branchesHit": 20, + "functionsFound": 6, + "functionsHit": 6 + } + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host", + "physicalLines": 317, + "sourceBlankLines": 9, + "normalizedLines": 164, + "minifiedBytes": 5772, + "lifecycle": { + "domMutation": 17, + "listener": 4, + "effect": 2, + "disposal": 6, + "focus": 0 + }, + "lcov": { + "branchesFound": 41, + "branchesHit": 40, + "functionsFound": 26, + "functionsHit": 26 + } + } + ] +} From 240a3cd7866494c6d0e93b1fcc43e830ddb9cf5e Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Thu, 30 Jul 2026 20:45:13 +0200 Subject: [PATCH 3/6] docs(#577): record the measurement instrument in the changelog Part of #577. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XLuZUHRbTCFKDJf2Zf8nP1 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c6eacb..180c17d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,31 @@ auto-generated per-PR notes; this file is the curated, human-readable history. ## [Unreleased] ### Added +- **A reproducible UI-complexity measurement instrument** (#577) — + `build/ui-complexity-report.mjs` plus its pure, unit-tested half + (`build/ui-complexity-lib.mjs`) and the committed, auditable + `build/ui-complexity-manifest.json`. Dev tooling only: nothing from it enters + `dist/sql.html`. It exists because #577 (evaluate migrating the UI composition + layer to Preact) has to decide on evidence, and the two obvious instruments + both lie about this repository. Physical LOC does: the five shell-plumbing + modules total 1791 physical lines but **709 lines of code**, because each + invariant is documented next to the review bug that found it — so any arm + written at normal comment density "wins" while containing more code. Regex + counts of manual DOM mutation do too: run over raw source they match the prose + *inside* those comments, and three of the four families #577 names (`hidden`, + `dataset`, `replaceChildren`) go syntactically invisible under a vDOM, scoring + ~0 for a component arm by construction. So every count runs over + esbuild-transformed source — comments stripped, formatting normalized, by the + repo's own build tool — and the report emits its own metric tiering, so a + metric demoted to explanatory cannot be quietly promoted to a deciding one. + One canonical manifest is measured against every evaluation state via + `--manifest`, with an absent file reported rather than skipped: the two most + decision-relevant facts in the comparison are a file appearing and a file + disappearing, and per-state manifests would erase both. Tests include a + sabotage suite and a structural guard that the runner counts stripped code + rather than raw source, verified falsifiable. The S0 baseline and S1 control + measurements are pinned under `docs/design/577/`. + - **The foldable left navigation is reachable in the UI** (#487, phase 3 of 4 — the phase's own most complex wiring step). `app-shell.ts` composes phase 1's pure mode/resize-session core and phase 2's section registry with a new From 771b404d12cbede5bdedb5b95cf3c7c2f6780851 Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Fri, 31 Jul 2026 00:07:14 +0200 Subject: [PATCH 4/6] docs(#577): pin the S2 treatment measurement and its findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three evaluation states re-measured in one run from clean worktrees at their tagged SHAs, over one 18-entry superset manifest — the seven src/ui/shell/* files report absent for S0/S1, and app-shell.ts / left-rail.ts / right-inspector.ts report absent for S2. A treatment measured over a manifest omitting its own new files would report a deletion with no matching cost. Headline: the Preact arm is +330 code lines and +7.8 KB gzip against the vanilla control for the same feature. Domain and island lines are identical across all three states (the preservation constraint holding). Repo-owned bytes in the artifact went UP 3,536 B. manifest-v2/s2/esbuild-inputs.json is the per-input proof that the vanilla shell is absent from the built artifact rather than shipped alongside. Also records a measurement correction: S1's brotli figure differs from the one pinned earlier while its raw and gzip match exactly, because brotli output is not stable across Node versions. Re-measuring all three states in one run is what makes that column comparable. No recommendation is made here — ADR-0004 owns it, and the parity suite, the controlled-change experiment and the JSX annex are still outstanding. Part of #577 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XLuZUHRbTCFKDJf2Zf8nP1 --- CHANGELOG.md | 32 + docs/design/577/S2-EVIDENCE.md | 219 ++++ .../577/manifest-v2/canonical-manifest.json | 140 +++ .../manifest-v2/s0/bundle-size-report.json | 386 ++++++ .../manifest-v2/s0/ui-complexity-report.json | 344 ++++++ .../manifest-v2/s0/ui-complexity-report.md | 61 + .../manifest-v2/s1/bundle-size-report.json | 386 ++++++ .../manifest-v2/s1/ui-complexity-report.json | 355 ++++++ .../manifest-v2/s1/ui-complexity-report.md | 61 + .../manifest-v2/s2/bundle-size-report.json | 396 ++++++ .../577/manifest-v2/s2/esbuild-inputs.json | 1077 +++++++++++++++++ .../manifest-v2/s2/ui-complexity-report.json | 399 ++++++ .../manifest-v2/s2/ui-complexity-report.md | 61 + 13 files changed, 3917 insertions(+) create mode 100644 docs/design/577/S2-EVIDENCE.md create mode 100644 docs/design/577/manifest-v2/canonical-manifest.json create mode 100644 docs/design/577/manifest-v2/s0/bundle-size-report.json create mode 100644 docs/design/577/manifest-v2/s0/ui-complexity-report.json create mode 100644 docs/design/577/manifest-v2/s0/ui-complexity-report.md create mode 100644 docs/design/577/manifest-v2/s1/bundle-size-report.json create mode 100644 docs/design/577/manifest-v2/s1/ui-complexity-report.json create mode 100644 docs/design/577/manifest-v2/s1/ui-complexity-report.md create mode 100644 docs/design/577/manifest-v2/s2/bundle-size-report.json create mode 100644 docs/design/577/manifest-v2/s2/esbuild-inputs.json create mode 100644 docs/design/577/manifest-v2/s2/ui-complexity-report.json create mode 100644 docs/design/577/manifest-v2/s2/ui-complexity-report.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 180c17d3..6ff183fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,38 @@ auto-generated per-PR notes; this file is the curated, human-readable history. rather than raw source, verified falsifiable. The S0 baseline and S1 control measurements are pinned under `docs/design/577/`. +- **The S2 Preact treatment arm, measured** (#577) — `docs/design/577/S2-EVIDENCE.md` + plus `docs/design/577/manifest-v2/`. The treatment lives on the unmerged + `eval-577/s2-treatment` branch (tag `577/treatment`): Preact owns shell + presentation and lifecycle, and `src/ui/app-shell.ts`, `ui/left-rail.ts` and + `ui/right-inspector.ts` are **deleted** — the esbuild per-input attribution in + `manifest-v2/s2/esbuild-inputs.json` is the proof they are absent from the + artifact rather than shipped beside the Preact one. Nothing from that branch is + merged; only this evidence is. All three states were re-measured in ONE run + from clean worktrees at their tagged SHAs over one 18-entry superset manifest, + because a treatment measured over a manifest omitting its own new files reports + a deletion with no matching cost. Result: **+330 code lines** and **+7.8 KB + gzip** against the S1 control for the same feature, with domain and island + lines byte-identical across all three states (the preservation constraint + holding). Repo-owned bytes in the artifact went **up** 3 536 B — deleting a + 905-line module did not shrink the shipped application. Exactly one copy of + `@preact/signals-core` is present, proven by per-package attribution rather + than assumed. The evidence document also records nine costs the control does + not pay (asynchronous DOM updates relative to state writes; a `flushSync` + escape hatch wherever a caller reveals a host then acts inside it; a defect + class no gate can see, where importing the signals core instead of the Preact + bindings leaves the shell rendering once and never repainting; and — the + sharpest — that derived state has no *edge*, so a `computed` can report a value + change but never that a gesture ended, which silently cost a pointer drag-fold + its focus rescue), seven genuine improvements, and the refutation of the + evaluation's own flagged weak premise that one owner per subtree deletes the + focus-rescue category. Two real regressions were caught by Chromium/WebKit e2e + with 6 995 unit tests green; two remaining e2e failures were verified to fail + identically on the frozen control and are scored against neither arm. The + recommendation itself is ADR-0004's, and is not made here: the parity suite, + the controlled-change experiment and the JSX sensitivity annex are still + outstanding. + - **The foldable left navigation is reachable in the UI** (#487, phase 3 of 4 — the phase's own most complex wiring step). `app-shell.ts` composes phase 1's pure mode/resize-session core and phase 2's section registry with a new diff --git a/docs/design/577/S2-EVIDENCE.md b/docs/design/577/S2-EVIDENCE.md new file mode 100644 index 00000000..bf951321 --- /dev/null +++ b/docs/design/577/S2-EVIDENCE.md @@ -0,0 +1,219 @@ +# #577 state S2 — the Preact treatment arm, measured + +Evidence only. This document records what was built, what it cost, and what +broke; it does **not** state the recommendation, which ADR-0004 owns once the +parity suite and the controlled-change experiment exist. + +- **Arm:** `eval-577/s2-treatment`, tag `577/treatment`, commit `3f611b5e`. +- **Control:** `eval-577/s1-control`, tag `577/control`, commit `7e7c8c4b`. +- **Baseline:** `eval-577/s0-baseline`, tag `577/baseline`, commit `d3198478`. +- Neither evaluation branch is merged. They are retained as reproducible + evidence under acceptance criterion 7 as amended 2026-07-30. + +## How to reproduce + +All three states were measured in one run, from **clean worktrees at their +tagged SHAs**, over the **same 18-entry manifest** +(`manifest-v2/canonical-manifest.json`). A dirty tree changes the artifact by +exactly 6 bytes (`build/build.mjs`'s `-dirty` stamp), so a measurement from a +working tree is not reproducible from the recorded commit. + +```sh +git worktree add --detach 577/treatment +ln -s /node_modules /node_modules +cd +node build/ui-complexity-report.mjs --manifest --out --label s2 +node build/size-report.mjs --out +``` + +The manifest is a **superset**: the seven `src/ui/shell/*` files report `absent` +for S0 and S1, and `app-shell.ts` / `left-rail.ts` / `right-inspector.ts` report +`absent` for S2. Measuring the treatment over a manifest that omitted its own +new files would report a deletion with no matching cost, so the same file set is +run against every state. + +The original 11-file reports in `s0/` and `s1/` are unchanged and still pinned; +`manifest-v2/` supersedes them only for cross-state comparison. + +## Results + +Code lines are esbuild-normalized — comments stripped, formatting normalized — +because this repo's comment density is institutional review history, not +complexity, and counting raw text credits it as if it were. + +| | S0 baseline | S1 control | S2 treatment | S1 → S2 | +|---|---:|---:|---:|---:| +| code lines — plumbing | 599 | 742 | **1072** | **+330** | +| code lines — domain | 341 | 341 | 341 | **0** | +| code lines — island | 199 | 203 | 203 | **0** | +| **code lines — total** | **1139** | **1286** | **1616** | **+330** | +| artifact raw B | 2 126 521 | 2 130 619 | 2 149 976 | +19 357 | +| artifact gzip B | 623 736 | 624 764 | 632 519 | **+7 755** | +| artifact brotli B | 527 859 | 528 556 | 535 612 | +7 056 | +| repo-owned bytes in bundle | — | 631 633 | 635 169 | **+3 536** | + +**Domain and island lines are identical across all three states.** That is the +preservation constraint holding: the treatment changed no proven non-rendering +logic, and it did not claim a reduction in code that stays imperative under any +render model. + +### What the +7.8 KB gzip is made of + +Per-package attribution from the esbuild metafile: + +| package | S1 | S2 | +|---|---:|---:| +| `preact` | absent | 12 519 B | +| `@preact/signals` | absent | 3 140 B | +| `@preact/signals-core` | 4 713 B | 4 749 B | + +**Exactly one copy of `@preact/signals-core`** — the single-copy requirement is +proven by attribution, not assumed. The Preact bindings re-export the same +primitives over that one copy. + +The repo's **own** bytes in the artifact went **up** by 3 536 B. Deleting a +905-line module did not shrink the shipped application. + +### The deletion is real + +`manifest-v2/s2/esbuild-inputs.json` is the per-input attribution for the S2 +artifact. It contains **no** `app-shell.ts`, `left-rail.ts` or +`right-inspector.ts`, and all six `src/ui/shell/*` modules. The vanilla shell +was deleted, not shipped alongside the Preact one. + +### A measurement correction worth recording + +S1's brotli figure here (528 556) differs from the 528 747 pinned earlier, while +its raw and gzip figures match **exactly**. Identical raw and gzip prove the +artifact bytes are identical; brotli output is not stable across Node versions. +Re-measuring all three states in one run on one Node version is what makes the +brotli column internally comparable — and is why the earlier figure should not +be differenced against this one. + +## What the treatment cost + +Costs the control does not pay, each found while building: + +1. **DOM updates became asynchronous relative to state writes.** Eleven + assertions in `tests/unit/app.test.ts` needed an explicit flush. Not a test + artifact: any code that writes state and then reads the DOM is affected. +2. **`showHost` needed a synchronous escape hatch.** Callers reveal a host and + then act inside it — `app.showQuerySurface()` immediately focuses the SQL + editor — and a deferred render left the editor unfocusable because its host + was still `hidden`. Fixed with a `flushSync` built on + `options.debounceRendering`. Every future boundary of that shape must + remember it. +3. **A defect class with no gate coverage.** Importing `@preact/signals-core` + instead of `@preact/signals` leaves the reactivity bridge unwired: the shell + renders once and never repaints. `tsc`, `check-boundaries` and every + pure-signal test stayed green. Only rendering a real component caught it. The + imperative arm cannot have this failure mode — there is no bridge. +4. **Derived state has no EDGE.** A `computed` reports that a *value* changed; it + cannot report that a *gesture ended*. `navMode` reaches its final value + mid-drag, while focus capture is correctly gated off — so the commit produced + no dependency change and a pointer drag-fold silently rescued no focus. The + commit had to be re-attached by hand. The control has no equivalent hazard: + its rescue lives in the paint function every trigger calls. +5. **`drawer.ts`'s `attachDrawerResize` is not reusable** — it appends its handle + into the panel, which would put two owners on one subtree. The inspector + re-implements the drag. The control's reuse of that primitive was part of why + it stayed small. +6. **Foreign DOM cannot carry a Preact prop.** A `.ri-tool-slot` wrapper plus a + CSS rule per preserved-but-hidden child, and the two vanilla-repainted + switcher rows keep their `hidden` writes as an effect. +7. **Read placement became a performance-correctness rule.** Over 200 simulated + drag frames: the layout signal read by a leaf → root re-runs **1×**; the same + read moved to the root → root and rail re-run **201×**. +8. **`widthRevision`.** `sidebarPx`/`leftNavDrawerPx` are plain fields, so a + width-only commit changes nothing a `computed` observes. Kept inside a + measured file rather than `state.ts` so the cost is counted. +9. **Stale cross-references.** Ten-plus `domain` and `island` files still name + `app-shell.ts` in comments. They are deliberately not edited — a diff in a + domain file is a finding against the preservation constraint — so the arm + ships with dangling prose it is not allowed to fix. + +## What the treatment genuinely improved + +- `applyEffectiveLeftNavigationLayout` (~100 lines) and its four call sites are + gone; presentation is an expression of one `computed`. +- The rail's four per-button `effect()`s and its whole `dispose()` are gone. +- The inspector's `renderActive()` is gone — "exactly one tool exposed" is + structural. +- The `isSessionActive()` guard every non-gesture trigger had to remember + becomes the shape of the data (`dragLayout` non-null). +- The `untracked()` workaround (12 lines of comment in the control) is + unnecessary: a derivation with no side effects cannot widen its dependencies. +- Focus **capture** collapses from four inline sites to one effect plus the + commit re-attachment. +- One defensive guard becomes structurally **unreachable**: a focused drawer is + derived from a non-null section, so mode and section cannot disagree. The + control needed a real check because its mode lived in a DOM attribute. +- The inspector's mobile projection is genuinely reactive. The control samples + `isMobile` once at mount and is therefore **stale** after a breakpoint + crossing — a latent control defect, left unfixed because the control is frozen, + and explicitly **not** counted as a treatment line saving. + +## The weak premise, tested + +> "One owner per subtree deletes the focus-rescue category." + +**Refuted.** Preact offers no before-the-DOM-changes hook — `useLayoutEffect` +cleanups run after the diff — so capture cannot happen at paint time at all. The +work survives in full; only its location moves, and finding 4 above shows the +relocation introduced a failure the control does not have. + +## Deviation from the plan + +The ship log's next-step wording said to delete "the geometry half of +`left-nav-separator.ts`". **Not done, deliberately.** The canonical manifest +classifies that file as an **island** because "only its thin ARIA/paint glue is +plumbing, and that is not separable at file granularity", and hard rule 5 keeps +high-frequency pointer surfaces imperative. Splitting it would have handed the +treatment a line reduction it did not earn and put two writers on the same ARIA +attributes. The island's 203 lines are identical in S1 and S2, which is the +check that this deviation cost the treatment nothing it was owed. + +## Browser evidence + +Chromium and WebKit, full suite: **417 passed**. Two `tile-open-workbench` +specs (`:327`, `:453`) fail on both engines — and fail **identically on +`eval-577/s1-control` from a clean worktree**, so they belong to neither arm and +are not scored. Checking that mattered: an earlier S2 run failed a *different* +subset of the same file, so it is partly order-dependent, and reading either run +as a verdict without the baseline would have been wrong in both directions. + +Two genuine regressions were found by real-browser e2e **with 6 995 unit tests +green**, and both are fixed: + +- a live width measurement silently became push-based, so a viewport-clamped + 420px preference rendered at its full 420 instead of 313; +- an intent-gated focus restore did nothing, because a pointer drag drops the + focused descendant to `` on an intermediate `mousemove` frame, long + before any commit-time code runs. + +This is direct support for #577's insistence that the comparison be exercised in +real browsers rather than in happy-dom alone. + +## Still outstanding before ADR-0004 + +- the arm-agnostic parity suite (normalized subtree serialization, parametrized + over both states, pinned assertion count); +- the controlled-change experiment; +- the whole-slice JSX sensitivity annex. + +## Reading against the precommitted rule + +The rule was fixed **before** any measurement and is reproduced verbatim in the +ship log. It requires, for adoption, clear dominance on **both** (a) net +repo-owned production shell code after counting adapters, and (b) lifecycle and +focus ownership obligations *and* controlled change amplification — and states +that **any** mixed result is a RETAIN recommendation. + +On (a) the treatment is **+330 code lines (+26% over the control)** and **+3 536 +repo-owned bytes** in the artifact. That is not a reduction, so (a) fails +outright rather than mixing. (b) is not yet complete — the controlled-change +experiment has not run — but it cannot rescue (a) under a rule that requires +dominance on both. + +ADR-0004 records the recommendation. This document records only the numbers and +what produced them. diff --git a/docs/design/577/manifest-v2/canonical-manifest.json b/docs/design/577/manifest-v2/canonical-manifest.json new file mode 100644 index 00000000..5bda2270 --- /dev/null +++ b/docs/design/577/manifest-v2/canonical-manifest.json @@ -0,0 +1,140 @@ +{ + "_comment": [ + "The auditable file set for #577's UI-complexity measurement. Acceptance", + "criterion 4 requires the report to distinguish product-specific domain logic", + "from framework-like rendering plumbing, so THIS FILE is the artifact that", + "claim rests on — a wrong `class` here is a wrong decision, not a wrong", + "number. Every file the report names comes from here and nowhere else.", + "", + "Classes:", + " domain — preserved verbatim by both the vanilla control and the Preact", + " treatment. Any diff in these files during the evaluation is a", + " finding against #577's own 'preserve proven non-rendering", + " logic' constraint, not a licence.", + " plumbing — genuinely replaceable by a component model. This is the only", + " class the treatment may claim a reduction in.", + " island — stays imperative under ANY render model (CLAUDE.md hard rule 5:", + " signals coordinate state, they do not own every mousemove).", + " Counted so the report cannot present island code as a Preact", + " win or a Preact cost.", + "", + "The `sliceItem` field maps each file to the numbered item in #577's", + "'Evaluation scope' list it covers, so a reader can check the slice is", + "actually covered rather than taking the report's word for it.", + "", + "Classification rationale for the three non-obvious calls (a first pass at", + "this plan mislabelled all three, which would have overstated the", + "replaceable baseline by roughly 1000 physical lines):", + " - left-nav-separator.ts is an ISLAND, not plumbing: it is pointer/keyboard", + " mechanics plus resize-session bookkeeping. Only its thin ARIA/paint glue", + " is plumbing, and that is not separable at file granularity.", + " - nav-sections.ts and sidebar-upper.ts are DOMAIN shared verbatim: they are", + " NAV_SECTION_META plus persistent-host construction that BOTH arms need.", + " Only a ~10-line showSection is replaceable.", + "", + "#577 state S2 extends this manifest with the seven src/ui/shell/* files the", + "Preact treatment adds. The manifest is a SUPERSET measured identically against", + "every state: the new entries report `absent` for S0 and S1, and app-shell.ts /", + "left-rail.ts / right-inspector.ts report `absent` for S2. That is the only way", + "the three states share one file set — a treatment measured over a manifest that", + "omitted its own new files would be reporting a deletion with no matching cost.", + "S0 and S1 were RE-MEASURED over this extended manifest from their own tags; the", + "original 11-file reports remain pinned beside them unchanged.", + "", + "shell-context.types.ts is type-only: esbuild erases it entirely, so it measures", + "0 code lines. It is listed anyway rather than quietly excluded — a reader can", + "see it costs nothing, which is a claim, not an omission." + ], + "files": [ + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching" + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation" + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations" + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher" + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics" + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free" + }, + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam" + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host" + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes" + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)" + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector" + }, + { + "path": "src/ui/shell/shell-host.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — the composition root: shell geometry below the header, desktop/mobile projection, Query/Dashboard host switching (replaces app-shell.ts)" + }, + { + "path": "src/ui/shell/shell-view.ts", + "class": "plumbing", + "sliceItem": "1, 2, 5, 6 — the rendered application frame, the left navigation in wide/rail/drawer presentations, and the mobile bottom nav (replaces app-shell.ts markup + left-rail.ts)" + }, + { + "path": "src/ui/shell/shell-layout.ts", + "class": "plumbing", + "sliceItem": "2, 3 — the left-navigation layout as derived state (replaces applyEffectiveLeftNavigationLayout)" + }, + { + "path": "src/ui/shell/focus-settlement.ts", + "class": "plumbing", + "sliceItem": "3 — capture/settle across a structural transition where the focused source disappears" + }, + { + "path": "src/ui/shell/right-inspector-view.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host (replaces right-inspector.ts)" + }, + { + "path": "src/ui/shell/adopt.ts", + "class": "plumbing", + "sliceItem": "7 — the Preact/foreign-DOM boundary every imperative integration point crosses" + }, + { + "path": "src/ui/shell/shell-context.types.ts", + "class": "plumbing", + "sliceItem": "1 — type-only seam contract for the view; erased by the build" + } + ] +} diff --git a/docs/design/577/manifest-v2/s0/bundle-size-report.json b/docs/design/577/manifest-v2/s0/bundle-size-report.json new file mode 100644 index 00000000..6e352232 --- /dev/null +++ b/docs/design/577/manifest-v2/s0/bundle-size-report.json @@ -0,0 +1,386 @@ +{ + "schemaVersion": 1, + "artifact": { + "raw": 2126521, + "gzip": 623736, + "brotli": 527859 + }, + "js": { + "raw": 1868626, + "gzip": 508303, + "brotli": 419075 + }, + "css": { + "raw": 237839, + "gzip": 109628, + "brotli": 105834 + }, + "totalOutputBytes": 1868430, + "entryPoints": [ + { + "file": "main.js", + "entryPoint": "src/main.ts", + "bytes": 1868623 + } + ], + "ownership": { + "project": { + "bytes": 629424, + "pct": 33.687320370578504 + }, + "generated": { + "bytes": 457367, + "pct": 24.47867996125089 + }, + "external": { + "bytes": 781639, + "pct": 41.8339996681706 + }, + "other": { + "bytes": 0, + "pct": 0 + } + }, + "packages": [ + { + "name": "chart.js", + "bytes": 197174, + "pct": 10.55292411275777 + }, + { + "name": "@codemirror/view", + "bytes": 186268, + "pct": 9.969225499483523 + }, + { + "name": "date-fns", + "bytes": 47998, + "pct": 2.5688947405040596 + }, + { + "name": "@codemirror/state", + "bytes": 46747, + "pct": 2.5019401315542993 + }, + { + "name": "marked", + "bytes": 41843, + "pct": 2.2394737828016034 + }, + { + "name": "@dagrejs/dagre", + "bytes": 40422, + "pct": 2.163420625873059 + }, + { + "name": "@codemirror/lang-sql", + "bytes": 32138, + "pct": 1.720053734953945 + }, + { + "name": "@codemirror/autocomplete", + "bytes": 31522, + "pct": 1.6870848787484678 + }, + { + "name": "@lezer/lr", + "bytes": 26552, + "pct": 1.421086152545185 + }, + { + "name": "@codemirror/language", + "bytes": 24486, + "pct": 1.3105120341677237 + }, + { + "name": "@codemirror/commands", + "bytes": 23371, + "pct": 1.250836263600991 + }, + { + "name": "@lezer/common", + "bytes": 20388, + "pct": 1.091183507008558 + }, + { + "name": "@codemirror/search", + "bytes": 18472, + "pct": 0.9886375192006123 + }, + { + "name": "@lezer/xml", + "bytes": 8569, + "pct": 0.45862033900119353 + }, + { + "name": "@kurkle/color", + "bytes": 7597, + "pct": 0.40659805291073253 + }, + { + "name": "@lezer/highlight", + "bytes": 7178, + "pct": 0.38417280818655236 + }, + { + "name": "@codemirror/lang-xml", + "bytes": 5975, + "pct": 0.31978720101903735 + }, + { + "name": "@preact/signals-core", + "bytes": 4713, + "pct": 0.25224386249417957 + }, + { + "name": "@marijn/find-cluster-break", + "bytes": 2332, + "pct": 0.1248106699207356 + }, + { + "name": "style-mod", + "bytes": 2202, + "pct": 0.11785295676048875 + }, + { + "name": "chartjs-adapter-date-fns", + "bytes": 1688, + "pct": 0.09034322934228202 + }, + { + "name": "@lezer/json", + "bytes": 1599, + "pct": 0.08557987187103612 + }, + { + "name": "w3c-keyname", + "bytes": 1520, + "pct": 0.08135172310442457 + }, + { + "name": "crelt", + "bytes": 611, + "pct": 0.03270125185316014 + }, + { + "name": "@codemirror/lang-json", + "bytes": 274, + "pct": 0.014664718506981797 + } + ], + "topModules": [ + { + "path": "src/generated/json-schema-validators.js", + "bytes": 214595, + "pct": 11.485311197101309, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "src/generated/example-dashboards.ts", + "bytes": 187686, + "pct": 10.045118093800678, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/view/dist/index.js", + "bytes": 186268, + "pct": 9.969225499483523, + "owner": "external", + "group": "@codemirror/view" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chart.js", + "bytes": 165504, + "pct": 8.857918145180713, + "owner": "external", + "group": "chart.js" + }, + { + "path": "src/generated/json-schemas.js", + "bytes": 55086, + "pct": 2.9482506703489024, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/state/dist/index.js", + "bytes": 46747, + "pct": 2.5019401315542993, + "owner": "external", + "group": "@codemirror/state" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/marked/lib/marked.esm.js", + "bytes": 41843, + "pct": 2.2394737828016034, + "owner": "external", + "group": "marked" + }, + { + "path": "src/ui/app.ts", + "bytes": 41314, + "pct": 2.211161242326445, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@dagrejs/dagre/dist/dagre.esm.js", + "bytes": 40422, + "pct": 2.163420625873059, + "owner": "external", + "group": "@dagrejs/dagre" + }, + { + "path": "src/ui/dashboard.ts", + "bytes": 35268, + "pct": 1.8875740595045039, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/lang-sql/dist/index.js", + "bytes": 32138, + "pct": 1.720053734953945, + "owner": "external", + "group": "@codemirror/lang-sql" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chunks/helpers.dataset.js", + "bytes": 31641, + "pct": 1.693453862333617, + "owner": "external", + "group": "chart.js" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/autocomplete/dist/index.js", + "bytes": 31522, + "pct": 1.6870848787484678, + "owner": "external", + "group": "@codemirror/autocomplete" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/lr/dist/index.js", + "bytes": 26552, + "pct": 1.421086152545185, + "owner": "external", + "group": "@lezer/lr" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/language/dist/index.js", + "bytes": 24486, + "pct": 1.3105120341677237, + "owner": "external", + "group": "@codemirror/language" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/commands/dist/index.js", + "bytes": 23371, + "pct": 1.250836263600991, + "owner": "external", + "group": "@codemirror/commands" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/common/dist/index.js", + "bytes": 20388, + "pct": 1.091183507008558, + "owner": "external", + "group": "@lezer/common" + }, + { + "path": "src/ui/results.ts", + "bytes": 18828, + "pct": 1.0076909490855959, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/search/dist/index.js", + "bytes": 18472, + "pct": 0.9886375192006123, + "owner": "external", + "group": "@codemirror/search" + }, + { + "path": "src/dashboard/application/dashboard-viewer-session.ts", + "bytes": 15288, + "pct": 0.8182270676450282, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/dashboard-tree.ts", + "bytes": 13899, + "pct": 0.7438865785713139, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/file-menu.ts", + "bytes": 12919, + "pct": 0.6914361255171454, + "owner": "project", + "group": "src" + }, + { + "path": "src/core/chart-data.ts", + "bytes": 12028, + "pct": 0.6437490299342229, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/explain-graph.ts", + "bytes": 11980, + "pct": 0.6411800281519778, + "owner": "project", + "group": "src" + }, + { + "path": "src/state.ts", + "bytes": 10933, + "pct": 0.585143676776759, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/shortcuts.ts", + "bytes": 10307, + "pct": 0.5516396118666475, + "owner": "project", + "group": "src" + }, + { + "path": "src/net/ch-client.ts", + "bytes": 9860, + "pct": 0.5277157827694909, + "owner": "project", + "group": "src" + }, + { + "path": "src/dashboard/model/workspace-semantics.ts", + "bytes": 8712, + "pct": 0.46627382347746504, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/xml/dist/index.js", + "bytes": 8569, + "pct": 0.45862033900119353, + "owner": "external", + "group": "@lezer/xml" + }, + { + "path": "src/core/spec-schema.ts", + "bytes": 8272, + "pct": 0.44272464047355264, + "owner": "project", + "group": "src" + } + ], + "notes": [ + "Percentages are of raw contributed output bytes (metafile bytesInOutput); gzip/Brotli are measured per whole artifact only — compression is not additive across modules." + ] +} \ No newline at end of file diff --git a/docs/design/577/manifest-v2/s0/ui-complexity-report.json b/docs/design/577/manifest-v2/s0/ui-complexity-report.json new file mode 100644 index 00000000..474511ce --- /dev/null +++ b/docs/design/577/manifest-v2/s0/ui-complexity-report.json @@ -0,0 +1,344 @@ +{ + "schemaVersion": 1, + "label": "s0", + "metricTiers": { + "deciding": { + "ownedProductionCode": { + "owner": "this instrument" + }, + "parity": { + "owner": "tests/e2e/shell-parity.spec.js" + }, + "artifact": { + "owner": "build/size-report.mjs" + }, + "changeAmplification": { + "owner": "frozen controlled-change experiment" + }, + "cleanupObligations": { + "owner": "leak invariant + manual obligation count" + } + }, + "explanatory": { + "minifiedBytes": { + "demotedBecause": "ignores tree shaking, shared helpers, and complexity moved into a dependency" + }, + "lcovBranches": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lcovFunctions": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lifecycleSites": { + "demotedBecause": "hidden/dataset/replaceChildren go syntactically invisible under a vDOM" + } + } + }, + "environment": { + "commit": "d319847875b3b3eeff2c4ca4c43fd7dda12f8b40", + "commitDirty": "clean", + "node": "v25.9.0", + "esbuild": "0.28.1", + "platform": "darwin-arm64", + "packageLock": "sha256:9042ec0421903e38" + }, + "ownedProductionCode": { + "plumbingNormalizedLines": 599, + "islandNormalizedLines": 199, + "domainNormalizedLines": 341 + }, + "byClass": { + "domain": { + "files": 4, + "absentFiles": 0, + "physicalLines": 1367, + "normalizedLines": 341, + "nonCodeLines": 953, + "minifiedBytes": 8984, + "lifecycle": { + "domMutation": 10, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 4 + }, + "plumbing": { + "files": 4, + "absentFiles": 8, + "physicalLines": 1470, + "normalizedLines": 599, + "nonCodeLines": 817, + "minifiedBytes": 15143, + "lifecycle": { + "domMutation": 60, + "listener": 7, + "effect": 17, + "disposal": 34, + "focus": 7 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 4 + }, + "island": { + "files": 2, + "absentFiles": 0, + "physicalLines": 590, + "normalizedLines": 199, + "nonCodeLines": 350, + "minifiedBytes": 4156, + "lifecycle": { + "domMutation": 14, + "listener": 18, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 2 + } + }, + "overall": { + "files": 10, + "absentFiles": 8, + "physicalLines": 3427, + "normalizedLines": 1139, + "nonCodeLines": 2120, + "minifiedBytes": 28283, + "lifecycle": { + "domMutation": 84, + "listener": 25, + "effect": 21, + "disposal": 38, + "focus": 7 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 10 + }, + "files": [ + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam", + "physicalLines": 175, + "sourceBlankLines": 8, + "normalizedLines": 48, + "minifiedBytes": 951, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free", + "physicalLines": 853, + "sourceBlankLines": 39, + "normalizedLines": 150, + "minifiedBytes": 4836, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching", + "physicalLines": 880, + "sourceBlankLines": 30, + "normalizedLines": 328, + "minifiedBytes": 7603, + "lifecycle": { + "domMutation": 39, + "listener": 3, + "effect": 14, + "disposal": 22, + "focus": 4 + }, + "lcov": null + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes", + "physicalLines": 144, + "sourceBlankLines": 7, + "normalizedLines": 56, + "minifiedBytes": 993, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics", + "physicalLines": 443, + "sourceBlankLines": 29, + "normalizedLines": 157, + "minifiedBytes": 3170, + "lifecycle": { + "domMutation": 12, + "listener": 14, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation", + "physicalLines": 129, + "sourceBlankLines": 8, + "normalizedLines": 51, + "minifiedBytes": 775, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 1, + "disposal": 6, + "focus": 3 + }, + "lcov": null + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations", + "physicalLines": 202, + "sourceBlankLines": 13, + "normalizedLines": 69, + "minifiedBytes": 1464, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)", + "absent": true + }, + { + "path": "src/ui/shell/adopt.ts", + "class": "plumbing", + "sliceItem": "7 — the Preact/foreign-DOM boundary every imperative integration point crosses", + "absent": true + }, + { + "path": "src/ui/shell/focus-settlement.ts", + "class": "plumbing", + "sliceItem": "3 — capture/settle across a structural transition where the focused source disappears", + "absent": true + }, + { + "path": "src/ui/shell/right-inspector-view.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host (replaces right-inspector.ts)", + "absent": true + }, + { + "path": "src/ui/shell/shell-context.types.ts", + "class": "plumbing", + "sliceItem": "1 — type-only seam contract for the view; erased by the build", + "absent": true + }, + { + "path": "src/ui/shell/shell-host.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — the composition root: shell geometry below the header, desktop/mobile projection, Query/Dashboard host switching (replaces app-shell.ts)", + "absent": true + }, + { + "path": "src/ui/shell/shell-layout.ts", + "class": "plumbing", + "sliceItem": "2, 3 — the left-navigation layout as derived state (replaces applyEffectiveLeftNavigationLayout)", + "absent": true + }, + { + "path": "src/ui/shell/shell-view.ts", + "class": "plumbing", + "sliceItem": "1, 2, 5, 6 — the rendered application frame, the left navigation in wide/rail/drawer presentations, and the mobile bottom nav (replaces app-shell.ts markup + left-rail.ts)", + "absent": true + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher", + "physicalLines": 137, + "sourceBlankLines": 13, + "normalizedLines": 74, + "minifiedBytes": 1733, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector", + "physicalLines": 147, + "sourceBlankLines": 12, + "normalizedLines": 42, + "minifiedBytes": 986, + "lifecycle": { + "domMutation": 2, + "listener": 4, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host", + "physicalLines": 317, + "sourceBlankLines": 9, + "normalizedLines": 164, + "minifiedBytes": 5772, + "lifecycle": { + "domMutation": 17, + "listener": 4, + "effect": 2, + "disposal": 6, + "focus": 0 + }, + "lcov": null + } + ] +} diff --git a/docs/design/577/manifest-v2/s0/ui-complexity-report.md b/docs/design/577/manifest-v2/s0/ui-complexity-report.md new file mode 100644 index 00000000..056aa73a --- /dev/null +++ b/docs/design/577/manifest-v2/s0/ui-complexity-report.md @@ -0,0 +1,61 @@ +# UI complexity report — s0 + +Schema version 1. Counts run over esbuild-transformed +source (comments stripped, formatting normalized), never raw text. + +**Deciding metric owned here:** `ownedProductionCode` — normalized lines by class. +Every other number below is explanatory and must not be voted with: +- `minifiedBytes` — ignores tree shaking, shared helpers, and complexity moved into a dependency +- `lcovBranches` — falls when mechanisms move into Preact — externalized, not eliminated +- `lcovFunctions` — falls when mechanisms move into Preact — externalized, not eliminated +- `lifecycleSites` — hidden/dataset/replaceChildren go syntactically invisible under a vDOM + +## Owned production code (deciding) + +| Class | Files | Normalized (code) | Physical | Non-code | +|---|---:|---:|---:|---:| +| domain | 4 | **341** | 1367 | 953 | +| plumbing | 4 | **599** | 1470 | 817 | +| island | 2 | **199** | 590 | 350 | +| **total** | **10** | **1139** | **3427** | **2120** | + +Physical LOC overstates code by **2120** non-code lines +(comments plus formatting-only lines). A raw-LOC comparison would have credited +those as complexity — which is the specific error this instrument exists to prevent. + +## Explanatory metrics + +| File | Class | Code | Minified B | DOM | Listeners | Effects | Disposal | focus() | lcov BRF/FNF | +|---|---|---:|---:|---:|---:|---:|---:|---:|---| +| `src/application/left-nav.ts` | domain | 48 | 951 | 4 | 0 | 2 | 0 | 0 | _unmatched_ | +| `src/core/left-nav-layout.ts` | domain | 150 | 4836 | 0 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/app-shell.ts` | plumbing | 328 | 7603 | 39 | 3 | 14 | 22 | 4 | _unmatched_ | +| `src/ui/drawer.ts` | plumbing | 56 | 993 | 2 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/left-nav-separator.ts` | island | 157 | 3170 | 12 | 14 | 2 | 4 | 0 | _unmatched_ | +| `src/ui/left-rail.ts` | plumbing | 51 | 775 | 2 | 0 | 1 | 6 | 3 | _unmatched_ | +| `src/ui/nav-sections.ts` | domain | 69 | 1464 | 2 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/right-inspector.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/adopt.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/focus-settlement.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/right-inspector-view.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-context.types.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-host.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-layout.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-view.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/sidebar-upper.ts` | domain | 74 | 1733 | 4 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/splitters.ts` | island | 42 | 986 | 2 | 4 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/workbench/workbench-shell.ts` | plumbing | 164 | 5772 | 17 | 4 | 2 | 6 | 0 | _unmatched_ | + +> 8 manifest file(s) absent in this state — measured against the one canonical +> manifest so every state covers an identical file set (a deletion must show up, not vanish). + +> 10 file(s) had no lcov record — reported as unmatched, never as zero branches. + +## Environment + +- **commit**: `d319847875b3b3eeff2c4ca4c43fd7dda12f8b40` +- **commitDirty**: `clean` +- **node**: `v25.9.0` +- **esbuild**: `0.28.1` +- **platform**: `darwin-arm64` +- **packageLock**: `sha256:9042ec0421903e38` diff --git a/docs/design/577/manifest-v2/s1/bundle-size-report.json b/docs/design/577/manifest-v2/s1/bundle-size-report.json new file mode 100644 index 00000000..03824d72 --- /dev/null +++ b/docs/design/577/manifest-v2/s1/bundle-size-report.json @@ -0,0 +1,386 @@ +{ + "schemaVersion": 1, + "artifact": { + "raw": 2130619, + "gzip": 624764, + "brotli": 528556 + }, + "js": { + "raw": 1870835, + "gzip": 509125, + "brotli": 420019 + }, + "css": { + "raw": 239728, + "gzip": 109836, + "brotli": 105960 + }, + "totalOutputBytes": 1870639, + "entryPoints": [ + { + "file": "main.js", + "entryPoint": "src/main.ts", + "bytes": 1870832 + } + ], + "ownership": { + "project": { + "bytes": 631633, + "pct": 33.7656276812362 + }, + "generated": { + "bytes": 457367, + "pct": 24.449773580044038 + }, + "external": { + "bytes": 781639, + "pct": 41.784598738719765 + }, + "other": { + "bytes": 0, + "pct": 0 + } + }, + "packages": [ + { + "name": "chart.js", + "bytes": 197174, + "pct": 10.540462376760026 + }, + { + "name": "@codemirror/view", + "bytes": 186268, + "pct": 9.957453041447334 + }, + { + "name": "date-fns", + "bytes": 47998, + "pct": 2.5658611843332677 + }, + { + "name": "@codemirror/state", + "bytes": 46747, + "pct": 2.498985640735599 + }, + { + "name": "marked", + "bytes": 41843, + "pct": 2.236829233219237 + }, + { + "name": "@dagrejs/dagre", + "bytes": 40422, + "pct": 2.160865885935234 + }, + { + "name": "@codemirror/lang-sql", + "bytes": 32138, + "pct": 1.718022558067056 + }, + { + "name": "@codemirror/autocomplete", + "bytes": 31522, + "pct": 1.6850926341212817 + }, + { + "name": "@lezer/lr", + "bytes": 26552, + "pct": 1.419408020467872 + }, + { + "name": "@codemirror/language", + "bytes": 24486, + "pct": 1.3089644768445434 + }, + { + "name": "@codemirror/commands", + "bytes": 23371, + "pct": 1.2493591761959415 + }, + { + "name": "@lezer/common", + "bytes": 20388, + "pct": 1.089894950335153 + }, + { + "name": "@codemirror/search", + "bytes": 18472, + "pct": 0.9874700570232953 + }, + { + "name": "@lezer/xml", + "bytes": 8569, + "pct": 0.4580787634599728 + }, + { + "name": "@kurkle/color", + "bytes": 7597, + "pct": 0.40611790944164 + }, + { + "name": "@lezer/highlight", + "bytes": 7178, + "pct": 0.38371914623826403 + }, + { + "name": "@codemirror/lang-xml", + "bytes": 5975, + "pct": 0.3194095707402657 + }, + { + "name": "@preact/signals-core", + "bytes": 4713, + "pct": 0.25194599278642216 + }, + { + "name": "@marijn/find-cluster-break", + "bytes": 2332, + "pct": 0.12466328350900416 + }, + { + "name": "style-mod", + "bytes": 2202, + "pct": 0.11771378657239584 + }, + { + "name": "chartjs-adapter-date-fns", + "bytes": 1688, + "pct": 0.09023654483842151 + }, + { + "name": "@lezer/json", + "bytes": 1599, + "pct": 0.085478812320282 + }, + { + "name": "w3c-keyname", + "bytes": 1520, + "pct": 0.08125565648957389 + }, + { + "name": "crelt", + "bytes": 611, + "pct": 0.032662635602058974 + }, + { + "name": "@codemirror/lang-json", + "bytes": 274, + "pct": 0.014647401235620555 + } + ], + "topModules": [ + { + "path": "src/generated/json-schema-validators.js", + "bytes": 214595, + "pct": 11.471748423934281, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "src/generated/example-dashboards.ts", + "bytes": 187686, + "pct": 10.033256015725108, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/view/dist/index.js", + "bytes": 186268, + "pct": 9.957453041447334, + "owner": "external", + "group": "@codemirror/view" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chart.js", + "bytes": 165504, + "pct": 8.84745800766476, + "owner": "external", + "group": "chart.js" + }, + { + "path": "src/generated/json-schemas.js", + "bytes": 55086, + "pct": 2.944769140384649, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/state/dist/index.js", + "bytes": 46747, + "pct": 2.498985640735599, + "owner": "external", + "group": "@codemirror/state" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/marked/lib/marked.esm.js", + "bytes": 41843, + "pct": 2.236829233219237, + "owner": "external", + "group": "marked" + }, + { + "path": "src/ui/app.ts", + "bytes": 41314, + "pct": 2.2085501264541154, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@dagrejs/dagre/dist/dagre.esm.js", + "bytes": 40422, + "pct": 2.160865885935234, + "owner": "external", + "group": "@dagrejs/dagre" + }, + { + "path": "src/ui/dashboard.ts", + "bytes": 35268, + "pct": 1.8853450612330866, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/lang-sql/dist/index.js", + "bytes": 32138, + "pct": 1.718022558067056, + "owner": "external", + "group": "@codemirror/lang-sql" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chunks/helpers.dataset.js", + "bytes": 31641, + "pct": 1.6914540967017153, + "owner": "external", + "group": "chart.js" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/autocomplete/dist/index.js", + "bytes": 31522, + "pct": 1.6850926341212817, + "owner": "external", + "group": "@codemirror/autocomplete" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/lr/dist/index.js", + "bytes": 26552, + "pct": 1.419408020467872, + "owner": "external", + "group": "@lezer/lr" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/language/dist/index.js", + "bytes": 24486, + "pct": 1.3089644768445434, + "owner": "external", + "group": "@codemirror/language" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/commands/dist/index.js", + "bytes": 23371, + "pct": 1.2493591761959415, + "owner": "external", + "group": "@codemirror/commands" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/common/dist/index.js", + "bytes": 20388, + "pct": 1.089894950335153, + "owner": "external", + "group": "@lezer/common" + }, + { + "path": "src/ui/results.ts", + "bytes": 18828, + "pct": 1.0065009870958534, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/search/dist/index.js", + "bytes": 18472, + "pct": 0.9874700570232953, + "owner": "external", + "group": "@codemirror/search" + }, + { + "path": "src/dashboard/application/dashboard-viewer-session.ts", + "bytes": 15288, + "pct": 0.8172608397451353, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/dashboard-tree.ts", + "bytes": 13899, + "pct": 0.7430081378609127, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/file-menu.ts", + "bytes": 12919, + "pct": 0.6906196224926349, + "owner": "project", + "group": "src" + }, + { + "path": "src/core/chart-data.ts", + "bytes": 12028, + "pct": 0.6429888396424965, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/explain-graph.ts", + "bytes": 11980, + "pct": 0.6404228715428257, + "owner": "project", + "group": "src" + }, + { + "path": "src/state.ts", + "bytes": 11032, + "pct": 0.5897450015743284, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/shortcuts.ts", + "bytes": 10307, + "pct": 0.5509881917355514, + "owner": "project", + "group": "src" + }, + { + "path": "src/net/ch-client.ts", + "bytes": 9860, + "pct": 0.5270926138073675, + "owner": "project", + "group": "src" + }, + { + "path": "src/dashboard/model/workspace-semantics.ts", + "bytes": 8712, + "pct": 0.4657232100902419, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/xml/dist/index.js", + "bytes": 8569, + "pct": 0.4580787634599728, + "owner": "external", + "group": "@lezer/xml" + }, + { + "path": "src/core/spec-schema.ts", + "bytes": 8272, + "pct": 0.44220183584325995, + "owner": "project", + "group": "src" + } + ], + "notes": [ + "Percentages are of raw contributed output bytes (metafile bytesInOutput); gzip/Brotli are measured per whole artifact only — compression is not additive across modules." + ] +} \ No newline at end of file diff --git a/docs/design/577/manifest-v2/s1/ui-complexity-report.json b/docs/design/577/manifest-v2/s1/ui-complexity-report.json new file mode 100644 index 00000000..70925a86 --- /dev/null +++ b/docs/design/577/manifest-v2/s1/ui-complexity-report.json @@ -0,0 +1,355 @@ +{ + "schemaVersion": 1, + "label": "s1", + "metricTiers": { + "deciding": { + "ownedProductionCode": { + "owner": "this instrument" + }, + "parity": { + "owner": "tests/e2e/shell-parity.spec.js" + }, + "artifact": { + "owner": "build/size-report.mjs" + }, + "changeAmplification": { + "owner": "frozen controlled-change experiment" + }, + "cleanupObligations": { + "owner": "leak invariant + manual obligation count" + } + }, + "explanatory": { + "minifiedBytes": { + "demotedBecause": "ignores tree shaking, shared helpers, and complexity moved into a dependency" + }, + "lcovBranches": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lcovFunctions": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lifecycleSites": { + "demotedBecause": "hidden/dataset/replaceChildren go syntactically invisible under a vDOM" + } + } + }, + "environment": { + "commit": "7e7c8c4b6d8c5853d4e59dc599c5705f8e9cacab", + "commitDirty": "clean", + "node": "v25.9.0", + "esbuild": "0.28.1", + "platform": "darwin-arm64", + "packageLock": "sha256:9042ec0421903e38" + }, + "ownedProductionCode": { + "plumbingNormalizedLines": 742, + "islandNormalizedLines": 203, + "domainNormalizedLines": 341 + }, + "byClass": { + "domain": { + "files": 4, + "absentFiles": 0, + "physicalLines": 1367, + "normalizedLines": 341, + "nonCodeLines": 953, + "minifiedBytes": 8984, + "lifecycle": { + "domMutation": 10, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 4 + }, + "plumbing": { + "files": 5, + "absentFiles": 7, + "physicalLines": 1766, + "normalizedLines": 742, + "nonCodeLines": 949, + "minifiedBytes": 17444, + "lifecycle": { + "domMutation": 72, + "listener": 7, + "effect": 17, + "disposal": 36, + "focus": 8 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 5 + }, + "island": { + "files": 2, + "absentFiles": 0, + "physicalLines": 603, + "normalizedLines": 203, + "nonCodeLines": 359, + "minifiedBytes": 4269, + "lifecycle": { + "domMutation": 14, + "listener": 18, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 2 + } + }, + "overall": { + "files": 11, + "absentFiles": 7, + "physicalLines": 3736, + "normalizedLines": 1286, + "nonCodeLines": 2261, + "minifiedBytes": 30697, + "lifecycle": { + "domMutation": 96, + "listener": 25, + "effect": 21, + "disposal": 40, + "focus": 8 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 11 + }, + "files": [ + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam", + "physicalLines": 175, + "sourceBlankLines": 8, + "normalizedLines": 48, + "minifiedBytes": 951, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free", + "physicalLines": 853, + "sourceBlankLines": 39, + "normalizedLines": 150, + "minifiedBytes": 4836, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching", + "physicalLines": 905, + "sourceBlankLines": 31, + "normalizedLines": 340, + "minifiedBytes": 7856, + "lifecycle": { + "domMutation": 41, + "listener": 3, + "effect": 14, + "disposal": 23, + "focus": 4 + }, + "lcov": null + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes", + "physicalLines": 144, + "sourceBlankLines": 7, + "normalizedLines": 56, + "minifiedBytes": 993, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics", + "physicalLines": 443, + "sourceBlankLines": 29, + "normalizedLines": 157, + "minifiedBytes": 3170, + "lifecycle": { + "domMutation": 12, + "listener": 14, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation", + "physicalLines": 129, + "sourceBlankLines": 8, + "normalizedLines": 51, + "minifiedBytes": 775, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 1, + "disposal": 6, + "focus": 3 + }, + "lcov": null + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations", + "physicalLines": 202, + "sourceBlankLines": 13, + "normalizedLines": 69, + "minifiedBytes": 1464, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)", + "physicalLines": 271, + "sourceBlankLines": 20, + "normalizedLines": 131, + "minifiedBytes": 2048, + "lifecycle": { + "domMutation": 10, + "listener": 0, + "effect": 0, + "disposal": 1, + "focus": 1 + }, + "lcov": null + }, + { + "path": "src/ui/shell/adopt.ts", + "class": "plumbing", + "sliceItem": "7 — the Preact/foreign-DOM boundary every imperative integration point crosses", + "absent": true + }, + { + "path": "src/ui/shell/focus-settlement.ts", + "class": "plumbing", + "sliceItem": "3 — capture/settle across a structural transition where the focused source disappears", + "absent": true + }, + { + "path": "src/ui/shell/right-inspector-view.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host (replaces right-inspector.ts)", + "absent": true + }, + { + "path": "src/ui/shell/shell-context.types.ts", + "class": "plumbing", + "sliceItem": "1 — type-only seam contract for the view; erased by the build", + "absent": true + }, + { + "path": "src/ui/shell/shell-host.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — the composition root: shell geometry below the header, desktop/mobile projection, Query/Dashboard host switching (replaces app-shell.ts)", + "absent": true + }, + { + "path": "src/ui/shell/shell-layout.ts", + "class": "plumbing", + "sliceItem": "2, 3 — the left-navigation layout as derived state (replaces applyEffectiveLeftNavigationLayout)", + "absent": true + }, + { + "path": "src/ui/shell/shell-view.ts", + "class": "plumbing", + "sliceItem": "1, 2, 5, 6 — the rendered application frame, the left navigation in wide/rail/drawer presentations, and the mobile bottom nav (replaces app-shell.ts markup + left-rail.ts)", + "absent": true + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher", + "physicalLines": 137, + "sourceBlankLines": 13, + "normalizedLines": 74, + "minifiedBytes": 1733, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector", + "physicalLines": 160, + "sourceBlankLines": 12, + "normalizedLines": 46, + "minifiedBytes": 1099, + "lifecycle": { + "domMutation": 2, + "listener": 4, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host", + "physicalLines": 317, + "sourceBlankLines": 9, + "normalizedLines": 164, + "minifiedBytes": 5772, + "lifecycle": { + "domMutation": 17, + "listener": 4, + "effect": 2, + "disposal": 6, + "focus": 0 + }, + "lcov": null + } + ] +} diff --git a/docs/design/577/manifest-v2/s1/ui-complexity-report.md b/docs/design/577/manifest-v2/s1/ui-complexity-report.md new file mode 100644 index 00000000..731b98f2 --- /dev/null +++ b/docs/design/577/manifest-v2/s1/ui-complexity-report.md @@ -0,0 +1,61 @@ +# UI complexity report — s1 + +Schema version 1. Counts run over esbuild-transformed +source (comments stripped, formatting normalized), never raw text. + +**Deciding metric owned here:** `ownedProductionCode` — normalized lines by class. +Every other number below is explanatory and must not be voted with: +- `minifiedBytes` — ignores tree shaking, shared helpers, and complexity moved into a dependency +- `lcovBranches` — falls when mechanisms move into Preact — externalized, not eliminated +- `lcovFunctions` — falls when mechanisms move into Preact — externalized, not eliminated +- `lifecycleSites` — hidden/dataset/replaceChildren go syntactically invisible under a vDOM + +## Owned production code (deciding) + +| Class | Files | Normalized (code) | Physical | Non-code | +|---|---:|---:|---:|---:| +| domain | 4 | **341** | 1367 | 953 | +| plumbing | 5 | **742** | 1766 | 949 | +| island | 2 | **203** | 603 | 359 | +| **total** | **11** | **1286** | **3736** | **2261** | + +Physical LOC overstates code by **2261** non-code lines +(comments plus formatting-only lines). A raw-LOC comparison would have credited +those as complexity — which is the specific error this instrument exists to prevent. + +## Explanatory metrics + +| File | Class | Code | Minified B | DOM | Listeners | Effects | Disposal | focus() | lcov BRF/FNF | +|---|---|---:|---:|---:|---:|---:|---:|---:|---| +| `src/application/left-nav.ts` | domain | 48 | 951 | 4 | 0 | 2 | 0 | 0 | _unmatched_ | +| `src/core/left-nav-layout.ts` | domain | 150 | 4836 | 0 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/app-shell.ts` | plumbing | 340 | 7856 | 41 | 3 | 14 | 23 | 4 | _unmatched_ | +| `src/ui/drawer.ts` | plumbing | 56 | 993 | 2 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/left-nav-separator.ts` | island | 157 | 3170 | 12 | 14 | 2 | 4 | 0 | _unmatched_ | +| `src/ui/left-rail.ts` | plumbing | 51 | 775 | 2 | 0 | 1 | 6 | 3 | _unmatched_ | +| `src/ui/nav-sections.ts` | domain | 69 | 1464 | 2 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/right-inspector.ts` | plumbing | 131 | 2048 | 10 | 0 | 0 | 1 | 1 | _unmatched_ | +| `src/ui/shell/adopt.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/focus-settlement.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/right-inspector-view.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-context.types.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-host.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-layout.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/shell-view.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/sidebar-upper.ts` | domain | 74 | 1733 | 4 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/splitters.ts` | island | 46 | 1099 | 2 | 4 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/workbench/workbench-shell.ts` | plumbing | 164 | 5772 | 17 | 4 | 2 | 6 | 0 | _unmatched_ | + +> 7 manifest file(s) absent in this state — measured against the one canonical +> manifest so every state covers an identical file set (a deletion must show up, not vanish). + +> 11 file(s) had no lcov record — reported as unmatched, never as zero branches. + +## Environment + +- **commit**: `7e7c8c4b6d8c5853d4e59dc599c5705f8e9cacab` +- **commitDirty**: `clean` +- **node**: `v25.9.0` +- **esbuild**: `0.28.1` +- **platform**: `darwin-arm64` +- **packageLock**: `sha256:9042ec0421903e38` diff --git a/docs/design/577/manifest-v2/s2/bundle-size-report.json b/docs/design/577/manifest-v2/s2/bundle-size-report.json new file mode 100644 index 00000000..2658bd48 --- /dev/null +++ b/docs/design/577/manifest-v2/s2/bundle-size-report.json @@ -0,0 +1,396 @@ +{ + "schemaVersion": 1, + "artifact": { + "raw": 2149976, + "gzip": 632519, + "brotli": 535612 + }, + "js": { + "raw": 1890126, + "gzip": 517030, + "brotli": 426416 + }, + "css": { + "raw": 239794, + "gzip": 109847, + "brotli": 105969 + }, + "totalOutputBytes": 1889930, + "entryPoints": [ + { + "file": "main.js", + "entryPoint": "src/main.ts", + "bytes": 1890123 + } + ], + "ownership": { + "project": { + "bytes": 635169, + "pct": 33.60807014016392 + }, + "generated": { + "bytes": 457308, + "pct": 24.19708666458546 + }, + "external": { + "bytes": 797453, + "pct": 42.19484319525062 + }, + "other": { + "bytes": 0, + "pct": 0 + } + }, + "packages": [ + { + "name": "chart.js", + "bytes": 197174, + "pct": 10.432873175196965 + }, + { + "name": "@codemirror/view", + "bytes": 186268, + "pct": 9.85581476562624 + }, + { + "name": "date-fns", + "bytes": 47998, + "pct": 2.5396707814575143 + }, + { + "name": "@codemirror/state", + "bytes": 46747, + "pct": 2.4734778536771205 + }, + { + "name": "marked", + "bytes": 41843, + "pct": 2.213997343816967 + }, + { + "name": "@dagrejs/dagre", + "bytes": 40422, + "pct": 2.1388093738921548 + }, + { + "name": "@codemirror/lang-sql", + "bytes": 32160, + "pct": 1.7016503256734377 + }, + { + "name": "@codemirror/autocomplete", + "bytes": 31522, + "pct": 1.6678924616255628 + }, + { + "name": "@lezer/lr", + "bytes": 26552, + "pct": 1.4049197589328706 + }, + { + "name": "@codemirror/language", + "bytes": 24521, + "pct": 1.297455461313382 + }, + { + "name": "@codemirror/commands", + "bytes": 23371, + "pct": 1.2366066468070247 + }, + { + "name": "@lezer/common", + "bytes": 20388, + "pct": 1.078770113178795 + }, + { + "name": "@codemirror/search", + "bytes": 18472, + "pct": 0.9773906970099422 + }, + { + "name": "preact", + "bytes": 12519, + "pct": 0.6624054859174678 + }, + { + "name": "@lezer/xml", + "bytes": 8583, + "pct": 0.4541438042678829 + }, + { + "name": "@kurkle/color", + "bytes": 7597, + "pct": 0.40197255983025826 + }, + { + "name": "@lezer/highlight", + "bytes": 7218, + "pct": 0.38191890704946746 + }, + { + "name": "@codemirror/lang-xml", + "bytes": 5975, + "pct": 0.31614927536998727 + }, + { + "name": "@preact/signals-core", + "bytes": 4749, + "pct": 0.2512791479049489 + }, + { + "name": "@preact/signals", + "bytes": 3140, + "pct": 0.16614371960866275 + }, + { + "name": "@marijn/find-cluster-break", + "bytes": 2332, + "pct": 0.12339081341636991 + }, + { + "name": "style-mod", + "bytes": 2202, + "pct": 0.11651225177652083 + }, + { + "name": "chartjs-adapter-date-fns", + "bytes": 1688, + "pct": 0.0893154772928098 + }, + { + "name": "@lezer/json", + "bytes": 1607, + "pct": 0.08502960427105766 + }, + { + "name": "w3c-keyname", + "bytes": 1520, + "pct": 0.08042625917362019 + }, + { + "name": "crelt", + "bytes": 611, + "pct": 0.03232923970729075 + }, + { + "name": "@codemirror/lang-json", + "bytes": 274, + "pct": 0.014497891456297323 + } + ], + "topModules": [ + { + "path": "src/generated/json-schema-validators.js", + "bytes": 214536, + "pct": 11.35153153820512, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "src/generated/example-dashboards.ts", + "bytes": 187686, + "pct": 9.93084399951321, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/view/dist/index.js", + "bytes": 186268, + "pct": 9.85581476562624, + "owner": "external", + "group": "@codemirror/view" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chart.js", + "bytes": 165504, + "pct": 8.757149735704498, + "owner": "external", + "group": "chart.js" + }, + { + "path": "src/generated/json-schemas.js", + "bytes": 55086, + "pct": 2.914711126867133, + "owner": "generated", + "group": "src/generated" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/state/dist/index.js", + "bytes": 46747, + "pct": 2.4734778536771205, + "owner": "external", + "group": "@codemirror/state" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/marked/lib/marked.esm.js", + "bytes": 41843, + "pct": 2.213997343816967, + "owner": "external", + "group": "marked" + }, + { + "path": "src/ui/app.ts", + "bytes": 41350, + "pct": 2.1879117215981547, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@dagrejs/dagre/dist/dagre.esm.js", + "bytes": 40422, + "pct": 2.1388093738921548, + "owner": "external", + "group": "@dagrejs/dagre" + }, + { + "path": "src/ui/dashboard.ts", + "bytes": 35352, + "pct": 1.8705454699380402, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/lang-sql/dist/index.js", + "bytes": 32160, + "pct": 1.7016503256734377, + "owner": "external", + "group": "@codemirror/lang-sql" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chunks/helpers.dataset.js", + "bytes": 31641, + "pct": 1.6741889911266554, + "owner": "external", + "group": "chart.js" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/autocomplete/dist/index.js", + "bytes": 31522, + "pct": 1.6678924616255628, + "owner": "external", + "group": "@codemirror/autocomplete" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/lr/dist/index.js", + "bytes": 26552, + "pct": 1.4049197589328706, + "owner": "external", + "group": "@lezer/lr" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/language/dist/index.js", + "bytes": 24521, + "pct": 1.297455461313382, + "owner": "external", + "group": "@codemirror/language" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/commands/dist/index.js", + "bytes": 23371, + "pct": 1.2366066468070247, + "owner": "external", + "group": "@codemirror/commands" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/common/dist/index.js", + "bytes": 20388, + "pct": 1.078770113178795, + "owner": "external", + "group": "@lezer/common" + }, + { + "path": "src/ui/results.ts", + "bytes": 18812, + "pct": 0.9953807812987783, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/search/dist/index.js", + "bytes": 18472, + "pct": 0.9773906970099422, + "owner": "external", + "group": "@codemirror/search" + }, + { + "path": "src/dashboard/application/dashboard-viewer-session.ts", + "bytes": 15281, + "pct": 0.808548464757954, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/dashboard-tree.ts", + "bytes": 13899, + "pct": 0.7354240633250967, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/file-menu.ts", + "bytes": 12919, + "pct": 0.6835702909631574, + "owner": "project", + "group": "src" + }, + { + "path": "src/core/chart-data.ts", + "bytes": 12062, + "pct": 0.6382246961527676, + "owner": "project", + "group": "src" + }, + { + "path": "src/ui/explain-graph.ts", + "bytes": 11980, + "pct": 0.6338859111184012, + "owner": "project", + "group": "src" + }, + { + "path": "src/state.ts", + "bytes": 11052, + "pct": 0.5847835634124016, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/preact/dist/preact.module.js", + "bytes": 10686, + "pct": 0.5654177667955956, + "owner": "external", + "group": "preact" + }, + { + "path": "src/ui/shortcuts.ts", + "bytes": 10307, + "pct": 0.5453641140148048, + "owner": "project", + "group": "src" + }, + { + "path": "src/net/ch-client.ts", + "bytes": 9860, + "pct": 0.5217124443762468, + "owner": "project", + "group": "src" + }, + { + "path": "src/dashboard/model/workspace-semantics.ts", + "bytes": 8724, + "pct": 0.4616043980464885, + "owner": "project", + "group": "src" + }, + { + "path": "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/xml/dist/index.js", + "bytes": 8583, + "pct": 0.4541438042678829, + "owner": "external", + "group": "@lezer/xml" + } + ], + "notes": [ + "Percentages are of raw contributed output bytes (metafile bytesInOutput); gzip/Brotli are measured per whole artifact only — compression is not additive across modules." + ] +} \ No newline at end of file diff --git a/docs/design/577/manifest-v2/s2/esbuild-inputs.json b/docs/design/577/manifest-v2/s2/esbuild-inputs.json new file mode 100644 index 00000000..888ccf29 --- /dev/null +++ b/docs/design/577/manifest-v2/s2/esbuild-inputs.json @@ -0,0 +1,1077 @@ +{ + "_comment": [ + "Per-input attribution for the S2 artifact, extracted from the esbuild", + "metafile. This is the evidence that the vanilla shell is ABSENT from the", + "built artifact rather than shipped beside the Preact one — grep it for", + "app-shell.ts, left-rail.ts or right-inspector.ts and there is nothing to", + "find. Regenerate with: node build/size-report.mjs (writes esbuild-meta.json)." + ], + "output": "main.js", + "inputs": { + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@kurkle/color/dist/color.esm.js": { + "bytesInOutput": 7597 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chunks/helpers.dataset.js": { + "bytesInOutput": 31641 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/dist/chart.js": { + "bytesInOutput": 165504 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chart.js/auto/auto.js": { + "bytesInOutput": 29 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/constants.js": { + "bytesInOutput": 186 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/constructFrom.js": { + "bytesInOutput": 141 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/toDate.js": { + "bytesInOutput": 35 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addDays.js": { + "bytesInOutput": 102 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addMonths.js": { + "bytesInOutput": 246 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addMilliseconds.js": { + "bytesInOutput": 48 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addHours.js": { + "bytesInOutput": 39 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/defaultOptions.js": { + "bytesInOutput": 34 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfWeek.js": { + "bytesInOutput": 229 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfISOWeek.js": { + "bytesInOutput": 52 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/getISOWeekYear.js": { + "bytesInOutput": 248 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/getTimezoneOffsetInMilliseconds.js": { + "bytesInOutput": 200 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/normalizeDates.js": { + "bytesInOutput": 89 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfDay.js": { + "bytesInOutput": 64 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInCalendarDays.js": { + "bytesInOutput": 108 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfISOWeekYear.js": { + "bytesInOutput": 102 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addMinutes.js": { + "bytesInOutput": 74 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addQuarters.js": { + "bytesInOutput": 38 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addSeconds.js": { + "bytesInOutput": 40 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addWeeks.js": { + "bytesInOutput": 38 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/addYears.js": { + "bytesInOutput": 39 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/compareAsc.js": { + "bytesInOutput": 59 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/isDate.js": { + "bytesInOutput": 113 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/isValid.js": { + "bytesInOutput": 66 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInCalendarMonths.js": { + "bytesInOutput": 118 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInCalendarYears.js": { + "bytesInOutput": 81 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInDays.js": { + "bytesInOutput": 407 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/getRoundingMethod.js": { + "bytesInOutput": 76 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInHours.js": { + "bytesInOutput": 87 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInMilliseconds.js": { + "bytesInOutput": 37 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInMinutes.js": { + "bytesInOutput": 68 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfDay.js": { + "bytesInOutput": 69 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfMonth.js": { + "bytesInOutput": 121 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/isLastDayOfMonth.js": { + "bytesInOutput": 60 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInMonths.js": { + "bytesInOutput": 254 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInQuarters.js": { + "bytesInOutput": 69 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInSeconds.js": { + "bytesInOutput": 69 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInWeeks.js": { + "bytesInOutput": 69 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/differenceInYears.js": { + "bytesInOutput": 159 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfQuarter.js": { + "bytesInOutput": 103 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfMonth.js": { + "bytesInOutput": 77 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfYear.js": { + "bytesInOutput": 110 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfYear.js": { + "bytesInOutput": 99 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfHour.js": { + "bytesInOutput": 68 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfWeek.js": { + "bytesInOutput": 239 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfMinute.js": { + "bytesInOutput": 65 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfQuarter.js": { + "bytesInOutput": 110 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/endOfSecond.js": { + "bytesInOutput": 67 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/en-US/_lib/formatDistance.js": { + "bytesInOutput": 1089 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/_lib/buildFormatLongFn.js": { + "bytesInOutput": 123 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/en-US/_lib/formatLong.js": { + "bytesInOutput": 423 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/en-US/_lib/formatRelative.js": { + "bytesInOutput": 169 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/_lib/buildLocalizeFn.js": { + "bytesInOutput": 400 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/en-US/_lib/localize.js": { + "bytesInOutput": 1994 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/_lib/buildMatchFn.js": { + "bytesInOutput": 556 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/_lib/buildMatchPatternFn.js": { + "bytesInOutput": 271 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/en-US/_lib/match.js": { + "bytesInOutput": 1866 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/locale/en-US.js": { + "bytesInOutput": 142 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/defaultLocale.js": { + "bytesInOutput": 0 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/getDayOfYear.js": { + "bytesInOutput": 56 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/getISOWeek.js": { + "bytesInOutput": 77 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/getWeekYear.js": { + "bytesInOutput": 376 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfWeekYear.js": { + "bytesInOutput": 251 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/getWeek.js": { + "bytesInOutput": 81 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/addLeadingZeros.js": { + "bytesInOutput": 86 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/format/lightFormatters.js": { + "bytesInOutput": 642 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/format/formatters.js": { + "bytesInOutput": 6951 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/format/longFormatters.js": { + "bytesInOutput": 718 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/_lib/protectedTokens.js": { + "bytesInOutput": 461 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/format.js": { + "bytesInOutput": 1303 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/getDefaultOptions.js": { + "bytesInOutput": 44 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/getISODay.js": { + "bytesInOutput": 61 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/transpose.js": { + "bytesInOutput": 259 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/Setter.js": { + "bytesInOutput": 514 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/Parser.js": { + "bytesInOutput": 184 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/EraParser.js": { + "bytesInOutput": 458 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/constants.js": { + "bytesInOutput": 739 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/utils.js": { + "bytesInOutput": 1246 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/YearParser.js": { + "bytesInOutput": 609 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/LocalWeekYearParser.js": { + "bytesInOutput": 671 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/ISOWeekYearParser.js": { + "bytesInOutput": 302 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/ExtendedYearParser.js": { + "bytesInOutput": 268 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/QuarterParser.js": { + "bytesInOutput": 747 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/StandAloneQuarterParser.js": { + "bytesInOutput": 747 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/MonthParser.js": { + "bytesInOutput": 770 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/StandAloneMonthParser.js": { + "bytesInOutput": 770 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/setWeek.js": { + "bytesInOutput": 95 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/LocalWeekParser.js": { + "bytesInOutput": 367 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/setISOWeek.js": { + "bytesInOutput": 85 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/ISOWeekParser.js": { + "bytesInOutput": 365 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/DateParser.js": { + "bytesInOutput": 550 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/DayOfYearParser.js": { + "bytesInOutput": 474 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/setDay.js": { + "bytesInOutput": 233 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/DayParser.js": { + "bytesInOutput": 816 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/LocalDayParser.js": { + "bytesInOutput": 1006 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/StandAloneLocalDayParser.js": { + "bytesInOutput": 1006 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/setISODay.js": { + "bytesInOutput": 70 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/ISODayParser.js": { + "bytesInOutput": 961 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/AMPMParser.js": { + "bytesInOutput": 602 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/AMPMMidnightParser.js": { + "bytesInOutput": 602 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/DayPeriodParser.js": { + "bytesInOutput": 594 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/Hour1to12Parser.js": { + "bytesInOutput": 426 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/Hour0to23Parser.js": { + "bytesInOutput": 349 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/Hour0To11Parser.js": { + "bytesInOutput": 387 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/Hour1To24Parser.js": { + "bytesInOutput": 368 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/MinuteParser.js": { + "bytesInOutput": 330 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/SecondParser.js": { + "bytesInOutput": 328 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/FractionOfSecondParser.js": { + "bytesInOutput": 249 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/ISOTimezoneWithZParser.js": { + "bytesInOutput": 419 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/ISOTimezoneParser.js": { + "bytesInOutput": 419 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/TimestampSecondsParser.js": { + "bytesInOutput": 188 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers/TimestampMillisecondsParser.js": { + "bytesInOutput": 184 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse/_lib/parsers.js": { + "bytesInOutput": 288 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parse.js": { + "bytesInOutput": 1940 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfHour.js": { + "bytesInOutput": 64 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfMinute.js": { + "bytesInOutput": 62 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/startOfSecond.js": { + "bytesInOutput": 65 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/date-fns/parseISO.js": { + "bytesInOutput": 2610 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.esm.js": { + "bytesInOutput": 1688 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@dagrejs/dagre/dist/dagre.esm.js": { + "bytesInOutput": 40422 + }, + "src/ui/dom.ts": { + "bytesInOutput": 1475 + }, + "src/ui/brand-logo.ts": { + "bytesInOutput": 3398 + }, + "src/ui/icons.ts": { + "bytesInOutput": 6678 + }, + "src/core/quoted-span.ts": { + "bytesInOutput": 183 + }, + "src/core/sql-spans.ts": { + "bytesInOutput": 1111 + }, + "src/core/sql-split.ts": { + "bytesInOutput": 874 + }, + "src/core/format.ts": { + "bytesInOutput": 3359 + }, + "src/core/saved-query.ts": { + "bytesInOutput": 2843 + }, + "src/generated/json-schemas.js": { + "bytesInOutput": 55086 + }, + "src/generated/json-schema-validators.js": { + "bytesInOutput": 214536 + }, + "src/core/json-schema-validation.ts": { + "bytesInOutput": 4983 + }, + "src/core/library-migrations.ts": { + "bytesInOutput": 1537 + }, + "src/core/param-scan.ts": { + "bytesInOutput": 512 + }, + "src/core/clickhouse-type.ts": { + "bytesInOutput": 3931 + }, + "src/core/param-type.ts": { + "bytesInOutput": 2278 + }, + "src/core/param-validate.ts": { + "bytesInOutput": 2561 + }, + "src/core/param-serialize.ts": { + "bytesInOutput": 1400 + }, + "src/core/optional-blocks.ts": { + "bytesInOutput": 2464 + }, + "src/core/relative-time.ts": { + "bytesInOutput": 4942 + }, + "src/core/param-pipeline.ts": { + "bytesInOutput": 4475 + }, + "src/core/query-time-range.ts": { + "bytesInOutput": 1471 + }, + "src/core/library-codec.ts": { + "bytesInOutput": 4510 + }, + "src/core/storage.ts": { + "bytesInOutput": 420 + }, + "src/core/recent-values.ts": { + "bytesInOutput": 1257 + }, + "src/core/spec-schema.ts": { + "bytesInOutput": 8272 + }, + "src/core/spec-draft.ts": { + "bytesInOutput": 3111 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@preact/signals-core/dist/signals-core.module.js": { + "bytesInOutput": 4749 + }, + "src/dashboard/model/portable-limits.ts": { + "bytesInOutput": 341 + }, + "src/dashboard/model/workspace-diagnostics.ts": { + "bytesInOutput": 718 + }, + "src/dashboard/model/json-limits.ts": { + "bytesInOutput": 973 + }, + "src/dashboard/model/canonical-json.ts": { + "bytesInOutput": 2504 + }, + "src/dashboard/model/query-ownership.ts": { + "bytesInOutput": 632 + }, + "src/dashboard/model/workspace-semantics.ts": { + "bytesInOutput": 8724 + }, + "src/dashboard/model/dashboard-document.ts": { + "bytesInOutput": 1981 + }, + "src/workspace/stored-workspace-ownership.ts": { + "bytesInOutput": 1369 + }, + "src/workspace/stored-workspace.ts": { + "bytesInOutput": 3014 + }, + "src/workspace/workspace-sync.ts": { + "bytesInOutput": 550 + }, + "src/core/workspace-key.ts": { + "bytesInOutput": 371 + }, + "src/core/left-nav-layout.ts": { + "bytesInOutput": 3123 + }, + "src/state.ts": { + "bytesInOutput": 11052 + }, + "src/workspace/workspace-dashboards.ts": { + "bytesInOutput": 961 + }, + "src/core/export.ts": { + "bytesInOutput": 1609 + }, + "src/core/stream.ts": { + "bytesInOutput": 1674 + }, + "src/core/field-config.ts": { + "bytesInOutput": 927 + }, + "src/core/chart-data.ts": { + "bytesInOutput": 12062 + }, + "src/core/logs.ts": { + "bytesInOutput": 1717 + }, + "src/core/kpi.ts": { + "bytesInOutput": 4242 + }, + "src/core/panel-cfg.ts": { + "bytesInOutput": 3720 + }, + "src/core/sql-lex.ts": { + "bytesInOutput": 1443 + }, + "src/core/schema-graph.ts": { + "bytesInOutput": 4642 + }, + "src/net/ch-client.ts": { + "bytesInOutput": 9860 + }, + "src/editor/editor-port.ts": { + "bytesInOutput": 247 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@marijn/find-cluster-break/src/index.js": { + "bytesInOutput": 2332 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/state/dist/index.js": { + "bytesInOutput": 46747 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/style-mod/src/style-mod.js": { + "bytesInOutput": 2202 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/w3c-keyname/index.js": { + "bytesInOutput": 1520 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/crelt/index.js": { + "bytesInOutput": 611 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/view/dist/index.js": { + "bytesInOutput": 186268 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/common/dist/index.js": { + "bytesInOutput": 20388 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/highlight/dist/index.js": { + "bytesInOutput": 7218 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/language/dist/index.js": { + "bytesInOutput": 24521 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/commands/dist/index.js": { + "bytesInOutput": 23371 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/autocomplete/dist/index.js": { + "bytesInOutput": 31522 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/lr/dist/index.js": { + "bytesInOutput": 26552 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/json/dist/index.js": { + "bytesInOutput": 1607 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/lang-json/dist/index.js": { + "bytesInOutput": 274 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/search/dist/index.js": { + "bytesInOutput": 18472 + }, + "src/editor/codemirror-base.ts": { + "bytesInOutput": 658 + }, + "src/core/spec-completion.ts": { + "bytesInOutput": 6416 + }, + "src/editor/spec-json-context.ts": { + "bytesInOutput": 5765 + }, + "src/editor/spec-completion-adapter.ts": { + "bytesInOutput": 2770 + }, + "src/editor/spec-editor.ts": { + "bytesInOutput": 4129 + }, + "src/ui/menu.ts": { + "bytesInOutput": 2370 + }, + "src/ui/confirm-menu.ts": { + "bytesInOutput": 418 + }, + "src/dashboard/model/tab-origin-badges.ts": { + "bytesInOutput": 2539 + }, + "src/ui/tabs.ts": { + "bytesInOutput": 4447 + }, + "src/application/dashboard-variable-config.ts": { + "bytesInOutput": 284 + }, + "src/core/dashboard-tree-ui-state.ts": { + "bytesInOutput": 1434 + }, + "src/core/dashboard-variables.ts": { + "bytesInOutput": 1307 + }, + "src/application/dashboard-tree-model.ts": { + "bytesInOutput": 7569 + }, + "src/application/left-nav.ts": { + "bytesInOutput": 684 + }, + "src/ui/placeholder.ts": { + "bytesInOutput": 216 + }, + "src/core/cell.ts": { + "bytesInOutput": 534 + }, + "src/core/doc-capability.ts": { + "bytesInOutput": 8016 + }, + "src/core/doc-documentation.ts": { + "bytesInOutput": 2458 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/marked/lib/marked.esm.js": { + "bytesInOutput": 41843 + }, + "src/core/doc-markdown.ts": { + "bytesInOutput": 5830 + }, + "src/ui/doc-markdown-view.ts": { + "bytesInOutput": 2277 + }, + "src/ui/chart-render.ts": { + "bytesInOutput": 2464 + }, + "src/core/sort.ts": { + "bytesInOutput": 290 + }, + "src/ui/grid-render.ts": { + "bytesInOutput": 2677 + }, + "src/ui/logs.ts": { + "bytesInOutput": 1084 + }, + "src/ui/kpi-panel.ts": { + "bytesInOutput": 1737 + }, + "src/core/result-choice.ts": { + "bytesInOutput": 598 + }, + "src/ui/panels.ts": { + "bytesInOutput": 6329 + }, + "src/core/explain.ts": { + "bytesInOutput": 1582 + }, + "src/core/script-result.ts": { + "bytesInOutput": 396 + }, + "src/core/dot.ts": { + "bytesInOutput": 585 + }, + "src/core/dot-layout.ts": { + "bytesInOutput": 1666 + }, + "src/core/type-display.ts": { + "bytesInOutput": 2022 + }, + "src/core/schema-cards.ts": { + "bytesInOutput": 1623 + }, + "src/core/panzoom.ts": { + "bytesInOutput": 439 + }, + "src/core/graph-layout.ts": { + "bytesInOutput": 868 + }, + "src/ui/toast.ts": { + "bytesInOutput": 728 + }, + "src/ui/schema-detail.ts": { + "bytesInOutput": 4600 + }, + "src/ui/detached-view.ts": { + "bytesInOutput": 1578 + }, + "src/ui/explain-graph.ts": { + "bytesInOutput": 11980 + }, + "src/core/variable-width.ts": { + "bytesInOutput": 409 + }, + "src/ui/var-field.ts": { + "bytesInOutput": 365 + }, + "src/ui/combobox.ts": { + "bytesInOutput": 2455 + }, + "src/ui/combo-footer.ts": { + "bytesInOutput": 448 + }, + "src/ui/relative-time-field.ts": { + "bytesInOutput": 2962 + }, + "src/ui/recent-field.ts": { + "bytesInOutput": 1028 + }, + "src/ui/enum-field.ts": { + "bytesInOutput": 1596 + }, + "src/ui/popover.ts": { + "bytesInOutput": 1570 + }, + "src/core/time-range.ts": { + "bytesInOutput": 4932 + }, + "src/ui/time-range-field.ts": { + "bytesInOutput": 4692 + }, + "src/ui/variable-option-field.ts": { + "bytesInOutput": 1800 + }, + "src/core/variable-selection.ts": { + "bytesInOutput": 391 + }, + "src/ui/multi-select-field.ts": { + "bytesInOutput": 3558 + }, + "src/ui/variable-bar.ts": { + "bytesInOutput": 6502 + }, + "src/ui/splitters.ts": { + "bytesInOutput": 984 + }, + "src/ui/drawer.ts": { + "bytesInOutput": 790 + }, + "src/core/panel-execution.ts": { + "bytesInOutput": 439 + }, + "src/ui/results.ts": { + "bytesInOutput": 18812 + }, + "src/core/connection-lifecycle.ts": { + "bytesInOutput": 2141 + }, + "src/ui/dialog-shell.ts": { + "bytesInOutput": 3346 + }, + "src/core/file-menu-model.ts": { + "bytesInOutput": 1707 + }, + "src/ui/dnd-mime.ts": { + "bytesInOutput": 178 + }, + "src/core/library-drag.ts": { + "bytesInOutput": 369 + }, + "src/core/tree-click-arbiter.ts": { + "bytesInOutput": 327 + }, + "src/application/dashboard-title.ts": { + "bytesInOutput": 284 + }, + "src/core/dashboard.ts": { + "bytesInOutput": 167 + }, + "src/dashboard/layouts/flow-layout.ts": { + "bytesInOutput": 2642 + }, + "src/dashboard/layouts/grafana-grid-layout.ts": { + "bytesInOutput": 5707 + }, + "src/dashboard/layouts/layout-registry.ts": { + "bytesInOutput": 1100 + }, + "src/dashboard/application/tile-membership.ts": { + "bytesInOutput": 218 + }, + "src/dashboard/application/dashboard-removal.ts": { + "bytesInOutput": 1505 + }, + "src/application/dashboard-delete.ts": { + "bytesInOutput": 1385 + }, + "src/application/dashboard-panel-metadata.ts": { + "bytesInOutput": 662 + }, + "src/dashboard/application/dashboard-commands.ts": { + "bytesInOutput": 5413 + }, + "src/dashboard/application/dashboard-query-resolver.ts": { + "bytesInOutput": 352 + }, + "src/dashboard/application/panel-creation.ts": { + "bytesInOutput": 812 + }, + "src/application/panel-creation-service.ts": { + "bytesInOutput": 883 + }, + "src/dashboard/application/library-assignment.ts": { + "bytesInOutput": 1464 + }, + "src/application/library-assignment-service.ts": { + "bytesInOutput": 2007 + }, + "src/ui/dashboard-tree.ts": { + "bytesInOutput": 13899 + }, + "src/ui/library-assign-menu.ts": { + "bytesInOutput": 2166 + }, + "src/ui/nav-sections.ts": { + "bytesInOutput": 1175 + }, + "src/ui/saved-history.ts": { + "bytesInOutput": 6841 + }, + "src/core/saved-io.ts": { + "bytesInOutput": 617 + }, + "src/dashboard/model/portable-bundle-codec.ts": { + "bytesInOutput": 2851 + }, + "src/dashboard/model/legacy-bundle.ts": { + "bytesInOutput": 561 + }, + "src/dashboard/model/bundle-order.ts": { + "bytesInOutput": 244 + }, + "src/dashboard/model/dashboard-export.ts": { + "bytesInOutput": 327 + }, + "src/workspace/workspace-operations.ts": { + "bytesInOutput": 412 + }, + "src/workspace/import-planner.ts": { + "bytesInOutput": 3673 + }, + "src/dashboard/application/empty-dashboard.ts": { + "bytesInOutput": 190 + }, + "src/application/dashboard-create.ts": { + "bytesInOutput": 277 + }, + "src/generated/example-dashboards.ts": { + "bytesInOutput": 187686 + }, + "src/ui/file-menu.ts": { + "bytesInOutput": 12919 + }, + "src/ui/app-header.ts": { + "bytesInOutput": 2001 + }, + "src/core/tile-reorder.ts": { + "bytesInOutput": 431 + }, + "src/core/dashboard-autoscroll.ts": { + "bytesInOutput": 837 + }, + "src/ui/dashboard-chart-interaction.ts": { + "bytesInOutput": 5559 + }, + "src/core/variable-options.ts": { + "bytesInOutput": 3298 + }, + "src/dashboard/model/json-merge-patch.ts": { + "bytesInOutput": 253 + }, + "src/dashboard/model/presentation-resolver.ts": { + "bytesInOutput": 2230 + }, + "src/dashboard/application/dashboard-viewer-session.ts": { + "bytesInOutput": 15281 + }, + "src/dashboard/application/panel-widen.ts": { + "bytesInOutput": 496 + }, + "src/dashboard/application/panel-tile-actions.ts": { + "bytesInOutput": 1563 + }, + "src/dashboard/application/panel-duplication.ts": { + "bytesInOutput": 796 + }, + "src/application/dashboard-panel-duplicate.ts": { + "bytesInOutput": 1094 + }, + "src/application/main-surface.ts": { + "bytesInOutput": 2166 + }, + "src/dashboard/model/dashboard-variable-store.ts": { + "bytesInOutput": 685 + }, + "src/ui/dashboard.ts": { + "bytesInOutput": 35352 + }, + "src/ui/theme-toggle.ts": { + "bytesInOutput": 188 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/lang-sql/dist/index.js": { + "bytesInOutput": 32160 + }, + "src/editor/ch-lang.ts": { + "bytesInOutput": 344 + }, + "src/ui/doc-pane.ts": { + "bytesInOutput": 7280 + }, + "src/core/param-comparison.ts": { + "bytesInOutput": 1451 + }, + "src/ui/login.ts": { + "bytesInOutput": 7493 + }, + "src/ui/shortcuts.ts": { + "bytesInOutput": 10307 + }, + "src/application/query-execution-service.ts": { + "bytesInOutput": 2184 + }, + "src/core/jwt.ts": { + "bytesInOutput": 277 + }, + "src/core/pkce.ts": { + "bytesInOutput": 474 + }, + "src/core/target.ts": { + "bytesInOutput": 177 + }, + "src/net/oauth.ts": { + "bytesInOutput": 1366 + }, + "src/net/oauth-config.ts": { + "bytesInOutput": 2003 + }, + "src/application/connection-session.ts": { + "bytesInOutput": 7117 + }, + "src/core/oauth-document-recovery.ts": { + "bytesInOutput": 4980 + }, + "src/application/oauth-document-recovery-session.ts": { + "bytesInOutput": 4494 + }, + "src/application/authenticated-execution-scope.ts": { + "bytesInOutput": 748 + }, + "src/core/sql-reference.ts": { + "bytesInOutput": 1110 + }, + "src/core/completions.ts": { + "bytesInOutput": 4174 + }, + "src/application/schema-catalog-service.ts": { + "bytesInOutput": 7232 + }, + "src/core/from-scope.ts": { + "bytesInOutput": 2208 + }, + "src/application/workbench-parameter-session.ts": { + "bytesInOutput": 1985 + }, + "src/application/ch-session-params.ts": { + "bytesInOutput": 286 + }, + "src/application/export-service.ts": { + "bytesInOutput": 6158 + }, + "src/application/schema-graph-session.ts": { + "bytesInOutput": 3425 + }, + "src/application/app-preferences.ts": { + "bytesInOutput": 189 + }, + "src/workspace/workspace-repository.ts": { + "bytesInOutput": 3459 + }, + "src/workspace/indexeddb-workspace-store.ts": { + "bytesInOutput": 2930 + }, + "src/ui/conflict-resolution.ts": { + "bytesInOutput": 732 + }, + "src/core/sql-route.ts": { + "bytesInOutput": 772 + }, + "src/core/query-source.ts": { + "bytesInOutput": 236 + }, + "src/ui/workbench/workbench-session.ts": { + "bytesInOutput": 6168 + }, + "src/application/query-document-session.ts": { + "bytesInOutput": 1208 + }, + "src/core/share.ts": { + "bytesInOutput": 961 + }, + "src/application/saved-query-service.ts": { + "bytesInOutput": 1137 + }, + "src/ui/workbench/workbench-shell.ts": { + "bytesInOutput": 5368 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/preact/dist/preact.module.js": { + "bytesInOutput": 10686 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/preact/hooks/dist/hooks.module.js": { + "bytesInOutput": 1833 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@preact/signals/dist/signals.module.js": { + "bytesInOutput": 3140 + }, + "src/ui/schema.ts": { + "bytesInOutput": 4408 + }, + "src/ui/sidebar-upper.ts": { + "bytesInOutput": 1393 + }, + "src/ui/left-nav-separator.ts": { + "bytesInOutput": 2678 + }, + "src/ui/shell/shell-layout.ts": { + "bytesInOutput": 431 + }, + "src/ui/shell/focus-settlement.ts": { + "bytesInOutput": 264 + }, + "src/ui/shell/adopt.ts": { + "bytesInOutput": 211 + }, + "src/ui/shell/right-inspector-view.ts": { + "bytesInOutput": 2440 + }, + "src/ui/shell/shell-view.ts": { + "bytesInOutput": 3262 + }, + "src/ui/shell/shell-host.ts": { + "bytesInOutput": 5822 + }, + "src/ui/app.ts": { + "bytesInOutput": 41350 + }, + "src/core/doc-context.ts": { + "bytesInOutput": 6944 + }, + "src/editor/codemirror-adapter.ts": { + "bytesInOutput": 7363 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@lezer/xml/dist/index.js": { + "bytesInOutput": 8583 + }, + "../../../../Workspaces/altinity/altinity-sql-browser/node_modules/@codemirror/lang-xml/dist/index.js": { + "bytesInOutput": 5975 + }, + "src/editor/code-viewer.ts": { + "bytesInOutput": 875 + }, + "src/main.ts": { + "bytesInOutput": 2643 + } + } +} diff --git a/docs/design/577/manifest-v2/s2/ui-complexity-report.json b/docs/design/577/manifest-v2/s2/ui-complexity-report.json new file mode 100644 index 00000000..1b98719e --- /dev/null +++ b/docs/design/577/manifest-v2/s2/ui-complexity-report.json @@ -0,0 +1,399 @@ +{ + "schemaVersion": 1, + "label": "s2", + "metricTiers": { + "deciding": { + "ownedProductionCode": { + "owner": "this instrument" + }, + "parity": { + "owner": "tests/e2e/shell-parity.spec.js" + }, + "artifact": { + "owner": "build/size-report.mjs" + }, + "changeAmplification": { + "owner": "frozen controlled-change experiment" + }, + "cleanupObligations": { + "owner": "leak invariant + manual obligation count" + } + }, + "explanatory": { + "minifiedBytes": { + "demotedBecause": "ignores tree shaking, shared helpers, and complexity moved into a dependency" + }, + "lcovBranches": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lcovFunctions": { + "demotedBecause": "falls when mechanisms move into Preact — externalized, not eliminated" + }, + "lifecycleSites": { + "demotedBecause": "hidden/dataset/replaceChildren go syntactically invisible under a vDOM" + } + } + }, + "environment": { + "commit": "3f611b5e3a2566dd56d73170239b8ff116fc834e", + "commitDirty": "clean", + "node": "v25.9.0", + "esbuild": "0.28.1", + "platform": "darwin-arm64", + "packageLock": "sha256:5dd460cf3b874b6c" + }, + "ownedProductionCode": { + "plumbingNormalizedLines": 1072, + "islandNormalizedLines": 203, + "domainNormalizedLines": 341 + }, + "byClass": { + "domain": { + "files": 4, + "absentFiles": 0, + "physicalLines": 1367, + "normalizedLines": 341, + "nonCodeLines": 953, + "minifiedBytes": 8984, + "lifecycle": { + "domMutation": 10, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 4 + }, + "plumbing": { + "files": 9, + "absentFiles": 3, + "physicalLines": 2091, + "normalizedLines": 1072, + "nonCodeLines": 920, + "minifiedBytes": 21747, + "lifecycle": { + "domMutation": 46, + "listener": 6, + "effect": 21, + "disposal": 30, + "focus": 5 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 9 + }, + "island": { + "files": 2, + "absentFiles": 0, + "physicalLines": 603, + "normalizedLines": 203, + "nonCodeLines": 359, + "minifiedBytes": 4269, + "lifecycle": { + "domMutation": 14, + "listener": 18, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 2 + } + }, + "overall": { + "files": 15, + "absentFiles": 3, + "physicalLines": 4061, + "normalizedLines": 1616, + "nonCodeLines": 2232, + "minifiedBytes": 35000, + "lifecycle": { + "domMutation": 70, + "listener": 24, + "effect": 25, + "disposal": 34, + "focus": 5 + }, + "lcovBranchesFound": 0, + "lcovFunctionsFound": 0, + "lcovUnmatchedFiles": 15 + }, + "files": [ + { + "path": "src/application/left-nav.ts", + "class": "domain", + "sliceItem": "2, 3 — the focused-section controller seam", + "physicalLines": 175, + "sourceBlankLines": 8, + "normalizedLines": 48, + "minifiedBytes": 951, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/core/left-nav-layout.ts", + "class": "domain", + "sliceItem": "2, 3 — pure mode/resize reducers, DOM-free", + "physicalLines": 853, + "sourceBlankLines": 39, + "normalizedLines": 150, + "minifiedBytes": 4836, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/app-shell.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — shell geometry below the header; desktop/mobile projection; Query/Dashboard host switching", + "absent": true + }, + { + "path": "src/ui/drawer.ts", + "class": "plumbing", + "sliceItem": "4 — right-inspector chrome and bounded resize primitives the control composes", + "physicalLines": 144, + "sourceBlankLines": 7, + "normalizedLines": 56, + "minifiedBytes": 993, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/left-nav-separator.ts", + "class": "island", + "sliceItem": "3 — the multi-frame fold transition's pointer/keyboard mechanics", + "physicalLines": 443, + "sourceBlankLines": 29, + "normalizedLines": 157, + "minifiedBytes": 3170, + "lifecycle": { + "domMutation": 12, + "listener": 14, + "effect": 2, + "disposal": 4, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/left-rail.ts", + "class": "plumbing", + "sliceItem": "2 — left navigation in rail presentation", + "absent": true + }, + { + "path": "src/ui/nav-sections.ts", + "class": "domain", + "sliceItem": "2 — section registry shared by wide, rail and drawer presentations", + "physicalLines": 202, + "sourceBlankLines": 13, + "normalizedLines": 69, + "minifiedBytes": 1464, + "lifecycle": { + "domMutation": 2, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/right-inspector.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host itself (absent in S0; added by the S1 control)", + "absent": true + }, + { + "path": "src/ui/shell/adopt.ts", + "class": "plumbing", + "sliceItem": "7 — the Preact/foreign-DOM boundary every imperative integration point crosses", + "physicalLines": 69, + "sourceBlankLines": 3, + "normalizedLines": 17, + "minifiedBytes": 237, + "lifecycle": { + "domMutation": 1, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/shell/focus-settlement.ts", + "class": "plumbing", + "sliceItem": "3 — capture/settle across a structural transition where the focused source disappears", + "physicalLines": 123, + "sourceBlankLines": 5, + "normalizedLines": 21, + "minifiedBytes": 294, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 1 + }, + "lcov": null + }, + { + "path": "src/ui/shell/right-inspector-view.ts", + "class": "plumbing", + "sliceItem": "4 — the right-inspector host (replaces right-inspector.ts)", + "physicalLines": 315, + "sourceBlankLines": 16, + "normalizedLines": 192, + "minifiedBytes": 2884, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 0, + "disposal": 1, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/shell/shell-context.types.ts", + "class": "plumbing", + "sliceItem": "1 — type-only seam contract for the view; erased by the build", + "physicalLines": 71, + "sourceBlankLines": 5, + "normalizedLines": 0, + "minifiedBytes": 0, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/shell/shell-host.ts", + "class": "plumbing", + "sliceItem": "1, 5, 6 — the composition root: shell geometry below the header, desktop/mobile projection, Query/Dashboard host switching (replaces app-shell.ts)", + "physicalLines": 636, + "sourceBlankLines": 36, + "normalizedLines": 391, + "minifiedBytes": 7169, + "lifecycle": { + "domMutation": 21, + "listener": 2, + "effect": 17, + "disposal": 23, + "focus": 3 + }, + "lcov": null + }, + { + "path": "src/ui/shell/shell-layout.ts", + "class": "plumbing", + "sliceItem": "2, 3 — the left-navigation layout as derived state (replaces applyEffectiveLeftNavigationLayout)", + "physicalLines": 167, + "sourceBlankLines": 9, + "normalizedLines": 39, + "minifiedBytes": 716, + "lifecycle": { + "domMutation": 0, + "listener": 0, + "effect": 2, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/shell/shell-view.ts", + "class": "plumbing", + "sliceItem": "1, 2, 5, 6 — the rendered application frame, the left navigation in wide/rail/drawer presentations, and the mobile bottom nav (replaces app-shell.ts markup + left-rail.ts)", + "physicalLines": 249, + "sourceBlankLines": 9, + "normalizedLines": 192, + "minifiedBytes": 3682, + "lifecycle": { + "domMutation": 1, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 1 + }, + "lcov": null + }, + { + "path": "src/ui/sidebar-upper.ts", + "class": "domain", + "sliceItem": "2 — upper-pane role switcher", + "physicalLines": 137, + "sourceBlankLines": 13, + "normalizedLines": 74, + "minifiedBytes": 1733, + "lifecycle": { + "domMutation": 4, + "listener": 0, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/splitters.ts", + "class": "island", + "sliceItem": "3, 4 — drag mechanics for both the left separator and the right inspector", + "physicalLines": 160, + "sourceBlankLines": 12, + "normalizedLines": 46, + "minifiedBytes": 1099, + "lifecycle": { + "domMutation": 2, + "listener": 4, + "effect": 0, + "disposal": 0, + "focus": 0 + }, + "lcov": null + }, + { + "path": "src/ui/workbench/workbench-shell.ts", + "class": "plumbing", + "sliceItem": "6 — Query surface host", + "physicalLines": 317, + "sourceBlankLines": 9, + "normalizedLines": 164, + "minifiedBytes": 5772, + "lifecycle": { + "domMutation": 17, + "listener": 4, + "effect": 2, + "disposal": 6, + "focus": 0 + }, + "lcov": null + } + ] +} diff --git a/docs/design/577/manifest-v2/s2/ui-complexity-report.md b/docs/design/577/manifest-v2/s2/ui-complexity-report.md new file mode 100644 index 00000000..09aefb7c --- /dev/null +++ b/docs/design/577/manifest-v2/s2/ui-complexity-report.md @@ -0,0 +1,61 @@ +# UI complexity report — s2 + +Schema version 1. Counts run over esbuild-transformed +source (comments stripped, formatting normalized), never raw text. + +**Deciding metric owned here:** `ownedProductionCode` — normalized lines by class. +Every other number below is explanatory and must not be voted with: +- `minifiedBytes` — ignores tree shaking, shared helpers, and complexity moved into a dependency +- `lcovBranches` — falls when mechanisms move into Preact — externalized, not eliminated +- `lcovFunctions` — falls when mechanisms move into Preact — externalized, not eliminated +- `lifecycleSites` — hidden/dataset/replaceChildren go syntactically invisible under a vDOM + +## Owned production code (deciding) + +| Class | Files | Normalized (code) | Physical | Non-code | +|---|---:|---:|---:|---:| +| domain | 4 | **341** | 1367 | 953 | +| plumbing | 9 | **1072** | 2091 | 920 | +| island | 2 | **203** | 603 | 359 | +| **total** | **15** | **1616** | **4061** | **2232** | + +Physical LOC overstates code by **2232** non-code lines +(comments plus formatting-only lines). A raw-LOC comparison would have credited +those as complexity — which is the specific error this instrument exists to prevent. + +## Explanatory metrics + +| File | Class | Code | Minified B | DOM | Listeners | Effects | Disposal | focus() | lcov BRF/FNF | +|---|---|---:|---:|---:|---:|---:|---:|---:|---| +| `src/application/left-nav.ts` | domain | 48 | 951 | 4 | 0 | 2 | 0 | 0 | _unmatched_ | +| `src/core/left-nav-layout.ts` | domain | 150 | 4836 | 0 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/app-shell.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/drawer.ts` | plumbing | 56 | 993 | 2 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/left-nav-separator.ts` | island | 157 | 3170 | 12 | 14 | 2 | 4 | 0 | _unmatched_ | +| `src/ui/left-rail.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/nav-sections.ts` | domain | 69 | 1464 | 2 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/right-inspector.ts` | plumbing | _absent_ | — | — | — | — | — | — | — | +| `src/ui/shell/adopt.ts` | plumbing | 17 | 237 | 1 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/shell/focus-settlement.ts` | plumbing | 21 | 294 | 0 | 0 | 0 | 0 | 1 | _unmatched_ | +| `src/ui/shell/right-inspector-view.ts` | plumbing | 192 | 2884 | 4 | 0 | 0 | 1 | 0 | _unmatched_ | +| `src/ui/shell/shell-context.types.ts` | plumbing | 0 | 0 | 0 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/shell/shell-host.ts` | plumbing | 391 | 7169 | 21 | 2 | 17 | 23 | 3 | _unmatched_ | +| `src/ui/shell/shell-layout.ts` | plumbing | 39 | 716 | 0 | 0 | 2 | 0 | 0 | _unmatched_ | +| `src/ui/shell/shell-view.ts` | plumbing | 192 | 3682 | 1 | 0 | 0 | 0 | 1 | _unmatched_ | +| `src/ui/sidebar-upper.ts` | domain | 74 | 1733 | 4 | 0 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/splitters.ts` | island | 46 | 1099 | 2 | 4 | 0 | 0 | 0 | _unmatched_ | +| `src/ui/workbench/workbench-shell.ts` | plumbing | 164 | 5772 | 17 | 4 | 2 | 6 | 0 | _unmatched_ | + +> 3 manifest file(s) absent in this state — measured against the one canonical +> manifest so every state covers an identical file set (a deletion must show up, not vanish). + +> 15 file(s) had no lcov record — reported as unmatched, never as zero branches. + +## Environment + +- **commit**: `3f611b5e3a2566dd56d73170239b8ff116fc834e` +- **commitDirty**: `clean` +- **node**: `v25.9.0` +- **esbuild**: `0.28.1` +- **platform**: `darwin-arm64` +- **packageLock**: `sha256:5dd460cf3b874b6c` From 355ca741c516f4e5024461dee25159ca6a5bfbe6 Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Fri, 31 Jul 2026 00:18:27 +0200 Subject: [PATCH 5/6] docs(#577): correct the browser-evidence attribution for two e2e failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An earlier draft called two tile-open-workbench failures pre-existing. They are not: origin/main passes all 21 specs in that file from a clean worktree, and both evaluation arms fail the same 2. The cause is the S1 control's own inspector, which defaults to inspectorPx 420 and narrows the Dashboard grid enough to break two tile-geometry assertions; S2 inherits the pane and the failures. They still do not differentiate the arms, so they remain unscored — but they are a side effect of the evaluation SLICE, which is worth recording, and #577's 'passes every behavioural gate' wording is not cleanly met by either arm because of them. Filed as #581 (inbox) for #488. Part of #577 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XLuZUHRbTCFKDJf2Zf8nP1 --- docs/design/577/S2-EVIDENCE.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/design/577/S2-EVIDENCE.md b/docs/design/577/S2-EVIDENCE.md index bf951321..837deb62 100644 --- a/docs/design/577/S2-EVIDENCE.md +++ b/docs/design/577/S2-EVIDENCE.md @@ -176,11 +176,27 @@ check that this deviation cost the treatment nothing it was owed. ## Browser evidence Chromium and WebKit, full suite: **417 passed**. Two `tile-open-workbench` -specs (`:327`, `:453`) fail on both engines — and fail **identically on -`eval-577/s1-control` from a clean worktree**, so they belong to neither arm and -are not scored. Checking that mattered: an earlier S2 run failed a *different* -subset of the same file, so it is partly order-dependent, and reading either run -as a verdict without the baseline would have been wrong in both directions. +specs (`:327`, `:453`) fail on both engines, and fail **identically on +`eval-577/s1-control` from a clean worktree** — so they do not differentiate the +arms and are not scored against either. + +They are **not** pre-existing, and the first version of this document was wrong +to imply it. `origin/main` passes all 21 specs in that file from a clean +worktree. The failures are introduced by the **S1 control's own inspector**: it +defaults to `inspectorPx` 420 (`state.ts`), so it occupies 420px as the last +child of `.main-row`, narrowing the Dashboard grid enough to break two tile +geometry assertions. S2 inherits the same pane and the same failures. + +That makes them a cost of the evaluation **slice**, not of either architecture — +a mock pane added for the comparison perturbing unrelated layout assertions. It +is worth recording for two reasons: the slice's own side effects are exactly the +kind of thing that quietly contaminates a comparison, and #577's gate wording +("passes every behavioural gate") is not cleanly met by *either* arm here. + +Checking this mattered twice over: an earlier S2 run failed a *different* subset +of the same file, so it is partly order-dependent, and reading any single run as +a verdict — without both the control and `main` as baselines — would have been +wrong in both directions. Two genuine regressions were found by real-browser e2e **with 6 995 unit tests green**, and both are fixed: From f749fd14fbae47f66598d01d139364c278f1e6fc Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Fri, 31 Jul 2026 00:20:00 +0200 Subject: [PATCH 6/6] docs(#577): correct the lifecycle claim after third-party review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The evidence said "Preact offers no before-the-DOM-changes hook". That is true for FUNCTION components — which is what the treatment arm uses — but overbroad as a statement about Preact: preact/src/diff/index.js calls componentWillUpdate (:203) and getSnapshotBeforeUpdate (:250) before diffChildren (:259), gated on isClassComponent. The weak premise is still refuted, and the corrected reason is stronger: getSnapshotBeforeUpdate IS a capture hook, so having it relocates the capture/restore protocol into a lifecycle method rather than deleting it. The component model changes where that code lives, never whether it exists. Also records two open points from the same review: the superset manifest proves nothing was omitted but does not prove no smaller Preact design exists (the controlled-change experiment is where that should be settled), and the pinned numbers have no re-verification path because the evaluation branches sit outside CI. The frozen 577/treatment branch keeps the original phrasing in its own source comment; the tag pins measured evidence, and this document is the corrected record. Part of #577 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XLuZUHRbTCFKDJf2Zf8nP1 --- docs/design/577/S2-EVIDENCE.md | 62 +++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/design/577/S2-EVIDENCE.md b/docs/design/577/S2-EVIDENCE.md index 837deb62..7a9582bd 100644 --- a/docs/design/577/S2-EVIDENCE.md +++ b/docs/design/577/S2-EVIDENCE.md @@ -157,10 +157,32 @@ Costs the control does not pay, each found while building: > "One owner per subtree deletes the focus-rescue category." -**Refuted.** Preact offers no before-the-DOM-changes hook — `useLayoutEffect` -cleanups run after the diff — so capture cannot happen at paint time at all. The -work survives in full; only its location moves, and finding 4 above shows the -relocation introduced a failure the control does not have. +**Refuted** — but the first version of this document refuted it with an +overbroad claim, corrected here after third-party review. + +*What was claimed:* "Preact offers no before-the-DOM-changes hook." +*What is true:* Preact 10 has no such hook **for function components**, which is +what this arm uses (and the modern idiom): `useLayoutEffect` cleanups run after +the diff. But `preact/src/diff/index.js` calls `componentWillUpdate` (`:203`) and +`getSnapshotBeforeUpdate` (`:250`) **before** `diffChildren` (`:259`) — gated on +`isClassComponent`. A class-component arm could therefore read the DOM before it +mutates. The frozen `577/treatment` branch carries the same overbroad phrasing in +`focus-settlement.ts`'s header comment; it is left as-is because the tag pins +measured evidence, and this document is the corrected record. + +**The premise is still refuted, and the correct reason is stronger.** +`getSnapshotBeforeUpdate` *is* a capture hook — React's own canonical example for +it is preserving scroll position across a mutation. Having it does not delete the +capture/restore work; it relocates it into a lifecycle method and hands you the +captured value back in `componentDidUpdate`. That is the same protocol +`focus-settlement.ts` implements by hand, under a different name. What the +component model changes is *where* this code lives, never *whether* it exists — +and finding 4 above shows the relocation introduced a failure mode the control +does not have. + +Adopting class components to reach that hook was not attempted, and would have +carried its own cost: a second component paradigm inside an arm whose whole +premise is that one render model replaces the imperative one. ## Deviation from the plan @@ -215,7 +237,37 @@ real browsers rather than in happy-dom alone. - the arm-agnostic parity suite (normalized subtree serialization, parametrized over both states, pinned assertion count); - the controlled-change experiment; -- the whole-slice JSX sensitivity annex. +- the whole-slice JSX sensitivity annex; +- **a re-verification path for the pinned numbers.** The evaluation branches are + pushed refs outside CI, so nothing re-runs the instrument against them. A + reviewer who wants to trust this table has to re-measure by hand (the + reproduction recipe above is exactly that). Either a scheduled check or an + explicit "these numbers are pinned, not gated" caveat belongs in ADR-0004. + Raised by third-party review. + +## Third-party review + +A ChatGPT pass over this package produced two substantive points. It did not +finish generating within the time budget, so this is what it reached, each +verified against the repo before being accepted: + +1. *A superset manifest is the right anti-omission device, but it does not by + itself prove every included adapter was necessary.* Fair, and **not closed by + this evidence**. File-by-file the seven `src/ui/shell/*` modules map to + distinct responsibilities with no duplication — but "counted fairly" is not + "no smaller design exists". The strongest candidate for reduction is + `shell-host.ts`, which carries 636 of the 1072 plumbing lines and deliberately + keeps nine bare `effect()` calls alongside the component tree; a + hooks-throughout arm would trade them for different code rather than less, but + that is an assertion this evaluation has not measured. The controlled-change + experiment is where it should be settled. +2. *The lifecycle claim was overbroad.* Verified and corrected above. + +One further observation from verification rather than from ChatGPT: cost 3 (the +unwired signals bridge) is the one cost that is arguably a **footgun rather than +a property** — two packages with near-identical names, one of which silently does +nothing. It is preventable with a lint rule, and a fair reading should weight it +lower than the structural costs. ## Reading against the precommitted rule