napkinstack 0.6.0__tar.gz → 0.6.2__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 (28) hide show
  1. {napkinstack-0.6.0 → napkinstack-0.6.2}/PKG-INFO +12 -10
  2. {napkinstack-0.6.0 → napkinstack-0.6.2}/README.md +11 -9
  3. {napkinstack-0.6.0 → napkinstack-0.6.2}/pyproject.toml +1 -1
  4. {napkinstack-0.6.0 → napkinstack-0.6.2}/pyproject.toml.orig +1 -1
  5. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/project.py +49 -7
  6. {napkinstack-0.6.0 → napkinstack-0.6.2}/LICENSE +0 -0
  7. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/__init__.py +0 -0
  8. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/cli.py +0 -0
  9. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/compat.py +0 -0
  10. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/discovery.py +0 -0
  11. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/doctor.py +0 -0
  12. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/fitness/__init__.py +0 -0
  13. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/fitness/boundaries.py +0 -0
  14. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/fitness/hygiene.py +0 -0
  15. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/fitness/manifests.py +0 -0
  16. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/fitness/plan.py +0 -0
  17. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/fitness/pr_scope.py +0 -0
  18. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/landed.py +0 -0
  19. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/modules.py +0 -0
  20. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/provenance.py +0 -0
  21. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/pull_request.py +0 -0
  22. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/skills.py +0 -0
  23. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/templates/module/AGENTS.md +0 -0
  24. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/templates/module/MANIFEST.yaml +0 -0
  25. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/templates/module/README.md +0 -0
  26. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/templates/module/docs/adr/.gitkeep +0 -0
  27. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/templates/module/src/.gitkeep +0 -0
  28. {napkinstack-0.6.0 → napkinstack-0.6.2}/src/napkinstack/templates/module/tests/.gitkeep +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napkinstack
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Engineering framework for several teams and their agents working in one repository: modules, contracts, guardrails in CI.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -20,21 +20,23 @@ modules, contracts, guardrails in CI. On the Django or Rails model, one command
20
20
  the project, which then receives new versions on demand; no application stack is imposed.
21
21
  Positioning and vocabulary: [`PRODUCT.md`](PRODUCT.md) §1.
22
22
 
23
- > **Status: v0.5.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): every barrier
24
- > the framework announces refuses what it claims to — the boundaries read the contracts a
25
- > module uses, a contract version someone relies on changes only with a proof, the module
26
- > checks and stale approvals are enforced, a verifier is not an author, and every verdict names
27
- > the framework that gave it. Proved on the project that exposed the defects: eight probes
28
- > replayed, each refused or accepted as announced. The contract proof now runs in the base's
29
- > tree, so a change cannot rewrite what judges it; the diagnosis says whether anything refuses
30
- > at all; and a record names what reached the default branch outside a pull request — it
31
- > records, it never refuses. A first pilot project, private, starts from here.
23
+ > **Status: v0.6.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): what the
24
+ > framework announces, it refuses — and what it cannot refuse, it says. The boundaries read the
25
+ > contracts a module uses, a contract version someone relies on changes only with a proof that
26
+ > runs in the base's tree, the module checks and stale approvals are enforced, a verifier is not
27
+ > an author, and every verdict names the framework that gave it. Since v0.6.0, a document still
28
+ > holding its template's words is refused, a deprecated module's consumers are named with their
29
+ > removal date, and a contract change runs the checks of both sides — the producer and every
30
+ > declared consumer. The diagnosis says whether anything refuses at all; a record names what
31
+ > reached the default branch outside a pull request, and never refuses it. A first pilot
32
+ > project, private, starts from here.
32
33
  > Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
33
34
  > [move to English](docs/governance/plans/2026-09-16-english-migration.md),
34
35
  > [frame, verify, approve](docs/governance/plans/2026-09-16-v0.3.0-frame-verify-approve.md),
35
36
  > [every barrier refuses](docs/governance/plans/2026-09-19-v0.4.0-every-barrier-refuses.md),
36
37
  > [a proof that cannot be rewritten](docs/governance/plans/2026-09-20-v0.5.0-a-proof-that-cannot-be-rewritten.md),
37
38
  > [the conformance suite](docs/governance/plans/2026-09-20-m12-conformance-suite.md),
