mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-11-06 11:07:50 +00:00
release-0.19.x
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ffc11387e1 |
feat(verify): verification evidence tied to a revision, slice 1 (TASK-3617) (#2228)
* feat(verify): verification evidence tied to a revision, slice 1 (TASK-3617)
Store, REST and the read-time judgment for per-criterion verdicts:
- item_verifications (SQLite 150, PG 120), append-only, with the NUL
census classified and trigger file 151 generated.
- internal/verification: criterion hash (whitespace-collapsed), the
removed / changed / superseded / current state, and the advisory
possibly_stale hint from a linked PR merged after the verdict.
- POST/GET /items/{ref}/verifications: atomic batch, seq-guarded so a
criterion hash is stored against the body it came from, via resolved
in-workspace and visibility-checked, activity "verified", item_updated
SSE.
- github_pr carries merge_sha / merged_at, fetched by pad github link
and reconcile.
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* test(verify): router tests, body-order sort, compress census, CLI merge fields (TASK-3617)
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* test(verify): close mutant holes M5 (re-spacing) and M8 (merged before the verdict); placeholder folds into missing (TASK-3617)
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* docs(api): the verifications endpoint (TASK-3617)
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* fix(verify): r1 findings: the retry re-checks access; a moved item is told from a gone via (TASK-3617)
- F1: when the item moves between the criteria read and the write, the
retry re-runs visibility and edit permission on the fresh item.
- F2: a zero-row insert re-checks the target in the transaction, so a
Postgres seq race retries (or 404s) instead of answering unknown_via.
- F3: verificationBeforeInsertHook test seam; tests for one move, an
item that keeps moving (409), and a grant revoked mid-retry (404).
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* fix(verify): r2 L1, the retry re-resolves via; gate findings (TASK-3617)
- The retry re-runs the via resolution and its visibility check, the same
re-check F1 added for the item; TestTASK3617_RetryRechecksVia.
- The state-history test asserted by row position, and one batch's rows
share a created_at, so their order is by random id: assert by criterion.
- nulscan.go's protected-column count (146: 27 JSON, 119 text).
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* test(store): the scan-cost pin follows the list, 146 columns (TASK-3617)
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* chore(verify): drop the comment resolveVia's doc comment superseded (TASK-3617 r3)
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* fix(verify): r4, verifications travel with a workspace export/import (TASK-3617)
- F1: VerificationExport, exported for the bundle's live items and
imported with the item and via ids remapped, every other value
verbatim. Each bundle row is checked before its insert (a refused
statement on Postgres loses the whole import); a row that fails, or
whose item did not import, is dropped and counted. Tests: a SQLite round
trip, the migrate-to-pg path (SQLite export into a Postgres import with
the slug preserved), and a hostile bundle on both backends.
- F2: docs/api.md says a criterion repeated in one request is
validation_error, as the code answers.
- Q2: gh's mergedAt zero time is read as not merged.
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* fix(store): item_verifications is a migrated table (TASK-3617 r4 gate)
The export now carries verifications, so the three lists that name what
`pad db migrate-to-pg` copies must name it too: MigratedTables (the NUL
preflight scans it), its export-section test map, and
migratedRefusalTables (the post-migration write refusal covers it).
Swept by item_reminders across cmd/, internal/ and docs/: those three
are the population.
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* chore(store): renumber TASK-3617 migrations after #2230 (sqlite 151/152, pg 121)
#2230 took sqlite 150 and pg 120 for decision_jobs.not_before. The
verification table is now sqlite 151 / pg 121, and its NUL trigger file
is 152_nul_invariant_triggers_post151 (regenerated, so the trigger names
carry pad_nul152_). References swept by the old file names and the bare
numbers in this branch's diff.
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
* test(verify): an id that is both a criterion and a placeholder is refused (TASK-3617, lead spot mutant)
Claude-Session: https://claude.ai/code/session_01DYmGZ1nCgxXCX4jPsQu5MW
|
||
|
|
47fe81ab99 |
fix(cli,mcp): explicit-limit callers read every page or say they are truncated (BUG-3793) (#2154)
* fix(cli,mcp): pad reconcile and pad github status read every page, and the MCP items resource says when it is truncated (BUG-3793)
Three callers set an explicit list limit and treated the capped answer
as complete: pad reconcile read one 500-row page of the workspace, pad
github status read 100 rows of each collection, and the MCP items
resource (pad://workspace/{ws}/items, 200 rows) read as the whole
workspace with no sign it was cut.
reconcile now uses ListAllItems, and github status a new
ListAllCollectionItems (its collection scan moved into
githubStatusScanItems so it is testable); both follow the server's
next-offset to the end. The resource asks for one row past its cap on
both transports (stdio --limit 201; remote fetchItemList honors the
flag) and, when that row comes back, serves 200 rows plus a second
text/plain block saying more exist. A list that fits is unchanged.
Resources are outside the tool catalog: no ToolSurfaceVersion move.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(cli): the completed-work report pages until the cutoff (BUG-3793, Codex r1 Medium)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
|
||
|
|
1204a31a73 |
feat(mcp): pad_item link-pr links a PR from supplied metadata, and unlink-pr removes it (TASK-3611) (#2098)
MCP had no PR-link action: the `field` description sent agents to a human, because `pad github link` reads the local branch and asks `gh`, and a field write of github_pr is refused (BUG-2696). An agent that opened a PR could not record it on the item. - `pad github link <ref> --pr-number N --pr-url U [--pr-title --pr-state --pr-branch --pr-repo]` links a PR the caller names. Nothing is fetched from GitHub, and it needs neither a checkout nor `gh`. Without the flags the command behaves as before. Under --format json, link and unlink answer with the item. - pad_item.action=link-pr / unlink-pr, on both transports. Remote PATCHes the typed github_pr / clear_github_pr members, and unlink-pr refuses an item with no link, as the CLI does. The catalog's `ref` is passed as the commands' item-ref positional. - github link / unlink leave the remote CLI-only table; github status stays. The BUG-2696 guard is re-aimed: these two must be routed, and no remaining hint may advertise the github_pr field write. ToolSurfaceVersion 0.95, additive (0.93 is #2096, 0.94 is #2097). Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ |
||
|
|
ecacc6d1b0 |
docs(cli): an example for every leaf command (TASK-2865, part 2) (#2022)
* docs(cli): an example for every pad item leaf command (TASK-2865, part 2: item) One common invocation per leaf, for agents first: the shape a caller would actually type, with TASK-5-style refs. The drift validator checks every one against the live tree. Each was also run against a throwaway server with refs mapped to real items; the only errors were preconditions (no signed-in user on a fresh install, a link the previous example had made, a reminder that had not fired). * docs(cli): an example for every pad workspace leaf command (TASK-2865, part 2: workspace) Validated by the drift validator. Run against a throwaway server: create, list, link, members, storage, export, import, deleted and switch succeed; invitations, audit-log and init refuse for want of a signed-in user or an initialized instance; invite surfaced BUG-3544 (a 500 on a fresh install). * docs(cli): examples for every pad project, auth and session leaf (TASK-2865, part 2) Validated by the drift validator, and run against a throwaway server where non-interactive: every project read, session list/prune/status and auth configure succeed (configure needs --mode without a terminal, so the example names it); whoami answers 'not logged in' on a fresh client. login, setup, the 2fa pair and reconcile (needs gh) were not run. * docs(cli): examples for the remaining leaf commands (TASK-2865, part 2) agent, bootstrap, collection, db, github unlink, mcp, playbook, push, role, server, tag, token, watch and webhook. Validated by the drift validator; run against a throwaway server where safe: collection create/delete/restore, role create/list/delete, playbook list/show/run, tag/webhook list, bootstrap, agent guide/status, mcp status, server info, db scan-nul and db backup succeed; watch list needs a signed-in user. * test(cli): every leaf command carries an example (TASK-2865) TestRealPadTree_EveryLeafHasAnExample walks the real tree; a group node is exempt. On part 1's tree it lists the 114 leaves this part covers. |
||
|
|
01fba76bfc |
refactor(cli): examples move from Long to cobra's Example field (TASK-2865, part 1) (#2020)
38 commands carried an 'Examples:' block inside Long. They now set Example, which cobra renders under its own heading in --help and cmdhelp reads first. cmdhelp's Example parser now drops a trailing '# comment' the way the Long parser always did, so the move changes no emitted example: a before/after diff of 'pad help --format json' over all 169 commands shows identical examples, identical args/flags/summary, and descriptions that differ only by the removed block. TestRealPadTree_ExamplesLiveInTheExampleField keeps Long free of example blocks. Examples for the leaf commands that have none follow per group. |
||
|
|
bee399bc94 |
fix(items): github_pr is written only through a typed, validated update member; the field-setter door refuses it (BUG-2696, ToolSurfaceVersion 0.47) (#1469)
A `--field github_pr=...` / MCP `field` setter stored the PR as a string on every transport, so no link rendered, and `=null` stored the string "null". BUG-2627 had exempted github_pr from the reserved-key refusal as remote MCP's only writer; that writer never worked. ItemUpdate gains a typed `github_pr` PR object (number > 0 and an absolute http(s) url required) plus `clear_github_pr`, lowered by the handler into the field patch after the caller's own fields_patch is refused every reserved key. `pad github link` / `unlink` / `project reconcile` use it and verify from the response that the change landed (an older server ignores the member and answers 200). Not exposed on the MCP catalog; a remote PR-link action is deferred to the catalog-trim decision. The point is validity, not sender identity. Create (still accepts hand-written reserved keys) is BUG-3163. Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N |
||
|
|
70e3659526 |
fix(items): twelve item-field writers send a patch instead of replacing the blob (BUG-3049 / BUG-3055) (#1349)
* wip(items): convert full-blob item writers to fields_patch (BUG-3049) * test(items): interleaving tests for the CLI and bulk doors (BUG-3049) * test(webmcp): pad_item update merges instead of replacing (BUG-3055) * test(web): per-door guards for the browser full-blob writers (BUG-3049) * docs(web): correct the BUG-2273 note the patch conversion falsified (BUG-3049) * style: gofmt the new server test (BUG-3049) * test(webmcp): repoint the dispatch update leg at the patch contract (BUG-3055) * test(e2e): status move preserves a concurrently written field (BUG-3049) * test(e2e): hold the stale-copy window open with the stream blocked (BUG-3049) * test(webmcp): label the non-detecting leg as such (codex round 2) * fix(items): refuse an unreadable fields snapshot in the bulk door (codex round 3) * fix(items): re-decide auto-populated dates under the write lock (codex round 4) * docs(types): ItemUpdate.fields is a full replace; updates use fields_patch (BUG-3049) |
||
|
|
367aae8e18 |
fix: one prefix grammar, and the ref parser widens to it (BUG-2943) (#1286)
* fix(collections): a DERIVED prefix is A-Z only (BUG-2943) DerivePrefix took the first BYTE of each word, so a collection named 'TEMP Rook A 2870' got the prefix 'TRA2'. parseItemRef resolves a PREFIX-NUMBER ref only when every prefix character is A-Z and otherwise falls through to a slug lookup, so every item in that collection printed an issue ID the CLI then refused: 'pad item show TRA2-2942' answered 'item not found' while the slug resolved fine. Two functions, each locally reasonable, disagreeing about what a prefix may contain — and the generator was the permissive one, so the failure surfaced at read time on an identifier the product itself minted and printed. The first-BYTE bug had a second half: a word starting with a multi-byte rune contributed a UTF-8 lead byte, so a collection named in most non-Latin scripts produced a prefix that is not even valid text. Non-letters are SKIPPED rather than mapped — there is no honest A-Z substitute for '2' or 'Omega', and inventing one puts a character in the ID that is in nobody's collection name. A name with no ASCII letters yields the empty string, which store.CreateCollection already turns into its ITEM fallback. SCOPE, stated because the first draft of this message overstated it (codex round 1 [P2]): DERIVED prefixes are safe now; the INVARIANT IS NOT ENFORCED. Three other doors store a prefix verbatim and unvalidated — CreateCollection with an explicit input.Prefix, UpdateCollection, and workspace import — so the same unresolvable-ID defect is still reachable through the API, the --prefix flag and a restore. Named on the trail with their call sites, held for a ruling rather than swept into this commit, because the import door wants a different answer from the other two: refusing a restore is not obviously right. The parity test lives in internal/store, where parseItemRef is: it asserts the generator against the RESOLVER rather than against a restatement of the resolver's rule, which is how these two drifted apart in the first place. Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR * fix(store): one prefix grammar at all four doors, and the parser widens to it (BUG-2943) The ruled shape, which dissolves the import dilemma rather than choosing a side of it: collections.IsValidPrefix is the single definition — an uppercase letter followed by uppercase letters or digits — and parseItemRef now asks it instead of carrying its own stricter A-Z rule. Because the PARSER widened, a workspace already carrying a prefix like AB1 resolves every item by its printed ID the moment this ships. No migration, no rewrite of an identifier a user's other records may reference. The four doors: - derive: unchanged from the previous commit, still letters-only, still within the grammar; - create with an explicit prefix: REFUSED if outside the grammar, with a message naming the rule. The caller typed it, so a refusal is actionable; - update: same, and it matters more here — update is the door someone reaches for to FIX a bad prefix, so it must not accept another one; - import: the most permissive door that can still be honest. Anything the parser resolves is accepted (which now includes digits); only a prefix NO surface could resolve is refused, naming the collection and saying the export can be edited. Carrying that verbatim would restore a workspace whose items print IDs the CLI answers 'not found' to, which is this item's defect rather than a compatibility owed. An ABSENT prefix on import is not an unresolvable one. Old exports and every fixture in the suite carry "", and the first version of this check refused them — turning a fix for unresolvable IDs into one that cannot restore an old bundle at all (caught by three server tests). It now takes the same derive-then-ITEM fallback CreateCollection applies, which also upgrades it: an empty prefix is itself unresolvable, since the ref would begin with a dash. A prefix accepted only because the parser widened is logged at WARN, so an operator can see an id-space that would have been rejected before rather than inferring it from a resolve failure that no longer happens. Tests for each door follow in the next commit. Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR * test(store): one test per prefix door, plus the parser round trip (BUG-2943) Each door asserted separately: 'they all call the same helper' is a claim about the code, not about behaviour, and the bug was two definitions disagreeing. - create with an explicit prefix: AB1 accepted and resolves; ab1, 1AB, 'A B', A-B, A!, a non-Latin letter and a bare digit refused, with the rule named; - update: AB1 accepted, a bad replacement refused AND the stored prefix unchanged after the refusal — update is the door someone uses to FIX a bad prefix, so it must not swap one unresolvable id-space for the next; - import: a digit-bearing prefix restores unrewritten and resolves; one no surface can resolve is refused naming the collection and the export; an ABSENT prefix takes the create-path fallback and comes back resolvable; - the parser: every prefix the doors accept round-trips, and 1AB / 9 / 'A B' / A! / a trailing dash / a bare prefix stay refused. One correction: my first version of the parser test asserted that 'ab1-42' is refused. It is not, and the code is right — parseItemRef upper-cases before splitting, which is what makes "pad item show task-5" work. Case-insensitivity is now PINNED rather than mis-asserted, because a later reader working from the grammar comment alone would otherwise 'fix' it. Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR (This message was rewritten once: the sentence above lost its example because the original was written with backticks inside a double-quoted shell string, which the shell EXECUTED and replaced with the command's empty output. The span was blank in the commit as first written.) * fix: the widened grammar reaches its consumers too (BUG-2943) Codex round 2. Widening parseItemRef without widening what CONSUMES a ref would have left the same two-definitions bug this unit is about, introduced by its own fix: - cmd/pad/cmd_github.go matched [A-Z]+-\d+, so 'pad github link' on a branch carrying a digit-bearing ref silently found nothing; - web localSearch's palette Enter fast-path could not recognise one either. Both now match collections.IsValidPrefix. Tests strengthened, both on codex's reading: - the import fallback pinned the VALUE, not just resolvability — asserting 'non-empty and parseable' passes an implementation that stamps ITEM on every absent prefix, giving every collection in a restored workspace the same id-space. Two legs now: an ordinary name derives TASK, a letterless name falls through to ITEM, which is what makes it 'derive, THEN ITEM'; - the WARN the ruling asked for had no test, so it was a line nobody would notice was gone. Now asserted, with a control that an ordinary prefix does NOT warn — a log everything trips is a log an operator learns to skip. Three comments still described the parser's A-Z rule as current, including one in the file that changed it. STILL OPEN, on the trail for a ruling: web paneTarget.ts keeps the narrow grammar on PURPOSE — its comment argues a digit-permitting shape would misclassify a slug like 'roadmap2-5' as a ref — and that argument cited the server rule this unit just widened. Whether the guard follows or stays is a question about the widening's blast radius, not a line to change quietly. Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR * fix(web,store): the pane guard follows the server, and the precedence is written down (BUG-2943) Both lead-ruled after codex round 2 surfaced them. paneTarget's REF_SHAPE kept a LETTERS-ONLY grammar deliberately, citing the server's A-Z loop as its warrant. The server dropped that rule, so the guard was holding a grammar nothing else holds — which does not avoid a wrong answer, it produces a different one. It now matches IsValidPrefix. The cost is real and is stated in the test rather than buried: an HREF whose last segment is ref-shaped under the wider grammar is compared by NUMBER with the prefix discarded, so a genuine slug like 'roadmap2-5' now counts as the same pane target as TASK-5. The existing test pinned the opposite and is REPLACED, naming what changed and why. The prefix is dropped because a moved item keeps a stale one (the server's own number-only fallback) and PaneGuardItem carries no prefix to compare; tightening that means widening that type and its callers, which is a separate change and is on the trail. The SLUG-channel leg is kept as its own test: provenance, not grammar, is what protects it — a target naming an item by slug is judged only as a slug. ResolveItem's ref-before-slug precedence is now documented on the function and pinned in both directions: 'ab1-42' resolves as a SLUG when no AB1-42 exists, and a live ref wins when it does (case-insensitively). The widening made more strings ref-shaped, so 'is my slug still findable' needed an answer that does not depend on reading the resolver. Web unit tests run here via a node_modules SYMLINK to the main checkout, which CLAUDE.md permits; npm ci was not run and must not be. Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR * fix(web): the self-pane guard was the fourth copy of the ref grammar (BUG-2943) Codex round 3 [P1]. The route-level refNumber() still matched [A-Za-z]+, so a master whose ref is R2-1 parsed as null, its item_number fell back to 0, and the same-item guard stopped recognising ?item=R2-1 as the master — mounting a second provider for the item already on screen. That is the FOURTH consumer found carrying its own copy of this grammar (github branch extraction, the search palette, the pane target guard, and now this). Four independent copies is the argument for the shared definition rather than for four careful edits, and it is why the widening had to be swept rather than applied where it was noticed. Also from round 3: comments saying these client regexes 'match collections.IsValidPrefix' were imprecise — the validator accepts uppercase only, while the client patterns accept either case on purpose, because a user types a ref however they like and the server upper-cases before splitting. They mirror the ref GRAMMAR, and now say so. Web gates run here through a node_modules symlink to the main checkout (permitted; npm ci is not): vitest 2195 passed, svelte-check 0 errors. Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR * fix(collections,e2e): the fifth and sixth copies, and the docs that taught the old rule (BUG-2943) Codex round 4, after I claimed the sweep was complete twice. - Two SEEDED PLAYBOOK BODIES carry their own ref grammar and instruct agents with it: playbook_library_plan.go and templates_sdd_spec.go both said a ref 'matches ^[A-Z]+-\d+$'. An agent following those literally would refuse to treat AB1-42 as a ref — a grammar copy that lives in PROSE and is executed by a reader rather than a regexp engine, which is why two sweeps of the code missed it. - Three e2e comments taught the defect as a rule: one of them carries the empirical confirmation ('GET /items/BS1-10 404'd while the slug worked'), which is precisely this bug. They now say the by-ref 404 is fixed and that the explicit prefix those suites pass buys DETERMINISM rather than dodging it. Counting honestly: six live copies of one grammar, found in four rounds of review, two of which I opened by asserting there were no more. The shared definition is the fix; every one of these was a place that had quietly made its own. Gates: go test ./... 0, make lint 0 issues, vitest 2195 passed, svelte-check 0 errors (web run through a node_modules symlink to the main checkout — permitted; npm ci is not). Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR |
||
|
|
744e3791e9 |
fix: parent commands exit non-zero on unknown subcommand (#850)
Parent command groups now return a non-zero exit on an unrecognized subcommand (e.g. `pad item bogus`, `pad role lst`) instead of printing help and exiting 0 — a silent failure for an agent-first CLI. Bare parents still show help; valid subcommands unaffected. Covers all 16 command groups plus a regression test that walks the real command tree so a future group can't silently regress. Fixes #850 Co-authored-by: Dave <xarmian@gmail.com> |
||
|
|
ac6e05d1e3 |
refactor(cli): split cmd/pad/main.go by resource (TASK-2015) (#866)
Mechanically split the 9,841-line cmd/pad/main.go god file into cohesive per-resource files (all package main): cmd_item.go, cmd_collection.go, cmd_workspace.go, cmd_auth.go, cmd_project.go, cmd_playbook.go, cmd_role.go, cmd_tag.go, cmd_github.go, cmd_webhook.go, cmd_agent.go, cmd_server.go, cmd_attachment.go, cmd_db.go, cmd_library.go, cmd_bootstrap.go. main.go now holds only main(), newRootCmd(), and shared config/client wiring (265 lines). Zero behavior change — a pure move of command constructors + helpers. All 169 top-level declarations preserved verbatim; the recursive --help command/flag tree is byte-identical to main. cmdhelp and the MCP catalog read command schemas at runtime, so they are unaffected. Updates the CLAUDE.md "Add a new CLI command" recipe to point contributors at the appropriate cmd_<resource>.go file and groups.go, so the file stops being a merge-conflict magnet for parallel agents. Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS |