napkinstack 0.7.3__tar.gz → 0.7.5__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.7.3 → napkinstack-0.7.5}/PKG-INFO +5 -5
- {napkinstack-0.7.3 → napkinstack-0.7.5}/README.md +4 -4
- {napkinstack-0.7.3 → napkinstack-0.7.5}/pyproject.toml +1 -1
- {napkinstack-0.7.3 → napkinstack-0.7.5}/pyproject.toml.orig +1 -1
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/doctor.py +46 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/pull_request.py +40 -5
- {napkinstack-0.7.3 → napkinstack-0.7.5}/LICENSE +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/__init__.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/assurance.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/cli.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/compat.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/discovery.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/fitness/__init__.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/fitness/boundaries.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/fitness/hygiene.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/fitness/manifests.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/fitness/plan.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/fitness/pr_scope.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/landed.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/modules.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/project.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/provenance.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/settings.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/skills.py +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/contracts/AGENTS.md +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/contracts/MANIFEST.yaml +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/contracts/README.md +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/contracts/docs/runbook.md +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/contracts/tests/.gitkeep +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/module/AGENTS.md +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/module/MANIFEST.yaml +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/module/README.md +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/module/docs/adr/.gitkeep +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/src/napkinstack/templates/module/src/.gitkeep +0 -0
- {napkinstack-0.7.3 → napkinstack-0.7.5}/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.7.
|
|
3
|
+
Version: 0.7.5
|
|
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,7 +20,7 @@ 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.7.
|
|
23
|
+
> **Status: v0.7.5, published** on [PyPI](https://pypi.org/project/napkinstack/).
|
|
24
24
|
> What each version changes, engine and project apart: [`CHANGELOG.md`](CHANGELOG.md).
|
|
25
25
|
> What is open: [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
|
|
26
26
|
|
|
@@ -84,8 +84,8 @@ To try it without installing anything, or to install it from this repository ins
|
|
|
84
84
|
registry — always pinned to a release tag:
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.7.
|
|
88
|
-
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.7.
|
|
87
|
+
uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.7.5" nstack init my-project
|
|
88
|
+
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.7.5"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
**Two channels, one published artefact.** The registry publishes: the PyPI artefact of a
|
|
@@ -105,7 +105,7 @@ Each project then pins its version and changes it through `nstack update`.
|
|
|
105
105
|
| Command | Role |
|
|
106
106
|
|---|---|
|
|
107
107
|
| `nstack init <folder>` | Creates the project: skeleton, git repository, initial commit, GitHub checklist |
|
|
108
|
-
| `nstack doctor` | Checks the workstation and the GitHub settings, read-only |
|
|
108
|
+
| `nstack doctor` | Checks the workstation and the GitHub settings, read-only; names a newer published version |
|
|
109
109
|
| `nstack settings` | Prints the calls that would bring the GitHub settings to the checklist; changes nothing |
|
|
110
110
|
| `nstack new-module <name> <owner> <criticality>` | Creates a module, with no imposed stack |
|
|
111
111
|
| `nstack new-contract <name>` | Creates a contract and, the first time, the module that holds them all |
|
|
@@ -5,7 +5,7 @@ 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.7.
|
|
8
|
+
> **Status: v0.7.5, published** on [PyPI](https://pypi.org/project/napkinstack/).
|
|
9
9
|
> What each version changes, engine and project apart: [`CHANGELOG.md`](CHANGELOG.md).
|
|
10
10
|
> What is open: [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
|
|
11
11
|
|
|
@@ -69,8 +69,8 @@ To try it without installing anything, or to install it from this repository ins
|
|
|
69
69
|
registry — always pinned to a release tag:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.7.
|
|
73
|
-
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.7.
|
|
72
|
+
uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.7.5" nstack init my-project
|
|
73
|
+
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.7.5"
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
**Two channels, one published artefact.** The registry publishes: the PyPI artefact of a
|
|
@@ -90,7 +90,7 @@ Each project then pins its version and changes it through `nstack update`.
|
|
|
90
90
|
| Command | Role |
|
|
91
91
|
|---|---|
|
|
92
92
|
| `nstack init <folder>` | Creates the project: skeleton, git repository, initial commit, GitHub checklist |
|
|
93
|
-
| `nstack doctor` | Checks the workstation and the GitHub settings, read-only |
|
|
93
|
+
| `nstack doctor` | Checks the workstation and the GitHub settings, read-only; names a newer published version |
|
|
94
94
|
| `nstack settings` | Prints the calls that would bring the GitHub settings to the checklist; changes nothing |
|
|
95
95
|
| `nstack new-module <name> <owner> <criticality>` | Creates a module, with no imposed stack |
|
|
96
96
|
| `nstack new-contract <name>` | Creates a contract and, the first time, the module that holds them all |
|
|
@@ -5,6 +5,9 @@ Workflows inform; it is the GitHub settings that block, and they are not copied
|
|
|
5
5
|
project. CHECKLIST is printed by nstack init, checked here, and repeated word for word in
|
|
6
6
|
the foundation manual, skeleton/.nstack/handbook/INSTALL.md.jinja (a test verifies it).
|
|
7
7
|
|
|
8
|
+
The registry is asked one question, without a token: which version is the latest published
|
|
9
|
+
(NSTACK_REGISTRY_URL, otherwise https://pypi.org). An answer changes no exit code.
|
|
10
|
+
|
|
8
11
|
GitHub is read with a token supplied by a human (GH_TOKEN, otherwise GITHUB_TOKEN):
|
|
9
12
|
fine-grained, limited to the repository, Administration: read permission. Without a token,
|
|
10
13
|
or when the API refuses a read, the setting is "not verified", never compliant. No write.
|
|
@@ -20,6 +23,8 @@ Rules:
|
|
|
20
23
|
first minute. The plan check asks for it at the discovery's go (C8). Not reused.
|
|
21
24
|
L6 CODEOWNERS starts with a default owner: the code owner review covers every path
|
|
22
25
|
L7 the template source reachable by anyone: a repository, not a path on one machine
|
|
26
|
+
N1 information, never a gap (D91): a published version newer than the project's, read from
|
|
27
|
+
the registry — the one place a project learns of it. No other command opens a connection
|
|
23
28
|
G1-G18 the GitHub settings of CHECKLIST; G6 is not applicable outside a public
|
|
24
29
|
repository, and on a private one G1-G5, G12, G13, G17 and G18 name the GitHub plan
|
|
25
30
|
or option required
|
|
@@ -318,6 +323,45 @@ def _workstation(root: Path, answers: dict) -> list[tuple[str, str, str, str]]:
|
|
|
318
323
|
return results
|
|
319
324
|
|
|
320
325
|
|
|
326
|
+
def _latest() -> str | None:
|
|
327
|
+
"""The latest version the registry publishes, or None when it did not answer."""
|
|
328
|
+
registry = (os.environ.get("NSTACK_REGISTRY_URL") or "https://pypi.org").rstrip("/")
|
|
329
|
+
request = urllib.request.Request(f"{registry}/pypi/napkinstack/json",
|
|
330
|
+
headers={"Accept": "application/json", "User-Agent": f"nstack/{__version__}"})
|
|
331
|
+
try:
|
|
332
|
+
with urllib.request.urlopen(request, timeout=5) as response:
|
|
333
|
+
version = str(json.load(response)["info"]["version"])
|
|
334
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
335
|
+
return None
|
|
336
|
+
return version if PUBLISHED.fullmatch(f"v{version}") else None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _number(version: str) -> tuple[int, ...]:
|
|
340
|
+
return tuple(int(part) for part in version.split("."))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _newer(answers: dict) -> None:
|
|
344
|
+
"""Says whether a version newer than the project's is published (N1). Information: taking
|
|
345
|
+
a version is the team's decision, at a cycle's boundary, and nothing here counts it as a gap."""
|
|
346
|
+
commit = str(answers.get("_commit") or "")
|
|
347
|
+
if not PUBLISHED.fullmatch(commit):
|
|
348
|
+
return # L1 already says the project is on no published version
|
|
349
|
+
print("\nNapkinStack")
|
|
350
|
+
latest = _latest()
|
|
351
|
+
if latest is None:
|
|
352
|
+
print(" NOT READ the registry did not answer: whether a newer version is published is not known")
|
|
353
|
+
return
|
|
354
|
+
if _number(latest) <= _number(commit.removeprefix("v")):
|
|
355
|
+
print(f" LATEST the project is on {commit}, the latest published version")
|
|
356
|
+
return
|
|
357
|
+
from napkinstack.project import release_notes
|
|
358
|
+
|
|
359
|
+
print(f" NEWER VERSION v{latest} is published; the project is on {commit} — information, not a gap\n"
|
|
360
|
+
f" What it changes: {release_notes(str(answers.get('_src_path') or ''), commit, f'v{latest}')}\n"
|
|
361
|
+
f" To take it, at a cycle's boundary: uv tool install \"napkinstack=={latest}\" "
|
|
362
|
+
"--with-executables-from pre-commit, then nstack update")
|
|
363
|
+
|
|
364
|
+
|
|
321
365
|
def _private(client: GitHub) -> bool:
|
|
322
366
|
"""Repository not public (private or internal); visibility unreadable: treated as public."""
|
|
323
367
|
try:
|
|
@@ -374,6 +418,8 @@ def run(root: Path) -> int:
|
|
|
374
418
|
for result in results:
|
|
375
419
|
_display(*result)
|
|
376
420
|
|
|
421
|
+
_newer(answers)
|
|
422
|
+
|
|
377
423
|
if plan.stage(root) == plan.UNFRAMED:
|
|
378
424
|
# Said here and nowhere else, once: every other run names the stage in one line and
|
|
379
425
|
# leaves it there. A stage is not a gap, and nothing below counts it as one.
|
|
@@ -185,6 +185,33 @@ def authors(root: Path, base: str, head: str, opener: str) -> tuple[set[str], se
|
|
|
185
185
|
return logins, sessions, unnamed
|
|
186
186
|
|
|
187
187
|
|
|
188
|
+
# A later commit does not name the earlier ones: the trailer is read on every commit an agent
|
|
189
|
+
# made (D93). Said as two ways out, because rewriting a reviewed branch is not always allowed.
|
|
190
|
+
UNNAMED_ACTION = (f"every commit an agent made carries \"{SESSION_TRAILER}: <id>\" (AGENTS.md §0) — "
|
|
191
|
+
"a commit added now does not name the earlier ones: recommit them, on a new branch "
|
|
192
|
+
"where the history is not rewritten; or have a person run the sheet.")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def summary(body: str, authorship: tuple[set[str], set[str], list[str]]) -> list[str]:
|
|
196
|
+
"""What the check read of a sheet, in a line, and what stands in a session's way before one
|
|
197
|
+
has done the work (D93): a verdict of one word told a verifier nothing of either."""
|
|
198
|
+
verifier, _, rows = read_sheet(body)
|
|
199
|
+
if not rows:
|
|
200
|
+
return []
|
|
201
|
+
counts: dict[str, int] = {}
|
|
202
|
+
for row in rows:
|
|
203
|
+
result = _result(row.get("result", ""))
|
|
204
|
+
result = result if result in RESULTS else "unreadable"
|
|
205
|
+
counts[result] = counts.get(result, 0) + 1
|
|
206
|
+
said = ", ".join(f"{number} {result}" for result, number in counts.items())
|
|
207
|
+
lines = [f" Read: {len(rows)} scenario(s) — {said}; verifier: {verifier or 'not named'}, "
|
|
208
|
+
f"compared with {len(authorship[0])} account(s) and {len(authorship[1])} session(s) of the change"]
|
|
209
|
+
if authorship[2] and not SESSION.match(verifier):
|
|
210
|
+
lines.append(f" NOTE: an agent's commits name no session ({', '.join(authorship[2])}): as it stands, "
|
|
211
|
+
f"only a person can verify this change.\n To let a session verify it: {UNNAMED_ACTION}")
|
|
212
|
+
return lines
|
|
213
|
+
|
|
214
|
+
|
|
188
215
|
def check_verifier(verifier: str, authorship: tuple[set[str], set[str], list[str]], fail: Fail) -> None:
|
|
189
216
|
"""T2: the verifier is named — people as @handle anywhere on the line, an agent session as
|
|
190
217
|
`session <id>` opening it — and none of the names is an author of the change."""
|
|
@@ -206,8 +233,7 @@ def check_verifier(verifier: str, authorship: tuple[set[str], set[str], list[str
|
|
|
206
233
|
"with a fresh context (.nstack/playbooks/verification.md).")
|
|
207
234
|
elif session and unnamed:
|
|
208
235
|
fail("T2", f"Test sheet: an agent's commits name no session ({', '.join(unnamed)}): the verifier "
|
|
209
|
-
f"cannot be told from their author.\n Action:
|
|
210
|
-
f"the trailer \"{SESSION_TRAILER}: <id>\" (AGENTS.md §0), then the sheet is run again.")
|
|
236
|
+
f"cannot be told from their author.\n Action: {UNNAMED_ACTION}")
|
|
211
237
|
|
|
212
238
|
|
|
213
239
|
def _result(cell: str) -> str:
|
|
@@ -216,7 +242,7 @@ def _result(cell: str) -> str:
|
|
|
216
242
|
|
|
217
243
|
def check_sheet(body: str, head: str, reasons: list[str], fail: Fail,
|
|
218
244
|
authorship: tuple[set[str], set[str], list[str]] = (set(), set(), []),
|
|
219
|
-
strict: bool = False) -> list[str]:
|
|
245
|
+
strict: bool = False, unchanged: Callable[[str], bool] = lambda commit: False) -> list[str]:
|
|
220
246
|
"""T1 to T5; returns the human-only scenarios, listed apart for the approver."""
|
|
221
247
|
verifier, header, rows = read_sheet(body)
|
|
222
248
|
if reasons and not rows:
|
|
@@ -257,7 +283,8 @@ def check_sheet(body: str, head: str, reasons: list[str], fail: Fail,
|
|
|
257
283
|
if result == "passed" and (row["evidence"] in EMPTY or not SHA.fullmatch(commit)):
|
|
258
284
|
fail("T3", f"scenario {ident}: passed without evidence and the commit verified.\n"
|
|
259
285
|
" Action: link the screenshot, video, trace or log, and give the commit.")
|
|
260
|
-
elif result in {"passed", "failed"} and SHA.fullmatch(commit) and not head.startswith(commit)
|
|
286
|
+
elif (result in {"passed", "failed"} and SHA.fullmatch(commit) and not head.startswith(commit)
|
|
287
|
+
and not unchanged(commit)):
|
|
261
288
|
# T4 split by kind (D71). An automated scenario is cheap and CI replays it when e2e is
|
|
262
289
|
# declared, so it is re-run. One that was explored by hand costs a round: its evidence keeps the commit
|
|
263
290
|
# it was produced on, and the verifier confirms at the head, naming what moved since.
|
|
@@ -362,12 +389,20 @@ def run(root: Path, base: str, body_file: Path | None = None) -> int:
|
|
|
362
389
|
|
|
363
390
|
reasons = [reason for folder, found in modules.items() if (reason := sheet_reason(folder, found))]
|
|
364
391
|
authorship = authors(root, base, head, os.environ.get("PR_AUTHOR", ""))
|
|
365
|
-
|
|
392
|
+
# A commit whose tree is the head's is the head, for a sheet (D93): merging the default
|
|
393
|
+
# branch into a pull request that already carried it changes a hash and no file.
|
|
394
|
+
def tree_of(commit: str) -> str:
|
|
395
|
+
return _git(root, "rev-parse", "--verify", "--quiet", f"{commit}^{{tree}}").stdout.strip()
|
|
396
|
+
|
|
397
|
+
human_only = check_sheet(body, head, reasons, fail, authorship, rerun_at_head(modules),
|
|
398
|
+
lambda commit: bool(tree_of(commit)) and tree_of(commit) == tree_of(head))
|
|
366
399
|
if modules:
|
|
367
400
|
check_cycle(root, body, labels, datetime.date.today(), fail)
|
|
368
401
|
|
|
369
402
|
print(f"Modules touched : {len(modules)}" + "".join(f"\n - {folder}" for folder in modules))
|
|
370
403
|
print(f"Test sheet : {'required' if reasons else 'not required'}")
|
|
404
|
+
for line in summary(body, authorship) if reasons else []:
|
|
405
|
+
print(line)
|
|
371
406
|
for scenario in human_only:
|
|
372
407
|
print(f" For the approver, human only: {scenario}")
|
|
373
408
|
for failure in failures:
|
|
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
|
|
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
|