39
+ > [what is announced refuses](docs/governance/plans/2026-09-20-v0.6.0-what-is-announced-refuses.md),
38
40
  > [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
39
41
 
40
42
  ## A project's journey
@@ -5,21 +5,23 @@ modules, contracts, guardrails in CI. On the Django or Rails model, one command
5
5
  the project, which then receives new versions on demand; no application stack is imposed.
6
6
  Positioning and vocabulary: [`PRODUCT.md`](PRODUCT.md) §1.
7
7
 
8
- > **Status: v0.5.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): every barrier
9
- > the framework announces refuses what it claims to — the boundaries read the contracts a
10
- > module uses, a contract version someone relies on changes only with a proof, the module
11
- > checks and stale approvals are enforced, a verifier is not an author, and every verdict names
12
- > the framework that gave it. Proved on the project that exposed the defects: eight probes
13
- > replayed, each refused or accepted as announced. The contract proof now runs in the base's
14
- > tree, so a change cannot rewrite what judges it; the diagnosis says whether anything refuses
15
- > at all; and a record names what reached the default branch outside a pull request — it
16
- > records, it never refuses. A first pilot project, private, starts from here.
8
+ > **Status: v0.6.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): what the
9
+ > framework announces, it refuses — and what it cannot refuse, it says. The boundaries read the
10
+ > contracts a module uses, a contract version someone relies on changes only with a proof that
11
+ > runs in the base's tree, the module checks and stale approvals are enforced, a verifier is not
12
+ > an author, and every verdict names the framework that gave it. Since v0.6.0, a document still
13
+ > holding its template's words is refused, a deprecated module's consumers are named with their
14
+ > removal date, and a contract change runs the checks of both sides — the producer and every
15
+ > declared consumer. The diagnosis says whether anything refuses at all; a record names what
16
+ > reached the default branch outside a pull request, and never refuses it. A first pilot
17
+ > project, private, starts from here.
17
18
  > Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
18
19
  > [move to English](docs/governance/plans/2026-09-16-english-migration.md),
19
20
  > [frame, verify, approve](docs/governance/plans/2026-09-16-v0.3.0-frame-verify-approve.md),
20
21
  > [every barrier refuses](docs/governance/plans/2026-09-19-v0.4.0-every-barrier-refuses.md),
21
22
  > [a proof that cannot be rewritten](docs/governance/plans/2026-09-20-v0.5.0-a-proof-that-cannot-be-rewritten.md),
22
23
  > [the conformance suite](docs/governance/plans/2026-09-20-m12-conformance-suite.md),
24
+ > [what is announced refuses](docs/governance/plans/2026-09-20-v0.6.0-what-is-announced-refuses.md),
23
25
  > [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
24
26
 
25
27
  ## A project's journey
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "napkinstack"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "Engineering framework for several teams and their agents working in one repository: modules, contracts, guardrails in CI."
5
5
  requires-python = ">=3.12"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "napkinstack"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "Engineering framework for several teams and their agents working in one repository: modules, contracts, guardrails in CI."
5
5
  requires-python = ">=3.12"
6
6
  license = "MIT"
@@ -23,6 +23,8 @@ from napkinstack import __version__
23
23
  SOURCE = "https://github.com/NapkinStack/engineering-os.git"
24
24
  ANSWERS = ".copier-answers.yml"
25
25
  DOWNGRADE = re.compile(r"You are downgrading from (\S+) to (\S+)\.")
26
+ # The forms of a GitHub source Copier records, from which a release page can be named.
27
+ GITHUB = re.compile(r"^(?:https://github\.com/|git@github\.com:|gh:)(?P<repo>[^/]+/[^/]+?)(?:\.git)?/?$")
26
28
 
27
29
 
28
30
  def default_ref() -> str:
@@ -30,6 +32,23 @@ def default_ref() -> str:
30
32
  return f"v{__version__}"
31
33
 
32
34
 
35
+ def release_notes(source: str, previous: str, current: str) -> str:
36
+ """Where to read what a version changes, before taking it. The published package carries
37
+ the engine and not the skeleton, so comparing two releases of it cannot show what an update
38
+ merges (D63): the notes are the only channel, and the command that brings the change names
39
+ them (D62).
40
+
41
+ It names the whole distance travelled, never the target alone (D66). One `nstack update`
42
+ can cross several versions, and the change that conflicts with the project's own files may
43
+ come from any of them: a release page holds one version and cannot answer for the others.
44
+ The CHANGELOG at the tag holds them all."""
45
+ found = GITHUB.match(source.strip())
46
+ if found:
47
+ return (f"https://github.com/{found['repo']}/blob/{current}/CHANGELOG.md"
48
+ f" — every section above {previous}")
49
+ return f"CHANGELOG.md, every section above {previous}, in {source}"
50
+
51
+
33
52
  def _git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
34
53
  return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True)
35
54
 
@@ -67,15 +86,27 @@ def _explain(exc: Exception, command: str, where: Path, source: str, ref: str) -
67
86
  if text.startswith("Cannot update: version from last update not detected"):
68
87
  return (f"FAIL [{command}] The project does not come from a published version (_commit in {ANSWERS}): "
69
88
  f"no merge base.\n{action}create the project from a vX.Y.Z tag.")
89
+ # An OSError carrying an errno is the filesystem answering, not the template being out of
90
+ # reach: a full disk, a read-only path, a directory an interrupted run left behind. Calling
91
+ # it "unreachable" sends the reader to --source, --ref and the network, none of which can
92
+ # help (D65). The OSErrors Copier raises for a template it could not read carry no errno.
93
+ if isinstance(exc, OSError) and exc.errno:
94
+ return (f"FAIL [{command}] {command} stopped on a local filesystem error: {text}\n"
95
+ f"{action}read the error above and fix what it names \u2014 space, permissions, or "
96
+ "something left behind by an interrupted run. The template and the network are "
97
+ "not in question.")
70
98
  if isinstance(exc, OSError) or text == "Local template must be a directory.":
71
99
  lines = [line.split("|", 1)[-1].strip() for line in text.splitlines() if line.strip()]
72
100
  # git writes the cause on an `error:` line and the outcome on a `fatal:` one; keeping
73
101
  # the last line alone drops the only one that says what happened (D54).
74
102
  detail = " — ".join(line for line in lines if line.startswith(("error:", "fatal:"))) or lines[-1]
75
- # A git `error:` line means git refused something local, so the action is local too:
76
- # Copier copies the template's uncommitted state into its clone before reading it.
77
- local = ("git refused a file of the template's working tree, which Copier copies as it "
78
- "is: commit or stash it, or remove the file git names above.")
103
+ # A git `error:` line means git refused something local, and git has already named it:
104
+ # a file of the template's uncommitted state, which Copier copies into its clone, or
105
+ # anything else it could not open. The action points at that line rather than guessing
106
+ # which of the two it was (D55).
107
+ local = ("git named what it could not handle above: fix that file, or, when the "
108
+ "template's working tree is uncommitted, commit or stash it — Copier copies it "
109
+ "as it is.")
79
110
  remote = "check --source and --ref (a vX.Y.Z tag), and network access."
80
111
  return (f"FAIL [{command}] Template {source} at version {ref} unreachable: {detail}\n"
81
112
  + action + (local if detail.startswith("error:") else remote))
@@ -89,9 +120,18 @@ def init(destination: Path, answers: dict[str, str | None], source: str, ref: st
89
120
  from napkinstack.doctor import CHECKLIST
90
121
 
91
122
  destination = destination.resolve()
92
- if destination.exists() and (not destination.is_dir() or any(destination.iterdir())):
93
- print(f"FAIL [init] {destination} is not empty: nstack init creates a new project.\n"
94
- " Action: pick a folder that is missing or empty.")
123
+ if destination.exists() and not destination.is_dir():
124
+ print(f"FAIL [init] {destination} is a file, not a folder: nstack init creates a new "
125
+ "project.\n Action: pick a folder that is missing or empty.")
126
+ return 1
127
+ # Naming what fills the folder: it is usually hidden — an editor's, an agent's — and a
128
+ # reader told only "not empty" goes hunting for it (D56).
129
+ entries = sorted(entry.name for entry in destination.iterdir()) if destination.exists() else []
130
+ if entries:
131
+ listed = ", ".join(entries[:4]) + (f", and {len(entries) - 4} more" if len(entries) > 4 else "")
132
+ print(f"FAIL [init] {destination} is not empty: it holds {listed}.\n"
133
+ " Action: pick a folder that is missing or empty, or create the project in a "
134
+ f"folder inside it — nstack init {destination}/<project>.")
95
135
  return 1
96
136
  data = {question: value for question, value in answers.items() if value is not None}
97
137
  try:
@@ -178,5 +218,7 @@ def update(root: Path, ref: str) -> int:
178
218
 
179
219
  print(f"Branch {branch}: NapkinStack {previous} -> {current}, merged with the project's "
180
220
  "adaptations.")
221
+ print(f"What {previous} -> {current} changes, engine and project apart: "
222
+ f"{release_notes(str(_answers(root).get('_src_path') or ''), previous, current)}")
181
223
  print(f"\nNext step: git push -u origin {branch}, then open the PR; CI validates it.")
182
224
  return 0
File without changes