mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
80e32b4e0a
* docs: make test-pg is not safe against itself in one worktree (BUG-3033) CLAUDE.md said the target is safe across worktrees, which is true and incomplete. TEST_PG_PROJECT is basename + cksum of the absolute path, so two overlapping runs in ONE worktree share a compose project: the second `up -d --wait` attaches to the first run's container, and whichever run finishes first executes the unconditional `down -v` and destroys the other's database mid-test. Measured on the BUG-3000 trail, where the survivor's connection-refused failures were first read as evidence about the code. The note also records that a PASSING run carries ~41 redis connection-refused lines, so that noise is not the diagnostic — the failure text is. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * fix(items): six hand-built doors carry the stale-body marker (BUG-3033) BUG-3000 put content_state on models.Item, so every door that serialises the struct inherits it. These build their own shape and inherit nothing: - PlaybookRunResponse.Body (handlers_playbooks.go) - AgentBootstrapConvention.Content (handlers_bootstrap.go) - BootstrapIncludeItem.Content, bodies mode (handlers_bootstrap.go) - the artifact export's markdown+YAML (handlers_artifact_export.go) - the MCP item resource's composed markdown (mcp/resources.go) - `pad playbook show` / `pad playbook run`, which decode narrow structs of their own and print the body The last two are in the unit because an agent reads what the CLI prints, not the wire: a marker the server sets and the CLI drops is a fix its reader never sees — the same gap this bug describes for the MCP resource, whose fetcher reads stdout and cannot surface a successful command's stderr. BootstrapIncludeItem is a fifth server door the bug does not name, found by this unit's sweep and folded in under CONVE-18 rather than filed: same file, same handler, same shape as the conventions door beside it. The artifact export is a portable format, so the marker goes into the existing provenance block as an additive omitempty key with NO format_version bump. Decode uses a plain yaml.Unmarshal with no KnownFields, so an older binary ignores the key; format_version is compared with exact equality, so a bump would reject artifacts in both directions for an added optional key. A current item's bytes are unchanged. It signals rather than refuses; BUG-3032 owns the signal/refuse/accept decision for the bundle format, and an additive key forecloses neither of the other two. Every marker is emitted only where a real body is — the includes site sets it inside the same wantBodies guard that sets Content — so a metadata-only projection cannot claim its absent body is stale. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * test(items): both-directions coverage for the six BUG-3033 doors One test per door, both directions on one item, ABSENCE ASSERTED FIRST. The order is the instrument: a marker stamped unconditionally satisfies every positive assertion in these files, so a presence leg proves something only after the same door has been shown to stay silent about a current row. Assertions are on the raw wire form — a decoded map, the exported bytes, the rendered markdown — rather than on a Go struct, because a struct round-trip passes even when the field is never serialised. That is exactly the failure this bug is: a value computed correctly in the store and dropped by a projection on the way out. Four legs beyond the plain both-directions pair: - PlaybookShowResponse inherits the marker through an embed nobody tested. Pinned, because a refactor to an explicit projection — the shape BootstrapCollection and BootstrapRole already took for size — would drop it silently. - A metadata-mode include must carry NO marker even when its item is stale, since it serves no body; the premise (that the group really withheld the body) is asserted before the conclusion. - A current artifact must not merely lack the value but lack the KEY, and a marked one must differ by exactly one line and still decode with body and fields intact. Those are the compatibility claims made testable rather than asserted in a comment. - An UNRECOGNISED content_state must not render as this unit's specific claim about a live editor. The vocabulary is shared with BUG-2995's content_outcome and is expected to grow. The CLI and MCP legs drive the command and the resource rather than the helper and the formatter (CONVE-19): a direct call vouches for the function while leaving the narrow decode struct — where the defect actually lives — free to omit the key. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * docs: the read-side content_state marker (BUG-3033, CONVE-23 sweep) Three consumer-facing artifacts described the write-side warning and said nothing about its read-side counterpart, so an agent reading them would still conclude a stale read is silent. That was already true before this change — BUG-3000 shipped the marker without documenting it outside the field's own doc comment — and adding six more doors is what makes it worth closing rather than inheriting. - CLAUDE.md gains a read-responses bullet beside the three write warnings: what the value means, that it is emitted only where a real body is, that the window is UNBOUNDED because nothing server-side flushes, and that the correct response is re-read later, never re-send. - internal/mcp/instructions.md had a sentence saying a get before the flush "answers with the PREVIOUS content" — true and now incomplete, since the get says so itself. It also gains the case an agent cannot infer from its own write: someone else's tab holds the edits and you wrote nothing. pad_playbook's entry says a marked body may be superseded, and that this is a reason to re-read, not to refuse to run. - internal/mcp/version.go gains a POST-0.31 NO BUMP entry. The disposition is BUG-2304's, argued at length in the BUG-2995 entry above it: nothing is dropped or retyped, and a current item renders exactly as before. The item resource is the half worth naming — a text resource has no additive key, so the marker is a line of markdown, and a consumer diffing that text sees a new block when and only when the body it is about is stale. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * docs: contract comments name the marker they now emit (BUG-3033) formatItemAsMarkdown's doc listed the blocks it composes and this change added one; the artifact's compatibility claim now cites the instrument that proves it rather than asserting it (testdata/*.golden.md pin the encoded bytes exactly and were not touched to land the key). Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * fix(cli): pad item edit warns before seeding $EDITOR from a stale body (BUG-3033) A seventh door, found by re-sweeping the population, and the only one whose consequence is not a confusing read. `pad item edit` is a read-modify-write over the WHOLE body: it seeds the editor from the row and PATCHes the entire edited text back with no concurrency token. When the row is behind the live collaborative document, the editor is seeded with content that predates edits which exist and are durable in the op-log, and the save replaces them with a version derived from a state before them — visible to neither the person editing nor the person whose tab holds them. Unlike an ordinary lost update this needs only ONE writer plus an open tab; the other side is the unflushed state of a document nobody is typing in. The warning is printed BEFORE the editor launches, which is the only point at which it can still change what the user does — afterwards it would be read beside an "Updated TASK-5" line. Its wording differs from its two siblings for the reason a third one exists at all: the consequence is that saving destroys something, not that the text is old. It warns and proceeds. Whether `edit` should refuse, prompt, or send a DIFF instead of the whole body is a behaviour decision with its own item (BUG-3035, which also asks whether `item update --stdin` joins any gate `edit` grows), not something to settle inside a sweep for missing markers. Two tests, and the second is the load-bearing one: the gating table vouches for the helper, and the end-to-end leg vouches for the BINDING — a fake editor announces itself on its own stderr, which OpenInEditor wires to os.Stderr, so the ordering claim is an assertion about one stream rather than about presence. Both mutants (drop the call; move it after the editor) are killed by that leg alone. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * fix(items): playbook summaries carry the marker; two tests stop passing wrongly Codex round 1, three findings, all valid. P2 — an eighth door, and this unit's population table missed it with the precedent already in the tree. A playbook `summary` is the body's first paragraph truncated, so a stale body makes a stale summary, and an agent ROUTES on that description. BUG-3000 put the marker on cli.ItemSummary.ContentPreview on exactly this reasoning ("a content_preview is content"); calling the same bytes a summary does not change where they came from. Carried on AgentBootstrapPlaybookMeta, which both the bootstrap blob and the /playbooks endpoint render, and through both CLI renderers — one stderr line naming the stale refs rather than a per-row marker, since a per-row marker on a long list is noise and a bare count sends the reader back to diff it by hand. Set only when a summary was actually emitted: no summary, no claim. It also does not mark the other metadata, which comes from the fields blob that the collaborative document does not hold. P3 — the edit tests accepted an always-on warning. They supplied only a stale response, so a call site forced to warn unconditionally passed them, and the helper's gating table cannot see a wiring error. Now both directions at the COMMAND boundary, with two premises asserted before any conclusion: the fake editor ran, and the edited body was really PATCHed. P3 — the artifact test accepted a corrupted body. It compared line counts and asserted only that the decoded body was non-empty, so an export that replaced the body with unrelated text passed. Now the EXACT body on both exports, plus byte equality after removing the marker line and normalising exported_at — with a check that the normalisation removed something, so the comparison cannot pass by stripping both sides. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * fix(items): backlink + search snippets carry the marker; add the class guard Codex round 2: two more doors and two test gaps, all valid. P2 — backlink snippets. models.Backlink.Snippet is a window cut from the SOURCE item's body, so a stale body makes a stale snippet. This unit's sweep could not see it: its vantage point was internal/server, internal/mcp and internal/cli, and this shape is built in the STORE. The predicate needed a non-default table alias (the backlink queries call items `s`), so contentStateSQL became contentStateSQLFor(alias) with the old constant as its "i" case — a function rather than a second copy, since two hand-maintained spellings of this predicate would be two chances to drift. The marker describes the SOURCE of each link, never the target the caller asked about, and a test asserts marking the target changes nothing. P2 — `pad item search` and `pad item backlinks` printed body-derived snippets and dropped the marker the wire already carried. Both now use one shared renderer with the playbook summaries, so the surfaces cannot word the same fact three ways. P3 — the edit test asserted only that a PATCH occurred, though it claimed to exercise saving; it now asserts the exact content that reached the server. P3 — the summary test had one stale row, which cannot tell a correct renderer from one naming every ref or only the first; it now interleaves stale and current entries, asserts the complete set and EXACTLY ONE warning line, and covers the bootstrap renderer that had no test at all. CONVE-35 — three rounds each found another member of this class, which means the reviews were measuring my enumeration rather than the code. So the table is now a test: every site turning an item body into text under another name is listed with its ruling, keyed on the exact call LINES with multiplicity, and anything else fails closed. Its first draft keyed on file+helper and a negative control caught the hole: a SECOND call site in an already-listed file passed silently — and handlers_bootstrap.go, which is listed, is exactly where a new projection would be written. Four controls now run: a new compiling call site goes red, a broken walk root goes red, a removed site goes red, and comments mentioning a helper stay green. The guard states what it cannot see — a door built on a helper it does not know — and its failure message asks for the helper, not just the site. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * fix(items): title-only search results drop the body marker; guard parses Go Codex round 3, six findings, all valid. P2, and a defect this unit INTRODUCED: Store.Search's direct-ref and numeric paths set Snippet = Item.Title and clear the body, but kept the BODY's content_state — so the new CLI renderer announced that a TITLE was derived from a stale body. Cleared at the source rather than patched in the renderer, which restores models.Item.ContentState's documented invariant and fixes every other consumer of those results at the same time. The FTS path beside it keeps the marker deliberately: there the snippet IS cut from the body, so the marker still qualifies text the result serves. A test asserts both directions, because "clear it everywhere" would delete the signal this unit exists for. P2 — the enumeration guard was itself fail-open. It passed a one-line function calling a helper, a helper assigned to a local and called through it, a comment inside the call expression, and an argument changed below the opening line of a multiline call, while FAILING on prose and strings that merely mentioned a helper. Wrong in both directions is the signature of a scanner not reading the language it claims to read, so it now parses Go: call expressions matched on the AST, where comments and line breaks are not facts. A helper NAME used as a value is reported too and fails closed, since the call through an alias is invisible. All four of codex's bypasses now go red and both false-positive cases stay green. P2 — FTSSnippet was missing from the helper list entirely, so four existing derivation sites were outside the table. Added and ruled; the stated boundary was understating itself and now says so. P2 — browser renderers (BacklinksPanel, CommandPalette) drop the marker. The wire and the TypeScript type now carry it; where a stale badge belongs in a panel or a palette is a UI decision, filed as BUG-3050 on the same grounds BUG-3033 was split from BUG-3000. P3 — cross-workspace backlinks had no coverage: clearing ContentState on every result of that query left the same-workspace test and the existing cross-workspace tests all green. P3 — the snippet tests had ONE stale row while their comment claimed interleaving, the same gap fixed for summaries one test earlier; a renderer reporting only the first stale source passed them. Both now use multiple stale rows separated by current ones, and that mutant dies on all four legs. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA * fix(items): close the guard's helper-named bypass; seven tests stop passing wrongly Codex round 4, enumeration-prompted. No new Go door — its census of 434 non-test files found the two remaining Go omissions are already filed (workspace export on BUG-3032, cross-workspace copy attached to it) and the browser set belongs to BUG-3050. Everything below is in-class. The guard had a real bypass. A declaration whose own NAME was a helper got a special traversal that checked CallExprs and returned false, so the value-use arm never ran inside it: a compiling, callable `func (p ReviewProjection) PlaybookSummary() string { render := contentPreview; return render(p.Content) }` served unmarked stale prose while the guard passed, and renaming the method made it fail. The arm was redundant as well as wrong — notAValueUse already marks every FuncDecl.Name — so it is gone. It also skipped a `build` directory it never disclosed, which hid a real file at internal/server/build/. Three doc claims it made were false and are corrected rather than softened: "anywhere under internal/ or cmd/" (it skips four directories, now named), "helper values fail closed" (they did not), and that the browser surface is a pure rendering choice (the share paths do field copies that discard the marker before any rendering decision). It now states plainly that its claim is narrow — no UNRULED USE of four helpers — and must not be cited as proof that every door carries the marker. Seven mutations survived the tests, all the same species: they asserted the MARKER without asserting the BODY it qualifies. Replacing a served body with "WRONG BODY", dropping it entirely, or suppressing stale summaries and snippets while keeping the warnings all passed. Every door test now pins the exact text, and the backlink premise finally sees the snippet it is about — cli.Dim writes to color.Output, so an os.Stdout swap missed it and an earlier draft recorded that as a limit and asserted row headings instead, which is precisely what let that mutation live. Also: the numeric direct-ref path had no coverage (the test queried a prefixed ref only), backlink source-correlation passed when replaced with "any op-log row exists anywhere", and the FTS leg never asserted a snippet was produced. Search's surviving-marker comment claimed it always qualifies served text; an FTS title match on an empty-bodied item is the counter-example, and the comment now says so. Claude-Session: https://claude.ai/code/session_01PDYjP67KTAcqpFpo3ijAAA