The shared writer separates column and type selection from physical output. On 10 October 2026, the API and local XLSX/CSV creation adapter are integrated in main; registered synthetic tests passed all 158 cases (138 module and 20 regression cases), with no errors or skips. Server adoption was verified in release 164, with authenticated readback. R-019 live acceptance, Google adoption and verification of the package loaded on the PC remain separate obligations.
The internal API write_table(source, spec, destination, mode="create") takes records or matrix rows. An explicit schema declares each header, exact source field, supported transform and type. The compiler produces a versioned Workbook / Sheet / Column / Cell contract; adapters only write this contract. Historical header resolution stays at the runtime boundary and reuses the existing shared projection across all public rows.
Every matrix row in the shared contract is data, even when it equals the header. The local create adapter consumes a header only when declared by the caller or produced by record projection; a matrix without declared columns keeps its first row as data. Writing never removes duplicate rows. Private fields and runtime-declared sensitive fields are excluded recursively, including inside structured cell values and source metadata.
from tabular.api import write_table
receipt = write_table(
[{"observed": "2027-04-12", "amount": "12.50 USD"}],
{"locale": "en", "name": "Data", "column_specs": [
{"header": "Date", "source": "observed", "type": "date"},
{"header": "Amount", "source": "amount", "type": "currency", "currency": "USD"}]},
{"backend": "local_xlsx", "path": "synthetic.xlsx"})
column_specs separates source, the exact record key, from header, presentation text in any language or alphabet. The planner, extractor or executor that knows the schema must supply this map, for example {"source":"amount","header":"金額"}. Record key order does not matter. The writer does not translate keys, infer equivalence from values or position, or call Frontier. No per-language aliases are added; an explicit map also works without a header lexicon.
The normal plan can declare the map from the schema already available to it, without an extra model call. Historical header-only resolution remains at its existing boundary; when a map is missing and the correspondence is not unique, it returns an error listing headers and available fields. Dates with distinct roles are not merged: the caller must declare the chosen source.
Native numbers and booleans keep their types. Strings become numbers or amounts only when requested by the schema, using a deterministic grammar and an explicit locale for local separators. Currency must be declared. Values beyond 15 significant digits, non-finite numbers and lossy conversions remain text with a notice. A code string such as 00123 stays text unless conversion is explicitly requested.
Complete dates become real date values. Month/year, year-only, assumed years marked with * and impossible dates remain text. Datetimes with a timezone or precision beyond milliseconds remain text until an appropriate conversion is declared. Missing days, years or midnight are never supplied. Numeric locale does not select a regional date grammar: slash dates follow the existing observed-date helper's day/month/year contract; use ISO dates for an unambiguous interface.
The initial contract accepts no formulas. A type="formula" field or a value extracted from a site cannot grant formula authority. New active XLSX strings stay text; Google uses explicit string values. CSV adds a visible apostrophe to active text, including headers and Notes, and reports this change.
| Format | Shared API modes | Types and limits |
|---|---|---|
| Local XLSX | create, overwrite requested sheets, append | multiple sheets; numbers, dates and booleans; 1,048,576 rows, 16,384 columns, 32,767 characters per cell; operational cap of 5 million cells |
| Local CSV | create, overwrite, append | one data sheet, text; the same local operational limits; Notes in a second CSV |
| Google Sheets | create with all data | multiple sheets and typed values; 1 million rows, 18,278 columns, 50,000 characters per cell and 10 million cells |
| Device | local creation planned through the updated package | XLSX creation uses only the standard library; delivery and verification of the loaded package are required. The API’s explicit device backend remains rejected |
Physical row limits include the header; the caller’s max_rows counts data rows. Numeric locale is global spec.locale; a different per-sheet locale is rejected. A caller limit of zero adds no constraint; physical and operational limits still apply. Every declared truncation reports the limiting field, used amount and known availability. Notes must fit within limits; otherwise writing stops before modifying the destination.
Declared source flags such as truncated and collection_partial, available_total, used and cap metadata remain in per-sheet source_metadata, cleaned of private and sensitive fields. An unknown total stays null. Counts alone do not authorize a claim of complete or incomplete collection.
source_partial and source_truncated describe the source declaration; writer_truncated and truncations describe reductions during writing. The receipt’s general total counts rows received by the writer and does not replace the declared source total. A partial collection flag produces the localized WARN_TABLE_PARTIAL_SOURCE notice in chat and Notes even without a textual reason. A verified file readback does not prove that all site pages were collected.
The browser broker produces a typed envelope with its producer and a collection-specific scope. Metadata follows act_sites → read_sites → extract_entries → filter_entries → write_table through the local adapter as a runtime-resolved argument: page text, rows and the model cannot declare source completeness. Successful extraction or filtering preserves upstream incompleteness. A known cause produces a localized explanation in Notes and chat; an unknown cause retains the generic warning without exposing its technical code. Provenance retains only the HTTP(S) origin and path, without credentials, query or fragment. Producer and scope do not identify invoices or authorize new record associations or row removal.
The text preview after navigation is separate from stored data: the 4,000-character presentation cap exposes presentation_truncated, preview_used and preview_available_total, without declaring a collection cut or changing collection limits. An actual collection stop instead retains its partial state and cause through to the sheet; file verification does not cancel them.
Notices and provenance appear both in the chat-facing receipt and in a Note sheet. New sheet name collisions are resolved and declared, for example as Note_2. An existing user Note sheet is preserved: a write that would replace it is rejected. Generated Notes carry an internal ownership marker; updates never silently leave stale generated Notes behind.
For CSV, Notes use <name>_note.csv. The receipt lists both files and each undo. Updating an existing pair requires an explicit destination.notes_path; an unrelated Notes file is not implicitly overwritten. Publishing the pair is not atomic: failure on the second file retains the first as a partial result.
Receipts distinguish compiled rows, verified rows and known created or modified artifacts. XLSX and CSV are read back before publication; Google is read back by comparing typed values and formats. A known created Google ID remains in the receipt with undo even when readback fails. A creation error without an ID has an unknown outcome and is never retried automatically.
The integrated code routes local creation in backends.files.local.create_spreadsheet through write_table, retaining the caller’s consent boundary, shared projection and ordinary undo for all created files, including CSV Notes. XLSX creation and readback use the standard-library writer; XLSX overwrite and append still require openpyxl. The observed-date normalizer is also pure and consumes explicitly supplied month names; the server facade retains catalog lookup.
Distribution must include the writer and date-normalizer support files. Updated create code in a package missing those supports returns an error without claiming a verified file; an entirely older package continues to run its older code. A server package offer does not prove which package a device loaded: actual readback is needed before claiming adoption.
This change does not adopt the existing Google paths: the shared API reuses the Python OAuth client, but consent, replay with original inputs and Google adoption require separate verification. Local adoption passed registered cases 1012/22366 and 1013/22367; verification of the package actually loaded on the PC remains pending. R-019 stays open: file-writing verification establishes neither complete collection nor successful live acceptance.