Skip to content

Refactor text system: TextType/TextEntry replaces SpecialText - #1294

Merged
anobaka merged 46 commits into
mainfrom
claude/local-file-organization-arch-rlzmkr
Sep 2, 2026
Merged

Refactor text system: TextType/TextEntry replaces SpecialText#1294
anobaka merged 46 commits into
mainfrom
claude/local-file-organization-arch-rlzmkr

Conversation

@anobaka

@anobaka anobaka commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

This PR refactors the text management system by replacing the legacy SpecialText model with a new TextType/TextEntry architecture. The change introduces a modular text processing framework with support for text operations (capture, template, trim, remove), vocabulary management, and integration with the workflow system.

Key Changes

Core Architecture

  • Removed: SpecialText model and ISpecialTextService interface
  • Added: New TextType and TextEntry models in Bakabase.Modules.Text
  • Added: ITextOps interface for text operation abstractions (capture, template, trim, remove-wrapped, remove-texts)
  • Added: ITextVocabularyService for managing text vocabularies and entries
  • Added: TextVocabularyService implementation with CRUD operations and indexing

Workflow Integration

  • Added: Text operation activities (TextCaptureActivity, TextTemplateActivity, TextTrimActivity, TextRemoveWrappedActivity, TextRemoveTextsActivity)
  • Added: File system activities (FsFileNameOpActivity, FsSaveNameActivity, FsExpandChildrenActivity)
  • Added: Workflow triggers (FsWatchTrigger, FsManualScanTrigger, FsScheduledScanTrigger)
  • Added: WorkflowFsWatchService for file system monitoring
  • Added: WorkflowSchedulerTask for scheduled workflow execution
  • Enhanced: IWorkflowActivity interface with new properties for activity metadata and cardinality support
  • Enhanced: WorkflowRunner with improved variable interpolation and execution context

File Rename Management

  • Added: FileRenameEntry model for tracking file rename operations
  • Added: FileRenameEntryService for managing rename entries
  • Added: FileNameSanitizer utility for safe file naming
  • Added: Database migrations for TextType, TextEntry, and FileRenameEntry tables

UI Components

  • Added: Canvas-based workflow editor (CanvasEditor, CanvasNode, InspectorPanel, NodePalette)
  • Added: Text operation UI components (Capture, Template, Trim, RemoveWrapped, RemoveTexts)
  • Added: File system operation UI components (FsFileNameOp, FsSaveName)
  • Added: Workflow trigger UI components (FsWatch, FsManualScan, FsScheduledScan)
  • Added: Help center documentation for workflow concepts and examples
  • Removed: Legacy ActivityCard and ActivityPicker components
  • Removed: Legacy WorkflowEditor component

Documentation

  • Added: Comprehensive design documentation in docs/local-file-organization/:
    • foundations.md - Core concepts and type system
    • modules/01-text-processing.md - Text processing rules
    • modules/02-extractors.md - Entry extraction
    • modules/03-providers.md - Metadata providers
    • modules/04-resolution.md - Identity resolution
    • modules/05-enrichment.md - Field enrichment
    • modules/06-layout.md - Path templates and organization rules
    • modules/07-execution.md - Execution and rollback
    • modules/08-orchestration.md - Pipeline orchestration
    • modules/09-collections.md - Collection aggregation
  • Added: Visual guides (workflow-capability-map.html, workflow-editor-redesign.html, file-cleaning-workflow.html, specialtext-extension.html)

Testing

  • Added: Comprehensive test suites:
    • TextOpsTests - Text operation functionality
    • TextVocabularyServiceTests - Vocabulary service operations
    • WorkflowAutomationTests - Workflow automation scenarios
    • WorkflowVariablesTests - Variable interpolation
    • WorkflowFsApplyUndoTests - File system

https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H

…sting modules

Revise the uploaded MediaOrganizer standalone design into a
Bakabase-integrated architecture: reuse ThirdParty clients as metadata
providers, Enhancer conversion chain for property writes,
ResourceSourceLink for identity persistence, BTask hosting, and
Comparison/Alias/SpecialText for matching. Adds a concept glossary,
a user-defined organize-rule system with a path template DSL,
StandardValue-backed type-safe domain fields, franchise/IP grouping for
mixed libraries, plan/execute/rollback with resource-consistency
coupling, and an M0-M4 roadmap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…ta-driven pipelines

Per-layer components (signal extractors, providers, resolver policies)
become user-selectable; pipelines are DB-stored configurations with
read-only built-in presets that users can fork (copy-on-write). The
Domain concept shrinks to a field vocabulary plus a routing/display
label. Clarifies the Workflow module relationship: reuse its
registry/descriptor/config-form patterns but not its runner; Workflow
orchestrates around the organizer (triggers/activities) while the
organizer keeps its own slot-based state machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
Slot-by-slot comparison of the 8 built-in pipelines (signal chain,
provider chain, resolver chain, field vocabulary), the thin detector
slot, the four axes that actually differentiate them, and the
component-reuse payoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…ver chain semantics

