Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
44e864a
docs(notion-md): VRS for $EDITOR-based editing — refuse-lossy, one en…
schickling-assistant Jun 15, 2026
ffb6d15
fix(notion-md): refuse pages with not-round-trip-safe body blocks (R3…
schickling-assistant Jun 15, 2026
49c0d2d
fix(notion-md): canonicalize hosted-media URLs everywhere bodies are …
schickling-assistant Jun 15, 2026
e0f8e70
feat(notion-md): editor surfaces cat/put/edit (VRS Group A, R30-R39)
schickling-assistant Jun 15, 2026
23dc68a
feat(notion-md): schema-drift refusal via engine schema_snapshot (Gro…
schickling-assistant Jun 15, 2026
b38e6ff
feat(notion-md,notion-cli): Group G observability tests + notion edit…
schickling-assistant Jun 15, 2026
6ae9123
docs(notion-md): reconcile OTEL span table with implemented notion_md…
schickling-assistant Jun 15, 2026
26e6442
fix(notion-cli): build renderer TUI apps lazily to fix umbrella TDZ c…
schickling-assistant Jun 16, 2026
7b3fc37
feat(notion-md): surface integration token fingerprint on gateway errors
schickling-assistant Jun 16, 2026
39fba1a
docs(notion-cli): link upstream bun#30634 for the #787 lazy-init work…
schickling-assistant Jun 16, 2026
563772d
docs(notion-md): restructure VRS into layered subsystems + sync-progr…
schickling-assistant Jun 16, 2026
d1e03eb
docs(notion-md): add edit --read-only requirement (R46) to 01-editor VRS
schickling-assistant Jun 16, 2026
c267b69
feat(notion-md): add `edit --read-only` inspection-only editor session
schickling-assistant Jun 16, 2026
3fe487e
docs(notion-md): rebalance 03-sync-engine to own leaked push-engine m…
schickling-assistant Jun 16, 2026
3bea5ec
docs(notion-md): fix stale cross-cutting anchor links in 01-editor
schickling-assistant Jun 16, 2026
f0c12df
docs(notion-md): lift leaked impl detail out of VRS requirements into…
schickling-assistant Jun 16, 2026
5abce09
feat(notion-md): force list tightness in canonical body (spread:false)
schickling-assistant Jun 16, 2026
d8e7d7c
feat(notion-md): route pull receive through the canonical body form
schickling-assistant Jun 16, 2026
f2a1ace
refactor(notion-md): make pull body total, drop dead endpoint fallback
schickling-assistant Jun 16, 2026
6c5d635
refactor(notion-effect-client): delete vestigial markdownToBlocks con…
schickling-assistant Jun 16, 2026
6f5cdbe
refactor(notion-md): dedupe stripChildAnchors onto one definition
schickling-assistant Jun 16, 2026
e936846
test(notion-md): lock planMarkdownUpdate over a canonical base/remote
schickling-assistant Jun 16, 2026
2ddc7c0
docs(notion-md): clarify editor-surface frames the canonical body ver…
schickling-assistant Jun 16, 2026
bbd358b
refactor(notion): move canonical body fn into notion-effect-client (O…
schickling-assistant Jun 16, 2026
bc90f7f
docs(notion-md): record canonical-body decision 0019, re-baseline demo
schickling-assistant Jun 16, 2026
8f2327d
fix(nix): refresh pnpm-deps FOD hashes after the canonical-body dep move
schickling-assistant Jun 16, 2026
8c6259a
docs(notion-md): compact VRS decision log — delete 6 superseded recor…
schickling-assistant Jun 16, 2026
425a36c
test(notion-md): pin the canonical-body two-oracle invariant + demo f…
schickling-assistant Jun 16, 2026
48382ac
fix(nix): reconcile notion-md pnpm-deps FOD hash after canonical-body…
schickling-assistant Jun 16, 2026
228cc0d
fix(notion): address PR #786 codex review (media-url host gate, utils…
schickling-assistant Jun 16, 2026
8f4f19a
fix(nix): reconcile pnpm-deps FOD hashes after lockfile change
schickling-assistant Jun 16, 2026
5b064dd
fix(nix): reconcile oxc-config pnpm-deps FOD hash after lockfile change
schickling-assistant Jun 16, 2026
13430b1
fix(notion-effect-client): declare inherited utils peer deps via geni…
schickling-assistant Jun 16, 2026
fd86d27
feat(notion-md): staged sync progress + drift notes for `edit` (R43–R…
schickling-assistant Jun 16, 2026
17e719b
docs(notion-md): add R47 (visible remote drift on the write path)
schickling-assistant Jun 16, 2026
26d6b97
fix(notion-cli): avoid concurrent import TDZ
schickling-assistant Jun 16, 2026
b9b23a5
test(notion-cli): allow cold concurrent import
schickling-assistant Jun 16, 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
32 changes: 32 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion nix/oxc-config-plugin.nix
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ let
pnpm = pinnedPnpm;
};
packageDir = "packages/@overeng/oxc-config";
pnpmDepsHash = "sha256-0MeOm3vZjJiGpmVAyt6fOavjhYfehVswkXvN6DGLsjQ=";
pnpmDepsHash = "sha256-rdI4O8FDKX3N095b/fBFJQxtzUYPM62cl3mqKROmMMw=";

srcPath =
if builtins.isAttrs src && builtins.hasAttr "outPath" src then
Expand Down
2 changes: 1 addition & 1 deletion packages/@overeng/genie/nix/build.nix
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let
# Managed by the repo FOD refresh workflow — do not edit manually.
depsBuilds = {
"." = {
hash = "sha256-yV0ONh4haXUHi9isWdVnsuKfEXjGO8ESqsDrKALbVuU=";
hash = "sha256-6L+2U0Nssegn2SW0CiuNxPLRMSNCx5aGjVh6TaSlWNw=";
};
};
nativeNodePackages = [ opentuiCoreNative ];
Expand Down
2 changes: 1 addition & 1 deletion packages/@overeng/megarepo/nix/build.nix
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ let
# Managed by the repo FOD refresh workflow — do not edit manually.
depsBuilds = {
"." = {
hash = "sha256-1f7bldN6rGvybyvQZ00pQKp24zCL9ceoxpP8dvfU2Kg=";
hash = "sha256-lebuXoDP5ihwV9xwyz8fiBDpXMd1+biCeHGvTpit07c=";
};
};
nativeNodePackages = [ opentuiCoreNative ];
Expand Down
13 changes: 13 additions & 0 deletions packages/@overeng/notion-cli/docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ _Avoid_: alias, mode
A command implemented directly inside the Bun-compatible root CLI, such as `notion db info`.
_Avoid_: local command

**Editor alias**:
The top-level `notion edit <page>` command, an intentional marquee verb that
delegates to `notion md edit`. The only first-level command outside the
`md`/`schema`/`db` namespaces. Distinct from a retired legacy alias.
_Avoid_: shortcut, legacy alias

**Editor command**:
A `notion md` command for editor-based page editing, owned by
`@overeng/notion-md`: the stateless `cat`/`put` body pipes (stdin/stdout, no local
file) and `edit` (an ephemeral file-engine session over a `$TMPDIR` temp tree).
`--frontmatter` is read-only on `cat` and read/write on `edit`.
_Avoid_: streaming command (it spans the engine-backed `edit`), pipe command

**Node-backed Leaf**:
A `notion db` command that must execute in the packaged Node runtime because datasource-sync imports `node:sqlite`.
_Avoid_: sqlite command, replica namespace
Expand Down
2 changes: 2 additions & 0 deletions packages/@overeng/notion-cli/docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ This document defines package-level requirements for `@overeng/notion-cli`. It i
- **R04 Database namespace:** Database metadata, replica sync, status, conflict, diagnostics, and export workflows must live under `notion db`.
- **R05 Markdown namespace:** Markdown page workflows must live under `notion md` and be composed from `@overeng/notion-md`.
- **R06 Schema namespace:** Schema generation, introspection, config generation, and drift detection must live under `notion schema`.
- **R17 Markdown editor surface:** `notion md` must expose the `@overeng/notion-md` editor commands `cat`, `put`, and `edit` for editor-based two-way page editing (the stateless `cat`/`put` pipes and the engine-backed `edit`).
- **R18 Editor alias:** The root must expose a top-level `notion edit <page>` alias that delegates to `notion md edit`. This is an intentional marquee verb, not a legacy compatibility alias under R03; it is the only first-level command outside the `md`/`schema`/`db` namespaces.

### Must Preserve Runtime Boundaries

Expand Down
5 changes: 4 additions & 1 deletion packages/@overeng/notion-cli/docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ This spec does not define:

## Command Surface

Trace: R01-R06, R11-R13.
Trace: R01-R06, R11-R13, R17-R18.

```text
notion
├── edit <page> # top-level alias → md edit (marquee verb, R18)
├── md ... # @overeng/notion-md command tree
│ ├── cat / put / edit # editor surface: cat/put pipes + engine-backed edit (R17)
│ └── sync / status / plan # file-based surface
├── schema
│ ├── generate
│ ├── introspect
Expand Down
2 changes: 1 addition & 1 deletion packages/@overeng/notion-cli/nix/build.nix
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ let
# Managed by the repo FOD refresh workflow — do not edit manually.
depsBuilds = {
"." = {
hash = "sha256-CuFkj+1ti/aKBhqG8ZnJmJLHq64CKujgwVgxVneOnHo=";
hash = "sha256-ooTKSmikoUsrTRHe0Okjvv2607KVxojksmznKCebk2s=";
};
};
nativeNodePackages = [ opentuiCoreNative ];
Expand Down
5 changes: 4 additions & 1 deletion packages/@overeng/notion-cli/src/cli-command.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const placeholderCommand = (name: string) =>
Command.make(name, {}, () => Effect.void).pipe(Command.withDescription(`${name} command`))

describe('notion root command composition', () => {
it('does not expose the removed root sqlite command', async () => {
it('exposes md/schema/db plus the top-level edit alias, not the removed sqlite command', async () => {
const command = makeNotionRootCommand({
schemaCommand: placeholderCommand('schema'),
dbCommand: placeholderCommand('db'),
notionMdDispatchCommand: placeholderCommand('md'),
notionEditAliasCommand: placeholderCommand('edit'),
})

const completions = await Effect.runPromise(Command.getBashCompletions(command, 'notion'))
Expand All @@ -22,6 +23,8 @@ describe('notion root command composition', () => {
expect(completionText).toContain('schema')
expect(completionText).toContain('db')
expect(completionText).toContain('md')
// R18: the top-level `notion edit` marquee alias is a first-level command.
expect(completionText).toContain('edit')
expect(completionText).not.toContain('sqlite')
})
})
Expand Down
62 changes: 54 additions & 8 deletions packages/@overeng/notion-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import { Command } from '@effect/cli'
import { NodeContext, NodeRuntime } from '@effect/platform-node'
import { Cause, Effect, Layer, Option } from 'effect'
import { Cause, Effect, type Exit, Layer, Option } from 'effect'

import { editorExitCode } from '@overeng/notion-md'
import { CurrentWorkingDirectory } from '@overeng/utils/node'
import { rewriteHelpSubcommand } from '@overeng/utils/node/cli-help-rewrite'
import { CliVersion, resolveCliVersion } from '@overeng/utils/node/cli-version'
Expand Down Expand Up @@ -41,32 +42,77 @@ const makeNotionRootCommand = <
MdRequirements,
MdError,
MdConfig,
EditName extends string,
EditRequirements,
EditError,
EditConfig,
>({
schemaCommand,
dbCommand,
notionMdDispatchCommand,
notionEditAliasCommand,
}: {
readonly schemaCommand: Command.Command<SchemaName, SchemaRequirements, SchemaError, SchemaConfig>
readonly dbCommand: Command.Command<DbName, DbRequirements, DbError, DbConfig>
readonly notionMdDispatchCommand: Command.Command<MdName, MdRequirements, MdError, MdConfig>
readonly notionEditAliasCommand: Command.Command<
EditName,
EditRequirements,
EditError,
EditConfig
>
}) =>
Command.make('notion').pipe(
Command.withSubcommands([schemaCommand, dbCommand, notionMdDispatchCommand]),
// `edit` is the top-level marquee alias for `md edit` (R18); it is the only
// first-level command outside the md/schema/db namespaces.
Command.withSubcommands([
notionEditAliasCommand,
Comment thread
schickling-assistant marked this conversation as resolved.
schemaCommand,
dbCommand,
notionMdDispatchCommand,
]),
Command.withDescription(
'Notion CLI - database operations, schema generation, and markdown sync',
),
)

/**
* Map the program `Exit` to the editor-surface exit-code contract
* (notion-md `exit-codes.ts`), mirroring the standalone `notion-md` binary.
*
* Without this the umbrella `notion edit` alias would collapse every tagged
* editor failure (e.g. 3 lossy / 6 schema-drift / 8 abort) to the framework's
* default exit 1, so `notion edit` and `notion-md edit` would disagree for
* scripts. Safe for non-editor commands (schema/db/md): `editorExitCode` falls
* back to 1 for any unmapped failure and 0 on success, matching the previous
* default teardown (Ctrl+C now maps to 130, consistent with `notion-md`).
*/
const editorTeardown = <E, A>(exit: Exit.Exit<E, A>, onExit: (code: number) => void): void => {
onExit(editorExitCode(exit))
}

const runRootCli = async (argv: ReadonlyArray<string>) => {
const [{ notionMdDispatchCommand }, { dbCommand }, { schemaCommand }] = await Promise.all([
import('@overeng/notion-md/cli-program'),
import('./commands/db/mod.ts'),
import('./commands/schema/mod.ts'),
])
/*
* These trees are imported CONCURRENTLY. That concurrency is what triggers the
* upstream Bun bug oven-sh/bun#30634 (TDZ on a re-exported `const` read during
* parallel dynamic `import()`, Node-fine) — which is why every renderer's TUI
* app is built lazily via `get*App()` instead of at module top level (#787).
* TODO(bun#30634): once the Bun fix (PR oven-sh/bun#30656) ships and we pin a
* Bun version that includes it, the lazy `get*App()` workaround can be reverted
* to plain top-level `const *App = createTuiApp(...)`. See
* `concurrent-import.unit.test.ts` (the regression guard).
*/
const [{ notionMdDispatchCommand, notionEditAliasCommand }, { dbCommand }, { schemaCommand }] =
await Promise.all([
import('@overeng/notion-md/cli-program'),
import('./commands/db/mod.ts'),
import('./commands/schema/mod.ts'),
])
const command = makeNotionRootCommand({
schemaCommand,
dbCommand,
notionMdDispatchCommand,
notionEditAliasCommand,
})
const cli = Command.run(command, {
name: 'notion',
Expand Down Expand Up @@ -98,7 +144,7 @@ const runRootCli = async (argv: ReadonlyArray<string>) => {
makeOtelCliLayer({ serviceName: 'notion-cli' }),
),
),
NodeRuntime.runMain({ disableErrorReporting: true }),
NodeRuntime.runMain({ disableErrorReporting: true, teardown: editorTeardown }),
)
}

Expand Down
10 changes: 6 additions & 4 deletions packages/@overeng/notion-cli/src/commands/db/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@overeng/notion-datasource-sync/cli/effect-command'
import { outputOption as tuiOutputOption, outputModeLayer } from '@overeng/tui-react/node'

import { InfoApp } from '../../renderers/InfoOutput/app.ts'
import { getInfoApp } from '../../renderers/InfoOutput/app.ts'
import { InfoView } from '../../renderers/InfoOutput/view.tsx'

/** Re-export internal types for TypeScript declaration emit */
Expand All @@ -24,7 +24,7 @@ export type { PlatformError } from '@effect/platform/Error'
import { NotionConfig, NotionDatabases, NotionDataSources } from '@overeng/notion-effect-client'
import { run } from '@overeng/tui-react'

import { resolveNotionToken, tokenOption } from '../schema/mod.ts'
import { resolveNotionToken, tokenOption } from '../shared.ts'

const databaseIdArg = Args.text({ name: 'database-id' }).pipe(
Args.withDescription('The Notion database ID to operate on'),
Expand All @@ -49,8 +49,10 @@ const infoCommand = Command.make(
authToken: resolvedToken,
})

const infoApp = getInfoApp()

yield* run(
InfoApp,
infoApp,
(tui) =>
Effect.gen(function* () {
const program = Effect.gen(function* () {
Expand Down Expand Up @@ -90,7 +92,7 @@ const infoCommand = Command.make(
Effect.provide(Layer.merge(configLayer, FetchHttpClient.layer)),
)
}),
{ view: React.createElement(InfoView, { stateAtom: InfoApp.stateAtom }) },
{ view: React.createElement(InfoView, { stateAtom: infoApp.stateAtom }) },
).pipe(Effect.provide(outputModeLayer(output)))
}),
).pipe(Command.withDescription('Display information about a Notion database'))
Expand Down
57 changes: 25 additions & 32 deletions packages/@overeng/notion-cli/src/commands/schema/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,23 @@ import { fileURLToPath } from 'node:url'

import { Args, Command, Options } from '@effect/cli'
import { FetchHttpClient, FileSystem } from '@effect/platform'
import { Effect, Layer, Option, Redacted, Schema } from 'effect'
import { Effect, Layer, Option, Schema } from 'effect'
import React from 'react'

import { EffectPath } from '@overeng/effect-path'
import {
NotionConfig,
NotionDatabases,
NotionDataSources,
resolveNotionToken as resolveNotionTokenFromEnv,
} from '@overeng/notion-effect-client'
import { NotionConfig, NotionDatabases, NotionDataSources } from '@overeng/notion-effect-client'
import { run } from '@overeng/tui-react'
import { outputOption as tuiOutputOption, outputModeLayer } from '@overeng/tui-react/node'

import { DiffApp } from '../../renderers/DiffOutput/app.ts'
import { getDiffApp } from '../../renderers/DiffOutput/app.ts'
import { DiffView } from '../../renderers/DiffOutput/view.tsx'
import { GenerateConfigApp } from '../../renderers/GenerateConfigOutput/app.ts'
import { getGenerateConfigApp } from '../../renderers/GenerateConfigOutput/app.ts'
import { GenerateConfigView } from '../../renderers/GenerateConfigOutput/view.tsx'
import { GenerateApp } from '../../renderers/GenerateOutput/app.ts'
import { getGenerateApp } from '../../renderers/GenerateOutput/app.ts'
import { GenerateView } from '../../renderers/GenerateOutput/view.tsx'
import { IntrospectApp } from '../../renderers/IntrospectOutput/app.ts'
import { getIntrospectApp } from '../../renderers/IntrospectOutput/app.ts'
import { IntrospectView } from '../../renderers/IntrospectOutput/view.tsx'
import { resolveNotionToken, tokenOption } from '../shared.ts'

/** Re-export internal types for TypeScript declaration emit */
export type { PlatformError } from '@effect/platform/Error'
Expand All @@ -38,6 +34,8 @@ import { computeDiff, hasDifferences, parseGeneratedFile } from '../../diff.ts'
import { introspectDatabase, type PropertyTransformConfig } from '../../introspect.ts'
import { formatCode, writeSchemaToFile } from '../../output.ts'

export { resolveNotionToken, tokenOption } from '../shared.ts'

// -----------------------------------------------------------------------------
// Exported Errors
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -75,19 +73,6 @@ const getGeneratorVersion = Effect.gen(function* () {
return pkg.version
}).pipe(Effect.orElseSucceed(() => 'unknown'))

/** Resolve the Notion API token as a `Redacted` value from the CLI option or the environment. */
export const resolveNotionToken = (token: Option.Option<string>) =>
Option.isSome(token) === true
? Effect.succeed(Redacted.make(token.value))
: resolveNotionTokenFromEnv()

/** CLI option for providing a Notion API token (defaults to `NOTION_API_TOKEN`). */
export const tokenOption = Options.text('token').pipe(
Options.withAlias('t'),
Options.withDescription('Notion API token (defaults to NOTION_API_TOKEN env var)'),
Options.optional,
)

// -----------------------------------------------------------------------------
// Generate Command
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -215,8 +200,10 @@ const generateCommand = Command.make(
authToken: resolvedToken,
})

const generateApp = getGenerateApp()

yield* run(
GenerateApp,
generateApp,
(tui) =>
Effect.gen(function* () {
const program = Effect.gen(function* () {
Expand Down Expand Up @@ -305,7 +292,7 @@ const generateCommand = Command.make(
Effect.provide(Layer.merge(configLayer, FetchHttpClient.layer)),
)
}),
{ view: React.createElement(GenerateView, { stateAtom: GenerateApp.stateAtom }) },
{ view: React.createElement(GenerateView, { stateAtom: generateApp.stateAtom }) },
).pipe(Effect.provide(outputModeLayer(tuiOutput)))
}),
).pipe(Command.withDescription('Generate Effect schema from a Notion database'))
Expand All @@ -329,8 +316,10 @@ const introspectCommand = Command.make(
authToken: resolvedToken,
})

const introspectApp = getIntrospectApp()

yield* run(
IntrospectApp,
introspectApp,
(tui) =>
Effect.gen(function* () {
const program = Effect.gen(function* () {
Expand Down Expand Up @@ -400,7 +389,7 @@ const introspectCommand = Command.make(
Effect.provide(Layer.merge(configLayer, FetchHttpClient.layer)),
)
}),
{ view: React.createElement(IntrospectView, { stateAtom: IntrospectApp.stateAtom }) },
{ view: React.createElement(IntrospectView, { stateAtom: introspectApp.stateAtom }) },
).pipe(Effect.provide(outputModeLayer(output)))
}),
).pipe(Command.withDescription('Introspect a Notion database and display its schema'))
Expand Down Expand Up @@ -439,8 +428,10 @@ const generateFromConfigCommand = Command.make(
authToken: resolvedToken,
})

const generateConfigApp = getGenerateConfigApp()

yield* run(
GenerateConfigApp,
generateConfigApp,
(tui) =>
Effect.gen(function* () {
const program = Effect.gen(function* () {
Expand Down Expand Up @@ -529,7 +520,7 @@ const generateFromConfigCommand = Command.make(
)
}),
{
view: React.createElement(GenerateConfigView, { stateAtom: GenerateConfigApp.stateAtom }),
view: React.createElement(GenerateConfigView, { stateAtom: generateConfigApp.stateAtom }),
},
).pipe(Effect.provide(outputModeLayer(output)))
}),
Expand Down Expand Up @@ -571,8 +562,10 @@ const diffCommand = Command.make(
authToken: resolvedToken,
})

const diffApp = getDiffApp()

yield* run(
DiffApp,
diffApp,
(tui) =>
Effect.gen(function* () {
const program = Effect.gen(function* () {
Expand Down Expand Up @@ -634,7 +627,7 @@ const diffCommand = Command.make(
Effect.provide(Layer.merge(configLayer, FetchHttpClient.layer)),
)
}),
{ view: React.createElement(DiffView, { stateAtom: DiffApp.stateAtom }) },
{ view: React.createElement(DiffView, { stateAtom: diffApp.stateAtom }) },
).pipe(Effect.provide(outputModeLayer(output)))
}),
).pipe(
Expand Down
Loading
Loading