From 6df61a1a0f491ae1daa89fddb7cfa1c8f09ee522 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Wed, 27 May 2026 11:32:02 +0200 Subject: [PATCH] refactor(utils): expose Lineage namespace via main @overeng/utils entry Move the `Lineage` annotation namespace (added in #688) out of `@overeng/react-inspector` and into `@overeng/utils`, exposed through the package's existing main isomorphic entry. Non-React consumers can now annotate schemas with epistemic lineage without pulling in the inspector, while sharing a single source of truth for the schema, symbols, helpers, and display formatting. Exposed via the existing `.` export (same pattern as `InMemoryBacking`) rather than a separate `./lineage` subpath, since every other isomorphic module already lives on the main entry. Co-Authored-By: Claude Opus 4.7 (1M context) --- nix/oxc-config-plugin.nix | 2 +- packages/@overeng/genie/nix/build.nix | 2 +- packages/@overeng/megarepo/nix/build.nix | 2 +- packages/@overeng/notion-cli/nix/build.nix | 2 +- packages/@overeng/notion-md/nix/build.nix | 2 +- .../@overeng/react-inspector/package.json | 8 + .../react-inspector/package.json.genie.ts | 1 + .../src/schema/effectSchema.tsx | 36 +-- .../react-inspector/src/schema/mod.tsx | 4 +- .../stories/effect-schema.stories.tsx | 14 +- packages/@overeng/tui-stories/nix/build.nix | 2 +- .../src/isomorphic/lineage/mod.ts} | 227 +++++++++++------- .../src/isomorphic/lineage/mod.unit.test.ts} | 24 +- packages/@overeng/utils/src/isomorphic/mod.ts | 3 + pnpm-lock.yaml | 24 ++ 15 files changed, 216 insertions(+), 137 deletions(-) rename packages/@overeng/{react-inspector/src/schema/lineage.ts => utils/src/isomorphic/lineage/mod.ts} (66%) rename packages/@overeng/{react-inspector/src/schema/lineage.test.ts => utils/src/isomorphic/lineage/mod.unit.test.ts} (85%) diff --git a/nix/oxc-config-plugin.nix b/nix/oxc-config-plugin.nix index 060aaa874..ed2b866a4 100644 --- a/nix/oxc-config-plugin.nix +++ b/nix/oxc-config-plugin.nix @@ -28,7 +28,7 @@ let pnpm = pinnedPnpm; }; packageDir = "packages/@overeng/oxc-config"; - pnpmDepsHash = "sha256-UPDXMkAo6ZbawNzCZRiQ7066m6fo+Shq6CfhMHHx07w="; + pnpmDepsHash = "sha256-HM+QdonkgSfj4h4Q4XmVKpQD2hW2Rf4TLanvFYYaqo8="; srcPath = if builtins.isAttrs src && builtins.hasAttr "outPath" src then diff --git a/packages/@overeng/genie/nix/build.nix b/packages/@overeng/genie/nix/build.nix index fbb49ed50..fefc6b298 100644 --- a/packages/@overeng/genie/nix/build.nix +++ b/packages/@overeng/genie/nix/build.nix @@ -25,7 +25,7 @@ let # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { "." = { - hash = "sha256-D7Z0w54Tn/LmKVZuun9ndutkAtJGM/dkDEAfKcgfJ1Q="; + hash = "sha256-xc01fEymQyAwgwDbnaa5uVCoE30XhKG8bO9CHtnigik="; }; }; nativeNodePackages = [ opentuiCoreNative ]; diff --git a/packages/@overeng/megarepo/nix/build.nix b/packages/@overeng/megarepo/nix/build.nix index d3555e3e1..6340d5a45 100644 --- a/packages/@overeng/megarepo/nix/build.nix +++ b/packages/@overeng/megarepo/nix/build.nix @@ -24,7 +24,7 @@ let # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { "." = { - hash = "sha256-AgCjUDGkQ115HNjU9GSRRKkJEofXLgaHzEtObR54Nmo="; + hash = "sha256-MyW7Um2sE/4H5bWIdHHN4kOdnqj8c7MGQSeaA/JE0Fs="; }; }; nativeNodePackages = [ opentuiCoreNative ]; diff --git a/packages/@overeng/notion-cli/nix/build.nix b/packages/@overeng/notion-cli/nix/build.nix index 12095e473..c539cedce 100644 --- a/packages/@overeng/notion-cli/nix/build.nix +++ b/packages/@overeng/notion-cli/nix/build.nix @@ -21,7 +21,7 @@ let # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { "." = { - hash = "sha256-Nzrd6Wz7OWympJSsLBU0uodHL5/v1fgfvBvx/Q9IBf4="; + hash = "sha256-2mqls7rueBX2fJb9dNb+m3Lo1iD9dFK8jO18rM1DdZA="; }; }; nativeNodePackages = [ opentuiCoreNative ]; diff --git a/packages/@overeng/notion-md/nix/build.nix b/packages/@overeng/notion-md/nix/build.nix index a9e5e2246..966c362b9 100644 --- a/packages/@overeng/notion-md/nix/build.nix +++ b/packages/@overeng/notion-md/nix/build.nix @@ -20,7 +20,7 @@ let # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { "." = { - hash = "sha256-jU1WCdUEuiPC6EJRfbg8poF+JGVR8+KJ7ACnmjsnerw="; + hash = "sha256-tbtOTx40FPsbl8oJSHjk6k+1D3vstfiVvzwPj2zt/S8="; }; }; smokeTestArgs = [ "--help" ]; diff --git a/packages/@overeng/react-inspector/package.json b/packages/@overeng/react-inspector/package.json index dae31263b..4ee9fa3d7 100644 --- a/packages/@overeng/react-inspector/package.json +++ b/packages/@overeng/react-inspector/package.json @@ -42,6 +42,14 @@ "vitest": "3.2.4" }, "peerDependencies": { + "@effect/cluster": "^0.58.2", + "@effect/experimental": "^0.60.0", + "@effect/opentelemetry": "^0.63.0", + "@effect/platform": "^0.96.1", + "@effect/platform-node": "^0.106.0", + "@effect/rpc": "^0.75.1", + "@effect/workflow": "^0.18.0", + "@playwright/test": "^1.59.1", "effect": "^3.21.2", "react": "^19.2.3" }, diff --git a/packages/@overeng/react-inspector/package.json.genie.ts b/packages/@overeng/react-inspector/package.json.genie.ts index 953f50e5f..7cadb81fc 100644 --- a/packages/@overeng/react-inspector/package.json.genie.ts +++ b/packages/@overeng/react-inspector/package.json.genie.ts @@ -36,6 +36,7 @@ const workspaceDeps = catalog.compose({ }, }, peerDependencies: { + workspace: [utilsPkg], external: catalog.pick(...peerDepNames), }, }) diff --git a/packages/@overeng/react-inspector/src/schema/effectSchema.tsx b/packages/@overeng/react-inspector/src/schema/effectSchema.tsx index 3edd8fbe1..1f38a0f37 100644 --- a/packages/@overeng/react-inspector/src/schema/effectSchema.tsx +++ b/packages/@overeng/react-inspector/src/schema/effectSchema.tsx @@ -1,16 +1,6 @@ import type { Schema as S, SchemaAST } from 'effect' -import { - type Authority, - type Freshness, - type LineageDisplay, - type Reference, - getAuthority, - getFreshness, - getLineage, - getLineageDisplay, - getReference, -} from './lineage.ts' +import { Lineage } from '@overeng/utils' /** Symbols used by Effect Schema for annotations */ const IdentifierAnnotationId = Symbol.for('effect/annotation/Identifier') @@ -87,10 +77,10 @@ export interface SchemaInfo { * @see https://github.com/overengineeringstudio/effect-utils/issues/687 */ export interface LineageBundle { - display: LineageDisplay - authority?: Authority - freshness?: Freshness - reference?: Reference + display: Lineage.LineageDisplay + authority?: Lineage.Authority + freshness?: Lineage.Freshness + reference?: Lineage.Reference } const isNullishAst = (ast: SchemaAST.AST): boolean => { @@ -759,20 +749,20 @@ export const getSchemaInfo = (schema: S.Schema.AnyNoContext): SchemaInfo => { * lineage to the refinement wrapper, and unwrapping would lose it. The * helpers themselves try raw-then-unwrapped so either layer wins. */ - const lineageValue = getLineage(schema) + const lineageValue = Lineage.getLineage(schema) const lineage: LineageBundle | undefined = lineageValue !== undefined ? { - display: getLineageDisplay(lineageValue), - authority: getAuthority(schema), - freshness: getFreshness(schema), - reference: getReference(schema), + display: Lineage.getLineageDisplay(lineageValue), + authority: Lineage.getAuthority(schema), + freshness: Lineage.getFreshness(schema), + reference: Lineage.getReference(schema), } : (() => { /* No primary Lineage but companion annotations may still exist. */ - const authority = getAuthority(schema) - const freshness = getFreshness(schema) - const reference = getReference(schema) + const authority = Lineage.getAuthority(schema) + const freshness = Lineage.getFreshness(schema) + const reference = Lineage.getReference(schema) if (authority === undefined && freshness === undefined && reference === undefined) { return undefined } diff --git a/packages/@overeng/react-inspector/src/schema/mod.tsx b/packages/@overeng/react-inspector/src/schema/mod.tsx index f80ee41ae..722bd4dea 100644 --- a/packages/@overeng/react-inspector/src/schema/mod.tsx +++ b/packages/@overeng/react-inspector/src/schema/mod.tsx @@ -37,6 +37,4 @@ export { SchemaAwareObjectValue } from './SchemaAwareObjectValue.tsx' export { SchemaAwareObjectPreview } from './SchemaAwareObjectPreview.tsx' export { SchemaTooltip, type SchemaTooltipProps } from './SchemaTooltip.tsx' -import * as Lineage from './lineage.ts' - -export { Lineage } +export { Lineage } from '@overeng/utils' diff --git a/packages/@overeng/react-inspector/stories/effect-schema.stories.tsx b/packages/@overeng/react-inspector/stories/effect-schema.stories.tsx index 92c3e8d1d..4fde74e99 100644 --- a/packages/@overeng/react-inspector/stories/effect-schema.stories.tsx +++ b/packages/@overeng/react-inspector/stories/effect-schema.stories.tsx @@ -964,16 +964,18 @@ export const RuntimeTaggedUnionNarrowing = { const OrderTotalsSchema = Schema.Struct({ subtotal: Schema.Number.pipe(Lineage.sourceOfTruth({ owner: 'orders' })), tax: Schema.Number.pipe(Lineage.sourceOfTruth()), - total: Schema.Number.pipe(Lineage.derivedFrom(['subtotal', 'tax'], 'Pure', { pure: true })), + total: Schema.Number.pipe( + Lineage.derivedFrom({ from: ['subtotal', 'tax'], how: 'Pure', pure: true }), + ), displayTotal: Schema.String.pipe(Lineage.computed({ fn: 'formatMoney(total)' })), - cachedFxRate: Schema.Number.pipe(Lineage.cache('fxRate', { ttlMs: 60_000 })), - mirroredStripeId: Schema.String.pipe(Lineage.mirror('id', { system: 'stripe' })), - legacyOrderRef: Schema.String.pipe(Lineage.external('legacy-erp', 'order-id')), - lastSyncedSnapshot: Schema.Number.pipe(Lineage.projection('total', { stalenessMs: 30_000 })), + cachedFxRate: Schema.Number.pipe(Lineage.cache({ of: 'fxRate', ttlMs: 60_000 })), + mirroredStripeId: Schema.String.pipe(Lineage.mirror({ of: 'id', system: 'stripe' })), + legacyOrderRef: Schema.String.pipe(Lineage.external({ system: 'legacy-erp', ref: 'order-id' })), + lastSyncedSnapshot: Schema.Number.pipe(Lineage.projection({ of: 'total', stalenessMs: 30_000 })), customerId: Schema.String.pipe( Lineage.authority({ writers: ['orders-svc'], readers: ['*'] }), Lineage.freshness({ capturedAt: 'event-time', maxAgeMs: 5_000 }), - Lineage.foreignKey('Customer', 'id'), + Lineage.foreignKey({ targetSchema: 'Customer', targetField: 'id' }), ), }).annotations({ identifier: 'OrderTotals', diff --git a/packages/@overeng/tui-stories/nix/build.nix b/packages/@overeng/tui-stories/nix/build.nix index a7305a594..ecdbf6516 100644 --- a/packages/@overeng/tui-stories/nix/build.nix +++ b/packages/@overeng/tui-stories/nix/build.nix @@ -21,7 +21,7 @@ let # Managed by the repo FOD refresh workflow — do not edit manually. depsBuilds = { "." = { - hash = "sha256-0VxYgFoPszkCa1bpaSEWGFm5TBIO1g8CMDhVzrKNVx8="; + hash = "sha256-d/vdg8I2DCZgZnGz5XaYeOibHhaJio1dqp81mrmjS98="; }; }; nativeNodePackages = [ opentuiCoreNative ]; diff --git a/packages/@overeng/react-inspector/src/schema/lineage.ts b/packages/@overeng/utils/src/isomorphic/lineage/mod.ts similarity index 66% rename from packages/@overeng/react-inspector/src/schema/lineage.ts rename to packages/@overeng/utils/src/isomorphic/lineage/mod.ts index 0ae7f62c6..4a96edbd7 100644 --- a/packages/@overeng/react-inspector/src/schema/lineage.ts +++ b/packages/@overeng/utils/src/isomorphic/lineage/mod.ts @@ -105,9 +105,13 @@ export type Reference = typeof Reference.Type * Annotation symbols * -------------------------------------------------------------------------- */ +/** Annotation symbol for the `Lineage` annotation. */ export const LineageAnnotationId = Symbol.for('effect/annotation/Lineage') +/** Annotation symbol for the `Authority` annotation. */ export const AuthorityAnnotationId = Symbol.for('effect/annotation/Authority') +/** Annotation symbol for the `Freshness` annotation. */ export const FreshnessAnnotationId = Symbol.for('effect/annotation/Freshness') +/** Annotation symbol for the `Reference` annotation. */ export const ReferenceAnnotationId = Symbol.for('effect/annotation/Reference') /* -------------------------------------------------------------------------- @@ -156,11 +160,12 @@ const unwrapAst = (ast: SchemaAST.AST): SchemaAST.AST => { * schema decoder; a corrupt or unrecognized value yields `undefined` rather * than throwing — the inspector must never crash on bad annotations. */ -const readAnnotation = ( - schema: Schema.Schema.AnyNoContext, - id: symbol, - decoder: Schema.Schema, -): A | undefined => { +const readAnnotation = (args: { + schema: Schema.Schema.AnyNoContext + id: symbol + decoder: Schema.Schema +}): A | undefined => { + const { schema, id, decoder } = args const decode = Schema.decodeUnknownOption(decoder) const raw = schema.ast.annotations[id] if (raw !== undefined) { @@ -178,23 +183,27 @@ const readAnnotation = ( return undefined } +/** Read the `Lineage` annotation from a schema, if present. */ export const getLineage = (schema: Schema.Schema.AnyNoContext): Lineage | undefined => - readAnnotation(schema, LineageAnnotationId, Lineage) + readAnnotation({ schema, id: LineageAnnotationId, decoder: Lineage }) +/** Read the `Authority` annotation from a schema, if present. */ export const getAuthority = (schema: Schema.Schema.AnyNoContext): Authority | undefined => - readAnnotation(schema, AuthorityAnnotationId, Authority) + readAnnotation({ schema, id: AuthorityAnnotationId, decoder: Authority }) +/** Read the `Freshness` annotation from a schema, if present. */ export const getFreshness = (schema: Schema.Schema.AnyNoContext): Freshness | undefined => - readAnnotation(schema, FreshnessAnnotationId, Freshness) + readAnnotation({ schema, id: FreshnessAnnotationId, decoder: Freshness }) +/** Read the `Reference` annotation from a schema, if present. */ export const getReference = (schema: Schema.Schema.AnyNoContext): Reference | undefined => - readAnnotation(schema, ReferenceAnnotationId, Reference) + readAnnotation({ schema, id: ReferenceAnnotationId, decoder: Reference }) /* -------------------------------------------------------------------------- * Ergonomic constructors * * Each returns a `Schema -> Schema` function suitable for `.pipe(...)`, e.g. - * Schema.Number.pipe(derivedFrom(['subtotal', 'tax'])) + * Schema.Number.pipe(derivedFrom({ from: ['subtotal', 'tax'] })) * -------------------------------------------------------------------------- */ const fieldRef = (path: string): LineageRef => ({ @@ -224,56 +233,81 @@ const coerceDerivationKind = ( } const annotate = - (id: symbol, value: V) => + (args: { id: symbol; value: V }) => (schema: S): S => - schema.annotations({ [id]: value }) as S + schema.annotations({ [args.id]: args.value }) as S const lineageAnnotation = (value: Lineage) => (schema: S): S => - annotate(LineageAnnotationId, value)(schema) + annotate({ id: LineageAnnotationId, value })(schema) +/** Mark a field as the authoritative source of truth. */ export const sourceOfTruth = (opts?: { owner?: string; system?: string }) => lineageAnnotation({ _tag: 'SourceOfTruth', ...opts }) -export const derivedFrom = ( - from: ReadonlyArray, - how?: DerivationKind | DerivationKind['_tag'], - opts?: { pure?: boolean }, -) => +/** Mark a field as derived from one or more upstream fields. */ +export const derivedFrom = (args: { + from: ReadonlyArray + how?: DerivationKind | DerivationKind['_tag'] + pure?: boolean +}) => lineageAnnotation({ _tag: 'Derived', - from: from.map(coerceRef), - how: coerceDerivationKind(how), - ...opts, + from: args.from.map(coerceRef), + how: coerceDerivationKind(args.how), + ...(args.pure !== undefined ? { pure: args.pure } : {}), }) -export const projection = (of: string | LineageRef, opts?: { stalenessMs?: number }) => - lineageAnnotation({ _tag: 'Projection', of: coerceRef(of), ...opts }) +/** Mark a field as a (possibly stale) projection of another field. */ +export const projection = (args: { of: string | LineageRef; stalenessMs?: number }) => + lineageAnnotation({ + _tag: 'Projection', + of: coerceRef(args.of), + ...(args.stalenessMs !== undefined ? { stalenessMs: args.stalenessMs } : {}), + }) -export const cache = (of: string | LineageRef, opts?: { ttlMs?: number }) => - lineageAnnotation({ _tag: 'Cache', of: coerceRef(of), ...opts }) +/** Mark a field as a cached copy of another field, with optional TTL. */ +export const cache = (args: { of: string | LineageRef; ttlMs?: number }) => + lineageAnnotation({ + _tag: 'Cache', + of: coerceRef(args.of), + ...(args.ttlMs !== undefined ? { ttlMs: args.ttlMs } : {}), + }) -export const mirror = (of: string | LineageRef, opts?: { system?: string }) => - lineageAnnotation({ _tag: 'Mirror', of: coerceRef(of), ...opts }) +/** Mark a field as a mirror of another field, optionally from a foreign system. */ +export const mirror = (args: { of: string | LineageRef; system?: string }) => + lineageAnnotation({ + _tag: 'Mirror', + of: coerceRef(args.of), + ...(args.system !== undefined ? { system: args.system } : {}), + }) -export const external = (system: string, ref?: string) => +/** Mark a field as an external reference (e.g. an opaque foreign-system id). */ +export const external = (args: { system: string; ref?: string }) => lineageAnnotation( - ref !== undefined ? { _tag: 'External', system, ref } : { _tag: 'External', system }, + args.ref !== undefined + ? { _tag: 'External', system: args.system, ref: args.ref } + : { _tag: 'External', system: args.system }, ) +/** Mark a field as computed at read time (not persisted). */ export const computed = (opts?: { fn?: string; description?: string }) => lineageAnnotation({ _tag: 'Computed', ...opts }) -export const authority = (a: Authority) => annotate(AuthorityAnnotationId, a) -export const freshness = (f: Freshness) => annotate(FreshnessAnnotationId, f) -export const foreignKey = (targetSchema: string, targetField?: string) => - annotate( - ReferenceAnnotationId, - targetField !== undefined - ? { _tag: 'ForeignKey', targetSchema, targetField } - : { _tag: 'ForeignKey', targetSchema }, - ) +/** Attach an `Authority` annotation describing readers/writers. */ +export const authority = (a: Authority) => annotate({ id: AuthorityAnnotationId, value: a }) +/** Attach a `Freshness` annotation describing temporal capture semantics. */ +export const freshness = (f: Freshness) => annotate({ id: FreshnessAnnotationId, value: f }) +/** Attach a `ForeignKey` reference annotation pointing at another schema. */ +export const foreignKey = (args: { targetSchema: string; targetField?: string }) => + annotate({ + id: ReferenceAnnotationId, + value: + args.targetField !== undefined + ? { _tag: 'ForeignKey', targetSchema: args.targetSchema, targetField: args.targetField } + : { _tag: 'ForeignKey', targetSchema: args.targetSchema }, + }) /* -------------------------------------------------------------------------- * Display-ready bundle @@ -317,69 +351,86 @@ const derivationToString = (how: DerivationKind): string => { } } +type Detail = { label: string; value: string } + +const withDetails = (args: { + base: Omit + details: ReadonlyArray +}): LineageDisplay => + args.details.length > 0 ? { ...args.base, details: args.details } : args.base + +/** Build a pre-rendered display bundle for a `Lineage` value. */ export const getLineageDisplay = (lineage: Lineage): LineageDisplay => { switch (lineage._tag) { case 'SourceOfTruth': { - const parts: { label: string; value: string }[] = [] + const parts: Detail[] = [] if (lineage.owner !== undefined) parts.push({ label: 'owner', value: lineage.owner }) if (lineage.system !== undefined) parts.push({ label: 'system', value: lineage.system }) - return { - badge: '⇆', - badgeTitle: 'Source of truth', - kindLabel: 'Source of truth', - summary: - lineage.system !== undefined ? `Owned by ${lineage.system}` : 'Authoritative value', - details: parts.length > 0 ? parts : undefined, - } + return withDetails({ + base: { + badge: '⇆', + badgeTitle: 'Source of truth', + kindLabel: 'Source of truth', + summary: + lineage.system !== undefined ? `Owned by ${lineage.system}` : 'Authoritative value', + }, + details: parts, + }) } case 'Derived': { const fromList = lineage.from.map(refToString).join(', ') const how = derivationToString(lineage.how) - return { - badge: 'ƒ', - badgeTitle: `Derived from ${fromList}`, - kindLabel: 'Derived', - summary: `${how} of ${fromList}`, - details: lineage.pure === true ? [{ label: 'pure', value: 'true' }] : undefined, - } + return withDetails({ + base: { + badge: 'ƒ', + badgeTitle: `Derived from ${fromList}`, + kindLabel: 'Derived', + summary: `${how} of ${fromList}`, + }, + details: lineage.pure === true ? [{ label: 'pure', value: 'true' }] : [], + }) } case 'Projection': { const of = refToString(lineage.of) - return { - badge: '≈', - badgeTitle: `Projection of ${of}`, - kindLabel: 'Projection', - summary: `Projection of ${of}`, + return withDetails({ + base: { + badge: '≈', + badgeTitle: `Projection of ${of}`, + kindLabel: 'Projection', + summary: `Projection of ${of}`, + }, details: lineage.stalenessMs !== undefined ? [{ label: 'staleness', value: `${lineage.stalenessMs}ms` }] - : undefined, - } + : [], + }) } case 'Cache': { const of = refToString(lineage.of) - return { - badge: '☷', - badgeTitle: `Cache of ${of}`, - kindLabel: 'Cache', - summary: `Cached value of ${of}`, - details: - lineage.ttlMs !== undefined ? [{ label: 'ttl', value: `${lineage.ttlMs}ms` }] : undefined, - } + return withDetails({ + base: { + badge: '☷', + badgeTitle: `Cache of ${of}`, + kindLabel: 'Cache', + summary: `Cached value of ${of}`, + }, + details: lineage.ttlMs !== undefined ? [{ label: 'ttl', value: `${lineage.ttlMs}ms` }] : [], + }) } case 'Mirror': { const of = refToString(lineage.of) - return { - badge: '↻', - badgeTitle: `Mirror of ${of}`, - kindLabel: 'Mirror', - summary: - lineage.system !== undefined - ? `Mirror of ${of} from ${lineage.system}` - : `Mirror of ${of}`, - details: - lineage.system !== undefined ? [{ label: 'system', value: lineage.system }] : undefined, - } + return withDetails({ + base: { + badge: '↻', + badgeTitle: `Mirror of ${of}`, + kindLabel: 'Mirror', + summary: + lineage.system !== undefined + ? `Mirror of ${of} from ${lineage.system}` + : `Mirror of ${of}`, + }, + details: lineage.system !== undefined ? [{ label: 'system', value: lineage.system }] : [], + }) } case 'External': { return { @@ -394,13 +445,15 @@ export const getLineageDisplay = (lineage: Lineage): LineageDisplay => { } } case 'Computed': { - return { - badge: '⊙', - badgeTitle: 'Computed (not persisted)', - kindLabel: 'Computed', - summary: lineage.description ?? lineage.fn ?? 'Computed at read time', - details: lineage.fn !== undefined ? [{ label: 'fn', value: lineage.fn }] : undefined, - } + return withDetails({ + base: { + badge: '⊙', + badgeTitle: 'Computed (not persisted)', + kindLabel: 'Computed', + summary: lineage.description ?? lineage.fn ?? 'Computed at read time', + }, + details: lineage.fn !== undefined ? [{ label: 'fn', value: lineage.fn }] : [], + }) } } } diff --git a/packages/@overeng/react-inspector/src/schema/lineage.test.ts b/packages/@overeng/utils/src/isomorphic/lineage/mod.unit.test.ts similarity index 85% rename from packages/@overeng/react-inspector/src/schema/lineage.test.ts rename to packages/@overeng/utils/src/isomorphic/lineage/mod.unit.test.ts index 5d30e8234..09b3a1919 100644 --- a/packages/@overeng/react-inspector/src/schema/lineage.test.ts +++ b/packages/@overeng/utils/src/isomorphic/lineage/mod.unit.test.ts @@ -18,7 +18,7 @@ import { mirror, projection, sourceOfTruth, -} from './lineage.ts' +} from './mod.ts' describe('Lineage annotations: round-trip', () => { it('sourceOfTruth', () => { @@ -27,7 +27,7 @@ describe('Lineage annotations: round-trip', () => { }) it('derivedFrom (bare field names + default Pure)', () => { - const s = Schema.Number.pipe(derivedFrom(['subtotal', 'tax'])) + const s = Schema.Number.pipe(derivedFrom({ from: ['subtotal', 'tax'] })) expect(getLineage(s)).toEqual({ _tag: 'Derived', from: [ @@ -40,11 +40,11 @@ describe('Lineage annotations: round-trip', () => { it('derivedFrom (explicit DerivationKind + pure flag)', () => { const s = Schema.Number.pipe( - derivedFrom( - [{ _tag: 'Field', path: '$.items' }], - { _tag: 'Aggregation', op: 'sum' }, - { pure: true }, - ), + derivedFrom({ + from: [{ _tag: 'Field', path: '$.items' }], + how: { _tag: 'Aggregation', op: 'sum' }, + pure: true, + }), ) const got = getLineage(s) expect(got).toEqual({ @@ -56,22 +56,22 @@ describe('Lineage annotations: round-trip', () => { }) it('projection / cache / mirror / external / computed', () => { - expect(getLineage(Schema.Number.pipe(projection('total', { stalenessMs: 5000 })))).toEqual({ + expect(getLineage(Schema.Number.pipe(projection({ of: 'total', stalenessMs: 5000 })))).toEqual({ _tag: 'Projection', of: { _tag: 'Field', path: '$.total' }, stalenessMs: 5000, }) - expect(getLineage(Schema.Number.pipe(cache('total', { ttlMs: 1000 })))).toEqual({ + expect(getLineage(Schema.Number.pipe(cache({ of: 'total', ttlMs: 1000 })))).toEqual({ _tag: 'Cache', of: { _tag: 'Field', path: '$.total' }, ttlMs: 1000, }) - expect(getLineage(Schema.String.pipe(mirror('id', { system: 'stripe' })))).toEqual({ + expect(getLineage(Schema.String.pipe(mirror({ of: 'id', system: 'stripe' })))).toEqual({ _tag: 'Mirror', of: { _tag: 'Field', path: '$.id' }, system: 'stripe', }) - expect(getLineage(Schema.String.pipe(external('stripe', 'cus_123')))).toEqual({ + expect(getLineage(Schema.String.pipe(external({ system: 'stripe', ref: 'cus_123' })))).toEqual({ _tag: 'External', system: 'stripe', ref: 'cus_123', @@ -89,7 +89,7 @@ describe('Lineage annotations: round-trip', () => { const f = Schema.Number.pipe(freshness({ capturedAt: 'event-time', maxAgeMs: 60000 })) expect(getFreshness(f)).toEqual({ capturedAt: 'event-time', maxAgeMs: 60000 }) - const r = Schema.String.pipe(foreignKey('Order', 'id')) + const r = Schema.String.pipe(foreignKey({ targetSchema: 'Order', targetField: 'id' })) expect(getReference(r)).toEqual({ _tag: 'ForeignKey', targetSchema: 'Order', diff --git a/packages/@overeng/utils/src/isomorphic/mod.ts b/packages/@overeng/utils/src/isomorphic/mod.ts index 515be4d37..821ec17c4 100644 --- a/packages/@overeng/utils/src/isomorphic/mod.ts +++ b/packages/@overeng/utils/src/isomorphic/mod.ts @@ -10,6 +10,9 @@ export { /** In-memory backing for distributed semaphore (useful for tests) */ export * as InMemoryBacking from './in-memory-backing.ts' +/** Schema annotations for tracking data lineage (authority, freshness, references) */ +export * as Lineage from './lineage/mod.ts' + /** Debug utilities for tracing scope and finalizer lifecycle */ export * from './ScopeDebugger.ts' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfcbcad99..1e0b8c038 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1092,6 +1092,30 @@ importers: packages/@overeng/react-inspector: dependencies: + '@effect/cluster': + specifier: ^0.58.2 + version: 0.58.2(f335cd339cca8128b299febedcdd4641) + '@effect/experimental': + specifier: ^0.60.0 + version: 0.60.0(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(effect@3.21.2)(ioredis@5.6.1) + '@effect/opentelemetry': + specifier: ^0.63.0 + version: 0.63.0(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@3.21.2) + '@effect/platform': + specifier: ^0.96.1 + version: 0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2) + '@effect/platform-node': + specifier: ^0.106.0 + version: 0.106.0(@effect/cluster@0.58.2(f335cd339cca8128b299febedcdd4641))(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(@effect/rpc@0.75.1(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(effect@3.21.2))(@effect/sql@0.51.1(@effect/experimental@0.60.0(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(effect@3.21.2)(ioredis@5.6.1))(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(effect@3.21.2))(effect@3.21.2) + '@effect/rpc': + specifier: ^0.75.1 + version: 0.75.1(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(effect@3.21.2) + '@effect/workflow': + specifier: ^0.18.0 + version: 0.18.0(@effect/experimental@0.60.0(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(effect@3.21.2)(ioredis@5.6.1))(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(@effect/rpc@0.75.1(@effect/platform@0.96.1(patch_hash=08d6466db56675b7a32a3a3c64815a5b784f583b310b6758471a97d3db6edd32)(effect@3.21.2))(effect@3.21.2))(effect@3.21.2) + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 is-dom: specifier: 1.1.0 version: 1.1.0