Adds 5.2: signal.pattern engine with SignalRuleSet (typed named-group
bindings) so users can define extraction rules; built-in plain-text
signal components become seeded rule sets. Rewrites 6 into a two-phase
matching model: search phase as a resolver chain of policies
(Final/Augment/Pass, FirstHit vs FanOutUnion strategies, worked
examples) and an enrichment phase with parallel EnrichAsync, identity
sets, per-field merge policies and provenance. Extends the provider
adapter contract accordingly and updates glossary, roadmap, and open
questions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
Replace the monolithic HTML with docs/local-file-organization/: an
overview (execution plan + progress tracker), a foundations doc
(concepts, component model, type system, reuse map), and nine
implementation modules ordered bottom-up from atomic capabilities
(text rules, extractors, providers) through decision layers
(resolution, enrichment) and disk-level capabilities (layout,
execution) to top-level orchestration (pipelines, collections).

Docs cross-reference by links instead of copying content; v1
comparison material is dropped. Renames the Signal concept to
TextRule/Extractor/Clue (state Signaled -> Extracted) and adds a
'capabilities gained on completion' section with examples to every
module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…3-phase runner

Drops the Certainty/Generate/Transform node-role taxonomy and the
merge-as-chain-node design. Resolution now has a single component
concept (ICandidateGenerator, with CandidateBasis expressing certainty
per candidate) and a kernel runner with three fixed phases: generate
(ordered, early-exit on deterministic candidates), consolidate
(deterministic key-join always on, heuristic join configurable,
conflict flagging, thin-field semantics), and decide (basis-aware
banding). Documents the canonical MatchCandidate contract and syncs
the pipeline examples in orchestration, enrichment, foundations and
the overview.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…ailure-mode defenses

MatchCandidate loses hardcoded Year/WorkType: canonical members are only
what any work has (identity, titles, links) plus runner bookkeeping;
all domain data moves into the typed FieldBag populated per provider
field declarations. Heuristic consolidation and fuzzy scoring reference
features via FeaturePredicate config reusing Comparison-module
strategies, so the kernel contains no domain field names. Adds concrete
Consolidate/Decide implementations, a failure-mode analysis with
defenses (conservative defaults, visibility flags, IsSameWork /
NotSameWork override assertions, code-lookup title cross-validation),
and syncs provider/orchestration docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…, title role

Restructures the consolidation section around the three-question
separation (equivalence vs display value vs authoritative data), an
exhaustive logic inventory showing the consolidator does not grow with
domains, and pure-function/non-destructive replay semantics. Identity
assertions (IsSameWork/NotSameWork) become a consolidator input with
highest priority, the heuristic title gate compares alias-expanded
normalized title sets, and a dedicated section documents title's role
in both join paths including the granularity-mismatch signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…rent focus

Marks docs/local-file-organization as pending (resolution-layer
semantics not yet converged) and adds the file-cleaning workflow
design: a user-defined chain built directly on the existing Workflow
module (manual trigger carrying constants + child enumeration, filter
activities, text-transform activities wrapping FileNameModifier
operations and SpecialText-driven wrapped-text removal, and a
save-name action with preview/apply/undo via FileRenameRecord).
Extends SpecialText with user-defined custom types referenced as text
sets by cleaning nodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…xpand/variables mechanisms

Adds a 12-node worked example (HTML) covering multi-level entries and
cross-node linkage: the parent folder's cleaned title is captured as a
variable, inherited by expanded child file items, and combined with a
per-file episode capture in a template rename. Promotes the mechanisms
the example requires into the design doc: ExpandTo outcome with
expand.fs.children, item variables with capture/template activities and
{var} interpolation, common scope/requiredVars node options with
skip-not-drop semantics, and DeepestFirst apply ordering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…O contracts

SpecialText extension upgraded to an elegant id-referenced design:
CustomTextType table + nullable CustomTypeId on SpecialText + a
discriminated TextSetRef (builtin:{enum} / custom:{id}) so renames
never break node references; seeded editable sets replace hardcoded
vocabularies; marked as implementation step one (the feature's only
schema change). The example doc gains per-node IN/OUT/cardinality/
variable-contract strips and a section on typing and conflict
isolation: kind namespacing, AcceptedInputItemTypes hard validation,
variable supply/demand as editor lint, and generic-node interop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…bility/binding spec

