Skip to content

Commit 367bcb5

Browse files
dkackmanclaude
andcommitted
Merge branch 'ui-stabilization/phase-0' into develop
UI stabilization Phase 0: ratchet and baseline, .svelte counted in the gate report, U1-U5 fixed, twin pins, seam-map rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents 12aa920 + a6928db commit 367bcb5

30 files changed

Lines changed: 826 additions & 119 deletions

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@ jobs:
6969
cache-dependency-path: ui/package-lock.json
7070
- run: npm ci
7171
- name: Format check
72-
run: npx prettier --check src e2e *.ts *.js
72+
run: npx prettier --check src e2e scripts *.ts *.js
7373
- name: Lint
7474
run: npm run lint
75+
- name: Architecture ratchet
76+
run: npm run metrics -- --check ../docs/stabilization/ui/baseline.json
7577
- name: Type check
7678
run: npm run check
7779
- name: Unit tests

‎docs/ARCHITECTURE.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,7 @@ one to open.
150150
| Concept | Owner | Rule | Enforced by |
151151
| --- | --- | --- | --- |
152152
| The UI reads engine fields | `ui/src/lib/plan.ts`: `describePlan`, `ui/src/lib/results.ts`: `sectionBySubfolder` | The UI reads `plan`, `version` and `subfolder` as fields the server sends and derives nothing of its own, and it never sends `acknowledged_cost`. | `ui/src/lib/plan.test.ts`, `ui/src/lib/results.test.ts` |
153+
| Reference prefixes in the UI | `ui/src/lib/references.ts` | The only module in `ui/src` that spells a reference prefix; each equals `dw/references.py`'s. | `tests/test_ui_twins.py::test_the_ui_spells_every_reference_prefix_the_engine_does`; `prefix_literals` in `ui/scripts/arch-metrics.mjs` |
154+
| Output kinds on the job page | `dw/server/outputs.py`: `MEDIA_KINDS`, `output_kinds` | The page renders an output by the `output_kinds` the job detail carries, never by its extension. | `tests/test_server.py::test_a_job_reports_each_output_files_media_kind`, `ui/src/lib/pages/JobPage.test.ts` |
155+
| UI copies of engine rules | the constants `tests/test_ui_twins.py` reads | A list the UI must hold equals its owner, read from the TS source; a rule tested on both sides reads one case file in `tests/fixtures/`. | `tests/test_ui_twins.py` |
156+
| UI architecture ratchet | `ui/scripts/arch-metrics.mjs` | No metric rises above `docs/stabilization/ui/baseline.json`. | the `ui` job in `.github/workflows/ci.yml`; `npm run preflight` |

‎docs/stabilization/ROADMAP.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,9 @@ The gates measured `dw_mcp` and `ui/src` but never read them. Two follow-up
827827
surveys do: [mcp-assessment.md](mcp-assessment.md) and
828828
[ui/ASSESSMENT.md](ui/ASSESSMENT.md). Every gate's UI SLOC row above counts only `.ts`:
829829
pygount has no lexer for `.svelte` and reports those files as 0 lines, so
830-
`ui/src` is about 15,500 raw lines, not the ~2,270 code lines shown.
830+
`ui/src` is about 15,500 raw lines, not the ~2,270 code lines shown. With `.svelte` counted (`scripts/arch_report.py`'s
831+
`svelte_code_lines`), the UI is 12,969 code lines at `stabilization-gate-4`,
832+
against 20,362 for the engine and 6,956 for the API.
831833

832834
## Working rules for the duration
833835

‎docs/stabilization/ui/ROADMAP.md‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ works on is what the earlier phases leave behind. Phase 0's plan is
2121

2222
## Decisions
2323

24-
Recorded 2026-10-01. The two marked **open** are Don's.
24+
Recorded 2026-10-01; Don ruled on both open questions the same day.
2525

26-
- **Freeze (open).** The engine pass froze features until its guardrails
26+
- **Freeze (yes, Don 2026-10-01).** The engine pass froze features until its guardrails
2727
existed. The UI equivalent: no new pages, components or UI features until
2828
gate 2; the implementer fixes tester bugs in `ui/` through existing code,
2929
and a fix that needs a new component waits or is labelled `stabilization`.
30-
Proposed rather than assumed, because the harness's tester bugs land in the
31-
UI less often than in the engine.
32-
- **Component library (open; recommended: Bits UI).** Headless, so the
30+
- **Component library: Bits UI (approved, Don 2026-10-01).** Headless, so the
3331
`app.css` token system, the achromatic chrome and the WCAG AA pairs in
3432
`ui/CLAUDE.md` stay as they are. It is native to Svelte 5 runes, covers every
3533
hand-built widget (dialog, alert dialog, popover, combobox, tooltip, toggle
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"files_over_size_ceiling": 7,
3+
"complex_functions": 14,
4+
"long_functions": 2,
5+
"import_cycles": 1,
6+
"modules_in_import_cycles": 2,
7+
"prefix_literals": 15,
8+
"a11y_suppressions": 7
9+
}

