@tdk-landscape/tdk-cli-core 1.3.50 → 1.3.52

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 (92) hide show
  1. package/.github/workflows/ci.yml +13 -0
  2. package/.github/workflows/release-binaries.yml +76 -1
  3. package/CHANGELOG.md +50 -0
  4. package/README.md +54 -17
  5. package/cli/README.md +16 -43
  6. package/cli/biome.json +0 -4
  7. package/cli/dist/commands/help.d.ts.map +1 -1
  8. package/cli/dist/commands/help.js +22 -39
  9. package/cli/dist/commands/help.js.map +1 -1
  10. package/cli/dist/commands/project.d.ts +6 -0
  11. package/cli/dist/commands/project.d.ts.map +1 -1
  12. package/cli/dist/commands/project.js +16 -0
  13. package/cli/dist/commands/project.js.map +1 -1
  14. package/cli/dist/commands/up.d.ts.map +1 -1
  15. package/cli/dist/commands/up.js +5 -0
  16. package/cli/dist/commands/up.js.map +1 -1
  17. package/cli/dist/commands/upgrade.d.ts +3 -0
  18. package/cli/dist/commands/upgrade.d.ts.map +1 -1
  19. package/cli/dist/commands/upgrade.js +43 -1
  20. package/cli/dist/commands/upgrade.js.map +1 -1
  21. package/cli/dist/utils/constants.d.ts +1 -1
  22. package/cli/dist/utils/constants.d.ts.map +1 -1
  23. package/cli/dist/utils/constants.js +1 -1
  24. package/cli/dist/utils/constants.js.map +1 -1
  25. package/cli/dist/utils/doctor-runtime.js +5 -5
  26. package/cli/dist/utils/doctor-runtime.js.map +1 -1
  27. package/cli/dist/utils/prompt.d.ts.map +1 -1
  28. package/cli/dist/utils/prompt.js +6 -1
  29. package/cli/dist/utils/prompt.js.map +1 -1
  30. package/cli/dist/utils/service-urls.js +1 -1
  31. package/cli/dist/utils/service-urls.js.map +1 -1
  32. package/cli/package.json +2 -1
  33. package/cli/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +2 -2
  34. package/cli/src/commands/__tests__/doctor.test.ts +8 -8
  35. package/cli/src/commands/__tests__/upgrade.test.ts +90 -3
  36. package/cli/src/commands/help.ts +22 -39
  37. package/cli/src/commands/project.ts +20 -0
  38. package/cli/src/commands/up.ts +10 -0
  39. package/cli/src/commands/upgrade.ts +52 -1
  40. package/cli/src/utils/constants.ts +1 -1
  41. package/cli/src/utils/doctor-runtime.ts +5 -5
  42. package/cli/src/utils/prompt.ts +8 -1
  43. package/cli/src/utils/service-urls.ts +1 -1
  44. package/discovery/identity-management-backend/service.yaml +2 -2
  45. package/discovery/identity-management-frontend/service.yaml +1 -1
  46. package/discovery/registry.star +3 -1
  47. package/discovery/services/platform/orchestrator/orchestrator-glue-backend/service.yaml +2 -2
  48. package/docs/demo.svg +217 -0
  49. package/engine/assets/docker/golden-layers.Dockerfile +18 -18
  50. package/engine/assets/docker/wake-gateway/Dockerfile +4 -3
  51. package/engine/assets/docker/wake-gateway/decide.test.ts +72 -1
  52. package/engine/assets/docker/wake-gateway/decide.ts +51 -2
  53. package/engine/assets/docker/wake-gateway/server.ts +165 -28
  54. package/engine/assets/scripts/mac-m1-resource-monitor.sh +1 -1
  55. package/engine/scripts/debug-container.sh +8 -3
  56. package/engine/spec.master +2 -2
  57. package/engine/topologies/platform/docker/compose/traefik_standalone.star +23 -1
  58. package/engine/topologies/platform/docker/layers/prisma/prisma_build.star +1 -1
  59. package/engine/topologies/platform/docker/networking/traefik_static_routes.star +62 -8
  60. package/engine/topologies/tilt/common/utils.star +1 -1
  61. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +89 -12
  62. package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +20 -0
  63. package/install.sh +19 -242
  64. package/openspec/changes/prioritized-cold-start/design.md +27 -3
  65. package/openspec/changes/prioritized-cold-start/proposal.md +1 -0
  66. package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +9 -3
  67. package/openspec/changes/prioritized-cold-start/tasks.md +24 -1
  68. package/package.json +1 -1
  69. package/platform/services/platform/proxy/service.yaml +1 -1
  70. package/scripts/record-demo.sh +43 -0
  71. package/scripts/release-binaries.sh +8 -5
  72. package/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +2 -2
  73. package/tests/tilt-engine/test_sablier_defer_start.py +41 -0
  74. package/tests/tilt-engine/test_standalone_traefik.py +14 -0
  75. package/tests/tilt-engine/test_traefik_static_wake_route.py +68 -0
  76. package/openspec/changes/archive/2026-04-29-scan-repo-gaps-fix/codebase-audit-report.md +0 -62
  77. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/.openspec.yaml +0 -5
  78. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/design.md +0 -94
  79. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/proposal.md +0 -46
  80. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/ai-slop-cleanup/CLEANUP_REPORT_8.md +0 -174
  81. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/ai-slop-cleanup/CRITICAL_ASSESSMENT_8.md +0 -160
  82. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/circular-deps/CRITICAL_ASSESSMENT_4.md +0 -381
  83. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/dead-code/CRITICAL_ASSESSMENT_3.md +0 -316
  84. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/deduplication/CRITICAL_ASSESSMENT_1.md +0 -290
  85. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/deduplication/IMPLEMENTATION_SUMMARY_1.md +0 -233
  86. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/defensive-cleanup/CRITICAL_ASSESSMENT_6.md +0 -835
  87. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/legacy-cleanup/CRITICAL_ASSESSMENT_7.md +0 -254
  88. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/legacy-cleanup/IMPLEMENTATION_SUMMARY_7.md +0 -187
  89. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/strong-typing/CRITICAL_ASSESSMENT_5.md +0 -269
  90. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/type-consolidation/CRITICAL_ASSESSMENT_2.md +0 -312
  91. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/specs/type-consolidation/IMPLEMENTATION_SUMMARY.md +0 -170
  92. package/openspec/changes/archive/2026-05-04-comprehensive-cleanup-final/tasks.md +0 -137
@@ -35,6 +35,8 @@ See `proposal.md` (Why) for the motivating case. Relevant current behavior:
35
35
 
36
36
  Add a `deferStart` key to the manifest's existing `sablier` block (`engine/topologies/tilt/manifest/schema.star`'s `sablier` schema, alongside `enable`/`group`/`sessionDuration`), default `false`. `_register_replicas` (and any other call site that currently receives the single `auto_init_apps` flag for app resources) resolves per-resource auto-init as `auto_init_apps and not resource_defers_start(manifest)` instead of using `auto_init_apps` directly. The resource's **config-gen** and **docker_build** resources keep `auto_init=True` unconditionally (or follow `auto_init_config_gen`, already `True` by default) so the image still builds during `tdk up` — only the running container is held back.
37
37
 