The transform nodes read/write item.fs.entry's WorkingName and carry
fs-specific options, so their kinds move from transform.text.* to
transform.fs.* to match their declared I/O type. Generic text
processing lives in a stateless TextOps service (the actual reusable
component); workflow activities are bindings of that capability onto a
concrete item type. Records the facet-based generalization path
(AcceptedItemFacet validated via item type descriptors' ClrType) for
when a second text-bearing item type appears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
… facet

Accepts the review challenge that transform.fs.trim/capture/removeWrapped
cannot answer 'which property does this operate on': the fs namespace has
several string properties, so the systematic answer is a facet — items
declare a single working text (FsEntryItem.WorkingText == WorkingName)
and text nodes accept any item implementing ITextWorkpiece, validated by
the editor via item type descriptors' ClrType. Text nodes return to the
transform.text.* namespace with zero fs awareness; fs semantics reach
them only through injected system variables (extension, parentName).
Drops the scope option (mixed flows are expressed by chain ordering:
directory segment -> save dir plan -> expand ChildrenOnly -> file
segment), keeps fileNameOp deliberately fs-bound, and restructures the
example to the new 13-node shape with facet contracts per node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…ssthrough

Clarifies how facet-typed text nodes chain with exact-typed fs nodes:
the facet check is a capability predicate on the chain's current type,
which never changes through facet nodes (mandatory Passthrough, no
Fixed output), so item.fs.entry flows intact from capture into
saveName. Runtime preservation is guaranteed by WithWorkingText
returning the implementer's concrete type; capture only writes
variables and keeps the item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…fs.manualScan

Surveys the Workflow module as a whole: current engine capabilities
(typed chain validation, AdaptToNext, runner/rehydrator, runs), the
actual node inventory (2 triggers, 5 item types, 5 activities - one
subscription/downloader business line), six cross-cutting engine
extensions (manual trigger with payload, ExpandTo, facet contracts,
variables, two-phase runs, scheduled triggers) each with its first
consumer, and the node roadmap by domain. Renames the cleaning trigger
to fs.manualScan since manual filesystem scanning is not
cleaning-specific.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…visions

Converts file-cleaning-workflow and workflow-capability-map from
markdown to styled HTML matching the example doc, cross-linked; the
markdown sources are removed to keep a single source of truth. Applies
the three confirmed revisions: cardinality becomes a static activity
descriptor declaration for ExpandTo (editor filtering/marking/stats),
the facet term is replaced by capability contract
(AcceptedItemInterface) throughout, and variables move off item types
into an engine-level per-item sidecar (ctx.Variables) with inheritance,
persistence, interpolation and editor autocomplete/lint - simplifying
ITextWorkpiece to WorkingText + WithWorkingText.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
Full design for the text-system rework, grounded in a code survey
(~15 consumer files, four call patterns): custom text types become
first-class (Custom=100 enum member, CustomTextType table with stable
ids, CustomTypeId column, discriminated TextSetRef), and the mixed
ISpecialTextService retires in favor of a vocabulary/operations split -
ITextVocabularyService (storage, custom types, set resolution, seeds)
and ITextOps (stateless clean/removeWrapped/trim/date-parse/interpolate,
unit-testable without a DB) - housed in a new Bakabase.Modules.Text.
Includes a member-by-member migration map, per-consumer migration
table, idempotent seed ledger semantics, API/frontend changes, and a
three-PR implementation split. Cross-linked from the cleaning design
and the capability map's implementation order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
…e abstraction

Replaces the enum-plus-custom-table split (and its builtin:/custom:
discriminated TextSetRef) with one entity: TextType rows carry an
optional WellKnown handle marking built-ins (undeletable, unrenamable,
semantics bound at code consumption sites) and a Values/Pairs shape,
with entries in TextEntry. References collapse to a single int typeId;
GetTypes() serves pickers and the management page from one aggregated
list, and custom Pairs types become usable as wrapper pairs directly.
Existing SpecialText rows migrate losslessly via an app-level migrator
(V220/V230 precedent) keeping EF migrations pure-schema; the SpecialText
naming, enum, service and table retire entirely. Cleaning design and
example synced to typeId references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
Audited every claim in the design against current main:

- Shape must be three-state, not two. Language also stores a pair
  (regex -> language label), and delimiter pairs (Wrapper) are
  semantically distinct from replacement mappings
  (Standardization/Language) - removeWrapped's wrappers parameter can
  only accept the former. The frontend already renders exactly these
  three shapes (Single/Wrapper/Value1ToValue2).
- Drop the old table drop. App migrators run after EF schema
  migrations and hand-editing generated migrations is forbidden, so
  removing the model would let EF drop the table before the data
  migrator could read it - losing user rows on version-skipping
  upgrades. SpecialText's model and table now stay as a dormant relic.
- Add the hidden ICustomDateTimeParser contract: SpecialTextService
  implements it and is wired via AddStandardValue, so the new TextOps
  must too or date parsing silently degrades.
- Correct the blast radius: 12 real call sites, 43 files referencing
  the interface (25 downloaders pass it through), 167 mentioning the
  name.
- Add four missed frontend couplings plus an API breakage (the display
  name editor indexes the response by enum value), the pretreatment
  endpoint, and the migrator's real version-threshold gating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
First step of the text-system rework: builtin and user-defined text
types will share one table and one id space, so anything referencing a
type deals with a single integer. TextType carries an optional
WellKnown handle (non-null marks a builtin, whose semantics are bound
at consumption sites) and a Shape describing how entries use their two
value slots — Values, DelimiterPair or MappingPair, since delimiter
pairs and replacement mappings are not interchangeable. TextEntry
keeps Value2 nullable for every shape so historical rows never lose
data.

Schema only: the existing SpecialText service and table keep running
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
Retires ISpecialTextService, whose single surface mixed storage CRUD,
text operations and seeding, in favour of two services living in the new
Bakabase.Modules.Text:

- ITextVocabularyService owns types and entries. Builtin types are rows
  carrying a WellKnown handle, so GetTypes() returns one list for every
  picker; ResolveSet is the only way consumers read words, and it
  provisions a missing builtin on demand so a fresh database never
  throws where the old GetAll simply returned nothing.
- ITextOps carries the operations, stateless and vocabulary-driven, and
  also serves as ICustomDateTimeParser — that binding is what feeds the
  user's date formats into StandardValue conversions, so it is covered by
  a test rather than left to compile-time luck.

All 43 files touching the old interface move over: 12 real call sites
map one-to-one onto the new methods, the rest is constructor plumbing
through the downloader chain. Adds TextSystemMigrator, which copies
legacy rows into the new tables after seeding builtins; it preserves
Value2 even for shapes that ignore it (Volume ordinals) and skips
entries already present, so a rerun after a crash cannot duplicate.
The legacy SpecialText table and model deliberately stay in place —
app migrators run after EF migrations, so dropping the table would
destroy the source before the copy on a version-skipping upgrade.

Tests split into TextVocabularyServiceTests and TextOpsTests, the
latter needing no database for anything but the vocabulary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
The text page now renders one list of types straight from GetTypes,
where builtins carry a badge and lose their rename/delete actions while
user-defined types gain them. Entry editing keys off the type's shape
rather than a fixed enum, so a custom mapping type gets the same two
inputs as Standardization does.

Follows the type through its remaining frontend couplings: the
display-name template editor no longer indexes the response by an enum
value (it finds the wrapper type by its well-known handle), and the
enhancer target label and its StandardValue map move to
WellKnownTextType. Localization keys move from SpecialTextType.* to
WellKnownTextType.* and gain the shape and type-management strings.

SDK regenerated: the specialText endpoints are gone, replaced by the
text ones, and constants.ts picks up the two new enums.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
Three bugs were reported against the new text management page, all
rooted in how builtin type rows came into existence.

The page showed 2 types instead of 7 because TextSystemMigrator never
ran: its gate was "2.4.0-beta", which sits below the installed
2.4.0-beta.82, and the gate asks whether the installed version is older.
Raising it to 2.4.0-beta.99 fixes that instance, but the page should not
depend on a one-shot migrator at all, so EnsureBuiltinTypes now runs from
BakabaseHost.ExecuteCustomProgress on every startup — post-migration,
before anything serves a request. Builtin types are defined in code, so
their rows are an invariant, not seed data.

"Add preset data" returned 500 because ResolveSet(WellKnownTextType)
provisioned a missing builtin on read. Consumers share one scoped
DbContext and several resolve concurrently, so two of them racing produced
"A second operation was started on this context instance". Reads are now
pure: a missing builtin resolves to an empty set, exactly as the old
type-filtered query returned an empty list. Databases written by the
lazily-provisioning build may already hold duplicate rows, so startup
collapses them onto the oldest row and moves the entries across. That
repair is why the new indexes are not unique — schema migrations run
before it could clean up after them, so a unique index would fail
startup on precisely the databases that need repairing.

Seeding splits accordingly: EnsureBuiltinTypes creates type rows only,
AddPrefabEntries (POST text/prefabs, renamed from text/seeds) tops up
entries and stays user-triggered, so entries the user deleted are not
resurrected on every launch.

The three shapes were named for the storage model rather than the user;
they are now "Single text" / "Bracket pair" / "Replacement" with a hint
line each, shown both in the type picker and under custom types.
Long server errors also overflowed the toast instead of wrapping it.

Finishes the migration off ISpecialTextService, which was left half
done. Bakabase.Abstractions no longer carries the unused domain model or
SpecialTextType — the enum was still being emitted into the frontend's
generated constants.ts as dead API. The db model moves to
Bakabase.InsideWorld.Business as LegacySpecialText with a raw int type
column; the table stays mapped because app migrators run after EF
migrations, so dropping it would destroy the source before the copy runs
on a version-skipping upgrade. Remaining "special text" naming is gone
from the downloader chain, the dashboard stat and the frontend.

Closes #1253
Running the pretreatment test with nothing entered round-trips to the
server only to render an unchanged empty string. Trimmed, so whitespace
alone does not enable it either.
Marks the extension complete across the three linked designs and adds an
"implementation deviations" table to its own doc: seeding split into
EnsureBuiltinTypes / AddPrefabEntries, reads made pure, non-unique
indexes with a startup merge, the legacy model demoted out of
abstractions, and the custom seed set deferred.

The capability map's ordering is under review and may change; what it
says today is the ordering as designed, not a decision.
The event bus writes a run's PayloadJson and the runner reads it back, so
the two ends have to agree on serializer settings; they did, by separate
copies happening to match. WorkflowJson.Options makes that agreement
structural, and gives the manual-run path something to share rather than
adding a fourth copy.

No behaviour change — the settings are the ones all the copies already
used.
Runs could only be created by an event. This adds the other entry point:
POST workflow/{id}/run, deliberately skipping both gates the event path
applies — the trigger filter (the user named this definition rather than
broadcasting an event) and the enabled flag (a workflow is most worth
trying by hand while it is switched off and being built).

Where the payload comes from is the trigger's business. The contract
gains BuildManualPayload(triggerFilterJson, argsJson) with a default that
reads it from what the user typed, so every event trigger becomes
replayable without writing code for it, and RequiresManualPayload so the
UI knows whether to ask. A trigger whose inputs live on the definition
overrides both — the shape fs.manualScan will take. The payload is built
before the run row is written, so a bad one is a failed request instead
of a run that dies the moment it starts.

The descriptor now carries the payload's field list, reflected the same
way item types already are, so the manual-run editor opens on a real
skeleton rather than an empty box.

Replaces RunNowAsync, which took an already-built payload and had no
callers.
The E1-E6 codenames were opaque outside the one table that defined them,
so the map now opens with a lookup that gives each a name and a
one-liner, and every later reference leads with the name.

Records what a line-by-line read of the engine turned up, each with its
effect on scheduling:
- the engine had never executed a run — an injected provider was the
  resolving scope's, and every caller hands the runner to a BTask that
  outlives it. The two existing business lines had never worked.
- type inference exists twice, in the service and as a hand-written
  mirror in the editor; the capability contract has to enter both.
- two-phase running needs a second migration, so "the only schema
  change" in the cleaning design was wrong.
- expansion is cheaper than estimated: step stats already carry
  independent in/out counts, which retires an open question.

Ordering is now two candidates rather than one decision: the original
by-engine-capability sequence, and a thin-vertical-slice one that pulls
two-phase running forward on the grounds that a cleaning flow without
apply is a toy. Four open questions are listed as open rather than
answered.
The map jumped straight into a status inventory, so a reader had to
reverse-engineer what the pieces are and how they connect. A new first
section lays out the engine's concepts in the order they are used —
what the user builds (definition, trigger config, activity chain), what
code registers (trigger, item type, activity), how a run executes, and
how the editor validates types along the chain — with each member marked
as existing or as belonging to one of the six extensions.

It closes with the mapping the design has needed all along: each
one-line requirement of file cleaning against the concept it actually
uses. That surfaces the point the prose never stated outright — only
four things in cleaning require touching the engine, and everything else
registers into slots that already exist, which is what lets a first
vertical slice run without the capability contract or variables.

Sections shift by one and the cross-references in the cleaning design
follow; anchors verified in both directions.
…ip ci]

The owner has positioned Workflow as the app's bottom-most automation
layer: arbitrary internal events, user-composed chains, arbitrary
actions. The map now states that positioning up front, with the mapping
onto the three registry slots that makes per-event/per-action cost one
class plus one hook.

Backs it with a verified audit (three parallel code sweeps plus an
independent critic that re-opened every citation; all engine claims
held):

- Five second-round findings on what app-wide generality requires of
  the engine: config parse failures silently become defaults ahead of
  destructive steps; one scope and DbContext span a whole run; runtime
  items are unchecked objects and the AI transform deserializes LLM
  text straight into typed input for side effects; events are published
  inline with no log and unmatched ones vanish; definition mutation has
  no in-flight guards, so disable and delete do not stop what is
  already queued or running.
- A verified menu of ~25 candidate trigger surfaces (with four leverage
  points where one bridge covers a family) and ~25 candidate actions,
  each with the service method it would wrap and how item/config supply
  its inputs.
- Open question 5: which hardening items gate destructive actions and
  when they land.
Ordering is thin vertical slices; the rename preview is one dual-mode
component shared by the confirm panel and the run detail; the plan is
one table whose rows live through pending/excluded/applied/undone; type
inference converges on backend-shipped contract metadata before the
capability contract lands; and the hardening hard-prerequisites become
acceptance criteria of the apply batch rather than side work.
The first vertical slice of file cleaning (docs/file-cleaning-workflow.html,
plan B batch 1): pick directories, chain name operations, and see the rename
plan — with the disk untouched by construction.

