Skip to content
This repository was archived by the owner on Sep 23, 2026. It is now read-only.

Extract reflection engine from reflector into devonian - #23

Merged
michielbdejong merged 2 commits into
mainfrom
claude/youthful-mendel-3ujtxo
Sep 21, 2026
Merged

michielbdejong merged 2 commits into
mainfrom
claude/youthful-mendel-3ujtxo

Conversation

@michielbdejong

Copy link
Copy Markdown
Contributor

Summary

Extracts the bidirectional reflection engine from localthought/reflector into devonian as a reusable, platform-agnostic component. This enables other hosts (reflector itself, bridges, and future integrations) to share the same battle-tested reflection logic instead of maintaining separate copies.

Key Changes

  • src/reflect/engine.ts — Core ReflectionEngine class that orchestrates bidirectional synchronization of records between two syncables-backed systems:

    • Issue creation with echo suppression (via origin markers) and idempotency (via id-map + destination marker scan)
    • Open/closed state reconciliation using a persisted ledger to detect which side changed
    • Comment reflection under counterpart issues
    • Configurable direction (bidirectional or a-to-b), retry behavior, and drain timeout
  • src/reflect/marker.ts — Origin marker codec for embedding/parsing hidden HTML comments that link reflected copies back to their originals:

    • embedMarker() / parseMarker() / stripMarker() / hasMarker() / renderMarker()
    • Namespaced tags (<!-- <namespace>:origin ... -->) to avoid collisions
    • Percent-encoded payload to prevent early comment closure
  • src/reflect/id-map.ts — Persisted correspondence between originals and reflected copies:

    • InMemoryIdMap for in-memory storage
    • FileIdMap for JSON-file-backed persistence
    • Symmetric, kind-scoped (issue/comment) linking
  • src/reflect/kv-store.ts — Tiny persisted string→string store for reflection metadata:

    • InMemoryKvStore for in-memory storage
    • FileKvStore for JSON-file-backed persistence
    • Used to track last-agreed state per reflected pair
  • src/reflect/runner.ts — Background loop + on-demand trigger wrapper:

    • ReflectionRunner serializes concurrent reflectNow() calls so they never overlap
    • Runs immediately on start(), then repeats on a configurable interval
    • Tracks last run summary and errors for status endpoints
  • src/reflect/index.ts — Public API barrel export

  • Comprehensive test suite (__tests__/unit/reflect/):

    • engine.test.ts — 13 tests covering issue creation, state reflection, comment reflection, bidirectionality, idempotency, and error handling
    • marker.test.ts — 10 tests for the origin marker codec
    • id-map.test.ts — Tests for in-memory and file-backed id-map
    • kv-store.test.ts — Tests for in-memory and file-backed kv-store
    • runner.test.ts — Tests for background loop serialization and interval behavior
  • Documentation:

    • AI session log (docs/ai-logs/sessions/2026-09-21-...md) documenting the extraction decision and implementation
    • AI disclosure infrastructure (docs/ai-logs/README.md, pending-historical-sessions.md) per NLnet policy

Notable Implementation Details

  • Echo suppression: Reflected copies carry an origin marker naming their source system; a copy is never reflected onward by checking the marker before reflection.
  • Idempotency: Combines id-map lookup with a destination marker scan, so a lost id-map doesn't cause duplicate reflections.
  • State reconciliation: Uses a persisted ledger of last-agreed state to detect which side changed, avoiding bouncing when both sides diverge.
  • Platform agnostic: ReflectionEngine has no notion of OpenAPI, overlays, or auth — a host provides two ReflectionSide objects already bound to syncables clients, keeping the engine portable across any two compatible endpoints.
  • Serialized concurrency: ReflectionRunner ensures manual triggers and scheduled ticks never overlap via a promise chain.

Version

