Skip to content

CUI 1.4: the instruments, CUI's own documentation in IntelliSense, and a faster sync runbook - #20

Merged
albertwoo merged 4 commits into
mainfrom
feat/cui-1.4-sync
Nov 12, 2026
Merged

albertwoo merged 4 commits into
mainfrom
feat/cui-1.4-sync

Conversation

@albertwoo

Copy link
Copy Markdown
Contributor

What this does

CUI 1.4 shipped the instrument components and documented itself in the headers. This takes both, and then fixes what the first pass left in the documentation it carried.

Four commits, in order:

c232b49 Sync to CUI 1.4 and draw the instruments
0306ec5 Carry CUI's documentation out to the C# developer
07fcd9e Take the last junk out of the documentation, and close three override holes
c88f5dc Rename sync-cui to cui-sync, and give the runbook a faster start

1. CUI 1.4 (5a9c12b)

DotChart, EventTrack, Readout and DotDisplay reach C#, on a new gallery page (29). The generator learned record arrays, double arrays and in-place primitive arrays, and a new override: a capacity is the array's length, so it leaves the signature and the wrapper passes .Length — two numbers for one buffer is a buffer overrun the type system cannot see. That is the only public surface this release removes (CurveEditor, GradientEditor, KvEditor, QueryBuilder, EventTrack); the only one it adds beyond the new components is DotChart taking ReadOnlySpan<DotSeries>, the way every other chart takes its series.

Seven public …Count fields also went, derived from their array instead.

2. Documentation

The generator used to read one of the five places CUI writes documentation: the comment above a declaration. It now reads all of them — above the declaration, beside it (CUI_API void cui_spacer(...); /* flexible space */, which libclang does not attach), above the definition in the C source, the paragraph above the options struct, and the paragraph above an item record or enum when the widget is the only reader of it. Measured on the artifacts a consumer sees:

before after
struct fields documented only by their C type 2001 786
wrapped functions whose summary is only cui_button_ex 206 94
DotChartOpts.Unit const char * unit after values in the readout, e.g. "MG"
BlameOpts cui_blame_opts Source with who last changed each line…

Two things are never a summary, and ShippedDocTests fails if either reaches the package's XML: a section title bar (---- Terminal ----, which libclang attaches to the declaration beneath it) and a note CUI writes to its own next reader (left to right in any interface: see cui__keep_ltr, which had become the summary of fifteen widgets including ui.CodeEditor and ui.Gantt).

The tests read the summaries through their XML nodes rather than XElement.Value, which silently drops <paramref> and <see> — and they assert on CuiSharp.xml, the file an IDE reads, not on the generator's own output.

3. Override holes closed

