Summary
--protocol-check's "no template placeholder text in data rows" item reports an affirmative clean on workbooks that carry template hint text in a populated data row — including hint text that has already reached the package graph as a node identifier. Both aero encodings contain a ValidationResult whose id is the literal string Stable URI or local ID (the template's own hint for that column), and protocol-check passes both.
Root cause: an off-by-one between where hints survive and where the scan starts
The pack template reserves the row under each header for hint text and expects data from the row after it. The scan honors that layout: it begins at head + data_offset (= header + 2), treating the hint row as the template's own.
But the extractor writes its first data row into the hint row (header + 1). And excel_writer clears hint text only in columns where the model wrote a value (src/uofa_cli/excel_writer.py:155 — the clearing is per-cell, deliberate, and correct as far as it goes). So a first data row the model fills partially keeps the template's hint in every untouched column — and that row is, by construction, the one row the scan never examines.
Net effect: the single most likely location for a hint leak is the single location the check skips. The check does not say "skipped"; it says clean.
Why this is worth more than a one-line fix
This is F-3d's mirror image. F-3d (Johnson pilot) found the extractor leaves hint text in data rows; this check was written to catch exactly that, and it passes on exactly that. It is the seventh instance of the confirm-only instrument shape in this project's records: a check whose passing condition is satisfiable without the property it exists to verify. The detection route is also worth recording: no check fired; a review session read a node name skeptically and traced it. The mechanical layer failed closed-eyes-open; the review layer caught it — which is the system's intended division of labor, and the reason the fix below stays mechanical rather than getting "smarter."
Fix, three layers, all dumb on purpose
protocol-check: scan from head + 1 instead of head + data_offset. A populated hint row is by definition a leak; there is no legitimate state in which hint text coexists with data in that row. (src/uofa_cli/protocol_check.py, _SHEETS data_offset usage around line 150.)
Import-side guard: refuse (or fail protocol-check's package-side pass on) any node whose id matches the active pack's hint set (_template_placeholders, protocol_check.py:116). The workbook scan can be right and a future writer bug could still mint a hint-named node; the graph is the last line and should hold it.
Writer-side cure (the real one): when any data lands in the hint row, clear the entire row's hint text, not only the written columns — or move hints out of cell values entirely (comments/styling), making the leak impossible by construction rather than catchable after the fact.
Regression fixtures
A workbook whose first data row is partially filled in the hint row (the aero leak, minimal repro) — must fail the workbook scan after fix 1.
A package carrying a node id equal to a hint string — must fail the package-side guard after fix 2.
The current aero COU1/COU2 pre-correction workbooks are real-world fixtures; snapshot before the C3 correction lands.
Affected artifacts
aero COU1 and COU2: Validation Results C3 corrected to source-absent in the governed pass (correction surfaced in both AUTHOR_SUMMARY correction lists).
No effect on Johnson (its leak sites were caught and cleared in the pilot under F-3d).
protocol-check results predating the fix that reported the placeholder item clean should be read as "not checked at the hint row."
Summary
--protocol-check's "no template placeholder text in data rows" item reports an affirmative clean on workbooks that carry template hint text in a populated data row — including hint text that has already reached the package graph as a node identifier. Both aero encodings contain a ValidationResult whose id is the literal string Stable URI or local ID (the template's own hint for that column), and protocol-check passes both.
Root cause: an off-by-one between where hints survive and where the scan starts
The pack template reserves the row under each header for hint text and expects data from the row after it. The scan honors that layout: it begins at head + data_offset (= header + 2), treating the hint row as the template's own.
But the extractor writes its first data row into the hint row (header + 1). And excel_writer clears hint text only in columns where the model wrote a value (src/uofa_cli/excel_writer.py:155 — the clearing is per-cell, deliberate, and correct as far as it goes). So a first data row the model fills partially keeps the template's hint in every untouched column — and that row is, by construction, the one row the scan never examines.
Net effect: the single most likely location for a hint leak is the single location the check skips. The check does not say "skipped"; it says clean.
Why this is worth more than a one-line fix
This is F-3d's mirror image. F-3d (Johnson pilot) found the extractor leaves hint text in data rows; this check was written to catch exactly that, and it passes on exactly that. It is the seventh instance of the confirm-only instrument shape in this project's records: a check whose passing condition is satisfiable without the property it exists to verify. The detection route is also worth recording: no check fired; a review session read a node name skeptically and traced it. The mechanical layer failed closed-eyes-open; the review layer caught it — which is the system's intended division of labor, and the reason the fix below stays mechanical rather than getting "smarter."
Fix, three layers, all dumb on purpose
protocol-check: scan from head + 1 instead of head + data_offset. A populated hint row is by definition a leak; there is no legitimate state in which hint text coexists with data in that row. (src/uofa_cli/protocol_check.py, _SHEETS data_offset usage around line 150.)
Import-side guard: refuse (or fail protocol-check's package-side pass on) any node whose id matches the active pack's hint set (_template_placeholders, protocol_check.py:116). The workbook scan can be right and a future writer bug could still mint a hint-named node; the graph is the last line and should hold it.
Writer-side cure (the real one): when any data lands in the hint row, clear the entire row's hint text, not only the written columns — or move hints out of cell values entirely (comments/styling), making the leak impossible by construction rather than catchable after the fact.
Regression fixtures
A workbook whose first data row is partially filled in the hint row (the aero leak, minimal repro) — must fail the workbook scan after fix 1.
A package carrying a node id equal to a hint string — must fail the package-side guard after fix 2.
The current aero COU1/COU2 pre-correction workbooks are real-world fixtures; snapshot before the C3 correction lands.
Affected artifacts
aero COU1 and COU2: Validation Results C3 corrected to source-absent in the governed pass (correction surfaced in both AUTHOR_SUMMARY correction lists).
No effect on Johnson (its leak sites were caught and cleared in the pilot under F-3d).
protocol-check results predating the fix that reported the placeholder item clean should be read as "not checked at the hint row."