Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
- [x] Deliver [ticket-020](project/ticket-020/README.md): record Goal 2.1.291
release notes and publication evidence without another version change.
State: `DONE / PUBLICATION`; classification: `SERVICE / integration`.
- [ ] Deliver [ticket-021](project/ticket-021/README.md): detect only writable
Python `__version__` declarations and support conventional version modules.
State: `IN_PROGRESS / VALIDATION`; classification: `BUG / application`.
- [ ] After governance bootstrap, execute the sequential phases defined in
[the refactoring plan](docs/GOAL_KORU_SUBACTOR_REFACTORING_PLAN.md), with one
narrowly scoped ticket active at a time.
Expand All @@ -71,7 +74,7 @@
> `ticket-010 DONE`; `ticket-011 DONE`; `ticket-012 DONE`;
> `ticket-013 DONE`; `ticket-014 CANCELLED`; `ticket-015 DONE`;
> `ticket-016 DONE`; `ticket-017 DONE`; `ticket-018 DONE`;
> `ticket-019 DONE`; `ticket-020 DONE`.
> `ticket-019 DONE`; `ticket-020 DONE`; `ticket-021 IN_PROGRESS`.

> **Recently shipped (manual note):** `goal all [PATHS...]` monorepo sweep —
> runs `goal -a` in every git repo with uncommitted changes under the given
Expand Down
8 changes: 7 additions & 1 deletion goal/cli/version_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
"setup.py",
"pom.xml",
}
_PYTHON_VERSION_FILENAMES = {
"__init__.py",
"version.py",
"_version.py",
"__about__.py",
}
_DERIVED_LOCKS = {
"pyproject.toml": ("uv.lock", "poetry.lock", "pdm.lock"),
"package.json": ("package-lock.json", "pnpm-lock.yaml"),
Expand Down Expand Up @@ -291,7 +297,7 @@ def _configured_specs(config) -> tuple[str, ...]:


def _candidate_spec(path: Path, content: Optional[str] = None) -> Optional[str]:
if path.name == "__init__.py":
if path.name in _PYTHON_VERSION_FILENAMES:
try:
text = content if content is not None else path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
Expand Down
29 changes: 21 additions & 8 deletions goal/config/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def _detect_version_files(self) -> List[str]:
}
manifests = ("pyproject.toml", "package.json", "Cargo.toml", "setup.py")
shallowest: Dict[str, str] = {}
init_candidates: List[str] = []
python_version_candidates: List[str] = []