Each failed silently before: capacityParams had no entry in ValidateOverrides (a typo'd array name passed and left a count meant for nothing); PrimitiveElement spelled a bool element byte while the span said bool, which would have emitted an uncompilable CopyTo; ReadBeside parsed a wrapped declaration's first line twice.

4. cui-sync

Renamed from sync-cui, and restructured for a shorter start: 162 lines of procedure, with the binding rules and the error→override table in references/bindings.md and the traps in references/traps.md, each read only when the runbook points at it. New scripts/sync-status.ps1 answers "where does this sync stand" in one command — pinned tag vs newest, version drift across the three places it lives, the coverage totals, and whether the numbers quoted in README.md and bindings/README.md still match coverage.txt. It exits 1 on a PENDING or a stale number; run against this branch it found nine stale numbers, all corrected here.

Verification

Every gate ran against this tree, and printed its own success line:

  • dotnet build -warnaserror, dotnet test — 1 002 tests (974 + 28), format clean
  • tools/regen.ps1 -NoNative -NoDiff — no diff, 15 generator contract checks (one new), export-api: no diff
  • tools/api-surface.ps1 — reviewed by set difference: 8 removals, all four capacity parameters
    (CurveEditor, GradientEditor, KvEditor, QueryBuilder, in both their extension and static
    form); 79 additions. Seven friendly …Count fields also went, and a line-set difference cannot see
    those — other structs still carry identically-shaped field Int32 …Count lines — so they were
    checked per struct instead.
  • tools/verify-template.ps1 — 83.5 s, pack → new → build → test → AOT → dotnet cui
  • AOT gallery 2 918 400 B, zero IL2xxx/IL3xxx; every shape inside its SIZE.md budget
  • frames 0.83 ms mean, p50 0.70, p95 1.16, 0 B allocated per frame
  • sync-status.ps1 exits 0

Review notes

  • docs/API-SURFACE.txt is the approval file — review it by set difference, not by reading. Note the
    blind spot above: a field whose shape exists elsewhere in the surface does not show as removed.
  • bindings/coverage.txt: 496 functions, 0 PENDING, 175 MANUAL each naming its member and file.
  • The one behaviour worth a second look is the in-place array copy-back: a primitive array is copied back every frame even when the override carries changedWhen, because native sorts and clamps in place and the index it reports refers to its order. Only the record conversion is gated, because that is the one that allocates. InstrumentTests.TheEventTrackSortsAndClampsTheEventsItWasGiven pins it.

binwenw added 4 commits November 12, 2026 11:41
CUI 1.3.1 (e8688b8) -> 1.4.0 (5a9c12b). Four functions, eight structs, and
three things the generator had never had to express.

The four components are the instruments: a dot-matrix chart with annotations
drawn over it, an event track whose dots are dragged, added and removed in
place, a readout, and a dot-matrix display. Three generate; DotChart is
hand-written, because a `cui_dot_series` carries its samples as a bare
pointer and their number belongs to the call rather than to the series, so it
can never be a self-describing record. Its wrapper builds the native array in
the frame arena from managed DotSeries, as the line and bar charts already do,
and pads a short series with zero rather than reading past its end.

What the generator learned, all of it from this sync:

- A record array inside an option struct (CUI 1.4's `annotations`). The
  element struct needs a recordNames entry before the field maps, and the
  count beside it now derives from the array like a string array's does, so
  seven public `...Count` fields became hidden derivations.
- A `double` array. CUI's time axes are in hours, not floats, and neither
  IsDoubleArray nor the raw layer's pointer heuristic knew about it; the
  plural check in Parameter() now consults the overrides, because `double
  *times` is non-const and plural and still the array the widget edits.
- An in-place primitive array. Copying it back only when the widget reports a
  change is wrong: native sorts and clamps the caller's array before it
  draws and reports neither as a change, so the wrapper would hand back a
  sort order it does not have. Primitives now always copy back; only the
  record conversion, which allocates, stays gated.

One rule came out of it that was not upstream's idea but the API's: a
capacity is the array's length, so it leaves the signature (capacityParams).
Five widgets took both, and a capacity past the end of the span was a buffer
overrun the type system could not catch. CurveEditor, GradientEditor,
KvEditor and QueryBuilder lose the parameter too, which is the only public
surface this sync removes.

CoverageAccountTests now fails on a PENDING line in coverage.txt. A pending
option struct is skipped whole, which is how this sync first presented
itself: the build broke on a missing ChartOpts forty files from the cause.

Gallery page 29 draws all four instruments on one screen: three cups of
coffee as a dot-matrix chart with the LIMIT annotation, the same five cups on
the track below it, and the readouts that answer the question.

Gates: build -warnaserror, 994 tests, format, regen -NoDiff with 14 contract
checks and no diff from export-api, api-surface reviewed by set difference
(15 removals, all intended), verify-template, AOT 2 918 400 B against the
2.95 MB budget with 0 IL2xxx/IL3xxx, frame 0.82 ms p50 0.68 / p95 1.10 /
p99 1.33 with 0 B allocated, size-report and its budget check.
CUI 1.4 documented itself in the headers — every field, every constant that
needs it, a description above every component — and none of it reached a
wrapper. The generator read one of the four places upstream writes: the
comment above a declaration.

All four are read now, in order: above the declaration, beside it
(`CUI_API void cui_spacer(...);   /* flexible space */`, which libclang does
not attach to the declaration and is read off the source line), the comment
above the definition in the C source, and — for a widget whose function has
none — the paragraph above its options struct, because that is the component's
description. A comment that is only a struct's own note ("Options for every
container.") is not used as a call's summary.

What that buys, measured on the artifacts a package consumer sees:

- 1 200 struct and option fields that said `const char * unit` or
  `_Bool no_grid` now say what they mean (`after values in the readout, e.g.
  "MG"`). Fields in Structs.g.cs documented only by their C type: 2001 → 786.
- 88 wrapped functions gained a sentence; the ones that still say only their C
  name ("cui_button_ex") are 118, down from 206, and are the ones upstream has
  not documented.
- Every enum member CUI comments on says what it is for, so `AxisUnits.Hours`
  reads "hours shown as a time of day: 12.25 is 12:15, and 30 is 06:00 the next
  day" instead of nothing.
- An options struct opens with its component's description instead of
  `cui_split_opts`: "Resizable split view: two panes with a draggable divider."
- The raw P/Invoke layer carries the same sentence as the friendly wrapper,
  because that is where an expert lands when the wrapper is not enough.

A section's title bar is the trap: libclang attaches `---- Terminal ----` to
the declaration below it, so an options struct's summary opened with the ruler
and then the section's name. `CleanComment` drops the ruler, keeps the prose
that follows it in the same block, and refuses a block that is only a title.

ShippedDocTests asserts on CuiSharp.xml — the IntelliSense file the package
ships — rather than on the generator's output, so what is pinned is what a
developer's tooltip shows. Two of its cases fail against the previous
generated output and the banner case fails with the banner rule disabled;
both were checked that way.

Gates: build -warnaserror, 1 000 tests, format, regen -NoDiff with 14 contract
checks and no diff from export-api, api-surface unchanged (comments only), and
every checked-in generated file refreshed.
… holes

The 1.4 documentation pass left three things behind, found by reading the
shipped documentation rather than the source that generates it.

*CUI's notes to itself became our summaries.* Fifteen public functions in CUI's
sources are thin wrappers that open with `/* left to right in any interface:
see cui__keep_ltr */`, and that sentence reached `ui.CodeBlock`, `ui.JsonView`,
`ui.CodeEditor`, `ui.DiffView`, `ui.Counter`, `ui.Countdown`,
`ui.ShortcutRecorder`, `ui.WeekCalendar`, `ui.Gantt`, `ui.Terminal`,
`ui.HexView`, `ui.LogView`, `ui.OtpInput`, `ui.TimeDial` and `ui.Kbd` as their
documentation — hiding the description the header does have for most of them.
Two causes: `CommentAbove` trimmed stars before slashes, so `/* x */` came out
as `* x`, and a definition's note was preferred over the header's. Both fixed
(`CommentText`, `PublicNote`), and a note that names a `cui__` symbol is not a
sentence for an app developer, so it is never used as one.

*A component's paragraph was on its item record, not its options struct.*
libclang attaches a comment to the declaration under it, and CUI writes the
component's paragraph above the first thing a section declares — `Blame`'s above
`cui_blame_commit`, `ToolCall`'s above `cui_task_status`. Those widgets had no
summary at all. They take that paragraph now when the widget is the only
function that reads the declaration, which is what keeps `cui_viz_item`'s
Treemap paragraph off Funnel, Waffle and Bar list, who share the record. The
options struct the widget configures adopts the sentence too, so `BlameOpts`
says what Blame is instead of `cui_blame_opts`. Wrapped functions with nothing
but a C name: 118 → 94, and what is left is what upstream has not written.

*`Ui.DotChart` took `DotSeries[]`.* Every other chart takes a span of its
series, and an array parameter means a call site either allocates per frame or
keeps a field it does not want. `scoped ReadOnlySpan<DotSeries>` makes the
inline stack form work — `ui.DotChart(id, [new(values, "Caffeine", accent:
true)], count)` — which the gallery now does. Also: the clamped sample count
(`Math.Max(1, count)`) was used for the frame block but the unclamped `count`
was passed to native; both are the clamped one now.

Three override holes closed, each of which failed silently:

- `capacityParams` had no entry in `ValidateOverrides`, so a typo in the array
  name passed the generator and left a count meant for nothing. It now requires
  the reason, the parameter, the array in the same signature, and an int beside
  a pointer.
- `PrimitiveElement` spelled a `bool` element `byte` while the span was spelled
  by a second lookup that said `bool`: an in-place `bool*` array would have
  emitted a `Span<bool>.CopyTo(Span<byte>)` that cannot compile. One name for
  the element now, used for both.
- `ReadBeside` appended the first line of a wrapped declaration twice
  (`j = i` where `i + 1` was meant).

`ShippedDocTests` reads the summaries through the XML nodes rather than
`XElement.Value`, which drops `<paramref>` and `<see>` — it was asserting on
what a regex would see, not what a developer does. Eight cases now, including
`NoSummaryIsAnInternalNote` and `ADescriptionAttachedToTheItemRecordStill
ReachesTheWidget`; both fail against the previous generated output (checked by
stashing it) and pass now.

`native/build-native-container.sh` hands its uid/gid to the image and chowns
`runtimes/` on the way out, so a local run no longer leaves files the next
non-root build cannot overwrite.

Gates: build -warnaserror, 1 002 tests, format, regen -NoDiff with 15 contract
checks (one new) and no export-api diff, api-surface reviewed (one intended
line: `DotChart`'s span), template 83.5 s, AOT 2 918 400 B with 0 IL warnings,
frame 0.83 ms / 0 B per frame, size report inside every shape's budget.
The skill was named after the thing it syncs rather than after the job, which
made it the odd one out beside `cui-dotnet`. It is `cui-sync` now, everywhere:
the registration in `.pi/settings.json`, the project AGENTS.md, the recorder's
remark, and the coverage test's failure message.

The content changed more than the name. An agent reading the old file paid for
179 lines to learn a five-step loop, and then went grepping the repository for
the four numbers the READMEs quote, for which version the two files agree on,
and for how far behind the pin was. Three things now happen instead:

`skills/cui-sync/scripts/sync-status.ps1` answers all of it in one call —
pinned tag against upstream's newest, version drift between Directory.Build.props
and the template, the coverage totals, and whether the numbers in README.md and
bindings/README.md still match coverage.txt. It exits 1 on a PENDING or a stale
number, so a sync has a check rather than a claim. Run against this tree it
found nine stale numbers, including README.md still advertising "all 492 CUI
1.3 functions" and nine pages of gallery; both are now the 1.4 truth, which is
496 functions, 170 options, 104 records, 277 wrappers, 29 pages.

`SKILL.md` is 162 lines and front-loaded: the loop, the exit criteria (what
GENERATED/MANUAL/EXCLUDED/RAW-SKIP mean and who decides), the four things that
follow an API change, the gates, "done means", and the hard rules. Everything
that used to make it long sits behind a trigger:

- `references/bindings.md` — the conventions a wrapper follows, the "one length,
  never two" rule with the override that enforces it, the message → fix table
  (now with the four decisions that are easy to get wrong), naming, and how the
  documentation pipeline works. Read when bindgen stops.
- `references/traps.md` — the generator truths, the C ergonomics that look like
  binding bugs, the PowerShell traps (including `-like` being case-insensitive,
  which the new script hit), gates that fail for a reason that is not yours, and
  what is true by design so nobody "fixes" it. Read when something is odd.

Two things were added because they were missing, not because they were long:
`capacityParams`, `recordFieldCounts` and `returnTypes` were absent from the
error table even though the last two syncs used them, and the doc pipeline
(five sources, two things that are never a summary) had no written rule at all.

Gates: build -warnaserror, 1 002 tests, format, regen -NoDiff with 15 contract
checks and no export-api diff, and both scripts run (diff from its new path,
status exits 0 on this tree and 1 with a PENDING injected).
@albertwoo
albertwoo merged commit da255c3 into main Nov 12, 2026
24 checks passed
@albertwoo
albertwoo deleted the feat/cui-1.4-sync branch November 12, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant