@maccesar/titools 4.19.0 → 4.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +11 -6
  2. package/agents/ti-pro.md +3 -0
  3. package/commands/ti-audit.md +11 -10
  4. package/commands/ti-new-screen.md +1 -0
  5. package/lib/config.js +1 -0
  6. package/package.json +1 -1
  7. package/skills/purgetss/SKILL.md +1 -1
  8. package/skills/purgetss/references/cli-commands.md +25 -6
  9. package/skills/purgetss/references/customization-deep-dive.md +9 -4
  10. package/skills/purgetss/references/multi-density-images.md +35 -5
  11. package/skills/purgetss/references/version-history.md +5 -0
  12. package/skills/ti-expert/SKILL.md +5 -2
  13. package/skills/ti-expert/references/patterns.md +2 -0
  14. package/skills/ti-game/SKILL.md +5 -2
  15. package/skills/ti-game/references/api.md +38 -4
  16. package/skills/ti-game/references/debugging-performance.md +1 -1
  17. package/skills/ti-game/references/examples.md +6 -5
  18. package/skills/ti-game/references/patterns.md +1 -1
  19. package/skills/ti-game/references/project-setup.md +7 -3
  20. package/skills/ti-game/references/recipes-action.md +1 -1
  21. package/skills/ti-game/references/recipes-adventure-effects.md +1 -1
  22. package/skills/ti-game/references/roadmap.md +8 -4
  23. package/skills/ti-game/references/tilemaps.md +1 -1
  24. package/skills/ti-reuse-first/SKILL.md +88 -0
  25. package/skills/ti-reuse-first/references/alloy-reuse.md +127 -0
  26. package/skills/ti-reuse-first/references/diagnostics.md +137 -0
  27. package/skills/ti-reuse-first/references/indirection.md +89 -0
  28. package/skills/ti-reuse-first/references/purgetss-reuse.md +84 -0
  29. package/skills/ti-reuse-first/references/the-ladder.md +123 -0
  30. package/skills/ti-ui/SKILL.md +2 -0
@@ -1,6 +1,6 @@
1
1
  # Upstream example catalog
2
2
 
