t1k:cocos:playable:modularize-contribute
| Field | Value |
|---|---|
| Module | playable |
| Version | 2.14.4 |
| Effort | medium |
| Tools | — |
Keywords: contribute, novel-subsystem, package-extraction, scaffold-package, split-proposal
How to invoke
Section titled “How to invoke”/t1k:cocos:playable:modularize-contribute<project-root> [--apply] [--mode add|update] [--split game-foundation]contribute (Pillar D — secondary)
Section titled “contribute (Pillar D — secondary)”The main goal of t1k-cocos-playable-modularize is to refactor/reuse/modularize the project itself
(structure-audit → reuse-scan → refactor), scoped to assets/scripts. This phase is the smaller
follow-on: if something in assets/scripts is worth contributing back, separate it into a real
@playablelabs/<name> package (plain readable TS) and let the dev decide on publishing. Obeys the
shared CLI contract.
Two modes — ADD and UPDATE
Section titled “Two modes — ADD and UPDATE”Contributing back means getting the improvement into a package, which is two distinct jobs. Both
are in scope; the mode is decided by the reuse-scan verdict, not by preference.
| Mode | Input verdict | What it produces |
|---|---|---|
| ADD | CONTRIBUTE — novel, no upstream package | A new @playablelabs/<name> package skeleton under assets/packages/. |
| UPDATE | EXTEND — package exists, local diverged ahead | A PR on the package’s upstream source repo, plus a handoff record at reports/contribute/<name>-<version>/. No new package, and nothing written into assets/packages/ — a sync mirror of published output, not source. |
UPDATE is the easy one to miss. It only has an input because reuse-scan runs the divergence
check — if a subsystem where local was better got tagged plain REUSE, the Tier 2 swap already deleted
the improvement and there is nothing left here to contribute. If you reach this phase and suspect that
happened, recover it from git history before proceeding.
When to invoke
Section titled “When to invoke”- After
refactorlanded: turnreuse-scan’s CONTRIBUTE tags into new packages (ADD) and its EXTEND tags into package updates (UPDATE). - Mid-walk, before
refactorhas landed — UPDATE mode only, to unblock a Tier 2 EXTEND unit whoserequires:names this package. See the exception in Workflow step 1. - To propose decomposing the coarse
@playablelabs/game-foundationinto finer packages.
Skip this phase entirely if the scan produced no CONTRIBUTE and no EXTEND — contribution is optional, not a required step. That is the normal outcome for a project with nothing novel.
What it does
Section titled “What it does”ADD — new package
Section titled “ADD — new package”- Re-verify no upstream equivalent exists. Check both sources: one
doc_searchper candidate and the CPM listing at https://cpm.playablelabs.ai/ (a missed synonym hides an existing package, and CPM is the naming authority). - Separate it into a package: copy the subsystem source (from
assets/scripts/<sub>) intoassets/packages/@playablelabs/<name>/, add apackage.json(withpublishConfig.registry) + anindex.tsbarrel. Straight, readable TypeScript — the code is copied as-is. This is the one sanctioned write intoassets/packages/: a brand-new package is absent fromnode_modulesandplayableSync, so no sync targets it. That immunity ends once it is published and installed — from then on treat it as a mirror like any other, and any further change goes through UPDATE. - STOP. Print the publish-decision note.
UPDATE — existing package
Section titled “UPDATE — existing package”The target is the package’s own upstream source repo, cloned. Not assets/packages/, and not a
copy of it staged elsewhere. assets/packages/@playablelabs/<name>/ is disqualified twice over:
it is remove-then-copy replaced on every sync, and it is published-tarball output
(*.core.ts/*.core.impl.ts split, wrong db:// asset-root basename) that cannot be built or
published. Relocating a copy of it fixes only the first problem. Read
../t1k-cocos-playable-modularize/references/shared-cpm-facts.md
§ “assets/packages/ is a SYNC MIRROR” before this step — it is the SSOT for both reasons.
- Identify the package and find its source repo. Confirm the installed version
(
get-installed-packages,npm view @playablelabs/<name> --registry <host>, or the CPM web UI), then locate the upstream repo that publishes it. Clone it. If no clone is available, stop and say so — do not substitute the vendored copy, and do not infer the package’s internal structure from it. A structural claim read offassets/packages/describes build output only. - Author the change against real repo source, on a branch, as a PR. Port the behaviour, not the file: the local copy may carry game-specific coupling that must not enter a shared package (apply the naming test below), and it is shaped for a file layout that does not exist upstream.
- Express the version as the repo’s mechanism does. Decide the class — patch (fix) · minor
(added capability) · major (breaking signature) — then apply it the way that repo ships versions:
a
release:label on the PR where the version is CI-owned,bump-local-versionfor a genuinely local package. Never hand-edit aversionfield on a CI-owned repo, and nevernpm publishby hand — it bypasses the obfuscate / core-split / path-rewrite pipeline. - Record the handoff at
reports/contribute/<name>-<version>/: aRELEASE.mdnaming the upstream PR, the base and target versions, what changed and why, and the divergence’s origin. Keep only the evidence a reader needs — the local diff or the changed file for provenance. This directory is a record, not a patch source and not a buildable tree; nothing is ever published from it. Do not give it apackage.jsoncarryingpublishConfig.registry:reports/is not inPublishService’sIGNORE_DIRS, so such a file makes the record show up inlist-local-packagesunder the same name as the real package. - Reinstall after the release publishes, through the extension, so
assets/packages/picks the change up by sync rather than by hand. - STOP. Print the publish-decision note.
Why the upstream PR is not optional. For ADD mode the framing below (“publishing is the dev’s call”) holds. For UPDATE mode on a project that installs via the package-manager extension it does not: the improvement has no durable home in the project — the only in-tree copy is scheduled for deletion by the next sync. Landing it upstream is the only way it survives. Report it as “land it upstream or lose it”, never as “publish or keep it locally”.
Details: references/package-skeleton.md.
Shared-package naming test (both modes)
Section titled “Shared-package naming test (both modes)”Before anything leaves the project, apply the reusable-code rule from
rules/code-conventions-cocos.md: would this name read correctly in a completely different game?
BeadsOutPool fails; ObjectPool<T> passes. A game token leaking into a shared package is the defect
that makes the package unusable for its next consumer — and in UPDATE mode it is worse, because it
lands in a package other projects already depend on.
Publishing is the dev’s call (not this skill’s) — with one exception
Section titled “Publishing is the dev’s call (not this skill’s) — with one exception”contributechanges the package locally only (plain readable TS) — a new skeleton in ADD mode, an upstream PR + a handoff record in UPDATE mode — and prints the publish steps. It never publishes.- The exception: UPDATE mode on a package-manager-extension project. Whether to land the change upstream is still the dev’s decision, but it is not a free one — declining it discards the contribution, because the only in-project copy lives under a directory the next sync deletes. Present it as “land it upstream or lose it”, never as “publish or keep it locally”.
- Publishing is never a hand-run
npm publish— that bypasses the obfuscate / core-split / path-rewrite pipeline and yields a tarball unlike every other@playablelabspackage. A package with an upstream source repo ships through that repo’s release flow (arelease:label on the merging PR, version derived by CI from the newest tag — never a hand-editedversionfield); a genuinely local one ships through the extension’sbump-local-version→build-local-package→publish-local-package, run against a real source checkout. SSOT:shared-cpm-facts.md§ “Publishing is NOTnpm publish”.
Workflow
Section titled “Workflow”-
Validate
<project-root>+ confirm the project refactor landed (healthy project — don’t extract from one still failing structure-audit/reuse-scan). Else exit 2 with the reason.How to confirm “landed” — check the artifact, not the vibe.
refactoris required to emit a change + status report after every--apply, so that report is the evidence:Terminal window node ../t1k-cocos-playable-modularize-refactor/scripts/refactor-status.cjs \--plan <plan.json> --done <orders> --verify <verify-imports.json>Landed = every Tier 0–3 unit in the plan is
done(nonependingorrolled-back) and theverify-importsgate is green. No status report at all → treat as NOT landed and stop, rather than assuming a clean tree means the refactor ran — an untouched legacy project also has a clean tree, and that is exactly the project you must not extract packages from.The gate is
refactorhaving landed.Exception — UPDATE mode unblocking a Tier 2 EXTEND unit.
The landed-gate does not apply to UPDATE mode invoked as a Rule 1 unblock.
decision-policy.md§ “Rule 1 — REUSE +divergence.localAhead === true→ EXTEND” orderscontributeUPDATE → release → repoint, andplan-refactor.cjs(localahead-reuse-becomes-extend) encodes that ordering literally: the Tier 2 unit it emits carriesrequires: ["contribute (UPDATE mode) on <pkg>: land the local divergence in the package and release it before this swap runs"]. Requiring every Tier 0–3 unitdonebefore UPDATE may run would require the very unit UPDATE exists to unblock — the two documents would have no satisfiable path between them.The exception is evidence-gated, not a judgement call. Skip the landed-gate only when all three hold, each read off the refactor plan:
--mode update(never ADD, never--split), and- the plan contains a unit with
autoDecision.rule === "localahead-reuse-becomes-extend"whoserequires:names the package you are about to update, and - that unit is not
donein the status report.
Any one missing → the landed-gate applies unchanged, and that includes the no-plan case: with no refactor plan there is no unit to cite, so “no status report at all → NOT landed and stop” still holds. Record the citing unit’s
orderin the run’s report so the skipped gate is auditable.Why UPDATE is safe here and ADD is not. The gate exists so a new package is never extracted out of an unhealthy project’s
assets/scripts. UPDATE extracts nothing: it ports an already-written local improvement into the package’s upstream source repo. Project structural health is not a precondition for that, which is why the carve-out is scoped to UPDATE and leaves ADD — and end-of-pipeline UPDATE at Phase 6 — gated exactly as before. -
Collect candidates from the reuse-scan report and split by mode: CONTRIBUTE → ADD, EXTEND → UPDATE. Re-verify each ADD candidate against both discovery sources (
doc_search+ CPM); for each UPDATE, confirm the target package’s current published version.A module marked
status: "TBD"indocs/design-manifest.jsonis ADD-mode input.TBDmeans the design declares a module the code does not have yet — novel work by definition, so it has no upstream counterpart to reuse. Read it alongside the reuse-scan verdicts rather than instead of them:TBDsays the module is unbuilt, the scan says whether anything upstream already does it. -
ADD — separate (report-only):
node scripts/scaffold-package.cjs --name <n> --from <dir-or-file>prints the file plan + publish-decision note.--fromtakes a directory OR a single.tsfile and is repeatable — extract exactly the files the CONTRIBUTE unit needs, since a folder often mixes REUSE/KEEP/CONTRIBUTE files (a file’s sibling.metais co-copied).--applycopies the source + writes package.json + index barrel. Run it from the Cocos project dir (the one holdingassets/):--from, the default destination, and--outare all resolved againstprocess.cwd()and confined to it, so an--out ../…escape is rejected.This script is ADD-mode only. UPDATE mode scaffolds nothing — its change is authored in the package’s upstream source repo (§ UPDATE above), and its
reports/contribute/…handoff record is written by hand, not generated. -
Split proposal (optional):
--split game-foundationemits the advisory split table (references/split-proposal-format.md) — proposals only. -
Report the new package path(s) (ADD) and the upstream PR + handoff record (UPDATE), with the publish-decision note →
reports/.
Scripts
Section titled “Scripts”node scripts/scaffold-package.cjs --name <n> --from assets/scripts/utils/Log.ts # report-only: single-file unitnode scripts/scaffold-package.cjs --name <n> --from assets/scripts/foo.ts --from .../bar.ts # multi-file unit (repeatable)node scripts/scaffold-package.cjs --name <n> --from assets/scripts/<sub> --apply # whole dir -> @playablelabs/<n> (plain readable TS)ADD mode only, run from the Cocos project dir. UPDATE mode has no script — its change is authored in the package’s upstream source repo; see § UPDATE.
References
Section titled “References”references/package-skeleton.md— package layout, package.json shape, dev publish-decision steps.references/split-proposal-format.md— game-foundation split proposal table + when-to-split criteria.
Gotchas
Section titled “Gotchas”- Secondary phase — optional. The deliverable of the skill set is a modularized project (
assets/scripts); contribute only fires when there’s genuinely novel code (ADD) or a local improvement over an existing package (UPDATE). - Contribution is ADD or UPDATE — not just new packages. The common real case is UPDATE: the package already exists and the project fixed or extended it. Producing a new package for something an existing one covers is a duplicate, and duplicates are worse than no contribution.
- UPDATE depends entirely on
reuse-scanhaving tagged EXTEND. If a diverged subsystem was tagged plain REUSE, the Tier 2 swap already deleted the local improvement — clean compile, no error. Recover from git history before this phase, or the contribution is silently lost. - In UPDATE mode, never edit
assets/packages/— and never read it as source either. It is remove-then-copy replaced on every sync, so an edit there is deleted at the next sync of that package at any version; and it holds published-tarball output (*.core.impl.tssplit, wrongdb://asset-root basename), so a copy of it cannot be built or published no matter where you put it. Staging a “full copy” elsewhere fixes only the first half — the durable target is the package’s upstream source repo, cloned. Worse, porting a patch onto a newer base inside that directory upgrades the installed tree as a side effect, applying an upgrade a plan may have deliberately held back with no record that it happened. SSOT:shared-cpm-facts.md§ “assets/packages/is a SYNC MIRROR”. - Never infer a package’s internal structure from the vendored copy. The
.core.ts/.core.impl.tssplit is publish-time obfuscation, not a code convention — merging a patch against it invents a delegation shape that exists nowhere upstream, and the merge has to be thrown away. Clone the source repo before reasoning about structure. - In UPDATE mode, port the behaviour, not the file. The local copy often carries game-specific coupling. Copying it wholesale pushes a game token into a package other projects already depend on.
- Re-verify ADD candidates against BOTH sources. doc_search alone can miss a synonym; CPM alone only matches what you looked up. A duplicate package is the expensive mistake here.
- Extract at file granularity, not whole folders. A folder like
assets/scripts/utilsoften mixes verdicts (e.g.MathConstants.ts=REUSE,Log.ts/MeshFoot.ts=CONTRIBUTE,ImageScaler.ts=KEEP). Use--from <file.ts>(repeatable) to package exactly the CONTRIBUTE files — don’t--fromthe whole dir when it’s mixed. - Never publishes. Scaffolds locally (plain readable TS); the dev decides publish (manual or AI). Prints the steps.
- Runs on a healthy project only — require refactor landed; re-verify CONTRIBUTE via doc_search before extracting. Except UPDATE mode unblocking a Tier 2 EXTEND unit, which runs mid-walk by design (Workflow step 1 § Exception).