container-superposition 0.1.8 → 0.1.9

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 (162) hide show
  1. package/dist/tool/commands/adopt.js +1 -1
  2. package/dist/tool/commands/adopt.js.map +1 -1
  3. package/dist/tool/commands/doctor.d.ts +1 -0
  4. package/dist/tool/commands/doctor.d.ts.map +1 -1
  5. package/dist/tool/commands/doctor.js +1370 -73
  6. package/dist/tool/commands/doctor.js.map +1 -1
  7. package/dist/tool/questionnaire/composer.d.ts +3 -1
  8. package/dist/tool/questionnaire/composer.d.ts.map +1 -1
  9. package/dist/tool/questionnaire/composer.js +87 -18
  10. package/dist/tool/questionnaire/composer.js.map +1 -1
  11. package/dist/tool/questionnaire/presets.d.ts.map +1 -1
  12. package/dist/tool/questionnaire/presets.js +1 -0
  13. package/dist/tool/questionnaire/presets.js.map +1 -1
  14. package/dist/tool/questionnaire/questionnaire.d.ts.map +1 -1
  15. package/dist/tool/questionnaire/questionnaire.js +3 -1
  16. package/dist/tool/questionnaire/questionnaire.js.map +1 -1
  17. package/dist/tool/schema/project-config.d.ts.map +1 -1
  18. package/dist/tool/schema/project-config.js +5 -1
  19. package/dist/tool/schema/project-config.js.map +1 -1
  20. package/dist/tool/schema/types.d.ts +4 -2
  21. package/dist/tool/schema/types.d.ts.map +1 -1
  22. package/docs/overlays.md +158 -147
  23. package/docs/specs/001-verbose-plan-graph/spec.md +5 -12
  24. package/docs/specs/002-superposition-config-file/spec.md +5 -12
  25. package/docs/specs/003-mkdocs2-overlay/spec.md +2 -9
  26. package/docs/specs/004-doctor-fix/spec.md +1 -8
  27. package/docs/specs/005-cuda-overlay/spec.md +2 -9
  28. package/docs/specs/006-rocm-overlay/spec.md +3 -10
  29. package/docs/specs/007-target-aware-generation/spec.md +4 -11
  30. package/docs/specs/008-project-file-canonical/spec.md +7 -8
  31. package/docs/specs/009-project-env/spec.md +3 -10
  32. package/docs/specs/010-compose-env-materialization/spec.md +3 -10
  33. package/docs/specs/011-overlay-parameters/spec.md +2 -9
  34. package/docs/specs/012-ollama-cli-overlay/spec.md +47 -0
  35. package/docs/specs/013-doctor-dependency-check/spec.md +250 -0
  36. package/docs/specs/014-doctor-compose-port-cross-validation/spec.md +276 -0
  37. package/docs/specs/015-doctor-env-example-drift/spec.md +248 -0
  38. package/docs/specs/016-doctor-reproducibility-check/spec.md +276 -0
  39. package/docs/specs/017-doctor-dry-run/spec.md +276 -0
  40. package/docs/specs/{007-init-project-file → 018-init-project-file}/spec.md +2 -9
  41. package/docs/specs/taxonomy.md +186 -0
  42. package/overlays/.presets/full-observability.yml +113 -0
  43. package/overlays/.presets/k8s-dev.yml +174 -0
  44. package/overlays/.presets/local-llm.yml +105 -0
  45. package/overlays/.presets/vector-ai.yml +150 -0
  46. package/overlays/.shared/vscode/js-ts-settings.json +19 -0
  47. package/overlays/.shared/vscode/markdown-extensions.json +8 -0
  48. package/overlays/alertmanager/devcontainer.patch.json +0 -1
  49. package/overlays/alertmanager/docker-compose.yml +8 -0
  50. package/overlays/alertmanager/overlay.yml +1 -0
  51. package/overlays/amp/devcontainer.patch.json +4 -1
  52. package/overlays/bun/devcontainer.patch.json +1 -10
  53. package/overlays/bun/overlay.yml +8 -1
  54. package/overlays/claude-code/devcontainer.patch.json +6 -1
  55. package/overlays/codex/devcontainer.patch.json +5 -0
  56. package/overlays/comfyui/docker-compose.yml +1 -0
  57. package/overlays/comfyui/overlay.yml +4 -0
  58. package/overlays/commitlint/devcontainer.patch.json +1 -6
  59. package/overlays/docker-sock/overlay.yml +1 -0
  60. package/overlays/dotnet/overlay.yml +4 -1
  61. package/overlays/fuseki/.env.example +5 -0
  62. package/overlays/fuseki/README.md +173 -0
  63. package/overlays/fuseki/devcontainer.patch.json +18 -0
  64. package/overlays/fuseki/docker-compose.yml +29 -0
  65. package/overlays/fuseki/overlay.yml +42 -0
  66. package/overlays/fuseki/verify.sh +58 -0
  67. package/overlays/gemini-cli/devcontainer.patch.json +4 -1
  68. package/overlays/go/overlay.yml +6 -1
  69. package/overlays/grafana/devcontainer.patch.json +0 -1
  70. package/overlays/grafana/docker-compose.yml +8 -2
  71. package/overlays/grafana/overlay.yml +6 -1
  72. package/overlays/jaeger/.env.example +11 -0
  73. package/overlays/jaeger/README.md +33 -4
  74. package/overlays/jaeger/devcontainer.patch.json +9 -1
  75. package/overlays/jaeger/docker-compose.yml +17 -0
  76. package/overlays/jaeger/overlay.yml +1 -12
  77. package/overlays/java/overlay.yml +6 -1
  78. package/overlays/jupyter/docker-compose.yml +1 -0
  79. package/overlays/jupyter/overlay.yml +1 -0
  80. package/overlays/keycloak/devcontainer.patch.json +0 -1
  81. package/overlays/keycloak/docker-compose.yml +1 -0
  82. package/overlays/keycloak/overlay.yml +15 -0
  83. package/overlays/localstack/docker-compose.yml +1 -0
  84. package/overlays/localstack/overlay.yml +19 -1
  85. package/overlays/loki/devcontainer.patch.json +0 -1
  86. package/overlays/loki/docker-compose.yml +8 -0
  87. package/overlays/loki/overlay.yml +1 -0
  88. package/overlays/mailpit/docker-compose.yml +1 -0
  89. package/overlays/mailpit/overlay.yml +1 -0
  90. package/overlays/minio/devcontainer.patch.json +1 -1
  91. package/overlays/minio/docker-compose.yml +1 -0
  92. package/overlays/minio/overlay.yml +23 -2
  93. package/overlays/mkdocs/devcontainer.patch.json +1 -5
  94. package/overlays/mkdocs/overlay.yml +3 -1
  95. package/overlays/mkdocs2/devcontainer.patch.json +1 -5
  96. package/overlays/mkdocs2/overlay.yml +2 -0
  97. package/overlays/mongodb/docker-compose.yml +2 -0
  98. package/overlays/mongodb/overlay.yml +26 -2
  99. package/overlays/mysql/docker-compose.yml +2 -0
  100. package/overlays/mysql/overlay.yml +36 -2
  101. package/overlays/nats/docker-compose.yml +1 -0
  102. package/overlays/nats/overlay.yml +18 -2
  103. package/overlays/nodejs/devcontainer.patch.json +1 -12
  104. package/overlays/nodejs/overlay.yml +8 -1
  105. package/overlays/ollama/README.md +4 -3
  106. package/overlays/ollama/docker-compose.yml +1 -0
  107. package/overlays/ollama/overlay.yml +6 -1
  108. package/overlays/ollama/verify.sh +5 -28
  109. package/overlays/ollama-cli/README.md +90 -0
  110. package/overlays/ollama-cli/devcontainer.patch.json +3 -0
  111. package/overlays/ollama-cli/overlay.yml +19 -0
  112. package/overlays/{ollama → ollama-cli}/setup.sh +7 -10
  113. package/overlays/ollama-cli/verify.sh +49 -0
  114. package/overlays/open-webui/docker-compose.yml +1 -0
  115. package/overlays/open-webui/overlay.yml +8 -1
  116. package/overlays/opencode/devcontainer.patch.json +4 -1
  117. package/overlays/otel-collector/README.md +4 -0
  118. package/overlays/otel-collector/devcontainer.patch.json +4 -1
  119. package/overlays/otel-collector/docker-compose.yml +8 -4
  120. package/overlays/otel-collector/overlay.yml +1 -0
  121. package/overlays/otel-demo-nodejs/devcontainer.patch.json +0 -1
  122. package/overlays/otel-demo-nodejs/docker-compose.yml +1 -0
  123. package/overlays/otel-demo-nodejs/overlay.yml +9 -1
  124. package/overlays/otel-demo-python/devcontainer.patch.json +0 -1
  125. package/overlays/otel-demo-python/docker-compose.yml +1 -0
  126. package/overlays/otel-demo-python/overlay.yml +6 -1
  127. package/overlays/pandoc/README.md +10 -0
  128. package/overlays/pandoc/devcontainer.patch.json +0 -5
  129. package/overlays/pandoc/overlay.yml +2 -0
  130. package/overlays/pandoc/setup.sh +10 -0
  131. package/overlays/pgvector/devcontainer.patch.json +11 -5
  132. package/overlays/pgvector/docker-compose.yml +1 -0
  133. package/overlays/pgvector/overlay.yml +3 -0
  134. package/overlays/playwright/devcontainer.patch.json +0 -5
  135. package/overlays/playwright/overlay.yml +2 -1
  136. package/overlays/postgres/docker-compose.yml +1 -0
  137. package/overlays/postgres/overlay.yml +4 -1
  138. package/overlays/pre-commit/devcontainer.patch.json +1 -7
  139. package/overlays/prometheus/devcontainer.patch.json +0 -1
  140. package/overlays/prometheus/docker-compose.yml +8 -0
  141. package/overlays/prometheus/overlay.yml +1 -0
  142. package/overlays/promtail/devcontainer.patch.json +1 -2
  143. package/overlays/promtail/docker-compose.yml +8 -0
  144. package/overlays/promtail/overlay.yml +1 -0
  145. package/overlays/qdrant/docker-compose.yml +1 -0
  146. package/overlays/qdrant/overlay.yml +5 -1
  147. package/overlays/rabbitmq/docker-compose.yml +1 -0
  148. package/overlays/rabbitmq/overlay.yml +25 -2
  149. package/overlays/redis/docker-compose.yml +7 -0
  150. package/overlays/redis/overlay.yml +15 -1
  151. package/overlays/redpanda/docker-compose.yml +1 -0
  152. package/overlays/redpanda/overlay.yml +15 -3
  153. package/overlays/rocm/overlay.yml +2 -1
  154. package/overlays/rust/overlay.yml +3 -1
  155. package/overlays/sqlserver/docker-compose.yml +1 -0
  156. package/overlays/sqlserver/overlay.yml +17 -0
  157. package/overlays/tempo/devcontainer.patch.json +0 -1
  158. package/overlays/tempo/docker-compose.yml +8 -0
  159. package/overlays/tempo/overlay.yml +1 -0
  160. package/overlays/windsurf-cli/devcontainer.patch.json +4 -1
  161. package/package.json +1 -1
  162. package/overlays/.shared/otel/otel-base-config.yaml +0 -30
@@ -0,0 +1,276 @@
1
+ # Feature Specification: Doctor Compose / Port Cross-Validation
2
+
3
+ **Spec ID**: `014-doctor-compose-port-cross-validation`
4
+ **Taxonomy**: `CLI-UX`
5
+ **Created**: 2026-04-24
6
+ **Author**: PM Agent
7
+ **Status**: Approved
8
+ **Input**: Feature assessment — `cs doctor` does not cross-check that ports declared in `devcontainer.json` `forwardPorts` match ports actually exposed by Docker Compose services, nor that compose service ports are accessible through the devcontainer configuration.
9
+
10
+ ## Problem Statement
11
+
12
+ `cs doctor` already checks that individual overlay port declarations do not collide, but it never
13
+ verifies cross-file consistency: a port that is `EXPOSE`d or bound in `docker-compose.yml` may be
14
+ absent from `devcontainer.json` `forwardPorts` (making it inaccessible from the host), or
15
+ `forwardPorts` may list a port that no compose service actually exposes (dead entry). These
16
+ mismatches are invisible until the developer tries to connect to a service and finds it
17
+ unreachable or wonders why a port they never requested is being forwarded.
18
+
19
+ ## Goals
20
+
21
+ - Detect ports in `devcontainer.json` `forwardPorts` that do not correspond to any exposed port
22
+ across the compose services in `docker-compose.yml`.
23
+ - Detect ports exposed by compose services that are absent from `forwardPorts` (informational
24
+ warning, not a hard failure — some ports are intentionally internal).
25
+ - Operate on the generated output files, not the overlay sources, so the check reflects the
26
+ actual composed result.
27
+
28
+ ## Non-Goals
29
+
30
+ - Checking that ports are not in use on the host machine at doctor-run time (that is a runtime
31
+ concern, not a configuration concern).
32
+ - Modifying the port allocation algorithm in `composer.ts`.
33
+ - Detecting intra-container port conflicts between services (already handled by the port
34
+ uniqueness check during composition).
35
+ - Auto-fixing missing `forwardPorts` entries — the right fix is a `cs regen` once overlays are
36
+ corrected; port additions require overlay-level decisions.
37
+
38
+ ## Design
39
+
40
+ ### Port cross-validation check
41
+
42
+ `checkPortCrossValidation(outputPath)` is a new synchronous function in
43
+ `tool/commands/doctor.ts`. It operates on the generated output directory and returns
44
+ `CheckResult[]`.
45
+
46
+ **Early return**: if there is no `docker-compose.yml` in `outputPath` (plain devcontainer stack),
47
+ return a single pass ("No compose stack — port cross-validation skipped").
48
+
49
+ **Step 1 — Collect forwarded ports**
50
+
51
+ Parse `devcontainer.json` from `outputPath`. Extract `forwardPorts` array. Each entry may be a
52
+ bare port number, a `"host:container"` string, or a `"container/proto"` string. Normalise to the
53
+ integer container port. If `devcontainer.json` has no `forwardPorts` key, treat as empty array.
54
+
55
+ **Step 2 — Collect exposed compose ports**
56
+
57
+ Parse `docker-compose.yml` from `outputPath`. For each service, collect:
58
+
59
+ - `ports:` entries (short form `"host:container"`, `"container"`, `"host:container/proto"`; long
60
+ form object with `target`). Normalise to the integer container (target) port.
61
+ - `expose:` entries (container-internal; not host-forwarded but still reachable within the
62
+ devcontainer network).
63
+
64
+ Build two sets:
65
+
66
+ - `boundPorts` — ports present in `ports:` (host-forwarded)
67
+ - `exposedPorts` — ports present in `expose:` only (container-internal)
68
+
69
+ **Step 3 — Cross-check**
70
+
71
+ 1. For each port in `forwardPorts`: if it is not in `boundPorts ∪ exposedPorts` →
72
+ **fail** ("Port `<N>` is listed in `forwardPorts` but is not exposed by any compose service").
73
+ 2. For each port in `boundPorts` that is not in `forwardPorts` →
74
+ **warn** ("Port `<N>` is bound by a compose service but is not in `forwardPorts` — it may be
75
+ inaccessible from the host").
76
+ 3. `exposedPorts`-only entries that are absent from `forwardPorts` are **not** reported (internal
77
+ communication is intentional).
78
+
79
+ Pass check message: "`N` forwarded port(s) all match compose service declarations."
80
+
81
+ ### Fix eligibility
82
+
83
+ Port cross-validation findings are **not auto-fixable** (`manual-only`). The correct resolution
84
+ is to edit the overlay's `devcontainer.patch.json` or run `cs regen` after fixing the overlay.
85
+ Both unknown forwarded ports (typos) and missing `forwardPorts` (coverage gaps) require the
86
+ developer to make an intentional decision.
87
+
88
+ ### DoctorReport changes
89
+
90
+ `DoctorReport` gains a `portCrossValidation: CheckResult[]` field.
91
+
92
+ `generateReport()` gains a `portCrossValidationChecks` parameter.
93
+
94
+ `formatAsText()` gains a "Port Cross-Validation" section that shows only failures and warnings;
95
+ the section is suppressed entirely if all checks pass (matching the conventions for the
96
+ `dependencies` and `parameters` sections).
97
+
98
+ `reportToFindings()` adds:
99
+
100
+ ```typescript
101
+ ...checksToFindings(report.portCrossValidation, 'ports', 'full'),
102
+ ```
103
+
104
+ `executeFixRun()` calls `checkPortCrossValidation()` in the re-check pass.
105
+
106
+ `doctorCommand()` calls `checkPortCrossValidation(outputPath)` and passes results to
107
+ `generateReport()`.
108
+
109
+ ### Port normalisation rules
110
+
111
+ The normalisation helper `parseContainerPort(entry: string | number): number | null` extracts
112
+ the container-side port from any `ports:` or `forwardPorts:` entry format:
113
+
114
+ | Input | Result |
115
+ | --------------------- | ------ |
116
+ | `5432` | `5432` |
117
+ | `"5432"` | `5432` |
118
+ | `"5432/tcp"` | `5432` |
119
+ | `"5433:5432"` | `5432` |
120
+ | `"5433:5432/tcp"` | `5432` |
121
+ | `{ target: 5432 }` | `5432` |
122
+ | `"0.0.0.0:5433:5432"` | `5432` |
123
+
124
+ Returns `null` for unparseable entries (log a warning, skip the entry).
125
+
126
+ ### Affected files
127
+
128
+ | File | Change |
129
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
130
+ | `tool/commands/doctor.ts` | Add `checkPortCrossValidation()`, `parseContainerPort()`, wire into report infrastructure, `PRIORITY` map |
131
+ | `tool/__tests__/commands.test.ts` | Tests for: no compose stack (skip), matching ports (pass), forward-only port (fail), bound-only port (warn), mixed scenario |
132
+ | `CHANGELOG.md` | Entry under `### Added` |
133
+
134
+ ### User-visible behaviour
135
+
136
+ ```
137
+ Port Cross-Validation:
138
+ ✗ Port 9090 is listed in forwardPorts but is not exposed by any compose service
139
+ → Remove port 9090 from forwardPorts or add it to a compose service
140
+ ⚠ Port 5432 is bound by postgres service but is not in forwardPorts — it may be inaccessible
141
+ → Add 5432 to forwardPorts in your overlay's devcontainer.patch.json, then run cs regen
142
+ ✓ Port 6379 (redis): forwarded and exposed — OK
143
+ ```
144
+
145
+ ### Backward compatibility
146
+
147
+ No changes to generated files or project file format. Purely additive check on existing output.
148
+
149
+ ## User Scenarios & Testing
150
+
151
+ ### User Story 1 — Stale `forwardPorts` entry caught (P1)
152
+
153
+ A developer removed the `prometheus` overlay from their project but `forwardPorts` retained
154
+ port `9090`. Doctor now flags the orphaned entry instead of silently forwarding a port to
155
+ nowhere.
156
+
157
+ **Why this priority**: A `forwardPorts` entry with no backing service is confusing and indicates
158
+ an inconsistent configuration. It is the clearest actionable signal.
159
+
160
+ **Independent Test**: Write a `devcontainer.json` with `forwardPorts: [9090]` and a
161
+ `docker-compose.yml` with no service exposing 9090. Run `doctorCommand`. Assert `fail` finding
162
+ mentioning port 9090 not exposed by any service.
163
+
164
+ **Acceptance Scenarios**:
165
+
166
+ 1. **Given** `forwardPorts: [9090]` and no compose service exposes 9090, **When** `cs doctor`
167
+ runs, **Then** a `fail` finding reports "Port 9090 is listed in forwardPorts but is not
168
+ exposed by any compose service".
169
+ 2. **Given** `forwardPorts: [5432]` and postgres exposes `5432`, **When** `cs doctor` runs,
170
+ **Then** no failure is reported for port 5432.
171
+
172
+ ---
173
+
174
+ ### User Story 2 — Silently inaccessible compose port flagged (P2)
175
+
176
+ A developer composed `redis` and `postgres` but their devcontainer patch never added the postgres
177
+ port to `forwardPorts`. Doctor warns them that port 5432 is bound but not forwarded.
178
+
179
+ **Why this priority**: Missing `forwardPorts` is easy to overlook and causes "connection refused"
180
+ errors that are hard to diagnose without knowing about `forwardPorts`.
181
+
182
+ **Independent Test**: Write a `docker-compose.yml` with postgres exposing port `5432` on the
183
+ host. Write a `devcontainer.json` with `forwardPorts: [6379]`. Run `doctorCommand`. Assert
184
+ a `warn` finding for port 5432.
185
+
186
+ **Acceptance Scenarios**:
187
+
188
+ 1. **Given** postgres exposes host port 5432 and `forwardPorts` does not include 5432, **When**
189
+ `cs doctor` runs, **Then** a `warn` finding reports port 5432 not in `forwardPorts`.
190
+ 2. **Given** all compose ports are also in `forwardPorts`, **When** `cs doctor` runs, **Then**
191
+ the port cross-validation section is suppressed (all pass).
192
+
193
+ ---
194
+
195
+ ### User Story 3 — Plain devcontainer stack skipped (P3)
196
+
197
+ A developer uses a plain devcontainer (no Docker Compose). Doctor skips port cross-validation
198
+ gracefully.
199
+
200
+ **Acceptance Scenarios**:
201
+
202
+ 1. **Given** no `docker-compose.yml` in the output directory, **When** `cs doctor` runs, **Then**
203
+ port cross-validation returns a single pass and the section is suppressed in text output.
204
+
205
+ ---
206
+
207
+ ### Edge Cases
208
+
209
+ - `forwardPorts` absent from `devcontainer.json`: treated as empty — warn for every bound port.
210
+ - Compose service with only `expose:` (not `ports:`): not warned if absent from `forwardPorts`
211
+ (intentionally container-internal).
212
+ - Port `0` or invalid port entries: skip with a debug trace, do not report as a failure.
213
+ - Compose file not parseable: return a single `fail` ("Could not parse docker-compose.yml for
214
+ port cross-validation — file may be malformed").
215
+
216
+ ## Requirements
217
+
218
+ ### Functional Requirements
219
+
220
+ - **FR-001**: `checkPortCrossValidation()` MUST return `fail` for each port in `forwardPorts`
221
+ that is not in the set of ports declared in the compose `ports:` or `expose:` blocks.
222
+ - **FR-002**: `checkPortCrossValidation()` MUST return `warn` for each port in compose `ports:`
223
+ blocks that is absent from `forwardPorts`.
224
+ - **FR-003**: Ports present only in compose `expose:` blocks (not `ports:`) MUST NOT generate
225
+ a warning if absent from `forwardPorts`.
226
+ - **FR-004**: When no `docker-compose.yml` is present in the output directory, the check MUST
227
+ return a single pass and generate no findings.
228
+ - **FR-005**: The check MUST support all `ports:` entry formats (short string, long object,
229
+ IP-prefixed) using `parseContainerPort()`.
230
+ - **FR-006**: Findings MUST be marked `manual-only` (no auto-fix).
231
+
232
+ ### Key Entities
233
+
234
+ - **`boundPorts`**: set of integer container ports declared in compose `ports:` blocks (host-accessible).
235
+ - **`exposedPorts`**: set of integer container ports declared in compose `expose:` blocks only.
236
+ - **`forwardedPorts`**: set of integer ports from `devcontainer.json` `forwardPorts`.
237
+
238
+ ## Dependencies & Impact
239
+
240
+ - **Affected Areas**: `tool/commands/doctor.ts`, `tool/__tests__/commands.test.ts`, `CHANGELOG.md`
241
+ - **Compatibility Impact**: None — purely additive check category.
242
+ - **Required Documentation Updates**: `CHANGELOG.md`
243
+ - **Verification Plan**: Unit tests in `commands.test.ts`; manual test with a real compose-based
244
+ project file.
245
+
246
+ ## Success Criteria
247
+
248
+ ### Measurable Outcomes
249
+
250
+ - **SC-001**: `cs doctor` on an output directory where `forwardPorts` contains a port not in any
251
+ compose service reports a `fail` within the Port Cross-Validation section.
252
+ - **SC-002**: `cs doctor` on a plain devcontainer (no `docker-compose.yml`) produces no output
253
+ in the Port Cross-Validation section.
254
+ - **SC-003**: `npm test` passes with at least 3 new test cases covering: no compose stack, stale
255
+ forwarded port, missing forwarded port.
256
+ - **SC-004**: No existing doctor tests regress.
257
+
258
+ ## Open Questions
259
+
260
+ | # | Question | Owner | Resolution |
261
+ | --- | ------------------------------------------------------------------------------------- | ----- | ------------------------------------------------------- |
262
+ | 1 | Should bound-but-not-forwarded ports be `warn` or `info`? Both options are defensible | PM | Pending — lean toward `warn` to surface it visibly |
263
+ | 2 | Should `--fix` add missing `forwardPorts` entries by patching the project file? | PM | Pending — deferred; unclear which overlay owns the port |
264
+
265
+ ## Out of Scope
266
+
267
+ - Checking host-side port availability at runtime.
268
+ - Modifying how `composer.ts` generates `forwardPorts`.
269
+ - Validating that overlay-level port declarations match compose file declarations (that is the
270
+ overlay-reviewer agent's job).
271
+
272
+ ## Implementation Notes
273
+
274
+ - `parseContainerPort` strips protocol suffixes (`/tcp`, `/udp`) and range notation (`8080-8090`), returning the numeric port or `null` for non-numeric values.
275
+ - The check skips early when `docker-compose.yml` is absent in the output path (compose-only feature).
276
+ - `js-yaml` is imported statically at the top of `doctor.ts` (was previously a dynamic import in `checkPortCrossValidation`, which caused an `await` in a synchronous function — fixed by moving to a top-level static import).
@@ -0,0 +1,248 @@
1
+ # Feature Specification: Doctor `.env.example` Drift Detection
2
+
3
+ **Spec ID**: `015-doctor-env-example-drift`
4
+ **Taxonomy**: `CLI-UX`
5
+ **Created**: 2026-04-24
6
+ **Author**: PM Agent
7
+ **Status**: Approved
8
+ **Input**: Feature assessment — `cs doctor` does not detect when `.env.example` is stale: missing keys for parameters added by new overlays, or keys left over from overlays that were removed.
9
+
10
+ ## Problem Statement
11
+
12
+ When a developer adds or removes overlays and runs `cs regen`, the generated `docker-compose.yml`
13
+ and `devcontainer.json` are updated, but `.env.example` may drift from the actual parameter set
14
+ in use. A key missing from `.env.example` means a new team member who onboards by copying the
15
+ file will silently omit a required secret. A stale key left behind from a removed overlay
16
+ creates confusion about which variables are needed. Doctor should detect both directions of drift
17
+ and offer a `--fix` that regenerates `.env.example` to match the current overlay selection.
18
+
19
+ ## Goals
20
+
21
+ - Detect parameter keys referenced in the current overlay selection that are absent from the
22
+ generated `.env.example`.
23
+ - Detect keys present in `.env.example` that are no longer declared by any selected overlay.
24
+ - Allow `doctor --fix` to regenerate `.env.example` from the current project configuration.
25
+
26
+ ## Non-Goals
27
+
28
+ - Validating the actual values in `.env` (the user's runtime secrets file — not committed).
29
+ - Checking whether `.env` is gitignored (separate concern).
30
+ - Modifying the parameter schema or the `.env.example` generation logic in `composer.ts`.
31
+ - Generating `.env.example` from scratch when it does not exist (that is covered by spec 012 /
32
+ the existing parameters check in spec implemented for the parameter doctor feature).
33
+
34
+ ## Design
35
+
36
+ ### Env example drift check
37
+
38
+ `checkEnvExampleDrift(overlaysConfig, outputPath, workingDir)` is a new synchronous function in
39
+ `tool/commands/doctor.ts`. It returns `CheckResult[]`.
40
+
41
+ **Early return**: if no project file is present, return empty (no noise). If the output
42
+ directory has no `.env.example`, return a single pass ("No `.env.example` present — skipping
43
+ drift check") — the missing-file case is handled by the parameters check (spec 012).
44
+
45
+ **Step 1 — Collect declared parameter keys**
46
+
47
+ Call `collectOverlayParameters(overlaysConfig, selection.overlays)` where `selection` comes from
48
+ `loadProjectConfig(workingDir)`. This returns the full `ParameterDeclaration[]` for the selected
49
+ overlay set. Extract the set of unique `key` strings: `declaredKeys`.
50
+
51
+ **Step 2 — Parse `.env.example`**
52
+
53
+ Read `outputPath/.env.example`. Parse each non-comment, non-blank line as `KEY=...` (or
54
+ `KEY` alone). Extract the set of keys: `exampleKeys`. Lines starting with `#` are skipped.
55
+ Section header comments (`# --- Service ---`) are also skipped.
56
+
57
+ **Step 3 — Diff**
58
+
59
+ - `missingFromExample = declaredKeys − exampleKeys`: keys declared by overlays but absent from
60
+ `.env.example` → **fail** ("Parameter `<KEY>` declared by overlay `<id>` is missing from
61
+ `.env.example`").
62
+ - `staleInExample = exampleKeys − declaredKeys`: keys in `.env.example` not declared by any
63
+ selected overlay → **warn** ("Key `<KEY>` in `.env.example` is not declared by any selected
64
+ overlay — it may be stale").
65
+
66
+ Pass check message: "`.env.example` is in sync with `N` declared parameter(s)."
67
+
68
+ ### Fix action: `env-example-regen`
69
+
70
+ Registered in `REMEDIATION_REGISTRY`:
71
+
72
+ - **Safety class**: `safe-unattended`
73
+ - **Execution kind**: `regeneration`
74
+ - **Planned changes**:
75
+ - "Regenerate `.env.example` from current overlay selection"
76
+
77
+ `executeEnvExampleRegen(outputPath, overlaysConfig, overlaysDir, workingDir, silent)`:
78
+
79
+ 1. Load project config.
80
+ 2. Rebuild answers via `buildAnswersFromProjectConfig()` + `applyPresetSelections()`.
81
+ 3. Call `composeDevContainer(answers, overlaysDir, { isRegen: true })` — the composer already
82
+ regenerates `.env.example` as part of a full regen.
83
+ 4. Re-check: verify drift findings are resolved.
84
+
85
+ Unknown stale keys are regenerated away (the composer rewrites `.env.example` from scratch). This
86
+ is safe because `.env.example` is a committed template, not a secrets file.
87
+
88
+ ### DoctorReport changes
89
+
90
+ `DoctorReport` gains an `envExampleDrift: CheckResult[]` field.
91
+
92
+ `generateReport()` gains an `envExampleDriftChecks` parameter.
93
+
94
+ `formatAsText()` gains a ".env.example Drift" section; suppressed if all pass.
95
+
96
+ `reportToFindings()` adds:
97
+
98
+ ```typescript
99
+ ...checksToFindings(report.envExampleDrift, 'manifest', 'full'),
100
+ ```
101
+
102
+ `executeFixRun()` calls `checkEnvExampleDrift()` in the re-check pass.
103
+
104
+ ### Affected files
105
+
106
+ | File | Change |
107
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
108
+ | `tool/commands/doctor.ts` | Add `checkEnvExampleDrift()`, `executeEnvExampleRegen()`, wire into report infrastructure, `REMEDIATION_REGISTRY`, `PRIORITY` map |
109
+ | `tool/__tests__/commands.test.ts` | Tests for: missing key (fail), stale key (warn), in-sync (pass), fix action |
110
+ | `CHANGELOG.md` | Entry under `### Added` |
111
+
112
+ ### User-visible behaviour
113
+
114
+ ```
115
+ .env.example Drift:
116
+ ✗ Parameter POSTGRES_PASSWORD declared by postgres is missing from .env.example
117
+ → Run cs regen or use --fix to regenerate .env.example
118
+ → Fixable with --fix flag
119
+ ⚠ Key OLD_API_KEY in .env.example is not declared by any selected overlay — it may be stale
120
+ → Remove OLD_API_KEY from .env.example or run --fix to regenerate
121
+ → Fixable with --fix flag
122
+ ✓ .env.example is in sync with 5 declared parameter(s)
123
+ ```
124
+
125
+ ### Backward compatibility
126
+
127
+ No changes to existing generated files or project file format. Purely additive check.
128
+
129
+ ## User Scenarios & Testing
130
+
131
+ ### User Story 1 — Missing key caught after overlay addition (P1)
132
+
133
+ A developer adds the `vault` overlay (which declares `VAULT_TOKEN`) to their project and runs
134
+ `cs regen`. Due to a bug or partial regen, `.env.example` is not updated. Doctor detects the
135
+ missing key immediately.
136
+
137
+ **Why this priority**: A missing key in `.env.example` silently breaks onboarding for every
138
+ future team member who copies the file as their starting `.env`.
139
+
140
+ **Independent Test**: Write a `.superposition.yml` selecting `postgres`. Write a `.env.example`
141
+ that lacks `POSTGRES_PASSWORD`. Run `doctorCommand`. Assert `fail` finding for
142
+ `POSTGRES_PASSWORD`.
143
+
144
+ **Acceptance Scenarios**:
145
+
146
+ 1. **Given** `.superposition.yml` selects `postgres` (declares `POSTGRES_PASSWORD`) and
147
+ `.env.example` does not contain `POSTGRES_PASSWORD`, **When** `cs doctor` runs, **Then** a
148
+ `fail` finding reports "`POSTGRES_PASSWORD` declared by `postgres` is missing from
149
+ `.env.example`".
150
+ 2. **Given** the same setup with `--fix`, **When** doctor runs, **Then** `.env.example` is
151
+ regenerated containing `POSTGRES_PASSWORD` and the re-check passes.
152
+ 3. **Given** `.env.example` contains all keys for the current overlay selection, **When**
153
+ `cs doctor` runs, **Then** the `.env.example Drift` section is suppressed.
154
+
155
+ ---
156
+
157
+ ### User Story 2 — Stale key caught after overlay removal (P2)
158
+
159
+ A developer removes `redis` from their project but forgets to re-run regen. `.env.example`
160
+ still contains `REDIS_PASSWORD`. Doctor warns them it is stale.
161
+
162
+ **Why this priority**: Stale `.env.example` keys cause confusion about required configuration and
163
+ may expose unneeded credentials to documentation reviewers.
164
+
165
+ **Independent Test**: Write a `.superposition.yml` without `redis`. Write a `.env.example` that
166
+ contains `REDIS_PASSWORD`. Run `doctorCommand`. Assert `warn` finding for `REDIS_PASSWORD`.
167
+
168
+ **Acceptance Scenarios**:
169
+
170
+ 1. **Given** `.env.example` contains `REDIS_PASSWORD` but no selected overlay declares it,
171
+ **When** `cs doctor` runs, **Then** a `warn` finding reports "`REDIS_PASSWORD` in `.env.example`
172
+ is not declared by any selected overlay".
173
+ 2. **Given** `--fix` is used, **When** doctor runs, **Then** `.env.example` is regenerated without
174
+ `REDIS_PASSWORD`.
175
+
176
+ ---
177
+
178
+ ### Edge Cases
179
+
180
+ - `.env.example` with only comments and blank lines: treated as having zero keys; any declared
181
+ parameters generate `fail` findings.
182
+ - Overlay with parameters but none sensitive: still checked for drift (drift check is not
183
+ limited to sensitive parameters).
184
+ - `.env.example` present but empty (zero bytes): treated the same as all-comment case.
185
+ - No project file: return empty result (no noise).
186
+ - Compose `${VAR:-default}` references in docker-compose.yml but not in overlay `parameters:`:
187
+ not flagged — the drift check only compares declared overlay parameters, not raw compose
188
+ variable references.
189
+
190
+ ## Requirements
191
+
192
+ ### Functional Requirements
193
+
194
+ - **FR-001**: `checkEnvExampleDrift()` MUST return `fail` for each parameter key declared by a
195
+ selected overlay that is absent from `.env.example`.
196
+ - **FR-002**: `checkEnvExampleDrift()` MUST return `warn` for each key in `.env.example` not
197
+ declared by any selected overlay.
198
+ - **FR-003**: When `.env.example` is absent, the check MUST return a single pass (the missing-file
199
+ scenario is handled by the parameters check in `checkParameters()`).
200
+ - **FR-004**: `executeEnvExampleRegen()` MUST regenerate `.env.example` via a full `composeDevContainer`
201
+ call and MUST NOT leave stale keys behind.
202
+ - **FR-005**: Comment lines and blank lines in `.env.example` MUST NOT be counted as parameter keys.
203
+ - **FR-006**: When no project file is present, the check MUST return an empty result.
204
+
205
+ ### Key Entities
206
+
207
+ - **`declaredKeys`**: set of parameter key strings from `collectOverlayParameters()` for the current overlay selection.
208
+ - **`exampleKeys`**: set of key strings parsed from non-comment, non-blank lines in `.env.example`.
209
+
210
+ ## Dependencies & Impact
211
+
212
+ - **Affected Areas**: `tool/commands/doctor.ts`, `tool/__tests__/commands.test.ts`, `CHANGELOG.md`
213
+ - **Compatibility Impact**: None — purely additive check category.
214
+ - **Required Documentation Updates**: `CHANGELOG.md`
215
+ - **Verification Plan**: Unit tests in `commands.test.ts`; manual test after adding then removing
216
+ an overlay from a real project file.
217
+
218
+ ## Success Criteria
219
+
220
+ ### Measurable Outcomes
221
+
222
+ - **SC-001**: `cs doctor` on a project where `.env.example` is missing a declared parameter
223
+ reports a `fail` in the `.env.example Drift` section.
224
+ - **SC-002**: `cs doctor --fix` on the same setup regenerates `.env.example` and the re-check
225
+ passes with no drift findings.
226
+ - **SC-003**: `npm test` passes with at least 3 new test cases covering: missing key, stale key,
227
+ and fix action.
228
+ - **SC-004**: No existing doctor tests regress.
229
+
230
+ ## Open Questions
231
+
232
+ | # | Question | Owner | Resolution |
233
+ | --- | ----------------------------------------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------ |
234
+ | 1 | Should missing `.env.example` keys be `fail` or `warn`? Missing required params are fail; | PM | Pending — lean toward `fail` for keys with no default, `warn` for keys with defaults |
235
+ | | but all parameters already have defaults (required-without-default is a separate check) | | |
236
+ | 2 | Should section header comments be preserved when regenerating `.env.example`? | PM | The composer already handles this; not a new concern |
237
+
238
+ ## Out of Scope
239
+
240
+ - Validating values in the user's runtime `.env` file.
241
+ - Checking whether `.env` is listed in `.gitignore`.
242
+ - Detecting drift in `devcontainer.json` `remoteEnv` (that is covered by the parameters check).
243
+
244
+ ## Implementation Notes
245
+
246
+ - `checkEnvExampleDrift` uses `collectOverlayParameters` to get the declared parameter set for the selected overlays, then diffs against lines parsed from the existing `.env.example`.
247
+ - The check returns early (no findings) when `.env.example` is absent — the parameters check is responsible for detecting its absence when required.
248
+ - `executeEnvExampleRegen` calls `composeDevContainer` with `isRegen: false` writing only to the output path, then re-reads `.env.example` to verify the file was produced.