Skip to content

refactor(reference): adopt durable task notices - #185

Merged
FelineStateMachine merged 1 commit into
feat/141-effect-libraryfrom
feat/184-task-notice-migration
Jul 20, 2026
Merged

refactor(reference): adopt durable task notices#185
FelineStateMachine merged 1 commit into
feat/141-effect-libraryfrom
feat/184-task-notice-migration

Conversation

@FelineStateMachine

Copy link
Copy Markdown
Owner

Motivation

The reference task island still hand-rolls module state, listeners, publishNotice, and useTaskNotice for the exact durable-notice pattern introduced by #184. Keeping the migration stacked on #184 lets the effect library and its reference usage land together while preserving a focused review boundary.

Change

  • Declare the task success and rejection messages with s.notice<Task> alongside the existing log and trace effects.
  • Render the package-owned <Notices /> surface in TaskList.
  • Remove the author-owned notice type, module state, publisher, listener set, and hook.
  • Refresh the starter mirrors and generated snapshot hashes.

This PR intentionally targets feat/141-effect-library and depends on #184.

Testing Completed

  • deno check apps/reference/src/islands/use-tasks.ts apps/reference/src/islands/TaskList.tsx
  • deno task test:update:create — 8 passed
  • deno task test:unit — 460 passed
  • Focused formatting and lint checks
  • deno task build — production reference build and secret scan passed

Responsible AI Disclosure

Implemented with AI assistance. The resulting diff was scope-reviewed and validated with the repository's type checks, generated-snapshot workflow, unit suite, linting, and production build.

@FelineStateMachine
FelineStateMachine marked this pull request as ready for review July 20, 2026 09:46
@FelineStateMachine
FelineStateMachine merged commit 24ac0d0 into feat/141-effect-library Jul 20, 2026
FelineStateMachine added a commit that referenced this pull request Jul 20, 2026
…contract (#184)

* fix(preact): sync-state quality pass — first-class blocked dispositions, typed enrollment errors, account-gate freshness

The template half of #175, following the runtime half in #180.

DeviceStatus names why nothing is syncing, not merely that it is not:
the one-line Data sync verdict distinguishes owner mismatch, store
refusals (no schema, rejected ticket), an unopenable sink record, and
the absence of any sync location, with owner-mismatch copy naming the
owning account when known. The no-sync hint now leads with ticket
enrollment; the compiled-in env-var path is the stated alternative.

TicketEnrollForm relays typed refusals verbatim — SyncEnrollmentError,
SyncOwnerError, and DataSinkError carry their own remediation, and the
generic paste-again line cannot fix an unprovisioned store or a foreign
sync owner.

AccountGate re-reads the session when the runtime is recreated, gains
the owner-mismatch branch offering exactly the two remediations (stop
sync releases the election, restore adopts the owner), and surfaces a
definite store problem in the backed-up state. Starter mirror and
snapshot regenerated.

* feat(effects): built-in effect library and the custom-unit authoring contract

Closes #141. Grows the #139 effect machinery into a tiered standard library
and formalizes the contract for authoring custom units.

Contract (effects.ts):
- EffectContext gains `op` and `writeCreatedAt` so units can see the operation
  and measure saved→fate latency.
- PermanentEffectError / isPermanentEffectError: a handler throws it to retire
  an obligation immediately (a hopeless failure) instead of re-arming until
  maxAttempts. The ledger honors the severity.
- requireMutationRuntime + anonymousEffectName expose the seams the library is
  built on, keeping every built-in on the public authoring surface.

Built-in units (schema/effect-library.ts), tiered by risk:
- Observation — s.trace (saved→fate span in diagnostics, OTLP-shaped, no vendor
  coupling), s.debug (dev-only timeline, stripped in PROD). s.log stays in the
  core beside the runtime recorder.
- Data-internal — s.notice (durable, UI-agnostic message queue, idempotent by
  journal id; closes "a rejected write flashed success"), s.mark (patch the row
  on fate, convergent, skips a rolled-back insert), s.chain (issue a follow-up
  verb on synced, no saga API).
- External — s.webhook (POST row+fate with journalId as Idempotency-Key; 4xx →
  permanent, 5xx/429/network → retryable; default 24h delivery window).

Runtime:
- notice-queue.ts: durable queue (OPFS→localStorage→memory), idempotent
  enqueue, TTL + dismissal, subscribe; activeNotices diagnostic.
- write-ledger.ts installs recordTrace/recordDebug/enqueueNotice/applyMark and
  owns the queue singleton; public listNotices/subscribeNotices/dismissNotice.
- diagnostics.ts: effectTraces, effectDebugTimeline, activeNotices.

Preact: useNotices hook + Notices component render the durable queue; a toast
stack is a userland wrapper over the same hook.

Docs: docs/effects.md (library reference + authoring contract + worked custom
unit), teaching-path link from nouns-and-verbs, README reference entries.

Tests: per-unit happy/rejection/idempotency, notice-queue idempotence/TTL/
reload, ledger permanent-failure retirement, and a custom unit built purely on
the public contract. Reference app adds s.trace to addTask (dogfood); starter
mirror + snapshot regenerated. Manifest updated for the three new modules.

Deferred, noted for a follow-up: migrating the reference app's hand-rolled
task-notice to s.notice (a focused island refactor).

* fix(effects): review remediations — durable anonymous-unit identity, notice-queue boot merge, a11y

Addresses findings from the multi-agent + Codex review of #184.

Anonymous-unit identity (HIGH — silent mis-binding across reloads):
- notice/mark/chain no longer take their durable name from a global
  declaration counter (module-load-order dependent, so a re-armed
  obligation could bind to the WRONG unit — for chain, the wrong
  follow-up verb). They are now named `<verb>#<position>` at mutation()
  time, from the author-chosen verb and their slot in its effects — an
  identity independent of which module loads first.
- trace/debug/webhook are content-named and now share one unit per
  identity (cachedBuiltin), so reusing one across verbs aggregates
  instead of throwing a duplicate-name error. webhook keys on url+config.

Notice queue:
- Boot-window clobber (HIGH): load() now MERGES persisted entries with
  any enqueued during the async load window instead of overwriting, so a
  notice an effect enqueues at a boot re-arm is not silently dropped.
- TTL drift (MEDIUM): list() is now a pure read; retirement happens in
  sweep(), wired to a periodic timer in the queue owner; the activeNotices
  count is computed from the live view so it never overcounts expired
  entries.

mark best-effort (MEDIUM): applyMark swallows a denied/vanished-row patch
instead of quarantine-spamming; documented that mark is a convenience over
the status column, not a delivery guarantee (use notice/webhook for that).

Notices a11y (MEDIUM): the aria-live region stays mounted (hidden) when
empty so the first notice is announced; custom children are keyed.

Tests: anonymous-unit verb-scoped naming, content-name sharing across
verbs, notice boot-merge. All green; build refreshed.

* fix(effects): make built-ins replay-safe

* refactor(reference): adopt durable task notices (#185)

* fix(sync): authenticate PoP preflights and align demo notices (#186)

Use proof-of-possession connect URLs for store-status checks during enrollment and boot. Migrate the demo overlay to durable notices and improve recovery/passkey diagnostics.
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.

1 participant