Files
pad/internal/appstore/testdata/genericnested
xarmian 85c49cf2fd feat(appstore): FencedTx, the appstore skeleton, and the boundary and write-capture guards (TASK-3388) (#1764)
* feat(appstore): FencedTx, the appstore skeleton, and the boundary and write-capture guards (TASK-3388)

SPEC-6 U1 (DOC-3371 v12 §4): the door every app write will go through, and
the two tests that guard it. No app mutations or routes yet; U2 adds them
under these guards.

- store.FencedTx:
  - BeginFenced's first statement reads app_installs (auth_epoch, state,
    workspace), FOR SHARE on Postgres, and refuses unless the admitted epoch
    is current, the install is active and the workspace matches.
  - There is no raw Exec and no free-form query. The lock primitives
    (LockWorkspaceSeq, NextItemNumber, NextSeq) take the human item path's
    own workspace lock.
  - Companion guards: requireCompanionCollection, and requireCompanionItem
    for derived rows.
  - Every field is unexported, and a zero value refuses every method.
- internal/appstore: holds *store.Store concretely; withFence runs one
  mutation in one FencedTx; CheckFence is the live path until U2.
- The boundary test (go/packages + go/types) walks every module function
  the package can reach. It fails on:
  - a *store.Store method off a reviewed allow-list (stronger than the
    spec's deny-list)
  - database/sql calls outside the door
  - Store.db or (*Store).DB
  - an interface call or declaration an internal/store type answers
  - reflect or unsafe
  - the ordinary attachments Put

  One fixture per rule under testdata is asserted to be reported.
- The reverse test: no package outside internal/store and internal/appstore
  references BeginFenced.
- storetest.CaptureWrites:
  - SQLite: modernc's pre-update hook on a single connection, with a
    sentinel write proving the hook is still on it.
  - Postgres: statement-level audit triggers installed by the harness.

  The harness is proven red on a human create (items plus FTS5 shadow
  tables) and on a test-only trigger write at depth > 0, and silent on a
  read. The blind inventory is asserted exactly: items_fts, comments_fts,
  documents_fts and sqlite_sequence.
- guardConn.Unwrap exposes the driver connection to the harness.
- Migration: app_installs (SQLite 112 / PG 086); its columns are added to
  the NUL baseline, since no writer exists before U8, which must validate
  origin.

Moved to U2, where they are first used: DebounceActivity, CheckItemCap and
the via_app columns.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(appstore): close the boundary checker's gaps and the harness's coverage holes (TASK-3388)

Codex round 1. Each gap now has a testdata fixture asserted to be reported,
and the six P1 fixtures were checked against the round-0 checker:
- package-var: package-level var initializers and init functions are walked
  for the root and every module package it brings in that the human store's
  own closure does not
- generic: instantiated generic methods are normalized through Origin()
- embedded-sql: rules check a method's DECLARING receiver, so a method
  promoted through an embedded *sql.DB or *store.Store is caught at the call
- func-field: a call through a function value is refused (dynamic-call)
  unless it is a parameter of an unexported root function that every call
  site passes a literal or a named function
- linkname: a go:linkname directive or an unsafe import in a walked package
  is refused
- whole module: the walk covers the whole module, not only internal/
- embedded-store: the old checker caught this only by walking into
  DeleteItem's body; it is now caught at the call

Against the round-0 checker, all six P1 fixtures fail (pkgvar, helperinit,
generic, embeddedsql, funcfield, linkname); the extra embeddedstore fixture
passed there, for the reason above.

Harness:
- SQLite: PRAGMA data_version, read on the hooked connection at the start
  and end, refuses a capture during which another connection committed. A
  replaced connection is refused at the sentinel. Both branches are tested;
  the data_version test passes silently without the check.
- Postgres: triggers are ensured at every capture, so tables created later
  are covered; a table created during a capture is refused; captures are
  serialized per database.
- The Postgres lock tests wait until pg_stat_activity shows a backend
  waiting on a Lock, instead of treating 500ms as proof of blocking.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(appstore): refuse every function value instead of verifying callbacks; working-trigger check (TASK-3388)

Codex round 2 found five P1s in the checker and one P2 in the harness.

Four of the P1s were bypasses of one mechanism: verifying where a callback
parameter came from. They were reassignment, aliasing through a pointer, a
method expression shifting the argument index, and interface dispatch past
the call-site census. This is a guard, not a proof, so the mechanism is
removed rather than patched again:
- withFence(fn) is gone; app mutations open and commit their FencedTx
  directly.
- Every call through a function value is refused (dynamic-call).
- Any use of a variable, parameter or field of function type is refused
  (func-value). This also closes the fifth P1: an exported function-typed
  package var in the store's own import closure, run through
  sync.Once.Do, whose initializer the walk excludes.

Interface checks now judge the INSTANTIATED receiver, since Mutator[string]
can be implemented where Mutator[T] is not; Origin() is used only for
indexing.

Harness P2: a pad_write_audit_t trigger counts only when it is enabled,
statement-level, AFTER, on INSERT/UPDATE/DELETE, and calls
pad_write_audit_fn. Otherwise it is dropped and recreated.

Fixtures genericiface, callbackparam, methodexprcallback and oncevar each
pass against the round-1 checker and are caught now. A Postgres test
disables the trigger and expects the next capture to see the write.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(appstore): refuse interface dispatch on unbound type arguments (TASK-3388)

Codex round 3: a generic helper run[T](m Mutator[T]) is walked once, with T
unbound, so Mutator[T] never matched *store.Store and the call went
unflagged. An interface call whose receiver is, or is instantiated with, a
type parameter is now refused, rather than propagating type arguments
through the walk. Fixture genericifacebody passes the round-2 checker and is
caught now.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(appstore): no interface dispatch inside a generic body (TASK-3388)

Codex round 4 found two more ways a type parameter hides from the
round-3 check:
- an anonymous interface constraint (or a generic alias)
- a type parameter nested inside a func, slice, map or alias type argument

Chasing type parameters through every type form is open-ended, so the class
is closed outright: a generic function body, walked once with its parameters
unbound, may make no interface method call except on error. A non-generic
function cannot hold an unbound type parameter, so the ordinary
implemented-by-store check stays exact there. Fixtures genericconstraint and
genericnested both pass the round-3 checker and are caught now.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
2026-10-03 16:02:18 -04:00
..