Skip to content

t1k:cocos:base:architecture-patterns

FieldValue
Modulebase
Version3.3.2
Effortmedium
Tools—

Keywords: architecture, architecture-audit, code quality, component-split, dip, enabling-extraction, fsm pattern, god-class, patterns, pooling, refactor, signalbus, solid, srp

/t1k:cocos:base:architecture-patterns
[audit] [<project-root>] [--json <out>]

Structure decisions for Cocos Creator 3.x + TypeScript. Knowledge and grading only — this skill writes no project files.

Two SSOTs, neither of them this file.

  • Doctrine — the SOLID table, module boundaries (GameplayController is the only orchestrator; SignalBus is the only cross-module channel), mandatory class roles, codebase flow, the anti-patterns table — lives in the project’s docs/system-design.md § “Code Structure Rules (Mandatory)”. Read it there.
  • The artifact — that doc’s template, its module-table contract, and the generate/verify/migrate scripts — belongs to t1k-cocos-base-system-design.

This skill owns only what neither of those does: when a pattern becomes mandatory, the split test, the enabling-extraction concept, and the audit that grades a codebase against its design doc. It reads the doc and the manifest; it never writes either. That separation is deliberate — a skill that both defines the target and grades against it can never report that the target itself is wrong.

Related: t1k-cocos-base-code-conventions = what things are called. Per-pattern skills (t1k-cocos-playable-object-pool, -fsm, -signalbus) = how each is written.

Not the same thing as t1k-cocos-base-clean-architecture. That skill is a different, opt-in 4-layer (Domain/Application/Presentation/Infrastructure) style for features whose rules/formulas/state transitions outlive one deliverable — this skill’s Service/Component/Controller doctrine and that skill’s 4-layer doctrine are alternatives, not layers of the same thing. Pick one per module.

IntentPath
”What are the architecture rules?”Not here — docs/system-design.md § Code Structure Rules
”Which pattern does this code need?”Mandatory pattern triggers
”Is this class too big / should I split it?”The split test
”Why can’t I swap in the library package?”Enabling extraction
”Does the code match the design doc?”audit
”I have no design doc / it is not on the template”Not here — t1k-cocos-base-system-design
”Generate or verify the manifest”Not here — t1k-cocos-base-system-design

The design doc’s anti-patterns table says don’t do X. This table says when you are obliged to introduce Y — the gap the doc leaves. These are completion gates, not suggestions.

TriggerRequired patternReasoning
instantiate() / new Node() per-spawn during gameplayObject pooling (ObjectPoolManager)Allocation + GC reclaim land mid-frame. On a playable’s budget this is the dominant framedrop source, and it only appears under load — so it survives casual testing.
2+ booleans encoding current activity (_isDead, _isPaused)FSM (StateManager)N booleans encode 2^N states, most contradictory. An FSM makes the legal set explicit and illegal states unrepresentable.
A module reaching into another module directlySignalBusAlready banned by the doc’s module-boundary rule; listed here because it is the trigger that most often blocks a package swap later.
A class with 3+ change reasons (see below)Component split (SRP)Blocks reuse; makes every later change a merge conflict.
Global mutable state read from 3+ sitesService singletonAd-hoc module-level state has no defined init order and no teardown hook, so it leaks across scene reloads.

Counter-trigger — never apply a pattern without its trigger. A pool for a boot-time spawn, an FSM for one boolean, or an interface with one implementation is pure added complexity. The trigger is the justification (YAGNI, per rules/coding-guidelines.md §2).

Line count is a symptom, not the rule. Apply the change-reason test: list the distinct reasons this file would be edited over the next six months. Three or more unrelated reasons → split.

