Test an AI-written CSV exporter as three separate contracts: valid serialization, meaning-preserving round trips, and one named spreadsheet workflow.
A CSV export can look perfect in a text editor and still fail the person who opens it. The commas line up. Every row has the expected number of fields. A parser reads the file without complaint. Then a spreadsheet drops a leading zero from an identifier, interprets a date-like label, chooses the wrong separator for the user’s locale, or treats a harmless formula-shaped note as a formula instead of text.
That is why “the CSV test passed” is too vague to support a release. An AI-written exporter has at least three contracts to earn: it must produce the declared CSV syntax, preserve the meaning the application actually owns, and behave acceptably in one named spreadsheet workflow. Those contracts overlap, but none proves the other two.
This guide builds a package-free Node 24 fixture around that separation. Its samples are synthetic and deliberately harmless: names, identifiers, punctuation, Unicode text, and local arithmetic-shaped strings such as =1+1. The fixture contains no external destinations or action-bearing formulas. Its purpose is to reveal boundaries, not demonstrate an attack.
One File Needs Three Signatures
The first signature is CSV syntax. It answers whether the exporter follows one declared grammar: delimiter, quote character, record ending, encoding, byte-order mark policy, header presence, and final line-ending policy. This is a question about the file that left the application.
The second signature is semantic round trip. It asks whether the approved logical table survives serialization and parsing. A customer code must still be the same string. An empty field must not silently become a missing field. A line break inside a note must remain inside that one note. This is a question about meaning, not merely accepted syntax.
The third signature is named spreadsheet interpretation. It asks what a particular spreadsheet product, version, locale, and import path does with the file. Directly opening a .csv is not necessarily the same operation as importing it with an explicit delimiter and column types. This is a question about a consumer outside Node.
Keep the signatures separate in the release record. A green syntax test must not be promoted into “safe in every spreadsheet.” A successful spreadsheet preview must not be promoted into a claim that the machine export preserves types it never represented.
Start With a Dialect Card, Not a Guess
RFC 4180, “Common Format and MIME Type for Comma-Separated Values (CSV) Files”, is the familiar reference for comma-delimited records. It describes CRLF record endings, optional headers, significant spaces, double-quoted fields, and doubling a quote that appears inside a quoted field. It is an Informational RFC, however, not a universal Internet Standard. Its own interoperability discussion acknowledges differences among implementations.
The W3C Model for Tabular Data and Metadata on the Web makes the practical consequence explicit: CSV exists in variants. Its guidance discusses dialect properties such as delimiter, encoding, quote character, line terminators, header count, blank-row handling, and trimming. It recommends UTF-8 for web publication and prefers CRLF while permitting LF in its broader profile.
Before reviewing code, write down the exact dialect the exporter promises. A compact card is enough:
{
"destination": "machine-import-v3",
"delimiter": ",",
"recordTerminator": "CRLF",
"quoteCharacter": "double quote",
"encoding": "UTF-8",
"bom": "absent",
"header": "present, one row",
"columns": 4,
"emptyString": "empty field",
"null": "rejected before export",
"finalTerminator": "present"
}
A semicolon-delimited file needs a different card. So does an LF-only feed. Do not hide those differences behind one csv: true option. Microsoft’s current Excel import and export guidance explains that separators can be selected during import and that the default list separator used for saving can depend on Windows regional settings. “Works on my machine” may therefore mean “matched my regional configuration by accident.”
Quoting Is a Grammar Rule
A correct serializer operates on a complete logical cell. It does not concatenate raw fragments around commas and hope the result remains one field. For the comma/CRLF profile, the bounded fixture applies these rules:
- Double a double quote inside a field.
- Quote a field containing the active delimiter, a quote, CR, or LF.
- Quote empty fields and fields with edge spaces for deterministic output.
- Join fields with the declared delimiter and records with the declared terminator.
- Add a BOM only when the named profile requires one.
For example, the logical value He said "hello" becomes "He said ""hello""". The value north,west is quoted under a comma dialect, while alpha;beta is quoted under a semicolon dialect. A CRLF inside a quoted note is data; a CRLF outside quotes ends the record.
This sounds elementary, which is precisely why generated implementations often compress it into a brittle replacement chain. The test suite should compare critical outputs with literal goldens. A round trip through code derived from the same algorithm can allow two matching mistakes to congratulate each other.
The Oracle Must Be Independent
The companion fixture uses a small state-machine parser written separately from the serializer. It recognizes the selected delimiter and record terminator, distinguishes quoted from unquoted state, unescapes doubled quotes, and rejects text after a closing quote. It also checks the expected column count.
That parser is not offered as a production CSV library. It is deliberately narrow test equipment for the declared profiles. Its independence matters more than its feature count. The test combines two kinds of evidence:
- Literal goldens prove exact bytes and characters for representative files.
- Semantic comparisons prove that the oracle reconstructs the original rectangular string table.
Do not require every accepted CSV input to return byte-for-byte unchanged after parse and re-export. Quoted and unquoted forms can represent the same logical cell, and RFC 4180 permits the last record to appear with or without a final line break. The fixture instead gives its own output one canonical form. It accepts a bounded noncanonical input such as a missing final terminator, then emits the declared canonical form.
CSV Does Not Carry Your Application’s Types
A CSV cell is a character sequence. Your application may call that sequence a customer ID, date label, decimal amount, missing value, or free-text note, but the file does not automatically preserve that schema.
The fixture therefore accepts only a rectangular array of strings. It rejects numbers, BigInt values, null, and undefined. That is not a claim that every exporter must reject them. It is a demand that type projection happen in an explicit domain layer rather than through JavaScript’s convenient coercion rules.
Consider four strings: 00123, 9007199254740993, 2026-09-02, and 1e3. The Node round trip keeps all four exactly because the parser returns strings. That result does not prove a spreadsheet will display them unchanged. Microsoft notes that directly opening a CSV uses current default data-format settings, and its example specifically calls out converting a column to text to preserve leading zeros.
Empty and missing values need the same discipline. CSV has no universal null token. If an empty string, an absent database value, and “not applicable” mean different things, define their wire representations before calling the serializer. Do not let all three fall through to an empty field and describe the result as lossless.
Exercise the Seams, Not Just the Happy Row
The runnable fixture covers plain fields, active delimiters, doubled quotes, embedded CRLF, edge spaces, empty middle and trailing cells, and the difference between one empty cell and a rejected zero-column row. It also rejects ragged rows instead of quietly shifting later columns.
Unicode deserves its own evidence. The tests carry München, Hebrew text, an emoji, and both precomposed and decomposed forms of an accented character. The machine-lossless profile preserves the supplied code-point sequence; it does not silently normalize identifiers. If a publishing contract adopts the W3C recommendation for Unicode Normal Form C, perform and document that transformation before serialization, with domain approval.
The named spreadsheet-view profile adds exactly one UTF-8 BOM and asserts the bytes EF BB BF at the start. That policy follows Microsoft’s guidance for opening UTF-8 CSV files correctly in Excel, which says a UTF-8 CSV can be opened normally when saved with a BOM and otherwise recommends an explicit import route. The machine profile forbids the BOM. Neither profile is “more CSV”; they serve different named destinations.
A Quoted Cell Can Still Be Formula-Shaped
CSV quoting keeps delimiters and line breaks inside one logical cell. It does not tell a spreadsheet that the recovered cell must remain plain text. After parsing "=1+1", the logical value still begins with =.
OWASP’s CSV Injection guidance identifies formula-shaped beginnings including =, +, -, @, tab, CR, LF, and full-width variants that may matter in some locales. It also warns that checking only the original first character is insufficient when a broken serializer lets separators or quotes create a new cell.
The fixture uses only harmless local examples:
=1+1
+1
-1+2
@SUM(1,1)
[TAB]=1+1
=1+1
It first proves that raw machine export preserves those strings exactly and that a correct serializer keeps a value such as note",=1+1 in one cell. It then applies an illustrative apostrophe-prefix transform to formula-shaped cells in the named spreadsheet-view profile. The oracle proves the transformed character sequence and the CSV boundaries.
That green Node test is intentionally not called “spreadsheet safe.” OWASP says common quote-and-prefix techniques may not remain reliable after an Excel save-and-reopen cycle, and its product-specific alternative carries a data-change tradeoff and may behave differently elsewhere. Its bottom line is the right release language: there is no universal CSV sanitization strategy for every spreadsheet and downstream consumer.
Make the transform a destination policy, not a global cleanup function. If exact machine interchange is required, preserve the original values in the machine feed. If a human-facing spreadsheet view needs a protective transformation, record that it changes the data and do not quietly feed the transformed export back into an API or database import.
Name the Spreadsheet Workflow
“Tested in Excel” is still underspecified. Record the product, version or build, operating system, locale, and exact opening path. Distinguish double-clicking the file from using Data > From Text/CSV. Record the selected delimiter, encoding, header choice, and column types. Then inspect both the displayed value and, where practical, the stored cell value.
Run the synthetic file through save and reopen as a separate step. Watch the leading-zero ID, large string ID, date-like label, scientific-looking label, Unicode text, embedded line break, and every harmless formula-shaped cell. The purpose is not to generalize from one spreadsheet. It is to decide whether one named workflow is acceptable.
If the export is also promised for another product, locale, or import route, that is another signature. Do not average the outcomes. A pass in one destination does not cancel a hold in another.
The Complete Reproducible Fixture
The four blocks below are the exact files used for the bounded evidence run. Save them together in one directory. The serializer is intentionally small, and the oracle is independently implemented so the candidate cannot validate itself with the same quoting logic.
csv-exporter.mjs
const QUOTE = '"';
function dialect(name, options) {
return Object.freeze({ name, quote: QUOTE, finalTerminator: true, ...options });
}
export const DIALECTS = Object.freeze({
commaCrLf: dialect('comma-crlf', {
delimiter: ',',
recordTerminator: '\r\n',
bom: false,
quoteAll: false,
}),
commaLf: dialect('comma-lf', {
delimiter: ',',
recordTerminator: '\n',
bom: false,
quoteAll: false,
}),
semicolonCrLf: dialect('semicolon-crlf', {
delimiter: ';',
recordTerminator: '\r\n',
bom: false,
quoteAll: false,
}),
namedSpreadsheetView: dialect('named-spreadsheet-view', {
delimiter: ',',
recordTerminator: '\r\n',
bom: true,
quoteAll: true,
}),
});
const FORMULA_SHAPED_PREFIXES = new Set([
'=',
'+',
'-',
'@',
'\t',
'\r',
'\n',
'=',
'+',
'-',
'@',
]);
function validateDialect(value) {
if (!value || typeof value !== 'object') {
throw new TypeError('A dialect object is required.');
}
if (typeof value.delimiter !== 'string' || [...value.delimiter].length !== 1) {
throw new TypeError('The delimiter must be exactly one Unicode character.');
}
if (value.delimiter === QUOTE || /[\r\n]/u.test(value.delimiter)) {
throw new TypeError('The delimiter cannot be a quote or line break.');
}
if (value.recordTerminator !== '\r\n' && value.recordTerminator !== '\n') {
throw new TypeError('This bounded fixture supports CRLF or LF records.');
}
if (value.quote !== QUOTE) {
throw new TypeError('This bounded fixture uses a double quote as quote character.');
}
}
function validateRows(rows) {
if (!Array.isArray(rows) || rows.length === 0) {
throw new TypeError('Rows must be a non-empty array.');
}
if (!Array.isArray(rows[0]) || rows[0].length === 0) {
throw new TypeError('Each row must contain at least one cell.');
}
const width = rows[0].length;
rows.forEach((row, rowIndex) => {
if (!Array.isArray(row) || row.length !== width) {
throw new TypeError(`Row ${rowIndex} does not match the declared width ${width}.`);
}
row.forEach((cell, columnIndex) => {
if (typeof cell !== 'string') {
throw new TypeError(`Cell ${rowIndex}:${columnIndex} must be a string.`);
}
});
});
}
function encodeCell(value, selectedDialect) {
const escaped = value.replaceAll(QUOTE, QUOTE + QUOTE);
const needsQuotes =
selectedDialect.quoteAll ||
value.length === 0 ||
value.includes(selectedDialect.delimiter) ||
value.includes(QUOTE) ||
/[\r\n]/u.test(value) ||
/^[ \t]|[ \t]$/u.test(value);
return needsQuotes ? `${QUOTE}${escaped}${QUOTE}` : escaped;
}
/**
* Serializes an already-projected rectangular table of strings.
* A cellTransform may implement one explicitly named destination policy.
*/
export function serializeCsv(
rows,
{ dialect: selectedDialect = DIALECTS.commaCrLf, cellTransform = (value) => value } = {},
) {
validateDialect(selectedDialect);
validateRows(rows);
if (typeof cellTransform !== 'function') {
throw new TypeError('cellTransform must be a function.');
}
const body = rows
.map((row, rowIndex) =>
row
.map((cell, columnIndex) => {
const transformed = cellTransform(cell, { rowIndex, columnIndex });
if (typeof transformed !== 'string') {
throw new TypeError(`Transformed cell ${rowIndex}:${columnIndex} must be a string.`);
}
return encodeCell(transformed, selectedDialect);
})
.join(selectedDialect.delimiter),
)
.join(selectedDialect.recordTerminator);
const terminator = selectedDialect.finalTerminator ? selectedDialect.recordTerminator : '';
const bom = selectedDialect.bom ? '\uFEFF' : '';
return bom + body + terminator;
}
export function formulaShapeKind(value) {
if (typeof value !== 'string') {
throw new TypeError('Formula-shape inspection requires a string.');
}
if (value.length === 0) return null;
return FORMULA_SHAPED_PREFIXES.has([...value][0]) ? [...value][0] : null;
}
/**
* An illustrative lexical transform for a specifically approved workflow.
* Passing its tests does not certify behavior in any spreadsheet product.
*/
export function apostropheGuardForNamedWorkflow(value) {
return formulaShapeKind(value) === null ? value : `'${value}`;
}
oracle.mjs
/**
* Small test-only parser written independently from the serializer.
* It supports only this fixture's explicit delimiter, quote, BOM, and line-ending rules.
*/
export function parseDelimited(input, dialect, { expectedColumns } = {}) {
if (typeof input !== 'string') throw new TypeError('Input must be a string.');
if (!dialect || typeof dialect !== 'object') throw new TypeError('Dialect is required.');
let text = input;
if (dialect.bom) {
if (!text.startsWith('\uFEFF')) throw new SyntaxError('Required UTF-8 BOM marker is absent.');
text = text.slice(1);
} else if (text.startsWith('\uFEFF')) {
throw new SyntaxError('Unexpected BOM for this dialect.');
}
const delimiter = dialect.delimiter;
const quote = dialect.quote;
const terminator = dialect.recordTerminator;
const rows = [];
let row = [];
let field = '';
let state = 'field-start';
let recordTouched = false;
let index = 0;
const endField = () => {
row.push(field);
field = '';
state = 'field-start';
};
const endRecord = () => {
endField();
rows.push(row);
row = [];
recordTouched = false;
};
while (index < text.length) {
if (state !== 'quoted' && text.startsWith(terminator, index)) {
endRecord();
index += terminator.length;
continue;
}
if (state !== 'quoted' && text.startsWith(delimiter, index)) {
endField();
recordTouched = true;
index += delimiter.length;
continue;
}
const character = text[index];
if (state === 'field-start') {
if (character === quote) {
state = 'quoted';
recordTouched = true;
index += 1;
continue;
}
if (character === '\r' || character === '\n') {
throw new SyntaxError(`Unexpected line ending at offset ${index}.`);
}
field += character;
state = 'unquoted';
recordTouched = true;
index += 1;
continue;
}
if (state === 'unquoted') {
if (character === quote) {
throw new SyntaxError(`Quote inside an unquoted field at offset ${index}.`);
}
if (character === '\r' || character === '\n') {
throw new SyntaxError(`Unexpected line ending at offset ${index}.`);
}
field += character;
index += 1;
continue;
}
if (state === 'quoted') {
if (character === quote) {
if (text[index + 1] === quote) {
field += quote;
index += 2;
} else {
state = 'after-quote';
index += 1;
}
} else {
field += character;
index += 1;
}
continue;
}
throw new SyntaxError(`Unexpected character after closing quote at offset ${index}.`);
}
if (state === 'quoted') throw new SyntaxError('Unterminated quoted field.');
if (state !== 'field-start' || row.length > 0 || recordTouched) endRecord();
if (expectedColumns !== undefined) {
if (!Number.isInteger(expectedColumns) || expectedColumns < 1) {
throw new TypeError('expectedColumns must be a positive integer.');
}
rows.forEach((parsedRow, rowIndex) => {
if (parsedRow.length !== expectedColumns) {
throw new SyntaxError(
`Row ${rowIndex} has ${parsedRow.length} cells; expected ${expectedColumns}.`,
);
}
});
}
return rows;
}
csv-exporter.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
import { Buffer } from 'node:buffer';
import {
DIALECTS,
apostropheGuardForNamedWorkflow,
formulaShapeKind,
serializeCsv,
} from './csv-exporter.mjs';
import { parseDelimited } from './oracle.mjs';
function assertRoundTrip(rows, dialect, options = {}) {
const serialized = serializeCsv(rows, { dialect, ...options });
const parsed = parseDelimited(serialized, dialect, { expectedColumns: rows[0].length });
return { serialized, parsed };
}
test('plain comma/CRLF output matches a literal golden', () => {
const rows = [
['id', 'note'],
['1', 'plain'],
];
const { serialized, parsed } = assertRoundTrip(rows, DIALECTS.commaCrLf);
assert.equal(serialized, 'id,note\r\n1,plain\r\n');
assert.deepEqual(parsed, rows);
});
test('delimiter, quotes, CRLF, and edge spaces match a literal golden', () => {
const rows = [
['kind', 'value'],
['delimiter', 'north,west'],
['quote', 'He said "hello"'],
['multiline', 'line 1\r\nline 2'],
['space', ' left and right '],
];
const golden =
'kind,value\r\n' +
'delimiter,"north,west"\r\n' +
'quote,"He said ""hello"""\r\n' +
'multiline,"line 1\r\nline 2"\r\n' +
'space," left and right "\r\n';
const { serialized, parsed } = assertRoundTrip(rows, DIALECTS.commaCrLf);
assert.equal(serialized, golden);
assert.deepEqual(parsed, rows);
});
test('empty cells remain cells, including at the end of a row', () => {
const rows = [
['a', '', 'c'],
['tail', '', ''],
];
const { serialized, parsed } = assertRoundTrip(rows, DIALECTS.commaCrLf);
assert.equal(serialized, 'a,"",c\r\ntail,"",""\r\n');
assert.deepEqual(parsed, rows);
const oneEmptyCell = [['']];
assert.equal(serializeCsv(oneEmptyCell), '""\r\n');
assert.deepEqual(parseDelimited('""\r\n', DIALECTS.commaCrLf), oneEmptyCell);
});
test('UTF-8 strings and distinct Unicode sequences survive semantically', () => {
const precomposed = 'é';
const decomposed = 'e\u0301';
assert.notEqual(precomposed, decomposed);
const rows = [
['city', 'rtl', 'symbol', 'precomposed', 'decomposed'],
['München', 'שלום', '😀', precomposed, decomposed],
];
const { parsed } = assertRoundTrip(rows, DIALECTS.commaCrLf);
assert.deepEqual(parsed, rows);
assert.notEqual(parsed[1][3], parsed[1][4]);
});
test('LF is emitted only by the explicitly named LF profile', () => {
const rows = [
['id', 'note'],
['1', 'line'],
];
const { serialized, parsed } = assertRoundTrip(rows, DIALECTS.commaLf);
assert.equal(serialized, 'id,note\n1,line\n');
assert.equal(serialized.includes('\r'), false);
assert.deepEqual(parsed, rows);
});
test('semicolon profile keeps a decimal comma and quotes its own delimiter', () => {
const rows = [
['amount', 'note'],
['12,50', 'alpha;beta'],
];
const { serialized, parsed } = assertRoundTrip(rows, DIALECTS.semicolonCrLf);
assert.equal(serialized, 'amount;note\r\n12,50;"alpha;beta"\r\n');
assert.deepEqual(parsed, rows);
});
test('named spreadsheet-view profile has one UTF-8 BOM and quotes every cell', () => {
const rows = [
['code', 'city'],
['00123', 'München'],
];
const { serialized, parsed } = assertRoundTrip(rows, DIALECTS.namedSpreadsheetView);
const bytes = Buffer.from(serialized, 'utf8');
assert.equal(serialized, '\uFEFF"code","city"\r\n"00123","München"\r\n');
assert.deepEqual([...bytes.subarray(0, 3)], [0xef, 0xbb, 0xbf]);
assert.equal(serialized.slice(1).includes('\uFEFF'), false);
assert.deepEqual(parsed, rows);
});
test('type-looking values remain strings in the bounded Node round trip', () => {
const rows = [
['code', 'large-id', 'date-like', 'scientific-like'],
['00123', '9007199254740993', '2026-09-02', '1e3'],
];
const { parsed } = assertRoundTrip(rows, DIALECTS.commaCrLf);
assert.deepEqual(parsed, rows);
assert.ok(parsed[1].every((value) => typeof value === 'string'));
});
test('noncanonical missing final terminator parses, then canonicalizes on export', () => {
const parsed = parseDelimited('a,b', DIALECTS.commaCrLf, { expectedColumns: 2 });
assert.deepEqual(parsed, [['a', 'b']]);
assert.equal(serializeCsv(parsed), 'a,b\r\n');
});
test('a wrong dialect fails the declared column-count contract', () => {
const commaText = serializeCsv(
[
['id', 'note'],
['1', 'plain'],
],
{ dialect: DIALECTS.commaCrLf },
);
assert.throws(
() => parseDelimited(commaText, DIALECTS.semicolonCrLf, { expectedColumns: 2 }),
/expected 2/u,
);
});
test('benign formula-shaped cells are detected before serialization', () => {
const harmlessRows = [
['kind', 'value'],
['equals', '=1+1'],
['plus', '+1'],
['minus', '-1+2'],
['at', '@SUM(1,1)'],
['tab', '\t=1+1'],
['carriage-return', '\r=1+1'],
['line-feed', '\n=1+1'],
['full-width-equals', '=1+1'],
['full-width-plus', '+1'],
['full-width-minus', '-1+2'],
['full-width-at', '@SUM(1,1)'],
['separator-boundary', 'note",=1+1'],
];
const raw = assertRoundTrip(harmlessRows, DIALECTS.commaCrLf);
assert.deepEqual(raw.parsed, harmlessRows);
assert.equal(formulaShapeKind(raw.parsed[1][1]), '=');
assert.equal(formulaShapeKind(raw.parsed.at(-1)[1]), null);
const expectedGuardedRows = harmlessRows.map((row) => row.map(apostropheGuardForNamedWorkflow));
const guarded = assertRoundTrip(harmlessRows, DIALECTS.namedSpreadsheetView, {
cellTransform: apostropheGuardForNamedWorkflow,
});
assert.deepEqual(guarded.parsed, expectedGuardedRows);
assert.ok(guarded.parsed.flat().every((value) => formulaShapeKind(value) === null));
assert.equal(guarded.parsed.at(-1)[1], 'note",=1+1');
});
test('serializer rejects semantic ambiguity and shape drift', () => {
assert.throws(() => serializeCsv([]), /non-empty array/u);
assert.throws(() => serializeCsv([[]]), /at least one cell/u);
assert.throws(() => serializeCsv([['a', 'b'], ['c']]), /declared width/u);
assert.throws(() => serializeCsv([['a'], [null]]), /must be a string/u);
assert.throws(() => serializeCsv([['a'], [42]]), /must be a string/u);
assert.throws(() => serializeCsv([['a'], [9007199254740993n]]), /must be a string/u);
});
test('bounded parser rejects malformed quoting and unexpected BOM policy', () => {
assert.throws(
() => parseDelimited('a,"unterminated', DIALECTS.commaCrLf),
/Unterminated/u,
);
assert.throws(
() => parseDelimited('\uFEFFa,b\r\n', DIALECTS.commaCrLf),
/Unexpected BOM/u,
);
assert.throws(
() => parseDelimited('"a"tail,b\r\n', DIALECTS.commaCrLf),
/after closing quote/u,
);
});
package.json
{
"name": "csv-exporter-contract-fixture",
"private": true,
"type": "module",
"scripts": {
"test": "node --test csv-exporter.test.mjs"
}
}
Run the Package-Free Fixture
Node 24 includes the stable node:test runner, so the fixture needs no registry packages. It uses node:assert/strict for comparisons and Buffer for the BOM bytes.
node --test csv-exporter.test.mjs
# Exact command used for the verified Node v24.19.0 evidence run:
& 'C:\Users\benny\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe' --test 'D:\CodexTemp\blog-publisher-20260902\csv-fixture\csv-exporter.test.mjs'
The suite has four explicit profiles: comma/CRLF without BOM, comma/LF without BOM, semicolon/CRLF without BOM, and a quoted UTF-8-BOM spreadsheet-view profile. It also deliberately parses a comma file under the semicolon contract and requires the expected-column check to fail. A consumer should not silently accept one wide cell when two columns were promised.
Write Outcomes at the Size of the Evidence
| Evidence | Outcome | What it means |
|---|---|---|
| Goldens, independent parse, fixed width, and exact string fixtures pass for one dialect | RELEASE: machine contract | The named serializer profile earned its bounded syntax and semantic claims. |
| Delimiter, BOM, header, line ending, null policy, or column schema is unknown | HOLD | The exporter does not yet have a testable contract. |
| Named product/build/locale/import route and save-reopen checks preserve the approved synthetic values | RELEASE: named view | Only that recorded spreadsheet workflow is approved. |
| Formula-shaped untrusted cells have no approved destination policy, or a destination remains untested | HOLD | Correct CSV quoting alone does not settle spreadsheet interpretation. |
The most useful review question is not “Does this code export CSV?” It is “Which of the three contracts has this evidence actually earned?” Once that question is visible, an AI-written exporter becomes much easier to judge. The commas have a declared grammar. The strings have an owned meaning. The spreadsheet has a name, a version, a locale, and a path. Everything beyond those boundaries remains honestly on hold.
Review Your Draft in One Workspace
Check AI-likelihood signals, revise structure and tone, and review the result before you publish.
Open AI Humanizer