container-superposition 0.1.8 → 0.1.10
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.
- package/README.md +3 -0
- package/dist/tool/cli/args.d.ts.map +1 -1
- package/dist/tool/cli/args.js +1 -1
- package/dist/tool/cli/args.js.map +1 -1
- package/dist/tool/commands/adopt.d.ts.map +1 -1
- package/dist/tool/commands/adopt.js +15 -21
- package/dist/tool/commands/adopt.js.map +1 -1
- package/dist/tool/commands/doctor.d.ts +1 -0
- package/dist/tool/commands/doctor.d.ts.map +1 -1
- package/dist/tool/commands/doctor.js +1370 -73
- package/dist/tool/commands/doctor.js.map +1 -1
- package/dist/tool/questionnaire/composer.d.ts +3 -1
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +273 -20
- package/dist/tool/questionnaire/composer.js.map +1 -1
- package/dist/tool/questionnaire/presets.d.ts.map +1 -1
- package/dist/tool/questionnaire/presets.js +1 -0
- package/dist/tool/questionnaire/presets.js.map +1 -1
- package/dist/tool/questionnaire/questionnaire.d.ts.map +1 -1
- package/dist/tool/questionnaire/questionnaire.js +3 -1
- package/dist/tool/questionnaire/questionnaire.js.map +1 -1
- package/dist/tool/schema/project-config.d.ts.map +1 -1
- package/dist/tool/schema/project-config.js +174 -1
- package/dist/tool/schema/project-config.js.map +1 -1
- package/dist/tool/schema/types.d.ts +53 -2
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/overlays.md +188 -147
- package/docs/specs/001-verbose-plan-graph/spec.md +5 -12
- package/docs/specs/002-superposition-config-file/spec.md +5 -12
- package/docs/specs/003-mkdocs2-overlay/spec.md +2 -9
- package/docs/specs/004-doctor-fix/spec.md +1 -8
- package/docs/specs/005-cuda-overlay/spec.md +2 -9
- package/docs/specs/006-rocm-overlay/spec.md +3 -10
- package/docs/specs/007-target-aware-generation/spec.md +4 -11
- package/docs/specs/008-project-file-canonical/spec.md +7 -8
- package/docs/specs/009-project-env/spec.md +3 -10
- package/docs/specs/010-compose-env-materialization/spec.md +3 -10
- package/docs/specs/011-overlay-parameters/spec.md +2 -9
- package/docs/specs/012-ollama-cli-overlay/spec.md +47 -0
- package/docs/specs/013-doctor-dependency-check/spec.md +250 -0
- package/docs/specs/014-doctor-compose-port-cross-validation/spec.md +276 -0
- package/docs/specs/015-doctor-env-example-drift/spec.md +248 -0
- package/docs/specs/016-doctor-reproducibility-check/spec.md +276 -0
- package/docs/specs/017-doctor-dry-run/spec.md +276 -0
- package/docs/specs/{007-init-project-file → 018-init-project-file}/spec.md +2 -9
- package/docs/specs/019-project-mounts/spec.md +176 -0
- package/docs/specs/taxonomy.md +186 -0
- package/docs/superposition-yml.md +467 -0
- package/overlays/.presets/full-observability.yml +113 -0
- package/overlays/.presets/k8s-dev.yml +174 -0
- package/overlays/.presets/local-llm.yml +105 -0
- package/overlays/.presets/vector-ai.yml +150 -0
- package/overlays/.shared/vscode/js-ts-settings.json +19 -0
- package/overlays/.shared/vscode/markdown-extensions.json +8 -0
- package/overlays/alertmanager/devcontainer.patch.json +0 -1
- package/overlays/alertmanager/docker-compose.yml +8 -0
- package/overlays/alertmanager/overlay.yml +1 -0
- package/overlays/amp/devcontainer.patch.json +4 -1
- package/overlays/ansible/README.md +163 -0
- package/overlays/ansible/devcontainer.patch.json +14 -0
- package/overlays/ansible/overlay.yml +18 -0
- package/overlays/argocd/README.md +158 -0
- package/overlays/argocd/devcontainer.patch.json +9 -0
- package/overlays/argocd/overlay.yml +17 -0
- package/overlays/argocd/setup.sh +29 -0
- package/overlays/argocd/verify.sh +14 -0
- package/overlays/bun/devcontainer.patch.json +1 -10
- package/overlays/bun/overlay.yml +8 -1
- package/overlays/claude-code/devcontainer.patch.json +6 -1
- package/overlays/codex/devcontainer.patch.json +5 -0
- package/overlays/comfyui/docker-compose.yml +1 -0
- package/overlays/comfyui/overlay.yml +4 -0
- package/overlays/commitlint/devcontainer.patch.json +1 -6
- package/overlays/docker-sock/overlay.yml +1 -0
- package/overlays/dotnet/overlay.yml +4 -1
- package/overlays/fuseki/.env.example +5 -0
- package/overlays/fuseki/README.md +173 -0
- package/overlays/fuseki/devcontainer.patch.json +18 -0
- package/overlays/fuseki/docker-compose.yml +29 -0
- package/overlays/fuseki/overlay.yml +42 -0
- package/overlays/fuseki/verify.sh +58 -0
- package/overlays/gemini-cli/devcontainer.patch.json +4 -1
- package/overlays/go/overlay.yml +6 -1
- package/overlays/grafana/devcontainer.patch.json +0 -1
- package/overlays/grafana/docker-compose.yml +8 -2
- package/overlays/grafana/overlay.yml +6 -1
- package/overlays/jaeger/.env.example +11 -0
- package/overlays/jaeger/README.md +33 -4
- package/overlays/jaeger/devcontainer.patch.json +9 -1
- package/overlays/jaeger/docker-compose.yml +17 -0
- package/overlays/jaeger/overlay.yml +1 -12
- package/overlays/java/overlay.yml +6 -1
- package/overlays/jupyter/docker-compose.yml +1 -0
- package/overlays/jupyter/overlay.yml +1 -0
- package/overlays/keycloak/devcontainer.patch.json +0 -1
- package/overlays/keycloak/docker-compose.yml +1 -0
- package/overlays/keycloak/overlay.yml +15 -0
- package/overlays/localstack/docker-compose.yml +1 -0
- package/overlays/localstack/overlay.yml +19 -1
- package/overlays/loki/devcontainer.patch.json +0 -1
- package/overlays/loki/docker-compose.yml +8 -0
- package/overlays/loki/overlay.yml +1 -0
- package/overlays/mailpit/docker-compose.yml +1 -0
- package/overlays/mailpit/overlay.yml +1 -0
- package/overlays/minio/devcontainer.patch.json +1 -1
- package/overlays/minio/docker-compose.yml +1 -0
- package/overlays/minio/overlay.yml +23 -2
- package/overlays/mkdocs/devcontainer.patch.json +1 -5
- package/overlays/mkdocs/overlay.yml +3 -1
- package/overlays/mkdocs2/devcontainer.patch.json +1 -5
- package/overlays/mkdocs2/overlay.yml +2 -0
- package/overlays/mongodb/docker-compose.yml +2 -0
- package/overlays/mongodb/overlay.yml +26 -2
- package/overlays/mysql/docker-compose.yml +2 -0
- package/overlays/mysql/overlay.yml +36 -2
- package/overlays/nats/docker-compose.yml +1 -0
- package/overlays/nats/overlay.yml +18 -2
- package/overlays/nodejs/devcontainer.patch.json +1 -12
- package/overlays/nodejs/overlay.yml +8 -1
- package/overlays/ollama/README.md +4 -3
- package/overlays/ollama/docker-compose.yml +1 -0
- package/overlays/ollama/overlay.yml +6 -1
- package/overlays/ollama/verify.sh +5 -28
- package/overlays/ollama-cli/README.md +90 -0
- package/overlays/ollama-cli/devcontainer.patch.json +3 -0
- package/overlays/ollama-cli/overlay.yml +19 -0
- package/overlays/{ollama → ollama-cli}/setup.sh +7 -10
- package/overlays/ollama-cli/verify.sh +49 -0
- package/overlays/open-webui/docker-compose.yml +1 -0
- package/overlays/open-webui/overlay.yml +8 -1
- package/overlays/opencode/devcontainer.patch.json +4 -1
- package/overlays/otel-collector/README.md +4 -0
- package/overlays/otel-collector/devcontainer.patch.json +4 -1
- package/overlays/otel-collector/docker-compose.yml +8 -4
- package/overlays/otel-collector/overlay.yml +1 -0
- package/overlays/otel-demo-nodejs/devcontainer.patch.json +0 -1
- package/overlays/otel-demo-nodejs/docker-compose.yml +1 -0
- package/overlays/otel-demo-nodejs/overlay.yml +9 -1
- package/overlays/otel-demo-python/devcontainer.patch.json +0 -1
- package/overlays/otel-demo-python/docker-compose.yml +1 -0
- package/overlays/otel-demo-python/overlay.yml +6 -1
- package/overlays/pandoc/README.md +10 -0
- package/overlays/pandoc/devcontainer.patch.json +0 -5
- package/overlays/pandoc/overlay.yml +2 -0
- package/overlays/pandoc/setup.sh +10 -0
- package/overlays/pgvector/devcontainer.patch.json +11 -5
- package/overlays/pgvector/docker-compose.yml +1 -0
- package/overlays/pgvector/overlay.yml +3 -0
- package/overlays/playwright/devcontainer.patch.json +0 -5
- package/overlays/playwright/overlay.yml +2 -1
- package/overlays/postgres/docker-compose.yml +1 -0
- package/overlays/postgres/overlay.yml +4 -1
- package/overlays/pre-commit/devcontainer.patch.json +1 -7
- package/overlays/prometheus/devcontainer.patch.json +0 -1
- package/overlays/prometheus/docker-compose.yml +8 -0
- package/overlays/prometheus/overlay.yml +1 -0
- package/overlays/promtail/devcontainer.patch.json +1 -2
- package/overlays/promtail/docker-compose.yml +8 -0
- package/overlays/promtail/overlay.yml +1 -0
- package/overlays/qdrant/docker-compose.yml +1 -0
- package/overlays/qdrant/overlay.yml +5 -1
- package/overlays/rabbitmq/docker-compose.yml +1 -0
- package/overlays/rabbitmq/overlay.yml +25 -2
- package/overlays/redis/docker-compose.yml +7 -0
- package/overlays/redis/overlay.yml +15 -1
- package/overlays/redpanda/docker-compose.yml +1 -0
- package/overlays/redpanda/overlay.yml +15 -3
- package/overlays/rocm/overlay.yml +2 -1
- package/overlays/rust/overlay.yml +3 -1
- package/overlays/sqlserver/docker-compose.yml +1 -0
- package/overlays/sqlserver/overlay.yml +17 -0
- package/overlays/task/README.md +47 -0
- package/overlays/task/devcontainer.patch.json +9 -0
- package/overlays/task/overlay.yml +16 -0
- package/overlays/task/setup.sh +29 -0
- package/overlays/task/verify.sh +14 -0
- package/overlays/tempo/devcontainer.patch.json +0 -1
- package/overlays/tempo/docker-compose.yml +8 -0
- package/overlays/tempo/overlay.yml +1 -0
- package/overlays/windsurf-cli/devcontainer.patch.json +4 -1
- package/package.json +1 -1
- package/tool/schema/config.schema.json +74 -1
- 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.
|