[mine-blindspots hyperthrive corpus] TrailingProhibition never scans markdown table cells #159
Labels
No labels
P0
P1
P2
P3
bug
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#159
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Cross-project filing from a
mine-blindspotsrun against thehyperthrive-websitesrepo as external corpus. Bucket (c): scanning-surface gap — the cop's logic is fine, but a whole document region (markdown tables) never reaches it.Problem
hyperthrive-websites/context/stack/decisions.md:18(a markdown table "Why" cell):The trailing clause "— never raise it recklessly, but it is expected to rise" sits inside a table cell and is never examined by
TrailingProhibitionbecause table rows never becomeListItems::Items.Detection
plugins/os-aidd-lint/lib/aidd_lint/list_items.rb:7:This only matches bullet/numbered-list lines. A markdown table row (
| cell | cell | cell |) never matchesMARKER, soListItems.buildnever yields anItemfor it, andTrailingProhibition#investigate(which only callsListItems.build, pertrailing_prohibition.rb:34) never sees table-cell text at all — regardless of whether the clause-splitting logic inside would otherwise catch it.Correction
Add a table-cell scanner (parse
| ... | ... |rows, split into cells) that feeds the same clause-check logic (trailing_strong_clause/trailing_parenthetical_not)TrailingProhibitionalready uses for list items, rather than requiring table content to first become aListItems::Item.Pass/fail examples
Deliverability physics, not ambition. 5/day is an opening ramp cadence, not a permanent cap — never raise it recklessly, but it is expected to rise.(the trailing "— never raise it recklessly" clause).- Deliverability physics, not ambition — never raise it recklessly.if it already leads a bullet with "never" viaLEADS_WITH_PROHIBITION, or a table cell with no prohibition language at all.Provenance
/os-aidd-lint:mine-blindspotsrun, external corpushyperthrive-websites, bucket (c), cop citationlist_items.rb:7(MARKER), evidencehyperthrive-websites/context/stack/decisions.md:18. Verified independently by a second agent pass against the live cop source and the live corpus file. 2026-07-27.Fix direction
Add a table-cell scanner feeding the same clause logic
TrailingProhibitionalready has, so table content is not structurally invisible to the cop.Discoverer: hyperthrive-websites, session id unavailable, 2026-07-27. Filed from a mine-blindspots run against this repo as external corpus.
Classified DECISION-NEEDED — not implemented. The scanning-surface gap is real and confirmed; what is not settled is whether this cop's advice is coherent inside a table cell.
Headline
Table scanning is cheap to build and finds 5 new clauses in this repo — but 3 to 4 of them are cells where the prohibition is already leading its own sentence, and the cop's fix instruction ("promote it to its own bullet") is impossible to follow inside a table. Decide what the cop should say about a table cell before wiring the scanner.
What I measured
I prototyped a table-cell scanner (parse pipe rows, skip
|---|---|separators, honour the fence mask, feed cells to the existingtrailing_strong_clause/trailing_bare_not/trailing_parenthetical_notlogic — no change toListItems::MARKER) and ran it over the 99 files in this repo's lint scope. 5 new offenses, verbatim:it never authors hashes, token counts, safety tiers, or reversibility.Never writes files or calls gitdo NOT supply \git_state` or `safety_tier``(not a clean delete)do NOT mark \extraction_complete``Row 2 is the instructive one. That cell reads roughly
Judge only. Never writes files or calls git— the prohibition already leads its own sentence, which is precisely the shape the cop exists to produce. It is flagged only because a table cell has no notion of "leading the bullet":leads_with_prohibition?tests the start of the cell, and a cell routinely packs two or three sentences into one column because tables compress. In a bullet list, "clause 2 of 3" means buried. In a table cell, it often just means "second sentence".Options
Option A — scan cells, reuse the message as-is.
decisions.md:18's— never raise it recklesslyis missed.|---|---|row that instruction cannot be followed. The author's only compliant move is to destroy the table.Option B — scan cells, but gate on the prohibition NOT leading any sentence in the cell, and give tables their own message.
Never writes files or calls git→ suppressed (it leads a sentence).— never raise it recklessly, but it is expected to rise→ still caught (mid-sentence, after a dash).Option C — no change. Tables stay invisible. Cost: a locked-decision table (exactly the
decisions.mdcase that filed this) is a high-value place to hide a prohibition, and it is unlinted.Recommendation
Option B, confidence 65%.
Single deciding reason: the scanner is the easy half and Option A gets it wrong in a way that is worse than the miss — a lint that issues an unfollowable instruction trains authors to ignore the cop. Confidence is only 65% because Option B's per-sentence exemption is a new heuristic with no corpus behind it beyond these 5 rows; a larger sample might move me to C.
This is genuinely your call, because it is a scope question, not a bug: is TrailingProhibition a list-item cop (its message, its exemption logic, and its docstring all assume bullets) or a document cop? Answering that decides #159 and settles how the next surface-gap ticket gets triaged.
Default if nothing is said
Nothing ships for this issue. Tables remain unscanned. Issues #158 and #161 are already implemented and labelled
reviewon branchaidd-lint-trailing-prohibition-gapsand are independent of this decision — the prototype scanner above was measured but not committed.Option B implemented per Jared's decision. Branch
aidd-lint-trailing-prohibition-gaps, commit1d29fb8. TDD.What shipped
New
AiddLint::TableCells(lib/aidd_lint/table_cells.rb) plus acheck_cellpath on the cop.Rather than write a new row parser, it builds on the existing
TableMask, which was already in the codebase and already requires a header row followed by a separator row. That means a stray pipe line in prose is not mistaken for a table — pinned bytest_ignores_a_pipe_row_that_is_not_part_of_a_real_table.ListItems::MARKERis untouched, so no other cop's scanning surface moved.Three cell-specific rules, each pinned by a test:
— never raise it recklesslyis a clause that leads withnever), while a sentence-level gate keeps it and still exempts... Never writes files or calls git; never re-reads disk.(not a clean delete)in a category table is a clarification, not a rule.prohibition buried mid-cell ('...') -- lead the cell with it, or move the rule out of the table.A test asserts the bullet-shaped message does not appear on a cell offense.Tests
Six new tests on this issue: buried mid-cell (the ticket's must-fail), sentence-leads exemption, table-shaped message, parenthetical exemption, separator/clean-cell quiet, non-table pipe row quiet.
Corpus delta
Measured properly this time — baseline run in a detached
git worktreeat the parent commit, same 99 files, so nothing else in the tree could contaminate it.25 → 28. +3 offenses, 0 regressions, 0 removals. All three are cell offenses and all three are genuine:
Compare the memo's Option A prototype, which produced 5 on the same corpus. Option B suppressed exactly the two I flagged as unfollowable —
Never writes files or calls git(already leads a sentence) and(not a clean delete)(definitional aside) — and kept all three real ones. The remaining twodo NOT ...hits are mid-cell after a semicolon or dash, which is the smell, and the new message is advice a table author can actually act on.My 65% confidence in the memo was mostly about whether the per-sentence heuristic would hold up beyond the 5 sampled rows. It separated the corpus cleanly, so I'd now put it higher.
Labelled
review, not closed.ADR recorded: ADR-0069 (commit
9ba7190).Ran
/os-adr:findover the cop/scanner paths. It returned ADR-0058/0059/0060/0062/0063/0064/0067/0068 — all about plugin hosting, trigger architecture, the Tier-2 catalog, and which files are in scope (0062). Nothing governed which document regions a cop may read.That mattered because
TableMask's own docstring says it exists "so cops can skip table content when scanning body prose" — this change is the first to deliberately scan into tables, inverting what that primitive was built for. Per cc-os CLAUDE.md ("before changing/replacing an approach this codebase already uses… record it with/os-adr:create"), that needed a record rather than a silent inversion.docs/adr/0069-tier-1-cops-may-scan-markdown-table-cells-as-a-distinct-surface-with-surface-specific-exemption-and-remedy.mdbinds any cop that opts in to three constraints, so the next surface-gap ticket has a settled answer instead of relitigating:TableCellsprimitive, built onTableMask— one definition of "a table", andListItems::MARKERis never widened (adding a surface must not move another cop's surface).TableMaskkeeps its masking role for prose-scanning cops; scanning into tables is opt-in per cop, not a global redefinition of what a document is. The alternatives section records the measurements behind each rejection, including why a clause-level exemption gate was rejected — it would have suppressed this ticket's own evidence line.Suite still
296 runs, 718 assertions, 0 failures, 0 errors, 0 skips.