38
+ **Corrected live (2026-09-27, task 7.2/7.7):** that last sentence assumed wrongly. In this codebase `docker_build` has no `auto_init` of its own: Tilt attaches the image target to the `dc_resource` that deploys it, so `auto_init=False` skips the image build too. A `tdk up` on `tdk-erp-system` recorded zero builds for the deferred `data-governance-api`. **Fix:** each `deferStart` resource also gets a `<name>-image` `local_resource` (auto-init follows the landscape-wide `auto_init_apps`, not the deferred flag) that runs `docker build --target production --network host` into the tag the compose service declares. A plain `docker build` has no `only=` filter, so it builds from a copy of the Dockerfile (`<Dockerfile>.prebuild`) whose Dockerfile-specific `.dockerignore` is an allowlist generated from the same `only=`/`ignore=` lists `docker_build` uses (`TypescriptBuilders.prebuild_dockerignore`). Verified on `data-governance-api`: 14 kB build context, every layer served from the cache Tilt's own build populated, and filesystem layers identical to Tilt's image (only Tilt's `dev.tilt.*` labels differ). Tilt's own `docker_build` stays registered, so the first `tilt trigger` still gets live update.
39
+
38
40
  *Alternatives considered:*
39
41
  - A separate global "cold landscape" mode (e.g. `TILT_DEFER_ALL_APPS`): rejected because it can't express "just this one resource," which is the actual request (one API, not the whole landscape).
40
42
  - Piggybacking on `TILT_TRIGGER_MODE=manual` wholesale: rejected — that already exists and disables `enforce_migrator_deps` and replica counts landscape-wide; overloading it would change unrelated behavior for every resource, not just opted-in ones.
@@ -48,16 +50,18 @@ Confirmed live: `auto_init=False` leaves **no container object** until triggered
48
50
 
49
51
  *Alternative considered:* add an explicit `docker compose create <service>` step (without starting) right after image build, so every deferred resource always has a discoverable-but-stopped container from the start, keeping Docker-label discovery as the single mechanism for both cases. Rejected as the primary path: it re-adds exactly the container-creation cost (network setup, volume mounts) to `tdk up`'s critical path that deferring was meant to avoid, just shifted from "start" to "create," and it still needs D6's static route anyway for the moment between `tdk up` finishing and that create step actually completing. Recorded as a possible later optimization (pre-create eagerly to shave latency off the very first request) but not required for this change to work.
50
52
 
51
- ### D3: One wake gateway in front of every deferred resource; it calls Sablier's API or `tilt trigger` depending on container state
53
+ ### D3: One wake gateway in front of every deferred resource; it starts the resource directly when possible, falling back to Sablier's API or `tilt trigger`
52
54
 
53
55
  Since Sablier has no hook to piggyback on (see Context), the component that also needs to call Tilt was always going to be something this project writes, not a Sablier extension point. So rather than splitting "Sablier handles it, plus something else calls Tilt on the side," one small always-on gateway sits in front of every `deferStart` resource (behind D4's static route) and branches on whether a container object exists for it:
54
56
 
55
57
  - **Container exists (previously ran, now stopped/exited):** the gateway calls Sablier's own HTTP API (the same call Traefik's plugin would make) to get the existing Docker-label wake, and in parallel runs `tilt trigger <resource>` so Tilt's own state (e.g. a migrator that needs to re-run) catches up too — Sablier's Docker-level start alone doesn't guarantee that. In practice this branch is reached rarely: per D4's verified priority fix, once any container object exists at all, Traefik's Docker-label route (at its normal, higher priority) wins over this gateway's static route on the very next request, so ongoing traffic to an existing-but-stopped resource goes straight to Sablier's own native plugin, not through here. This branch mainly covers the brief window before Traefik's docker provider notices a just-created container.
56
- - **No container exists yet:** there is nothing for Sablier to manage (confirmed in Context — Sablier's Docker provider needs a labeled container to register, same constraint as Traefik's). The gateway skips Sablier entirely and calls `tilt trigger <resource>` directly, then polls the resource's healthcheck itself, holding the request exactly the way Sablier's blocking strategy would. This is the branch that matters for the proposal's actual motivating case, and the one the live rehearsal below exercises.
58
+ - **No container exists yet:** there is nothing for Sablier to manage (confirmed in Context — Sablier's Docker provider needs a labeled container to register, same constraint as Traefik's). This is the branch that matters for the proposal's actual motivating case (task 7.2's live `tdk-erp-system` run exercises exactly this), and it is **not** simply "call `tilt trigger` and wait" — see the queue-contention finding below. When the generated route carries the resource's compose invocation (project, compose files, env file, image — added by task 7.2's fix; see the note at the end of this section), the gateway runs `docker compose up -d --no-build <resource>` itself, the same command Tilt's own `<resource>-run-only` local_resource would run, using an already-built image. Only when no image exists yet (nothing has ever built this resource) does it fall back to `tilt trigger <resource>`, which both builds and starts it. Either way it then polls the resource's healthcheck itself, holding the request exactly the way Sablier's blocking strategy would.
57
59
 
58
60
  Either way, the gateway is the single thing the static route (D4) points at, and it's the same process regardless of which branch it takes — this only affects which downstream call it makes on a given request. In both branches, the gateway also explicitly `tilt trigger`s and polls every dependency named in the request's `X-Wake-Deps` header (D2/D4), rather than assuming either Sablier or Tilt cascades that on its own — confirmed necessary by the second probe in Context (Tilt parks a triggered resource on `waiting-for-dep` indefinitely if its dependency is itself untriggered) and doubly so by D5's finding that Compose `depends_on` cannot even validly express these dependencies in TDK's layout, so nothing but the gateway's own explicit handling was ever going to cover this.
59
61
 
60
- *Alternative:* always go through Tilt's trigger regardless of container state, and drop Sablier's Docker-level start entirely. Rejected: it makes every wake pay Tilt's reconciliation overhead even for the common case (a resource that only needs restarting), and it removes the one piece of this feature that doesn't depend on Tilt being reachable at all (Sablier + Traefik + Docker socket keep working even if the Tilt daemon itself is down, e.g. mid-`tilt up` restart).
62
+ *Alternative:* always go through Tilt's trigger regardless of container state, and drop Sablier's Docker-level start entirely. Rejected: it makes every wake pay Tilt's reconciliation overhead even for the common case (a resource that only needs restarting), and it removes the one piece of this feature that doesn't depend on Tilt being reachable at all (Sablier + Traefik + Docker socket keep working even if the Tilt daemon itself is down, e.g. mid-`tilt up` restart). It also turned out to have a second, more serious problem, found only by testing against a real landscape rather than a small synthetic one (see below): `tilt trigger` only *queues* a resource — it does not run ahead of, or preempt, builds Tilt already has in flight.
63
+
64
+ **Found live against the real `tdk-erp-system` landscape (2026-09-27), not caught by any synthetic probe:** `tilt trigger` alone does not solve the proposal's actual motivating problem when the landscape is under load. Tilt caps concurrent builds (a handful of slots; observed 3 in this landscape), and a re-vendor or config regeneration that touches many manifests queues a rebuild for every affected resource. A `deferStart` resource triggered while ~100 other resources were mid-rebuild sat queued behind them; the request timed out with a `504` after the gateway's full wait budget with no container ever created — precisely the "wait behind hundreds of resources" behavior the whole feature exists to avoid, just moved one layer down (from `tdk up`'s bring-up order to Tilt's build-queue order). **Fix:** `apply_compose_resource_registration.star` now also passes the resource's compose invocation (its Compose project name, absolute compose file paths, `.env` path if present, and declared image tag) into `generate_static_wake_route`'s `X-Wake-Compose-*`/`X-Wake-Image` headers; the standalone Traefik compose (`traefik_standalone.star`) mounts the project root read-only into the gateway container at the same absolute path so those compose files resolve identically there. The gateway's `ensureImage()` also retags Tilt's own `<repo>:tilt-<hash>` build as the declared `<repo>:dev` tag when needed, since a resource can be fully built without ever having run once (so the plain `:dev` tag was never created) — confirmed against the live landscape's own image list. This start path only ever creates/starts a container from an image that already exists; it never builds, so it cannot race or duplicate a build Tilt has in progress, and a resource that has genuinely never been built at all still correctly falls back to `tilt trigger`.
61
65
 
62
66
  **Implemented and verified live (2026-09-26), replacing speculation with what actually shipped:**
63
67
  - The gateway is real: `engine/assets/docker/wake-gateway/` (`decide.ts` + `server.ts`, a Bun HTTP server; `Dockerfile` bundling Bun, the Docker CLI, and the exact pinned `tilt` binary version this workspace runs). Config for a request (which resource, which container, which dependencies, which port) travels via `X-Wake-*` headers that a generated Traefik `headers` middleware injects — not the request path — so the original path/query reach the real backend unchanged once ready.
@@ -65,6 +69,10 @@ Either way, the gateway is the single thing the static route (D4) points at, and
65
69
  - Confirmed live: the Tilt/Compose *resource* name (what `tilt trigger` and Compose's own `depends_on` use) is not the same string as the Docker *container* name Compose actually creates (`<project>-<service>-1`, e.g. `analytics-autogenerated-data-governance-api-1` for resource `data-governance-api`) — `docker inspect` needs the latter. The gateway and the generated route both carry both identifiers (`X-Wake-Resource` / `X-Wake-Container`) rather than assuming they're interchangeable.
66
70
  - A full disposable rehearsal (isolated Tilt + Docker Compose + the built gateway image, torn down after) woke a cold `traefik/whoami` container from both "exited" and fully "absent" states and correctly proxied a request through with its original path intact.
67
71
 
72
+ **Verified live against `tdk-erp-system` (2026-09-27):** with the landscape quiet, the original URL answered `200` from a fully cold container in 19.2s via the direct compose start (task 7.2).
73
+
74
+ **Early `503` + `Retry-After` (task 7.8):** during a full `tdk up` bring-up the Docker daemon itself was the bottleneck: the direct start took ~5 min, and Bun's 255s idle cap dropped the held request with no response at all. Holding the request longer can't fix a saturated daemon, so the gateway now holds a waking request for at most 60s (`EARLY_RESPONSE_MS`, overridable via `X-Wake-Respond-Within-Ms`, capped at 240s), then answers `503` with `Retry-After: 10` while the start continues in the background. One wake runs per resource at a time: a retry, or another caller arriving meanwhile, joins the wake in progress rather than starting the container again. A resource that becomes healthy within the window is still answered directly, so the quiet-landscape case is unchanged.
75
+
68
76
  ### D4: A never-created resource's route comes from Traefik's static/file provider, not Docker-label discovery
69
77
 
70
78
  Since a never-created resource has no container and thus no labels, its Traefik router has to be defined statically. The standalone Traefik generator (`traefik_standalone.star`) already reserves the `@file` provider namespace by reference (`maintenance@file` in `traefik.star`) but does not actually enable it — its `command:` list only sets `--providers.docker=true`; there is no `--providers.file=true` and no generated dynamic-config file or mounted directory today. This change adds both: `--providers.file.directory=/etc/traefik/dynamic` (a new generated directory mounted into the `traefik` service), and, for every resource with `sablier.enable` + `deferStart` set, a generated file-provider entry defining that resource's router/service/middleware ahead of time, pointed at D3's wake gateway.
@@ -90,6 +98,22 @@ Dependency waking is therefore entirely the wake gateway's job, which was alread
90
98
 
91
99
  A deferred resource looks, from `docker ps`, identical to a resource that failed to start. `tdk up`'s summary and `tdk doctor` need to check the manifest's resolved `deferStart` (D1) directly rather than inferring "deferred" from container state, so a genuinely stuck resource is never miscategorized as intentionally cold.
92
100
 
101
+ ## Benchmark: D3's queue-bypass fix, measured
102
+
103
+ Task 7.4's live `tdk-erp-system` finding (D3) was that `tilt trigger` alone re-creates the original problem one layer down: a triggered resource queues behind Tilt's build-slot cap. That fix (direct `docker compose up --no-build` when an image already exists) needed a real, reproducible measurement, not just the one live pass that got interrupted by the strip-prefix bug — but re-testing against the real `tdk-erp-system` landscape wasn't available (developer's own call, 2026-09-27: stop hammering an already-overloaded shared Docker daemon). Instead, ran a disposable, isolated benchmark (own Tilt instance on a private port, own Compose project, fully torn down after — no interaction with any real project) that exercises the actual shipped `generate_static_wake_route()`/`decide.ts`/`server.ts` code, not a reimplementation:
104
+
105
+ - **Setup:** `update_settings(max_parallel_updates=2)` plus six "filler" `dc_resource`s with a genuinely slow (90s, uncached each run) Docker build, standing in for a large landscape's build queue — this is the same build-slot contention Tilt showed live (`max_parallel_updates` caps concurrent builds; the real landscape's default cap was observed at 3). Two target resources, both `auto_init=False`: `target-fast`'s route carries the compose-invocation headers (`X-Wake-Compose-*`/`X-Wake-Image`) with its image pre-tagged, exercising the new direct-start path; `target-slow`'s route omits them and has its own `docker_build`, so its first `tilt trigger` must acquire a build slot like the fillers, exercising the pre-7.4 fallback path unchanged.
106
+ - **Results** (wall-clock, cold container to first `200 OK`, via `curl` through the real Traefik static route and gateway):
107
+
108
+ | Scenario | Build-slot contention | Time to 200 |
109
+ |---|---|---|
110
+ | `target-fast` (direct compose start) | 2/2 slots busy (fillers) | **12.6s** |
111
+ | `target-fast` (direct compose start) | idle | 7.4s |
112
+ | `target-slow` (`tilt trigger` fallback) | 1/2 slots busy | 18.1s |
113
+ | `target-slow` (`tilt trigger` fallback) | idle | 22.8s |
114
+
115
+ - **What this shows:** `target-fast`'s latency stayed in a narrow band (7-13s) regardless of queue state — direct confirmation that the new path doesn't touch Tilt's build queue at all, matching the gateway's own log line (`compose start target-fast: started`, no `tilt trigger` call). `target-slow` paid a real, unconditional cost even at its best (idle) case — nearly double `target-fast`'s idle case — because every `tilt trigger` still goes through Tilt's own build-reconciliation step (confirmed via Tilt's build history for it) before `docker compose up` runs; that step is exactly what queues behind other builds live against `tdk-erp-system` (task 7.4's original 504). A fully apples-to-apples "both paths, both fully saturated" run wasn't captured (the 90s filler window and Tilt's own infra-resource build ordering made catching that exact instant unreliable without further burning the same finite build-queue setup repeatedly) — the mechanism-level proof (gateway logs showing which code path ran) plus the consistent directional numbers above are treated as sufficient verification, not a substitute for eventually re-running task 7.2's exact live URL once `tdk-erp-system`'s own queue is quiet.
116
+
93
117
  ## Risks / Trade-offs
94
118
 
95
119
  - [The static file-provider route (D4) and the Docker-discovered route disagree once the container exists, e.g. stale healthcheck path baked into the static entry] → Keep the static entry's routing rule byte-identical to what the generator already emits for the Docker-label version (same source-of-truth function), so the two can never drift; verified by generating both and diffing in task testing.
@@ -38,3 +38,4 @@ On a large landscape (`tdk-erp-system`: 107 resources, 110 containers), a reques
38
38
  - **Manifest schema:** `engine/topologies/tilt/manifest/schema.star`'s `sablier` block gains a new optional key (name TBD in design, e.g. `deferStart`).
39
39
  - **Generated projects:** every project with an opted-in `sablier` resource needs `tdk project --yes` (re-vendor) plus a golden image rebuild to pick up the change; existing non-opted-in resources are unaffected.
40
40
  - **Tests:** `tests/tilt-engine/` snapshot tests for the per-resource `auto_init` resolution and the generated static Traefik route; `bun test` unit tests for the wake gateway's decision logic.
41
+ - **Engine (Starlark) — found live, 2026-09-27:** two real gaps only surfaced by testing against the actual `tdk-erp-system` landscape, both now fixed (design.md D3, tasks.md 7.4/7.5): (1) `tilt trigger` alone re-created the original problem one layer down (queues behind Tilt's own build-slot cap under real load) — the gateway now prefers a direct `docker compose up --no-build` when an image already exists; (2) a woken resource still 404'd because the wake route didn't strip the same `apiPath` prefix the real Docker-label router does before forwarding — fixed by adding the matching `stripPrefix` middleware to the generated route. A disposable, isolated benchmark (design.md's "Benchmark" section) measured the first fix's real effect: the new path answers in ~7-13s independent of build-queue contention, versus ~18-23s for the old fallback even at its best case.
@@ -13,7 +13,7 @@ A resource with a `sablier` manifest block SHALL be able to mark itself deferred
13
13
  #### Scenario: Deferred resource is not started during tdk up
14
14
 
15
15
  - **WHEN** `tdk up` runs a landscape containing a resource with deferred start enabled
16
- - **THEN** that resource's image build completes during the run
16
+ - **THEN** that resource's image build completes during the run, under the image tag its compose service declares, so its first request can start it without building
17
17
  - **AND** the resource's container is not started, and `tdk up` does not wait on it to become healthy before finishing bring-up of the rest of the landscape
18
18
 
19
19
  #### Scenario: Non-opted-in resource is unaffected
@@ -65,13 +65,19 @@ Waking a deferred resource SHALL also wake any of its required startup dependenc
65
65
 
66
66
  ### Requirement: A Waking Request Waits For Health, Not Just Start
67
67
 
68
- A request that triggers a deferred resource's startup SHALL be held until the resource passes its health check or until a bounded timeout elapses, rather than being answered as soon as the container process starts.
68
+ A request that triggers a deferred resource's startup SHALL be held until the resource passes its health check, until a bounded hold window elapses, or until a bounded timeout elapses, rather than being answered as soon as the container process starts. The caller SHALL always receive a response; the connection SHALL NOT be dropped silently.
69
69
 
70
70
  #### Scenario: Client receives the real response
71
71
 
72
- - **WHEN** a request wakes a deferred resource
72
+ - **WHEN** a request wakes a deferred resource that becomes healthy within the hold window
73
73
  - **THEN** the client's request is held and then answered by the resource once it is healthy, rather than receiving an immediate "starting" response with no real answer
74
74
 
75
+ #### Scenario: Startup outlasts the hold window
76
+
77
+ - **WHEN** a request wakes a deferred resource that is still not healthy when the hold window ends (e.g. the Docker daemon is saturated by a full `tdk up` bring-up)
78
+ - **THEN** the request is answered with `503` and a `Retry-After` header while the start continues in the background
79
+ - **AND** a retry, or any other request that arrives meanwhile, joins that same start instead of starting the resource again, and is answered by the resource once it is healthy
80
+
75
81
  #### Scenario: Startup exceeds the timeout
76
82
 
77
83
  - **WHEN** a deferred resource does not become healthy within its configured wake timeout
@@ -41,6 +41,29 @@
41
41
  ## 7. Documentation and integration
42
42
 
43
43
  - [x] 7.1 Document `deferStart` in the same place the `sablier` block itself is documented. **Done**: this repo has no separate docs file for `sablier` at all — its only documentation is `schema.star`'s field description and `resource-features.ts`'s feature entry, both already updated in group 2/task 2.2 (D5's `depends_on` mention removed from that description since D5 no longer uses it). No standalone docs page exists to additionally update.
44
- - [ ] 7.2 Re-vendor into `tdk-erp-system` (`tdk project --yes`, touch the Tiltfile, rebuild affected images), set `sablier: {enable: true, deferStart: true}` on `data-governance-api`'s `service.json`, and run a full `tdk up`. Verify that `data-governance-api` does not start during the initial bring-up, that `curl http://api.tdk-erp-system.localhost/api/data-governance/health` while the rest of the landscape is still starting returns a real (not `404`/connection-refused) health response within the resource's normal cold-start time — directly closing the gap reproduced live during design (see design.md Context) — and that `tdk doctor` does not flag it before that request and does not flag it as broken after. **Still open**: this needs the actual `tdk-erp-system` checkout and its real, long-running Docker landscape, which exist only on the developer's own machine, not in a cloud session's ephemeral container — confirmed 2026-09-27 (no Docker daemon running, no `tdk-erp-system` checkout, no persistent landscape in this environment). A disposable synthetic probe covering the mechanism this task would exercise was run instead — see the note below — but this task itself needs to be completed against the real project locally.
44
+ - [ ] 7.2 Re-vendor into `tdk-erp-system` (`tdk project --yes`, touch the Tiltfile, rebuild affected images), set `sablier: {enable: true, deferStart: true}` on `data-governance-api`'s `service.json`, and run a full `tdk up`. Verify that `data-governance-api` does not start during the initial bring-up, that `curl http://api.tdk-erp-system.localhost/api/data-governance/health` while the rest of the landscape is still starting returns a real (not `404`/connection-refused) health response within the resource's normal cold-start time — directly closing the gap reproduced live during design (see design.md Context) — and that `tdk doctor` does not flag it before that request and does not flag it as broken after. **Still open, in progress on the developer's own machine (2026-09-27):**
45
+ - Confirmed the free-tier key in `.env` grants nothing (`⚠️ No premium resources unlocked for this key`); the license-gated overlay was exercised by manually copying `tdk-cli-extensions/premium/networking/sablier_container_cycle.star` over the vendored stub, which is exactly what a real grant would do (`vendorTdkExtension()`'s own overlay step, `template-engine.ts`).
46
+ - Added the `sablier` block to `data-governance-api/service.json`, re-vendored, and confirmed the generated route (`traefik-dynamic/data-governance-api.yml`) and cold behavior (no container until first request) all work as designed.
47
+ - First real pass found and fixed two genuine bugs (not environment noise): (a) task 7.4's queue-bypass fix (`tilt trigger` alone queued behind Tilt's build-slot cap and 504'd — see D3); (b) after that fix, a second real bug — the app only serves its bare health path (`/health`), and the generated wake route forwarded the full public path (`/api/data-governance/health`) unstripped, unlike the real Docker-label `-project` router, so a successful wake still 404'd. Fixed by adding a matching `stripPrefix` middleware to the generated route (`traefik_static_routes.star`), reusing the same `cli_api_path()` the Docker-label router strips with. Both fixes are unit/Starlark-tested (33/33 passing) and re-vendored into the live project.
48
+ - **Re-test after both fixes is inconclusive, not failing**: a `docker rm -f` + cold curl got a `502` from Traefik itself (not the gateway's own 504), and the gateway's log showed only its initial "wake" decision line, never a "compose start ... started/failed" result — `docker compose up` inside the gateway appears to have stalled rather than erred. Root-caused to the machine, not the code: `tilt get uiresource` showed **104 of 250 resources still `pending`**, mid a mass rebuild (Tilt's build-slot cap processing 3 at a time) triggered by this task's own repeated `tdk project --yes` + forced-Tiltfile-re-eval cycles across this session — sustained load average ~26-27, `docker ps`/`traefik ping` themselves taking 10-30s, and Traefik logged a stale `data-governance-api@docker` service IP getting "no route to host" consistent with a container that started to come up and was torn down mid-flight under this contention. **Decision (2026-09-27, developer's own call, not further probed by any session)**: stop touching the live landscape rather than keep polling an already-overloaded daemon (load average climbed to ~38 after this note was first written, queue hadn't drained) or force a Docker Desktop restart that would kill the ~110 containers of a 14+-hour-running landscape. The code changes (task 7.4's queue-bypass fix and this task's strip-prefix fix) ship as-is on the strength of their unit/Starlark tests (33/33 passing) plus the one clean live pass that *did* complete end-to-end before the path-stripping bug was found (container created from cold via direct compose start, no Tilt-queue wait, "compose start data-governance-api: started" logged, and a genuine 502→200 progression as each bug was fixed in turn) — not on an unattainable final green run under a saturated host. This task stays open (checkbox unchecked) rather than being marked done on inference: the one remaining step is a single clean re-run of `curl http://api.tdk-erp-system.localhost/api/data-governance/health` against a quiescent `tdk-erp-system` landscape, entirely at the developer's own convenience, with no further re-vendor/re-eval cycles needed first (the fixes are already vendored in).
49
+ - **Live re-test passed, 2026-09-27 18:55** (landscape quiet: Tilt no longer running, load ~17, no re-vendor/re-eval needed): warm `curl http://api.tdk-erp-system.localhost/api/data-governance/health` → `200` with the real health body in 0.6s (the same request 404'd before task 7.5's strip-prefix fix). Then `docker rm -f` the container, confirmed absent, same `curl` → **`200` `{"status":"healthy",...}` in 19.2s** from fully cold. Gateway log: `wake data-governance-api: state=absent compose=true trigger=[]` → `compose start data-governance-api: started` — task 7.4's direct-start path, no Tilt involvement at all (Tilt was not even running). This closes the proposal's motivating gap on the real landscape. Side observation: Traefik's own Docker-label routers for this resource are `disabled` because the Sablier Traefik plugin fails its integrity check on download (`plugin integrity check failed`, plugins disabled), so every request currently goes through the wake route — a separate, pre-existing plugin-download problem, not part of this change.
50
+ - **Still unverified live (why the box stays unchecked):** the two parts of 7.2 that need Tilt itself running — (a) task 3.3, that a fresh `tdk up` leaves `data-governance-api` with no container until its first request, and (b) that `tdk doctor` reports it as deferred rather than stalled. Both are covered by unit tests (3.1/3.2 predicate tests, 6.2's doctor tests) but not yet observed on this landscape. Next step: one `tdk up` in `tdk-erp-system`, then `docker ps -a --filter name=data-governance-api` (expect nothing) and `tdk doctor`.
51
+ - **`tdk up` run, 2026-09-27 19:08 (global `tdk` 1.3.51, premium overlay in place, container removed first):**
52
+ - ✅ `tdk up`'s summary printed `data-governance-api: .../api/data-governance/health (deferred — starts on first request)` (task 6.1, live).
53
+ - ✅ After the Tiltfile evaluated, Tilt left `data-governance-api` at `updateStatus: none` / `runtimeStatus: none` and created no container (task 3.3's container half, live).
54
+ - ❌ **Image not built.** Contrary to the design comment in `_register_single_resource` and the spec's "Deferred Start Opt-In" requirement, `auto_init=False` skips the resource's image build as well as its container: this Tilt session recorded zero builds for `data-governance-api`, and its newest image predates the session. The image only existed because earlier sessions built it. On a fresh project the first wake finds no image, `ensureImage()` fails, and the gateway falls back to `tilt trigger` — the queueing path task 7.4 was meant to avoid.
55
+ - ❌ **Cold wake during a full bring-up timed out for the caller.** Hit the URL at 19:10:42 with 134 resources pending: the gateway chose the direct compose path (`state=absent compose=true`), but `docker image inspect` took ~2 min and `docker compose up` ~3 min more because the Docker daemon itself was saturated by the bring-up (`tdk doctor` also aborted: `docker ps` hung >10s). The container started at 19:15:36 (~5 min), after Bun's 255s idle timeout had already dropped the client connection (`HTTP 000`). A follow-up request returned `200` in 16s. So bypassing Tilt's build queue works, but under a full bring-up the Docker daemon is the next bottleneck, and the gateway gives the caller no response in that case.
56
+ - ⏸ `tdk doctor`'s deferred reporting (6.2) not observed yet: doctor exits at its Docker-responsiveness check while the daemon is this loaded.
57
+ - **Open decisions, resolved by the developer 2026-09-27:** (1) build the image during `tdk up` → task 7.7; (2) answer slow wakes with an early `503` + `Retry-After` → task 7.8. Both implemented and tested; neither re-vendored into `tdk-erp-system` yet. **What remains for this task:** re-vendor, then one `tdk up` to confirm the `data-governance-api-image` resource builds and the container stays absent, a cold request during bring-up to confirm a `503` + `Retry-After` followed by a successful retry, and `tdk doctor` once the Docker daemon is responsive enough for its 10s check.
45
58
  - **2026-09-27 disposable probe (torn down after, nothing kept):** to verify task 7.3's fix end-to-end rather than only at the Starlark-unit level, stood up an isolated Tilt + Docker Compose project (two `traefik/whoami` services, `dep-service` and `primary-service`, both `dc_resource(auto_init=False)`) plus a standalone Traefik with the file provider enabled. Generated `primary-service`'s static route with the **real, unmodified** `generate_static_wake_route()` from `traefik_static_routes.star`, passing `dependencies=[("dep-service", "wakeprobe")]` the same shape `resolve_defer_start_dependencies` now produces — confirmed the emitted YAML carries `X-Wake-Deps: "dep-service=wakeprobe-dep-service-1"`. Ran the real `wake-gateway/server.ts` (as a local Bun process against this container's own Tilt daemon and Docker socket, since building the packaged Docker image failed here on an unrelated Alpine/`apk` proxy TLS issue — the packaged image itself is unaffected, this only affected building it ad hoc in this sandbox). A single HTTP request against `primary-service`'s route, while both containers were confirmed absent beforehand, resulted in **both** `wakeprobe-primary-service-1` and `wakeprobe-dep-service-1` being created and started (verified via `docker ps` and `tilt logs` showing both `tilt trigger` calls succeed) — this is the exact behavior task 7.3 added and that D5's requirement describes, now observed working end-to-end, not just unit-tested. The final proxy-through step returned a connection error, but only because the gateway ran as a host process outside the Compose network and so couldn't resolve the container's Compose-DNS name (`primary-service`) — confirmed by curling the container's bridge IP directly and getting a real `200`; that proxy-through mechanic itself was already verified live in task 4.3's own probe when the gateway ran inside the correct Docker network, and is unrelated to 7.3's change. One real, unrelated hiccup along the way: `primary-service`'s first `tilt trigger` failed with a Docker Hub `429 Too Many Requests` on the image pull (transient registry throttling, not a bug); a second trigger with the image already cached succeeded normally.
46
59
  - [x] 7.3 (Found 2026-09-26 re-reading the shipped code) Wire real dependency data into the `generate_static_wake_route(...)` call at `apply_compose_resource_registration.star:442` — it always passed no `dependencies=` argument, so every generated route shipped `X-Wake-Deps: ""` despite the gateway side and the generator's `dependencies` param both being implemented and unit-tested in isolation. **Done, 2026-09-26**: added `resolve_manifest_dependency_names(manifest, all_services_map)` (reuses the existing `_resolve_dependency_to_resource` that `_build_resource_deps` already trusts for Tilt's own `resource_deps` ordering — an ambiguous name behaves identically here as it already does there) and `resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map)` (filters those resolved names to the ones whose own manifest is itself `resource_defers_start`), both in `apply_compose_resource_registration.star`, no leading underscore so they're directly `load()`-able for tests per task 3.1's precedent. The call site now passes `dependencies=resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map)`. 6 new cases in `test_sablier_defer_start.py` cover: exact-name resolution, an unresolvable name falling back to exclusion (not a guess), a dependency that resolves but isn't itself deferStart, and the free-tier license gate keeping the result empty even for a `dependsOn` target whose manifest asks for `deferStart` (mirrors task 3.1's own license-gate tests, since `resource_defers_start` is reused unchanged) — full 166/166 passing (166 = prior 160 + these 6; the pre-existing 29 unrelated failures in `tests/tilt-engine/` — `test_agents_md_*`, `test_daemon_*`, `test_snapshot_scanning`, etc. — are identical before and after this change, confirmed by diffing the failure list against a `git stash` baseline). The "licensed and actually wakes a peer deferStart resource" end-to-end case still can't be exercised from this repo's free-tier stub, same limitation task 3.1 already documented — folded into task 7.2's live run rather than duplicated here.
60
+
61
+ - [x] 7.5 (Found live during task 7.2's real `tdk-erp-system` run, 2026-09-27, immediately after 7.4's fix) A woken resource still 404'd: the app only serves its bare health path (e.g. `/health`), but the generated wake route forwarded the full public path (`/api/data-governance/health`) unstripped — unlike the real Docker-label `-project` router, which strips that prefix via its own `stripprefix` middleware (`traefik.star`'s `get_backend_traefik_labels`). **Done**: `generate_static_wake_route` (`traefik_static_routes.star`) now emits a matching `stripPrefix` middleware ahead of the headers middleware, reusing the same `cli_api_path()` the Docker-label router already strips with, so the two paths agree by construction rather than by convention. New `test_static_route_strips_the_same_api_path_prefix_the_docker_router_does` case (asserts the middleware exists, strips the right prefix, and runs before the headers middleware in the router's list) — 34/34 Starlark tests passing. Re-vendored into `tdk-erp-system` and confirmed via `docker exec ... wget /health` (200) vs `wget /api/data-governance/health` (404) against the real container that the app itself only serves the stripped path, proving the fix addresses a real gap rather than a theoretical one.
62
+
63
+ - [x] 7.6 Benchmark task 7.4's queue-bypass fix with real, reproducible numbers rather than relying solely on the one interrupted live pass. **Done, 2026-09-27**: a disposable, isolated benchmark (own Tilt instance on a private port, own Compose project, torn down completely after — no interaction with any real project) exercising the actual shipped `generate_static_wake_route()`/`decide.ts`/`server.ts` code against a genuine build-slot queue (`max_parallel_updates=2`, six slow-building filler resources). Full setup, results table, and interpretation recorded in `design.md`'s new "Benchmark: D3's queue-bypass fix, measured" section. Headline results: the new direct-start path (`target-fast`) answered in 7.4-12.6s regardless of whether the build queue was saturated; the old `tilt trigger`-only fallback (`target-slow`) cost 18.1-22.8s even at its best (idle) case, confirmed via Tilt's own build history to be paying its build-reconciliation step unconditionally — the same step that queues behind other builds and caused the original live `504`.
64
+
65
+ - [x] 7.7 (Found live, task 7.2's `tdk up` run, 2026-09-27) Build a deferred resource's image during `tdk up`: `auto_init=False` also skipped its `docker_build`, contrary to D1's original assumption. **Done** (`3f25fe6`): `_register_deferred_image_prebuild` adds a `<name>-image` `local_resource` per `deferStart` resource that builds the same image under the compose-declared tag, using a Dockerfile copy with an allowlist `.dockerignore` from the new `TypescriptBuilders.prebuild_dockerignore` (mirrors `docker_build`'s `only=`/`ignore=`). New `test_prebuild_dockerignore_mirrors_docker_build_only_and_ignore` case; 35/35 related Starlark tests pass. Verified by hand on `data-governance-api` with the exact command the resource runs (throwaway tag): 14 kB context, all layers cached, filesystem layers identical to Tilt's image.
66
+
67
+ - [x] 7.8 (Found live, task 7.2's `tdk up` run, 2026-09-27) Never drop a waking request silently. **Done** (`3f25fe6`): `server.ts` holds for `earlyResponseMs()` (60s default, `X-Wake-Respond-Within-Ms` override capped at 240s under Bun's 255s idle limit), then answers `503` + `Retry-After: 10` while the start continues; an in-flight map keeps one wake per resource so retries join it. Spec updated ("Startup outlasts the hold window" scenario). 18/18 `bun test` cases pass (4 new for the timing rules); `server.ts`/`decide.ts` type-check under `strict` with Bun types. Not yet exercised live: the in-flight join and the 503 path need a slow start to observe (task 7.2's remaining step).
68
+
69
+ - [x] 7.4 (Found live during task 7.2's real `tdk-erp-system` run, 2026-09-27) `tilt trigger`-only wake failed under real load: with ~100+ resources mid-rebuild after a re-vendor (Tilt caps concurrent builds at a handful of slots), a triggered `deferStart` resource sat queued behind them and the request timed out with a `504` — the exact "wait behind a huge landscape" problem this feature exists to remove, just relocated to Tilt's build queue. **Done**: the gateway now prefers creating/starting the resource directly with `docker compose up -d --no-build <resource>` (the same command Tilt's own `<resource>-run-only` local_resource runs) whenever the generated route supplies the resource's compose invocation, falling back to `tilt trigger` only when no image has ever been built for it yet (`ensureImage()` first tries retagging Tilt's own `<repo>:tilt-<hash>` build as `<repo>:dev` in case the plain tag was never created). Changes: `decide.ts`'s `decideWakeAction` gained `useComposeStart`/`hasComposeInvocation` and `wakeTimeoutMs()` (180s for a direct start, 240s when a build might be needed, under Bun's 255s idle cap) with new `decide.test.ts` cases; `server.ts` gained `composeStart()`/`ensureImage()` and reads `X-Wake-Compose-Project`/`X-Wake-Compose-Files`/`X-Wake-Env-File`/`X-Wake-Image`; `traefik_static_routes.star`'s `generate_static_wake_route` gained matching optional params/headers (new `test_static_route_carries_compose_invocation_for_direct_start` case) plus a `normalize_abs_path()` helper (Tilt's `os.path.abspath` doesn't collapse `..`, and the generated Tiltfile's project root is exactly `config.main_dir + "/../.."` — confirmed with a throwaway probe, new `test_normalize_abs_path_collapses_dot_segments` case); `apply_compose_resource_registration.star` gained `_wake_compose_invocation()` wiring the resource's actual compose files/env/image into the call site; `traefik_standalone.star` mounts the project root read-only into the gateway container at the same normalized absolute path so those compose files resolve identically there (new `test_wake_gateway_mounts_project_root_at_its_own_absolute_path` case). Dockerfile gained `docker-cli-compose`. 33/33 Starlark tests and the full `bun test` suite (25 assertions) pass. Live re-verification against `tdk-erp-system` (re-vendored, route regenerated with the new headers, gateway/Traefik rebuild in progress under the same real build-queue contention that motivated this) folded into 7.2's final result below rather than duplicated here.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.50",
3
+ "version": "1.3.52",
4
4
  "description": "Run 100 microservices on a 16 GB laptop without Kubernetes. Scaffold services and run them locally with hot reload, built on Tilt.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {
@@ -10,5 +10,5 @@ dependsOn:
10
10
  - database-management
11
11
  runtime: docker
12
12
  traefik:
13
- host: traefik.beauty-crm.localhost
13
+ host: traefik.tdk-project.localhost
14
14
  pathPrefix: /dashboard
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # Re-record docs/demo.svg, the animated terminal demo shown in README.md.
3
+ #
4
+ # Needs `tdk` on PATH and termtosvg (pip install termtosvg). Run from the
5
+ # repo root: scripts/record-demo.sh
6
+ # The demo runs real tdk commands in a temporary folder that is shown as
7
+ # ~/my-shop. `tdk up` is only mentioned, because it needs Docker and Tilt.
8
+ set -euo pipefail
9
+
10
+ if [[ "${1:-}" != "--play" ]]; then
11
+ cast="$(mktemp -d)/demo.cast"
12
+ # `script` gives termtosvg a real terminal even when run from CI or an IDE.
13
+ script -qec "termtosvg record '$cast' -c '$0 --play' -g 92x30" /dev/null >/dev/null
14
+ termtosvg render "$cast" docs/demo.svg -t window_frame -M 3000
15
+ exit 0
16
+ fi
17
+
18
+ export FORCE_COLOR=1
19
+ dir="$(mktemp -d)/my-shop"
20
+ mkdir -p "$dir" && cd "$dir"
21
+
22
+ tidy() { sed -u "s#$dir#~/my-shop#g"; }
23
+ type_cmd() {
24
+ printf '\033[35m~/my-shop $\033[0m '
25
+ for ((i = 0; i < ${#1}; i++)); do printf '%s' "${1:$i:1}"; sleep 0.035; done
26
+ sleep 0.5; printf '\n'
27
+ }
28
+
29
+ clear; sleep 0.5
30
+ type_cmd "tdk project --yes"
31
+ tdk project --yes 2>&1 | tidy | grep --line-buffered -E "Initializing|✓ Created: .tdk/project|Generated: .tdk/.tdk-out/Tiltfile|docker-compose|✅"
32
+ sleep 1.8
33
+ type_cmd "tdk resource orders-api --type backend --stack shop"
34
+ printf 'y\n' | tdk resource orders-api --type backend --stack shop 2>&1 | tidy | grep --line-buffered -E "Port:|Path:|Dockerfile|backend source|test file|✅"
35
+ sleep 1.8
36
+ type_cmd "tdk resource shop-web --type frontend --stack shop"
37
+ printf 'y\n' | tdk resource shop-web --type frontend --stack shop 2>&1 | tidy | grep --line-buffered -E "Port:|Path:|✅"
38
+ sleep 1.8
39
+ type_cmd "tdk stacks --services"
40
+ tdk stacks --services 2>&1 | tidy | grep -v '^\s*$'
41
+ sleep 1.2
42
+ printf '\n\033[2m# next: tdk up shop (Tilt builds, runs and hot-reloads the whole stack)\033[0m\n'
43
+ sleep 4
@@ -45,14 +45,17 @@ cp -R "${ROOT_DIR}/cli/templates" "${RELEASE_DIR}/tdk-cli/cli/templates"
45
45
 
46
46
  (
47
47
  cd "${RELEASE_DIR}"
48
- shasum -a 256 tdk-linux-amd64 tdk-linux-arm64 tdk-darwin-amd64 tdk-darwin-arm64 > checksums.txt
49
- zip -qr "tdk-cli-${RELEASE_TAG}-binaries.zip" \
50
- tdk-linux-amd64 tdk-linux-arm64 tdk-darwin-amd64 tdk-darwin-arm64 \
51
- tdk-cli checksums.txt
52
48
  # Fixed filename (no version/tag in the name) so install.sh can always
53
49
  # find it via the stable /releases/latest/download/ URL, unlike the zip
54
- # above whose name embeds the tag and changes every release.
50
+ # below whose name embeds the tag and changes every release.
55
51
  tar -czf tdk-cli-engine.tar.gz tdk-cli
52
+ # install.sh and `tdk upgrade` verify the binary and the engine tarball
53
+ # against this file before installing them.
54
+ shasum -a 256 tdk-linux-amd64 tdk-linux-arm64 tdk-darwin-amd64 tdk-darwin-arm64 \
55
+ tdk-cli-engine.tar.gz > checksums.txt
56
+ zip -qr "tdk-cli-${RELEASE_TAG}-binaries.zip" \
57
+ tdk-linux-amd64 tdk-linux-arm64 tdk-darwin-amd64 tdk-darwin-arm64 \
58
+ tdk-cli checksums.txt
56
59
  )
57
60
 
58
61
  echo "Built release assets in ${RELEASE_DIR}"
@@ -86,7 +86,7 @@ DB_WAIT_INTERVAL="${DB_WAIT_INTERVAL:-2}"
86
86
  # =============================================================================
87
87
  #
88
88
  # Problem: Tilt creates databases but doesn't know what Infisical contains.
89
- # This causes DATABASE_URL mismatch (e.g., beauty_crm_staff vs staff_db_prod)
89
+ # This causes DATABASE_URL mismatch (e.g., tdk_project_staff vs staff_db_prod)
90
90
  #
91
91
  # Solution: TILT_* prefixed variables ALWAYS override Infisical secrets
92
92
  # Tilt sets TILT_DATABASE_URL → we export it as DATABASE_URL BEFORE infisical run
@@ -151,7 +151,7 @@ wait_for_postgres() {
151
151
 
152
152
  # If we have psql, do a deeper health check
153
153
  if command -v psql >/dev/null 2>&1; then
154
- if PGPASSWORD="${db_password:-}" psql -h "${db_host}" -p "${db_port}" -U "${db_user:-beauty_crm}" -d "${db_name}" -c "SELECT 1" >/dev/null 2>&1; then
154
+ if PGPASSWORD="${db_password:-}" psql -h "${db_host}" -p "${db_port}" -U "${db_user:-tdk_project}" -d "${db_name}" -c "SELECT 1" >/dev/null 2>&1; then
155
155
  log_success "✅ Database ${db_name} is ready!"
156
156
  return 0
157
157
  fi
@@ -191,6 +191,47 @@ def test_resolve_manifest_dependency_names_exact_match(tmp_path):
191
191
  assert _run_resolve_names(tmp_path, manifest, all_services_map) == ["identity-management-backend"]
192
192
 
193
193
 
194
+ def test_prebuild_dockerignore_mirrors_docker_build_only_and_ignore(tmp_path):
195
+ """A deferStart resource's image is pre-built by a plain `docker build`
196
+ (auto_init=False skips the dc_resource's own docker_build -- found live,
197
+ task 7.2). A plain build has no only= filter, so the generated allowlist
198
+ must include exactly what docker_build's only= does (root lockfiles, the
199
+ service, docker-templates, its internal libraries) and re-exclude its
200
+ ignore= patterns, or the whole project root becomes the build context."""
201
+ project = tmp_path / "project"
202
+ service = project / "services" / "analytics" / "data-governance-api"
203
+ (service / ".autogenerated").mkdir(parents=True)
204
+ (service / "package.json").write_text(
205
+ json.dumps({"dependencies": {"@tdk-project/platform-logging": "workspace:*", "zod": "^3"}})
206
+ )
207
+ dockerfile = service / ".autogenerated" / "Dockerfile.app.autogenerated"
208
+ dockerfile.write_text("FROM scratch\n")
209
+ (service / ".autogenerated" / "Dockerfile.app.autogenerated.dockerignore").write_text(
210
+ "# comment\nnode_modules\n*.md\n!.env.example\n"
211
+ )
212
+ tiltdir = tmp_path / "tilt"
213
+ tiltdir.mkdir()
214
+ r = run_starlark(
215
+ tiltdir,
216
+ "load('@ORCHESTRATOR/builders/typescript.star', 'prebuild_dockerignore')\n"
217
+ f"r = {{'content': prebuild_dockerignore({str(dockerfile)!r}, {str(project)!r}, "
218
+ "'services/analytics/data-governance-api')}\n",
219
+ )
220
+ lines = r["content"].splitlines()
221
+ res = "services/analytics/data-governance-api"
222
+ assert lines[0] == "*"
223
+ for allowed in ["package.json", "bun.lock", res, "shared-platform-engineering/docker-templates",
224
+ "shared-platform-engineering/platform-logging"]:
225
+ assert "!" + allowed in lines, (allowed, lines)
226
+ # Only the service's own relative path is allowed, never the absolute one.
227
+ assert not any(line.startswith("!" + str(project)) for line in lines), lines
228
+ # Re-excludes come after the allowlist (dockerignore: later lines win).
229
+ assert res + "/node_modules" in lines and res + "/*.md" in lines
230
+ assert lines.index(res + "/node_modules") > lines.index("!" + res)
231
+ # The Dockerfile's own negations aren't carried over (same as docker_build's ignore=).
232
+ assert not any(".env.example" in line for line in lines)
233
+
234
+
194
235
  def test_resolve_manifest_dependency_names_dedupes_and_skips_empty(tmp_path):
195
236
  manifest = {"dependsOn": []}
196
237
  assert _run_resolve_names(tmp_path, manifest, {}) == []
@@ -144,3 +144,17 @@ def test_standalone_compose_enables_file_provider_and_wake_gateway(tmp_path):
144
144
  assert "/etc/traefik/dynamic:ro" in yaml_text
145
145
  assert "wake-gateway:" in yaml_text
146
146
  assert "/root/.tilt-dev:ro" in yaml_text
147
+
148
+
149
+ @pytest.mark.skipif(shutil.which("tilt") is None, reason="tilt CLI not installed")
150
+ def test_wake_gateway_mounts_project_root_at_its_own_absolute_path(tmp_path):
151
+ """The gateway runs `docker compose up --no-build` with the same compose
152
+ files Tilt uses; relative paths inside them resolve client-side, so the
153
+ project must exist at the same absolute path in the gateway container."""
154
+ r = _run_starlark(
155
+ tmp_path,
156
+ "os.environ['TDK_PROJECT_ROOT'] = '/Users/dev/tdk-erp-system/.tdk/.tdk-out/../..'\n"
157
+ "load('@COMPOSE/traefik_standalone.star', 'generate_standalone_traefik_compose')\n"
158
+ "r = {'yaml': generate_standalone_traefik_compose()}\n",
159
+ )
160
+ assert "- /Users/dev/tdk-erp-system:/Users/dev/tdk-erp-system:ro" in r["yaml"]
@@ -106,6 +106,31 @@ def test_static_route_headers_carry_container_names_not_just_resource_names(tmp_
106
106
  assert 'X-Wake-Proxy-Port: "4604"' in yaml
107
107
 
108
108
 
109
+ def test_static_route_strips_the_same_api_path_prefix_the_docker_router_does(tmp_path):
110
+ """Found live against tdk-erp-system (task 7.2): the app itself only
111
+ serves its bare health path (e.g. `/health`), not the full
112
+ `/api/data-governance/health` the public route matches on -- the real
113
+ Docker-label `-project` router strips that prefix (traefik.star), and
114
+ the wake route's own proxy-through must do the same or a woken resource
115
+ 404s even though the wake itself succeeded."""
116
+ r = run_starlark(
117
+ tmp_path,
118
+ "load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
119
+ f"manifest = {MANIFEST!r}\n"
120
+ "r = {'yaml': generate_static_wake_route(\n"
121
+ " 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
122
+ " 'tdk-wake-gateway', 9999,\n"
123
+ ")}\n",
124
+ )
125
+ yaml = r["yaml"]
126
+ assert "data-governance-api-wake-strip" in yaml
127
+ assert 'prefixes:\n - "/api/data-governance"' in yaml
128
+ # Both middlewares must actually be attached to the router, in strip-then-headers order.
129
+ strip_idx = yaml.index("- data-governance-api-wake-strip")
130
+ headers_idx = yaml.index("- data-governance-api-wake-headers")
131
+ assert strip_idx < headers_idx
132
+
133
+
109
134
  def test_static_route_with_no_dependencies_omits_deps_header_value(tmp_path):
110
135
  r = run_starlark(
111
136
  tmp_path,
@@ -117,3 +142,46 @@ def test_static_route_with_no_dependencies_omits_deps_header_value(tmp_path):
117
142
  ")}\n",
118
143
  )
119
144
  assert 'X-Wake-Deps: ""' in r["yaml"]
145
+ # No compose details passed -> the gateway can only use `tilt trigger`.
146
+ assert "X-Wake-Compose-" not in r["yaml"]
147
+ assert "X-Wake-Image" not in r["yaml"]
148
+
149
+
150
+ def test_normalize_abs_path_collapses_dot_segments(tmp_path):
151
+ """Tilt's os.path.abspath leaves `..` in place; the generated Tiltfile's
152
+ project root is config.main_dir + '/../..', so this must collapse it."""
153
+ r = run_starlark(
154
+ tmp_path,
155
+ "load('@NETWORKING/traefik_static_routes.star', 'normalize_abs_path')\n"
156
+ "r = {\n"
157
+ " 'root': normalize_abs_path('/Users/dev/tdk-erp-system/.tdk/.tdk-out/../..'),\n"
158
+ " 'dot': normalize_abs_path('/a/./b//c/'),\n"
159
+ " 'empty': normalize_abs_path(''),\n"
160
+ "}\n",
161
+ )
162
+ assert r == {"root": "/Users/dev/tdk-erp-system", "dot": "/a/b/c", "empty": ""}
163
+
164
+
165
+ def test_static_route_carries_compose_invocation_for_direct_start(tmp_path):
166
+ """With compose details, the gateway can `docker compose up --no-build` the
167
+ resource itself instead of waiting in Tilt's build queue (task 7.2 finding)."""
168
+ root = "/Users/dev/tdk-erp-system"
169
+ compose_file = root + "/services/analytics/.autogenerated/docker-compose.app.autogenerated.yml"
170
+ override_file = root + "/services/analytics/.autogenerated/docker-compose.port-override.yml"
171
+ r = run_starlark(
172
+ tmp_path,
173
+ "load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
174
+ f"manifest = {MANIFEST!r}\n"
175
+ "r = {'yaml': generate_static_wake_route(\n"
176
+ " 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
177
+ " 'tdk-wake-gateway', 9999,\n"
178
+ f" compose_files=[{compose_file!r}, {override_file!r}],\n"
179
+ f" env_file={root + '/.env'!r},\n"
180
+ " image='analytics_data-governance-api:dev',\n"
181
+ ")}\n",
182
+ )
183
+ yaml = r["yaml"]
184
+ assert 'X-Wake-Compose-Project: "analytics-autogenerated"' in yaml
185
+ assert f'X-Wake-Compose-Files: "{compose_file},{override_file}"' in yaml
186
+ assert f'X-Wake-Env-File: "{root}/.env"' in yaml
187
+ assert 'X-Wake-Image: "analytics_data-governance-api:dev"' in yaml
@@ -1,62 +0,0 @@
1
- # TDK Codebase Audit Report
2
-
3
- Generated: 2026-04-29
4
-
5
- ## Summary
6
-
7
- - **Total markers found:** 4
8
- - **CLI (User-facing):** 4
9
- - **Discovery:** 0
10
- - **Engine:** 0
11
-
12
- ## Findings
13
-
14
- ### CLI (User-Facing) - Priority: HIGH
15
-
16
- ```
17
- cli/src/commands/resource.ts:164: // TODO: Check database, cache, etc.
18
- cli/src/commands/resource.ts:168:// TODO: Add your routes here
19
- cli/src/commands/resource.ts:231:// TODO: Implement your worker logic here
20
- cli/src/commands/config.ts:32: // TODO: Implement diff logic
21
- ```
22
-
23
- **Details:**
24
-
25
- 1. **resource.ts:164** - Database/cache health check TODO in backend index template
26
- - Context: Health check endpoint needs to verify database/cache connectivity
27
- - Impact: Generated services won't properly report health status
28
-
29
- 2. **resource.ts:168** - Routes placeholder TODO in backend index template
30
- - Context: Placeholder comment for developers to add their own routes
31
- - Impact: Documentation/placeholder - low functional impact
32
-
33
- 3. **resource.ts:231** - Worker logic TODO in worker index template
34
- - Context: Worker template only has a basic loop, no actual job processing
35
- - Impact: Generated workers aren't functional without implementation
36
-
37
- 4. **config.ts:32** - Diff logic TODO in config regenerate command
38
- - Context: Dry-run mode shows what would change but doesn't implement actual diff
39
- - Impact: Users can't see differences between current and new config
40
-
41
- ### Discovery - Priority: MEDIUM
42
-
43
- ```
44
- No findings
45
- ```
46
-
47
- ### Engine - Priority: MEDIUM
48
-
49
- ```
50
- No findings
51
- ```
52
-
53
- ## Next Steps
54
-
55
- 1. ✅ Review HIGH priority (CLI) findings - COMPLETED
56
- 2. Implement or remove TODOs as appropriate:
57
- - Implement health check with database/cache verification (resource.ts:164)
58
- - Add example routes template (resource.ts:168)
59
- - Create functional worker template with job processing structure (resource.ts:231)
60
- - Implement diff logic for config dry-run (config.ts:32)
61
- 3. Add tests for any new functionality
62
- 4. Re-run audit to verify cleanup
@@ -1,5 +0,0 @@
1
- schema: spec-driven
2
- created: 2026-05-04
3
- status: completed
4
- completed: 2026-05-04
5
- commit: cfa2ffc
@@ -1,94 +0,0 @@
1
- ## Context
2
-
3
- TDK CLI is a multi-language monorepo (TypeScript CLI, Python Discovery, Starlark Engine) that has grown organically. Previous cleanup efforts have addressed specific issues, but a comprehensive, systematic cleanup across all dimensions is needed.
4
-
5
- **Current State:**
6
- - Code duplication exists across CLI commands and utilities
7
- - Type definitions are scattered and sometimes duplicated
8
- - Unused imports and functions remain from refactors
9
- - Circular dependencies exist in the module graph
10
- - Weak types (`any`, `unknown`) are used extensively
11
- - Defensive try/catch patterns hide errors unnecessarily
12
- - Legacy fallback code remains from migrations
13
- - AI-generated comments and stubs litter the codebase
14
-
15
- **Previous Cleanup Work:**
16
- - Dead code elimination assessment completed (2026-04-30)
17
- - Circular dependency critical assessment completed (2026-05-01)
18
- - Type assessment report available (2026-05-03)
19
- - AI slop assessment completed (2026-05-03)
20
- - Deduplication assessment completed
21
-
22
- ## Goals / Non-Goals
23
-
24
- **Goals:**
25
- - Achieve 100% DRY compliance for code patterns appearing 3+ times
26
- - Consolidate all shared types into centralized type modules
27
- - Remove 100% of verified unused code (knip + manual verification)
28
- - Resolve all circular dependencies in the module graph
29
- - Replace 95%+ of weak types with strong, researched types
30
- - Remove all unnecessary defensive programming patterns
31
- - Eliminate all deprecated, legacy, and fallback code
32
- - Remove all AI slop, stubs, and unhelpful comments
33
-
34
- **Non-Goals:**
35
- - Complete rewrites of working functionality
36
- - Breaking changes to public APIs
37
- - Adding new features during cleanup
38
- - Perfection at the cost of progress (80/20 rule applies)
39
-
40
- ## Decisions
41
-
42
- **Decision 1: Research-First Approach for Types**
43
- - **Rationale**: Weak types must be replaced with correct types, not guesses
44
- - **Implementation**: Research actual runtime values, package types, and usage patterns before replacing `any`/`unknown`
45
-
46
- **Decision 2: Verification Before Removal**
47
- - **Rationale**: Dead code removal must not break working systems
48
- - **Implementation**: Use knip for detection, then verify with manual analysis and tests before removal
49
-
50
- **Decision 3: Parallel Subagent Execution**
51
- - **Rationale**: 8 independent workstreams can proceed simultaneously
52
- - **Implementation**: Each dimension has a dedicated subagent with clear scope
53
-
54
- **Decision 4: Critical Assessment Required**
55
- - **Rationale**: Each subagent must deeply understand current state before changing
56
- - **Implementation**: Every subagent produces a critical assessment document before implementing
57
-
58
- **Decision 5: High Confidence Only**
59
- - **Rationale**: Low-confidence changes risk introducing bugs
60
- - **Implementation**: Only implement recommendations with clear evidence and low risk
61
-
62
- ## Risks / Trade-offs
63
-
64
- **Risk: Removing code that appears unused but is dynamically loaded**
65
- - **Mitigation**: Manual verification beyond static analysis; test execution verification
66
-
67
- **Risk: Type replacements break runtime behavior**
68
- - **Mitigation**: Research actual values; use runtime checks if needed
69
-
70
- **Risk: Circular dependency fixes require architectural changes**
71
- - **Mitigation**: Document current state; propose minimal fixes; avoid major refactors
72
-
73
- **Risk: Defensive programming removal exposes hidden bugs**
74
- - **Mitigation**: Replace error-hiding with explicit error handling only where needed
75
-
76
- **Risk: Scope creep across 8 dimensions**
77
- - **Mitigation**: Strict boundaries per subagent; time-box each dimension
78
-
79
- ## Migration Plan
80
-
81
- No user-facing migration needed. Internal changes only.
82
-
83
- Deployment steps:
84
- 1. Execute all 8 subagents in parallel for research phase
85
- 2. Review all critical assessments
86
- 3. Implement high-confidence changes per dimension
87
- 4. Run full test suite after each dimension completes
88
- 5. Merge incrementally by dimension to limit blast radius
89
-
90
- ## Open Questions
91
-
92
- 1. Should we establish permanent tooling (knip, madge) in CI?
93
- 2. What's the policy for `any` types that are truly unavoidable?
94
- 3. Should we create a style guide to prevent future AI slop?