ci: add e2e tests & readiness checks#132
Open
MathiasLundgrenEY wants to merge 2754 commits into
Open
Conversation
…arning EmitWord stamps noMarkRPrInherit=true on every emitted `add r` row so the markRPr->rPr inheritance fill stays opt-out on dump->batch replay (matches the source's "run has no rFonts even though para mark does" shape). AddRun consumed the flag at line ~1587 to gate the inheritance fill, but the bare-key fallback at the bottom of AddRun never saw it on its curated allowlist. ApplyRunFormatting and TryCreateTypedChild both miss (it is not a real OOXML attribute), so every dump-emitted run landed the key in LastAddUnsupportedProps and the batch driver printed: WARNING: UNSUPPORTED props: noMarkRPrInherit Add "nomarkrprinherit" to addRunCuratedBare so the inheritance-toggle sentinel is treated as already-consumed instead of re-flagged as unknown.
…clock
Two findings from Mac PowerPoint round-trip on the modern gallery:
1. p15 -out variant: Mac PowerPoint's Effect Options writes a single
invX="1" attribute, not invX="1" invY="1" together. Setting both
makes PowerPoint silently reject the whole <p15:prstTrans> element
and play the mc:Fallback fade instead — the file opens with no
transition highlighted in the gallery and no preview animation.
Drop invY from the -out emission and the readback regex. Now matches
PowerPoint's own output byte-for-byte for direction-sensitive
presets (peelOff, airplane, origami, wind, fallOver, drape).
2. The "Cube" / "Rotate" / "Orbit" UI tiles are NOT separate elements —
they're all the same <p14:prism/> element with two boolean attrs:
bare <p14:prism/> → "Cube" (Exciting)
<p14:prism isContent="1"/> → "Rotate" (Dynamic Content)
<p14:prism isContent="1" isInverted="1"/> → "Orbit" (Dynamic Content)
"Clock" is similarly <p:wheel spokes="1"/> — a single-spoke wheel.
Wire all four as CLI tokens:
- `cube` accepted as input alias for `prism` (same bare XML, readback
stays as `prism` to preserve the OOXML element name)
- `rotate` and `orbit` are new canonical tokens, round-trip via
PrismTransition.IsContent / IsInverted
- `clock` accepted as input alias for `wheel-1` (readback stays as
`wheel-1`)
- Readback for <p14:prism> distinguishes the 3 variants by attr
combination so set transition=rotate/orbit round-trips
schemas/help/pptx/transition.json: extend enum with the 4 new tokens.
examples/ppt/transitions/transitions-dynamic.{sh,pptx}: replace the
broken prism-up / prism-right entries (prism is direction-less; the
suffix was silently dropped) with explicit prism / rotate / orbit
slides that actually round-trip.
examples/ppt/transitions/transitions-modern.{md,pptx}: regenerate with
the invX-only fix; add a "UI tiles backed by other elements" table
explaining the cube/rotate/orbit/clock cross-namespace mapping so
agents reading the modern trio know where to find those four tiles.
Tests (PptxTransitionR24Tests): 5 new round-trip cases (cube/prism/
rotate/orbit/clock). 64 transition tests pass.
…xplicit font EmitSection injected `set / docDefaults.font.latin=""` whenever the post- baseline-filter prop bag lacked a docDefaults.font key — intended to clear the BlankDocCreator-stamped Times New Roman when the source omits the slot. The baseline-skip loop directly above suppresses emits for keys whose source value matches the blank's value. When the source DOES carry an explicit `docDefaults.font` matching the blank (Calibri vs. Calibri) the skip fires and the prop is absent post-filter, which the empty-clear injection then misread as "source has no font" and stamped an empty string into the batch. Replay set the empty value, clearing the source font (rFonts ascii/hAnsi became blank) and dropping the document's intended typeface on round-trip. Guard the injection on the raw source Format presence instead of the filtered prop bag — only inject the empty clear when the source truly carries no docDefaults.font[.latin]. Existing TNR-clear path for source documents lacking the slot is preserved.
pptx dump emits `name` and `lang` on textbox add items (AddShape consumes
both — name defaults to "TextBox {N}" via cNvPr, lang stamps drawingML
rPr/@lang on the first run). pptx/textbox.json declared neither, so the
emitted batch tripped schema drift checks even though the handler accepted
the values.
textbox does not extend `_shared/shape`, so the `name` declaration in the
shared base does not flow through. Add `name` and `lang` directly on
pptx/textbox.json mirroring the pptx/shape.json declarations.
pptx dump emits zorder on shape/textbox add items: AddShape (Add.Shape.cs line 737) and ApplyShapePropsCore (Set.Shape.cs line 743) both consume zorder/z-order/order and reposition the shape in the slide shape tree. Schema declared zorder as get-only on both pptx/shape.json and pptx/textbox.json, so the emitted batch tripped schema-drift checks. Update pptx/shape.json to declare zorder add=true / set=true with the aliases (z-order, order) and an example. The matching textbox.json declaration was already updated in the previous commit when name/lang were added.
…files The .md walkthroughs are user-facing examples documentation focused on CLI usage; the OOXML element names, namespace prefixes, mc:Choice/ mc:Fallback wrapper anatomy, and prst attribute mechanics that crept in during development belong in source-code comments, not here. Rewrite the affected passages to describe the same behavior in CLI terms: - "OOXML representation" code blocks (modern, morph) — removed. - "Stored as <p:transition spd=...>" — replaced with "Get surfaces the value as the read-only transitionSpeed format key". - "<p14:reveal/> element had no dir attribute" / "p15 namespace" / "CT_OptionalBlackTransition" — replaced with plain-language descriptions of the user-visible behavior. - transitions-modern.md's "UI tiles backed by other elements" table drops the OOXML column, keeps just (UI name → CLI token). Net effect: the same agent or user reading these .md files now learns which CLI token to use and what to expect from Get/Preview, without the implementation-detail noise.
`get /slide[N]/shape[K]` bubbled the first run's size and color up to
the shape-level Format dictionary unconditionally. When the textbox
held runs with mixed formatting (e.g. run[1] red 28pt, run[2] blue
14pt), the shape-level Format reported only run[1]'s values, so an
agent inspecting `Format["color"]` couldn't tell whether the textbox
was uniformly red or actually mixed.
Walk every run in the text body once before emitting; if the size or
color set has more than one distinct value, suppress that key from
shape-level Format. `ContainsKey("size")` / `ContainsKey("color")` is
now the contract for "this is a meaningful summary".
Run-level Get is unchanged — `/slide[N]/shape[K]/paragraph[P]/run[R]`
still returns the per-run value. Set on the shape path also stays as
a broadcast (writes the same value to every run) so the asymmetry
between input and output stays explicit.
Heuristic stays narrow: only `size` and `color` are checked. Font /
bold / italic / underline etc. continue to surface the first run's
value — they have lower mixed-formatting incidence in practice and
expanding the probe risks suppressing keys users expect to see.
`add /slide[N] --type slide --prop title=T --prop text=B` emitted two shapes asymmetrically: the title got `<p:ph type="title"/>`, but the content was a bare textbox with no placeholder reference. On Get one came back as `type=title phType=title isTitle=true`, the other as a plain `type=textbox` with no phType. The content side also never inherited layout styling because it wasn't bound to any layout slot. Both shapes are auto-emitted in response to the same `add slide` shortcut and bind the same way conceptually — populate a layout slot with caller text. Tag the content shape with the matching marker so the two paths produce symmetric on-disk shapes and symmetric readback. CreateTextShape gains `placeholderType` / `placeholderIndex` optional parameters; the title path keeps its isTitle flag (with the implicit title placeholder), and the content path now passes `placeholderType: PlaceholderValues.Body, placeholderIndex: 1`. NodeBuilder's existing placeholder classifier handles the Get-side readback — no further changes there. Existing callers that didn't need a placeholder (everything except Add.Slide's content path) keep the previous default-null behavior. Side effect: `/slide[N]/placeholder[K]` now finds the content shape alongside the title, so direct slot addressing works without needing the `/slide[N]/shape[K]` form.
…d depth Two doc-only schema clarifications. pptx/slide.json — layout slot population Reading `help slide --json` showed `layout` documented as a metadata field with no guidance on what happens to the layout's placeholder slots. The actual contract: `layout` is metadata only — slots are NOT auto-materialized. Population paths are the `--prop title=…` / `--prop text=…` shortcuts (which emit shapes with <p:ph type="title"/> / <p:ph type="body" idx="1"/> markers respectively) and the explicit `--type placeholder --prop phType=…` route. Surface those three paths in the layout description, and cross-reference the OOXML markers in the `title` / `text` property descriptions so on-disk shape and Get readback (type=title vs type=placeholder phType=body) are predictable. pptx/table.json — default read depth `get /slide[N]/table[K]` returned table-level Format plus row stubs but empty cells, while `get /slide[N]/shape[K]` returned paragraphs and runs in one call. Reading the schema didn't reveal that `--depth N` already controls how deep Get descends. Document the knob in the table's top-level note: depth=1 is the row-stub summary; depth=2 materializes cells; direct addressing via `get .../row[R]/cell[C]` is always available. The default stays at 1 because large tables (e.g. 50x10) at depth=2 dominate the response.
`officecli add file.pptx --type slide` (no parent argument) raised the default System.CommandLine error "Required argument missing: <parent>" with no hint that pptx slide takes '/', docx takes /body, xlsx takes /Sheet1. New users had to guess the per-handler convention or read source. Expand the parent argument's Description so the per-handler examples appear in `officecli add --help` output. Also flag the zsh single- quote habit for paths with brackets, since unquoted /slide[1] gets glob-expanded by zsh and bash.
zsh treats `[N]` as a glob character class; bash with extglob also expands unquoted brackets. Copy-pasting an example like `--prop from=/slide[1]/shape[1]` from `officecli help` output into a zsh shell yielded `zsh: no matches found`, and the user had to recognize the brackets needed quoting. Wrap the path token (not the whole example string) in single quotes across the four `examples` arrays where bracketed paths appeared: schemas/help/pptx/connector.json — from / to shape refs schemas/help/pptx/moderncomment.json — parent comment ref schemas/help/pptx/picture.json — link=slide[N] action token schemas/help/xlsx/slicer.json — pivotTable ref `note` / `path` / `positional` fields (which document the canonical form, not copy-paste material) stay unquoted — they're definitions, not shell-ready strings.
Bug: EmitChart never emitted a `set /slide[N]/chart[K]/axis[@ROLE=ROLE]` row. Chart-level shortcuts (axisMin/axisMax/axisTitle) only target the primary value axis, so any per-role override — especially role=value2 on a secondary axis — was silently lost on dump→batch replay. role=value2 min/max/title all dropped; primary axis tick/format tweaks not covered by chart-level keys also dropped. Root cause: EmitChart only emitted a single `add chart` row with chart-level props. The Set side accepts axis sub-paths, but the dump side never walked them. Fix: After the add row, iterate {category, value, value2, series}. For each role, Get the axis sub-path, filter out BuildAxisNode's synthetic defaults (visible=true, majorGridlines true/false matching AddChart's seeded state, majorTickMark=out, crosses=autoZero, etc.), and emit a set row when any non-default key remains. Missing roles (pie / doughnut have no axes) are silently skipped via the Get-throws catch. New behavior: dump now round-trips per-axis min/max/title/tick/gridline overrides. Secondary value axis (role=value2) finally survives a replay. Chart types without the axis silently skip — no spurious rows for pie etc.
Bug: dump only emitted slideWidth / slideHeight on the `set /` row. firstSlideNum, rtl, show.loop, show.narration, show.useTimings, print.what, print.colorMode, compatMode, removePersonalInfo were all silently dropped on dump→batch replay. Root cause: EmitPresentationProps hard-coded the slide-size pair and ignored every other key TrySetPresentationSetting (Set.Presentation.cs) accepts. Fix: Iterate a curated allowlist (PresentationEmitKeys) mirroring the setter's accepted bare keys, plus a special `direction → rtl` rewrite because Get emits `direction=rtl` while the setter case key is `rtl`. Empty strings are dropped (the standard "value matches OOXML default, PopulatePresentationSettings omitted it" signal). New behavior: presentation attribute set survives round-trip. Defaults still produce zero items so unchanged decks don't gain spurious rows.
Bug: a slide with [group at zorder=1, shape at zorder=2] (group behind, shape in front) replayed as [shape, group] = [1, 2] — z-order flipped because the group's add row carried no zorder prop and AddGroup defaults to append. Root cause: EmitGroup ran FilterEmittableProps on the direct-Get of the group path, which strips zorder. The slide-enumeration NodeBuilder branch that surfaces zorder fires only when the group appears as a *child* of the slide, not on a direct Get of the group's own path. Fix: after FilterEmittableProps, look up zorder on the source grpNode (passed in by the slide walker) and preserve it. Schema follows suit — pptx/group.json declares zorder add/set/get=true (was all false), matching the AddGroup / SetGroup handler behavior that already accepted the prop.
Schema-emit drift: handler-side add/set already accepted these keys, but the help schema either omitted them entirely or marked them add=false. dump emit (which mirrors what add/set accept) produced JSON whose property set was not reflected in `officecli help <type>` output — agents inspecting the schema as the canonical contract had no way to learn the key existed. Aligned declarations: _shared/chart: dispBlanksAs, varyColors flipped to add=true (set/get were already true; emit went into the chart add row, schema lagged). pptx/chart, pptx/table: declare zorder add/set/get=true (handler accepted via ApplyShapePropsCore; emit started carrying it after the R6 shape schema alignment). pptx/paragraph: declare bold, italic, color, size, lang. dump's single-run fold path collapses a paragraph's only run's character props onto the paragraph (defRPr); the schema needs to declare them so the resulting set row's keys validate. docx/comment: declare runStart add=true. dump emits this when the comment range starts inside a paragraph (after the Nth run) so replay can restore the intra-paragraph anchor. docx/table-cell: declare skipGridSync set=true. The R3 set-side fix recognized it; the schema follows. No handler changes — pure schema declarations matching long-standing handler behavior.
…hor crossBetween before majorUnit Radar and bubble dump→batch replays produced files PowerPoint refused to open. Two schema-order bugs in chart setters: 1. The varyColors setter anchored the new element after Grouping / BarGrouping / BarDirection only. Radar and scatter chart types have no Grouping element; their schema prefix is `radarStyle, varyColors, ser*` / `scatterStyle, varyColors, ser*`. The fallback PrependChild landed varyColors before radarStyle / scatterStyle, which the OOXML validator rejects as an unexpected child. 2. The axis crossBetween setter used AppendChild on the value axis. The CT_ValAx tail is `crossAx, crosses?, crossesAt?, crossBetween?, majorUnit?, minorUnit?, dispUnits?, extLst?`, so AppendChild lands crossBetween after majorUnit when the chart builder already emitted majorUnit. PowerPoint silently rejected the resulting file. Extend the varyColors anchor chain to also accept RadarStyle / ScatterStyle, and re-anchor crossBetween after crossesAt / crosses / crossAx so it precedes majorUnit regardless of emit order.
ChartHelper.Reader emitted Format[\"transparency\"] in raw OOXML alpha units (e.g. 70000 for 30% opaque). The series transparency setter expects 0..100 percent and converts to alpha = (100 - transparency) * 1000. Replaying a dumped chart fed 70000 to the setter, producing a negative alpha element that fails the SrgbClr/Alpha MinInclusive=0 schema constraint and breaks PowerPoint open. Emit transparency as a 0..100 percent computed from (100000 - alpha) / 1000, matching the setter input contract. The companion `alpha` field still mirrors the raw OOXML units for round-trippable color-with-alpha inputs.
…eeds no run AddPlaceholder seeds the txBody first paragraph with <a:endParaRPr> only — no <a:r> element. The batch emitter walked the source paragraph and treated the seeded paragraph the same as a shape/textbox seed (which DOES include one empty <a:r>), so the first run was emitted as `set .../paragraph[1]/run[1]`. When the source paragraph carried run-only attrs (e.g. lang) without text on the paragraph-level collapse, replay targeted a non-existent run and the batch step failed. Thread a `seededFirstParaHasRun` flag through EmitTextBody / EmitParagraph and set it false for the placeholder caller. When the seeded paragraph has no run, both the multi-run path and the single-run-collapse runOnly follow-up switch from `set .../run[1]` to `add run` so the run is materialized on demand. Shape/textbox callers keep the existing rewrite-the-seed behavior to avoid the +1 phantom-run drift on round trips.
EmitRun and EmitFirstRunAsSet routed run-internal `link=slide[N]` through DummyCtxStripSlideJump, which silently removed the link prop on the assumption the shape-level emit had already deferred it. But a shape can carry both a shape-level link and per-run slide jumps on its text, and the shape-level defer only owns the shape's hyperlink. The run's slide-jump was lost on every dump, leaving the replayed run with no hyperlink at all. Thread SlideEmitContext through EmitTextBody → EmitParagraph → EmitRun/ EmitFirstRunAsSet and add DeferRunSlideJumpLink — the run analogue of DeferSlideJumpLink. Slide-jump links get queued onto ctx.DeferredLinks with the run's positional path as the target, joining the existing end-of-batch flush so the cross-slide reference resolves once every target slide is materialized. External URLs and named actions stay on the inline `add run` path (AddRun.ApplyRunHyperlink writes them directly).
…series set ChartHelper.Reader emits per-series outline as three Format keys (outlineColor, lineWidth, lineDash) but the series setter only honored the compound `outline=color:width:dash` form. Dump→batch replays of charts with explicit per-series outline lost the line spec because the emitter forwarded the read-side keys verbatim and the setter routed them to unsupported. Add `outlinecolor` / `linecolor` cases that update only the SolidFill inside the outline element (preserving any existing width / dash), and add `outlinewidth` / `outlinedash` aliases mirroring the existing `lineWidth` / `lineDash` handlers. SolidFill is inserted before any PrstDash child to keep CT_LineProperties schema order.
… on chart add
The chart batch emitter flattens NodeBuilder's per-series Format keys
onto the chart-level `add` row as dotted `series{N}.<key>` so the chart
setter can apply them after the chart is built. The flatten list
covered color / lineWidth / lineDash / marker / markerSize / smooth /
outlineColor / transparency but omitted gradient. Charts whose series
each carried a distinct GradientFill replayed against the chart-level
gradient fallback only — every series got the first series's gradient
spec.
Add gradient, outlineWidth and outlineDash to the flatten list so each
series's own spec round-trips. The dotted setter route through the
existing per-series cases (gradient / outlinewidth / outlinedash /
linecolor) already lands them correctly.
AddPlaceholder always assigned an idx to non-title placeholders by matching the layout slot. When source XML carried <p:ph type='subTitle'/> with no idx attribute, NodeBuilder correctly omitted phIndex from the dump (its emit-only-when-Index-has-value contract), but AddPlaceholder on replay auto-bound idx=1 from the layout match. The resulting <p:ph type='subTitle' idx='1'/> inherited body's default bullet style from the layout/master cascade and rendered a phantom bullet that the source never had. Detect whether the caller explicitly provided idx and, when phType is subTitle without an explicit idx, bind to the layout slot by type alone — leaving Index unset on the <p:ph> emit. ECMA-376 lets the type attribute drive the slot match for the title family; subtitle has the same property. Other placeholder types keep the existing auto-allocate behavior because they routinely share a phType across a slide (body slots at idx=1, idx=2, ...) and need the disambiguator.
NodeBuilder's line readback covered solid fill / none / dash / width but ignored <a:gradFill> inside <a:ln>. Reader emit dropped the gradient and replay rebuilt the shape with a bare <a:ln/>, which PowerPoint resolves to a default thin black stroke. Gradient lines — including the spec-form connectors that present multi-color flow lines — visually flattened on every round trip. Surface the gradient as Format["line.gradient"] using the same spec form the shape-fill gradient already emits, and add `line.gradient` / `linegradient` cases on both the shape (ShapeProperties.cs) and the connector (Set.Shape.cs) setters. The fill child is inserted before any PrstDash sibling to keep CT_LineProperties schema order (fill → prstDash → headEnd → tailEnd), which PowerPoint silently rejects when violated.
The earlier fix re-anchored crossBetween in ChartHelper.Axis's axis-role setter, but the chart-level `crossbetween` case in ChartHelper.Setter still used AppendChild and landed crossBetween after majorUnit / minorUnit. Dumped charts that go through the chart-level Set path (bar, column) replayed with an out-of-order valAx that PowerPoint silently rejected. Apply the same anchor chain (CrossesAt → Crosses → CrossingAxis → InsertAfterSelf) on the chart-level path so both entry points land crossBetween in the right position regardless of whether the source build emitted majorUnit before the cb set.
`add chart --prop series1.outlineColor=#FF0000` (and outlineWidth / outlineDash) silently dropped because DeferredSeriesSubkeys only listed the compound `outline` key. IsDeferredKey returned false for the dotted subkey form, so the property was handled before the series element existed in the chart XML, then discarded. Add `outlinecolor`, `outlinewidth`, `outlinedash` to DeferredSeriesSubkeys so they route through HandleSeriesDottedProperty after build, matching the existing dotted-subkey contract.
ConnectorToNode had per-key readback for outline width, dash, solid color, and alpha, but no branch for <a:gradFill> on the connector's <a:ln>. A connector authored with a gradient line (or one whose gradient was applied via `set --prop line.gradient=…`) round-tripped to a bare <a:ln/> on dump→batch replay because the spec key never surfaced in Get; replay then fell back to the theme's default thin stroke. Read the GradientFill via ReadGradientString and emit `line.gradient`, mirroring the existing shape-outline gradient readback.
A series authored with `trendline=poly:3` or `trendline=movingAvg:5` round-tripped to bare `poly` / `movingAvg` on Get and dump, silently dropping the polynomial degree and moving-average window size. The reader only emitted <c:trendlineType val>; the adjacent <c:order val> and <c:period val> children were ignored. Format the spec via FormatTrendlineSpec, which appends `:N` for poly (from <c:order>) and movingAvg (from <c:period>). Other trendline types (linear, log, exp, power) keep their bare-name spec. Applied at both the chart-level trendline summary and the per-series readback so dump→batch replay rebuilds the exact curve.
ReadGradientSpec only appended the `:angle` suffix when the linear gradient's angle was non-zero, so a series gradient authored as `gradient=A5A5A5-D0D0D0:0` round-tripped to bare `A5A5A5-D0D0D0` — the explicit zero-degree (left→right) direction collapsed into "no angle specified", letting the writer fall back to the schema default on the next round-trip. Emit `:0` whenever <a:lin ang=…> is present, regardless of value. The angle is a meaningful user choice (0 = horizontal, 90 = vertical, …) and must survive dump→batch replay verbatim.
Shape gradients use `C1-C2-ANGLE` while chart series gradients use `C1-C2:ANGLE`. Users routinely mix the two — feeding a chart-style spec into a shape (`fill=FF0000-00FF00:45` or `gradient=…:90`) parsed the colon as part of the second color token, then the parser either rejected the value or silently dropped the angle. Normalize a trailing `:N` (integer in [-360, 360]) to `-N` at the front of NormalizeGradientValue when the input is not already a typed prefix (radial:/path:/linear;). Get/dump still emit each surface's canonical separator unchanged, so existing round-trips and the schema contract are untouched.
…pec call InnerText is a nullable reference type; pass "" when null so the non-nullable parameter contract holds. The guarding HasValue check ensures Val itself is non-null, but does not propagate to InnerText.
- pptx/chart.json chartType: inline the enum values list (the per-format override replaced the shared base atomically; without inlining `values`, programmatic consumers like SchemaContractTests lose the legal-value array).
- pptx/textbox.json + shape.json size/bold/italic/color: flip get=false; these forward to the first run on Add/Set but Get exposes only effective.size / effective.color / inheritance summary at the shape level, with bare bold/italic only on the inner run.
- _shared/table-row.json cols: revise description ("asserts the new row's cell count matches the table grid") and example to cols=2 — handler enforces match, "override" was misleading.
- pptx/table-cell.json text: override to add=false; pptx tables are strictly rectangular per OOXML, so a standalone Add cell only succeeds when the row currently has fewer cells than the grid (an illegal state). Set + Get unchanged.
tooltip is the screen-tip on a hyperlink; without a 'link' to attach to, the value would be silently dropped. Mirror the Set-side guard so callers see a descriptive ArgumentException instead of a no-op, and bundle link+tooltip in a single call (or Add the shape first, then Set link+tooltip together). CONSISTENCY(shape-tooltip-requires-link).
CheckInBackground returned early on Directory.CreateDirectory failure
and SaveConfig silently swallowed write errors. Together they meant
every officecli call in a read-only-home container respawned the
refresh process — one HEAD /releases/latest per command instead of
one per 24h, polluting origin analytics and the caller's egress.
LoadConfig + SaveConfig now iterate a candidate list: ~/.officecli/
config.json first, then $TMPDIR/officecli-config.json only when
IsInContainer() trips (docker, k8s, podman, lambda, cloudrun, gcp-
functions). Desktop and VM users never touch /tmp; iteration stops
at first success. SaveConfig returns false when every candidate
fails and CheckInBackground uses that to skip the spawn.
While here, shorten the UA: OfficeCLI/{ver} replaces OfficeCLI-
UpdateChecker/{ver}, with " (container)" appended in container envs.
Server-side stats on d.officecli.ai accept both forms via
OfficeCLI(?:-UpdateChecker)?/(\d+\.\d+\.\d+) during the rollover.
ConfigDir + ConfigPath are now get-only properties so tests can
swap \$HOME between cases without restarting the process.
… together; map friendly lineDash aliases to sys*/lg* variants
crosses / crossesAt branches mutually wiped each other: both branches called RemoveAllChildren on the COUSIN type before writing, so a single Set call that supplied both keys silently dropped whichever ran first. Restrict each branch to remove only its own type (CT_ValAx schema lets both children coexist, and a Set with both keys is the natural way to switch a value axis crossing point).
Also fix lineDash mapping per schemas/help/_shared/chart-series.json contract: friendly aliases ("dash" / "dot" / "dashDot") all resolve to the sys* OOXML variants (SystemDash / SystemDot / SystemDashDot). The previous mapping wrote the literal Dash / Dot / DashDot enum members, so Set("dash") + Get round-tripped as "dash" instead of the documented "sysDash". 'solid' remains the only round-trip-stable token.
Set bold.cs=false (and italic.cs=false) writes <w:bCs val="false"/> via the explicit-false-override path. The I18n reader only checked element presence, so Get reported bold.cs=true even after the user had toggled it off — diverging from how the bare bold / italic readback (which uses IsToggleOn) treats the same Val=false sentinel. Gate the read on element AND (Val absent OR Val truthy), so the explicit-off form rounds-trips as "absent from Format" instead of as a phantom true.
…fusals Two production hardening hunks were added in cee0b5b and parts of 84e18bb to satisfy tests that pinned aspirational "should throw" contracts; no real user reported the silent-drop / silent-no-op as a problem, so adding throws on previously-tolerant code paths is a behavior break for any existing caller passing tooltip without link, or echoing core properties through Set. Revert: - PowerPointHandler.Add.Shape.cs: shape Add no longer throws on tooltip without link (full revert of cee0b5b). - PowerPointHandler.Set.Media.cs: picture Set tooltip alone returns to "silent passthrough" (the test that wanted a throw was the only voice for the new behavior). - PowerPointHandler.Set.cs: drop the created / modified / extended.application read-only guards; Set silently ignores them as before. The substantive bug fixes from the same commits (errBars canonical case, axis crosses + crossesAt mutual-wipe, bold.cs Val=false readback, lineDash friendly aliases, schema/handler-reality alignment, tblGridChange capture, numLevel → ilvl rename) stay.
…rts to get=false Earlier session change flipped formula.get on the shared equation base from false to true on the strength of "handler already emits Format[\"formula\"]". That holds for pptx (PowerPointHandler.NodeBuilder.cs reconstructs LaTeX from <m:oMath>) but NOT for docx — WordHandler's equation Get path doesn't surface a formula key. The shared base therefore overstated docx's contract. Restore the conservative shared baseline (get=false) and add a pptx-specific override that re-asserts get=true. docx equation Get readback stays honest until WordHandler grows the matching emit (no current user has reported needing it).
The xpath / action null guards added in 84e18bb weren't fixing any reported issue — they were "while I'm here" defensive code added alongside legitimate handler-canonical fixes. No caller passes null today, and if one ever did, the NullReferenceException a few lines down is just as loud as the ArgumentNullException would be. Strip the gratuitous additions; the partPath guard (which predates this session) stays.
…ses column alignment
\mathbb{R}/\mathcal{L} emitted m:scr without required val attribute, and
in wrong child order (m:sty before m:scr violates CT_MRPR sequence). Set
val to DoubleStruck/Script and put scr before sty.
cases environment emitted m:mcJc directly under m:mc, but schema requires
it wrapped in m:mcPr. Files with matrices in cases failed validation and
would not open in Word.
…row commands Tokenizer mapped \| to literal '|'; now tokenizes as Vert command so it renders as ‖ (double vertical bar). The original behavior collapsed norm expressions like \|x\|_2 to |x|_2. Command table additions: to gets mapsto iff implies impliedby land wedge lor vee lnot neg mid parallel — all previously fell through as literal text in math output.
…urface Adds 25 examples (31-55) covering matrices (pmatrix/bmatrix/vmatrix), cases, auto-sized delimiters (left/right with various bracket types, floor/ceiling), overbrace/underbrace/overset, math fonts (mathbb/cal/ bf/rm), cancel/cancelto/boxed, accents (bar/vec/tilde/ddot/overline), hyperbolic and inverse trig, operatorname, modular arithmetic, double integral with text, big operators (bigcup/bigcap/coprod), full uppercase Greek set, matrix dots (cdots/vdots/ddots), spacing controls, textcolor, set theory, norm and inner product.
…espace
LaTeX like `\sum_{n=1}^{\infty} \frac{1}{n^s}` rendered with an empty
dotted placeholder between the summation symbol and the fraction, with
`\frac` floating as a sibling outside `<m:e/>` instead of being the
summand.
Root cause: the tokeniser collects the space between `\infty}` and
`\frac` into a standalone Text(" "). The n-ary base parser then called
ParseSingleArg on that whitespace token, which strips leading whitespace
and returns an empty run for a pure-whitespace token. The base was
therefore emitted as empty and `\frac` was parsed as the next sibling.
Fix: in the `\sum`/`\int`/`\iint`/`\iiint`/`\prod`/`\coprod`/`\bigcup`/
`\bigcap` branch, skip any pure-whitespace Text tokens between the
sub/sup limits and the base expression. The fraction (or whatever
follows the limits) now lands inside `<m:e/>` and Word renders it as the
summand without the empty box.
…instead of font-size
Add placeholder Add forwarded its leftover prop bag through Set, which
then routed `size=half` through ParseFontSize and threw
ArgumentException("Invalid font size: 'half'"). The placeholder @sz
attribute (full/half/quarter — half-slide / quarter-slide layout slots,
ECMA-376 §19.7.10) was never consumed by AddPlaceholder, so the value
fell through to the run/shape font-size branch and crashed before any
unsupported-prop accounting could see it.
Consume size/sz in AddPlaceholder, map matching values onto
PlaceholderShape.Size, and add the key to the consumed set so it no
longer leaks into the passthrough Set call.
…n arithmetic =OFFSET(A1,5,0)&"x" was returning "0x" when A6 was empty. Empty/missing cells reached through direct ref / OFFSET / INDIRECT collapsed to FormulaResult.Number(0), and the string-concat operator (`&`) then stringified the 0 into "0". Excel coerces empty cells to "" in concat and 0 in arithmetic; collapsing both contexts to the same numeric 0 loses the concat semantics. Add FormulaResult.Blank() — a no-value result whose AsString() returns "" (so concat matches Excel) and whose AsNumber() returns 0 (so arithmetic keeps working). ToCellValueText preserves the direct-reference "=A6" → 0 behaviour by handling IsBlank explicitly. Empty-cell paths in ResolveCellResult now return Blank() instead of Number(0).
… falls back to OS culture when --locale absent
`officecli create file.docx` now shapes blank docs around the user's locale
in two ways:
1. Explicit --locale ar-SA (and similar RTL locales) stamps <w:bidi/> on
sectPr (page layout direction) AND on docDefaults/pPrDefault (paragraph
default direction). Paragraphs added afterwards inherit RTL without any
per-paragraph direction=rtl. RTL locale set: ar, he, iw, yi, ji, ur,
fa, ps, sd, ks, ug, ku, ckb, dv, syr, nqo (CJK and Thai are LTR and
keep current behavior).
2. When --locale is not given, fall back to the OS user culture so
Arabic / Hebrew / Chinese / … users get a doc shaped for their
language without having to repeat the locale on every command. The
OS snapshot is captured once at process startup in Program.cs
(CFLocale on macOS, $LANG/$LC_ALL on Linux, user UI culture on
Windows) before the rest of the cli pins the thread-current culture
to Invariant for deterministic OOXML/JSON/CSS output. Latin-script
Western locales (en/fr/de/es/it/pt/nl/ru/pl) and empty/C/POSIX
cultures resolve to null so no-arg `create` keeps the modern
Calibri/LTR baseline (no Times New Roman regression for English
users, neutral output in CI without locale config). Auto-detected
locales emit a one-line stderr note ("locale 'ar-SA' inferred from
OS user culture …") so the shaping is visible and overridable.
Schema fix in the same change: docdefaults.rtl=true used to write
<w:rtl/> inside rPrDefault/rPr, which the OOXML validator rejects
(CT_RPrDefault excludes <w:rtl/> from its content model even though
Word still renders the file). The setter now writes <w:bidi/> inside
pPrDefault/pPr instead — the canonical "this document's paragraphs are
RTL by default" location Word emits itself — and cleans up the legacy
rPrDefault/<w:rtl/> shape on re-toggle so older docs migrate on next
set. Get-readback follows: docDefaults.rtl is sourced from pPrDefault/
bidi, not rPrDefault/rtl. validate now passes cleanly on docs created
with --locale ar-SA.
Surfaced by running a fictional Arabic-agent workflow through officecli end-to-end (create blank doc with LANG=ar_SA, build letter/invoice/ list, render with real Word, observe friction). Four fixes: 1. `add table --prop columns=N` was silently dropped, leaving a 1-column table with no UNSUPPORTED warning. `columns` is the natural English spelling and the first thing an agent reaches for; the canonical key is `cols`. Now accepts both. The deeper "Add never warns on unknown props" issue (foreach iteration marks every key as accessed in the tracker — see TrackingPropertyDictionary.cs:24-37) is a wider design gap and out of scope for this change. 2. settings.xml schema order: themeFontLang was being inserted at the very front of <w:settings>, before characterSpacingControl. The correct CT_Settings sequence has characterSpacingControl (~pos 63), compat (~pos 78), themeFontLang (~pos 80) — themeFontLang belongs AFTER compat. The validator surfaced the violation as "unexpected child characterSpacingControl" which was misleading; the offending element was actually themeFontLang. Since themeFontLang only gets written when --locale (or the OS-snapshot fallback) yields a value, every RTL-locale doc was failing validation while LTR docs passed. AI agents creating Arabic content would see `validate` fail on every fresh file and think their usage was wrong. 3. help schema for /numbering/abstractNum's `format` property listed chineseCounting / japaneseCounting / koreanCounting but omitted the script-localized formats users on the other side of the locale map actually need: arabicAlpha (أ ب ت ث), arabicAbjad (أ ب ج د), hebrew1, hebrew2, hindiNumbers/Vowels/Counting/Consonants, thaiCounting/Numbers/Letters. All are accepted by the handler; they just weren't discoverable through `help docx abstractNum`. 4. Tables added at /body in an RTL doc rendered with LTR column order unless the user remembered to pass --prop direction=rtl on every table. Paragraphs already inherit section bidi for free; tables use <w:bidiVisual/> on tblPr which Word does NOT propagate from sectPr. AddTable now consults a new IsTableContextRtl(parent) helper — walks to the owning section (or docDefaults/pPrDefault) — and auto-stamps BiDiVisual when no explicit direction prop was passed. Explicit direction=ltr still suppresses it, so the override path stays open.
…table
Two leftovers from the previous Arabic-agent UX pass:
1. Every blank docx (RTL or LTR) failed `officecli validate` with
"[MarkupCompatibility] The Ignorable attribute is invalid - The
value 'w14 w15 wp14' contains an invalid prefix that is not
defined" — the root <w:document> element declared wp14 and w15
namespaces but not w14, while listing all three under mc:Ignorable.
Since the Add helpers stamp w14:paraId / w14:textId on every
paragraph they create, the missing declaration was a real schema
violation, not just a cosmetic one. Add the missing
AddNamespaceDeclaration("w14", "…/office/word/2010/wordml").
2. `officecli add file.docx /body --type table --prop columns=4`
silently produced a 1-column table — the canonical key is `cols`
(now aliased to `columns` after the previous pass) but ANY bare
typo (`column`, `numcols`, `colwidths=` misspelled as `colwidth`)
still slipped through without warning. Root cause:
WordHandler.Add.Table.cs iterates `properties` via
`foreach (var (k,v) in properties)`, which routes through
TrackingPropertyDictionary.GetEnumerator and marks every key as
accessed (deliberate — see TrackingPropertyDictionary.cs:24-37,
chart/media handlers need this for the
`.Where(IsDeferredKey)` pattern). With every key reported as
"accessed", tracking.UnusedKeys came back empty and the CLI emitted
no UNSUPPORTED warning. Fix layered in two pieces: the switch over
bare keys grows a default case that pushes truly unknown bare keys
(not r{N}c{M} cell content, not dotted-key fallback territory) to
LastAddUnsupportedProps; and CommandBuilder.Add merges that list
into the unsupported set that the CLI surfaces — mirroring what the
resident-server path already does. JSON envelope picks up the same
"code: unsupported_property" warning shape.
…concept name Set on a run/ptab/instrText/etc. with `align=center` previously surfaced the literal user key plus the generic valid-run-props list, e.g. `align (valid run props: text, bold, italic, ...)`. The hint pointed to typography props but never named the actual concept the user was trying to reach — alignment, which is a paragraph- or ptab-only property in OOXML, not a run prop. When the key matches align/halign/alignment, emit `align (alignment is a paragraph/ptab property, not a run prop)` so the unsupported notice points at the right path level. Ptab runs still take the dedicated `case "align" when ptabEl != null` branch above and are unaffected.
The find prop skip-list excluded 'scope' from format/paragraph bucketing, but no code path ever read it — silently accepted and silently ignored. Drop it from the literal so it routes through the normal property dispatch and becomes visible as unsupported.
Start of a selector-vocabulary migration: text/value selectors are moving out of --prop into their own top-level flags so --prop can go back to meaning 'property to mutate'. First flag in this family is --find, covering the most common selector pattern. query: --text is renamed to --find (hard rename, no alias — the old flag had no users). Resident-server arg key 'text' → 'find' in lockstep. Semantics unchanged: case-insensitive substring filter. set: --find is added as a top-level flag that merges into the props array as 'find=value' before downstream dispatch, so handlers, the resident server, and batch all keep working with zero changes. The legacy --prop find=VALUE form still works but emits a stderr hint steering users to --find. Combining both --find and --prop find= is rejected as ambiguous. Local-only repro scripts and test comments were updated to the new flag name; those files are gitignored and not part of the commit.
Completes the find/replace selector-vocabulary pair started by --find. --replace VALUE merges into the props array as 'replace=value' before handler dispatch, mirroring --find's sugar-only approach so handlers stay untouched. Empty string is preserved as a meaningful value (Word delete-only: '--replace ""' wraps the match in w:del without an insertion), so the merge guard uses != null rather than IsNullOrEmpty. The legacy --prop replace=VALUE form still works but emits the same deprecation hint as --prop find=. When both legacy keys are present, the hint coalesces into one line.
3.1.3 has a moderate-severity advisory: a crafted CFB file with cyclic Left/Right sibling links in its directory entries drives DirectoryTree.TryGetDirectoryEntry into an infinite loop, blocking the calling thread until the process is killed. OleHelper.UnwrapOle10NativeIfCfb feeds untrusted bytes from `add ole src=...` and from foreign docx/pptx OLE parts into RootStorage.Open, so the attack surface is reachable in normal officecli usage. 3.1.4 is API-compatible; OLE test sweep (1114 tests, 0 failures) and release build (0 warnings) pass locally. NU1902 warning from the CI publish step is silenced as a side effect.
…ts in outline query: split `listobject` (real ListObjects only) from `table` (real plus heuristically detected blocks). Detection is strict and high-precision — a sparse-cell anchor pass requiring a contiguous >=2-column non-numeric-text header, >=1 data row, and no overlap with a real ListObject. Detected blocks carry type=detectedtable, stable=false, and an honest range path (/Sheet1/A1:D10) — never a fabricated /table[N]. Cost is O(non-empty cells), independent of the declared sheet dimension, and every qualifying block on every sheet is reported. view outline: add per-sheet table and chart counts alongside the existing formula/error/pivot/ole counts (text and JSON variants), so a single outline call reveals a workbook's tabular and chart structure across all sheets.
Convert `--prop find=`/`--prop replace=` to the new top-level `--find`/ `--replace` flags in the Word revisions example. `--prop regex=true` stays (regex has not migrated). Empty replace uses `--replace ""` to preserve the delete-only tracked-change semantics.
Add agents-project with scripts and documentation to help import agents.zip into an agents/ directory. Includes support for both shell scripts and npm commands.
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.
Adds Playwright e2e tests, a startup readiness endpoint, and CI readiness polling.