Skip to content

docs(domain): add the Sync bounded context - #1297

Merged
github-actions[bot] merged 1 commit into
mainfrom
schedule/issue-sweep/2026-10-15/762-sync-bounded-context
Oct 15, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
schedule/issue-sweep/2026-10-15/762-sync-bounded-context

Conversation

@JSdotNet

Copy link
Copy Markdown
Owner

Closes #762

Summary

Adds Sync to the domain model as a bounded context. New .devbook/domain/sync/ with context.md (boundary, and the sync feature-flag chapter that merged device-pairing, task-sync and session-sync), domain.md (Device and Pairing Code aggregates, the Change Precedence domain service, the cursor and identity value objects), domain.invariants.md (constant-time secret checks, one-use pairing codes, owner scope, the push cap — each with Enforced at: and its tests), model.md, features.md and requirements.md. Owner, Pairing code, Registration credential, Device token and the "Pairing a device" feature and requirements move there from Tasks; Tasks keeps Device and Tombstone. The context map gains the Supporting Sync row, a Sync node with labelled edges and a published-language row; the dependencies chapters of Inbox, Tasks, Sessions and Devbook, the building-block view, tech/desktop.md and the README follow. No file under src/ or tests/ changes. This also settles the sync row of drift issue #896, which stays open for its other keys.

Decision taken without asking (follow it, or ask for the alternative)

Question. How should Sync enter the domain model? Three choices: (1) its subdomain type; (2) the relationship pattern on each edge; (3) which DomainModels types are aggregates. Under (3), Tasks' domain.md, features.md and requirements.md currently own the pairing vocabulary (Owner, Pairing code, Registration credential, Device token, the 'Pairing a device' feature and its requirements), so the question includes whether those move.

Chosen. (1) Sync is a Supporting subdomain. It is built in-house (ADR 0005 rejected off-the-shelf sync) and its precedence and capture-document rules are tailored to Backlog's local-first model, but it is not what makes the product different. (2) Sync is upstream to Tasks, Sessions and Devbook as an Open Host Service with a Published Language: the replica protocol, as DTOs in Backlog.Modules.Sync.Abstractions. Each downstream context reaches it through a Backlog.Infrastructure.Sync adapter and never sees inside the documents. Inbox keeps its existing Customer/Supplier edge (Inbox is the customer of the intake channel, ADR 0009). (3) Device and Pairing Code are aggregate roots. Task and Annotation Change Precedence is a domain service. OwnerId, DeviceId, OwnerScope, SyncCursor, the three replica cursors and Session Record Key are value objects. Owner, Pairing code, Registration credential and Device token, plus the 'Pairing a device' feature and requirements, move from Tasks to Sync. Device (the shared-kernel machine identity cited by DeviceIdentity.cs) and Tombstone stay in Tasks. No new ADR.

Alternative. Classify Sync as Generic, and leave the pairing vocabulary, feature and requirements in Tasks. In that version the Sync context would hold only the replica mechanics (cursors, precedence, batch caps) and point back at tasks/domain.md for Owner, Pairing code, Registration credential and Device token.

Why the choice. The pairing rules are enforced only in Backlog.Modules.Sync: RedeemPairingCodeCommand, Sha256CredentialHasher and the OwnerScope threading. Only Sync's tests prove them (DevicePairingTests). Tasks code never touches a pairing code or a credential, so keeping those terms in Tasks would leave its language describing rules it does not own. The issue itself asks for the invariants and requirements to live in the Sync context. Supporting rather than Generic: the map has no Generic context so far, Sync is built in-house rather than bought, and it carries Backlog-specific rules (capture as a document kind, the annotation container, tombstone-wins-on-tie) that an off-the-shelf product would not supply. Under devbook-arc42.md, ownership and the boundary go in the context chapter, so no ADR is opened.

To follow the alternative instead. Taking the alternative would change three things. First, the context-map row would read Generic. Second, sync/domain.md, sync/features.md and sync/requirements.md would drop the moved terms, the 'Pairing a device' feature and its requirements, and link to them in Tasks instead; tasks/domain.md, tasks/features.md and tasks/requirements.md would stay as they are. Third, .devbook/tech/desktop.md and .devbook/arc42/05-building-block-view.md would need no re-pointing. The README rewrite, the inbox/dependencies.md fix, the new folder and the context-map node and edges would stay the same either way.

