Skip to content
This repository was archived by the owner on Sep 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2a05dcc
Support DID subjects and links in native Atomic Data (#9)
michielbdejong Sep 9, 2026
f62fd77
Extract GitHub Devonian lens package
Sep 10, 2026
aa6fc19
Make GitHub lens package consumable from Devonian
Sep 10, 2026
1f57597
Extract Clockify and Notion integration lenses
Sep 10, 2026
a116d93
Publish stable GitHub lens entrypoints
Sep 10, 2026
9480c40
Expose bundled integration plugins
Sep 10, 2026
8faa883
Extract Google Calendar lens package
Sep 10, 2026
bf0c786
Describe Google Calendar lens package
Sep 10, 2026
0ccfbb7
Export Devonian source entry for browser consumers
Sep 10, 2026
1265a11
Include Devonian source runtime in package
Sep 10, 2026
5be2be0
Merge Google Calendar lens extraction
Sep 10, 2026
0fe34dd
Export Google Calendar lens entrypoints
Sep 10, 2026
eda6a94
Merge Clockify and Notion lens extraction
Sep 10, 2026
a986676
Add Atomic lib dev dependency for lens tests
Sep 10, 2026
15a902c
Add generic import query descriptor
Sep 10, 2026
d589d5f
Expose calendar lens defaults
Sep 10, 2026
f2dabe6
Export Google Calendar import lens
Sep 10, 2026
91b5a47
Expose Google Calendar lens defaults
Sep 10, 2026
efc53e6
Export atomic integration lens subpaths
Sep 10, 2026
8f35c94
Publish OAuth provider descriptors
Sep 10, 2026
dd6a2d5
Make unified lens package CI self-contained
Sep 10, 2026
4ec37e2
Describe provider token handling
Sep 10, 2026
a4076d1
Share Atomic JSON value types with consumers
Sep 10, 2026
83413f0
Export shared calendar JSON value type
Sep 10, 2026
eb1876b
Move GitHub integration implementation into Devonian
Sep 10, 2026
68c23cf
Preserve Calendar import scope semantics
Sep 10, 2026
58a2a44
Merge pull request #12 from localthought/codex/devonian-unified-lenses
michielbdejong Sep 10, 2026
006866b
Export Google Calendar host UI
Sep 10, 2026
c14742b
Describe direct setup in integration registry
Sep 10, 2026
92da823
Export external integration evidence registry
Sep 10, 2026
64e9f7c
Merge pull request #13 from localthought/codex/devonian-unified-lenses
michielbdejong Sep 10, 2026
bd1a67b
Use Atomic import identity property URLs
Sep 10, 2026
2757d10
Run GitHub tests through host aliases
Sep 10, 2026
d7e6834
Resolve relocated package test fixtures
Sep 10, 2026
6b546f8
Merge pull request #14 from localthought/codex/devonian-unified-lenses
michielbdejong Sep 10, 2026
6128c70
Publish integration presentation metadata
Sep 10, 2026
fb415ef
Import registered upgrade component
Sep 10, 2026
efee3b6
Merge pull request #15 from localthought/codex/devonian-unified-lenses
michielbdejong Sep 10, 2026
1dda6be
Remove extracted Atomic integrations (#16)
michielbdejong Sep 10, 2026
322fe00
Extract passive platform lens modules (#17)
michielbdejong Sep 10, 2026
698c2c7
Document GitHub issue and comment mapping example (#18)
michielbdejong Sep 10, 2026
12feb3c
Merge main into codex/atomic-did-identities
claude Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,15 @@ pnpm build:release
pnpm publish
```
The native resource API accepts HTTP(S) and DID identities, including AtomicServer `did:ad:` resources, properties and links. Identity allocation still uses an HTTP(S) base; bind existing DID resources explicitly. See [Atomic Data API](docs/atomic-data.md).

### Passive platform lenses

Provider transformations live in `platform-lenses/github-issues/lens/` and
`platform-lenses/google-calendar/lens/`, exposed through the corresponding
`devonian/platform-lenses/<platform>/lens` package entry points. These functions
consume supplied data and return projections or patches; they do not fetch,
subscribe, persist, or checkpoint. Their surrounding platform modules retain
existing runtime behavior and compatibility entry points.

See [the platform lens boundaries](docs/atomic-data.md#passive-platform-lenses)
for the forward and reverse mappings and their scope.
174 changes: 174 additions & 0 deletions __tests__/unit/platformLenses.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { describe, expect, it } from 'vitest';
import {
project,
unproject,
issuePatch,
type Issue,
} from '../../platform-lenses/github-issues/lens/index.js';
import { planCalendarValues } from '../../platform-lenses/google-calendar/lens/edit.js';
import {
applyCalendarEdit,
planCalendarEdit,
} from '../../platform-lenses/google-calendar/sync.js';

const issue: Issue & { assignee: string } = {
number: 42,
title: 'Before',
body: null,
state: 'open',
labels: ['bug', { name: 'ATOMIC:DOING' }],
assignee: 'someone',
};
const name = 'https://atomicdata.dev/properties/name';
const baseline = {
[name]: 'Before',
summary: 'Before',
description: 'Details',
};
const properties = {
summary: 'summary',
description: 'description',
start: 'start',
end: 'end',
};
const remote = {
summary: 'Before',
description: 'Details',
attendees: [{ email: 'person@example.com' }],
};

describe('passive platform lenses', () => {
it.each(['Todo', 'Doing', 'Done'] as const)(
'round trips GitHub %s without changing unrelated data',
(status) => {
const desired = { title: 'After', body: 'Edited', status };
const previous = structuredClone(issue);
const result = unproject(desired, issue);
expect(project(result)).toEqual(desired);
expect(result.number).toBe(42);
expect(result.assignee).toBe('someone');
expect(result.labels).toContain('bug');
expect(issue).toEqual(previous);
expect(unproject(desired, result)).toEqual(result);
},
);

it('keeps the GitHub runtime patch minimal', () => {
const before = project(issue);
expect(issuePatch(before, before)).toEqual({});
expect(issuePatch({ ...before, status: 'Done' }, before)).toEqual({
state: 'closed',
});
});

it('maps Calendar edits without replacing provider-only fields or mutating input', () => {
const row = { ...baseline, [name]: 'After', description: '' };
const previous = structuredClone(row);
const edit = planCalendarValues(row, baseline, properties, remote)!;
expect(edit.patch).toEqual({ summary: 'After', description: '' });
expect({ ...remote, ...edit.patch }.attendees).toEqual(remote.attendees);
expect(row).toEqual(previous);
const acknowledged = { ...row, ...edit.acknowledged };
expect(
planCalendarValues(acknowledged, acknowledged, properties, {
...remote,
...edit.patch,
}),
).toBeUndefined();
});

it('rejects competing Calendar edits and invalid intervals', () => {
expect(() =>
planCalendarValues(
{ ...baseline, [name]: 'Local' },
baseline,
properties,
{ ...remote, summary: 'Remote' },
),
).toThrow('conflict');
expect(() =>
planCalendarValues(
{ ...baseline, start: { date: '2026-09-12' } },
baseline,
properties,
{ ...remote, end: { date: '2026-09-11' } },
),
).toThrow('end must follow');
});
});

describe('Calendar runtime after lens extraction', () => {
const baselineKey = 'https://atomicdata.dev/properties/importBaseline';
const config = {
platform: 'google-calendar',
destinations: { event: { table: 'urn:calendar', rowClass: 'urn:event' } },
properties,
};
const row = {
...baseline,
[name]: 'After',
[baselineKey]: { values: baseline },
'https://atomicdata.dev/properties/localId': JSON.stringify([
'google-calendar',
'event',
'primary',
'42',
]),
'https://atomicdata.dev/properties/parent': 'urn:calendar',
'https://atomicdata.dev/properties/isA': ['urn:event'],
};

it('does not checkpoint failed writes', async () => {
const edit = planCalendarEdit('urn:event:42', row, config, {
...remote,
etag: 'v1',
})!;
let checkpointed = false;
await expect(
applyCalendarEdit(
edit,
async () => row,
async () => ({ status: 503, body: '' }),
async () => {
checkpointed = true;
},
),
).rejects.toThrow('503');
expect(checkpointed).toBe(false);
});

it('recovers a lost checkpoint without repeating the remote write', async () => {
const edit = planCalendarEdit('urn:event:42', row, config, {
...remote,
etag: 'v1',
})!;
const savedRemote = { ...remote, ...edit.patch, etag: 'v2' };
await expect(
applyCalendarEdit(
edit,
async () => row,
async () => ({ status: 200, body: JSON.stringify(savedRemote) }),
async () => {
throw new Error('Checkpoint unavailable');
},
),
).rejects.toThrow('Checkpoint unavailable');
const replay = planCalendarEdit('urn:event:42', row, config, savedRemote)!;
expect(replay.patch).toEqual({});
let writes = 0;
let checkpoint: unknown;
await applyCalendarEdit(
replay,
async () => row,
async () => {
writes++;
throw new Error('Unexpected write');
},
async (values) => {
checkpoint = values;
},
);
expect(writes).toBe(0);
expect(checkpoint).toMatchObject({ [name]: 'After', summary: 'After' });
});
});
26 changes: 26 additions & 0 deletions __tests__/unit/reconcileRecord.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { reconcileRecord } from '../../src/reconcileRecord.js';

describe('reconcileRecord', () => {
it('marks equal observations as agreed without changes', () => {
const decision = reconcileRecord(
{ title: 'before' },
{ title: 'same' },
{ title: 'same' },
);
expect(decision.conflicts).toEqual([]);
expect(decision.agreed).toEqual({ title: 'same' });
expect(decision.local).toEqual({});
expect(decision.remote).toEqual({});
});

it('reports a conflict when both sides edit the same field', () => {
const decision = reconcileRecord(
{ title: 'before' },
{ title: 'local' },
{ title: 'remote' },
);
expect(decision.conflicts).toHaveLength(1);
expect(decision.conflicts[0].property).toBe('title');
});
});
11 changes: 10 additions & 1 deletion __tests__/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ import {

export default defineConfig({
test: {
exclude: [...configDefaults.exclude, 'build/**/*'],
exclude: [
...configDefaults.exclude,
'build/**/*',
// These GitHub package tests exercise the Atomic host contract and run
// in the consuming application, which provides the host dependencies.
'platform-lenses/github-issues/adapter.test.ts',
'platform-lenses/github-issues/atomic.live.test.ts',
'platform-lenses/github-issues/automation.test.ts',
'platform-lenses/github-issues/github.live.test.ts',
],
coverage: {
provider: 'v8',
exclude: [...coverageConfigDefaults.exclude, 'build/**/*'],
Expand Down
31 changes: 31 additions & 0 deletions docs/atomic-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,34 @@ The behavioral tests cover distinct identical records, scoped identities, round-
Specification references: [JSON-AD](https://docs.atomicdata.dev/core/json-ad.html), [Atomic Schema](https://docs.atomicdata.dev/schema/intro.html), [Atomic Commits](https://docs.atomicdata.dev/commits/intro.html), [TypeScript SDK](https://atomicdata-dev.github.io/atomic-data-browser/docs/modules/_tomic_lib.html).

DID identifiers (including AtomicServer `did:ad:` base64 identities) are accepted as resource subjects, property keys, classes, links and connector scopes. Identifiers are preserved exactly; validation does not resolve DIDs or verify signatures. Identity allocator bases still require HTTP(S) URLs because the allocator appends child paths. Bind existing DID resources explicitly before ingesting their external records.

## Passive platform lenses

The `platform-lenses/github-issues/lens/` and
`platform-lenses/google-calendar/lens/` directories contain passive mappings.
A caller supplies resource data; these modules neither access datasets nor own
credentials, subscriptions, identity lookups, or durable synchronization state.

- GitHub's `project(issue)` maps issue fields to title/body/status.
`unproject(value, previousIssue)` maps back while preserving the issue number,
unrelated fields, and labels other than `atomic:doing`. The reverse mapping
normalizes that workflow label to the requested status. `issueFields` and
`issuePatch` support the existing runtime's field writes. The bridge's Atomic
property mapping for issues and comments lives in `lens/resources.mjs`.
- Calendar's `calendarProjection` and `calendarRecurrenceProjection` add Atomic
display/recurrence properties while retaining provider fields.
`planCalendarValues(row, baseline, properties, remote)` computes reverse
patches for summary, description, location, start, and end from supplied data.
It retains the existing field conflict and interval validation. It does not
write recurrence changes or replace the complete Google event. Empty strings
explicitly clear supported text fields.

Import these through `devonian/platform-lenses/github-issues/lens` or
`devonian/platform-lenses/google-calendar/lens`. Calendar's projection helpers
still require the consuming application's compatible `@tomic/lib` calendar
helpers. Previous adapter, projection, recurrence, types, and sync entry points
remain available.

Transport, dataset membership checks, preview workflows, ETags, and checkpointing
remain in the surrounding platform modules. This extraction does not introduce
an event handler, dataset abstraction, Loro replay, or distributed convergence.
23 changes: 23 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,27 @@ export default tseslint.config(
},
},
},
{
files: ['platform-lenses/**/*.{js,mjs,ts}'],
plugins: {
'@typescript-eslint': tseslint.plugin,
},
languageOptions: {
parserOptions: {
project: null,
},
globals: {
...globals.browser,
...globals.node,
settings: 'readonly',
},
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
);
26 changes: 23 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,36 @@
"node": ">= 22.11 < 23"
},
"exports": {
".": "./build/src/main.js"
".": "./src/main.ts",
"./platform-lenses/github-issues": "./platform-lenses/github-issues/index.mjs",
"./platform-lenses/github-issues/bridge": "./platform-lenses/github-issues/bridge.mjs",
"./platform-lenses/github-issues/ports": "./platform-lenses/github-issues/ports.mjs",
"./platform-lenses/github-issues/proxy": "./platform-lenses/github-issues/proxy.mjs",
"./platform-lenses/github-issues/adapter": "./platform-lenses/github-issues/adapter.ts",
"./platform-lenses/github-issues/tracker-actions": "./platform-lenses/github-issues/tracker-actions.ts",
"./platform-lenses/github-issues/plugin": "./platform-lenses/github-issues/plugin.ts",
"./platform-lenses/github-issues/plugin.js": "./platform-lenses/github-issues/plugin.js",
"./platform-lenses/github-issues/atomic": "./platform-lenses/github-issues/atomic.ts",
"./platform-lenses/google-calendar": "./platform-lenses/google-calendar/index.ts",
"./platform-lenses/google-calendar/sync": "./platform-lenses/google-calendar/sync.ts",
"./platform-lenses/google-calendar/recurrence": "./platform-lenses/google-calendar/recurrence.ts",
"./platform-lenses/github-issues/lens": "./platform-lenses/github-issues/lens/index.ts",
"./platform-lenses/google-calendar/lens": "./platform-lenses/google-calendar/lens/index.ts"
},
"files": [
"docs",
"build",
"vocab"
"vocab",
"platform-lenses",
"src"
],
"devDependencies": {
"@automerge/automerge-repo-network-broadcastchannel": "^2.0.0",
"@automerge/automerge-repo-storage-nodefs": "^2.0.0",
"@eslint/js": "~9.17",
"@types/eslint__js": "~8.42",
"@types/node": "~20",
"@tomic/lib": "^0.40.0",
"@typescript-eslint/parser": "~8.19",
"@vitest/coverage-v8": "^3.0.8",
"@vitest/eslint-plugin": "~1.1",
Expand Down Expand Up @@ -54,10 +71,13 @@
"license": "Apache-2.0",
"dependencies": {
"@automerge/automerge-repo": "^2.0.0",
"@tomic/lib": "^0.40.0",
"fast-json-stable-stringify": "^2.1.0",
"effect": "^3.16.0",
"tslib": "~2.8"
},
"peerDependencies": {
"@tomic/lib": ">=0.40.0"
},
"volta": {
"node": "22.12.0"
}
Expand Down
Loading
Loading