GameplayController (420 lines)
reason 1: tuning spawn difficulty -> SpawnDirector
reason 2: camera follow feel -> CameraController <- swappable for a package
reason 3: score + combo rules -> ScoreService
reason 4: win/lose transition -> stays (this IS the orchestrator's job)

A 300-line class with one change reason is fine; a 90-line class with four is not. The conventions’ ~200-line cap is a prompt to run this test, not the violation itself. Note the orchestrator legitimately holds win/lose transition — the doc makes GameplayController the only class allowed to call GameView/CTAService, so do not “clean” that out.

The concept linking architecture work to package reuse. A subsystem that should become a published @playablelabs/* package sometimes cannot be, because it is fused into a larger class — there is no seam to cut.

GameplayController.updateCamera() <- camera logic fused into the orchestrator
cannot swap @playablelabs/camera-controller
| SRP split (the enabling extraction)
v
CameraController.update() <- seam now exists
| reuse swap now possible
v
import { CameraController } from '@playablelabs/camera-controller'

This is why the refactor plan has two reuse passes:

  1. Reuse first where the seam already exists — never refactor code a swap is about to delete.
  2. Architecture second, including enabling extractions.
  3. Reuse again — the extractions expose seams the first scan could not see.

Flag every enabling extraction in the audit output so pass 3 knows where to look.

Architecture conformance is a judgment call, not a regex match — so the audit is AI-driven (read the code, reason against the design doc, emit findings) rather than a deterministic script, per rules/ai-driven-design.md. Report-only: it never mutates project source. Its whole output is the JSON below, persisted to reports/architecture-audit.json — see § “Persisting the audit”.

Inputs: the project’s docs/system-design.md + assets/scripts + the prior-art table from t1k-cocos-base-knowledge-sweep (Cocos-scoped corpus sweep). Grades against: the doc’s documented modules/signals/class roles and its § Code Structure Rules and the trigger table above.

Run early, as a third plan input alongside structure-audit and reuse-scan.

Before you grade: check what you are grading against

Section titled “Before you grade: check what you are grading against”

Architecture cannot be graded without a stated target, and a doc that is absent, stale, or unreadable is not one. This is a blocking finding, not something to work around — and the repair belongs to t1k-cocos-base-system-design, not here.

Doc stateWhat to do
Absent, or not on the canonical templateStop. Report designDocStatus and name t1k-cocos-base-system-design. Do not invent a target.
Present but staleStop. Grading against a doc known to be wrong produces findings nobody can act on.
docs/design-manifest.json shows parse: "partial" / "unparseable"The module map is incomplete — say so in the report rather than treating the modules you can see as the whole project.
Present and currentGrade.

Worked example of the output shape (two findings, one with an enabling extraction): references/audit-json-example.md.

requiredPattern names the pattern the finding obliges, and it is read downstream as data, not prose — t1k-cocos-playable-modularize-refactor keys a per-pattern editor-state override off it (EDITOR_STATE_BY_PATTERN). Emit one of these exact spellings:

requiredPatternShape of the workDownstream editor state
object-poolingin-file editopen (tier default — verification is an in-editor compile)
fsmin-file editopen
signalbusin-file editopen
component-splitin-file edit + new sibling .ts, scene re-pointopen
service-singletonin-file editopen
module-splitbulk .ts + .meta move across directoriesclosed — a live editor is a second writer racing the importer

The last row is the one that matters: it is the split decision-policy.md rule 7 step 3 consumes, and it is the only pattern here whose work is a file move rather than an in-file edit. A move-shaped finding emitted under any other spelling falls through to the tier default and gets planned with the editor OPEN — the silent scene-corruption path rule 8 exists to avoid (#329). Adding a new move-shaped pattern means adding it to BOTH this table and EDITOR_STATE_BY_PATTERN.

enablingExtraction: true + exposesSeamFor drive the Tier 2.75 second reuse pass; a finding without them is ordinary quality work. source keeps provenance so a reader can trace any finding back to the design doc or the trigger table.

exposesSeamFor must be evidence, not a guess. Naming a package the split would unlock is a claim about what already exists upstream — so it comes from the t1k-cocos-base-knowledge-sweep prior-art table, not from intuition about what @playablelabs probably ships. An unbacked exposesSeamFor sends Tier 2.75 hunting for a package that does not exist, and the extraction that justified itself on that promise becomes pure added surface.

“The sweep found nothing” and “the sweep never ran” are different facts — never collapse them. Both leave exposesSeamFor unset, but they say opposite things about the project, so the audit records which one happened via a top-level priorArt field:

Sweep statepriorArtWhat to emit
Ran, no hit for this seam"searched"Drop exposesSeamFor; the finding stands as ordinary SRP work. A real negative.
MCP unavailable / not invoked"unavailable"Drop exposesSeamFor and say so in the report. Every enabling-extraction claim in this run is unverified — not disproven.

Reporting an unavailable sweep as a clean negative manufactures a false “no upstream package exists”, which is the expensive direction of this error (rules/negative-result-scope.md): a false “already exists” costs one check and self-corrects, a false “nothing upstream” gets built. When priorArt is "unavailable", Tier 2.75 has no input it can trust — re-run the sweep before treating that pass as complete rather than concluding there were no seams to fill.

The audit is a path-addressable artifact, not just a returned summary: Phase 3 of the modularize runbook gates on the file existing, and plan-refactor.cjs reads it as --arch <path>. Persist it the way the two sibling plan inputs do (structure-audit step 4, reuse-scan step 6):

Report the findings, then persist the JSON to reports/architecture-audit.json (override with --json <out>). Writing a report under reports/ is not a source mutation — “report-only” constrains what the audit may change in assets/scripts, not whether it may leave its own output on disk.

The invoking skill body writes the file. Reasoning about the code may be delegated — the audit is review-shaped work, and t1k-cocos-reviewer is a reasonable place to do it — but that agent is deliverable: return with no write tool, so its findings come back as a returned summary and the body that spawned it persists them.

Never brief a deliverable: return agent to produce a path. With no write tool it falls back to Bash heredoc appends, which truncate silently at roughly 6 KB per call, and a mid-append read is indistinguishable from a dead agent — so the briefer cannot tell a partial write from a lost one, and a mid-task re-brief corrupts the run (#275). If you want an agent to be the writer, route to a write-capable one and name it; do not widen the reviewer’s read-only grant (#232).

Feed the persisted file to plan-refactor.cjs --arch reports/architecture-audit.json so findings become ordered Tier 2.5 plan steps rather than ad-hoc edits.

  • Do not restate architecture doctrine in this skill or in a plan. docs/system-design.md is the SSOT. Copying its SOLID/module-boundary/anti-pattern content anywhere else guarantees the two drift, and the copy is the one people read.
  • Do not author or repair the design doc from here. An absent or stale doc is a blocking finding you report, and t1k-cocos-base-system-design fixes. Fixing it inline makes the audit grade its own homework.
  • Line count is a symptom, not the rule. Splitting a 250-line class with one change reason yields two coupled classes and a worse codebase. Run the change-reason test first.
  • Do not refactor code a reuse swap will delete. Check the reuse-scan report before any SOLID work — the most common wasted effort in a legacy pass.
  • Applying a pattern without its trigger is also a violation. See the counter-trigger above.
  • Splitting an attached @ccclass Component breaks scene references silently. Moving behaviour out leaves the old @property refs dangling and the new component unattached. tsc cannot catch it — the editor is authoritative. Re-wire and verify before deleting the old class.
  • A SignalBus conversion that forgets onDestroy unsubscribe trades a coupling bug for a leak. Pair every subscribe with teardown using a named method — inline lambdas can never be unsubscribed (indexOf reference match).
  • Enabling extractions are worthless if the second reuse pass never runs. Tier 2.5 without Tier 2.75 leaves you with more files and the same duplicated logic — a net loss.
  • The audit is judgment, so it is not byte-reproducible. Two runs may rank findings differently. Treat the JSON as a prioritized worklist, not a regression baseline; do not gate CI on its contents.
  • docs/system-design.md (per project) — SSOT for architecture doctrine. This skill grades against it.
  • t1k-cocos-base-system-design — owns that doc as an artifact: template, module-table contract, and the generate/verify/migrate scripts. Everything this skill used to do with files now lives there.
  • t1k-cocos-base-knowledge-sweep — Cocos-scoped prior-art sweep; supplies the audit’s third input and the evidence behind exposesSeamFor. Its t1k-cocos-base-knowledge-sweep/references/doc-search-contract.md is the Cocos scoping profile for querying the corpus (general SSOT: core’s t1k-knowledge-retrieval).
  • rules/knowledge-sweep-cocos.md — auto-loaded rule binding brainstorm/plan/audit to that sweep in Cocos projects.
  • rules/code-quality-cocos.md — the binding obligation this skill serves.
  • t1k-cocos-playable-modularize-refactor — executes these findings as Tier 2.5 / 2.75.
  • t1k-cocos-playable-object-pool, -fsm, -signalbus — how to write each pattern.