napkinstack 0.3.0__tar.gz → 0.3.1__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 (24) hide show
  1. {napkinstack-0.3.0 → napkinstack-0.3.1}/PKG-INFO +5 -4
  2. {napkinstack-0.3.0 → napkinstack-0.3.1}/README.md +4 -3
  3. {napkinstack-0.3.0 → napkinstack-0.3.1}/pyproject.toml +1 -1
  4. {napkinstack-0.3.0 → napkinstack-0.3.1}/pyproject.toml.orig +1 -1
  5. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/pull_request.py +46 -22
  6. {napkinstack-0.3.0 → napkinstack-0.3.1}/LICENSE +0 -0
  7. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/__init__.py +0 -0
  8. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/cli.py +0 -0
  9. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/discovery.py +0 -0
  10. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/doctor.py +0 -0
  11. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/__init__.py +0 -0
  12. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/boundaries.py +0 -0
  13. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/manifests.py +0 -0
  14. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/plan.py +0 -0
  15. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/pr_scope.sh +0 -0
  16. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/modules.py +0 -0
  17. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/project.py +0 -0
  18. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/skills.py +0 -0
  19. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/AGENTS.md +0 -0
  20. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/MANIFEST.yaml +0 -0
  21. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/README.md +0 -0
  22. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/docs/adr/.gitkeep +0 -0
  23. {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/src/.gitkeep +0 -0
  24. {napkinstack-0.3.0 → napkinstack-0.3.1}/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.3.0
3
+ Version: 0.3.1
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,10 +20,11 @@ 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.2.0, published** ([PyPI](https://pypi.org/project/napkinstack/)), English
24
- > throughout. A first pilot project, private, starts from it and puts it to the test before
25
- > the rest. Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
23
+ > **Status: v0.3.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): test sheet,
24
+ > framing and bounded cycles. A first pilot project, private, starts from it and puts it to
25
+ > the test before the rest. Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
26
26
  > [move to English](docs/governance/plans/2026-09-16-english-migration.md),
27
+ > [frame, verify, approve](docs/governance/plans/2026-09-16-v0.3.0-frame-verify-approve.md),
27
28
  > [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
28
29
 
29
30
  ## A project's journey
@@ -5,10 +5,11 @@ 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.2.0, published** ([PyPI](https://pypi.org/project/napkinstack/)), English
9
- > throughout. A first pilot project, private, starts from it and puts it to the test before
10
- > the rest. Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
8
+ > **Status: v0.3.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): test sheet,
9
+ > framing and bounded cycles. A first pilot project, private, starts from it and puts it to
10
+ > the test before the rest. Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
11
11
  > [move to English](docs/governance/plans/2026-09-16-english-migration.md),
12
+ > [frame, verify, approve](docs/governance/plans/2026-09-16-v0.3.0-frame-verify-approve.md),
12
13
  > [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
13
14
 
14
15
  ## A project's journey
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "napkinstack"
3
- version = "0.3.0"
3
+ version = "0.3.1"
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.3.0"
3
+ version = "0.3.1"
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"
@@ -3,8 +3,8 @@ Pull request rules read from its description: the test sheet (PDR-0003) and the
3
3
  (PDR-0002).
4
4
 
5
5
  Rules:
6
- T1 a test sheet when the pull request touches a user-facing module, or one of
7
- criticality high or critical
6
+ T1 a test sheet when the pull request changes a user-facing module, or one of
7
+ criticality high or critical — at the base or at the head, the stricter
8
8
  T2 a verifier named, and every scenario row filled in: id, given · when · then, a kind
9
9
  (automated, explored, human only — reason), a result (passed, failed, not verified)
10
10
  T3 no scenario passed without its evidence and the commit it was verified on
@@ -15,7 +15,9 @@ Rules:
15
15
  K3 delivery work names a ready or in-progress deliverable of the cycle
16
16
  K4 the out-of-cycle label carries its justification
17
17
 
18
- Delivery work: a pull request that changes a module — a folder holding a MANIFEST.yaml.
18
+ Delivery work: a pull request that changes a module — a folder holding a MANIFEST.yaml —
19
+ beyond its description: its manifest, AGENTS.md, README.md and docs/ (D24). A NapkinStack
20
+ update or a documentation fix is neither delivery work nor a reason for a sheet.
19
21
  The out-of-cycle label lifts K1 to K3, visibly and countably (docs/os/10-measurement.md).
20
22
 
21
23
  Usage : nstack pr-check [--root ROOT] [--base BASE] [--body-file FILE]
@@ -35,9 +37,10 @@ from pathlib import Path
35
37
  import yaml
36
38
 
37
39
  from napkinstack.fitness import plan
38
- from napkinstack.fitness.manifests import find_manifests
40
+ from napkinstack.fitness.manifests import MODULE_DIRS, find_manifests
39
41
 
40
42
  LABEL = "out-of-cycle"
43
+ DESCRIPTION = {"MANIFEST.yaml", "AGENTS.md", "README.md"}
41
44
  DELIVERABLE = re.compile(r"^Deliverable:[ \t]*(D[1-9][0-9]*)\b", re.I | re.M)
42
45
  JUSTIFICATION = re.compile(r"^Out of cycle:[ \t]*(\S.*)$", re.I | re.M)
43
46
  SHEET_CRITICALITIES = {"high", "critical"}
@@ -59,27 +62,47 @@ def _git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
59
62
  return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True)
60
63
 
61
64
 
62
- def touched_modules(root: Path, files: list[str]) -> dict[str, dict]:
63
- """Folder, relative to the root, to manifest, for every module the files change."""
65
+ def changes_behaviour(path: str) -> bool:
66
+ """False for a module's description — its manifest, AGENTS.md, README.md, docs/ — and an
67
+ empty placeholder: changing those is neither delivery work nor a reason for a sheet (D24)."""
68
+ return not (path in DESCRIPTION or path.startswith("docs/") or path.rsplit("/", 1)[-1] == ".gitkeep")
69
+
70
+
71
+ def _manifest(text: str) -> dict:
72
+ try:
73
+ data = yaml.safe_load(text) or {}
74
+ except yaml.YAMLError:
75
+ data = {}
76
+ return data if isinstance(data, dict) else {}
77
+
78
+
79
+ def touched_modules(root: Path, base: str, files: list[str]) -> dict[str, list[dict]]:
80
+ """Folder, relative to the root, to its manifests — at the head and at the base — for
81
+ every module whose behaviour the files may change, a deleted module included."""
82
+ manifests = {path.relative_to(root).as_posix(): [_manifest(path.read_text(encoding="utf-8"))]
83
+ for path in find_manifests(root)}
84
+ listed = _git(root, "ls-tree", "-r", "--name-only", base).stdout.split()
85
+ for path in listed:
86
+ parts = path.split("/")
87
+ if parts[-1] == "MANIFEST.yaml" and parts[0] in MODULE_DIRS and len(parts) in (2, 3):
88
+ manifests.setdefault(path, []).append(_manifest(_git(root, "show", f"{base}:{path}").stdout))
64
89
  touched = {}
65
- for manifest in find_manifests(root):
66
- folder = manifest.parent.relative_to(root).as_posix()
67
- if any(path.startswith(f"{folder}/") for path in files):
68
- try:
69
- data = yaml.safe_load(manifest.read_text(encoding="utf-8")) or {}
70
- except yaml.YAMLError:
71
- data = {}
72
- touched[folder] = data if isinstance(data, dict) else {}
90
+ for path, found in sorted(manifests.items()):
91
+ folder = path.removesuffix("/MANIFEST.yaml")
92
+ if any(file.startswith(f"{folder}/") and changes_behaviour(file[len(folder) + 1:]) for file in files):
93
+ touched[folder] = found
73
94
  return touched
74
95
 
75
96
 
76
- def sheet_reason(folder: str, manifest: dict) -> str | None:
77
- """Why a module requires a test sheet (T1), or None."""
78
- module = manifest.get("module") if isinstance(manifest.get("module"), dict) else {}
79
- if module.get("user_facing") is True:
97
+ def sheet_reason(folder: str, manifests: list[dict]) -> str | None:
98
+ """Why a module requires a test sheet (T1), or None: the stricter of its manifests, so
99
+ that a pull request cannot lower its own requirement."""
100
+ modules = [data["module"] for data in manifests if isinstance(data.get("module"), dict)]
101
+ if any(module.get("user_facing") is True for module in modules):
80
102
  return f"{folder} (user-facing)"
81
- if module.get("criticality") in SHEET_CRITICALITIES:
82
- return f"{folder} (criticality {module['criticality']})"
103
+ for module in modules:
104
+ if module.get("criticality") in SHEET_CRITICALITIES:
105
+ return f"{folder} (criticality {module['criticality']})"
83
106
  return None
84
107
 
85
108
 
@@ -217,7 +240,8 @@ def run(root: Path, base: str, body_file: Path | None = None) -> int:
217
240
  return 0
218
241
  files = _git(root, "diff", "--name-only", f"{base}...HEAD").stdout.split()
219
242
  head = (os.environ.get("PR_HEAD_SHA") or _git(root, "rev-parse", "HEAD").stdout).strip().lower()
220
- modules = touched_modules(root, files)
243
+ fork = _git(root, "merge-base", base, "HEAD").stdout.strip() or base
244
+ modules = touched_modules(root, fork, files)
221
245
  labels = {label.strip() for label in os.environ.get("PR_LABELS", "").split(",") if label.strip()}
222
246
 
223
247
  failures: list[str] = []
@@ -225,7 +249,7 @@ def run(root: Path, base: str, body_file: Path | None = None) -> int:
225
249
  def fail(rule: str, message: str) -> None:
226
250
  failures.append(f"[{rule}] {message}")
227
251
 
228
- reasons = [reason for folder, data in modules.items() if (reason := sheet_reason(folder, data))]
252
+ reasons = [reason for folder, found in modules.items() if (reason := sheet_reason(folder, found))]
229
253
  human_only = check_sheet(body, head, reasons, fail)
230
254
  if modules:
231
255
  check_cycle(root, body, labels, datetime.date.today(), fail)
File without changes