Skip to content

feat: adopt core 0.43's per-aspect writes, wire parsers and installs - #131

Open
cuibonobo wants to merge 2 commits into
mainfrom
claude/amazing-ritchie-qyy1cn
Open

cuibonobo wants to merge 2 commits into
mainfrom
claude/amazing-ritchie-qyy1cn

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Brings the server up to the latest published core releases: @haverstack/core 0.43, adapter-local 0.42, commons 0.38, wire-types 0.42 and conformance-fixtures 0.37. Where the old code had a per-route exception, this applies one shared rule:

  • One write endpoint per aspect. PATCH /records/:id/content, PUT .../parent, PUT .../unlisted, plus POST (edit list) and PUT (replace) on .../associations and .../permissions. The change-set PATCH /records/:id and the /delete sub-paths are removed. PATCH /entity is now PATCH /entity/content.
  • Every write reads and checks If-Match, including writes that don't bump the version.
  • Every endpoint core specifies is parsed by core's parser for it. The server's own copies are removed: booleanParam, isOwnerActingAlone, and the hand-written checks on POST /types and migrate. knownParams() and the readJson key list remain only for routes the server defines itself.
  • One paging rule for history. Versions (now a { versions, cursor } envelope) and the journal share readHistoryPage: a 500-row page cap, with a cursor that is null exactly when nothing follows. The query limit clamp is removed, because Stack now sets and caps query defaults itself.
  • Migrate is gated by ScopedStack, so an installed app can commit migrations within its own type families.
  • GET /records/:id hides tombstones unless called with ?includeDeleted=true.
  • App installs: POST /installs (advertised in discovery), plus an owner-only review surface at /installs/requests for listing, reviewing the current plan, approving and declining.
  • Also: LocalAdapter.open({ create }), includeUnlisted read from the change filter, all ten commons types seeded (adds image, post and site), and grantType calls take a type family.

Stored data: pending install requests live in a new sibling file, <DB_PATH>.installs. It is additive, and nothing existing needs migrating.

Docs

  • docs/api.md: Discovery, Records (per-aspect writes, history/restore/migrate, optimistic concurrency), query defaults, tombstones, Unlisted, change-feed kinds, Types, the new Installs section, Entity, Permissions and the grant surface (baseId).
  • docs/deployment.md: the .nonces and .installs sibling stores, and how install requests are bounded.
  • .env.example: the full commons type list.

The wire contract itself is core's. This PR implements core 0.43 and does not change the contract.

Verification

pnpm run format:check && pnpm run lint && pnpm typecheck && pnpm test all pass, with 652 tests across 32 files. pnpm run build also succeeds.

The conformance gate dispatches every 0.37 fixture. The only skips are the same two as before, each of which pins a client obligation rather than a server one.

New tests:

  • tests/routes/installs.test.ts and tests/lib/installRequestStore.test.ts: the install surfaces and the request store.
  • tests/lib/historyPage.test.ts: the paging rule.

Notes for reviewers

  • Behavior changes clients will see: the removed endpoints listed above, the versions envelope, and a stale If-Match on a write that doesn't bump the version now answering 412.
  • Fixture inconsistency in core: the delete-record-soft fixture's body keeps content that the spec's tombstone projection empties. The test pins the spec. Filed as delete-record-soft fixture shows content the tombstone projection empties core#475.
  • Possible follow-up: approving an install re-plans the stored manifest, so if the stack changed after the owner's review, what gets applied can differ from what they saw. A plan fingerprint check on approve would close that.
  • Not covered here: core main has three unreleased changesets (the ownerEntityId rename, portable copy, reshare-needs-read). A smaller follow-up will adopt them once they're published.

Generated by Claude Code

Bring the server up to @haverstack/core 0.43 (adapter-local 0.42,
commons 0.38, wire-types 0.42, conformance-fixtures 0.37).

One rule per concern rather than per-route exceptions:

- Every aspect of a record has one write endpoint: PATCH .../content,
  PUT .../parent, PUT .../unlisted, and POST (edit) / PUT (replace) on
  .../associations and .../permissions. The change-set PATCH and the
  /delete sub-paths are gone; PATCH /entity is PATCH /entity/content.
- Every write reads and checks If-Match, including the no-bump ones.
- Every endpoint core specifies is parsed by core's parser for it;
  knownParams()/readJson keys remain only for server-defined routes.
  The server's own booleanParam, isOwnerActingAlone and the POST /types
  and migrate hand checks are removed.
- Versions and the journal page by one rule (readHistoryPage): a
  500-row server ceiling announced through an exact cursor. The query
  limit clamp is gone, since Stack resolves and caps query defaults.
- POST /records/:id/migrate is ScopedStack's to gate, so an installed
  app can commit migrations within its own families.
- GET /records/:id hides tombstones unless ?includeDeleted=true.

New: POST /installs (advertised in discovery) and an owner-only review
surface at /installs/requests. Pending requests live in a new sibling
store, <DB_PATH>.installs — additive, no migration of existing data.

Also: LocalAdapter.open({ create }), includeUnlisted read from the
change filter, the full commons set seeded (image, post, site), and
the conformance gate dispatching every 0.37 fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRRrTNcLcf2TtfuL9wB8Z8
Review findings on the install surface:

- GET /installs/requests/:id returns an ETag naming the plan it shows,
  a hash of its canonical JSON. Approve reads If-Match and answers 412
  (precondition_failed) when the plan has changed, so what lands is what
  was reviewed — the same If-Match rule every other write follows.
- Requests are removed by the id that names them, never by key, so a
  manifest re-sent mid-approval is no longer deleted in its place.
- requireActingAlone() refuses a delegated session before its body is
  read, in the same order as requireAuth() and requireOwner().
- Manifests compare as canonical JSON, so reordered keys keep an id.
- ifMatchVersion()/ifMatchTag() live in src/lib/ifMatch.ts, and
  serializePlan spreads every plan field rather than listing them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRRrTNcLcf2TtfuL9wB8Z8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants