Refactor: code organisation, one operation per file, a folder per context #7
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/structure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Code organisation: small files, one operation each, a folder per context.
docs/code-organisation.mdholds the conventions (one operation per file named after its export; a context is a folder with anindex.tsfor the server and aschemas.tsfor what crosses RPC; regions; sizes). CLAUDE.md carries the short form.max-lines(150 for.ts, 250 for.vue),max-lines-per-function(60), and ano-restricted-importsrule per context that forbids reaching inside another context. The files that were over the limits are ineslint-suppressions.json, ESLint's baseline: a listed file may not get worse, and a fixed one must be dropped. It went from 71 files to 34, all legacy code that its own phase rewrites.packages/coreby context (domain/,events/,errors/,commands/,queries/,ports/); the workflow interpreter split intoworkflow/run/.packages/contractsby context withviews/.server/kanbotby context plusplatform/(runtime, RPC mount, live updates with one projections file per context); the workflow service, node kinds, plugin host and runtime split by operation.shared/types.ts,shared/workflows.ts,shared/filters.tsandshared/workflowPresets.tsbecome folders by topic with barrels, so their importers are unchanged. App composables grouped by context underapp/composables/<context>/.A move, not a rewrite: 390 tests pass with only import paths changed; the characterisation suite's expectations are untouched. Lint, typecheck, Storybook and the Nuxt build pass.
Part of the DDD/CQRS refactor; the PRs are stacked, each based on the one before, so merge them in order: Phase 0 → U0 → Phase 1 → Phase 2 → Phase 3 → Phase 4 → structure → plugin classes → UI pages.
CI: the Forgejo job has not run on these branches (every run was cancelled before a runner picked it up; no runner is registered for the repo). The same steps (
npm ci, lint, typecheck, tests, Storybook build) pass innode:24-bookwormlocally.🤖 Generated with Claude Code
U1 of the UI track. Atoms and molecules get what they used to load as props, and organisms reach the server only through composables, so scripts/atomic-baseline.json is [] and any violation now fails lint. Atoms and molecules: - StatusBadge takes the column's `label` (the task page passes useBoardSettings().label). - AppViewSwitch takes the current `path` from app.vue. - TimerChip takes `timer` and emits `stop`; app.vue owns useTimer(). The chip ticks its own clock so only it redraws every second. - MetadataEditor takes `fields`. The new TaskMetadataEditor organism (no markup of its own) feeds it from useTaskFields(), so the fields are still fetched when the editor appears. - LauncherPicker takes an optional `pickApp` (useLauncherCommands()); presets-only mode never needs it. - TaskCommentBox takes `submit` (useTaskActions().comment) instead of `taskId`. Composables, still wrapping REST for routes not on RPC yet, with the same useFetch keys as before: - useTasks.ts: useTaskList ('tasks'), useTaskFields ('task-fields'), useTaskActions (answer, rework, plan, comment, attachments). - useProjects.ts: useProjectList ('projects'), useProjectCommands (pick folder, inspect, create, update, file settings), useMemoryCommands. - useWorkflows.ts: useWorkflowList ('workflows'). - useAppSettings.ts: useAppSettings ('app-settings'). - useLaunchers.ts: useLauncherAvailability ('launchers'), useLauncherCommands. - useSavedViews.ts: useSavedViewCommands. useGuidedTour.ts: useTourSettings ('settings-tour'). Every changed atom and molecule has a story. Storybook gains a NuxtLink stand-in and VueUse's useNow, as the app has them. The atoms test now passes StatusBadge its label instead of mocking useBoardSettings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>R1 of the code-organisation track (docs/code-organisation.md). A move, not a rewrite: packages/core/src/{board,tracking,notifications,workflow} each become a folder with domain/ (entities, value objects, pure rules under domain/rules/), events/, errors/, commands/, queries/ and ports/, one file per operation named after its export, and an index.ts that is the context's only public face. The kernel is one concept per file too (Commits, Commands, Dispatcher, EventMeta, StoredEvent, newEvent, EventStore, UnitOfWork, StoreError, Signal, EphemeralBus). workflow/Run.ts was over the limits, with two functions over 60 lines. It is workflow/run/ now: the Schemas (Wake, Entry, NodeResult, Ending, TaskRun), the services (RunHost, NodeKinds) and the interpreter split into focused functions - Walk holds the run's local state and the Activities it does to the world, enter picks the first node, visit walks one node, exhausted and runAction handle a stage out of runs and an action's run-or-park, afterWake and stepTo as before. The Activities are made in exactly the same order as before, so a run parked under the old interpreter replays the same names. Every importer (server, tests, contracts, infra-sqlite) imports a context from its index, which drops their no-restricted-imports suppressions; the eslint baseline shrinks from 71 files to 41. The stylistic spaced-comment rule learns the `//#region` marker the docs prescribe. packages/core/src/board/Task.ts stays as a shim for the one app import of TaskStatus until the app side moves (another branch). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>R4 of the code organisation. The domain composables move into one folder per context, the RPC clients into api/<context>/, each file as it was: - board/: useTasks, useTaskCommands, useBoardSettings, useSavedViews, useRunnerStatus. workflow/: useWorkflowCommands, useWorkflows. projects/: useProjects, useLaunchers. tracking/: useTracking. notifications/: useNotifications. settings/: useAppSettings, useSettingsSearch. time/: useTimer. - ui/ (the app shell): useAppPanels, useTheme, useLocale, useGlobalShortcuts, useRouteAction, useGuidedTour, useFormat. - platform/: useLiveUpdates, useRpcQuery. - api/platform/{client,liveUpdates}, api/board/tasks, api/workflow/workflow, api/tracking/tracking. Nuxt scans only the top level of composables/, so nuxt.config.ts lists the context folders in imports.dirs (never api/, whose clients load lazily), and .storybook/main.ts scans the same list. The lazy import('./api/…') calls and the explicit ~/composables/… imports in components, pages and a story follow the files. scripts/check-atomic.ts already walked the folders; it now also recognises '../api/<context>/…' as reaching the server. app/utils/themes.ts stays in the baseline: it is the theme registry (25 entries of data) and three one-line helpers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>R2 of the code-organisation track (docs/code-organisation.md). A move, not a rewrite. packages/contracts/src/<context>/ holds one RpcGroup per file (board/Tasks, tracking/{Connections,Sources,Sync}, plugins/Plugins, workflow/TaskWorkflow, platform/LiveUpdates), its DTO Schemas under views/, and an index per context; Id.ts and Errors.ts stay at the root because they span contexts, and src/index.ts keeps KanbotRpcs. Tasks.ts and LiveUpdates.ts remain as shims for the app's imports until the app side moves; Tracking.ts and Plugins.ts could not (next to tracking/ and plugins/ a case-insensitive disk resolves `./tracking` to the shim), so those five app imports move with the app. server/kanbot/<context>/ holds each context's rpc/<Group>Handlers.ts, adapters/<Port>Legacy.ts, consumers/<Name>.ts and services: board/, tracking/ (TrackerSync and its loop apart), notifications/, workflow/ and plugins/. server/kanbot/platform/ is the runtime (layers.ts, consumers.ts, lifecycle.ts behind runtime/index.ts), the RPC mount, live updates (subscribe.ts, project.ts, projections/<context>.ts composing PROJECTIONS), current.ts, edge.ts, EphemeralBusLive.ts and legacyEvents.ts. Split to size: workflow/service.ts by operation under workflow/service/ (fire, wake, lifecycle, adoption, gates, sessions, sendBack, catchUp...), whose index keeps the names today's callers use, so server/agent/engine.ts and the tests' vi.mock of the service work as before; workflow/nodes.ts into one file per node kind plus BuiltInNodeKinds; engine.ts into DurableEngine, MemoryEngine and TaskRuns; pluginHost.ts into loadEntries, dependencyOrder, pluginContext (the network permission), activation, contributionProblem, pluginStatus, trackersOf and PluginHost; the Redmine plugin into types, messages, connection, client, mapping and the provider, still behind makeRedmine. scripts/gen-plugins.ts writes server/kanbot/plugins/bundled.generated.ts now. Every importer follows (server, tests, scripts, CLAUDE.md paths); the eslint baseline shrinks to 38 files, none of them in the moved areas. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>The board's list, a task's page and the custom fields in use are read through `TaskQueries.{list,detail,fields}` (packages/contracts/src/board), answered by the Board's new read-side port `TaskReads` (over today's repository in TaskReadsLegacy) and the `getTaskDetail` query, which names a missing task as TaskNotFound. The views in contracts/src/board/views mirror shared/types field for field (TaskView, TaskListItemView, TaskDetailView and the parts of a detail: events, questions, attachments, reviews, time entries, memory proposals, the plan report and the workflow state as stored, with the fields older rows lack optional). The app keeps consuming the shared types: the api module hands the decoded views out under those names. A task's project and workflow cross as declared JSON (EmbeddedProject, EmbeddedWorkflow) until their own contexts' Schemas land. useTaskList, useTaskFields and useTaskDetail are useRpcQuery queries, one per file, under the keys and refresh conditions the pages had ('tasks' on task.*, project.* and runner.changed; `task-<id>` on the task's messages, runner.changed and launchers.changed); the board refreshes only the projects' counts itself now. useTaskActions gets planAnswer; useReviewActions and useTimeEntryCommands hold what the task page still posted by hand. /api/tasks/fields is gone. /api/tasks/[id] stays for the Electron shell but answers through the query, so it leaves LEGACY_API_ROUTES (14 entries). test/rpc/taskQueries.test.ts checks each view equals the repository's output. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>S1 of the class-based plugin SDK (plan: Interlude R/S, S). A plugin is now a class extending KanbotPlugin with a @Plugin(meta) decorator; what it contributes are classes too, each with the decorator of its kind: a tracker extends Tracker (Effect) or PromiseTracker (async/await, a thrown TrackerError is the failure) and carries @TrackerType with its type, label, icon and connection form Schemas. Legacy decorators (experimentalDecorators), no reflect-metadata: metadata.ts keeps what each decorator declared in a WeakMap by class (readPlugin, readContribution), dependencies arrive through constructors (the PluginContext), and manifestOf() derives the manifest. definePlugin goes. The tracker Schemas move one per file under tracker/. The host asks kinds, not kinds of thing: ContributionKind (kinds/) knows its base class and decorator, validates a class against the manifest and its permissions, claims and registers instances into its core port and withdraws them; TrackerKind serves the Trackers port from its registry. The host takes { manifest, plugin } sources: it decodes kanbot-plugin.json as before (permissions readable without running anything) and, before anything runs, checks the class says the same (noMetadata, undecorated, unknownKind, needsPermission, undeclared, notProvided, manifestStale), so a build that lost the decorators fails loudly. Activation builds the plugin, runs onActivate in its scope, then each contribution through its kind; a kind's refusal (trackerTaken) or a throw marks only that plugin failed. gen-plugins bundles each plugin's entry with esbuild (plugins/tsconfig.json), reads the decorators and writes kanbot-plugin.json (contributes as { kind, type, … } entries, locales listed) along with the catalogues and bundled.generated.ts; --check fails when stale. esbuild becomes a declared devDependency (the version already installed). Toolchain (S0): nitro.typescript.tsConfig and nitro.esbuild.options.tsconfigRaw both set experimentalDecorators, plus packages/tsconfig.json and plugins/tsconfig.json for Vite and vitest. test/plugins/decorators.test.ts reads every decorated member of the Redmine plugin and probes a method decorator, so a toolchain regression shows. Both decorators also throw when called in the TC39 shape. Redmine is RedminePlugin + RedmineTracker over the split client, mapping, connection and messages files. trackerContract takes the tracker class and the plugin class; the host tests use made-up plugins written as classes, including a PromiseTracker. The lint baseline loses trackerContract.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>packages/core/src/execution holds what a session is told and allowed: the AgentSession and Question Schemas, the Session*/Question*/Plan/Review/SentBack events, the pure rules (readOnlyViolation, planFirstViolation, mutatingGitCommand, classifyCommand, decideToolUse), Kanbot's tools as AgentTools over a ToolHost port, and the prompts as pure functions with {{tool:name}} tokens. Its ports — AgentRuntimes, SessionStore, QuestionStore, Workspace, TaskBoard, LiveSessions, WorkflowGate — are served by the adapters in server/kanbot/execution and by the new agent_sessions table (migration 0010, dual-written with the task's session id and metrics). server/kanbot/execution runs the sessions: ExecutionService forks each task, review and plan session into a FiberMap keyed by task, builds the SessionSpec (tools bound with JSON Schema, the gate over the rules and approvals, ask over the question row), turns the runtime's events into the same timeline lines the runner wrote, and tells the workflow as before. Claude Code runs behind the AgentRuntimes port as a legacy adapter over the SDK until the claude-code plugin serves it. server/agent/runner.ts, plan.ts and prompt.ts are shims over the service, so every caller and mock keeps working; the characterisation suite's expectations are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>Both tracks defined the AgentRuntimes port the same way; the Execution context now sits above the plugin host in the layer stack and takes the runtimes from RuntimeKind, so the interim adapter and its private Claude runtime go. Tests put a scripted runtime in front through startKanbot({ runtimes }), a layer that wraps the plugins' AgentRuntimes (test/support/scriptedRuntime.ts); without an override the sessions run on the claude-code plugin over the fake SDK, which the deliverAnswer characterisation test relies on. The plugin list also names a plugin's agent runtimes, and labels the two new permissions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>A setting had to be added in four places (the defaults and the per-field merge in getSettings, the zod schema and setSetting in PUT /api/settings, the app's defaultSettings) or it silently dropped. Now each section is a Schema in the Settings context (packages/core/src/settings), its defaults in SETTINGS_DEFAULTS beside it (plain data, so the app shows them without loading Effect), and reading is decoding the stored rows with it (readSettings): a field or section never stored reads back as its default, a row that does not decode as its section's default. The legacy fixes are decode steps: groomingModel becomes planningModel, this machine's launchers and the KANBOT_* numbers apply only where nothing was stored, and the columns workflows provide are added by the server's shim. getSettings, getSettingsRow, publicSettings and setSetting stay as that synchronous shim, so the legacy readers and the characterisation tests are unchanged; the old merge is kept in test/settings as the reference every stored shape must still read back as. Changing settings is the ChangeSettings command over the new Settings RPC group (get, update a patch of whole sections): checked by the Schemas, text trimmed and the board's own columns kept as PUT did, and one SettingsChanged (LocaleChanged for the language) per section that really changed. They are projected to the messages screens already handle (board.changed, launchers.changed, runner.changed, sync.changed, locale.changed) plus settings.changed, which the app's single settings query listens for; a consumer restarts the standup timer and lets the scheduler start what a higher limit allows. PUT /api/settings goes; GET stays for Electron. The app reads one shared copy (useAppSettings on useRpcQuery) and writes through useSettingsCommands, whose answer replaces that copy, so the refreshNuxtData('app-settings') workaround and defaultSettings() go. The tour finishes through the same command. Discord is a plugin now: its section, types and form leave the settings (the Integrations section goes; the tour and the menu point at Plugins). At boot, before the plugins start, a stored `discord` row becomes the Discord plugin's settings and secret (and an off bot an off plugin), then the row is dropped, so it happens once. The legacy bot reads the plugin's config until its plugin replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>