for dirpath, dirnames, filenames in os.walk("."):
dirnames[:] = [
Expand All @@ -292,8 +292,13 @@ def _detect_version_files(self) -> List[str]:
prev = shallowest.get(filename)
if prev is None or rel.count(os.sep) < prev.count(os.sep):
shallowest[filename] = rel
elif filename == "__init__.py":
init_candidates.append(rel)
elif filename in {
"__init__.py",
"version.py",
"_version.py",
"__about__.py",
}:
python_version_candidates.append(rel)

version_files: List[str] = []
version_path = Path("VERSION")
Expand All @@ -308,14 +313,22 @@ def _detect_version_files(self) -> List[str]:
if filename in shallowest:
version_files.append(f"{shallowest[filename]}:version")

# First __init__.py (shallowest, then alphabetical) that declares
# __version__ — only project packages remain after pruning above.
for rel in sorted(init_candidates, key=lambda p: (p.count(os.sep), p)):
# First conventional Python carrier (shallowest, then alphabetical)
# with a writable literal assignment. Imports and re-exports are not
# declarations and must never replace an explicit configured source.
for rel in sorted(
python_version_candidates, key=lambda p: (p.count(os.sep), p)
):
try:
if "__version__" in Path(rel).read_text():
content = Path(rel).read_text(encoding="utf-8")
if re.search(
r'^__version__\s*=\s*["\'][^"\']+["\']',
content,
re.MULTILINE,
):
version_files.append(f"{rel}:__version__")
break
except OSError as exc:
except (OSError, UnicodeDecodeError) as exc:
logger.debug("Unable to scan %s for __version__: %s", rel, exc)

if not version_files:
Expand Down
1 change: 1 addition & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ This file indexes governance tickets without taking ownership of
| **ticket-018** | [`README.md`](./ticket-018/README.md) | [`preprompt.md`](./ticket-018/preprompt.md) | - | [`ai-codex.md`](./ticket-018/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-018/ai-codex-logs.txt) | [`changelog.md`](./ticket-018/changelog.md) |
| **ticket-019** | [`README.md`](./ticket-019/README.md) | [`preprompt.md`](./ticket-019/preprompt.md) | - | [`ai-codex.md`](./ticket-019/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-019/ai-codex-logs.txt) | [`changelog.md`](./ticket-019/changelog.md) |
| **ticket-020** | [`README.md`](./ticket-020/README.md) | [`preprompt.md`](./ticket-020/preprompt.md) | - | [`ai-codex.md`](./ticket-020/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-020/ai-codex-logs.txt) | [`changelog.md`](./ticket-020/changelog.md) |
| **ticket-021** | [`README.md`](./ticket-021/README.md) | [`preprompt.md`](./ticket-021/preprompt.md) | - | [`ai-codex.md`](./ticket-021/ai-codex.md) | [`ai-codex-logs.txt`](./ticket-021/ai-codex-logs.txt) | [`changelog.md`](./ticket-021/changelog.md) |
<!-- AUTO:TICKET_INDEX:END -->
50 changes: 50 additions & 0 deletions project/ticket-021/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Ticket 021: Detect writable Python version declarations

- **ID**: ticket-021
- **Owner**: unresolved:human
- **Status**: IN_PROGRESS
- **Workflow state**: VALIDATION
- **Created**: 2026-08-10

## Goal and scope

Make Python version discovery select only writable, top-level
`__version__ = "..."` declarations. Importing or re-exporting `__version__`
must not make a file a version carrier, while conventional `version.py` and
`_version.py` modules must be discoverable. This keeps `goal check-versions`
from rewriting a valid `goal.yaml` to an unreadable selector.

## Acceptance criteria

- [x] AC-01: The user requested autonomous continuation and correct Goal
decisions about which files carry versions.
- [x] AC-02: Import-only `__init__.py` files are ignored by configuration
detection.
- [x] AC-03: A conventional Python version module with a literal assignment is
discovered and remains readable/writable by version-state logic.
- [x] AC-04: Focused and full tests plus governance pass.

## Validation evidence

- Focused version-discovery suite: 26 passed.
- Full Goal suite: 512 passed, 2 skipped.
- The detector run against `wellmanifest/wellm` selects
`src/wellmanifest/version.py:__version__` and excludes the compatibility
re-export in `src/well/__init__.py`.

## Session authorization

The user explicitly authorized autonomous implementation, testing and
publication. No repeated confirmation is required inside this bounded ticket;
exact-head validation and protected merge remain mandatory.

## Risk boundary

Only version-source discovery and regression tests change. Version precedence,
bump arithmetic, registry comparison, publication credentials and delivery
policy remain unchanged.

## Participants

- Human participant: unresolved; no user-* file was created by this script.
- Agent participant: [ai-codex.md](ai-codex.md)
4 changes: 4 additions & 0 deletions project/ticket-021/ai-codex-logs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
2026-08-10: Reproduced in wellmanifest/wellm: detection selected an import-only
src/well/__init__.py and made goal check-versions unreadable.
2026-08-10: New detector selects src/wellmanifest/version.py:__version__.
2026-08-10: Focused tests 26 passed; full suite 512 passed, 2 skipped.
34 changes: 34 additions & 0 deletions project/ticket-021/ai-codex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
participant-id: agent:codex
participant: codex
role: agent
ticket: ticket-021
---
# Participant: codex (AI agent)

## Understanding

Goal currently treats any `__version__` substring in the shallowest
`__init__.py` as a declaration. In wellm that selects a re-export, overwrites
the correct configured carrier and makes `check-versions` fail.

## Execution plan

1. Add import-only and conventional-version-module regression cases.
2. Centralize the anchored literal-assignment predicate in both discovery
paths.
3. Run focused/full tests and governance.
4. Deliver and publish through the protected Goal workflow.

## Actual changes

- Initialized the bounded ticket and reproduced the defect in wellm.
- Restricted Python carrier discovery to anchored literal assignments.
- Added conventional `version.py`, `_version.py` and `__about__.py` carriers.
- Added manager and version-state regression coverage; full suite passes.

## Blockers

- None inside the recorded intent; proceed without a second confirmation.
- New authority remains required for destructive action, secret access, new
external coordination, material objective expansion and trusted merge.
11 changes: 11 additions & 0 deletions project/ticket-021/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Ticket Changelog (ticket-021)

## [0.1.0] - 2026-08-10

- Initial governance scaffold created.
- No human participant identity or content was generated.
- Recorded the import-only false positive and conventional version-module
discovery scope.
- Import-only compatibility modules are no longer selected as version files.
- Conventional Python version modules now participate in discovery and
synchronization when they contain a literal assignment.
68 changes: 68 additions & 0 deletions project/ticket-021/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-021",
"summary": "Detect writable Python version declarations",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "requested"
},
"allowedPaths": [
"goal/config/manager.py",
"goal/cli/version_state.py",
"tests/test_detect_version_files.py",
"tests/test_version_state.py",
"project/ticket-021/**",
"TODO.md",
"project/TICKETS.md"
],
"forbiddenPaths": [
".governance/**",
".github/**",
"project/ticket-*/user-*.md",
"VERSION",
"pyproject.toml",
"uv.lock",
"goal/__init__.py"
],
"stacks": ["python"],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "97a27a5cbaff522d078bef2390358741f7b8eed4",
"targetBranch": "main",
"outcome": "Goal detects the real writable Python version carrier and never mistakes an import-only module for one",
"nonGoals": [
"No bump arithmetic, registry, dependency or publication change",
"No version metadata or governance package change"
],
"complexity": "S",
"estimatedMinutes": 25,
"budgets": {
"maxImplementationFiles": 4,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Use one anchored literal-assignment predicate for conventional Python version carrier modules",
"components": [
{"name": "version-source-discovery", "paths": ["goal/config/manager.py", "goal/cli/version_state.py", "tests/test_detect_version_files.py", "tests/test_version_state.py"]}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {"impact": "none", "states": [], "evidence": []},
"rollback": "Revert the discovery predicate and its regression tests"
},
"runtimeDependencies": [],
"validation": [
{"criterion": "AC-02", "commands": ["pytest tests/test_detect_version_files.py -q"], "evidence": "Import-only modules are excluded"},
{"criterion": "AC-03", "commands": ["pytest tests/test_version_state.py -q"], "evidence": "Conventional version modules are readable and writable"},
{"criterion": "AC-04", "commands": ["pytest -q", "./project/governance-check.sh"], "evidence": "Full suite and governance pass"}
]
}
}
12 changes: 12 additions & 0 deletions project/ticket-021/preprompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Ticket preprompt

