create-forge 0.2.0__tar.gz → 0.2.1__tar.gz

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 (55) hide show
  1. {create_forge-0.2.0 → create_forge-0.2.1}/CHANGELOG.md +14 -1
  2. {create_forge-0.2.0 → create_forge-0.2.1}/PKG-INFO +12 -2
  3. {create_forge-0.2.0 → create_forge-0.2.1}/README.md +10 -1
  4. {create_forge-0.2.0 → create_forge-0.2.1}/docs/adr/README.md +3 -0
  5. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/README.md +8 -8
  6. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/07-forge-cli-integration/README.md +8 -2
  7. create_forge-0.2.1/docs/roadmap-v1/roadmap/08-reference-archetype-validation/README.md +81 -0
  8. {create_forge-0.2.0 → create_forge-0.2.1}/pyproject.toml +3 -2
  9. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/cli.py +2 -1
  10. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/pipeline.py +47 -22
  11. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/staging.py +40 -3
  12. create_forge-0.2.1/tests/conftest.py +70 -0
  13. create_forge-0.2.1/tests/test_archetype_parity.py +161 -0
  14. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_cli.py +68 -1
  15. create_forge-0.2.1/tests/test_e2e_engine_generation.py +333 -0
  16. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_e2e_generation.py +24 -56
  17. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_engine_contract.py +8 -3
  18. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_pipeline.py +157 -12
  19. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_spec.py +3 -3
  20. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_staging.py +63 -1
  21. create_forge-0.2.0/docs/roadmap-v1/roadmap/08-reference-archetype-validation/README.md +0 -58
  22. create_forge-0.2.0/tests/conftest.py +0 -17
  23. {create_forge-0.2.0 → create_forge-0.2.1}/.gitignore +0 -0
  24. {create_forge-0.2.0 → create_forge-0.2.1}/LICENSE +0 -0
  25. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/00-governance-and-principles/README.md +0 -0
  26. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/01-python-core/README.md +0 -0
  27. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/02-developer-experience/README.md +0 -0
  28. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/03-quality-and-ci/README.md +0 -0
  29. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/04-runtime-and-configuration/README.md +0 -0
  30. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/05-security-and-supply-chain/README.md +0 -0
  31. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/06-extension-and-composition-contract/README.md +0 -0
  32. {create_forge-0.2.0 → create_forge-0.2.1}/docs/roadmap-v1/roadmap/09-blueprint-compatibility/README.md +0 -0
  33. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/__init__.py +0 -0
  34. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/compat.py +0 -0
  35. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/config.py +0 -0
  36. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/engine.py +0 -0
  37. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/models.py +0 -0
  38. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/prompts.py +0 -0
  39. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/registry.py +0 -0
  40. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/runner.py +0 -0
  41. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/spec.py +0 -0
  42. {create_forge-0.2.0 → create_forge-0.2.1}/src/create_forge/templates.toml +0 -0
  43. {create_forge-0.2.0 → create_forge-0.2.1}/tests/__init__.py +0 -0
  44. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_adr.py +0 -0
  45. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_config.py +0 -0
  46. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_drift.py +0 -0
  47. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_engine_adapter.py +0 -0
  48. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_engine_cross_repository.py +0 -0
  49. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_labels.py +0 -0
  50. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_models.py +0 -0
  51. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_prompts.py +0 -0
  52. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_registry.py +0 -0
  53. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_runner.py +0 -0
  54. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_update.py +0 -0
  55. {create_forge-0.2.0 → create_forge-0.2.1}/tests/test_update_network.py +0 -0
@@ -1,6 +1,19 @@
1
1
  # Changelog
2
2
 
3
3
  Generated by git-cliff from Conventional Commits.
4
+ ## [0.2.1] - 2026-08-31
5
+
6
+ ### Bug Fixes
7
+
8
+ - Generate engine project lockfiles
9
+
10
+ ### Refactor
11
+
12
+ - Derive component options from discovery, not an archetype list (#92)
13
+
14
+ ### Testing
15
+
16
+ - Extend end-to-end generation to the public engine (#93)
4
17
  ## [0.2.0] - 2026-08-30
5
18
 
6
19
  ### Documentation
@@ -36,7 +49,7 @@ Generated by git-cliff from Conventional Commits.
36
49
  - Implement the shared create pipeline behind an opt-in engine flag (#83)
37
50
  - Stage and finalise generation without leaving partials (#84)
38
51
  - Expose the CLI Application archetype through the engine path (#89)
39
- - Publish to PyPI and assign the first bounded engine range
52
+ - Publish to PyPI and assign the first bounded engine range (#90)
40
53
 
41
54
  ### Testing
42
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: create-forge
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Scaffold modern Python projects from maintained templates.
5
5
  Project-URL: Homepage, https://github.com/Sandsy09/create-forge
6
6
  Project-URL: Repository, https://github.com/Sandsy09/create-forge
@@ -27,6 +27,7 @@ Requires-Dist: rich>=13.9
27
27
  Requires-Dist: typer>=0.15
28
28
  Provides-Extra: engine
29
29
  Requires-Dist: forge-template<0.4,>=0.3.1; extra == 'engine'
30
+ Requires-Dist: uv<0.13,>=0.12; extra == 'engine'
30
31
  Description-Content-Type: text/markdown
31
32
 
32
33
  # create-forge
@@ -158,13 +159,22 @@ it in the same `0.3.0` release, and
158
159
  archetypes behind the hidden `new --engine-preview` flag's `--archetype`
159
160
  option. Neither change alters this CLI's default `new` answers, registry, or
160
161
  released dependency surface.
162
+ The Stage 08
163
+ [composition architecture review](https://github.com/Sandsy09/forge-template/blob/main/docs/composition-architecture-review.md)
164
+ is released in `forge-template 0.3.2`. On the engine-preview path,
165
+ `create-forge 0.2.1` now finalises the validated in-memory render with
166
+ `uv lock --directory <staging-directory>` before the atomic rename. The
167
+ result contains `uv.lock` and uses `uv run --locked poe check`, while the
168
+ default Copier path remains unchanged; see
169
+ [ADR 0021](docs/adr/0021-client-finalises-engine-lockfiles.md).
161
170
  The engine now also defines in-memory
162
171
  [generated-project validation](https://github.com/Sandsy09/forge-template/blob/main/docs/generated-project-validation.md)
163
172
  ([ADR 0030](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0030-generated-project-validation.md))
164
173
  before rendered output is returned; `render_project` already calls it before
165
174
  `--engine-preview` receives a result.
166
175
  This repository now depends on a real, released `forge-template` range —
167
- `>=0.3.1,<0.4`, published to PyPI as the optional `engine` extra
176
+ `>=0.3.1,<0.4`, published to PyPI with `uv>=0.12,<0.13` as the optional
177
+ `engine` extra
168
178
  (`pip install 'create-forge[engine]'`; [#9](https://github.com/Sandsy09/create-forge/issues/9),
169
179
  [ADR 0018](docs/adr/0018-pypi-distribution-and-the-first-engine-range.md)) —
170
180
  rather than a development-only pin. That range is reachable only behind
@@ -127,13 +127,22 @@ it in the same `0.3.0` release, and
127
127
  archetypes behind the hidden `new --engine-preview` flag's `--archetype`
128
128
  option. Neither change alters this CLI's default `new` answers, registry, or
129
129
  released dependency surface.
130
+ The Stage 08
131
+ [composition architecture review](https://github.com/Sandsy09/forge-template/blob/main/docs/composition-architecture-review.md)
132
+ is released in `forge-template 0.3.2`. On the engine-preview path,
133
+ `create-forge 0.2.1` now finalises the validated in-memory render with
134
+ `uv lock --directory <staging-directory>` before the atomic rename. The
135
+ result contains `uv.lock` and uses `uv run --locked poe check`, while the
136
+ default Copier path remains unchanged; see
137
+ [ADR 0021](docs/adr/0021-client-finalises-engine-lockfiles.md).
130
138
  The engine now also defines in-memory
131
139
  [generated-project validation](https://github.com/Sandsy09/forge-template/blob/main/docs/generated-project-validation.md)
132
140
  ([ADR 0030](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0030-generated-project-validation.md))
133
141
  before rendered output is returned; `render_project` already calls it before
134
142
  `--engine-preview` receives a result.
135
143
  This repository now depends on a real, released `forge-template` range —
136
- `>=0.3.1,<0.4`, published to PyPI as the optional `engine` extra
144
+ `>=0.3.1,<0.4`, published to PyPI with `uv>=0.12,<0.13` as the optional
145
+ `engine` extra
137
146
  (`pip install 'create-forge[engine]'`; [#9](https://github.com/Sandsy09/create-forge/issues/9),
138
147
  [ADR 0018](docs/adr/0018-pypi-distribution-and-the-first-engine-range.md)) —
139
148
  rather than a development-only pin. That range is reachable only behind
@@ -21,6 +21,9 @@ format](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions
21
21
  - [0016 — Test the reference client end to end against the released template](0016-end-to-end-reference-client-tests.md)
22
22
  - [0017 — Expose the CLI Application archetype through discovery-driven selection](0017-cli-application-archetype-exposure.md)
23
23
  - [0018 — Publish to PyPI and assign the first bounded engine range](0018-pypi-distribution-and-the-first-engine-range.md)
24
+ - [0019 — CLI archetype-parity review](0019-cli-archetype-parity-review.md)
25
+ - [0020 — Test the public engine path end to end](0020-engine-path-end-to-end-tests.md)
26
+ - [0021 — Finalise engine-generated lockfiles in the client](0021-client-finalises-engine-lockfiles.md)
24
27
 
25
28
  Add a new record by copying the most recent one and incrementing the number.
26
29
  Records are immutable: supersede them rather than editing.
@@ -13,13 +13,17 @@ define the mandatory generated-project outcomes, while the
13
13
  defines which concerns may belong in that baseline. The
14
14
  [Library archetype contract](https://github.com/Sandsy09/forge-template/blob/main/docs/library-archetype.md)
15
15
  defines the first production archetype, implemented on
16
- `forge-template/main` and released at `0.3.0`. The
16
+ `forge-template/main`, released at `0.3.0`, and included in the completed
17
+ two-archetype [composition review](https://github.com/Sandsy09/forge-template/blob/main/docs/composition-architecture-review.md)
18
+ released at `0.3.2`. The
17
19
  [CLI Application archetype contract](https://github.com/Sandsy09/forge-template/blob/main/docs/cli-application-archetype.md)
18
20
  selects the engine-owned, optionless `cli` archetype and derives its command
19
21
  from `ProjectSpec.project.repository_name`; FT-08.04 implemented it, and
20
22
  CF-08.02 exposes both archetypes behind `create-forge`'s hidden
21
23
  `--engine-preview` flag. Neither contract changes this repository's default
22
- `new` path or unassigned released engine range. The
24
+ `new` path. `create-forge 0.2.1` keeps the released engine range unchanged and
25
+ adds client-owned lock finalisation under
26
+ [ADR 0021](../adr/0021-client-finalises-engine-lockfiles.md). The
23
27
  [Python support policy](https://github.com/Sandsy09/forge-template/blob/main/docs/python-support.md)
24
28
  defines the generated-project CPython window, defaults, and release lifecycle.
25
29
  The [editor integration strategy](https://github.com/Sandsy09/forge-template/blob/main/docs/editor-integration.md)
@@ -60,12 +64,8 @@ The canonical
60
64
  [generated-project validation contract](https://github.com/Sandsy09/forge-template/blob/main/docs/generated-project-validation.md)
61
65
  and [ADR 0030](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0030-generated-project-validation.md)
62
66
  complete the `forge-template` side of Stage 07 by validating rendered output
63
- in memory. The shared stage remains open for `create-forge` CLI orchestration,
64
- filesystem safety, and end-to-end coverage.
65
- `forge-template/main` now contains the Library production catalogue, while
66
- this repository's exact development pin still sees the earlier empty
67
- catalogue. CLI Application implementation and coordinated CLI discovery remain
68
- later work.
67
+ in memory. Stage 07 and Stage 08 are now complete across both repositories;
68
+ the default Copier-to-engine cutover remains separate future work.
69
69
  This roadmap links to the canonical sources rather than maintaining second
70
70
  definitions.
71
71
 
@@ -60,9 +60,15 @@ the *public engine* against a *released* engine-and-assets unit -- because
60
60
  neither exists yet: `forge-template` has no `0.2.x` release and its
61
61
  production catalogue stays empty until
62
62
  [FT-08.02 / forge-template#41](https://github.com/Sandsy09/forge-template/issues/41).
63
- Those two criteria carry forward verbatim to **CF-08.04**, filed under
63
+ Those two criteria carried forward verbatim to **CF-08.04**, filed under
64
64
  [CF-EPIC-08](https://github.com/Sandsy09/create-forge/issues/39) and blocked
65
- on the same two things. See the canonical
65
+ on the same two things at the time. Both blockers have since resolved --
66
+ [FT-08.02 / forge-template#41](https://github.com/Sandsy09/forge-template/issues/41)
67
+ shipped the production catalogue, and
68
+ [#9](https://github.com/Sandsy09/create-forge/issues/9) published a released
69
+ engine range -- and CF-08.04 is complete
70
+ ([ADR 0020](https://github.com/Sandsy09/create-forge/blob/main/docs/adr/0020-engine-path-end-to-end-tests.md)).
71
+ See the canonical
66
72
  [end-to-end tests contract](https://github.com/Sandsy09/create-forge/blob/main/docs/end-to-end-tests.md).
67
73
  The atomic cutover that replaces both the v0.1.x registry seam and
68
74
  `--engine-preview` stays gated on
@@ -0,0 +1,81 @@
1
+ # Stage 08 — Reference Archetype Validation
2
+
3
+ ## Repository ownership
4
+
5
+ ### forge-template
6
+
7
+ - ~~**FT-08.01 — Define Library archetype contract**~~ — complete via the
8
+ [canonical contract](https://github.com/Sandsy09/forge-template/blob/main/docs/library-archetype.md),
9
+ [ADR 0031](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0031-library-archetype-contract.md),
10
+ and [forge-template PR #79](https://github.com/Sandsy09/forge-template/pull/79).
11
+ - ~~**FT-08.02 — Migrate the Library archetype to the composition contract**~~
12
+ — complete via [forge-template PR #81](https://github.com/Sandsy09/forge-template/pull/81).
13
+ - ~~**FT-08.03 — Select and define the second reference archetype contract**~~
14
+ — CLI Application selected via the
15
+ [canonical contract](https://github.com/Sandsy09/forge-template/blob/main/docs/cli-application-archetype.md),
16
+ [ADR 0034](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0034-select-cli-application-reference-archetype.md),
17
+ and [forge-template PR #82](https://github.com/Sandsy09/forge-template/pull/82).
18
+ - ~~**FT-08.04 — Implement the CLI Application reference archetype**~~ —
19
+ complete via
20
+ [forge-template PR #84](https://github.com/Sandsy09/forge-template/pull/84),
21
+ released at `forge-template` `0.3.0` alongside Library.
22
+ - ~~**FT-08.05 — Run composition architecture review**~~ — complete via the
23
+ [canonical review](https://github.com/Sandsy09/forge-template/blob/main/docs/composition-architecture-review.md),
24
+ [ADR 0037](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0037-two-archetype-composition-review.md),
25
+ [forge-template PR #87](https://github.com/Sandsy09/forge-template/pull/87),
26
+ and `forge-template 0.3.2`. Its client-finalisation finding is implemented
27
+ by create-forge [ADR 0021](../../../adr/0021-client-finalises-engine-lockfiles.md)
28
+ for the `0.2.1` release.
29
+
30
+ ### create-forge
31
+
32
+ - Epic: [CF-EPIC-08 / #39](https://github.com/Sandsy09/create-forge/issues/39)
33
+ - ~~**CF-08.01 — Expose Library archetype through create-forge**~~ — completed before roadmap filing.
34
+ - ~~**CF-08.02 — Expose CLI Application through create-forge**~~ — complete via
35
+ [ADR 0017](https://github.com/Sandsy09/create-forge/blob/main/docs/adr/0017-cli-application-archetype-exposure.md),
36
+ which moves this repository's exact development pin to `forge-template==0.3.0`
37
+ and adds a discovery-driven `--archetype` option and prompt behind
38
+ `--engine-preview`.
39
+ - ~~**CF-08.03 — Run CLI archetype-parity review**~~ — complete via
40
+ [ADR 0019](https://github.com/Sandsy09/create-forge/blob/main/docs/adr/0019-cli-archetype-parity-review.md)
41
+ and [create-forge#52](https://github.com/Sandsy09/create-forge/issues/52).
42
+ The shared ProjectSpec/pipeline path and engine-owned discovery were
43
+ confirmed generic; the one archetype-specific branch found
44
+ (`pipeline._resolved_component_options`) is now gated by a discovered
45
+ descriptor rather than a hardcoded archetype id. The engine path's
46
+ Library-shaped prompt set was recorded, not fixed, and tracked by
47
+ [create-forge#91](https://github.com/Sandsy09/create-forge/issues/91).
48
+ - ~~**CF-08.04 — Extend end-to-end generation to the public engine**~~ —
49
+ complete via
50
+ [ADR 0020](https://github.com/Sandsy09/create-forge/blob/main/docs/adr/0020-engine-path-end-to-end-tests.md)
51
+ and [create-forge#85](https://github.com/Sandsy09/create-forge/issues/85),
52
+ successor to [CF-07.06 / #51](https://github.com/Sandsy09/create-forge/issues/51).
53
+ `tests/test_e2e_engine_generation.py` generates both archetypes through
54
+ `--engine-preview` against the real installed engine, runs each generated
55
+ project's own checks, and proves the released-install compatibility
56
+ boundary (an out-of-range engine installed from a git tag, and no `engine`
57
+ extra at all) writes nothing. This closes
58
+ [CF-EPIC-08 / #39](https://github.com/Sandsy09/create-forge/issues/39).
59
+
60
+ The Library decision records the legacy answer mapping into
61
+ `component_options.library.packaging_mode`; CF-08.02 wires it on the engine
62
+ path via `spec.legacy_library_answers` and
63
+ `engine.map_legacy_library_options`. CLI Application is selected as
64
+ engine-owned ID `cli`, has no component options, and derives its console
65
+ command from `ProjectSpec.project.repository_name`; FT-08.04 implemented it
66
+ and create-forge #10 exposes it, both landing in this stage.
67
+
68
+ All repository-local children are complete. The two-archetype review removed
69
+ archetype assumptions from Foundation without adding inheritance or a shared
70
+ runtime layer, while create-forge now creates the dynamic lockfile in adjacent
71
+ staging before atomic placement. Stage 08 is complete across both
72
+ repositories.
73
+
74
+ ## Stage completion rule
75
+
76
+ - [x] Repo-local issues are complete or explicitly deferred.
77
+ - [x] Cross-repository blockers are resolved. -- #9/ADR 0018 was the last one.
78
+ - [x] Public contracts changed by this stage are documented/versioned --
79
+ ADR 0017, ADR 0019, ADR 0020, ADR 0021, and forge-template ADR 0037.
80
+ - [x] No implementation concern is duplicated across repositories -- CF-08.03
81
+ (ADR 0019) confirmed this directly.
@@ -7,7 +7,7 @@ build-backend = "hatchling.build"
7
7
 
8
8
  [project]
9
9
  name = "create-forge"
10
- version = "0.2.0"
10
+ version = "0.2.1"
11
11
  description = "Scaffold modern Python projects from maintained templates."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -44,7 +44,8 @@ dependencies = [
44
44
  # downloads an engine it doesn't call. src/create_forge/engine.py is the
45
45
  # only module that imports it; src/create_forge/compat.py holds the range
46
46
  # itself so cli.py's `doctor` can report it without importing the engine.
47
- engine = ["forge-template>=0.3.1,<0.4"]
47
+ # ADR 0021 adds bounded uv to finalise the engine render's dynamic lockfile.
48
+ engine = ["forge-template>=0.3.1,<0.4", "uv>=0.12,<0.13"]
48
49
 
49
50
  [project.scripts]
50
51
  create-forge = "create_forge.cli:app"
@@ -350,6 +350,7 @@ def _run_engine_preview(
350
350
 
351
351
  def _report_created(project_name: object, dst: Path, *, updatable: bool = True) -> None:
352
352
  """Print the success panel once a project has actually been written."""
353
+ check_command = "uv run poe check" if updatable else "uv run --locked poe check"
353
354
  update_line = (
354
355
  "[dim]Pull later template changes with: uvx create-forge update[/dim]"
355
356
  if updatable
@@ -360,7 +361,7 @@ def _report_created(project_name: object, dst: Path, *, updatable: bool = True)
360
361
  Panel(
361
362
  f"[bold]{project_name}[/bold] created at [dim]{dst}[/dim]\n\n"
362
363
  f" cd {dst.name}\n"
363
- " uv run poe check\n\n"
364
+ f" {check_command}\n\n"
364
365
  f"{update_line}",
365
366
  border_style="green",
366
367
  )
@@ -2,7 +2,7 @@
2
2
 
3
3
  This is the one internal generation path CF-07.01 introduces (ADR 0014).
4
4
  It depends on `create_forge.engine` -- and therefore, transitively, on the
5
- development-only `forge-template` dependency -- but its own source never
5
+ optional `forge-template` engine extra (ADR 0018) -- but its own source never
6
6
  imports `forge_template` directly: type annotations that need engine-owned
7
7
  types import them only under `TYPE_CHECKING`, so this module's runtime
8
8
  behaviour never requires the engine to be *type-checkable*, only to be
@@ -12,8 +12,8 @@ the only module whose source touches `forge_template` at runtime, per ADR
12
12
 
13
13
  `create_forge.cli` imports this module lazily, inside `--engine-preview`'s
14
14
  branch only, guarded by `try/except ImportError` -- see ADR 0014 for why:
15
- `forge-template` is not a runtime dependency of the released CLI, so no
16
- module reachable at `cli.py`'s own import time may depend on it.
15
+ `forge-template` is not installed by a plain `pip install create-forge`, so
16
+ no module reachable at `cli.py`'s own import time may depend on it.
17
17
  """
18
18
 
19
19
  from __future__ import annotations
@@ -57,24 +57,44 @@ def _resolved_component_options(
57
57
  answers: Mapping[str, object],
58
58
  archetype: str,
59
59
  component_options: Mapping[str, Mapping[str, object]] | None,
60
+ descriptors: Sequence[ComponentDescriptor],
60
61
  ) -> Mapping[str, Mapping[str, object]] | None:
61
62
  """Derive `component_options` when the caller supplied none.
62
63
 
63
- The one archetype-specific branch in this codebase (CF-08.02): `library`
64
- predates the engine, so its legacy `build_backend`/`versioning`
65
- answers need translating into the production `packaging_mode` option or
66
- a user's choice silently reverts to the engine's own default. `cli` has
67
- no options and needs no translation -- every other archetype passes
68
- through unchanged, keyed on the engine's own
69
- `map_legacy_library_answers` naming rather than on a local archetype
70
- list, so this does not grow into a per-archetype registry here.
64
+ `library` predates the engine, so its legacy `build_backend`/`versioning`
65
+ answers need translating into the production `packaging_mode` option or a
66
+ user's choice silently reverts to the engine's own default. CF-08.03's
67
+ archetype-parity review (ADR 0019) found this derivation keyed on a
68
+ hardcoded `archetype != "library"` check -- the one archetype-specific
69
+ branch in this codebase -- and generalised it: `engine.map_legacy_library_options`
70
+ already names the option it produces (`packaging_mode`), and the
71
+ selected archetype's own discovered `ComponentDescriptor.options` already
72
+ declares whether it accepts that name. Applying the mapping only when the
73
+ descriptor declares it needs no archetype id anywhere in this function,
74
+ so a future archetype that also wants the legacy mapping (or `library`
75
+ itself being renamed) needs no change here -- only in the engine's own
76
+ manifest.
77
+
78
+ The descriptor lookup runs before the mapping call, not after: an
79
+ archetype that declares no options at all (`cli`) never needs
80
+ `engine.map_legacy_library_options` invoked on its behalf, matching the
81
+ old code's behaviour of never calling it for a non-`library` archetype.
71
82
  """
72
- if component_options is not None or archetype != "library":
83
+ if component_options is not None:
73
84
  return component_options
85
+ descriptor = next((d for d in descriptors if d.id == archetype), None)
86
+ if descriptor is None or not descriptor.options:
87
+ return None
74
88
  legacy = legacy_library_answers(answers)
75
89
  if legacy is None:
76
90
  return None
77
- return {"library": engine.map_legacy_library_options(legacy)}
91
+ mapped = engine.map_legacy_library_options(legacy)
92
+ if not mapped:
93
+ return None
94
+ declared = {option.name for option in descriptor.options}
95
+ if not set(mapped) <= declared:
96
+ return None
97
+ return {archetype: mapped}
78
98
 
79
99
 
80
100
  def build_generation_request(
@@ -95,19 +115,20 @@ def build_generation_request(
95
115
  this pipeline mints no component identifiers of its own. An explicit
96
116
  `component_options` is likewise passed through unchanged; when the
97
117
  caller supplies none, `_resolved_component_options` derives the one
98
- legacy mapping this repository still owns. `discover()` runs for its own
99
- compatibility-ladder effect and to surface real descriptors to callers;
100
- `discover_archetypes()` is what actually drives selection, from
101
- `cli.py`.
118
+ legacy mapping this repository still owns, gated on the selected
119
+ archetype's own discovered descriptor rather than a hardcoded id
120
+ (CF-08.03, ADR 0019). `discover()`'s result is reused for exactly that
121
+ gate; `discover_archetypes()` is the separate, `kind`-filtered view that
122
+ actually drives selection, from `cli.py`.
102
123
 
103
124
  Every downstream call (`build_project_spec`, `validate`, `render`)
104
125
  independently re-checks package/protocol compatibility before doing its
105
126
  own work, so there is no side effect -- in-memory or otherwise -- before
106
127
  every check has passed.
107
128
  """
108
- engine.discover()
129
+ descriptors = engine.discover()
109
130
  resolved_options = _resolved_component_options(
110
- answers, archetype, component_options
131
+ answers, archetype, component_options, descriptors
111
132
  )
112
133
  payload = build_spec_payload(
113
134
  answers,
@@ -123,19 +144,23 @@ def build_generation_request(
123
144
 
124
145
 
125
146
  def finalise_generation_request(request: GenerationRequest, destination: Path) -> None:
126
- """Stage and finalise `request`'s rendered files (ADR 0015).
147
+ """Stage, lock, and finalise `request`'s rendered files (ADR 0021).
127
148
 
128
149
  Renders them into a directory adjacent to `destination`, then moves that
129
- directory into place atomically.
150
+ directory into place atomically. ``uv.lock`` is created after the reviewed
151
+ render is written and before the rename, so lock resolution cannot leave a
152
+ partial destination.
130
153
 
131
154
  `create-forge` does not call `forge_template.validate_rendered_project`
132
155
  itself -- `engine.render()` already did, as the last step inside
133
156
  `build_generation_request`. Reaching this function at all means that
134
157
  validation already passed; this function's only job is the filesystem
135
- half create-forge owns: staging, target-safety, and an atomic rename.
158
+ half create-forge owns: staging, target-safety, lock finalisation, and an
159
+ atomic rename.
136
160
  """
137
161
  with staging.staged(destination) as staging_dir:
138
162
  staging.write_files(
139
163
  staging_dir,
140
164
  ((file.target, file.content) for file in request.rendered.files),
141
165
  )
166
+ staging.create_uv_lock(staging_dir)
@@ -2,11 +2,12 @@
2
2
 
3
3
  Deliberately engine-free: nothing here imports `forge_template`, not even
4
4
  under `TYPE_CHECKING`. That keeps this module in the wheel and in the fast
5
- test suite with no `engine` dependency group installed, and lets it serve
6
- both `runner.scaffold()` (Copier writes straight to the destination; this
5
+ test suite with no optional `engine` extra (ADR 0018) installed, and lets it
6
+ serve both `runner.scaffold()` (Copier writes straight to the destination; this
7
7
  module only cleans up after a failure) and `pipeline.finalise_generation_request()`
8
8
  (the engine path; this module stages and atomically finalises). See
9
- [ADR 0015](../../docs/adr/0015-staged-filesystem-generation.md) and the
9
+ [ADR 0015](../../docs/adr/0015-staged-filesystem-generation.md),
10
+ [ADR 0021](../../docs/adr/0021-client-finalises-engine-lockfiles.md), and the
10
11
  canonical [filesystem generation contract](../../docs/filesystem-generation.md)
11
12
  for why the two paths differ and what each guarantees.
12
13
  """
@@ -15,6 +16,7 @@ from __future__ import annotations
15
16
 
16
17
  import contextlib
17
18
  import shutil
19
+ import subprocess
18
20
  import tempfile
19
21
  import warnings
20
22
  from pathlib import Path, PurePosixPath
@@ -90,6 +92,41 @@ def write_files(root: Path, files: Iterable[tuple[str, bytes]]) -> None:
90
92
  raise StagingError(msg) from exc
91
93
 
92
94
 
95
+ def create_uv_lock(root: Path) -> None:
96
+ """Resolve the staged project's committed lockfile before finalisation.
97
+
98
+ The engine render is deliberately side-effect free, so ``uv.lock`` is a
99
+ client-owned finalisation artefact. Run uv without a shell and translate
100
+ launch or resolution failures into the staging error surface; the
101
+ surrounding :func:`staged` context then removes the incomplete tree and
102
+ leaves the destination untouched.
103
+ """
104
+ command = ["uv", "lock", "--directory", str(root)]
105
+ try:
106
+ result = subprocess.run( # noqa: S603 - reviewed fixed executable
107
+ command,
108
+ capture_output=True,
109
+ text=True,
110
+ check=False,
111
+ )
112
+ except FileNotFoundError as exc:
113
+ msg = (
114
+ "could not create uv.lock because the uv executable is unavailable; "
115
+ "install uv>=0.12,<0.13 and retry"
116
+ )
117
+ raise StagingError(msg) from exc
118
+ except OSError as exc:
119
+ msg = f"could not launch uv to create uv.lock: {exc}"
120
+ raise StagingError(msg) from exc
121
+
122
+ if result.returncode != 0:
123
+ msg = (
124
+ f"uv lock failed with exit status {result.returncode}; check the "
125
+ "project's declared dependencies and package-index access, then retry"
126
+ )
127
+ raise StagingError(msg)
128
+
129
+
93
130
  def _on_rm_error(func: object, path: str, exc_info: object) -> None:
94
131
  """`shutil.rmtree` error handler: clear read-only and retry once.
95
132
 
@@ -0,0 +1,70 @@
1
+ """Shared pytest configuration for contributor-only test options and fixtures
2
+ shared across the `e2e`-marked suites.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import shutil
9
+
10
+ import pytest
11
+
12
+
13
+ def pytest_addoption(parser: pytest.Parser) -> None:
14
+ """Register the local forge-template checkout used by drift tests."""
15
+ parser.addoption(
16
+ "--forge-template-root",
17
+ metavar="PATH",
18
+ help=(
19
+ "Read copier.yml from a local forge-template checkout instead of "
20
+ "cloning its latest release tag."
21
+ ),
22
+ )
23
+
24
+
25
+ @pytest.fixture(scope="session")
26
+ def create_forge_command() -> str:
27
+ """Resolve the real console script -- not `python -m create_forge`, and
28
+ not Typer's `CliRunner`. Both would skip the `[project.scripts]` entry
29
+ point users actually invoke. Shared by every `e2e`-marked module that
30
+ drives the real binary: `tests/test_e2e_generation.py` (Copier path) and
31
+ `tests/test_e2e_engine_generation.py` (engine path).
32
+ """
33
+ found = shutil.which("create-forge")
34
+ if found is None: # pragma: no cover - packaging bug, not a test failure mode
35
+ pytest.fail(
36
+ "the 'create-forge' console script is not on PATH -- "
37
+ "run this suite via `uv run pytest`, which installs it"
38
+ )
39
+ return found
40
+
41
+
42
+ @pytest.fixture(scope="session")
43
+ def e2e_child_env() -> dict[str, str]:
44
+ """The subprocess environment for a real `create-forge` invocation.
45
+
46
+ Both e2e paths spawn a child `uv` of their own -- the Copier path's
47
+ `copier.yml` `_tasks` run `uv sync --all-groups` and `git commit` inside
48
+ the generated project; the engine path's tests run the generated
49
+ project's own `uv run poe check` directly. Two things must hold for that
50
+ child `uv` to target the generated project rather than this repository's
51
+ own environment: `VIRTUAL_ENV`, `UV_PROJECT_ENVIRONMENT`, `PYTHONHOME`,
52
+ and `PYTHONPATH` -- all set by `uv run` for *this* pytest process -- must
53
+ not leak into the child. And `git commit` (the Copier path only) needs an
54
+ identity CI does not have, which a plain `env=` argument supplies for the
55
+ whole subprocess tree with no plumbum-snapshot caveat (contrast
56
+ `tests/test_update_network.py`'s `_git_identity_for_template_tasks`
57
+ fixture, needed only because that test calls `scaffold()` in-process).
58
+ """
59
+ env = dict(os.environ)
60
+ for leak in ("VIRTUAL_ENV", "UV_PROJECT_ENVIRONMENT", "PYTHONHOME", "PYTHONPATH"):
61
+ env.pop(leak, None)
62
+ env.update(
63
+ {
64
+ "GIT_AUTHOR_NAME": "create-forge e2e",
65
+ "GIT_AUTHOR_EMAIL": "create-forge-e2e@example.invalid",
66
+ "GIT_COMMITTER_NAME": "create-forge e2e",
67
+ "GIT_COMMITTER_EMAIL": "create-forge-e2e@example.invalid",
68
+ }
69
+ )
70
+ return env