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.
- {napkinstack-0.3.0 → napkinstack-0.3.1}/PKG-INFO +5 -4
- {napkinstack-0.3.0 → napkinstack-0.3.1}/README.md +4 -3
- {napkinstack-0.3.0 → napkinstack-0.3.1}/pyproject.toml +1 -1
- {napkinstack-0.3.0 → napkinstack-0.3.1}/pyproject.toml.orig +1 -1
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/pull_request.py +46 -22
- {napkinstack-0.3.0 → napkinstack-0.3.1}/LICENSE +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/__init__.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/cli.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/discovery.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/doctor.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/__init__.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/boundaries.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/manifests.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/plan.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/fitness/pr_scope.sh +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/modules.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/project.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/skills.py +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/AGENTS.md +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/MANIFEST.yaml +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/README.md +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/docs/adr/.gitkeep +0 -0
- {napkinstack-0.3.0 → napkinstack-0.3.1}/src/napkinstack/templates/module/src/.gitkeep +0 -0
- {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.
|
|
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.
|
|
24
|
-
>
|
|
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.
|
|
9
|
-
>
|
|
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
|
|
@@ -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
|
|
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
|
|
63
|
-
"""
|
|
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
|
|
66
|
-
folder =
|
|
67
|
-
if any(
|
|
68
|
-
|
|
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,
|
|
77
|
-
"""Why a module requires a test sheet (T1), or None
|
|
78
|
-
|
|
79
|
-
if
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|