- **Task ID**: ticket-021
- **Task title**: Detect writable Python version declarations
- **Created**: 2026-08-10T11:46:28Z

Keep executable implementation outside this governance/evidence directory.
Read a human-owned user-*.md file only when one exists.
The request to execute this work creates SESSION_EXECUTION_AUTHORIZATION;
proceed within the recorded intent without a redundant confirmation prompt.
Require new authority for destructive action, secrets, external coordination,
material objective expansion and trusted merge approval.
16 changes: 16 additions & 0 deletions tests/test_detect_version_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ def test_prefers_shallowest_package(tmp_path):
assert init_entries == ["rootpkg/__init__.py:__version__"]


def test_import_only_initializer_defers_to_version_module(tmp_path):
compatibility = tmp_path / "src/well"
compatibility.mkdir(parents=True)
(compatibility / "__init__.py").write_text(
"from wellmanifest import __version__\n"
)
package = tmp_path / "src/wellmanifest"
package.mkdir(parents=True)
(package / "version.py").write_text('__version__ = "0.2.0rc4"\n')

files = _detect_in(tmp_path)

assert "src/well/__init__.py:__version__" not in files
assert "src/wellmanifest/version.py:__version__" in files


def test_multiline_version_contract_is_not_the_release_version(tmp_path):
(tmp_path / "VERSION").write_text(
"FORMAT=bioxfoundry.intent-version/v1\nARTIFACT=intent-corpus\n"
Expand Down
21 changes: 21 additions & 0 deletions tests/test_version_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
collect_version_sources,
resolve_version_decision,
validate_version_sources,
write_version_source,
)


Expand Down Expand Up @@ -245,6 +246,26 @@ def test_imported_version_name_is_not_detected_as_a_declaration(tmp_path, monkey
assert {source.spec for source in sources} == {"VERSION"}


def test_conventional_version_module_is_detected_and_writable(tmp_path, monkeypatch):
package = tmp_path / "src/package"
package.mkdir(parents=True)
version_module = package / "version.py"
version_module.write_text('__version__ = "1.0.0"\n')
(package / "__init__.py").write_text(
"from package.version import __version__\n"
)
(tmp_path / "VERSION").write_text("1.0.0\n")
monkeypatch.chdir(tmp_path)

sources = collect_version_sources({"versioning": {"files": ["VERSION"]}})

by_spec = {source.spec: source for source in sources}
assert "src/package/__init__.py:__version__" not in by_spec
assert by_spec["src/package/version.py:__version__"].value == "1.0.0"
assert write_version_source("src/package/version.py:__version__", "1.0.1")
assert version_module.read_text() == '__version__ = "1.0.1"\n'


def test_registry_failure_does_not_disable_local_git_decision(tmp_path, monkeypatch):
config = _init_release(tmp_path, "1.2.3")
monkeypatch.chdir(tmp_path)
Expand Down