Bumped to 0.6.0 and added ./reflect export to `package

https://claude.ai/code/session_01B2mbssSNDiu8GpKjaCFRn1

Per the decision recorded in ontola/atomic-plugins#6: reflector's
bidirectional GitHub-issue reflection engine (origin markers, id-map, echo
suppression, state reconciliation, comment reflection) was reflector-only
code duplicating what devonian is meant to be the shared home for. Extracts
it here, generalized so it depends on nothing but a syncables ApiClient per
side (no OpenAPI document, resource model, or auth of its own) plus a
namespaced marker codec (defaulting to `devonian`, so a host with markers
already in production — like reflector's `reflector:origin` — can keep its
own namespace).

Adds ReflectionEngine, ReflectionRunner (the background-loop/reflectNow/
status wrapper), and the marker/IdMap/KvStore support types under
devonian/reflect, with full test coverage against a real syncables client.
Bumps to 0.6.0; reflector's own dependency on this package is a follow-up
change pending publish.

Claude-Session: https://claude.ai/code/session_01B2mbssSNDiu8GpKjaCFRn1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every other export here ships raw .ts, which works for the bundler/vite-
transformed consumers (this repo's own tests, atomic-plugins) this package
has had so far. reflect's actual first consumer, reflector, is a plain
tsc-built Node app with no TS loader at runtime — importing a raw .ts file
there would fail outright. Points the "./reflect" export at build/src/reflect
(types + default conditions) instead, matching how syncables — reflector's
other TS dependency — already ships.

Claude-Session: https://claude.ai/code/session_01B2mbssSNDiu8GpKjaCFRn1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michielbdejong
michielbdejong merged commit c4d74d8 into main Sep 21, 2026
2 checks passed
michielbdejong pushed a commit that referenced this pull request Sep 21, 2026
The published devonian@0.6.0 tarball's build/ predates this repo's
src/reflect/ (ontola/atomic-plugins#6, PR #23) — nothing forced a
rebuild before `npm publish`, so it shipped whatever build/ happened to be
on disk. That leaves build/src/reflect missing from the registry package,
breaking the "./reflect" export's "types"/"default" resolution for any
real consumer (confirmed via reflector's PR #58, which fails to resolve
devonian/reflect against the published 0.6.0).

Adds prepublishOnly: npm run clean && npm run build, so publish always
ships a build/ that matches src/. Bumps to 0.6.1 since 0.6.0 is already
claimed on the registry and can't be overwritten.

Claude-Session: https://claude.ai/code/session_01B2mbssSNDiu8GpKjaCFRn1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
michielbdejong added a commit that referenced this pull request Sep 21, 2026
* Guarantee a fresh build before publish; release 0.6.1

The published devonian@0.6.0 tarball's build/ predates this repo's
src/reflect/ (ontola/atomic-plugins#6, PR #23) — nothing forced a
rebuild before `npm publish`, so it shipped whatever build/ happened to be
on disk. That leaves build/src/reflect missing from the registry package,
breaking the "./reflect" export's "types"/"default" resolution for any
real consumer (confirmed via reflector's PR #58, which fails to resolve
devonian/reflect against the published 0.6.0).

Adds prepublishOnly: npm run clean && npm run build, so publish always
ships a build/ that matches src/. Bumps to 0.6.1 since 0.6.0 is already
claimed on the registry and can't be overwritten.

Claude-Session: https://claude.ai/code/session_01B2mbssSNDiu8GpKjaCFRn1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Automate npm publish via GitHub Actions

Per feedback on this session's manual-publish fumble (devonian@0.6.0
shipped a stale build/ — see the previous commit): publishing should not
be a manual step at all. Adds .github/workflows/publish.yml, which runs on
every push to main that touches package.json, and publishes only when the
local version isn't already on the registry — so bumping the version in a
PR and merging it is the entire release process.

Needs an NPM_TOKEN repository secret (an npm automation token with publish
rights on devonian) added under Settings → Secrets and variables → Actions;
documented in the README's new "Publishing" section. That secret is not
something this session can add.

Claude-Session: https://claude.ai/code/session_01B2mbssSNDiu8GpKjaCFRn1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants