TIMEWARP SKILL — feature-cohesive folder placement, per-use-case folders, and the filename grammar <name>[-<function>]-<layer>.cs for product code under <family>/features/ and platform clusters under <family>/platform/ (family-generic: web, api, grpc all share this machinery — worked examples below use web): which layer a file belongs to, what to name it and which folder it goes in, the registry that backs TWA0015/TWA0016, and the membership-guard build errors. Invoke before creating, moving, or renaming a file under a feature slice or platform cluster, or when a TWA0015/TWA0016/membership-guard error appears. WHEN: Where does this handler file go?, What do I name this contract file?, Should this file get its own folder?, TWA0015, TWA0016, feature file matches no registered layer suffix, platform/postgres, add a function segment to the registry, split a module into its own assembly.

Maintained in timewarp-architecture · Canonical file: SKILL.md

Install

npx skills add TimeWarpEngineering/timewarp-architecture --skill tw-feature-placement

Or copy the SKILL.md into your agent's skills directory.


Feature placement and filename grammar

All logic lives in a concern folder under a shared tree — features/ for product concerns, platform/ for platform concerns — named by the filename grammar; artifact folders hold only the artifact definition (csproj, global-usings) and its entry-point bootstrap (program.cs, appsettings, launchSettings, host-config exemplars). The machinery is family-generic — web, api, and grpc each get their own features//platform//msbuild/ trees from the same SSOT registry (yarp excepted — single-project family, no concern trees). Worked examples below use web; substitute api//grpc/ for the family root and the same rules apply.

The litmus test for the fuzzy middle:

If this deployable were deleted, would the file still mean something? Yes → shared tree (which concern folder?). No → it is bootstrap; it stays with the artifact.

Home Use for Namespace Examples
web/features/<slice>/ Product concerns: an operation gets its own <slice>/<use-case>/ folder; a file serving more than one operation (shared contract, store) stays at slice root …Features.<Id> (TWA0009) admin/roles/create-role/, chat/chat-hub-server.cs
web/platform/<cluster>/ Platform concerns: a host/platform cluster split across layers — including a seam interface living beside the implementation it seams with, not sorted into a separate layer folder Non-Features (e.g. Configuration, Services) platform/postgres/, platform/identity-host/i-current-principal-accessor-application.cs + http-current-principal-accessor-server.cs
web/projects/<artifact>/ Artifact (csproj home) under the family projects/ group — definition (csproj, global-usings) and entry-point bootstrap only; content that would mean nothing if you imagine the deployable gone. Occupants: web-contracts/, web-application/, web-domain/, web-infrastructure/, web-server/, web-spa/ Host assembly defaults program.cs, sample-options.cs (binding/validation exemplar, not a real concern)
web/msbuild/ Build machinery for the web family (filename-grammar props, membership targets) n/a feature-membership.targets

Family root shape: multi-project container-app families group artifact folders under projects/ so the root reads as the placement rule (features/ + platform/ + projects/ + msbuild/). This shape is family-generic — web, api, and grpc each have their own features//platform//msbuild/ trees (task 129: all three now hold real content — web's product/platform slices, api's weather-forecast/, grpc's hello//superhero//greeter/ + platform/codegen/). yarp is a single-project family (yarp/ is the project — appsettings at its root); it is not nested under projects/ and has no concern trees.

Folder location is for humans; filename decides project membership. Each layer project composes its files with static filename globs keyed to a suffix under its own family's {Prefix}FeatureTreeRoot and {Prefix}PlatformTreeRoot (Web/Api/Grpc), not a folder path — a seam interface's -application.cs suffix pulls it into the family's -application compilation unit from wherever it physically sits, which is exactly why it lives beside its -server.cs implementation in platform/identity-host/ instead of a folder split by layer (the old web-application/abstractions/, retired: conflating layer with folder was never a principled reason to separate a seam from the concern it belongs to).

Modules follow concerns, not assemblies. A module (IModule) is a concern's registration manifest — the DI wiring that concern needs — and lives in the concern's folder like any other layer file: a product concern's module at its slice root (e.g. features/identity/in-memory-identity-stores-module-infrastructure.cs), a platform concern's module in its cluster (e.g. platform/postgres/postgres-db-module-server.cs). There are no assembly-level modules — an assembly is a compilation unit, not a concern. The host's program.cs remains the ordered composition root: module definition is logic (shared tree); module ordering is bootstrap (artifact folder). A concern with no registrations needs no module — no ceremony.

Product code for a web container-app lives in one feature-cohesive folder per sliceweb/features/<slice>/ — with every layer (contracts, application, domain, infrastructure, server) colocated in that folder. Host/platform clusters that are not product slices live under web/platform/<cluster>/ with the same -layer filename suffixes (so the same layer-project globs pick them up) but without …Features.<Id> namespaces. This is the answer to "where does this file live" and "what do I name it" — the most common file-placement decision in the repo.

Inside a slice, files group by use case, not by message kind: every operation gets its own <slice>/<use-case>/ folder holding all of that operation's layer files side by side — the contract next to its handler, not sorted into commands//queries/ subfolders. See Use-case folders below.

Detection — when to invoke

Signal How to find it
Creating/moving/renaming a .cs file under a product slice any web/features/<slice>/… folder
TWA0015 / TWA0016 diagnostic analyzer output names a filename and a function segment
"Feature file(s) match NO registered layer suffix" build error membership guard (feature-membership.targets)
Adding a new archetype (a new hand-authored generation pattern) needs a new registry function entry
"Should this module get its own assembly?" axis-2 note below

Grammar

<name>[-<function>]-<layer>.cs
Segment Required Meaning
name yes operation or slice-local subject, kebab-case (get-roles, hello)
function optional a registered archetype keyword; when present it must pair with exactly one layer (checked at build)
layer yes one of contracts, application, domain, infrastructure, server — decides which csproj globs the file

The function segment is a deliberate two-things-must-agree seam: naming the archetype in the filename and the layer both, so the analyzer can catch a mismatch instead of relying on a human remembering the pairing.

Worked examples per archetype

Filename Layer Function → required layer Living anchor
create-role-handler-application.cs application handlerapplication web/features/admin/roles/create-role/create-role-handler-application.cs
(reserved) <name>-endpoint-server.cs server endpointserver registered for a hand-authored server endpoint shim; the template generates FastEndpoints from contracts rather than hand-authoring them, so use this only for a genuinely hand-written endpoint

A mismatched pairing (e.g. create-role-handler-server.cs, function handler on layer server) is TWA0015 — see below.

Reserved layer headroom: domain is a registered layer with its own csproj glob and membership-guard entry, but most product slices need only contracts and application (plus infrastructure or server where relevant) — a slice earns a -domain.cs file only once it needs its own aggregate root (IAggregateRoot) rather than a platform/shared one. domain stays registered as intentional headroom for that case, the same way the reserved endpoint function above is kept documented but currently unused.

Contracts drop the function segment

For contracts, function and layer are the same thing, so writing both would stutter (-contract-contracts). Contract files use <name>-contracts.cs with no function segment, and every -contracts.cs file is held to the operation-contract shape (Command/Query, [ApiRoute], I*Details, Validator — see tw-web-api-contracts):

Escape hatch

Not every file is an archetype instance. When a file has no registered function, omit the function segment entirely — <name>-<layer>.cs — and the grammar imposes no archetype shape on it:

An unregistered or misspelled token that looks like it's trying to be a function is TWA0016, not a silent escape hatch — see below.

Use-case folders

The rule is unconditional: every operation gets its own <slice>/<use-case>/ folder holding every layer file for that operation, side by side — the contract next to its handler. A folder with only two files in it is correct; there is no size threshold below which an operation stays flat at slice root. Files that serve more than one operation (a shared bindable DTO, a store, an entity-type configuration) stay at slice root instead of picking one use-case folder to live in.

