Repository navigation
CUI 1.4: the instruments, CUI's own documentation in IntelliSense, and a faster sync runbook - #20
Merged
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
c232b490306ec507fcd9ec88f5dcsync-cuitocui-sync, and give the runbook a faster start1. CUI 1.4 (
5a9c12b)DotChart,EventTrack,ReadoutandDotDisplayreach C#, on a new gallery page (29). The generator learned record arrays,doublearrays 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 isDotCharttakingReadOnlySpan<DotSeries>, the way every other chart takes its series.Seven public
…Countfields 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:cui_button_exDotChartOpts.Unitconst char * unitBlameOptscui_blame_optsTwo things are never a summary, and
ShippedDocTestsfails 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 includingui.CodeEditorandui.Gantt).The tests read the summaries through their XML nodes rather than
XElement.Value, which silently drops<paramref>and<see>— and they assert onCuiSharp.xml, the file an IDE reads, not on the generator's own output.3. Override holes closed
Each failed silently before:
capacityParamshad no entry inValidateOverrides(a typo'd array name passed and left a count meant for nothing);PrimitiveElementspelled aboolelementbytewhile the span saidbool, which would have emitted an uncompilableCopyTo;ReadBesideparsed a wrapped declaration's first line twice.4.
cui-syncRenamed from
sync-cui, and restructured for a shorter start: 162 lines of procedure, with the binding rules and the error→override table inreferences/bindings.mdand the traps inreferences/traps.md, each read only when the runbook points at it. Newscripts/sync-status.ps1answers "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 inREADME.mdandbindings/README.mdstill matchcoverage.txt. It exits 1 on aPENDINGor 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 cleantools/regen.ps1 -NoNative -NoDiff— no diff, 15 generator contract checks (one new),export-api: no difftools/api-surface.ps1— reviewed by set difference: 8 removals, all four capacity parameters(
CurveEditor,GradientEditor,KvEditor,QueryBuilder, in both their extension and staticform); 79 additions. Seven friendly
…Countfields also went, and a line-set difference cannot seethose — other structs still carry identically-shaped
field Int32 …Countlines — so they werechecked per struct instead.
tools/verify-template.ps1— 83.5 s, pack → new → build → test → AOT →dotnet cuiIL2xxx/IL3xxx; every shape inside itsSIZE.mdbudgetsync-status.ps1exits 0Review notes
docs/API-SURFACE.txtis the approval file — review it by set difference, not by reading. Note theblind spot above: a field whose shape exists elsewhere in the surface does not show as removed.
bindings/coverage.txt: 496 functions, 0PENDING, 175MANUALeach naming its member and file.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.TheEventTrackSortsAndClampsTheEventsItWasGivenpins it.