3
- Verified against all 33 self-contained demos under `ti.game@c216e7f:example/` plus the launcher `example/app.js`. Wall contacts and `wallSlideSpeed` are not represented by a demo at this commit; use [the platformer recipe](recipes-action.md#platformer), which is verified against both native implementations and the README contract.
3
+ Verified against all 34 self-contained demos under `ti.game@3bea2f4:example/`, plus the launcher `example/app.js` and the shared back-control helper `example/backnav.js` (neither is a demo). `wallhit` now has one user — `worldwrap.js` counts bolt hits with it — but `wallSlideSpeed` and the `onWallLeft`/`onWallRight` flags still have no demo; for those use [the platformer recipe](recipes-action.md#platformer), verified against both native implementations and the README contract.
4
4
 
5
5
  The demos prove that API combinations run together on the current module. They are **not** production templates: most keep state in one file, repeat launcher/back-button plumbing, and still use pre-ES6 syntax. Start from the closest demo, retain the demonstrated engine calls, then apply the focused structure and cleanup patterns in this skill.
6
6
 
@@ -9,7 +9,7 @@ The demos prove that API combinations run together on the current module. They a
9
9
  | `basic.js` | Sheets, frame animation, drag/pinch/rotate, tween completion | [Scaffolding](patterns.md#the-scaffolding) |
10
10
  | `puzzle.js` | Multi-touch drag, press-to-lift, snapping and tween-back | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
11
11
  | `flappy.js` | Gravity impulse, invisible score/death zones, wrapping parallax | [Tap-to-flap](recipes-action.md#tap-to-flap) |
12
- | `platformer.js` | `solidWith`, `land`, `oneWay`, carried rider, camera follow, multi-touch controls | [Platformer](recipes-action.md#platformer) |
12
+ | `platformer.js` | `solidWith`, `land`, `oneWay`, carried rider, camera follow, multi-touch controls, and the same actions driven by a gamepad | [Platformer](recipes-action.md#platformer) |
13
13
  | `volley.js` | Restitution ball, `land`, collision response, coarse AI timer | [Ball sports](recipes-action.md#ball-sports--breakout-physics) |
14
14
  | `racing.js` | `carMode`, drift, skid marks, checkpoints and lap state | [Top-down racer](recipes-action.md#top-down-racer) |
15
15
  | `cards.js` | Dealing, fanned layout, selection tweens and idle wobble | [Cards and board games](recipes-adventure-effects.md#cards-and-board-games) |
@@ -18,6 +18,7 @@ The demos prove that API combinations run together on the current module. They a
18
18
  | `skate.js` | Endless-runner pooling, carried/uncarried terrain, parallax, emitter and audio hooks | [Endless runner](recipes-action.md#endless-runner) |
19
19
  | `pointclick.js` | Tap-to-walk with `findPath` → `followPath`, hotspots and `ySort` | [Point-and-click](recipes-adventure-effects.md#point--click-adventure) |
20
20
  | `particles.js` | Continuous/burst emitters, sprite target, HUD telemetry | [Particles](recipes-adventure-effects.md#particles) and [debugging](debugging-performance.md) |
21
+ | `worldwrap.js` | `worldWrapX` end to end: seam-aware camera, touch, overlap, solid resolution, swept movement, `wallhit`, and a full-width TileLayer that repeats across the seam | [Circular worlds](api.md#gameview) |
21
22
  | `rhythm.js` | Pooled notes, native velocity, `press` pads, timing and audio feedback | [Rhythm game](recipes-action.md#rhythm-game) |
22
23
  | `camera.js` | Two-axis follow, bounds, zoom, shake, camera effects, parallax and debug HUD | [Camera work](recipes-adventure-effects.md#camera-work) |
23
24
  | `rope.js` | Rope pinned to a sprite, fixed head, tail weight | [Ropes and chains](recipes-adventure-effects.md#ropes-and-chains) |
@@ -34,9 +35,9 @@ The demos prove that API combinations run together on the current module. They a
34
35
  | `timescale.js` | Slow/frozen scene, engine timer versus real-time interval | [Game-clock timers](patterns.md#timers-on-the-game-clock) |
35
36
  | `circles.js` | Rect/circle/rotated-rect solids and `block` versus bilateral `push` | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
36
37
  | `slopes.js` | Rotated-rect ramp normals and restitution supplied by a surface | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
37
- | `plinko.js` | Circular pegs, swept balls and trigger slots | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
38
- | `pool.js` | Bilateral circle momentum, rails, pockets and linear damping | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
39
- | `drum.js` | Analytic circular containment and contact-driven impulses | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
38
+ | `plinko.js` | Circular pegs, swept balls, trigger slots and `solidimpact` audio | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
39
+ | `pool.js` | Bilateral circle momentum, rails, pockets, linear damping and `solidimpact` | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
40
+ | `drum.js` | Analytic circular containment, contact-driven impulses and `solidimpact` | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
40
41
  | `wind.js` | `gravityX` as wind and as top-down constant acceleration | [Shaped solids](patterns.md#shaped-solids-ramps-round-posts-and-bodies-that-push-back--slopesjs-circlesjs-pooljs-drumjs) |
41
42
  | `tilemap.js` | One 120×90 `TileLayer`, visible-cell rendering, cell collision, live edits, A* and HUD | [Tile maps](tilemaps.md) |
42
43
 
@@ -1,6 +1,6 @@
1
1
  # Cross-cutting ti.game patterns
2
2
 
3
- Verified against `ti.game@c216e7f` and the runnable examples under `example/`. These are production-oriented extractions of the API combinations demonstrated upstream; they intentionally omit demo-launcher and back-navigation plumbing.
3
+ Verified against `ti.game@3bea2f4` and the runnable examples under `example/`. These are production-oriented extractions of the API combinations demonstrated upstream; they intentionally omit demo-launcher and back-navigation plumbing.
4
4
 
5
5
  See [examples.md](examples.md) to choose the source demo, [api.md](api.md) for exact members, and the genre recipe files for complete game loops.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Setting up a ti.game project
2
2
 
3
- Installing the module, wiring it into Classic and Alloy projects, organizing art and sound, and the lifecycle work the engine does not do for you. Verified against `ti.game@c216e7f` (2026-08-28).
3
+ Installing the module, wiring it into Classic and Alloy projects, organizing art and sound, and the lifecycle work the engine does not do for you. Verified against `ti.game@3bea2f4` (2026-09-02).
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -44,7 +44,7 @@ Then declare it in `tiapp.xml`. The iOS platform key is `iphone`, not `ios`:
44
44
 
45
45
  Pinning `version` is worth the extra attribute: with two module versions installed side by side, an unpinned entry silently picks the highest one.
46
46
 
47
- **The version string does not identify a feature set — the build date does.** The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20 and **`0.5.0`** on 2026-08-27, and in between each number kept accepting features without moving. So every one of them covers several incompatible builds:
47
+ **The version string does not identify a feature set — the build date does.** The manifest went `0.3.0` → **`0.4.0`** on 2026-08-20, **`0.5.0`** on 2026-08-27 and **`0.6.0`** on 2026-09-02, and in between each number kept accepting features without moving. So every one of them covers several incompatible builds:
48
48
 
49
49
  | A build from | Missing, compared to what this skill documents |
50
50
  | --- | --- |
@@ -61,8 +61,12 @@ Pinning `version` is worth the extra attribute: with two module versions install
61
61
  | `0.5.0` built before `85a723d` | additionally read-only `onWallLeft` / `onWallRight` and the `wallhit` transition event for sprite and TileLayer walls |
62
62
  | `0.5.0` built before `05cb60c` | additionally `wallSlideSpeed`, the native downward-speed cap while pressed against a wall |
63
63
  | `0.5.0` built before `c216e7f` | on Android only, string-row TileLayers with a nested JS `legend` decode as empty because the proxy rejects the ordinary map object; numeric data is unaffected |
64
+ | `0.5.0` built before `d7d471e` (2026-08-28) | additionally the whole gamepad surface: `buttondown`/`buttonup`, `stick`, `trigger`, `gamepadconnected`/`gamepaddisconnected`, and the `gamepads`/`gamepad`/dead-zone properties |
65
+ | `0.5.0` built before `af2e544` (2026-08-29) | additionally circular horizontal worlds: `gameView.worldWrapX` and per-sprite `wrapWorldX` |
66
+ | `0.5.0` built before `80d8f2b` (2026-08-30) | additionally the `solidimpact` event and `impactThreshold` |
67
+ | `0.5.0` built before `9c45cf6` (2026-09-02) — **this includes the artifacts most people have** | no crash fixes: writing `y`/`zIndex` from JS can abort the GL thread with *Comparison method violates its general contract*; a sound whose load failed throws `IllegalStateException` on the next `play()`; passing a mistyped value to `sheet`/`target`/`head`/`tail`/`font` aborts the process in JNI instead of raising a JS error. Also `pinch`/`rotate` fire per motion event instead of ~10 Hz, two-finger rotation can add a whole turn at the ±180° flip, the tap/drag slop ignores `cameraScale`, `cameraX`/`cameraY`/`cameraScale`/`cameraBounds`/`throttle`/`steering` are dropped when passed to a `create*` call, a removed sprite stays retained as the camera's follow target, `collidesWith`/`solidWith` reject a bare string, and the iOS glitch shader goes NaN after roughly 27 minutes |
64
68
 
65
- The newest local release tag is still `0.4.0` (2026-08-20), so tagged artifacts lag one manifest bump plus the HUD and TileLayer work that landed after `0.5.0` was introduced. If a documented call is `undefined`, check the manifest and build commit before hunting for a typo, and rebuild from upstream `main`. More reliable than either is feature detection: read a property **before** writing it (`typeof sprite.hitboxScaleX === 'undefined'`) or probe a method/factory (`typeof Game.createTileLayer === 'function'`). Write-then-read is not proof because Kroll can retain an unknown property on the proxy.
69
+ The newest upstream release is `0.5.0` (2026-09-02), and for once the tag is ahead of the local habit rather than behind it: it sits on `9c45cf6`, so the published zip carries every fix in the row above, while a module built locally from source any time in the previous two weeks reports the same `0.5.0` and does not. Same number, different engine — prefer the release artifact, or rebuild from upstream `main` and let the manifest read `0.6.0`. If a documented call is `undefined`, check the manifest and build commit before hunting for a typo, and rebuild from upstream `main`. More reliable than either is feature detection: read a property **before** writing it (`typeof sprite.hitboxScaleX === 'undefined'`) or probe a method/factory (`typeof Game.createTileLayer === 'function'`). Write-then-read is not proof because Kroll can retain an unknown property on the proxy.
66
70
 
67
71
  Building the module from source:
68
72
 
@@ -1,6 +1,6 @@
1
1
  # Action-game recipes
2
2
 
3
- Production-oriented patterns distilled from the upstream action demos at `ti.game@c216e7f`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads. The wall-contact API is source/README-backed because upstream has not added a wall-jump demo yet.
3
+ Production-oriented patterns distilled from the upstream action demos at `ti.game@3bea2f4`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads. The wall-contact API is source/README-backed because upstream has not added a wall-jump demo yet.
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -1,6 +1,6 @@
1
1
  # Adventure, presentation, and effects recipes
2
2
 
3
- Production-oriented patterns distilled from the upstream non-action demos at `ti.game@c216e7f`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads.
3
+ Production-oriented patterns distilled from the upstream non-action demos at `ti.game@3bea2f4`. Use [examples.md](examples.md) to locate the full runnable demo and [api.md](api.md) for exact defaults and payloads.
4
4
 
5
5
  <!-- TOC-START -->
6
6
  ## Contents
@@ -1,6 +1,6 @@
1
1
  # Missing, partial, and out-of-scope features
2
2
 
3
- Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` and both native implementations. Planned is not shipped; partial means the exact boundary below matters.
3
+ Verified against `ti.game@3bea2f4` (2026-09-02), especially upstream `TODO.md` and both native implementations. Planned is not shipped; partial means the exact boundary below matters.
4
4
 
5
5
  ## Not available today
6
6
 
@@ -8,12 +8,13 @@ Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` a
8
8
  | --- | --- | --- |
9
9
  | `gameView.panTo(x, y, options)` | Planned | Tween an invisible sprite, follow it for the cutscene, then follow the player again |
10
10
  | Full `sprite.parent` transform inheritance | Partial | `attachTo()` supplies position, optional rotation, and inherited opacity. Scale, visibility, flips, tint, hit-test hierarchy, y-sort hierarchy, and a `parent` property do not exist |
11
- | Native joystick/d-pad binding | Planned | Use overlaid Titanium controls that write native motion/input properties on `touchstart`/`touchend`; sibling views support simultaneous touches |
12
- | Gamepad support | Planned | Touch input only unless the app adds its own native integration |
11
+ | Native joystick/d-pad binding (`joystick.bind(sprite)`) | Planned | Use overlaid Titanium controls that write native motion/input properties on `touchstart`/`touchend`; sibling views support simultaneous touches. Upstream intends the same binding to accept a gamepad's left stick, which also does not exist yet |
13
12
  | Complete platformer slopes | Partial | `rotatedRect` geometry, normals, rider carrying, and sliding work. Smooth uphill walking, contact-only surface friction, and connected terrain are not implemented |
14
13
  | Tile animation | Planned TileLayer follow-up | Change ids deliberately or use sprites for the animated cells; there is no per-id native frame cycle |
15
14
  | `raycast()` against TileLayer cells | Planned TileLayer follow-up | Use sprite colliders for ray-visible obstacles or grid/cell queries appropriate to the game |
16
15
  | Tile trigger events (`collision`/`collisionend` for water/lava) | Planned TileLayer follow-up | Use sparse invisible trigger sprites or map the mover/world point to cells on a coarse decision/event path |
16
+ | `solidimpact` from TileLayer cells | Not implemented | Solid cells block, bounce and ground movers but emit no impact event. Put a sprite solid where the hit must be audible |
17
+ | Periodic repetition of `raycast()`, `findPath()`, particles, ropes and skid trails across a `worldWrapX` seam | Not implemented | Those queries and effects see a single linear world. Keep them away from the seam, or resolve the nearest image in JS before calling |
17
18
  | One-call Tiled loader for tilesets and multiple layers | Planned TileLayer follow-up | Parse the exported JSON in app code and create one TileLayer per layer/tileset |
18
19
  | Per-frame animation events | Planned | Split an action into short non-looping animations and react to `animationcomplete`, or use an engine timer aligned to a deliberate cadence |
19
20
  | Ping-pong, repeat/yoyo, random animation offset, per-sprite animation speed | Planned | Author ping-pong frames or re-launch a tween from `complete`; avoid a JS per-frame loop |
@@ -35,6 +36,9 @@ Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` a
35
36
  - Shape-aware solids: circle and rotated-rect geometry, one-way platforms, carried riders, containment, bilateral circle push, two-sided restitution, `gravityX`, and `linearDamping`.
36
37
  - Native wall contacts: `onWallLeft` / `onWallRight`, `wallhit`, TileLayer-wall parity, and `wallSlideSpeed`.
37
38
  - Native particles, ropes, effect/music sound backends, car physics, thrust, drag/pinch/rotate, and multi-touch sprites.
39
+ - Gamepads: normalized `buttondown`/`buttonup` names, throttled `stick`/`trigger`, connect/disconnect events, `gamepads`/`gamepad` snapshots, dead zone and stick-press hysteresis, and release-everything on background or disconnect.
40
+ - Circular horizontal worlds: `gameView.worldWrapX` plus per-sprite `wrapWorldX`, seam-aware camera, rendering, touch, overlap, solid resolution and swept movement, and a full-width TileLayer that repeats across the seam.
41
+ - Discrete physical impacts: the `solidimpact` event with per-receiver `impactThreshold`, compensated closing speed, shared contact point, opposite normals and mixed restitution, for `block`, `contain` and bilateral `push` responses between sprites.
38
42
 
39
43
  ## Deliberately out of scope upstream
40
44
 
@@ -45,6 +49,6 @@ Verified against `ti.game@c216e7f` (2026-08-28), especially upstream `TODO.md` a
45
49
 
46
50
  ## Checking a moving target
47
51
 
48
- The manifest remains `0.5.0` across shape-aware solids, the HUD, and TileLayer, so the version cannot prove a feature exists. Prefer the build commit/date and safe feature probes such as `typeof Game.createTileLayer === 'function'`. Read a property before writing it; Kroll can preserve unknown writes on a proxy.
52
+ The manifest number cannot prove a feature exists: it stayed at `0.5.0` across shape-aware solids, the HUD, TileLayer, gamepads, world wrapping and `solidimpact`, and only moved to `0.6.0` in the commit right after the `0.5.0` release was tagged. A locally built artifact stamped `0.5.0` may therefore be weeks older than the published release of the same number. Prefer the build commit/date and safe feature probes such as `typeof Game.createTileLayer === 'function'`. Read a property before writing it; Kroll can preserve unknown writes on a proxy.
49
53
 
50
54
  Before claiming an item is still absent, re-check upstream `TODO.md`, `README.md`, both platform proxies, and the examples. Source code wins over prose when they disagree.
@@ -1,6 +1,6 @@
1
1
  # Native tile maps
2
2
 
3
- Verified against `ti.game@c216e7f`: upstream `README.md` § Tile maps, `example/tilemap.js`, both `TileLayerProxy` implementations, both native `TileLayer` engines, scene collision, and `Pathfinder`.
3
+ Verified against `ti.game@3bea2f4`: upstream `README.md` § Tile maps, `example/tilemap.js`, both `TileLayerProxy` implementations, both native `TileLayer` engines, scene collision, and `Pathfinder`.
4
4
 
5
5
  Use `Game.createTileLayer()` for a large or mostly static grid. The engine stores the whole map but renders only cells inside the camera, batches the layer through one sheet, checks only collision cells under a mover, and feeds solid cells into `findPath`. A small map whose tiles need independent sprite behavior can still use sprites; `topdown.js` is a 16×12 example of that older pattern.
6
6
 
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: ti-reuse-first
3
+ description: "Use before anything new exists in a Titanium or Alloy project — 'hazme un panel de avisos', 'necesito otra pantalla', 'hazlo igual que el de Index', 'voy a copiar paneles/derechos como base', '¿dónde pongo esta función?'. A search order stopping at the first rung that holds: must this exist, is it already in app/, does a Ti.* API, the platform, an installed module or a PurgeTSS class cover it, can it be one line, and only then the smallest thing that works — copied exactly, without guards nobody asked for. AUTO-DETECT: if tiapp.xml exists, invoke BEFORE creating any controller, view, Widget, app/lib/ module, event or wrapper, or copying a file as a template: it answers whether the thing should exist and what already does it; ti-expert answers how to build what survives. Also audits app/ for duplicated controllers, single-listener events, wrappers around one call, and layout JS that TSS resolves — 'esto ya lo tengo en otro lado', 'simplifícalo', DRY, KISS. Not a licence to skip a module five call sites need."
4
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash(git *), Bash(node *)
5
+ ---
6
+
7
+ # Reuse before you write
8
+
9
+ ## Overview
10
+
11
+ Most of the avoidable damage in a mature Alloy app is not wrong code. It is code that should never have been written: the fourth panel that repeats the first three, the event with one listener, the wrapper around one call, the helper that restates a `Ti.*` API. It compiles, it passes review, and it is permanent.
12
+
13
+ This skill supplies the missing question. Every other Titanium skill answers *how do I build this well*; this one asks *should this exist at all, and if so, does something already do it*. Run it first — its answer changes what the other skills are asked to build.
14
+
15
+ The method is a search order with early exit: seven rungs, cheapest first, stop at the first one that holds. It is a search order, not a prohibition. Climbing all seven honestly and landing on "write the module" is a correct outcome — see [When adding structure is the right answer](references/the-ladder.md#when-adding-structure-is-the-right-answer). A bias against structure is the same failure as a bias toward it, pointed the other way.
16
+
17
+ ## When to use
18
+
19
+ Use it before:
20
+
21
+ - Creating any controller/view/style trio, Widget, or `app/lib/` module.
22
+ - Copying an existing controller, view, or panel as a template.
23
+ - Introducing an event, a wrapper, a service layer, a registry, or a factory.
24
+ - Answering "where should this live" — the answer is often "nowhere new".
25
+ - Reviewing a design that adds a layer, or a diff whose file count outgrew its intent.
26
+
27
+ Use it in diagnostic mode when someone says a codebase feels repetitive, when a bug had to be fixed in four places, or when planning a refactor.
28
+
29
+ Do not use it to choose between two designs that both already exist and both work — that is `ti-expert`'s decision matrix. Do not use it to argue against a module that several call sites genuinely need. And do not let it stall a one-line fix: rung 6 exists so trivial work stays trivial.
30
+
31
+ ## Required workflow
32
+
33
+ This file is an index. Open the reference that governs the decision before recommending or writing anything.
34
+
35
+ | Decision | Required reading |
36
+ | --- | --- |
37
+ | Anything new — first pass, always | [The ladder](references/the-ladder.md) |
38
+ | A new panel, screen, dialog, or a controller/view/style trio; Widget vs `<Require>` vs a shared base | [Alloy reuse](references/alloy-reuse.md) |
39
+ | An event, wrapper, service layer, registry, factory, or any new layer between two pieces of code | [Indirection](references/indirection.md) |
40
+ | Repeating class strings in XML, or reaching for a new TSS block | [PurgeTSS reuse](references/purgetss-reuse.md) |
41
+ | Auditing an existing `app/` for duplication and dead structure | [Diagnostics](references/diagnostics.md) |
42
+ | Whether the thing you would build already exists as a Titanium API | Verify the exact signature with the `ti-api` skill — never from memory |
43
+
44
+ Support each recommendation with `[source: references/<file>.md]`. If a claim cannot be verified from the loaded references or from the inspected project, prefix it with `FROM_MEMORY (unverified):` and do not present it as settled. This matters more here than in most skills: the whole method is *look before you build*, and a skill that answers from memory has already skipped its own first step.
45
+
46
+ ## The ladder
47
+
48
+ Ask these in order and stop at the first rung that holds. Full detail, with the Titanium specifics for each, is in [The ladder](references/the-ladder.md).
49
+
50
+ | # | Question | If it holds |
51
+ | --- | --- | --- |
52
+ | 0 | Does this need to exist? | Delete the requirement, not the code. State what you skipped and why. |
53
+ | 1 | Is it already in this codebase? | Reuse or parameterize it. `grep` before you conclude it is not. |
54
+ | 2 | Does a `Ti.*` API or Alloy builtin cover it? | Use it. Verify the signature with `ti-api`. |
55
+ | 3 | Does the platform own this workflow? | Use the native surface. Trust, permission and payment flows stay native. |
56
+ | 4 | Does a module already in `tiapp.xml` or `package.json` cover it? | Use it. New dependencies need an argument. |
57
+ | 5 | Can it be one line at the call site? | Write the line. A direct reference beats a mechanism. |
58
+ | 6 | None of the above. | Build the smallest thing that works, where the next person will look. |
59
+
60
+ Two rules keep the ladder honest:
61
+
62
+ **Rung 1 is a search, not a recollection.** "I do not think we have that" is not an answer to rung 1. Run the search — `grep`, `ls app/lib/`, `ls app/widgets/` — and report what you found. The command is cheap; the duplicate is permanent.
63
+
64
+ **Classic projects climb the same ladder.** Only the paths change: `Resources/` instead of `app/`, CommonJS modules instead of `app/lib/`, and rungs 2 through 5 are identical because they are about Titanium and the platform, not about Alloy. Rung 1's Alloy-specific answers — `<Require>`, Widgets, `$.args` — have no Classic equivalent; there the reuse mechanism is a CommonJS module that returns a configured view. Detect which kind of project you are in before quoting a path.
65
+
66
+ **Copy the file that solves your problem, not the one whose name sounds close.** A full-screen legal-text window and a three-line confirmation overlay share a folder and nothing else. Before adopting any file as a template, open it and check its *shape* matches: how much screen it takes, how it opens and closes, what it owns. See [the worked example](references/alloy-reuse.md#worked-example-1-the-wrong-template).
67
+
68
+ ## Diagnostic mode
69
+
70
+ Run the ladder backwards over code that already exists. The sweep — greps for near-identical controllers, single-listener events, wrappers around one call, and JS that recomputes what TSS resolves — is in [Diagnostics](references/diagnostics.md).
71
+
72
+ Report what you measure, with the command that measured it: *"14 of 18 panel controllers define their own `mostrarPanel`"* is a finding. *"There is a lot of duplication in the panels"* is an impression. The first one survives a disagreement; the second one starts an argument nobody can settle.
73
+
74
+ Duplication is evidence, not a verdict. Three copies that are drifting apart on purpose — because they are genuinely three different things — are cheaper than one abstraction with three flags. Say what the consolidation would cost as well as what it would save, then let the human decide.
75
+
76
+ ## Reference guides
77
+
78
+ - [The ladder](references/the-ladder.md) — the seven rungs, each translated into Titanium and Alloy decisions, plus when adding structure is the right answer.
79
+ - [Alloy reuse](references/alloy-reuse.md) — Widget vs `<Require>` vs a shared base vs a new trio; creating a controller on demand instead of embedding one per window; the measured panel case.
80
+ - [Indirection](references/indirection.md) — events vs direct calls, wrappers, service layers, and how to tell a boundary from a detour.
81
+ - [PurgeTSS reuse](references/purgetss-reuse.md) — custom rules and `apply` in `config.cjs` instead of repeating class strings; what belongs in `_app.tss` and what never goes in `app.tss`.
82
+ - [Diagnostics](references/diagnostics.md) — the sweep for duplication and dead structure in an existing `app/`, with the exact commands.
83
+
84
+ ## Related skills
85
+
86
+ - `ti-expert` — architecture, tiers, and the patterns worth introducing *after* this skill has established that something must be built.
87
+ - `purgetss` — the class contract itself; verify every utility class there.
88
+ - `ti-api` — the exact signature of any `Ti.*` API this skill sends you to reuse.
@@ -0,0 +1,127 @@
1
+ # Alloy reuse
2
+
3
+ Alloy has real mechanisms for reusing XML and JS together — `<Require>` and Widgets — and `app/lib/` is not the only place shared code can live. This reference is about choosing between them, and about the decision that comes before all of them: whether the new screen needs to be a new file at all.
4
+
5
+ ## The four exits
6
+
7
+ When something has to appear in more than one place, there are four ways out, and the reflex — copy the trio, or embed one instance everywhere — is usually the worst of them.
8
+
9
+ | Exit | What it is | When it is right |
10
+ | --- | --- | --- |
11
+ | **Parameterize what exists** | Pass `$.args` to a controller that already does 90% of it | The differences are text, an icon, a callback, or a visible/hidden branch |
12
+ | **`<Require>`** | Compose an app-local controller inside a view | Shares the host's lifecycle, styling and domain; no independent contract |
13
+ | **Widget** | A packaged component with `widget.json`, its own styles and assets | Owns a public API, its own state and cleanup, and could move to another project |
14
+ | **New trio** | A fresh controller/view/style | Genuinely a different screen, not a variation of one |
15
+
16
+ `ti-expert`'s [alloy-structure.md § "When to use widget vs require"](../../ti-expert/references/alloy-structure.md) has the Widget-vs-`<Require>` table in full; read it rather than deciding from memory. What it does not cover, and what this reference adds, is the first row — parameterizing rather than adding anything — and the question of *where the instance lives*, below.
17
+
18
+ Usage count alone does not decide. Three call sites for a thing with no state and no lifecycle may still be three lines of `$.args`; one call site for something with timers and cleanup may still deserve a Widget.
19
+
20
+ ## Where the instance lives: on demand, not one per window
21
+
22
+ A component needed in four windows does not need four instances. The reflex — a `<Require>` in each window's XML, a listener in each controller, and a `destruir()` each one has to remember to call — pays the cost of four view hierarchies, four listener registrations, and four chances to forget the cleanup, in exchange for saving a `createController` call.
23
+
24
+ The alternative is to build it when it is needed, on whatever window is visible, and destroy it on close:
25
+
26
+ ```js
27
+ // In the controller that knows which window is on screen.
28
+ Alloy.createController('paneles/avisoPush', opciones).mostrar()
29
+ ```
30
+
31
+ ```js
32
+ // In the component: it attaches to the window it was handed, and cleans itself up.
33
+ const ventana = $.args.ventana
34
+
35
+ function mostrar() {
36
+ if (!ventana) { return false }
37
+ ventana.add($.getView())
38
+ $.getView().show()
39
+ $.animarVista.open($.getView())
40
+ return true
41
+ }
42
+
43
+ function cerrar(despues) {
44
+ $.animarVista.close($.getView(), () => {
45
+ $.getView().hide()
46
+ ventana.remove($.getView())
47
+ $.destroy()
48
+ if (typeof despues === 'function') { despues() }
49
+ })
50
+ }
51
+ ```
52
+
53
+ The trade is real and worth stating: creating on demand costs a controller construction at the moment of use, which is visible if the component appears many times per second. That is the case for a list row, and it is why `ListView` templates exist. It is not the case for an alert, a confirmation, or a notice that arrives occasionally — there, a resident instance is memory held for an event that mostly does not happen.
54
+
55
+ The test: **how often does this appear, and does it need to survive being closed?** Rarely and no → create on demand. Constantly, or it holds state between appearances → resident.
56
+
57
+ ## When a family of screens has already formed
58
+
59
+ The interesting case is not one duplicate. It is the fifteenth, when a house pattern has emerged without anyone deciding on it, and each new screen re-types it.
60
+
61
+ Measure before proposing anything. In one shipping Alloy app, over the 18 modal panels in `app/controllers/paneles/` (excluding the shared header component):
62
+
63
+ ```bash
64
+ # Denominator: panel controllers, excluding the shared component.
65
+ PANELS=$(ls app/controllers/paneles/*.js | grep -v panelHeader)
66
+ for p in "animarVista.open" "animarVista.close" "opacoClick" '$.getView().hide()' "mostrarPanel"; do
67
+ printf "%-22s %s/%s\n" "$p" "$(grep -lF "$p" $PANELS | wc -l | tr -d ' ')" "$(echo "$PANELS" | wc -l | tr -d ' ')"
68
+ done
69
+ ```
70
+
71
+ | Repeated in every panel | Count |
72
+ | --- | --- |
73
+ | `$.animarVista.open($.getView())` | 16 / 18 |
74
+ | `$.animarVista.close($.getView(), …)` | 15 / 18 |
75
+ | `opacoClick` — dismiss on background tap | 15 / 18 |
76
+ | `$.getView().hide()` / `.show()` | 15 / 18 |
77
+ | An exported `mostrarPanel` | 14 / 18 |
78
+
79
+ And in the 18 corresponding views, counted as exact `class` attributes rather than substrings: 16 carry an identical `<Animation>` element, 14 an identical full-screen scrim, and 9 an identical zoom-in card.
80
+
81
+ That is a house pattern with no home. Every new panel re-types show, close, dismiss-on-background and animation, and a change to any of them is a fifteen-file edit that will be applied to twelve.
82
+
83
+ **What the numbers do not decide is the fix.** They establish the cost; the shape of the consolidation is a judgment call, and there are three defensible answers:
84
+
85
+ - **A Widget** (`app/widgets/panelBase/`) exposing `open()`, `close()` and a content slot. Strongest boundary, real public API, portable to the next project. Costs the most to introduce and forces every panel through one contract — which is wrong if the panels are drifting apart deliberately.
86
+ - **A shared behavior module** (`app/lib/panel.js`) that each controller hands `$` to, keeping the XML per-panel and lifting only the repeated JS. Smallest change, preserves per-panel layout freedom, does not unify the duplicated XML.
87
+ - **Leave it, and only stop the bleeding** — a `config.cjs` custom rule for the two repeated class strings, so at least new panels stop re-typing the appearance. Correct when the panels are genuinely diverging and a shared base would become a flag farm.
88
+
89
+ Present the measurement and the three options with what each costs. The choice of which is right depends on where the app is going, and that is the human's to make — a consolidation imposed on screens that were about to diverge is worse than the duplication it removed.
90
+
91
+ ## Worked example 1: the wrong template
92
+
93
+ **The task**: show a three-line notice over whatever window is visible when a push notification arrives with the app open.
94
+
95
+ **What happened**: `paneles/derechos` was copied as the template — a full-screen modal window built to scroll legal text. The result filled the screen for a three-line message.
96
+
97
+ **What was already there**: `paneles/tombolaSalir` — a scrim with a centered card, two buttons, dismiss on background tap. The same shape as the task. It was one folder listing away.
98
+
99
+ **The error was not in the code**; the copied file was perfectly good code. The error was choosing the template by proximity — same folder, similar name — instead of by shape. Both files are "panels"; only one of them is a small overlay.
100
+
101
+ **The check that would have caught it**: before copying any file as a template, open it and answer three questions. *How much screen does it take? How does it open and close? What does it own?* If any answer differs from the task, keep looking — the right file is usually in the same folder.
102
+
103
+ ## Worked example 2: four instances of a thing that appears once
104
+
105
+ **The task**: the same notice, reachable from four different windows.
106
+
107
+ **What was built first**: a `<Require>` in each of the four window XMLs, an `Alloy.Events` listener in each of the four controllers, and a `destruir()` each window had to remember to call. Four copies of a view hierarchy, permanently resident, for a notice that arrives occasionally.
108
+
109
+ **What replaced it**: one `Alloy.createController('paneles/avisoPush', opciones).mostrar()` in the controller that already tracks which window is visible, with the panel attaching itself to the window it is handed and calling `$.destroy()` on close. The four `<Require>` elements, the four listeners and the `destruir()` contract all went away.
110
+
111
+ **The tell**: *am I creating N copies of something that is only ever used one at a time?* If the answer is yes, the instance should be created at the moment of use. The question to ask out loud is the one that unlocked this: **can this be loaded dynamically?**
112
+
113
+ ## Worked example 3: the right template, plus inventions
114
+
115
+ **The task**: a panel inside the Tómbola window, behaving like the one already working in `index` — the same handling the Estilos and Ruleta panels use. The instruction named the reference implementation explicitly.
116
+
117
+ **What was built**: the reference behavior, plus additions nobody asked for. Touch events were intercepted, `touchEnabled` was toggled on and off around the transition, and an `animating` flag was introduced that refused to close the panel until the open callback fired. None of the reference panels carry any of that.
118
+
119
+ **What happened**: the flag deadlocked. When the open callback did not arrive, `animating` stayed `true` and the panel could no longer be closed. The user could not open and close it repeatedly, and could not keep drawing cards. Each attempt to fix the symptom added another guard on top of the first — a patch on a patch, all of it defending against a problem that had never been observed.
120
+
121
+ **What replaced it**: the reference behavior, unedited. Animate, hide the whole container on close, leave the events connected. The `touchEnabled` toggling and the `animating` lock were deleted, not repaired.
122
+
123
+ **The admission afterwards was the diagnosis**: *"intenté prevenir toques durante la animación sin evidencia de que hiciera falta"*. Three panels in the same app had run for months without that guard. The evidence that it was unnecessary was already in the codebase, in the very files that had been named as the model.
124
+
125
+ **The rule**: when the instruction is *do it like the one that already works*, reuse means copying the behavior, not copying it and improving it. Additions to a reference implementation need the same argument any new code needs — a real, observed failure. An input you are guarding against because it *might* happen is rung 0 work: the requirement does not exist yet, so neither should the guard.
126
+
127
+ **The tell**: *does the file I was told to copy do this?* If it does not, and no bug report says it should, do not add it. And if a symptom appears after an addition of yours, the first move is to remove the addition — not to guard it.
@@ -0,0 +1,137 @@
1
+ # Diagnostics
2
+
3
+ Running the ladder backwards over code that already exists. Each sweep below is a command, a reading, and — the part that matters — what would make the finding wrong.
4
+
5
+ Two rules before starting:
6
+
7
+ **Report the command with the number.** *"14 of 18 panel controllers define their own `mostrarPanel`"* is a finding someone can check and disagree with. *"There is a lot of duplication in the panels"* is an impression, and impressions produce arguments instead of decisions.
8
+
9
+ **Duplication is evidence, not a verdict.** Three copies drifting apart on purpose are cheaper than one abstraction with three flags. Every finding here needs its second half: what the consolidation would cost, not only what it would save. Then the human decides — this sweep produces a report, not a refactor.
10
+
11
+ ## 1. Near-identical controllers
12
+
13
+ Look for a house pattern that formed without anyone deciding on it: the same function names, the same open/close dance, the same event wiring, re-typed per screen.
14
+
15
+ ```bash
16
+ # Which function names recur across a family of controllers?
17
+ # find, not **/ — globstar is off in the bash 3.2 that ships with macOS,
18
+ # where **/*.js silently collapses to */*.js and misses everything deeper.
19
+ find app/controllers -name '*.js' -exec grep -ho "^function [a-zA-Z0-9_]*" {} + |
20
+ sort | uniq -c | sort -rn | head -20
21
+
22
+ # How many files in a family share one construct? Exclude the shared pieces that
23
+ # live in the same folder but are not members — they inflate the denominator.
24
+ FAM=$(ls app/controllers/paneles/*.js | grep -v panelHeader)
25
+ for p in "mostrarPanel" "opacoClick" "animarVista.open" '$.getView().hide()'; do
26
+ printf "%-22s %s/%s\n" "$p" "$(grep -lF "$p" $FAM | wc -l | tr -d ' ')" "$(echo "$FAM" | wc -l | tr -d ' ')"
27
+ done
28
+
29
+ # Rough size clustering — near-identical files tend to have near-identical line counts.
30
+ wc -l app/controllers/paneles/*.js | sort -n
31
+ ```
32
+
33
+ **Reading it:** a construct in most of a family is a house pattern with no home. Every new member re-types it, and a change to it is an N-file edit that will be applied to N-2.
34
+
35
+ **What makes it wrong:** the family is not a family. Screens that share a folder are not necessarily variations of one thing, and a shared base imposed on genuinely different screens becomes a flag farm. Check that the members do the same *kind* of job before proposing to unify them.
36
+
37
+ **Where to go:** [Alloy reuse § when a family of screens has already formed](alloy-reuse.md#when-a-family-of-screens-has-already-formed) — it lays out the three defensible fixes and what each costs.
38
+
39
+ ## 2. Events with a single listener
40
+
41
+ ```bash
42
+ # Every event name that is fired, and every one that is listened for.
43
+ grep -rhno "Alloy\.Events\.trigger(['\"][^'\"]*" app/ | sed "s/.*['\"]//" | sort | uniq -c | sort -rn
44
+ grep -rhno "Alloy\.Events\.on(['\"][^'\"]*" app/ | sed "s/.*['\"]//" | sort | uniq -c | sort -rn
45
+
46
+ # Same, for the leakier cross-context bus.
47
+ grep -rn "Ti\.App\.fireEvent\|Ti\.App\.addEventListener" app/
48
+ ```
49
+
50
+ **Reading it:** an event with one trigger and one listener, where the two modules can see each other, is a function call with a name to keep in sync and a listener to clean up. Replace it with a direct call or a callback in `$.args`.
51
+
52
+ An event fired but never listened for is dead. An event listened for but never fired is either dead or fired from native code — check before deleting.
53
+
54
+ **What makes it wrong:** the listener set is genuinely open (a theme finishing its download, a session expiring), or the sender is a lower layer that must not know about the receiver. Both are correct uses; the count alone cannot tell them apart. Read the two modules before concluding.
55
+
56
+ **Where to go:** [Indirection § events versus a direct call](indirection.md#events-versus-a-direct-call).
57
+
58
+ ## 3. Wrappers that wrap nothing
59
+
60
+ ```bash
61
+ # One-line functions that forward their arguments unchanged.
62
+ grep -rn -B1 -A3 "^function [a-zA-Z0-9_]*(" app/lib/*.js |
63
+ grep -A1 "^function" | grep -E "return (Ti|Modules|Alloy)\.[A-Za-z.]+\("
64
+
65
+ # How many callers does each exported function actually have?
66
+ grep -ho "^exports\.[a-zA-Z0-9_]*" app/lib/*.js | sed 's/exports\.//' | sort -u |
67
+ while read f; do printf "%-28s %s\n" "$f" "$(grep -rl "\.$f(" app/ | wc -l | tr -d ' ')"; done | sort -k2 -n
68
+ ```
69
+
70
+ **Reading it:** a function that forwards one call unchanged, from one site, adding no normalization, no error handling and no default, is a longer name for the thing it calls.
71
+
72
+ **What makes it wrong:** it is a vendor seam. A wrapper whose whole job is to be the one place that changes when the module does is doing that job even while it is trivial. Ask whether the module's API is spreading through controllers — if it is, the wrapper is earning its keep in advance.
73
+
74
+ **Where to go:** [Indirection § wrappers](indirection.md#wrappers).
75
+
76
+ ## 4. Layout computed in JavaScript
77
+
78
+ ```bash
79
+ # Measuring and repositioning after the draw.
80
+ grep -rn "postlayout\|\.rect\b\|\.size\.width\|\.size\.height" app/controllers/ app/lib/
81
+
82
+ # Positions and sizes assigned from JS rather than declared in TSS.
83
+ grep -rn "applyProperties({[^}]*\(top\|left\|right\|bottom\|width\|height\):" app/controllers/
84
+ ```
85
+
86
+ **Reading it:** a `postlayout` handler that measures a window and repositions its children runs after the first draw — the content visibly jumps — and re-runs on every layout pass. TSS conditionals (`[formFactor=tablet]`, `[platform=ios]`, `[if=…]`) resolve once, before anything is drawn.
87
+
88
+ **What makes it wrong:** the handler reacts to a real orientation change, or the value genuinely is not knowable at build time. When a decision must be made before the first draw, `Ti.Platform.displayCaps` answers it without waiting for a layout pass.
89
+
90
+ **Where to go:** [Indirection § layout logic in JavaScript](indirection.md#layout-logic-in-javascript).
91
+
92
+ ## 5. Repeated class strings in the views
93
+
94
+ ```bash
95
+ # The most-repeated class attributes in the project.
96
+ grep -rho 'class="[^"]*"' app/views/ | sort | uniq -c | sort -rn | head -20
97
+ ```
98
+
99
+ **Reading it:** the same combination in several views, meant to stay identical, is a name waiting to be given — an `apply` rule in `purgetss/config.cjs`.
100
+
101
+ **What makes it wrong:** the agreement is coincidental. Two screens with the same padding today for unrelated reasons will need different padding tomorrow, and a shared class couples things that were never related.
102
+
103
+ **Where to go:** [PurgeTSS reuse § when a repeated string has earned a name](purgetss-reuse.md#when-a-repeated-string-has-earned-a-name).
104
+
105
+ ## 6. Structure nobody reaches
106
+
107
+ ```bash
108
+ # Exported functions with no caller anywhere in app/.
109
+ grep -ho "^exports\.[a-zA-Z0-9_]*" app/lib/*.js | sed 's/exports\.//' | sort -u |
110
+ while read f; do
111
+ [ "$(grep -rl "\.$f(" app/ | grep -v "app/lib" | wc -l | tr -d ' ')" = "0" ] && echo "no external caller: $f"
112
+ done
113
+
114
+ # Config keys nobody reads, and modules declared but never required.
115
+ grep -o '"[a-zA-Z0-9_]*":' app/config.json | tr -d '":' | sort -u |
116
+ while read k; do
117
+ [ "$(grep -rl "Alloy\.CFG\.$k" app/ | wc -l | tr -d ' ')" = "0" ] && echo "unread config key: $k"
118
+ done
119
+ grep -o "<module[^>]*>[^<]*</module>" tiapp.xml | sed 's/.*>\(.*\)<.*/\1/' |
120
+ while read m; do
121
+ [ "$(grep -rl "require('$m')\|require(\"$m\")" app/ | wc -l | tr -d ' ')" = "0" ] && echo "module never required in JS: $m"
122
+ done
123
+ ```
124
+
125
+ **Reading it:** each of these is rung 0 in hindsight — something built for a caller that never arrived.
126
+
127
+ **What makes it wrong, and this one bites:** a `grep` for a caller finds only the callers spelled that way. A function reached through a dynamic name, from a Widget, from `alloy.js`, from a native module, or from a project file outside `app/` will look dead and will not be. Modules in particular are frequently declared for their build-time side effects and never `require`d in JS — reached instead from a `module=` attribute in a view, or simply linked into the build. That is normal, not dead. A module declared once per platform also shows up twice; pipe through `sort -u` before reading the list. **Never delete on the strength of this sweep alone.** Report the candidate, say how it was found, and let someone who knows the history confirm.
128
+
129
+ ## Reporting
130
+
131
+ Group findings by what they cost, not by how many there are:
132
+
133
+ 1. **Costs a bug soon** — one behavior in N places that must agree and are already drifting. Cite the files and how they differ.
134
+ 2. **Costs on every read** — a detour that always resolves to one place; a wrapper around one call; an event with one listener.
135
+ 3. **Costs nothing yet** — a house pattern with no home, worth consolidating when the next member is added rather than today.
136
+
137
+ For each, give the measurement, the command, the proposed change, **and what it would cost** — including the case for leaving it alone. Then stop. A sweep that ends in an unrequested refactor has answered a question nobody asked, and the diff will be judged against a request that does not exist.
@@ -0,0 +1,89 @@
1
+ # Indirection
2
+
3
+ Every layer between a caller and the work it wants done has to earn its place. This reference is about telling the ones that do from the ones that do not — and the distinction is not "how many layers" but whether each one is a **boundary** (a place where the two sides genuinely should not know each other, or where the implementation could plausibly change) or a **detour** (a hop that always resolves to the same destination).
4
+
5
+ Both look identical in a diff. Only one of them pays for itself.
6
+
7
+ ## Events versus a direct call
8
+
9
+ An event bus is the right tool when the sender must not know the receiver: several listeners, an unknown number of them, or a genuine layering boundary the sender should not reach across. When there is exactly one listener and the sender can already see it, an event is a function call with a name to keep in sync, a registration to perform, and a leak to clean up.
10
+
11
+ ```js
12
+ // A lib that has no business knowing about windows announces an intention.
13
+ // Somewhere else, exactly one controller is listening.
14
+ Alloy.Events.trigger('abrirTienda')
15
+ ```
16
+
17
+ ```js
18
+ // The controller that already owns abrirTienda() hands it over as a value.
19
+ if (esTemaNuevo) {
20
+ opciones.alAceptar = abrirTienda
21
+ }
22
+ Alloy.createController('paneles/avisoPush', opciones).mostrar()
23
+ ```
24
+
25
+ The second version deletes the event name, the listener, and the cleanup. It also fixes a layering problem the event was hiding: the module that handles push registration had to know that "open the store" was a thing that could happen. With a callback passed in, it does not — it takes a function and calls it, and the decision about what that function is stays in the controller that owns the navigation.
26
+
27
+ **Decision table:**
28
+
29
+ | Situation | Use |
30
+ | --- | --- |
31
+ | One known listener, caller can see it | Direct call or a callback in `$.args` |
32
+ | Parent needs to hear from a child controller it created | The child's exported function, or a callback the parent passed down |
33
+ | Several listeners, or the set is genuinely open | `Alloy.Events` (a `Backbone.Events` clone) |
34
+ | Sender must not know the receiver exists — a real seam | `Alloy.Events` |
35
+ | Cross-context, JS to native or between contexts | `Ti.App.fireEvent` — the last resort, and only then |
36
+
37
+ `ti-expert`'s decision matrix answers "`Ti.App.fireEvent` or EventBus?" with "always EventBus", and that is correct as far as it goes — it is comparing two event mechanisms, and `Ti.App.fireEvent` is the leakier one. It is not an argument that the interaction needs an event at all. Ask this question first; ask `ti-expert`'s second.
38
+
39
+ **The test:** *if I delete the event and call the function directly, what breaks?* If the answer is "nothing, but it feels less decoupled", the coupling was imaginary and the event was the cost of imagining it.
40
+
41
+ ## Wrappers
42
+
43
+ A wrapper around a native module is a real pattern with a real purpose: it is the one place that changes when the vendor does, and it keeps the module's API from spreading through the app.
44
+
45
+ It stops being that when it forwards one call, unchanged, from one call site.
46
+
47
+ ```js
48
+ // A boundary: normalizes two platform APIs, owns the error contract, one place to change.
49
+ function comprar(sku) { … }
50
+
51
+ // A detour: this is Ti.Media.vibrate with a longer name.
52
+ function vibrar(patron) {
53
+ Ti.Media.vibrate(patron)
54
+ }
55
+ ```
56
+
57
+ Signs a wrapper is a detour: it has one function; that function forwards its arguments unchanged; it has one call site; it adds no error handling, no normalization, and no default. Signs it is a boundary: it reconciles iOS and Android behavior; it owns retries, logging or error translation; it is called from several places; or it is the seam a test substitutes.
58
+
59
+ The honest version of "we might swap this vendor later" is: write the wrapper when the swap becomes plausible, not when it becomes imaginable. The refactor from direct calls to a wrapper is mechanical and a `grep` finds every site; the cost of carrying an unnecessary layer is paid every time someone reads the code.
60
+
61
+ ## Service layers and registries
62
+
63
+ `ti-expert` describes three architectural tiers and a set of patterns — Repository, Service layer, Factory, ServiceRegistry with dependency injection — and its matrix says a controller over 100 lines should be extracted to Tier 2. Those tiers are real and the guidance is sound *for apps that are at that tier*. The failure mode is applying a tier's machinery to an app that is not there yet, because the pattern is available rather than because the app needs it.
64
+
65
+ The line-count rule is a smell detector, not a instruction. A 140-line controller that does one screen's work, reads top to bottom, and shares nothing with any other screen is fine. A 90-line controller that repeats what three other controllers do is already a problem. Split when there is a second consumer, a real seam, or something worth testing on its own — not when a number is exceeded.
66
+
67
+ Before introducing a registry or dependency injection, answer: how many services are there, and how many of them have more than one implementation? Injection buys the ability to substitute. If nothing is ever substituted — not in tests, not per platform, not per environment — it has been bought and not used.
68
+
69
+ ## Layout logic in JavaScript
70
+
71
+ This is the same failure in a different costume: JavaScript recomputing what the style system resolves declaratively.
72
+
73
+ A `postlayout` handler that measures a window and repositions its children runs after the first draw, so the content visibly jumps; it re-runs on every layout pass; and each device rule it encodes is a hand-written `if` that will eventually miss a case. TSS conditionals — `[formFactor=tablet]`, `[formFactor=handheld]`, `[platform=ios]`, `[if=…]` — resolve once, before anything is drawn, and cost nothing at runtime.
74
+
75
+ The same applies to positioning by arithmetic. Anchoring a button with `right: 178` works exactly once; a horizontal container with `height: Ti.UI.SIZE`, where `left` is the gap from the previous sibling, keeps working when the text, the width or the language changes.
76
+
77
+ **The test:** *am I adding up widths in my head to pick a number?* If so, the layout system is being asked to accept a result it could have computed. Reserve `postlayout` for reacting to a real orientation change, and use `Ti.Platform.displayCaps` when a decision must be made before anything is drawn.
78
+
79
+ ## When indirection is the right call
80
+
81
+ Do not read this reference as an argument for flat code. These layers are worth their cost, and refusing them is the same error inverted:
82
+
83
+ - **A seam with two implementations today** — per-platform behavior, a test double, a mock server. Substitution that actually happens is what injection is for.
84
+ - **A repeated integration.** Five controllers each with their own `HTTPClient`, timeout and error handling is one bug in five places. The module that unifies them is not indirection; it is the fix.
85
+ - **An event with an open listener set.** A theme finishing its download, a session expiring — the publisher genuinely cannot know who cares, and the set grows. That is a bus, correctly used.
86
+ - **A wrapper at a vendor boundary.** When the native module's API is spreading through controllers, one wrapper is cheaper than the eventual migration.
87
+ - **Something with its own lifecycle** — state, timers, listeners, cleanup that belong together. One owner beats scattering them across the controllers that happen to use it.
88
+
89
+ State which of these applies when you propose a layer. "It is a seam: iOS and Android disagree here, and the wrapper is where they are reconciled" is a case anyone can check. "For decoupling" is not.