fs.manualScan is the first definition-configured trigger: no event fires it
and a manual run needs nothing typed — BuildManualPayload builds the payload
from the definition's own filter (roots, files/directories/both, depth,
extension filter) and validates it in the request, so a missing root fails
the click instead of producing a doomed run. Emits item.fs.entry
(FsEntryItem), whose invariant carries the whole feature: transforms touch
only WorkingName, and comparing it to OriginalName is how saveName knows
what changed.

transform.fs.fileNameOp wraps the existing FileNameModifier rule engine;
its config IS the file-name-modifier page's operation model, and the editor
reuses that page's OperationCard, so one rule engine has one config shape
and one editor everywhere.

action.fs.saveName is preview-only: each changed name becomes a
FileRenameEntry row — plan, apply progress and undo record are one row
moving through a status (owner decision 3), created now so batch 2 only
adds transitions. Non-optional defenses per the design: Windows-rules
sanitizing on every platform, duplicate-target detection within the plan
and against the disk, path-length precheck; a conflict becomes a Conflict
row rather than aborting the other 199. Writes go through the activity's
own child scope, per the engine rule that the run-wide DbContext belongs
to the runner.

One endpoint serves the plan to both faces of RenamePlanPanel (read-only
in this batch) — diff-colored rows via the FileNameModifier DiffHighlight,
per-status counts, conflicts-only filter — reachable from fs runs in the
runs drawer.

