Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file.

### Added

- **@overeng/react-inspector**: Lineage annotation namespace (#687). New `Lineage` module with `SourceOfTruth | Derived | Projection | Cache | Mirror | External | Computed` tagged union, plus composable companion annotations (`Authority`, `Freshness`, `ForeignKey`). All annotations are self-describing Effect Schemas with ergonomic `pipe`-style constructors (`Lineage.derivedFrom`, `Lineage.cache`, `Lineage.authority`, etc.). The schema-aware renderer surfaces a small superscript glyph next to annotated field names and a dedicated `LINEAGE` / `AUTHORITY` / `FRESHNESS` / `REF` block in the schema tooltip. `SchemaInfo` gains an optional `lineage: LineageBundle` field. Source-field path references in `Derived.from` carry `data-lineage-target` attributes for future jump-to-source wiring. Round-trip-tested via vitest.
- **@overeng/react-inspector**: Map/Set container labels (#686). `Schema.Map({key, value})` renders as `Map<K, V>(N)`, `Schema.Set(T)` as `Set<T>(N)`, plus the `Readonly*` variants. Detected via the `effect/annotation/TypeConstructor` annotation on `Declaration` ASTs.
- **@overeng/react-inspector**: Runtime tagged-union narrowing (#686). When a field's declared schema is `Schema.Union(A, B, C)` of `_tag`-discriminated variants and the runtime value carries a matching `_tag`, the inspector narrows the display (name, tooltip, container label, nested field resolution) to the matched variant. Narrowing happens on every path segment, not just the leaf, so nested fields under a tagged union resolve through the matched variant. `SchemaProvider` gains a `rootData` prop and the context exposes a new `getContextForPathWithValue(path, value)` method.
- **@overeng/react-inspector**: Schema-derived container labels for arrays, records, and tuples (#686). Arrays show `Array<Item>(N)` instead of `Array(N)`, records show `Record<string, Money>` instead of `Object`, tuples show `[string, number, boolean]`. Named array/record schemas (`.annotations({ identifier: ... })`) take precedence over the constructed label. `SchemaInfo` gains a `containerLabel?: string` field. `getFieldSchema` now falls back to `indexSignature.type` so per-field schema resolution works inside records.
- **@overeng/react-inspector**: Rich schema annotation tooltips. Hovering or keyboard-focusing a field name (or struct type badge) now shows a tooltip surfacing `description`, `examples`, `default`, refinement-derived constraints (min/max/length/pattern/format/...), and possible values for `Literal` / `Enums` / `Union`-of-literal / `TemplateLiteral` ASTs. Replaces the previous native `title=` attribute. New exports: `SchemaTooltip`, `SchemaInfo`, `getSchemaInfo`, `getConstraintsFromJSONSchema`, `getPossibleValuesFromAST`. `getFieldSchema` no longer eagerly unwraps refinement/transformation wrappers so user-supplied annotations on those wrappers reach the tooltip.
- **@overeng/genie**: `githubLabels()` runtime primitive for declarative GitHub Issue/PR label management (color, description, deprecation, legacy migrations). Consumed by `mq-cli repo labels` in `schickling/dotfiles`.
- **genie/external.ts**: Shared label catalog exports (`commonLabels`, `mqLabels`, `andonLabels`, `deprecatedDefaults`, `legacyMigrations`) for cross-repo label IaC. Effect-utils self-applies via `.github/labels.json.genie.ts`.
- **@overeng/notion-effect-client**: Add database create/update/archive helpers and switch live Notion integration tests to provision isolated per-run fixtures under `NOTION_TEST_PARENT_PAGE_ID` instead of relying on stale hard-coded workspace page/database IDs.
Expand Down
4 changes: 2 additions & 2 deletions packages/@overeng/notion-md/src/sync.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,10 @@ const parseFile = async (path: string) => {
*/
const readSyncStateFile = async (path: string): Promise<NmdSyncStateV1> => {
const parsed = await parseFile(path)
const pageId = parsed.frontmatter.notion_md.page_id
const filePageId = parsed.frontmatter.notion_md.page_id
const baseName = path.split(/[\\/]/u).at(-1) ?? path
const root = path.slice(0, Math.max(0, path.length - baseName.length))
const sidecarPath = `${root}.notion-md/sync/${pageId}.json`
const sidecarPath = `${root}.notion-md/sync/${filePageId}.json`
return JSON.parse(await readFile(sidecarPath, 'utf8')) as NmdSyncStateV1
}

Expand Down
6 changes: 3 additions & 3 deletions packages/@overeng/notion-md/src/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,16 +250,16 @@ const listRemoteTree = (opts: {
{ pageId: root.page.id, title: root.page.title, segments: ['index'] },
]

const visit = (opts: {
const visit = (visitOpts: {
readonly pageId: string
readonly parentSegments: readonly string[]
}): Effect.Effect<void, NmdError> =>
gateway.listChildPages({ pageId: opts.pageId }).pipe(
gateway.listChildPages({ pageId: visitOpts.pageId }).pipe(
Effect.flatMap((children) =>
Effect.forEach(
children,
(child) => {
const segments = [...opts.parentSegments, child.title]
const segments = [...visitOpts.parentSegments, child.title]
pages.push({ pageId: child.pageId, title: child.title, segments })
return visit({ pageId: child.pageId, parentSegments: segments })
},
Expand Down
87 changes: 87 additions & 0 deletions packages/@overeng/react-inspector/FORK_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,93 @@ Each entry should include:

### Added

#### Lineage Annotations (2026-05-25, addresses #687)

New `Lineage` annotation namespace describing the epistemic status of a
field (source of truth, derived, projection, cache, mirror, external,
computed) plus composable companions (`Authority`, `Freshness`,
`ForeignKey`). The schema-aware renderer surfaces a small superscript
glyph next to annotated field names and a dedicated `LINEAGE` block in
the schema tooltip. Source field paths embedded in derivation summaries
carry `data-lineage-target="$.path"` for future jump-to-source wiring.

- `SchemaInfo` gains an optional `lineage: LineageBundle` field, exported
alongside the existing display-ready types.
- `SchemaTooltip` renders LINEAGE, AUTHORITY, FRESHNESS, and REF rows when
the corresponding annotations are present.
- Inline badge skipped for the default `SourceOfTruth` kind to avoid
cluttering every authoritative field.

New story: `LineageAnnotations` (`stories/effect-schema.stories.tsx`).

#### Schema-Derived Container Labels (2026-05-25, addresses #686)

Arrays, records, and tuples now surface their schema-derived element/value
type in the type-badge slot instead of the runtime constructor name.

- `Schema.Array(Item)` → `Array<Item>(N)` (was `Array(N)`)
- `Schema.Array(Item).annotations({ identifier: 'Pinned' })` → `Pinned(N)`
- `Schema.Record({ key: String, value: Money })` → `Record<string, Money>`
(was `Object`)
- `Schema.Tuple(String, Number, Boolean)` → `[string, number, boolean](N)`

Element/value names follow the same precedence as everywhere else:
`title` ?? `identifier` ?? type-kind (`string`, `number`, ...). Anonymous
structs and unions return `undefined` and fall through to the previous
behavior.

`getFieldSchema` now falls back to the first `indexSignature.type` when no
`propertySignature` matches, so per-field schema resolution works inside
records (tooltip, pretty formatting, nested field navigation).

Out of scope (tracked as #686 follow-ups): `Map`/`Set` containers,
runtime tagged-union narrowing.

#### Rich Schema Annotation Tooltips (2026-05-25)

Replaces the previous native `title=` attribute (browser default tooltip) with
a proper React tooltip that surfaces every useful Effect Schema annotation at
once. Hover or keyboard-focus a field name, the root label, or a struct's
type badge.

**New file**: `src/schema/SchemaTooltip.tsx`

**New exports**:

- `SchemaTooltip`, `SchemaTooltipProps` — the standalone tooltip component
- `SchemaInfo`, `SchemaConstraint` — display-ready info bundle types
- `getSchemaInfo(schema)` — build the bundle for a given schema
- `getConstraintsFromJSONSchema(ast)` — walk refinement chain and surface
human-readable constraints from `JSONSchemaAnnotationId` (min/max length,
min/max value, pattern, format, multipleOf, uniqueItems)
- `getPossibleValuesFromAST(ast)` — detect `Literal`, `Enums`,
`Union`-of-literal, `TemplateLiteral` and surface allowed values (capped at
12 with `… +N more`)

**Annotations surfaced** (in addition to the previously-supported
identifier/title/description/pretty):

- `examples` (`ExamplesAnnotationId`), formatted via `pretty` if present
- `default` (`DefaultAnnotationId`)
- `documentation` (`DocumentationAnnotationId`)

**Behavior changes**:

- `getFieldSchema` no longer eagerly unwraps refinement/transformation
wrappers. Previously, user-supplied `description`/`examples`/`default`
annotations on a `.pipe(Schema.int(), Schema.between(...)).annotations(...)`
were silently lost because the wrapper was unwrapped before annotation
extraction. Downstream traversal still works because `getFieldSchema` and
`getArrayElementSchema` re-apply `unwrapAstForDisplay` at the top of each
call.
- `SchemaAwareObjectPreview` now wraps the type-badge span in a
`SchemaTooltip` so the badge itself is the hover target for the value
type's annotations (separate from the field-name tooltip which targets the
field's declared schema).
- Built-in Effect descriptions like `"a string"`, `"a number"` are filtered
out of `hasContent`, so primitive fields without user annotations do not
get a tooltip affordance.

#### Effect Schema Support (2024-12-17)

Optional support for Effect Schema annotations to enrich the inspector display.
Expand Down
73 changes: 73 additions & 0 deletions packages/@overeng/react-inspector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,77 @@ const SchemaInspector = withSchemaSupport(ObjectInspector)
</SchemaProvider>
```

### Schema Annotation Tooltips

When using `withSchemaSupport`, hovering or keyboard-focusing a field name (or
the type badge on a struct) shows a rich tooltip with the schema's
annotations:

- **description** (`Symbol.for('effect/annotation/Description')`)
- **examples** (`ExamplesAnnotationId`), formatted via `pretty` if present
- **default** (`DefaultAnnotationId`)
- **constraints** derived from `JSONSchemaAnnotationId` set by refinements
like `nonEmptyString`, `int`, `between`, `pattern`, `format`, ...
- **possible values** for `Schema.Literal`, `Schema.Enums`,
`Schema.TemplateLiteral`, and `Schema.Union` of literals (capped at 12 with
a `… +N more` suffix)
- **documentation** (`DocumentationAnnotationId`)

Fields with only Effect's built-in primitive descriptions ("a string",
"a number", ...) do not get a tooltip — only user-supplied annotations
trigger one.

```tsx
const AgeSchema = Schema.Number.pipe(Schema.int(), Schema.between(0, 150)).annotations({
identifier: 'Age',
title: 'Age',
description: 'Age in whole years',
examples: [18, 42, 80],
default: 0,
})
```

For full control, the `SchemaTooltip` component is exported directly:

```tsx
import { SchemaTooltip, getSchemaInfo } from '@overeng/react-inspector'
;<SchemaTooltip info={getSchemaInfo(AgeSchema)}>
<span>age</span>
</SchemaTooltip>
```

### Lineage annotations

A standardized vocabulary for the _epistemic_ status of a field — is it the
source of truth, a derivation, a projection, a cache, etc. — surfaced by the
inspector as inline badges and a dedicated tooltip section
([#687](https://github.com/overengineeringstudio/effect-utils/issues/687)).

| Glyph | Kind | Meaning |
| ----- | --------------- | -------------------------------------------------------- |
| `⇆` | Source of truth | Authoritative value (default; no badge rendered inline). |
| `ƒ` | Derived | Computed deterministically from listed source fields. |
| `≈` | Projection | Read-model view of another field; may be stale. |
| `☷` | Cache | Cached copy with an optional TTL. |
| `↻` | Mirror | Synced replica of a field, often from another system. |
| `↗` | External | Reference into an external system. |
| `⊙` | Computed | Pure read-time computation; not persisted. |

Companion annotations (`Authority`, `Freshness`, `ForeignKey`) compose
with any `Lineage` and surface as extra tooltip rows.

```tsx
import { Lineage } from '@overeng/react-inspector'
import { Schema } from 'effect'

const OrderTotals = Schema.Struct({
subtotal: Schema.Number.pipe(Lineage.sourceOfTruth({ owner: 'orders' })),
total: Schema.Number.pipe(Lineage.derivedFrom(['subtotal', 'tax'])),
customerId: Schema.String.pipe(
Lineage.foreignKey('Customer', 'id'),
Lineage.authority({ writers: ['orders-svc'] }),
),
})
```

See [FORK_CHANGELOG.md](./FORK_CHANGELOG.md) for details on fork-specific features.
1 change: 1 addition & 0 deletions packages/@overeng/react-inspector/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export {
useSchemaDisplayInfo,
type SchemaContextValue,
type SchemaProviderProps,
Lineage,
} from './schema/mod.tsx'

export {
Expand Down
Loading
Loading