Skip to content

refactor(types): single-source-of-truth contract types (WS1)#498

Merged
markhayden merged 15 commits into
mainfrom
refactor/contract-types
Jun 13, 2026
Merged

refactor(types): single-source-of-truth contract types (WS1)#498
markhayden merged 15 commits into
mainfrom
refactor/contract-types

Conversation

@markhayden

Copy link
Copy Markdown
Owner

Summary

WS1 of the audit refactor (.claude/specs/audit-2026-06/REPORT.md). Eliminates the plugin-contract type duplication that the audit found drifting across 2–6 layers, establishing the SDK as the single source of truth, then splits the oversized SDK types file. 14 commits, one revertable checkpoint each; tests + typecheck green on every commit.

The crux decision (resolved by hard constraint)

The audit report was internally contradictory on source-of-truth direction. It's settled by the SDK types module's documented self-containment + the publish guard (assertNoForbiddenImports bans @bakin/core//src/ in SDK output): the SDK is the canonical home; packages/core/src/plugin-types.ts re-exports from it. The "core is home for Task" direction is physically impossible for anything plugins reach through ctx.

Phase A — unify (kills the drift)

  • A0 delete 7 verified-dead files (~620 LOC)
  • A1–A4 single-home the genuinely-identical leaf contracts in the SDK, core re-exports: health-check family, ExecToolResult, the 14-type search cluster (reconciled SearchMaintenanceAPI + richer health snapshot UP into the SDK), manifest contract (dropped core's stale PluginManifest)
  • A5 — correction to the audit: PluginContext/BakinPlugin are not duplicates — core and the SDK are an intentional two-tier contract (core's ctx.runtime is the full AgentRuntimeAdapter that 6 plugins depend on for ~15 methods; the SDK exposes a curated subset). Documented the split in both type files; single-homed only the truly-identical primitives (EventBus/ActivityAPI/PluginLogger).
  • A6 TaskLogEntry single-homed (6 identical copies → 1); internal storage Task left distinct from the SDK's published projection (version/updatedAt are internal)
  • A7 AvailableModel reconciled to the accurate wire shape; A8 fixed the SDK WorkflowInstance (idinstanceId); A9 AgentUsage single-homed + dropped a plugin-dir-escaping import; A10 stripped the Next.js-era src/types residue (15 → 3 live types)

Phase B — split

  • B1 split the 1,524-line sdk/types/index.ts into 6 focused modules (primitives/manifest/runtime/services/registration/context) behind the same barrel; dropped 8 more dead misc types. Exported surface identical minus the dead types (125 → 117).
  • B2 moot — unification shrank core/plugin-types.ts 1,129 → 738 lines, under the split threshold.

Verification

  • bun run test (4,996 pass / 0 fail) + bun run typecheck green on every commit
  • bun run lint, full bun run build (3 binaries), build:vendors green
  • SDK self-containment verified (no forbidden imports in any types module); export-name set diffed before/after
  • Boot smoke in isolated BAKIN_HOME: 200, all 10 plugins load
  • Docs updated (repo-architecture.md, plugin-system.md); two-tier rationale recorded

Refs the audit report; next workstream is WS2 (core-extractions / adapter-boundary).

🤖 Generated with Claude Code

markhayden and others added 15 commits June 12, 2026 23:10
WS1 unifies the plugin contract types that exist as 2-6 drifting copies,
then splits the two type god-files. Crux decision resolved by hard
constraint: the SDK types module is documented self-contained and the
publish guard bans @bakin/core refs in its output, so the SDK is the
canonical home and core re-exports — the report's 'core is home for Task'
direction is impossible for types plugins reach through ctx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unreachable from any import surface (verified at HEAD by basename stem +
every named export across src/packages/plugins/cli/scripts/tests/dev/
docs/.github): new-task-dialog (live new-task UI is TaskDetailDrawer;
the docs screenshot captures by route, not this file), curated-browser,
calendar-view + its sole-consumer parser parsers/calendar, plugin-slot
(the SDK barrel explicitly does NOT re-export it), cli/ui/panel, and the
host skeleton-loader. Audit finding: arch-coupling-deadcode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HealthCheckResult, the HealthRepair* family, and PluginHealthCheckInput
were declared verbatim in both packages/sdk/src/types and
packages/core/src/plugin-types. The SDK (the plugin-author surface) is
now the sole declaration; core re-exports it. Core's richer doc comments
were ported up to the canonical SDK declaration. Core-internal
HealthCheckDef stays in core, now extending the SDK type. Audit finding:
sdk-gaps (hand-maintained SDK/core type fork).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ExecToolResult was declared identically in core and the SDK. Core now
re-exports it from @makinbakin/sdk/types. PluginToolContext and
ExecToolDefinition reference the imported type but stay in core for now —
they carry the task-service/runtime-adapter surface, which is unified
together with PluginContext in a later commit. Audit finding: sdk-gaps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 14-type search cluster (SearchSchemaField, SearchIndexDefinition,
SearchContentTypeDefinition, SearchQueryParams, SearchResult,
SearchResponse, SearchHealth{Index,Table,Snapshot}, SearchTransformOp,
FilePatternMapper, FileBackedContentTypeDefinition, SearchAPI,
SearchMaintenanceAPI) was duplicated across core and the SDK. Two real
drifts where core was the accurate superset, now reconciled UP into the
canonical SDK declaration: the SDK lacked SearchMaintenanceAPI +
SearchAPI.maintenance (used by the memory plugin via ctx.search) and its
SearchHealthSnapshot inlined a narrower per-table shape without
indexHealth/SearchHealthIndex. Core now re-exports the cluster. Audit
finding: sdk-gaps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nifest contract

Core's PluginManifest had drifted stale — missing runtimeCapabilities,
contributes, and devWatch — while core's own manifest parser already
imports the current PluginManifest from @makinbakin/sdk/types. So the
@bakin/core barrel served one shape and the parser used another. Core now
re-exports PluginManifest, PluginManifestSignature, SecretDeclaration,
PluginEntry, and BakinConfig from the SDK; the stale declarations are
deleted. Consumers gain the missing optional fields. Audit findings:
sdk-gaps, arch-coupling-deadcode (stale duplicate PluginManifest).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o-tier split

The audit's "single-home PluginContext/BakinPlugin" recommendation was
unsafe: core and the SDK are an intentional two-tier contract, not a
fork. core's PluginContext.runtime is the FULL AgentRuntimeAdapter that 6
core plugins depend on (memory.*, agents.writeWorkspaceFile,
config.replace, images.*, cron.getRaw — ~15 full-only methods); the SDK
exposes a curated subset. ctx.tasks returns PluginTask (core) vs Task
(SDK); BakinPlugin/StorageAdapter/NavItem/APIRoute/HookAPI/SkillDefinition
are intentionally fuller in core. Collapsing the boundary is WS2 work.

Both type files now carry a header documenting the two-tier design so the
divergence isn't "fixed" later. The genuinely-identical zero-dependency
leaf primitives EventBus, ActivityAPI, and PluginLogger are single-homed
in the SDK and re-exported. Audit finding: sdk-gaps (with a correction to
its blanket dedup recommendation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TaskLogEntry was byte-identical in 6 places. It's part of the published
TaskService contract (appendLog) and identical everywhere, so the SDK is
the canonical home; packages/core/src/tasks/store.ts re-exports it,
src/core/task-store.ts re-exports through @bakin/core/tasks/store
(preserving the facade's single import source), and plugin-types +
plugins/tasks/types import from it.

Task itself is NOT merged: the SDK's published Task (column: ColumnId,
string timestamps) and the internal storage Task/BakinTask (numeric
updatedAt + version optimistic-concurrency counter) are intentionally
different projections per the two-tier split — version is an internal
detail external plugins must not see. The internal Task/BakinTask
duplication is a separate core-internal concern, out of scope for the
SDK contract unification. Audit finding: arch-coupling-deadcode
(TaskLogEntry copy-pasted across 4-6 layers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AvailableModel was declared twice and drifted: the models plugin's copy
had name/tier/provider required + the curated-catalog enrichment fields;
the SDK's had them optional + an unused `source?`. The models plugin
builds the /available payload and its cache zod schema requires
name/tier/provider, so the SDK is reconciled UP to that accurate wire
shape (required fields, enrichment docs, `source?` dropped — read
nowhere). The models plugin now re-exports from the SDK; the team plugin
(already importing from the SDK) gets the accurate non-optional fields.
AvailableModelsResponse stays plugin-local. Audit finding: sdk-gaps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nceId)

The SDK's published WorkflowInstance keyed on `id`, but the real wire
shape the workflows plugin emits uses `instanceId` — a consumer reading
`.id` off the published type would get undefined. Corrected to the
accurate permissive view (instanceId + the commonly-read optional fields,
index signature retained); the full typed shape with stepStates/history
stays internal to the workflows plugin. Migrating the tasks plugin's
hand-rolled workflow types + raw fetches onto this is WS3 (sdk-gaps)
consumer work. Audit finding: sdk-gaps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ir-escaping import

AgentUsage was declared in src/core/agent-usage.ts, copied verbatim into
the health plugin's health-page.tsx, and imported by the team plugin's
overview-tab via a relative path that escaped the plugin directory into
src/core (the only client file in any plugin reaching outside its own
tree). It's now a type-only export from the SDK (the usage-panel wire
shape); src/core/agent-usage.ts re-exports it for server callers, and
both client components import it from @makinbakin/sdk/types. The verbatim
copy and the escaping import are gone. Audit finding: sdk-gaps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e types

src/types/index.ts carried 15 exports; only Heartbeat, ActivityEvent, and
ContentState are imported (by the SSE content store, use-sse, agent-status,
map-audit-message, and the host activity route). The other 12 — a 4th stale
Task model plus TaskLogEntry/TaskColumns/TaskBoard/ColumnId and the
Calendar*/Memory*/ProjectMeta/OfficeData leftovers — had zero importers and
are deleted. Audit finding: arch-coupling-deadcode (Next.js-era residue).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 1,524-line SDK types file is split into six cohesive modules behind
the same barrel: primitives, manifest, runtime, services, registration,
context. index.ts stays the @makinbakin/sdk/types entrypoint
(scripts/build-sdk-package.ts SDK_EXPORTS + the vendor bundle) and
re-exports the full surface via `export *`. Cross-module references use
type-only sibling imports; no module reaches @bakin/core or repo source,
so the published self-containment holds. The 8 dead misc types
(Calendar*, Memory*, ProjectMeta, the SDK's orphaned Heartbeat and
WorkflowStep — zero importers) are dropped in the move. Exported surface
is otherwise identical (125 → 117 = the 8 deletions); pure types, zero
runtime. Verified: typecheck, full suite, build:vendors, name-set diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…contract

repo-architecture.md and plugin-system.md described @makinbakin/sdk/types
as "full type re-exports"; it's now the canonical, self-contained source
of truth (split into focused modules behind a barrel) that core
re-exports the identical leaf types from, while core keeps its fuller
internal tier. Added the two-tier rationale so the intentional
divergence isn't "fixed" later. Closes the WS1 doc sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@markhayden markhayden merged commit fbd2b33 into main Jun 13, 2026
1 check passed
@markhayden markhayden deleted the refactor/contract-types branch June 13, 2026 16:57
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.

1 participant