Verified end to end against the live service: scan 3 entries, plan 2
Pending renames, Keep.txt planless, subdirectory untouched, disk unchanged.
Suite at 611 passed / 0 failed including 7 new tests covering the chain
validation, conflicts, sanitizing and payload rejection.
The feature landed in 21aa800; this carries the closing reference
instead of rewriting a pushed commit.

Closes #1283
…rdening

Apply & undo:
- FileRenameEntryService: ApplyRun executes Pending rows deepest-first with
  per-row persistence (the row is the crash-consistency record); UndoRun
  replays Applied rows shallowest-first; SetExcluded toggles Pending/Excluded;
  DeleteByRunIds cleans up after definition deletion.
- WorkflowController: excluded toggle, apply and undo endpoints; Delete now
  removes the runs' rename entries too.
- RenamePlanPanel grows its interactive face (same component, two faces):
  exclude checkboxes, apply with confirm + count, undo, toast summaries.
  The runs drawer opens it interactive for succeeded runs only.

Engine hardening (findings f5/f7/f9 + definition guards):
- Malformed activity ConfigJson now throws WorkflowActivityConfigException and
  fails the run even under the Skip policy, instead of silently running the
  step on defaults.
- The runner checks each item's CLR type against the step's accepted item
  types and refuses a broken chain at runtime.
- The runner only executes Pending runs, so a stale BTask can't rerun a
  cancelled or finished run.
- Disabling a definition cancels its Pending runs; deleting one refuses while
  a run is executing.
- IsDestructive on activities + validation that a destructive step may not
  directly consume model-generated (AdaptToNext) input; exposed on the
  activity descriptor for the editor.

Tested: 22 workflow tests incl. 8 new apply/undo/hardening tests; full suite
618 passed / 0 failed; live smoke against the running service (apply with an
excluded row, disk verified, undo restores everything, re-include works).
…ence convergence

Capability contracts (E3):
- IWorkflowActivity.AcceptedItemInterface: an activity can accept any item whose
  CLR type implements a contract interface, instead of growing a per-type tag
  list. Contracts derive from the IWorkflowItemContract marker; a
  contract-accepting activity must be Passthrough (validated on save) and the
  runner checks the live instance against the contract at run time.
- ITextWorkpiece: the text family's contract — one working text, replaced via
  WithWorkingText. FsEntryItem implements it (WorkingText ≡ WorkingName), so
  every text node applies to filesystem entries, and to any future
  text-bearing item type for free.

Text nodes (design §3.4), all vocabulary-driven via ITextOps:
- transform.text.removeWrapped — strip wrapped segments whose content matches
  a text set (wrappers must be a DelimiterPair type)
- transform.text.removeTexts — strip bare occurrences of a set's entries
- transform.text.trim — clean removal leftovers (whitespace/empty wrappers)
A dangling or wrong-shaped text-type reference becomes
WorkflowActivityConfigException: config staleness fails the run even under
the Skip policy.

Type-inference convergence (owner decision 4, B1):
- Descriptors now ship the facts: acceptedItemInterface per activity,
  implementsInterfaces per item type (contract interfaces only).
- The frontend chain walk and picker classification became generic set
  operations over that metadata — the accept rule evolves by changing backend
  metadata only, never frontend code.

Frontend: config forms + summaries for the three nodes (shared TextType
picker filtered by shape, match-mode select), "text" picker group, en/cn
locales; docs status advanced (batches 1–3 done in the capability map).

Tested: 5 new tests (end-to-end clean chain over disk, save-time rejection on
non-text item types, config-staleness run failure, descriptor metadata,
workpiece round-trip); 27 workflow tests green; full suite 623/0; live smoke:
'[SubTeam] Movie SPONSOR (1080p).mkv' → planned 'Movie .mkv' via HTTP.
…ion (E2)

Variable bag (E4):
- Each item now travels with a chain-local bag of named string values, exposed
  to activities as ctx.Variables. The bag rides BESIDE the item — item CLR
  shapes never grow fields for chain state. On expansion each child inherits a
  COPY of its parent's bag, so a season captured at the directory level reaches
  every episode file without siblings bleeding into each other.
- transform.text.capture: named regex groups over the working text write the
  bag (group name = variable name); onMiss chooses between continuing without
  variables and failing the item.
- transform.text.template: rebuilds the working text from {var:name},
  {var:name:pad(n)} and {originalText}; requiredVars is the missing-must-fail
  gate. Domain system variables (IHasWorkflowSystemVariables) resolve after
  the bag — FsEntryItem answers extension/originalName/parentName from its
  original disk state.