Acceptance criteria

    1. .devbook/domain/sync/ exists with context.md (index: root), domain.md, domain.invariants.md, model.md, features.md and requirements.md, each titled by kind with meta blocks — not proved by a test: Uncommitted scratchpad acceptance script, tests 'AC1: ...' (both). Partly also shown by node .devbook/_tools/devbook-meta/build.mjs --check and node tools/devbook/check-metadata.mjs passing with the new files.
    1. sync/context.md states inside/outside the boundary and has a feature-flag chapter key: sync naming device-pairing, task-sync, session-sync — not proved by a test: Uncommitted scratchpad acceptance script, 'AC2: ...'
    1. sync/domain.md names Device and Pairing Code aggregates, Change Precedence domain service, the value objects; Owner, Pairing code, Registration credential, Device token defined there and not in tasks/domain.md — not proved by a test: Uncommitted scratchpad acceptance script, 'AC3: ...'
    1. sync/domain.invariants.md has one invariant chapter per rule (a)-(d), each with Enforced at and the named tests — not proved by a test: Uncommitted scratchpad acceptance script, 'AC4: ...'
    1. sync/requirements.md holds pairing, token and owner-scope SHALLs with scenarios; tasks requirements/features no longer hold Pairing a device — not proved by a test: Uncommitted scratchpad acceptance script, 'AC5: ...'
    1. context-map.md has the Supporting Sync row, Sync node with labelled edges to Inbox, Tasks, Sessions, Devbook, a published-language row, and the strategic rule — not proved by a test: Uncommitted scratchpad acceptance script, 'AC6: ...'
    1. README no longer says 'owns no domain' / 'not a bounded context', names the Sync context, test-project table untouched — not proved by a test: Uncommitted scratchpad acceptance script, 'AC7: ...'
    1. inbox/dependencies.md links the Sync context; tasks, sessions and devbook dependencies each carry a Sync row — not proved by a test: Uncommitted scratchpad acceptance script, 'AC8: ...'
    1. No chapter, ADR or tech page references a moved anchor; tech/desktop.md and arc42/05 re-pointed — not proved by a test: Uncommitted scratchpad acceptance script, 'AC9: ...'. build.mjs --check also resolves every reference and passes.
    1. No file under src/ or tests/ changes; tasks/domain.md#device keeps its anchor — not proved by a test: Uncommitted scratchpad acceptance script, 'AC10: ...' (git status on src and tests is empty; ### Device present). It is also a property of the diff.
    1. build.mjs --check and check-metadata.mjs pass; DevbookBuilderParityTests stays green — test: tests/Backlog.Infrastructure.Devbook.UnitTests DevbookBuilderParityTests (3/3 passed); node .devbook/_tools/devbook-meta/build.mjs --check exit 0; node tools/devbook/check-metadata.mjs 0 blocking; tools/devbook/*.test.mjs 52/52 passed

Testing

  • Command: cd "D:/iswp/762-sync-bounded-context" && dotnet build Backlog.WithoutAppHeads.slnf --configuration Release -p:TreatWarningsAsErrors=true && dotnet test --solution Backlog.WithoutAppHeads.slnf --no-build --configuration Release --max-parallel-test-modules 1 --parallel none --results-directory <scratchpad>/tr762 (plus: node .devbook/_tools/devbook-meta/build.mjs --check; node tools/devbook/check-metadata.mjs; node --test tools/devbook/check-metadata.test.mjs)
  • Passed 16248, failed 0; repair attempts 0.
  • Owner session, after the review fixes: Both devbook checks pass (build.mjs --check; check-metadata.mjs: 192 files, 0 blocking).

Review

  • Fixed in the owner session: minor — the Sessions strategic rule in the context map said Sessions appears in no published-language row; the new Sync row lists it as a user, so the rule now says Sessions owns no row and only uses Sync's protocol.
  • Fixed in the owner session: minor — the Replica Cursor chapter said each cursor is "built only from a SyncCursor", which the record structs' public constructors contradict; reworded to what the types guarantee.
  • Left open, minor (.devbook/domain/context-map.md:38): Adding the Sync node and four edges to the context-map mermaid fence changes its hash, so the committed _archify artifact (context-map.1.architecture.standard.{html,json}) is now stale. node tools/diagrams/archify-artifacts.mjs scan reports it as stale, where before it was rendered. The Devbook pane withholds it and reports it out of date (ArchifyDiagramArtifactsTests: An_artifact_authored_before_the_fence_was_edited_is_withheld_and_reported_as_out_of_date), so the rendered context map no longer shows.
  • Left open, minor (.devbook/domain/sync/context.md:98): The Tasks inbound row says roadmap plans (ADR 0018), GitHub settings (ADR 0021), notes and sprint series ride the task feed under their owners' kind tokens. The new context map gives Sync edges only to Tasks, Sessions, Devbook and Inbox, and neither roadmap/dependencies.md nor progress/dependencies.md has a Sync row. progress/dependencies.md still calls sprint-series replication 'a transport too', which is the stance this change removed from sessions/dependencies.md. The map and the context chapter now disagree on who Sync's consumers are.
  • Left open, minor (.devbook/domain/sync/domain.invariants.md:92): 'A push over the cap is refused whole' is filed under '## Change Precedence', but Change Precedence (TaskChangePrecedence/AnnotationChangePrecedence.Supersedes) does not enforce it. Its own Enforced-at line names the endpoints in the Backlog.Modules.Sync.Api host and SyncRequestLimits. The rule is attributed to a domain service that never sees the batch.
  • Left open, minor (.devbook/domain/sync/domain.invariants.md:96): The new sync folder labels the same Backlog.Modules.Sync.Api.UnitTests endpoint test classes two ways. domain.invariants.md cites TaskSyncEndpointTests as 'unit:' and requirements.md (line 102) cites TaskSyncEndpointTests as 'integration:'. The checker's coverage-level rule reads one of the two as mislabelled, and the label is a choice to satisfy the file rather than a fact about the test.
  • Left open, minor (.devbook/domain/tasks/features.md:1083): The change adds the 'sync' feature-flag chapter and edits Multi-device sync's meta block, but does not add a 'feature-flag:' reference to .devbook/domain/sync/context.md#taking-part-in-sync. TaskSyncWorker.ShouldRun gates that feature on SyncFeatures.Sync, so the graph shows no gated-by edge for it. Sessions from another machine has the same gap.

What could not be proved

  • A devbook-only change: no repository test reads the new chapters' content. The implementer's failing-first acceptance script (10/11 failing before, 11/11 after) lives outside the repository and is not committed. Read the new sync/ chapters and the context map against Backlog.Modules.Sync.
  • The context-map mermaid fence changed, so its committed Archify render (.devbook/domain/_archify/context-map.1.*) is now stale and the Devbook pane withholds it. Re-deliver that one spec (not render --all), or let feat(devbook): render Archify artifacts on demand instead of committing them #1294's on-demand rendering pick it up if that merges first.
  • New advisory: tasks/domain.md#device and sync/domain.md#device both claim the term "device"; the chapters say how they differ (the shared-kernel machine vs. its registration under a service-issued DeviceId).

Merge Danger

Door. Two-way: a revert restores the previous chapters. No code, data or setting changes.

Blast radius. local — the domain/ chapters, the context map and the derived devbook database built from them.

Assumptions taken instead of asking

  • No unit test checks the constant-time property itself. FixedTimeEquals sits in Sha256CredentialHasher.Matches and HmacSyncCursorCodec, but the tests only check outcomes (A_wrong_credential_gets_no_token, An_unknown_device_fails_the_same_way_a_wrong_credential_does). Assumption: the invariant cites those outcome tests and names the FixedTimeEquals call sites as where it is enforced, rather than claiming a timing test exists.
  • No test checks that the code is burned before the device is added (TryBurn before devices.Add in RedeemPairingCodeCommand.cs). A_code_pairs_one_device_and_no_more proves only the effect: a second redemption fails and the owner's device count stays at 2. Assumption: the invariant cites that test.
  • The issue lists four chapters, but devbook-domain.md requires six for a new context (it adds model.md and features.md). Assumption: write all six.
  • Assumption: Session Record Key is a Sync value object, even though it encodes the Sessions identity rule (agent plus session id). The chapter links sessions/domain.md#session-identity rather than redefining it.
  • Assumption: the generated context-map renders under .devbook/domain/_archify/ are not regenerated in this change. If the check flags them as stale, re-deliver per spec rather than running render --all.
  • Open issue [Devbook drift] .devbook/domain/ #896 (devbook drift on .devbook/domain/) asks which context owns the sync flag. Assumption: this change settles that row, and the PR body says so without closing [Devbook drift] .devbook/domain/ #896, which covers more.
  • AC10 forbids any change under src/ or tests/, and adding a Sync-specific content test to tools/devbook would widen scope. So the failing-first tests are a node acceptance script kept outside the repository, at C:/Users/jobsc/AppData/Local/Temp/claude/D--Repos-Backlog--claude-worktrees-backlog-scenario-evidence-screenshots-bbc6c4/0476623e-28a7-4f64-a95b-8bc8dbb972fc/scratchpad/sync-context.acceptance.test.mjs. It was run before the change (10 of 11 failing) and after (11 of 11 passing). It is not committed, so it does not count as repository test coverage.
  • Worked directly inside the workflow worktree and did not invoke the delivery:flow-spec skill separately: the issue-sweep workflow that spawned this agent is the flow running the change, and it forbids commits and PRs here. Personal Validation and PR creation remain with the owning session.
  • Pairing code is now the Pairing Code aggregate, with aliases, rather than a separate term chapter. The devbook-domain rule says a concept that is already a chapter earns no second chapter. The anchor sync/domain.md#pairing-code is the same slug the old Tasks term had.
  • Sync's aggregate is headed ## Device, as AC3 names it. This raises one new advisory warning: tasks/domain.md#device and sync/domain.md#device both claim the term "device". The chapter text says why they differ: the shared-kernel machine against its registration with the sync service under a service-issued DeviceId. The repository already tolerates several such cross-context collisions, for example Sessions Environment and Environment.
  • The push-cap rule is not on an aggregate, so it sits under the Change Precedence domain service's invariants chapter, with Enforced at naming the three push endpoints. The constant-time rule covers the credential and the cursor signature in one chapter, under Device, as AC4(a) describes a single chapter.
  • The invariant tests for the endpoint rules are tagged unit:dotnet:Backlog.Modules.Sync.Api.UnitTests..., following the existing unit-tagged citations of that project in inbox/capture invariants. The moved requirements keep their original unit/integration test tags unchanged.
  • The constant-time invariant cites tests that prove a wrong secret or a tampered signature is refused. The chapter states that timing itself is not measured by any test.
  • The Sync DeviceId is not the shared-kernel DeviceIdentity: OwnEnvironment maps one onto the other. So the Sync outbound row to the shared kernel is Conformist and describes that mapping.
  • context.md declares deployment: service, because the sync service is a separately deployed host (Backlog.Modules.Sync.Api). The context map has no bounded-context chapter for Sync to agree or disagree with.
  • The sprint-series, roadmap, GitHub-settings and notes kinds that ride the task feed are mentioned in the Sync inbound Tasks row only. progress/dependencies.md still calls replication a transport and was left alone to keep scope.
  • The generated .devbook/domain/_archify/context-map.1.architecture.standard.{html,json} renders were not regenerated. They are now stale relative to the new Sync node. No CI check enforces their freshness.
  • The context-map Sessions rule that points at tasks/domain.md#device was left unchanged, because Device keeps its anchor and the rule is still accurate.

Opened as a draft by an attended run of delivery-schedule:schedule-issue-sweep (2026-10-15), told by the owner to decide without asking and to name the choice and the alternative.

🤖 Generated with Claude Code

Sync had no place in the domain model although Backlog.Modules.Sync
owns pairing, device tokens, owner scope and the replica protocol.
Adds .devbook/domain/sync/ as a Supporting context, moves the pairing
vocabulary, feature and requirements there from Tasks, and gives the
context map its Sync node, edges and published-language row.

Decision taken without asking: Supporting subdomain, Open Host
Service upstream of Tasks, Sessions and Devbook, with the pairing
language moved out of Tasks; the alternative is named in the pull
request.

Refs #762

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JSdotNet
JSdotNet marked this pull request as ready for review October 15, 2026 18:06
@github-actions
github-actions Bot merged commit 712e8f1 into main Oct 15, 2026
15 checks passed
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.

Add the Sync bounded context to the domain model

1 participant