diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c6eacb..6ff183fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,63 @@ 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 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/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/docs/design/577/S2-EVIDENCE.md b/docs/design/577/S2-EVIDENCE.md new file mode 100644 index 00000000..7a9582bd --- /dev/null +++ b/docs/design/577/S2-EVIDENCE.md @@ -0,0 +1,287 @@ +# #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** — 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 + +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 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: + +- 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; +- **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 + +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/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/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` 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 + } + } + ] +} 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); + } + }); +});