- WorkflowVariableInterpolator is the single owner of the token grammar;
  the editor mirrors it for autocomplete-style hints and soft linting.

Chain expansion (E2):
- WorkflowItemOutcome.ExpandTo(children) + Cardinality on activities; the
  runner refuses expansion from a OneToOne activity as a declaration bug.
- transform.fs.expandChildren: a directory becomes its direct children
  (target/extension filter, optional IncludeSelf); files pass through, so
  mixed streams survive. Step stats' independent in/out counts show the 1→N.

Frontend: capture/template/expandChildren forms; config forms now receive the
upstream chain so template shows clickable "available variables" (captures +
fs system vars) and warns on tokens nothing provides.

Deviation noted in the capability map: bags are not persisted with the run —
nothing reads historical variables yet.

Tested: 5 new tests incl. the flagship dir-capture → expand → file-capture →
template scenario (two shows prove sibling-bag isolation) and the cardinality
guard; full suite 628/0; live smoke via HTTP: 1 directory → 2 files planned as
'Breaking Bad - S01E01.mkv' with the funnel showing 1→2.
…an + directory watch

Scheduling:
- IWorkflowScheduledTrigger: a trigger can also fire on the clock; the
  interval is per-definition data read from its own TriggerFilterJson, so one
  trigger kind serves every cadence.
- WorkflowScheduler (predefined BTask, minutely): sweeps enabled definitions
  with a scheduled trigger, starts due ones through the exact manual-run
  path (identical validation and payload), never stacks a run behind a
  Pending/Running one, and records a failed start on the definition's
  LastError instead of killing the sweep.
- fs.scheduledScan: the manual scan plus intervalMinutes (0 = manual-only).

Directory watch (the FileMover-intake shape):
- fs.watch trigger + WorkflowFsWatchService: FileSystemWatcher per watched
  root; an entry fires only after staying quiet for the definition's settle
  period, so files still being copied are never touched. Paths are
  pre-filtered per unique TriggerFilterJson and the payload carries that
  filter verbatim — Matches is a string comparison, definitions sharing an
  identical filter share one event, and no definition sees entries filtered
  for someone else's configuration. Renewed activity re-arms an entry.
- The service exposes its refresh/note/tick pipeline publicly and takes the
  clock as a parameter, so tests drive it deterministically without timers.

Frontend: trigger UIs for both kinds (scheduled scan reuses the manual scan
form verbatim and appends the interval; watch gets roots/target/extensions/
settle), a shared FsRootsPicker extracted so the three fs forms cannot drift,
runs-drawer rename-plan button now covers all fs trigger kinds; en/cn
locales; docs mark E2–E6 and batches 1–5 complete.

Tested: 6 new tests (scheduler due/skip/no-stack semantics, interval parsing,
watch settle/dedupe/re-arm, per-definition extension filtering, end-to-end
watch → run → plan); live smoke: a dropped file fired a watch run after its
settle period, and the scheduler started the due definition on its minutely
sweep.
A full workflow topic alongside the path-mark guide, matching its structure:

- What Is It: the chain mental model (trigger → filter/transform/action),
  a minimal chain diagram next to what a run produces, the four part
  kinds with the editor's own colors, a three-step how-to, and the
  two-phase (plan → confirm → undo) safety callout.
- Examples: seven complete recipes ordered from first-use to full
  automation — basic name cleaning with preview, vocabulary-driven
  de-noising, cross-level renaming with variables + expansion, watched
  folder auto-cleaning, nightly scheduled cleaning, subscription →
  keyword filter → download queue, and download-completion notification.
  Every recipe is drawn with a shared ChainDiagram component whose
  category colors mirror the real editor.
- Concepts: ten glossary entries (trigger, activity, item type,
  type-checked chain, variables, plan/apply/undo, scheduled & watch
  triggers, manual run, error policy, run history).

The workflow page header gets the "?" entry point opening this topic.
All text ships in en + cn (158 keys each).
…skip ci]

Design doc for replacing the modal workflow editor with a full-screen
three-zone canvas editor:

- §0 a working prototype embedded in the page itself: drag nodes from a
  grouped palette into the chain with magnetic insertion slots (the chain
  parts and the slot glows within the snap radius), drag chain nodes to
  reorder or off to a remove zone, click a node to configure it in a
  fixed right-hand inspector, and type-incompatible nodes dimmed with the
  reason. Click-vs-drag uses a 6px threshold so plain clicks stay clicks.
- §1 diagnosis of the current modal editor's five pain points, each tied
  to the concrete file it comes from.
- §2–§3 the three-zone layout and the exact interaction contract
  (90px snap radius, let-way animation, keyboard fallback).
- §4 code mapping: chainWalk/activityFit/registries and every
  ConfigForm/FilterForm are reused verbatim; only the shell, the card and
  the picker are rewritten; dnd-kit stays, react-flow deliberately not
  introduced (the model is linear).
- §5 a three-batch implementation plan and the open decisions.

Prototype interactions verified end-to-end in Chromium (palette drop,
magnetic slot, select, dim, remove) with zero console errors.
Replaces the modal editor with the three-zone canvas editor designed in
docs/workflow-editor-redesign.html (owner decisions: route page, direct
replacement, entry points included):

- /workflows/editor route: node palette (grouped, searchable, incompatible
  nodes dimmed with the reason) · horizontal chain canvas (trigger node,
  typed edge pills, per-node config summaries and status dots) · fixed
  right-hand inspector hosting the exact same FilterForm/ConfigForm
  components, so the chain never jumps while configuring.
- Magnetic drag (useChainDrag): a 6px click-vs-drag threshold, slots that
  snap open with a glow inside a 90px radius, validity-gated magnetism for
  palette drops (invalid positions simply never open; single-accept nodes
  still enter via the auto-inserted AI bridge), drag-off-to-remove, and a
  click/keyboard path (Enter/arrows/Delete) so dragging is never the only
  way. Ghost follows the pointer imperatively; only slot/remove state goes
  through React.
- Entry points (file-cleaning design §6, previously missing): a "new from
  template: file cleaning" button on the workflow page, and an "upgrade to
  a cleaning workflow" button on the File Name Modifier page that carries
  the current operation set into a prefilled fileNameOp node via a one-shot
  sessionStorage seed.
- Old modal editor (WorkflowEditor/ActivityCard/ActivityPicker) removed;
  chainWalk/activityFit/registries and every form component are reused
  verbatim. ItemTypePill gains a horizontal variant for canvas edges.
- Fixes surfaced by the live drive: the item.fs.entry type had no localized
  display name (edge pills showed the raw i18n key).

Verified end-to-end in Chromium against the running app: template creation,
palette drag with slot glow, click-to-append, inspector editing enabling
save, save round-trip (create → ?id= URL → run/history controls), list
integration, trigger-locked note on edit, drag-to-remove with empty-state
hint, and the File Name Modifier hand-off; tsc at 342 (two below the prior
baseline — deleting the old editor removed its errors).
Ending a drag nulled session.current BEFORE cleanupGhost() read the ghost
back through that same ref, so the cloned card was never removed — every
completed or cancelled drag left a frozen copy at the drop position, which
read as "the node got stuck". The session now ends through one endSession
path that removes the ghost from the captured session object, and the
teardown also covers pointercancel (native drag / touch scroll stealing the
pointer) and unmount mid-drag; ghosts carry data-drag-ghost so tests can
assert none remain.

Verified in the running app: drop on a slot (inserted, 0 ghosts), drop in
empty canvas (cancelled, 0 ghosts), four rapid cancelled drags (no
accumulation), and a chain-node move — all clean, no console errors.
Owner feedback on the canvas editor: the chain hugged the top-left and the
canvas could neither pan nor zoom.

- The chain now renders on a transformed world layer inside an
  overflow-hidden viewport. On open, fit-view centers it vertically and
  horizontally; fit never shrinks below 75% so node labels stay readable —
  a chain wider than the view anchors to its start and pans instead.
- Pan: drag empty canvas space (interactive elements excluded, 3px
  threshold so clicks pass through), or plain wheel / trackpad two-finger.
- Zoom: ctrl/⌘ + wheel (= trackpad pinch) zooms to the cursor, 35%–200%;
  a corner toolbar adds −/+, a percent button that resets to 100%, and a
  fit button. The dot grid follows the view via background size/position.
- Pan and zoom write through a ref and apply styles imperatively — a
  pointermove stream must not re-render the editor; only the percent label
  goes through state. Magnetic slot hit-testing already works in screen
  coordinates, so dragging is unchanged at any zoom; the drag ghost now
  matches the source's on-screen size instead of its layout size.
- The remove zone is pinned to the canvas (absolute) rather than the old
  scroll container.

Verified in the running app: exact vertical centering on open, drag-pan and
wheel-pan, cursor-anchored zoom with toolbar sync, fit/reset buttons,
palette drop onto a glowing slot while zoomed out, node click still
selecting, zero leftover ghosts, no console errors.
Make the entire block surface the drag source and drop the top-right
drag-handle button, guarded by a 6px click-vs-drag threshold so plain
clicks and jitter stay inert. Rework drag/resize into pointer sessions
with a single exit path — pointerup commits, pointercancel and unmount
abort back to the committed layout — replacing the permanently
registered window listeners that were re-subscribed on every preview
render and read state through closures. Previews are deduped by grid
placement so pointermove no longer re-settles and re-renders while the
pointer stays in the same cell.

Closes #1293

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
Split enum imports that are only used in type positions into `import
type` (consistent-type-imports errors in the text-ops shared form and
the canvas inspector panel), and add the `common.action.rename` key the
text page uses to both locales, which the locale key-resolution test
flagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XD61DUNaB5iVEevJrA26H
@anobaka
anobaka merged commit 0c753ec into main Sep 2, 2026
2 checks passed
@anobaka
anobaka deleted the claude/local-file-organization-arch-rlzmkr branch September 2, 2026 14:38
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.

2 participants