mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
a17e2c415e
* 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