@deftai/directive-content 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.githooks/pre-push +10 -9
  2. package/Taskfile.yml +48 -58
  3. package/UPGRADING.md +1 -1
  4. package/docs/assets/directive-lifecycle-diagram.png +0 -0
  5. package/docs/directive-lifecycle.md +73 -0
  6. package/docs/getting-started.md +5 -1
  7. package/package.json +3 -3
  8. package/packs/skills/skills-pack-0.1.json +22 -22
  9. package/scm/github.md +20 -2
  10. package/tasks/change.yml +16 -31
  11. package/tasks/ci.yml +8 -0
  12. package/tasks/commit.yml +12 -19
  13. package/tasks/core.yml +10 -0
  14. package/tasks/engine.yml +42 -0
  15. package/tasks/framework.yml +3 -0
  16. package/tasks/install.yml +20 -19
  17. package/tasks/migrate.yml +26 -15
  18. package/tasks/project.yml +16 -0
  19. package/tasks/toolchain.yml +15 -5
  20. package/tasks/vbrief.yml +4 -3
  21. package/tasks/verify.yml +12 -14
  22. package/scripts/_agents_md.py +0 -494
  23. package/scripts/_cache_fetch.py +0 -635
  24. package/scripts/_cache_quota.py +0 -529
  25. package/scripts/_cache_refresh.py +0 -163
  26. package/scripts/_cache_validate.py +0 -209
  27. package/scripts/_content_root.py +0 -42
  28. package/scripts/_doctor_state.py +0 -277
  29. package/scripts/_event_detect.py +0 -305
  30. package/scripts/_events.py +0 -514
  31. package/scripts/_lifecycle_hygiene.py +0 -568
  32. package/scripts/_pathspec.py +0 -91
  33. package/scripts/_policy_show_cli.py +0 -266
  34. package/scripts/_precutover.py +0 -92
  35. package/scripts/_project_context.py +0 -224
  36. package/scripts/_project_definition_io.py +0 -164
  37. package/scripts/_relocate_snapshot.py +0 -209
  38. package/scripts/_relocate_states.py +0 -343
  39. package/scripts/_resolve_preflight_path.py +0 -152
  40. package/scripts/_safe_subprocess.py +0 -167
  41. package/scripts/_session_start_hook.py +0 -205
  42. package/scripts/_sor_gate_diff.py +0 -365
  43. package/scripts/_stdio_utf8.py +0 -59
  44. package/scripts/_triage_bootstrap_gitignore.py +0 -904
  45. package/scripts/_triage_classify_cli.py +0 -122
  46. package/scripts/_triage_queue_cli.py +0 -625
  47. package/scripts/_triage_scope_cli.py +0 -343
  48. package/scripts/_triage_scope_drift_cli.py +0 -121
  49. package/scripts/_triage_scope_ignores.py +0 -286
  50. package/scripts/_triage_scope_milestone.py +0 -432
  51. package/scripts/_triage_scope_mutations.py +0 -337
  52. package/scripts/_triage_scope_renderers.py +0 -207
  53. package/scripts/_triage_smoketest_stages.py +0 -674
  54. package/scripts/_triage_subscribe_cli.py +0 -140
  55. package/scripts/_triage_welcome_cli.py +0 -421
  56. package/scripts/_vbrief_build.py +0 -239
  57. package/scripts/_vbrief_fidelity.py +0 -479
  58. package/scripts/_vbrief_legacy.py +0 -589
  59. package/scripts/_vbrief_reconciliation.py +0 -883
  60. package/scripts/_vbrief_routing.py +0 -277
  61. package/scripts/_vbrief_safety.py +0 -778
  62. package/scripts/_vbrief_sources.py +0 -312
  63. package/scripts/_vbrief_speckit.py +0 -262
  64. package/scripts/_vbrief_story_quality.py +0 -353
  65. package/scripts/_vbrief_validation.py +0 -299
  66. package/scripts/build_dist.py +0 -412
  67. package/scripts/cache.py +0 -1078
  68. package/scripts/cache_scanner.py +0 -745
  69. package/scripts/candidates_log.py +0 -432
  70. package/scripts/capacity_backfill.py +0 -680
  71. package/scripts/capacity_show.py +0 -653
  72. package/scripts/ci_local.py +0 -689
  73. package/scripts/code_structure_validate.py +0 -765
  74. package/scripts/codebase_default_extractor.py +0 -495
  75. package/scripts/codebase_map.py +0 -304
  76. package/scripts/codebase_map_fresh.py +0 -104
  77. package/scripts/codebase_projection_registry.py +0 -94
  78. package/scripts/codebase_provider.py +0 -582
  79. package/scripts/doctor.py +0 -2552
  80. package/scripts/framework_commands.py +0 -505
  81. package/scripts/gh_rest.py +0 -882
  82. package/scripts/github_auth_modes.py +0 -437
  83. package/scripts/github_body.py +0 -292
  84. package/scripts/ip_risk.py +0 -531
  85. package/scripts/issue_emit.py +0 -670
  86. package/scripts/issue_ingest.py +0 -1064
  87. package/scripts/migrate_preflight.py +0 -418
  88. package/scripts/migrate_vbrief.py +0 -2677
  89. package/scripts/monitor_pr.py +0 -401
  90. package/scripts/pack_migrate_lessons.py +0 -336
  91. package/scripts/pack_migrate_patterns.py +0 -254
  92. package/scripts/pack_migrate_rules.py +0 -350
  93. package/scripts/pack_migrate_skills.py +0 -423
  94. package/scripts/pack_migrate_strategies.py +0 -311
  95. package/scripts/pack_migrate_swarm_spec.py +0 -250
  96. package/scripts/pack_render.py +0 -434
  97. package/scripts/packs_slice.py +0 -712
  98. package/scripts/platform_capabilities.py +0 -336
  99. package/scripts/policy.py +0 -2826
  100. package/scripts/policy_set.py +0 -324
  101. package/scripts/pr_check_closing_keywords.py +0 -524
  102. package/scripts/pr_check_protected_issues.py +0 -267
  103. package/scripts/pr_merge_readiness.py +0 -1004
  104. package/scripts/pr_wait_mergeable.py +0 -669
  105. package/scripts/prd_render.py +0 -159
  106. package/scripts/preflight_architecture_sor.py +0 -974
  107. package/scripts/preflight_branch.py +0 -289
  108. package/scripts/preflight_cache.py +0 -974
  109. package/scripts/preflight_gh.py +0 -721
  110. package/scripts/preflight_implementation.py +0 -272
  111. package/scripts/preflight_story_start.py +0 -838
  112. package/scripts/preflight_wip_cap.py +0 -149
  113. package/scripts/probe_session.py +0 -545
  114. package/scripts/project_render.py +0 -293
  115. package/scripts/quarantine_ext.py +0 -237
  116. package/scripts/reconcile_issues.py +0 -1442
  117. package/scripts/refresh-path.ps1 +0 -107
  118. package/scripts/release.py +0 -2030
  119. package/scripts/release_e2e.py +0 -1011
  120. package/scripts/release_publish.py +0 -486
  121. package/scripts/release_rollback.py +0 -980
  122. package/scripts/relocate.py +0 -1034
  123. package/scripts/resolve_changelog_unreleased.py +0 -667
  124. package/scripts/resolve_version.py +0 -490
  125. package/scripts/resume_conditions.py +0 -706
  126. package/scripts/ritual_sentinel.py +0 -609
  127. package/scripts/roadmap_render.py +0 -635
  128. package/scripts/rule_ownership_lint.py +0 -325
  129. package/scripts/scm.py +0 -591
  130. package/scripts/scope_audit_log.py +0 -387
  131. package/scripts/scope_decompose.py +0 -654
  132. package/scripts/scope_demote.py +0 -509
  133. package/scripts/scope_lifecycle.py +0 -1126
  134. package/scripts/scope_undo.py +0 -772
  135. package/scripts/session_start.py +0 -406
  136. package/scripts/setup_ghx.py +0 -339
  137. package/scripts/setup_windows.ps1 +0 -220
  138. package/scripts/slice_audit.py +0 -585
  139. package/scripts/slice_record.py +0 -530
  140. package/scripts/slice_record_existing.py +0 -692
  141. package/scripts/slug_normalize.py +0 -178
  142. package/scripts/spec_render.py +0 -477
  143. package/scripts/spec_validate.py +0 -238
  144. package/scripts/subagent_monitor.py +0 -658
  145. package/scripts/swarm_complete_cohort.py +0 -644
  146. package/scripts/swarm_launch.py +0 -1206
  147. package/scripts/swarm_readiness.py +0 -554
  148. package/scripts/swarm_verify_review_clean.py +0 -438
  149. package/scripts/swarm_worktrees.py +0 -497
  150. package/scripts/toolchain-check.py +0 -52
  151. package/scripts/triage_actions.py +0 -871
  152. package/scripts/triage_bootstrap.py +0 -1153
  153. package/scripts/triage_bulk.py +0 -630
  154. package/scripts/triage_classify.py +0 -932
  155. package/scripts/triage_help.py +0 -1685
  156. package/scripts/triage_queue.py +0 -1944
  157. package/scripts/triage_reconcile.py +0 -581
  158. package/scripts/triage_refresh.py +0 -643
  159. package/scripts/triage_scope.py +0 -999
  160. package/scripts/triage_scope_drift.py +0 -575
  161. package/scripts/triage_smoketest.py +0 -396
  162. package/scripts/triage_subscribe.py +0 -399
  163. package/scripts/triage_summary.py +0 -1011
  164. package/scripts/triage_welcome.py +0 -1178
  165. package/scripts/ts_check_lane.py +0 -86
  166. package/scripts/validate-links.py +0 -64
  167. package/scripts/validate_strategy_output.py +0 -212
  168. package/scripts/vbrief_activate.py +0 -228
  169. package/scripts/vbrief_migrate_conformance.py +0 -368
  170. package/scripts/vbrief_reconcile_graph.py +0 -306
  171. package/scripts/vbrief_reconcile_labels.py +0 -460
  172. package/scripts/vbrief_reconcile_umbrellas.py +0 -741
  173. package/scripts/vbrief_validate.py +0 -1144
  174. package/scripts/verify-stubs.py +0 -61
  175. package/scripts/verify_capacity.py +0 -160
  176. package/scripts/verify_encoding.py +0 -699
  177. package/scripts/verify_hooks_installed.py +0 -206
  178. package/scripts/verify_investigation.py +0 -360
  179. package/scripts/verify_judgment_gates.py +0 -827
  180. package/scripts/verify_no_task_runtime.py +0 -171
  181. package/scripts/verify_scm_boundary.py +0 -509
  182. package/scripts/verify_session_ritual.py +0 -389
  183. package/scripts/verify_tools.py +0 -426
  184. package/scripts/verify_vbrief_conformance.py +0 -478