commands/ and queries/ subfolders (or any other group-by-kind split, such as client-to-server//server-to-client/ for a hub) do not appear inside a slice — grouping by message kind is a layer instinct, and a feature-cohesive slice groups by use case instead. Folder path never affects project membership (only the filename suffix does — see Grammar above), so this is a pure human-navigation convention, not something the build enforces.

Worked example — a whole slice (web/features/admin/roles/):

admin/roles/
  create-role/
    create-role-contracts.cs
    create-role-handler-application.cs
  delete-role/
    delete-role-contracts.cs
    delete-role-handler-application.cs
  get-role/
    get-role-contracts.cs
    get-role-handler-application.cs
  get-roles/
    get-roles-contracts.cs
    get-roles-handler-application.cs
  update-role/
    update-role-contracts.cs
    update-role-handler-application.cs
  role-details-contracts.cs          # shared bindable shape used by every use case above
  role-store-application.cs          # shared store, not operation-specific

Every use-case folder here holds exactly one contract file and one handler file — two files is the normal case, not a special one. role-details-contracts.cs and role-store-application.cs each serve multiple use cases (or the whole slice), so they stay at admin/roles/ root rather than moving into any single use-case folder.

When a slice's operation name is identical to the slice name (a single-operation slice), the use-case folder is still literal — hello/hello/hello-contracts.cs, not a special-cased flat layout. When a hub or similar has one folder per message DIRECTION instead of per use case (client-to-server/, server-to-client/), that is the same group-by-kind instinct as commands//queries/ and collapses the same way: one folder per use case (send-message/, receive-message/), not one folder per direction.

Registry (SSOT)

source/analyzers/timewarp-architecture-convention-analyzers/feature-filename-grammar.json:

{
  "layers": [ "contracts", "application", "domain", "infrastructure", "server" ],
  "unroutedLayers": [ "tests" ],
  "functions": {
    "handler": "application",
    "endpoint": "server"
  }
}

This JSON is the single source of truth and is itself family-agnostic. An MSBuild target on the convention-analyzers project regenerates it into a family-agnostic artifact plus one standalone artifact per family before every compile:

unroutedLayers (task 135): a registered-but-unrouted layer (currently just tests, backing co-located Jaribu runfiles) is matched and validated by TWA0015/TWA0016 and the membership guard exactly like a routed layer — a -tests.cs file is a legitimate archetype, and create-role-handler-tests.cs still trips TWA0015 through the ordinary pairing logic (the handler function still requires -application, no matter which layer the file actually ends in) — but it gets no Compile glob in any family's feature-filename-grammar.g.props, so it claims no layer project's build. This is what lets a co-located test file live beside real slice code, stay a first-class grammar citizen (orphaned or misnamed -tests.cs files still trip the teaching membership-guard error), and still compile into nothing. Functions register only against routed layers — unroutedLayers entries never appear as a functions value.

Enforcement surface — honest scope: TWA0015/0016 and the membership guard only see a -tests.cs file when it is actually compiled as part of some MSBuild invocation. Because an unrouted layer claims no layer project's Compile glob by design, the repo's own dev build solution gate never compiles these files (and aggregators are deliberately not in .slnx). Compile coverage for co-located runfiles comes from:

  1. Standalone dotnet build / dotnet run on the runfile (synthesized single-file project).
  2. Family JARIBU_MULTI aggregators (task 136) under tests/container-apps/<family>/<family>-jaribu-tests/ — glob that family's features/**/*-tests.cs and platform/**/*-tests.cs; discovered by dev test (MTP bare dotnet test from the project dir). Web and api exist today; grpc when it gains runfiles. A new aggregator MUST carry a project-local global.json with "test": { "runner": "Microsoft.Testing.Platform" } AND the root SDK pin mirrored — that global.json is the sole signal dev test keys off to pick the MTP invocation; the csproj's TestingPlatformDotnetTestSupport property alone is NOT detected, and an aggregator missing the file silently falls to the unsupported VSTest path and fails at dev test time.
  3. dev template-smoke tiers 1–3 for the two exemplars (guard text, standalone run, aggregator MTP counts).

A broken or misnamed co-located test that is never run standalone and is not yet under an aggregator's glob can still sit undetected by dev build alone — prefer dev test (or standalone run) after adding a runfile.

Co-located Jaribu runfile preamble (the tests layer)

The canonical in-repo authoring convention for co-located Jaribu runfiles — preamble shape (#:project/#:package, PublishAot=false, NoWarn=$(NoWarn);…, Purpose region, the cnd:noEmit-escaped JARIBU_MULTI switch), C-create host graphs via HostGraphFactory, session-scoped fixtures for expensive closed-box graphs, aggregator wiring and its mandatory project-local global.json, and the maintenance rules that ride along: references/co-located-jaribu-runfiles.md.

Reference implementations: create-role-tests.cs (web, host-free), get-weather-forecasts-tests.cs (api, SetupOnce + HostGraphFactory). Jaribu itself (attributes, naming, assertions) is the cross-repo tw-jaribu skill.

Membership guard

Each family gets its own guard: <family>/msbuild/feature-membership.targets (imported once via <family>/Directory.Build.targets) walks every .cs under that family's features/ and platform/ trees and requires each one to match exactly one registered -{layer} suffix, generated from the same registry. A file matching zero registered suffixes is a build error — it would otherwise compile into no project at all:

Feature/platform file(s) match NO registered layer suffix and would compile into no project: <file>. Rename to <name>[-<function>]-<layer>.cs with layer one of: -contracts, -application, -domain, -infrastructure, -server. Registry: feature-filename-grammar.json. Trees: features/ and platform/.

Fix: rename the file to end in one of the registered layer suffixes. Dual-match (two suffixes claiming the same file) can't happen structurally once suffix nesting is rejected at generation time, so this guard's only failure mode in practice is a missing or misspelled layer suffix.

TWA0015 / TWA0016 — what they mean and how to fix

Diagnostic Trigger Fix
TWA0015 Filename's function segment is registered, but paired with the wrong layer suffix (e.g. -handler- on a file ending -server) Rename the file to end in the function's registered layer, or drop the function segment entirely if the file isn't actually that archetype
TWA0016 Filename's trailing segment looks like a function but isn't registered — an unrecognized token, a misspelling, a case mismatch (-Handler- vs -handler-), or an incomplete multi-segment function that shares the final segment of a registered multi-segment function without matching it fully Use a registered function name exactly as spelled/cased, or use the escape-hatch form <name>-<layer>.cs with no function segment if the file isn't an archetype instance

Both diagnostics report the file name, the offending segment, and the full list of registered pairs/functions so the fix doesn't require opening the registry to look it up.

Path-matching caution: these diagnostics only fire on the cohesive tree (web/features/…), never on web-spa/features/… (SPA exception below) or on generated scaffolding. Roslyn can report a glob-included file's path as project-relative with .. segments (e.g. web-server/../features/hello/hello-handler-application.cs); anything that scopes analysis to the cohesive tree must normalize such paths rather than matching a bare project-directory substring, or it risks silently treating the entire cohesive tree — or the entire SPA tree — as in or out of scope incorrectly.

Features substrate (cross-slice constants)

Some product files intentionally use the bare TimeWarp.Architecture.Features namespace — no slice …Features.<Id> — so multiple product slices can reference well-known ids without cross-slice coupling (TWA0009). This is the Features substrate tier, not a product slice.

Litmus Home
Compile-time constants or shapes many product slices must share (role ids, module ids) Bare …Features namespace; file still lives under a folder for humans (e.g. features/admin/roles/role-ids-contracts.cs, features/admin/modules/module-ids-contracts.cs)
Product operation / slice-owned logic …Features.<Id> under features/<slice>/

Document the choice in the file's #region Design (existing examples do). Do not invent a grab-bag shared assembly for one-off constants — substrate is for true cross-slice contract data only. SPA base types under web-spa/features/base/ also use bare Features by SPA convention; that is separate from the cohesive product-tree substrate above.

SPA exception

web-spa/features/ stays conventionally organized — one folder per slice, Razor SDK defaults, no cross-folder glob, no filename-grammar suffix requirement. Razor's own source-generation and item types make .razor a poor fit for the layer-suffix scheme, so the SPA is deliberately left out of the cohesive-tree rehome. Page/state/action placement inside web-spa/features/<slice>/ is a tw-slice-isolation question (namespace/tier), not a filename-grammar one.

Proto exception

grpc's proto-first artifacts — .proto source files (e.g. greet.proto) and their generated GreeterBase/message code — stay in their artifact folder (grpc-server/protos/) and are out of filename-grammar scope entirely: no -<layer> suffix, no cohesive-tree membership, not scanned by the membership guard or TWA0015/TWA0016. The proto toolchain owns their compilation and namespace (option csharp_namespace); a hand-authored implementation of a proto-generated service (e.g. GreeterService : Greeter.GreeterBase) is ordinary product code and follows the normal rules — it lives in its own grpc/features/<slice>/<use-case>/ <name>-server.cs like any other slice, it just happens to inherit from a proto-generated base class that lives elsewhere. Code-first gRPC contracts (protobuf-net.Grpc [ServiceContract] interfaces, [DataContract]/[ProtoContract] DTOs) are the opposite case: ordinary C# types under full grammar scope, -contracts.cs like any other contract.

Code-first service interfaces are not always a free -application.cs seam move: a protobuf-net.Grpc [ServiceContract] interface carries wire-protocol attributes (System.ServiceModel/Grpc.Core) that only the contracts project references by default — moving one to -application.cs needs the destination project's own global-usings.cs extended to match (global usings are per-project and do not flow through ProjectReference), and, more importantly, if any consumer outside the family (e.g. a WASM client) references the interface directly by referencing only *-contracts.csproj, moving it to -application.cs changes its compilation unit and breaks that consumer's reference unless the consumer's project reference also changes — check the consumer graph before applying the seam-interface pattern to a gRPC service interface, not just for plain seam interfaces.

Axis-2 note: per-module assembly splits are a glob operation

Implementation layers (application, domain, infrastructure) default to one assembly per layer across all slices; TWA0009 governs module privacy inside that shared assembly via namespace, not via a compiler-enforced assembly boundary. If a module later earns its own assembly (it gets large, sensitive, or heads toward service extraction), splitting it out is a csproj/glob change, not a file-move — the new project's Compile Include glob simply narrows to that module's slice folder(s) under the same cohesive tree. Files never move and namespaces don't change; only which project's glob claims them does.

Agent workflow