Files
pad/internal/appstore/testdata
xarmian a17e2c415e feat(appstore): app item create and field update inside the fence (TASK-3390) (#1765)
* feat(appstore): app item create and field update inside the fence (TASK-3390)

SPEC-6 U2a (DOC-3371 v12 §4).

store.FencedTx write methods:
- CreateItem:
  - the companion check, then the human create's workspace lock
  - the item cap under that lock, only when Options.PlanLimit is set (Pad
    Cloud), as the human create does; it refuses with ErrAppItemLimit and no
    count
  - a random-suffix slug, never a collision scan (a scan would reveal hidden
    slugs)
  - the row (source app, created_via_app and via_app = the install)
  - the initial version (via_app), status transition, item.created event and
    decision-job enqueue
  - wiki links confined to companion targets
  - an in-fence "created" activity
- UpdateItemFields:
  - the seq lock FIRST, then the companion and created-by-this-install
    checks, then the etag (HMAC over install, item and seq under a
    server-only key, compared in constant time)
  - a field merge, the status transition, update events and enqueue
  - an in-fence debounced "updated" activity
  - no content, title, relation, parent or attachment change is reachable

Activities are written on the fence's transaction (seq lock, then the
debounce lock, the order BUG-2777 permits). A wiki link to anything but a
live companion item stays broken.

appstore.CreateItem / UpdateItem validate against the companion schema:
- refused: undeclared, relation, computed, unique-scoped and reserved keys
- coerced and validated as the human path does, with defaults applied
- ANY pad-attachment: token is refused in any spelling, and
  [[workspace::REF]] is refused
- the create retries ONLY on the items slug conflict (IsItemSlugConflict),
  never on another unique violation

Writer identity: via_app joins the activity debounce key and the version
throttle comparison. The U3 projection block carries creator.via_app.

Migrations SQLite 113 / PG 087: via_app on items, comments, activities,
item_versions and item_links; created_via_app on items. FK to app_installs,
whose rows are never deleted.

Boundary checker: reviewed door helpers (store methods and package-level
functions), each with what it writes, audited by reading every body; only
createActivityQ, enqueueDecisionJobsTx, replaceWikiLinks and writeOutboxTx
write, all on the transaction they are handed. Named function-value entries
(the decision-set resolver, two in-body closures) are keyed by function; a
same-named closure elsewhere is still refused (fixture). stampAttachmentRefsTx
stays unreachable.

Tests:
- write census per mutation on both dialects
- every refusal captures nothing
- the item cap and its self-host exemption
- companion-only links
- interleaved app and human creates never share item_number or seq (red on
  Postgres without the lock)
- via_app identity in the debounce and the version throttle
- nine mutants, each fails its test

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

* fix(appstore): scoped link resolution, defaults under the rules, a required etag key (TASK-3390)

Codex round 1:
- P1: an app link was resolved over the whole workspace and confined
  afterwards, so a hidden item sharing a companion's title could win the
  match and leave the link broken: an existence signal. Resolution is now
  scoped (linkScope) to companion collections in every query, the
  number-only and collection-qualified fallbacks included, so a hidden item
  cannot influence the outcome. The human paths pass the zero scope and are
  unchanged.
- P2: schema defaults are applied after the key checks, so a default could
  land a relation, computed or uniqueness-scoped value, or a pad-attachment:
  token. Validation now runs against a schema with those defaults removed,
  and the effective fields are re-checked for tokens.
- P2: an empty or short etag key made etags forgeable offline. ETag and
  UpdateItem now refuse with ErrNoETagKey unless the key is at least 32
  bytes.

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