@deftai/directive-content 0.58.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.
- package/.githooks/pre-push +10 -9
- package/Taskfile.yml +57 -67
- package/UPGRADING.md +1 -1
- package/docs/assets/directive-lifecycle-diagram.png +0 -0
- package/docs/directive-lifecycle.md +73 -0
- package/docs/getting-started.md +5 -1
- package/package.json +3 -3
- package/packs/rules/rules-pack-0.1.json +3 -3
- package/packs/skills/skills-pack-0.1.json +22 -22
- package/scm/github.md +20 -2
- package/tasks/change.yml +16 -31
- package/tasks/ci.yml +8 -0
- package/tasks/commit.yml +12 -19
- package/tasks/core.yml +10 -0
- package/tasks/engine.yml +42 -0
- package/tasks/framework.yml +3 -0
- package/tasks/install.yml +20 -19
- package/tasks/migrate.yml +26 -15
- package/tasks/project.yml +16 -0
- package/tasks/relocate.yml +18 -48
- package/tasks/toolchain.yml +15 -5
- package/tasks/vbrief.yml +4 -3
- package/tasks/verify.yml +12 -14
- package/templates/agents-entry.md +1 -2
- package/scripts/_agents_md.py +0 -494
- package/scripts/_cache_fetch.py +0 -635
- package/scripts/_cache_quota.py +0 -529
- package/scripts/_cache_refresh.py +0 -163
- package/scripts/_cache_validate.py +0 -209
- package/scripts/_content_root.py +0 -42
- package/scripts/_doctor_state.py +0 -277
- package/scripts/_event_detect.py +0 -305
- package/scripts/_events.py +0 -514
- package/scripts/_lifecycle_hygiene.py +0 -568
- package/scripts/_pathspec.py +0 -91
- package/scripts/_policy_show_cli.py +0 -266
- package/scripts/_precutover.py +0 -92
- package/scripts/_project_context.py +0 -224
- package/scripts/_project_definition_io.py +0 -164
- package/scripts/_relocate_snapshot.py +0 -209
- package/scripts/_relocate_states.py +0 -343
- package/scripts/_resolve_preflight_path.py +0 -152
- package/scripts/_safe_subprocess.py +0 -167
- package/scripts/_session_start_hook.py +0 -205
- package/scripts/_sor_gate_diff.py +0 -365
- package/scripts/_stdio_utf8.py +0 -59
- package/scripts/_triage_bootstrap_gitignore.py +0 -904
- package/scripts/_triage_classify_cli.py +0 -122
- package/scripts/_triage_queue_cli.py +0 -625
- package/scripts/_triage_scope_cli.py +0 -343
- package/scripts/_triage_scope_drift_cli.py +0 -121
- package/scripts/_triage_scope_ignores.py +0 -286
- package/scripts/_triage_scope_milestone.py +0 -432
- package/scripts/_triage_scope_mutations.py +0 -337
- package/scripts/_triage_scope_renderers.py +0 -207
- package/scripts/_triage_smoketest_stages.py +0 -674
- package/scripts/_triage_subscribe_cli.py +0 -140
- package/scripts/_triage_welcome_cli.py +0 -421
- package/scripts/_vbrief_build.py +0 -239
- package/scripts/_vbrief_fidelity.py +0 -479
- package/scripts/_vbrief_legacy.py +0 -589
- package/scripts/_vbrief_reconciliation.py +0 -883
- package/scripts/_vbrief_routing.py +0 -277
- package/scripts/_vbrief_safety.py +0 -778
- package/scripts/_vbrief_sources.py +0 -312
- package/scripts/_vbrief_speckit.py +0 -262
- package/scripts/_vbrief_story_quality.py +0 -353
- package/scripts/_vbrief_validation.py +0 -299
- package/scripts/build_dist.py +0 -412
- package/scripts/cache.py +0 -1078
- package/scripts/cache_scanner.py +0 -745
- package/scripts/candidates_log.py +0 -432
- package/scripts/capacity_backfill.py +0 -680
- package/scripts/capacity_show.py +0 -653
- package/scripts/ci_local.py +0 -689
- package/scripts/code_structure_validate.py +0 -765
- package/scripts/codebase_default_extractor.py +0 -495
- package/scripts/codebase_map.py +0 -304
- package/scripts/codebase_map_fresh.py +0 -104
- package/scripts/codebase_projection_registry.py +0 -94
- package/scripts/codebase_provider.py +0 -582
- package/scripts/doctor.py +0 -2551
- package/scripts/framework_commands.py +0 -505
- package/scripts/gh_rest.py +0 -882
- package/scripts/github_auth_modes.py +0 -437
- package/scripts/github_body.py +0 -292
- package/scripts/ip_risk.py +0 -531
- package/scripts/issue_emit.py +0 -670
- package/scripts/issue_ingest.py +0 -1064
- package/scripts/migrate_preflight.py +0 -418
- package/scripts/migrate_vbrief.py +0 -2677
- package/scripts/monitor_pr.py +0 -401
- package/scripts/pack_migrate_lessons.py +0 -336
- package/scripts/pack_migrate_patterns.py +0 -254
- package/scripts/pack_migrate_rules.py +0 -350
- package/scripts/pack_migrate_skills.py +0 -423
- package/scripts/pack_migrate_strategies.py +0 -311
- package/scripts/pack_migrate_swarm_spec.py +0 -250
- package/scripts/pack_render.py +0 -434
- package/scripts/packs_slice.py +0 -712
- package/scripts/platform_capabilities.py +0 -336
- package/scripts/policy.py +0 -2826
- package/scripts/policy_set.py +0 -324
- package/scripts/pr_check_closing_keywords.py +0 -524
- package/scripts/pr_check_protected_issues.py +0 -267
- package/scripts/pr_merge_readiness.py +0 -1004
- package/scripts/pr_wait_mergeable.py +0 -669
- package/scripts/prd_render.py +0 -159
- package/scripts/preflight_architecture_sor.py +0 -974
- package/scripts/preflight_branch.py +0 -289
- package/scripts/preflight_cache.py +0 -974
- package/scripts/preflight_gh.py +0 -721
- package/scripts/preflight_implementation.py +0 -272
- package/scripts/preflight_story_start.py +0 -838
- package/scripts/preflight_wip_cap.py +0 -149
- package/scripts/probe_session.py +0 -545
- package/scripts/project_render.py +0 -293
- package/scripts/quarantine_ext.py +0 -237
- package/scripts/reconcile_issues.py +0 -1442
- package/scripts/refresh-path.ps1 +0 -107
- package/scripts/release.py +0 -2030
- package/scripts/release_e2e.py +0 -1011
- package/scripts/release_publish.py +0 -486
- package/scripts/release_rollback.py +0 -980
- package/scripts/relocate.py +0 -1034
- package/scripts/resolve_changelog_unreleased.py +0 -667
- package/scripts/resolve_version.py +0 -490
- package/scripts/resume_conditions.py +0 -706
- package/scripts/ritual_sentinel.py +0 -609
- package/scripts/roadmap_render.py +0 -635
- package/scripts/rule_ownership_lint.py +0 -325
- package/scripts/scm.py +0 -591
- package/scripts/scope_audit_log.py +0 -387
- package/scripts/scope_decompose.py +0 -654
- package/scripts/scope_demote.py +0 -509
- package/scripts/scope_lifecycle.py +0 -1126
- package/scripts/scope_undo.py +0 -772
- package/scripts/session_start.py +0 -406
- package/scripts/setup_ghx.py +0 -339
- package/scripts/setup_windows.ps1 +0 -220
- package/scripts/slice_audit.py +0 -585
- package/scripts/slice_record.py +0 -530
- package/scripts/slice_record_existing.py +0 -692
- package/scripts/slug_normalize.py +0 -178
- package/scripts/spec_render.py +0 -477
- package/scripts/spec_validate.py +0 -238
- package/scripts/subagent_monitor.py +0 -658
- package/scripts/swarm_complete_cohort.py +0 -644
- package/scripts/swarm_launch.py +0 -1206
- package/scripts/swarm_readiness.py +0 -554
- package/scripts/swarm_verify_review_clean.py +0 -438
- package/scripts/swarm_worktrees.py +0 -497
- package/scripts/toolchain-check.py +0 -52
- package/scripts/triage_actions.py +0 -871
- package/scripts/triage_bootstrap.py +0 -1153
- package/scripts/triage_bulk.py +0 -630
- package/scripts/triage_classify.py +0 -932
- package/scripts/triage_help.py +0 -1685
- package/scripts/triage_queue.py +0 -1944
- package/scripts/triage_reconcile.py +0 -581
- package/scripts/triage_refresh.py +0 -643
- package/scripts/triage_scope.py +0 -999
- package/scripts/triage_scope_drift.py +0 -575
- package/scripts/triage_smoketest.py +0 -396
- package/scripts/triage_subscribe.py +0 -399
- package/scripts/triage_summary.py +0 -1011
- package/scripts/triage_welcome.py +0 -1178
- package/scripts/ts_check_lane.py +0 -86
- package/scripts/validate-links.py +0 -64
- package/scripts/validate_strategy_output.py +0 -212
- package/scripts/vbrief_activate.py +0 -228
- package/scripts/vbrief_migrate_conformance.py +0 -368
- package/scripts/vbrief_reconcile_graph.py +0 -306
- package/scripts/vbrief_reconcile_labels.py +0 -460
- package/scripts/vbrief_reconcile_umbrellas.py +0 -741
- package/scripts/vbrief_validate.py +0 -1144
- package/scripts/verify-stubs.py +0 -61
- package/scripts/verify_capacity.py +0 -160
- package/scripts/verify_encoding.py +0 -699
- package/scripts/verify_hooks_installed.py +0 -206
- package/scripts/verify_investigation.py +0 -360
- package/scripts/verify_judgment_gates.py +0 -827
- package/scripts/verify_no_task_runtime.py +0 -171
- package/scripts/verify_scm_boundary.py +0 -509
- package/scripts/verify_session_ritual.py +0 -389
- package/scripts/verify_tools.py +0 -426
- package/scripts/verify_vbrief_conformance.py +0 -478
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""resolve_version.py -- Python mirror of the Taskfile VERSION resolver (#723)
|
|
3
|
-
plus the canonical semver -> PEP 440 normalization helper (#771).
|
|
4
|
-
|
|
5
|
-
This script is an INDEPENDENT Python mirror of the version-resolution
|
|
6
|
-
priority chain that the canonical Taskfile-side resolver implements
|
|
7
|
-
inline in ``Taskfile.yml`` ``vars: VERSION: { sh: ... }``. The Taskfile
|
|
8
|
-
inline POSIX ``sh:`` block is the ACTUAL resolver consumed by
|
|
9
|
-
``task build`` / ``task release`` (run via go-task's embedded
|
|
10
|
-
mvdan/sh interpreter so it works cross-platform without requiring
|
|
11
|
-
``uv`` / Python at parse time).
|
|
12
|
-
|
|
13
|
-
This Python module is NOT invoked from ``Taskfile.yml``. It exists so
|
|
14
|
-
Python callers (regression tests in ``tests/cli/test_resolve_version.py``,
|
|
15
|
-
``scripts/release.py::run_build``, future scripts that need the
|
|
16
|
-
version at import time, etc.) have a single source of truth for the
|
|
17
|
-
same resolution priority -- avoiding silent drift between the Taskfile
|
|
18
|
-
``sh:`` block and ad-hoc Python re-implementations.
|
|
19
|
-
|
|
20
|
-
Resolution priority (first match wins -- mirrors the Taskfile sh block):
|
|
21
|
-
1. ``$DEFT_RELEASE_VERSION`` -- set by ``scripts/release.py::run_build``
|
|
22
|
-
so the in-flight release version (e.g. ``0.21.0``) becomes the
|
|
23
|
-
build artifact filename during ``task release -- 0.21.0``. The
|
|
24
|
-
Taskfile literal previously hard-coded ``0.20.0``, which produced
|
|
25
|
-
``dist/deft-0.20.0.zip`` during the v0.21.0 cut (#723).
|
|
26
|
-
2. ``git describe --tags --abbrev=0`` (stripped of leading ``v``) --
|
|
27
|
-
reflects the latest annotated release tag for standalone
|
|
28
|
-
``task build`` invocations on a tagged checkout.
|
|
29
|
-
3. ``0.0.0-dev`` -- fallback for fresh checkouts with no tags or
|
|
30
|
-
repositories where ``git`` is unavailable.
|
|
31
|
-
|
|
32
|
-
The script writes the resolved version to stdout WITHOUT a trailing
|
|
33
|
-
newline so its output matches the Taskfile inline ``sh:`` block's
|
|
34
|
-
``printf '%s'`` shape byte-for-byte (no trailing whitespace either
|
|
35
|
-
way). ``stderr`` is intentionally silent on the happy path.
|
|
36
|
-
|
|
37
|
-
If you change the priority chain here, you MUST also update the inline
|
|
38
|
-
``sh:`` block in ``Taskfile.yml`` (and vice versa) -- the two are kept
|
|
39
|
-
in lockstep by convention, not by code reuse.
|
|
40
|
-
|
|
41
|
-
PEP 440 normalization (#771)
|
|
42
|
-
----------------------------
|
|
43
|
-
``to_pep440(version)`` is the SINGLE CANONICAL converter from deft's
|
|
44
|
-
semver-shaped release tags (``vX.Y.Z`` / ``vX.Y.Z-rc.N`` / etc.) to
|
|
45
|
-
Python-package-safe PEP 440 versions. It is consumed by:
|
|
46
|
-
|
|
47
|
-
* ``scripts/release.py`` Step 5 -- syncs ``[project].version`` in
|
|
48
|
-
``pyproject.toml`` so the root metadata stops drifting from the
|
|
49
|
-
released tag (Phase A of #771);
|
|
50
|
-
* ``tests/content/test_pyproject_version_freshness.py`` -- regression
|
|
51
|
-
gate that fails if pyproject drifts;
|
|
52
|
-
* any FUTURE pip-packaging path (root-repo or thin wrapper, see #11)
|
|
53
|
-
MUST consume ``to_pep440`` rather than reimplementing the rule --
|
|
54
|
-
this is the documented Phase C extension hook so exactly ONE
|
|
55
|
-
normalization rule governs release-tag / CLI / PyPI surfaces.
|
|
56
|
-
|
|
57
|
-
Disposable / test-only tags (``v0.0.0-test.N``, etc.) are explicitly
|
|
58
|
-
classified non-publishable: ``to_pep440`` raises
|
|
59
|
-
``NonPublishableVersionError`` and ``is_publishable`` returns False.
|
|
60
|
-
The release pipeline catches this and skips the pyproject sync rather
|
|
61
|
-
than emitting a polluting throwaway version.
|
|
62
|
-
|
|
63
|
-
Refs #723, #74 (release foundation), #716 (safety hardening), #721
|
|
64
|
-
(canonical recovery anchor for the v0.21.0 cut session), #771
|
|
65
|
-
(pyproject truthfulness + PEP 440 normalization), #11 (future pip
|
|
66
|
-
packaging consumes this helper).
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
from __future__ import annotations
|
|
70
|
-
|
|
71
|
-
import os
|
|
72
|
-
import re
|
|
73
|
-
import subprocess
|
|
74
|
-
import sys
|
|
75
|
-
from collections.abc import Iterable
|
|
76
|
-
from pathlib import Path
|
|
77
|
-
|
|
78
|
-
DEV_FALLBACK = "0.0.0-dev"
|
|
79
|
-
ENV_VAR = "DEFT_RELEASE_VERSION"
|
|
80
|
-
|
|
81
|
-
# Framework install root for the vendored-install metadata lookups (#1323).
|
|
82
|
-
# This script lives at ``<install>/scripts/resolve_version.py``; its parent's
|
|
83
|
-
# parent is the framework deposit (``<install>``) where the Go installer
|
|
84
|
-
# writes the canonical ``VERSION`` manifest and the bare ``.deft-version``
|
|
85
|
-
# derivative. In framework-self-dev the same path resolves to the repo root.
|
|
86
|
-
_FRAMEWORK_ROOT = Path(__file__).resolve().parent.parent
|
|
87
|
-
|
|
88
|
-
# Parses the ``tag``/``ref`` field of the ``<install>/VERSION`` YAML manifest.
|
|
89
|
-
# Multiline so a single ``re.search`` finds whichever of the two lines comes
|
|
90
|
-
# first (they carry the same value in a well-formed manifest). Mirrors the
|
|
91
|
-
# inline regex in ``run::_VERSION_MANIFEST_TAG_RE``.
|
|
92
|
-
_MANIFEST_TAG_RE = re.compile(
|
|
93
|
-
r"^(?:tag|ref):\s*['\"]?v?([\d.][\w.-]*)['\"]?\s*$",
|
|
94
|
-
re.MULTILINE,
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
# ---------------------------------------------------------------------------
|
|
98
|
-
# PEP 440 normalization (#771)
|
|
99
|
-
# ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
# Accepts an optional leading ``v`` followed by strict ``X.Y.Z`` and an
|
|
102
|
-
# optional pre-release suffix ``-(rc|alpha|beta|test).N``. ``-test.N``
|
|
103
|
-
# is parsed (so we can classify it explicitly) but is NEVER mapped to a
|
|
104
|
-
# PEP 440 form -- see ``_NON_PUBLISHABLE_KINDS`` below.
|
|
105
|
-
_PEP440_TAG_RE = re.compile(
|
|
106
|
-
r"^v?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)"
|
|
107
|
-
r"(?:-(?P<kind>rc|alpha|beta|test)\.(?P<num>\d+))?$"
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# Mapping from the semver-style pre-release token to PEP 440's compressed
|
|
111
|
-
# spelling. PEP 440 collapses ``rc.3`` -> ``rc3`` (no separator) and
|
|
112
|
-
# spells ``alpha`` / ``beta`` as ``a`` / ``b``.
|
|
113
|
-
_PRE_KIND_MAP: dict[str, str] = {
|
|
114
|
-
"alpha": "a",
|
|
115
|
-
"beta": "b",
|
|
116
|
-
"rc": "rc",
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
# Pre-release tokens that classify a tag as non-publishable. ``test.N``
|
|
120
|
-
# is reserved for disposable / e2e-rehearsal tags (e.g. ``v0.0.0-test.1``
|
|
121
|
-
# from ``task release:e2e``) -- the release pipeline MUST skip the
|
|
122
|
-
# pyproject sync for these so PyPI / consumer-visible metadata is never
|
|
123
|
-
# polluted with throwaway versions.
|
|
124
|
-
_NON_PUBLISHABLE_KINDS: frozenset[str] = frozenset({"test"})
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class NonPublishableVersionError(ValueError):
|
|
128
|
-
"""Raised when a tag is classified as non-publishable for PyPI.
|
|
129
|
-
|
|
130
|
-
The release pipeline catches this in ``scripts/release.py`` Step 5
|
|
131
|
-
and skips the ``pyproject.toml`` ``[project].version`` rewrite so
|
|
132
|
-
disposable-tag releases (e.g. ``v0.0.0-test.1`` from the e2e
|
|
133
|
-
rehearsal harness) never leak into Python-packaging metadata.
|
|
134
|
-
|
|
135
|
-
Subclassing ``ValueError`` keeps catch-blocks that already trap
|
|
136
|
-
``ValueError`` (e.g. argparse error reporting) backward compatible;
|
|
137
|
-
callers that need to distinguish the publishability classification
|
|
138
|
-
from a generic parse failure check the concrete type.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def to_pep440(version: str) -> str:
|
|
143
|
-
"""Normalize a semver-shaped release tag to a PEP 440 version string.
|
|
144
|
-
|
|
145
|
-
Mappings (#771 acceptance):
|
|
146
|
-
|
|
147
|
-
``v0.22.0`` -> ``"0.22.0"``
|
|
148
|
-
``v0.20.0-rc.3`` -> ``"0.20.0rc3"``
|
|
149
|
-
``v0.20.0-beta.2`` -> ``"0.20.0b2"``
|
|
150
|
-
``v0.20.0-alpha.1`` -> ``"0.20.0a1"``
|
|
151
|
-
``v0.0.0-test.1`` -> raises ``NonPublishableVersionError``
|
|
152
|
-
|
|
153
|
-
The leading ``v`` is optional (matching ``_from_git`` which strips
|
|
154
|
-
it) so callers can pass either ``v0.22.0`` or ``0.22.0``.
|
|
155
|
-
|
|
156
|
-
Raises
|
|
157
|
-
------
|
|
158
|
-
NonPublishableVersionError
|
|
159
|
-
For ``test.N`` (and any other ``_NON_PUBLISHABLE_KINDS``) tags.
|
|
160
|
-
ValueError
|
|
161
|
-
For anything that does not parse as ``[v]X.Y.Z[-(rc|alpha|beta|test).N]``.
|
|
162
|
-
"""
|
|
163
|
-
if not isinstance(version, str):
|
|
164
|
-
raise ValueError(f"version must be a string, got {type(version).__name__}")
|
|
165
|
-
candidate = version.strip()
|
|
166
|
-
if not candidate:
|
|
167
|
-
raise ValueError("version must be a non-empty string")
|
|
168
|
-
match = _PEP440_TAG_RE.match(candidate)
|
|
169
|
-
if match is None:
|
|
170
|
-
raise ValueError(
|
|
171
|
-
f"Cannot normalize {candidate!r} to PEP 440: expected "
|
|
172
|
-
f"[v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta|test).N"
|
|
173
|
-
)
|
|
174
|
-
base = f"{int(match['major'])}.{int(match['minor'])}.{int(match['patch'])}"
|
|
175
|
-
kind = match.group("kind")
|
|
176
|
-
if kind is None:
|
|
177
|
-
return base
|
|
178
|
-
if kind in _NON_PUBLISHABLE_KINDS:
|
|
179
|
-
raise NonPublishableVersionError(
|
|
180
|
-
f"Version {candidate!r} carries non-publishable pre-release "
|
|
181
|
-
f"tag {kind!r}.{match.group('num')} -- release pipeline MUST "
|
|
182
|
-
f"skip pyproject.toml [project].version sync for this tag."
|
|
183
|
-
)
|
|
184
|
-
# Greptile advisory (#774): defensive .get() guard so a future regex
|
|
185
|
-
# extension that adds a kind without registering a mapping raises a
|
|
186
|
-
# clean ValueError instead of a bare KeyError. _PEP440_TAG_RE and
|
|
187
|
-
# _PRE_KIND_MAP / _NON_PUBLISHABLE_KINDS are kept in lockstep by
|
|
188
|
-
# convention; this guard converts a contract drift into an actionable
|
|
189
|
-
# diagnostic for the next maintainer.
|
|
190
|
-
pep_kind = _PRE_KIND_MAP.get(kind)
|
|
191
|
-
if pep_kind is None:
|
|
192
|
-
raise ValueError(
|
|
193
|
-
f"Unmapped pre-release kind {kind!r} for version {candidate!r}; "
|
|
194
|
-
"add it to _PRE_KIND_MAP or _NON_PUBLISHABLE_KINDS to keep "
|
|
195
|
-
"_PEP440_TAG_RE in lockstep with the publishability classifier."
|
|
196
|
-
)
|
|
197
|
-
pep_num = int(match.group("num"))
|
|
198
|
-
return f"{base}{pep_kind}{pep_num}"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def is_publishable(version: str) -> bool:
|
|
202
|
-
"""Return True iff ``version`` normalizes to a publishable PEP 440 string.
|
|
203
|
-
|
|
204
|
-
A return of False means the caller MUST NOT propagate ``version`` to
|
|
205
|
-
PyPI-facing metadata (e.g. ``pyproject.toml`` ``[project].version``).
|
|
206
|
-
Both ``NonPublishableVersionError`` and a generic parse ``ValueError``
|
|
207
|
-
classify as non-publishable -- a malformed tag is not safe to publish.
|
|
208
|
-
"""
|
|
209
|
-
try:
|
|
210
|
-
to_pep440(version)
|
|
211
|
-
except (NonPublishableVersionError, ValueError):
|
|
212
|
-
return False
|
|
213
|
-
return True
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _tag_name_from_ref(ref: str) -> str:
|
|
217
|
-
"""Return a tag name from a raw tag, ``refs/tags/*`` ref, or ls-remote line."""
|
|
218
|
-
candidate = ref.strip()
|
|
219
|
-
if not candidate:
|
|
220
|
-
return ""
|
|
221
|
-
parts = candidate.split()
|
|
222
|
-
if len(parts) >= 2:
|
|
223
|
-
candidate = parts[1]
|
|
224
|
-
if candidate.endswith("^{}"):
|
|
225
|
-
candidate = candidate[:-3]
|
|
226
|
-
prefix = "refs/tags/"
|
|
227
|
-
if candidate.startswith(prefix):
|
|
228
|
-
candidate = candidate[len(prefix) :]
|
|
229
|
-
return candidate.strip()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _publishable_tag_sort_key(version: str) -> tuple[int, int, int, int, int]:
|
|
233
|
-
"""Sort key for publishable release tags.
|
|
234
|
-
|
|
235
|
-
``git tag --sort=v:refname`` is not consistently available through
|
|
236
|
-
``ls-remote`` output, so remote-aware freshness checks sort tags locally.
|
|
237
|
-
Stable releases sort after prereleases for the same X.Y.Z.
|
|
238
|
-
"""
|
|
239
|
-
candidate = version.strip()
|
|
240
|
-
match = _PEP440_TAG_RE.match(candidate)
|
|
241
|
-
if match is None:
|
|
242
|
-
raise ValueError(
|
|
243
|
-
f"Cannot sort {candidate!r}: expected "
|
|
244
|
-
f"[v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N"
|
|
245
|
-
)
|
|
246
|
-
kind = match.group("kind")
|
|
247
|
-
if kind in _NON_PUBLISHABLE_KINDS:
|
|
248
|
-
raise NonPublishableVersionError(
|
|
249
|
-
f"Version {candidate!r} carries non-publishable pre-release tag {kind!r}."
|
|
250
|
-
)
|
|
251
|
-
prerelease_rank = {"alpha": 0, "beta": 1, "rc": 2, None: 3}.get(kind)
|
|
252
|
-
if prerelease_rank is None:
|
|
253
|
-
raise ValueError(f"Unmapped pre-release kind {kind!r} for {candidate!r}")
|
|
254
|
-
prerelease_num = int(match.group("num") or 0)
|
|
255
|
-
return (
|
|
256
|
-
int(match["major"]),
|
|
257
|
-
int(match["minor"]),
|
|
258
|
-
int(match["patch"]),
|
|
259
|
-
prerelease_rank,
|
|
260
|
-
prerelease_num,
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def latest_publishable_tag(tags: Iterable[str]) -> str | None:
|
|
265
|
-
"""Return the newest publishable release tag from raw tag/ref strings.
|
|
266
|
-
|
|
267
|
-
Malformed and explicitly non-publishable tags are ignored. The returned
|
|
268
|
-
value preserves the tag spelling after ref cleanup (including a leading
|
|
269
|
-
``v`` when present) so callers can report the exact source tag.
|
|
270
|
-
"""
|
|
271
|
-
best_tag: str | None = None
|
|
272
|
-
best_key: tuple[int, int, int, int, int] | None = None
|
|
273
|
-
for raw in tags:
|
|
274
|
-
tag = _tag_name_from_ref(raw)
|
|
275
|
-
if not tag or not is_publishable(tag):
|
|
276
|
-
continue
|
|
277
|
-
try:
|
|
278
|
-
key = _publishable_tag_sort_key(tag)
|
|
279
|
-
except (NonPublishableVersionError, ValueError):
|
|
280
|
-
continue
|
|
281
|
-
if best_key is None or key > best_key:
|
|
282
|
-
best_tag = tag
|
|
283
|
-
best_key = key
|
|
284
|
-
return best_tag
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def latest_local_publishable_tag(repo_root: Path | None = None) -> str | None:
|
|
288
|
-
"""Return the newest local publishable tag, or None when unavailable."""
|
|
289
|
-
cwd = repo_root if repo_root is not None else _FRAMEWORK_ROOT
|
|
290
|
-
try:
|
|
291
|
-
result = subprocess.run(
|
|
292
|
-
["git", "tag", "--list"],
|
|
293
|
-
capture_output=True,
|
|
294
|
-
text=True,
|
|
295
|
-
timeout=10,
|
|
296
|
-
check=False,
|
|
297
|
-
cwd=str(cwd),
|
|
298
|
-
)
|
|
299
|
-
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
300
|
-
return None
|
|
301
|
-
if result.returncode != 0:
|
|
302
|
-
return None
|
|
303
|
-
return latest_publishable_tag((result.stdout or "").splitlines())
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def latest_remote_publishable_tag(
|
|
307
|
-
remote: str = "origin",
|
|
308
|
-
repo_root: Path | None = None,
|
|
309
|
-
) -> str | None:
|
|
310
|
-
"""Return the newest publishable tag advertised by ``remote``.
|
|
311
|
-
|
|
312
|
-
This helper intentionally does not fetch or mutate the local tag store.
|
|
313
|
-
It is for read-only freshness checks that need to distinguish a stale
|
|
314
|
-
local checkout from the actual published release tags.
|
|
315
|
-
"""
|
|
316
|
-
cwd = repo_root if repo_root is not None else _FRAMEWORK_ROOT
|
|
317
|
-
try:
|
|
318
|
-
result = subprocess.run(
|
|
319
|
-
["git", "ls-remote", "--tags", "--refs", remote],
|
|
320
|
-
capture_output=True,
|
|
321
|
-
text=True,
|
|
322
|
-
timeout=10,
|
|
323
|
-
check=False,
|
|
324
|
-
cwd=str(cwd),
|
|
325
|
-
)
|
|
326
|
-
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
327
|
-
return None
|
|
328
|
-
if result.returncode != 0:
|
|
329
|
-
return None
|
|
330
|
-
return latest_publishable_tag((result.stdout or "").splitlines())
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
# ---------------------------------------------------------------------------
|
|
334
|
-
# Resolver priority chain (#723)
|
|
335
|
-
# ---------------------------------------------------------------------------
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def _from_env() -> str | None:
|
|
339
|
-
value = os.environ.get(ENV_VAR, "").strip()
|
|
340
|
-
return value or None
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def _from_manifest(base_dir: Path | None = None) -> str | None:
|
|
344
|
-
"""Return the version from ``<base_dir>/VERSION`` manifest, or None (#1323).
|
|
345
|
-
|
|
346
|
-
Reads the canonical install manifest's ``tag``/``ref`` field so a vendored
|
|
347
|
-
``.deft/core/`` install (no nested ``.git``) resolves its real version
|
|
348
|
-
rather than ``0.0.0-dev``. ``base_dir`` defaults to the framework root.
|
|
349
|
-
"""
|
|
350
|
-
base = base_dir if base_dir is not None else _FRAMEWORK_ROOT
|
|
351
|
-
manifest = base / "VERSION"
|
|
352
|
-
try:
|
|
353
|
-
if not manifest.is_file():
|
|
354
|
-
return None
|
|
355
|
-
text = manifest.read_text(encoding="utf-8")
|
|
356
|
-
except OSError:
|
|
357
|
-
return None
|
|
358
|
-
match = _MANIFEST_TAG_RE.search(text)
|
|
359
|
-
if match is None:
|
|
360
|
-
return None
|
|
361
|
-
return match.group(1).strip() or None
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def _from_deft_version(base_dir: Path | None = None) -> str | None:
|
|
365
|
-
"""Return the version from ``<base_dir>/.deft-version`` plaintext, or None (#1323).
|
|
366
|
-
|
|
367
|
-
Strips a leading ``v`` so the value matches the bare ``X.Y.Z`` shape.
|
|
368
|
-
``base_dir`` defaults to the framework root.
|
|
369
|
-
"""
|
|
370
|
-
base = base_dir if base_dir is not None else _FRAMEWORK_ROOT
|
|
371
|
-
marker = base / ".deft-version"
|
|
372
|
-
try:
|
|
373
|
-
if not marker.is_file():
|
|
374
|
-
return None
|
|
375
|
-
version = marker.read_text(encoding="utf-8").strip()
|
|
376
|
-
except OSError:
|
|
377
|
-
return None
|
|
378
|
-
if version.startswith("v"):
|
|
379
|
-
version = version[1:]
|
|
380
|
-
return version or None
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def _payload_is_own_git_root(payload_dir: Path) -> bool:
|
|
384
|
-
"""Return True iff ``payload_dir`` is itself a git top-level (#1454).
|
|
385
|
-
|
|
386
|
-
Guards the ``git describe`` version fallback so a vendored
|
|
387
|
-
``.deft/core/`` install (no VERSION manifest, no ``.deft-version``)
|
|
388
|
-
does NOT walk up into the consumer repo and report the CONSUMER's
|
|
389
|
-
tag as the framework version. ``git rev-parse --show-toplevel`` run
|
|
390
|
-
from ``payload_dir`` resolves to the enclosing repo's root; only when
|
|
391
|
-
that root IS ``payload_dir`` (framework-self-dev, where the payload
|
|
392
|
-
directory is the repo) do we trust ``git describe``.
|
|
393
|
-
|
|
394
|
-
Best-effort: a missing ``git`` binary, a timeout, a non-zero exit
|
|
395
|
-
(``payload_dir`` is not inside any repo), or empty output all return
|
|
396
|
-
False so the caller falls through to the dev fallback rather than
|
|
397
|
-
raising. Mirrors ``run::_payload_is_own_git_root``.
|
|
398
|
-
"""
|
|
399
|
-
try:
|
|
400
|
-
result = subprocess.run(
|
|
401
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
402
|
-
capture_output=True,
|
|
403
|
-
text=True,
|
|
404
|
-
timeout=10,
|
|
405
|
-
check=False,
|
|
406
|
-
cwd=str(payload_dir),
|
|
407
|
-
)
|
|
408
|
-
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
409
|
-
return False
|
|
410
|
-
if result.returncode != 0:
|
|
411
|
-
return False
|
|
412
|
-
toplevel = (result.stdout or "").strip()
|
|
413
|
-
if not toplevel:
|
|
414
|
-
return False
|
|
415
|
-
try:
|
|
416
|
-
return Path(toplevel).resolve() == payload_dir.resolve()
|
|
417
|
-
except OSError:
|
|
418
|
-
return False
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
def _from_git() -> str | None:
|
|
422
|
-
"""Return the latest annotated tag (without leading ``v``) or None.
|
|
423
|
-
|
|
424
|
-
Rooted at the framework root so a vendored ``.deft/core/`` install does
|
|
425
|
-
not pick up the consumer repo's tags (the manifest / ``.deft-version``
|
|
426
|
-
branches catch that case first; this is the framework-self-dev path).
|
|
427
|
-
|
|
428
|
-
#1454: additionally guarded so the fallback only fires when the
|
|
429
|
-
framework root is ITSELF the git top-level. On a vendored install with
|
|
430
|
-
no manifest the payload is a subdirectory of the consumer repo, so an
|
|
431
|
-
unguarded ``git describe`` would bleed the consumer's tag.
|
|
432
|
-
"""
|
|
433
|
-
if not _payload_is_own_git_root(_FRAMEWORK_ROOT):
|
|
434
|
-
return None
|
|
435
|
-
try:
|
|
436
|
-
result = subprocess.run(
|
|
437
|
-
["git", "describe", "--tags", "--abbrev=0"],
|
|
438
|
-
capture_output=True,
|
|
439
|
-
text=True,
|
|
440
|
-
timeout=10,
|
|
441
|
-
check=False,
|
|
442
|
-
cwd=str(_FRAMEWORK_ROOT),
|
|
443
|
-
)
|
|
444
|
-
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
445
|
-
return None
|
|
446
|
-
if result.returncode != 0:
|
|
447
|
-
return None
|
|
448
|
-
tag = (result.stdout or "").strip()
|
|
449
|
-
if not tag:
|
|
450
|
-
return None
|
|
451
|
-
if tag.startswith("v"):
|
|
452
|
-
tag = tag[1:]
|
|
453
|
-
return tag or None
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
def resolve_version() -> str:
|
|
457
|
-
"""Resolve the version using the documented priority chain.
|
|
458
|
-
|
|
459
|
-
Priority (first match wins -- mirrors ``run::_resolve_version``):
|
|
460
|
-
1. ``$DEFT_RELEASE_VERSION`` env override.
|
|
461
|
-
2. ``<install>/VERSION`` manifest ``tag``/``ref`` field (#1323).
|
|
462
|
-
3. ``<install>/.deft-version`` plaintext (#1323).
|
|
463
|
-
4. ``git describe --tags --abbrev=0`` rooted at the framework root.
|
|
464
|
-
5. ``0.0.0-dev`` fallback.
|
|
465
|
-
"""
|
|
466
|
-
env_value = _from_env()
|
|
467
|
-
if env_value:
|
|
468
|
-
return env_value
|
|
469
|
-
manifest_value = _from_manifest()
|
|
470
|
-
if manifest_value:
|
|
471
|
-
return manifest_value
|
|
472
|
-
deft_version_value = _from_deft_version()
|
|
473
|
-
if deft_version_value:
|
|
474
|
-
return deft_version_value
|
|
475
|
-
git_value = _from_git()
|
|
476
|
-
if git_value:
|
|
477
|
-
return git_value
|
|
478
|
-
return DEV_FALLBACK
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
def main(argv: list[str] | None = None) -> int:
|
|
482
|
-
# No flags today; argv is accepted for symmetry with sibling scripts
|
|
483
|
-
# that follow the argparse convention.
|
|
484
|
-
del argv
|
|
485
|
-
sys.stdout.write(resolve_version())
|
|
486
|
-
return 0
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if __name__ == "__main__":
|
|
490
|
-
sys.exit(main(sys.argv[1:]))
|