@@ -1,412 +0,0 @@
1
- #!/usr/bin/env python3
2
- """build_dist.py -- cross-platform release-archive builder (#736).
3
-
4
- Replaces the prior ``tasks/core.yml::build`` shape which dispatched
5
- ``tar -czf ... --exclude=...`` on Linux / macOS but
6
- ``Compress-Archive -Path . -DestinationPath dist\\deft-X.Y.Z.zip -Force``
7
- on Windows. The Windows branch had no exclude list, so the resulting zip
8
- included ``.git/`` (often hundreds of MB of history), ``backup/``,
9
- ``node_modules/``, AND -- because ``Compress-Archive -Path .`` walks the
10
- output directory recursively -- the prior ``dist/`` artifact, leading to
11
- unbounded growth on every re-run.
12
-
13
- The fix is a single Python helper using stdlib ``tarfile`` and ``zipfile``
14
- with one canonical exclude list, dispatched as a single command from
15
- ``tasks/core.yml::build`` (no platform split). Format selection is
16
- data-driven by ``sys.platform`` (overridable via ``--format`` for tests).
17
-
18
- The script is intentionally stdlib-only -- it ships with the framework
19
- distribution itself, so any external dependency would be a chicken-and-egg
20
- problem during release.
21
-
22
- Windows installer manifest resources (#1441)
23
- --------------------------------------------
24
- This script packages the *framework* archive; it does NOT build the Go
25
- ``deft-install`` binaries (the release workflow's ``go build`` matrix does).
26
- The Windows binaries embed an ``asInvoker`` application manifest so Windows'
27
- installer-detection heuristic does not auto-elevate the ``install-*.exe``
28
- asset (which would pop a UAC prompt and break headless ``--yes`` runs). The
29
- manifest is carried by the committed per-arch resource objects
30
- ``cmd/deft-install/resource_windows_{amd64,arm64}.syso``; ``go build`` links
31
- them automatically for ``GOOS=windows`` and ignores them elsewhere, so no
32
- step here (and no extra release tooling) is required. To regenerate them
33
- after editing ``cmd/deft-install/deft-install.manifest`` or
34
- ``versioninfo.json``, run ``go generate ./cmd/deft-install/``.
35
-
36
- Usage
37
- -----
38
- uv run python scripts/build_dist.py --version 0.22.0
39
- uv run python scripts/build_dist.py --version 0.22.0 --format zip
40
- uv run python scripts/build_dist.py --version 0.22.0 \\
41
- --exclude-extra .venv,htmlcov
42
-
43
- Exit codes
44
- ----------
45
- 0 -- archive written
46
- 1 -- runtime failure (filesystem error, archive write failure)
47
- 2 -- configuration error (missing --version / missing root)
48
-
49
- Refs #736.
50
- """
51
-
52
- from __future__ import annotations
53
-
54
- import argparse
55
- import os
56
- import re
57
- import sys
58
- import tarfile
59
- import zipfile
60
- from pathlib import Path
61
-
62
- # Make sibling helpers importable both when run as __main__ and when imported
63
- # by tests via importlib.util.spec_from_file_location.
64
- sys.path.insert(0, str(Path(__file__).resolve().parent))
65
-
66
- from _stdio_utf8 import reconfigure_stdio # noqa: E402
67
-
68
- reconfigure_stdio()
69
-
70
-
71
- # ---- Constants --------------------------------------------------------------
72
-
73
- # Canonical exclude list. Top-level directory names that MUST never be in
74
- # the archive per the #736 acceptance criteria + the broader .gitignore
75
- # conventions in this repo. The match is purely by path-component name --
76
- # any directory anywhere under the root whose basename matches one of these
77
- # is pruned during the os.walk traversal.
78
- #
79
- # The first four entries (.git, dist, backup, node_modules) mirror the
80
- # previous Linux/macOS tar exclude list verbatim so the cross-platform
81
- # parity test in tests/content/test_taskfile_zip_parity.py can assert the
82
- # exclude set is preserved as the task contract changes shape. The
83
- # additional entries (__pycache__ ... .ruff_cache) extend the list for
84
- # Python-tooling artifacts that appear in development checkouts and would
85
- # otherwise bloat the archive.
86
- DEFAULT_EXCLUDES: tuple[str, ...] = (
87
- ".git",
88
- "dist",
89
- "backup",
90
- "node_modules",
91
- "__pycache__",
92
- ".venv",
93
- "htmlcov",
94
- ".pytest_cache",
95
- ".mypy_cache",
96
- ".ruff_cache",
97
- ".coverage",
98
- )
99
-
100
- # Repo-relative prefixes that should not be shipped in consumer framework
101
- # payloads. These are project-management / forensic artifacts for this source
102
- # repository, not runtime framework files. Keep vbrief/schemas/** and other
103
- # runtime surfaces by pruning only the historical lifecycle folders.
104
- DEFAULT_EXCLUDED_PATH_PREFIXES: tuple[str, ...] = (
105
- "history/archive",
106
- "vbrief/completed",
107
- "vbrief/cancelled",
108
- )
109
-
110
- EXIT_OK = 0
111
- EXIT_RUNTIME_ERROR = 1
112
- EXIT_CONFIG_ERROR = 2
113
-
114
- # Vendored TypeScript engine test-file exclusion (#1878). The dist artifact
115
- # ships the TypeScript engine sources under packages/{cli,core}/. The engine's
116
- # own *.test.* / *.spec.* files would be discovered by a vitest-based consumer's
117
- # default include glob (**/*.{test,spec}.?(c|m)[jt]s?(x)) and fail their CI, so
118
- # they are excluded from the produced artifact -- keeping the tarball/zip
119
- # consistent with the installer-side prune (pruneVendoredTSTests in
120
- # cmd/deft-install/deposit.go). Only test SOURCE files are dropped; non-test
121
- # engine sources (e.g. index.ts) are retained.
122
- _VENDORED_TS_TEST_RE = re.compile(r"(?i)\.(test|spec)\.(c|m)?[jt]sx?$")
123
-
124
- # Archive root directory inside the produced artifact. Consumers extracting
125
- # the tarball / zip get a single top-level ``deft/`` directory rather than
126
- # a sea of top-level files, matching the previous tar(1) behaviour where
127
- # ``tar -czf foo.tar.gz .`` produces ``./`` entries that extract into the
128
- # current directory but most tools display as ``./<name>``.
129
- ARCHIVE_ROOT = "deft"
130
-
131
- # C1 flatten (#1875 / #1669 Wave-1 LockedDecisions). The #1875 move relocated
132
- # every shippable asset under a single ``content/`` root in the SOURCE repo.
133
- # The consumer-facing deposit layout (``.deft/core/<x>``) MUST stay byte-stable,
134
- # so the archive strips the ``content/`` prefix when packaging: a source file at
135
- # ``content/coding/coding.md`` ships as ``deft/coding/coding.md`` and deposits to
136
- # ``.deft/core/coding/coding.md`` exactly as before the move. Non-content entries
137
- # (engine / harness / repo-dev) and the named root harness-entry files
138
- # (AGENTS.md / main.md / SKILL.md / REFERENCES.md) are unaffected. This is the
139
- # single flatten point -- the Go installer (cmd/deft-install) needs no change.
140
- CONTENT_PREFIX = "content/"
141
-
142
-
143
- def _flatten_content_prefix(rel_posix: str) -> str:
144
- """Strip the leading ``content/`` prefix so the deposit stays byte-stable."""
145
- if rel_posix == "content":
146
- return rel_posix
147
- if rel_posix.startswith(CONTENT_PREFIX):
148
- return rel_posix[len(CONTENT_PREFIX) :]
149
- return rel_posix
150
-
151
-
152
- # ---- Path filtering ---------------------------------------------------------
153
-
154
-
155
- def _iter_source_files(
156
- root: Path,
157
- excludes: frozenset[str],
158
- excluded_prefixes: tuple[str, ...] = DEFAULT_EXCLUDED_PATH_PREFIXES,
159
- ) -> list[tuple[Path, str]]:
160
- """Return a sorted list of ``(absolute_path, archive_relative_posix)``.
161
-
162
- Walks ``root`` skipping any directory or file whose basename matches
163
- an entry in ``excludes``. Returns deterministic ordering so the
164
- produced archive is reproducible across runs and across platforms
165
- (the task contract ``test_idempotent_rerun`` depends on this).
166
-
167
- Three pruning paths apply:
168
-
169
- 1. Directory pruning -- ``dirnames`` is mutated in place so any
170
- directory whose basename matches an exclude is skipped along with
171
- its entire subtree. The dist/ output dir is implicitly pruned by
172
- being in the canonical exclude list, which delivers the
173
- idempotency guarantee called out in the #736 acceptance criteria.
174
- 2. File pruning -- bare filenames whose basename matches an exclude
175
- (e.g. ``.coverage`` is written as a single regular file at the
176
- repo root by coverage.py, NOT a directory) are skipped. Without
177
- this branch the directory-only prune would silently fail to honor
178
- the documented intent for file-shaped artifacts (Greptile P1
179
- review on PR #773).
180
- 3. Path-prefix pruning -- directories and files whose repo-relative POSIX
181
- path starts with an entry in ``excluded_prefixes`` are dropped, keeping
182
- consumer archives free of source-repo forensic history while preserving
183
- runtime siblings such as ``vbrief/schemas/**``.
184
- 4. Vendored TS test-file pruning -- files under ``packages/`` whose basename
185
- matches the vitest test glob (``*.test.*`` / ``*.spec.*``) are dropped so
186
- a vitest-based consumer never discovers the framework's own tests, while
187
- non-test engine sources are retained (#1878).
188
- """
189
- entries: list[tuple[Path, str]] = []
190
- for dirpath, dirnames, filenames in os.walk(root):
191
- # Mutate dirnames in place to prune the walk -- canonical os.walk
192
- # idiom. Sort for determinism.
193
- kept_dirnames: list[str] = []
194
- for dirname in sorted(dirnames):
195
- if dirname in excludes:
196
- continue
197
- child = Path(dirpath) / dirname
198
- try:
199
- child_rel = child.relative_to(root).as_posix()
200
- except ValueError:
201
- continue
202
- if _matches_excluded_prefix(child_rel, excluded_prefixes):
203
- continue
204
- kept_dirnames.append(dirname)
205
- dirnames[:] = kept_dirnames
206
- for fname in sorted(filenames):
207
- if fname in excludes:
208
- # File-level pruning -- catches single-file artifacts
209
- # like .coverage that os.walk surfaces in `filenames`,
210
- # not `dirnames`.
211
- continue
212
- abs_path = Path(dirpath) / fname
213
- try:
214
- rel = abs_path.relative_to(root)
215
- except ValueError:
216
- # Defensive: os.walk should never yield a path outside
217
- # root, but symlinked traversals can in theory.
218
- continue
219
- rel_posix = rel.as_posix()
220
- if _matches_excluded_prefix(rel_posix, excluded_prefixes):
221
- continue
222
- if _is_vendored_ts_test(rel_posix):
223
- # Drop the vendored TS engine's own test files so a
224
- # vitest-based consumer does not discover and fail on them,
225
- # mirroring the installer-side prune (#1878).
226
- continue
227
- # C1 flatten: ship content/<x> as <x> so the deposit is byte-stable.
228
- entries.append((abs_path, _flatten_content_prefix(rel_posix)))
229
- return entries
230
-
231
-
232
- def _matches_excluded_prefix(rel_posix: str, prefixes: tuple[str, ...]) -> bool:
233
- """Return True when ``rel_posix`` is at or below an excluded path prefix."""
234
- return any(rel_posix == prefix or rel_posix.startswith(f"{prefix}/") for prefix in prefixes)
235
-
236
-
237
- def _is_vendored_ts_test(rel_posix: str) -> bool:
238
- """Return True for a vendored TypeScript engine test SOURCE file (#1878).
239
-
240
- True only when the repo-relative POSIX path is under ``packages/`` AND its
241
- basename matches the vitest-discoverable test glob (``*.test.*`` /
242
- ``*.spec.*`` with a ``[jt]s``/``x``/``c``/``m`` extension). Non-test engine
243
- sources under ``packages/`` (e.g. ``packages/core/src/index.ts``) return
244
- False so only the framework's own tests are dropped from the artifact.
245
- """
246
- if rel_posix != "packages" and not rel_posix.startswith("packages/"):
247
- return False
248
- basename = rel_posix.rsplit("/", 1)[-1]
249
- return _VENDORED_TS_TEST_RE.search(basename) is not None
250
-
251
-
252
- # ---- Archive writers --------------------------------------------------------
253
-
254
-
255
- def _write_tar_gz(root: Path, output: Path, entries: list[tuple[Path, str]]) -> int:
256
- """Write a gzipped tar archive of ``entries`` to ``output``.
257
-
258
- Each entry is added under the ``ARCHIVE_ROOT`` prefix so extraction
259
- yields a single top-level directory.
260
- """
261
- output.parent.mkdir(parents=True, exist_ok=True)
262
- count = 0
263
- with tarfile.open(output, "w:gz") as tar:
264
- for abs_path, rel in entries:
265
- tar.add(abs_path, arcname=f"{ARCHIVE_ROOT}/{rel}", recursive=False)
266
- count += 1
267
- return count
268
-
269
-
270
- def _write_zip(root: Path, output: Path, entries: list[tuple[Path, str]]) -> int:
271
- """Write a deflate-compressed zip archive of ``entries`` to ``output``."""
272
- output.parent.mkdir(parents=True, exist_ok=True)
273
- count = 0
274
- with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zf:
275
- for abs_path, rel in entries:
276
- zf.write(abs_path, arcname=f"{ARCHIVE_ROOT}/{rel}")
277
- count += 1
278
- return count
279
-
280
-
281
- # ---- Format / path resolution ----------------------------------------------
282
-
283
-
284
- def select_format(arg: str | None) -> str:
285
- """Return the archive format token (``tar`` or ``zip``).
286
-
287
- When ``arg`` is None the platform default applies: ``zip`` on Windows
288
- (``sys.platform`` startswith ``win``), ``tar`` everywhere else.
289
- """
290
- if arg:
291
- return arg.lower()
292
- if sys.platform.startswith("win"):
293
- return "zip"
294
- return "tar"
295
-
296
-
297
- def output_path(root: Path, version: str, fmt: str) -> Path:
298
- """Return the final artifact path for ``version`` + ``fmt`` under root."""
299
- suffix = "zip" if fmt == "zip" else "tar.gz"
300
- return root / "dist" / f"deft-{version}.{suffix}"
301
-
302
-
303
- # ---- Public build entry point ----------------------------------------------
304
-
305
-
306
- def build(
307
- root: Path,
308
- version: str,
309
- fmt: str,
310
- extra_excludes: tuple[str, ...] = (),
311
- ) -> Path:
312
- """Produce the release archive and return its path.
313
-
314
- Idempotency: the canonical exclude list contains ``dist`` so the
315
- output directory is pruned during traversal -- a stale prior artifact
316
- sitting at ``dist/deft-<version>.<ext>`` cannot be ingested into the
317
- new archive. As a belt-and-suspenders guard we also unlink the target
318
- output file if it already exists so the new archive is fresh.
319
- """
320
- excludes = frozenset((*DEFAULT_EXCLUDES, *extra_excludes))
321
- output = output_path(root, version, fmt)
322
- if output.exists():
323
- output.unlink()
324
- entries = _iter_source_files(root, excludes)
325
- if fmt == "zip":
326
- _write_zip(root, output, entries)
327
- else:
328
- _write_tar_gz(root, output, entries)
329
- return output
330
-
331
-
332
- # ---- CLI --------------------------------------------------------------------
333
-
334
-
335
- def _build_parser() -> argparse.ArgumentParser:
336
- parser = argparse.ArgumentParser(
337
- prog="build_dist.py",
338
- description=(
339
- "Build a cross-platform release archive (#736). Default format "
340
- "is tar.gz on Linux/macOS and zip on Windows; override with "
341
- "--format. Excludes .git, dist, backup, node_modules, and "
342
- "Python-tooling caches by default."
343
- ),
344
- )
345
- parser.add_argument(
346
- "--version",
347
- required=True,
348
- help=(
349
- "Version string used as the archive filename suffix "
350
- "(e.g. 0.22.0). Passed by tasks/core.yml::build via "
351
- "{{.VERSION}}, which itself resolves through "
352
- "scripts/resolve_version.py's priority chain."
353
- ),
354
- )
355
- parser.add_argument(
356
- "--format",
357
- choices=("tar", "zip"),
358
- default=None,
359
- help=(
360
- "Archive format override. tar=tar.gz, zip=zip. Default is "
361
- "platform-driven (zip on Windows, tar.gz elsewhere)."
362
- ),
363
- )
364
- parser.add_argument(
365
- "--root",
366
- type=Path,
367
- default=None,
368
- help=("Repository root to package (default: parent of the scripts/ directory)."),
369
- )
370
- parser.add_argument(
371
- "--exclude-extra",
372
- default="",
373
- help=(
374
- "Comma-separated extra directory basenames to exclude in "
375
- "addition to the canonical list."
376
- ),
377
- )
378
- return parser
379
-
380
-
381
- def _parse_extras(raw: str) -> tuple[str, ...]:
382
- """Split ``raw`` (comma-separated) into a tuple, stripping empties."""
383
- return tuple(p.strip() for p in raw.split(",") if p.strip())
384
-
385
-
386
- def main(argv: list[str] | None = None) -> int:
387
- parser = _build_parser()
388
- args = parser.parse_args(argv)
389
- if not args.version:
390
- print("error: --version is required", file=sys.stderr)
391
- return EXIT_CONFIG_ERROR
392
- root = (args.root or Path(__file__).resolve().parent.parent).resolve()
393
- if not root.is_dir():
394
- print(f"error: root not found: {root}", file=sys.stderr)
395
- return EXIT_CONFIG_ERROR
396
- fmt = select_format(args.format)
397
- extras = _parse_extras(args.exclude_extra)
398
- try:
399
- out = build(root, args.version, fmt, extras)
400
- except OSError as exc:
401
- print(f"error: {exc}", file=sys.stderr)
402
- return EXIT_RUNTIME_ERROR
403
- try:
404
- printable = out.relative_to(root)
405
- except ValueError:
406
- printable = out
407
- print(f"Created {printable}")
408
- return EXIT_OK
409
-
410
-
411
- if __name__ == "__main__":
412
- sys.exit(main())