‎docs/stabilization/ui/phase-0.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
## Global Constraints
1414

15-
- Freeze, if Don confirms it (ROADMAP *Decisions*): no new UI features. Change only what a task names.
15+
- Freeze (ROADMAP *Decisions*, Don 2026-10-01): no new UI features. Change only what a task names.
1616
- The only new modules are `ui/scripts/arch-metrics.mjs`, `ui/src/lib/references.ts`, test files and `tests/fixtures/*.json`.
1717
- Every new test fails on the code before its fix. Run it and see the failure before writing the fix.
1818
- No count-pinning assertions. Baseline numbers live in `docs/stabilization/ui/baseline.json`, never in a test.

‎dw/server/jobs.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
TERMINAL_STATES,
6060
Job,
6161
)
62+
from .outputs import output_kinds
6263

6364
logger = logging.getLogger("dw")
6465

@@ -702,6 +703,9 @@ def _record_progress(self, job, event):
702703
event["files"] = self._relative_output_names(
703704
event["files"], job.spec.get("output_dir")
704705
)
706+
# A running job's page renders each output as its step ends,
707+
# before there is a manifest to classify
708+
event["output_kinds"] = output_kinds([{"files": event["files"]}])
705709
if "manifest" in event:
706710
# workflow_end carries the run's full manifest nested under this
707711
# key - it must match get_job's rendering of the same list rather

‎dw/server/outputs.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@
5656
".txt": "text",
5757
}
5858

59+
60+
def output_kinds(manifest):
61+
"""Each file a job manifest lists, mapped to its MEDIA_KINDS entry, or
62+
None for a kind the gallery does not show. A client renders an output
63+
by this rather than keeping its own extension list."""
64+
kinds = {}
65+
for entry in manifest or []:
66+
if not isinstance(entry, dict):
67+
continue
68+
for name in entry.get("files") or []:
69+
kinds[name] = MEDIA_KINDS.get(os.path.splitext(name)[1].lower())
70+
return kinds
71+
72+
5973
# The allowlist members that are not already-compressed containers -
6074
# everything else in MEDIA_KINDS deflates for about nothing, so it is
6175
# stored instead (see zip_download)

‎dw/server/routes/jobs.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@
5555
RUNNING,
5656
TERMINAL_STATES,
5757
)
58-
from ..outputs import absolute_served_url, asset_library_for_job, served_url
58+
from ..outputs import (
59+
absolute_served_url,
60+
asset_library_for_job,
61+
output_kinds,
62+
served_url,
63+
)
5964

6065
logger = logging.getLogger("dw")
6166

@@ -226,7 +231,8 @@ def get_job(request: Request, job_id: str):
226231
if job is None:
227232
raise HTTPException(status_code=404, detail="Unknown job")
228233
# a historical job is already a detail dict; a live one renders itself
229-
return job if isinstance(job, dict) else manager.describe(job)
234+
detail = job if isinstance(job, dict) else manager.describe(job)
235+
return {**detail, "output_kinds": output_kinds(detail.get("manifest"))}
230236

231237

232238
@router.get("/api/jobs/{job_id}/workflow")

‎scripts/arch_report.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import collections
1616
import itertools
1717
import pathlib
18+
import re
1819
import statistics
1920
import subprocess
2021
import sys
@@ -98,6 +99,23 @@ def extract(ref, root, into):
9899
tar.extractall(into, filter="data")
99100

100101

102+
# A line that is only a comment. A multi-line /* */ comment's inner lines
103+
# count as code: matching them by a leading '*' would also match a CSS '*'
104+
# selector, and an undercount hides growth where an overcount does not.
105+
SVELTE_COMMENT = re.compile(r"^(<!--.*-->|//.*|/\*.*\*/)$")
106+
107+
108+
def svelte_code_lines(path):
109+
"""Code lines in a .svelte file, which pygount reads as 0: every line
110+
that is neither blank nor a comment on its own. Script, markup and style
111+
all count, since all three are what a reader holds."""
112+
return sum(
113+
1
114+
for line in path.read_text(encoding="utf-8").splitlines()
115+
if line.strip() and not SVELTE_COMMENT.match(line.strip())
116+
)
117+
118+
101119
def sloc(tree):
102120
from pygount import SourceAnalysis
103121

@@ -110,7 +128,12 @@ def sloc(tree):
110128
and keep(path.relative_to(tree))
111129
and not any(part in EXCLUDED for part in path.parts)
112130
):
113-
counts[layer] += SourceAnalysis.from_file(str(path), layer).code_count
131+
if path.suffix == ".svelte":
132+
counts[layer] += svelte_code_lines(path)
133+
else:
134+
counts[layer] += SourceAnalysis.from_file(
135+
str(path), layer
136+
).code_count
114137
return counts
115138

116139

0 commit comments

Comments
 (0)