stack-dev/stack-close skill split; publish stack-machinery note; model-selection fix (#47 Block 2)
Files changed: - CHANGES.md - VERSION - instructions/claude-code-model-selection.md - instructions/dev/stack-close/SKILL.md - instructions/dev/stack-dev/SKILL.md - tools/CONTRACT.md - tools/chemenu/commands/git_publish.py - tools/chemenu/tests/test_git_publish.py
This commit is contained in:
@@ -423,6 +423,41 @@ def counted_files_of(
|
||||
return [change for change in changes if not is_exempt(change.path, prefixes)]
|
||||
|
||||
|
||||
# The same scope `version-parts.md` names for the stack version itself -
|
||||
# "tools/, types/, instructions/, AGENTS.md and the contracts" - reused here
|
||||
# to decide whether a publish's changeset falls under it. Not a copy of that
|
||||
# rule: version-parts.md states the scope in prose for a human choosing a
|
||||
# version part, this instantiates the same boundary in code for a different
|
||||
# question (does this publish deserve the closing-phase reminder below).
|
||||
STACK_MACHINERY_PREFIXES = ("tools/", "types/", "instructions/")
|
||||
STACK_MACHINERY_NAMES = ("AGENTS.md",)
|
||||
|
||||
STACK_MACHINERY_NOTE = (
|
||||
"Note: this publish touched stack machinery. What a stack-dev session "
|
||||
"does next - closing prose, a changelog entry's accuracy, whether a "
|
||||
"docs/ page went stale - is not covered by docs verify, instructions "
|
||||
"verify, or pytest. No tool checks it; a session has to."
|
||||
)
|
||||
|
||||
|
||||
def touches_stack_machinery(changed_files: list[str]) -> bool:
|
||||
"""Whether `changed_files` includes a path under `version-parts.md`'s
|
||||
scope for the stack version - `tools/`, `types/`, `instructions/`,
|
||||
`AGENTS.md`, or any `<stage>/CONTRACT.md`. A publish in this class is,
|
||||
by construction of the `stack-dev`/`stack-close` split, always followed
|
||||
by the unchecked closing phase - `STACK_MACHINERY_NOTE` times a reminder
|
||||
to land exactly there, for any session, not only one that read the
|
||||
skill that names it."""
|
||||
for path in changed_files:
|
||||
if path in STACK_MACHINERY_NAMES:
|
||||
return True
|
||||
if path.endswith("CONTRACT.md"):
|
||||
return True
|
||||
if path.startswith(STACK_MACHINERY_PREFIXES):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
YES_REMOVED_MESSAGE = (
|
||||
"--yes no longer exists. The Mass-Update Gate is cleared with `--confirm <token>`, and the "
|
||||
"token comes from the gate's own refusal output - run this command without it first to see "
|
||||
@@ -1138,3 +1173,5 @@ def publish_command(
|
||||
},
|
||||
)
|
||||
success(f"Published changes to {remote}/{branch}." if push else "Committed changes (not pushed).")
|
||||
if touches_stack_machinery(changed_files):
|
||||
typer.echo(STACK_MACHINERY_NOTE)
|
||||
|
||||
@@ -10,6 +10,7 @@ from chemenu.commands._util import EXIT_NEEDS_CLEARANCE
|
||||
from chemenu.commands.git_publish import (
|
||||
DEFAULT_MASS_UPDATE_THRESHOLD,
|
||||
GATE_EXEMPT_PREFIXES,
|
||||
STACK_MACHINERY_NOTE,
|
||||
YES_REMOVED_MESSAGE,
|
||||
FileChange,
|
||||
attention_notes,
|
||||
@@ -29,6 +30,7 @@ from chemenu.commands.git_publish import (
|
||||
rerun_command,
|
||||
scale_line,
|
||||
sync_command,
|
||||
touches_stack_machinery,
|
||||
)
|
||||
|
||||
|
||||
@@ -290,6 +292,26 @@ def test_gate_message_names_generated_files_as_their_own_reason():
|
||||
assert "kb/provenance.md" not in message
|
||||
|
||||
|
||||
def test_stack_machinery_detects_the_version_parts_scope():
|
||||
"""The same boundary version-parts.md names for the stack version: tools/,
|
||||
types/, instructions/, AGENTS.md, and any <stage>/CONTRACT.md."""
|
||||
assert touches_stack_machinery(["instructions/dev/issue-tracking.md"])
|
||||
assert touches_stack_machinery(["tools/chemenu/commands/git_publish.py"])
|
||||
assert touches_stack_machinery(["types/instruction.md"])
|
||||
assert touches_stack_machinery(["AGENTS.md"])
|
||||
assert touches_stack_machinery(["kb/CONTRACT.md"])
|
||||
assert touches_stack_machinery(["raw/CONTRACT.md"])
|
||||
|
||||
|
||||
def test_stack_machinery_excludes_ordinary_content():
|
||||
"""kb/ content, docs/ prose and work/ scratch carry no normative sentence
|
||||
and are not what the closing-phase reminder is about."""
|
||||
assert not touches_stack_machinery(["kb/entities/systems/Foo.md"])
|
||||
assert not touches_stack_machinery(["docs/why-gates-are-code.md"])
|
||||
assert not touches_stack_machinery(["work/ingest-x/extract-0.md"])
|
||||
assert not touches_stack_machinery([])
|
||||
|
||||
|
||||
# --- publish_command integration: a real git repo + a local bare remote ---
|
||||
|
||||
|
||||
@@ -356,6 +378,24 @@ def test_below_threshold_publish_goes_straight_through(repo):
|
||||
assert _git(repo, "status", "--porcelain", "-uall").stdout == ""
|
||||
|
||||
|
||||
def test_publish_notes_stack_machinery_after_success(repo, capsys):
|
||||
"""The closing-phase reminder lands exactly once, after the OK line, and
|
||||
only when the changeset actually falls under version-parts.md's scope."""
|
||||
(repo / "instructions").mkdir()
|
||||
(repo / "instructions/example.md").write_text("x\n", encoding="utf-8")
|
||||
_publish(message="touch instructions")
|
||||
out = capsys.readouterr().out
|
||||
assert STACK_MACHINERY_NOTE in out
|
||||
assert out.count(STACK_MACHINERY_NOTE) == 1
|
||||
|
||||
|
||||
def test_publish_stays_quiet_for_ordinary_content(repo, capsys):
|
||||
_write_files(repo, 1)
|
||||
_publish(message="ordinary content")
|
||||
out = capsys.readouterr().out
|
||||
assert STACK_MACHINERY_NOTE not in out
|
||||
|
||||
|
||||
def test_at_threshold_publish_asks_for_clearance_and_stages_nothing(repo):
|
||||
_write_files(repo, 10)
|
||||
with pytest.raises(typer.Exit) as excinfo:
|
||||
|
||||
Reference in New Issue
Block a user