| name | markout-composite-cells-cards |
| version | 0.35.2 |
| description | Use when a single value must render dense in Markdown but decompose into typed columns in TSV/JSONL — before/after changes, fractions, shares, percentages, segment breakdowns, deltas and goal polarity — including MarkoutNumberFormat for Change<V> counts — or when building metric / role-matrix / verdict cards with threshold-based MarkoutEmphasis (Change<V>, Fraction, Share, Percent, Segments, Delta/Goal, MetricChange<T>, MultiSourceRow, Verdict). This is Markout's most advanced tier. Don't decompile the assembly or web-search the API — these composite types are here. |
Composite cells & cards — one declaration, dense cell + decomposed columns
Composite cells are data-only scalar properties. With FieldLayout.Table (default) each becomes
a row: Markdown renders a dense human value, and TableFormatter (TSV/JSONL) decomposes the same
cell into typed columns — no pre-stringifying, one declaration serves both. This replaces
pre-formatting numbers into strings (which loses the machine-readable form).
Routing gate: if the task requires TSV, JSONL, plain/pretty/ANSI output, or a runtime format
switch, also invoke markout-output-formats before coding.
Required setup
Markout has no reflection fallback. Every report needs a partial context registering each model
type and a Serialize call that passes it. [MarkoutSerializable] is optional customization, not
a prerequisite for registration. When the report needs an H1, use a scalar title property with
[MarkoutSerializable(TitleProperty = nameof(Title))].
using Markout;
[MarkoutContext(typeof(QualityCard))]
public partial class QualityCardContext : MarkoutSerializerContext { }
MarkoutSerializer.Serialize(card, Console.Out, QualityCardContext.Default);
Composite cell primitives
| Shape | Dense Markdown | Decomposed columns |
|---|
Change<V> + [MarkoutDelta(Delta.Percent)] | 98555 → 61190 (−38%) | before, after, delta_pct |
Fraction(count, total) | 24/24 | count, total |
Share(value, whole) + [MarkoutUnit("s")] | 103s (93%) | value, pct |
Percent(part, whole) | 93% | pct |
Segments(new Segment(l, v), …) | 21/171/236 | one column per label |
Change<Fraction> / Change<Segments> | 24/24 → 24/24 | {sub}_before / {sub}_after |
[MarkoutSerializable]
public class QualityCard
{
[MarkoutPropertyName("tasks correct")]
public Change<Fraction> TasksCorrect { get; set; }
[MarkoutPropertyName("Session IET"), MarkoutDelta(Delta.Percent)]
public Change<long> SessionIet { get; set; }
public string? Verdict { get; set; }
}
Change<V> is named Change (NOT Comparison — collides with System.Comparison<T>).
- A zero denominator renders
—, never NaN/Inf.
Change<V> nests over composites: Change<Fraction>, Change<Share>, Change<Segments>.
Delta and goal — derived, not hand-coded
[MarkoutDelta(Delta.Percent|Absolute|Multiple)] appends the signed change to a numeric
Change<V>: (−38%), (+120), 15 → 5 (3× fewer).
[MarkoutDeltaNoun("solved")] adds a caller noun: 4/6 → 6/6 (+2 solved).
[MarkoutNumberFormat("N0")] applies one .NET numeric format to scalar Change<V> operands and
an absolute/noun delta: 165 → 1,168 (+1,003). Structured TSV/JSONL decomposition stays numeric
and ungrouped. Keep the property typed; do not preformat the operands or delta.
[MarkoutGoal(Goal.Higher|Lower)] makes Markout derive two columns — a structural direction
(increased/decreased/introduced/resolved/unchanged) and a goal-applied status
(good/bad/neutral) — so you don't hand-code ceiling/floor polarity. Goal.Higher, 0.001
treats sub-threshold movement as unchanged. In Markdown the polarity word renders inline:
98555 → 61190 (−38%, good).
Structured output
var jsonl = new MarkoutWriterOptions { TableMode = MarkoutTableMode.Jsonl, JsonTypedValues = true };
MarkoutSerializer.Serialize(card, Console.Out, new TableFormatter(), QualityCardContext.Default, jsonl);
JSONL is heterogeneous (each record only its cell's keys); TSV keeps a uniform column union
(absent cells blank). JsonTypedValues = true emits numbers as JSON numbers; without it, all strings.
When a composite is a column of an element table (List<T> with a composite property), TSV/JSONL
decompose it into {column}_{sub} columns (score_before, bugs_status); Markdown keeps the dense
cell. [MarkoutIgnoreColumnWhen]-hidden columns drop from the decomposition too.
Card shapes (declare a list; the generator picks the layout)
List<MetricChange<T>> — a gated metric per row. MetricChange<T>(Name, Before, After, Target?, TargetLabel?, Status, StatusLabel?), T : struct numeric (a composite T is compile error
MARKOUT005). Set { Goal = Goal.Higher|Lower } to derive direction/status; Markdown inlines
the status word and a goal marker on the label (Failures (-)), dropping the Status column.
List<MultiSourceRow> — a role matrix. MultiSourceRow(label, params Source[]),
Source(role, IMarkoutCell? value); roles pivot to columns in Markdown (absent role → -), JSONL
emits {role}_{side}_{field} keys. Scalars work: new Source("baseline", 2); Source.Text(role, s).
Set the label header with [MarkoutLabelHeader("Metric")]. To declaratively bold scalar cells
that clear a threshold in rich output, set emphasis on the row:
new MultiSourceRow(label, sources) { Emphasis = MarkoutEmphasis.AtLeast(cut) } (or .AtMost(cut)).
Plain text and TSV/JSONL ignore emphasis and preserve raw values.
Verdict(GateStatus Status, string? Label = null) — a first-class verdict cell; use as a Source.
[MarkoutSection(Name="…", IncludeSectionInStructuredRows = true)] prepends a section column to
TSV/JSONL — multiplex several card sections into one stream. Decomposition keys are snake_case.
Guardrails
- Hand Markout already-correct values — composites derive only intrinsics (delta, percent,
direction/status); Markout does not aggregate or bind external data.
- Don't pre-stringify numbers; use the composite so the TSV/JSONL form stays typed.
- Put
[MarkoutIgnoreInTable] on card lists only when nested inside another table.