napkinstack 0.19.2__tar.gz → 0.19.4__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.19.2 → napkinstack-0.19.4}/PKG-INFO +6 -6
- {napkinstack-0.19.2 → napkinstack-0.19.4}/README.md +5 -5
- {napkinstack-0.19.2 → napkinstack-0.19.4}/pyproject.toml +1 -1
- {napkinstack-0.19.2 → napkinstack-0.19.4}/pyproject.toml.orig +1 -1
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/doctor.py +4 -4
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/fitness/manifests.py +2 -2
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/modules.py +2 -2
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/owners.py +1 -1
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/pull_request.py +14 -4
- {napkinstack-0.19.2 → napkinstack-0.19.4}/LICENSE +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/__init__.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/assurance.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/attest.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/board.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/cli.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/compat.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/discovery.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/evidence.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/fitness/__init__.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/fitness/boundaries.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/fitness/hygiene.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/fitness/plan.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/fitness/pr_scope.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/hold.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/landed.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/project.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/provenance.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/settings.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/skills.py +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/AGENTS.md +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/MANIFEST.yaml +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/README.md +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/docs/runbook.md +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/tests/.gitkeep +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/module/AGENTS.md +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/module/MANIFEST.yaml +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/module/README.md +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/module/src/.gitkeep +0 -0
- {napkinstack-0.19.2 → napkinstack-0.19.4}/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.19.
|
|
3
|
+
Version: 0.19.4
|
|
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
|
|
@@ -21,7 +21,7 @@ modules, contracts, guardrails in CI. On the Django or Rails model, one command
|
|
|
21
21
|
the project, which then receives new versions on demand; no application stack is imposed.
|
|
22
22
|
Positioning and vocabulary: [`PRODUCT.md`](PRODUCT.md) §1.
|
|
23
23
|
|
|
24
|
-
> **Status: v0.19.
|
|
24
|
+
> **Status: v0.19.4, published** on [PyPI](https://pypi.org/project/napkinstack/).
|
|
25
25
|
> What each version changes, engine and project apart: [`CHANGELOG.md`](CHANGELOG.md).
|
|
26
26
|
> What is open: [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
|
|
27
27
|
|
|
@@ -85,8 +85,8 @@ To try it without installing anything, or to install it from this repository ins
|
|
|
85
85
|
registry — always pinned to a release tag:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.19.
|
|
89
|
-
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.19.
|
|
88
|
+
uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.19.4" nstack init my-project
|
|
89
|
+
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.19.4"
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
**Two channels, one published artefact.** The registry publishes: the PyPI artefact of a
|
|
@@ -127,8 +127,8 @@ Each project then pins its version and changes it through `nstack update`.
|
|
|
127
127
|
|
|
128
128
|
**Prerequisites**: uv and git. The guardrails really block on a public GitHub repository,
|
|
129
129
|
or on a private one under the Team or Pro plan; on a private repository on the Free plan
|
|
130
|
-
CI informs without blocking
|
|
131
|
-
([
|
|
130
|
+
CI informs without blocking, and `doctor` says the repository *unguarded*
|
|
131
|
+
([PDR-0006](docs/pdr/0006-work-where-the-forge-cannot-guard.md)).
|
|
132
132
|
Off GitHub (GitLab, Forgejo, Gitea, another forge or none), the checks are attested on the
|
|
133
133
|
workstation and every command says what it cannot verify ([PDR-0018](docs/pdr/0018-git-is-required-the-forge-is-the-project-s-choice.md)).
|
|
134
134
|
|
|
@@ -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.19.
|
|
8
|
+
> **Status: v0.19.4, 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.19.
|
|
73
|
-
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.19.
|
|
72
|
+
uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.19.4" nstack init my-project
|
|
73
|
+
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.19.4"
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
**Two channels, one published artefact.** The registry publishes: the PyPI artefact of a
|
|
@@ -111,8 +111,8 @@ Each project then pins its version and changes it through `nstack update`.
|
|
|
111
111
|
|
|
112
112
|
**Prerequisites**: uv and git. The guardrails really block on a public GitHub repository,
|
|
113
113
|
or on a private one under the Team or Pro plan; on a private repository on the Free plan
|
|
114
|
-
CI informs without blocking
|
|
115
|
-
([
|
|
114
|
+
CI informs without blocking, and `doctor` says the repository *unguarded*
|
|
115
|
+
([PDR-0006](docs/pdr/0006-work-where-the-forge-cannot-guard.md)).
|
|
116
116
|
Off GitHub (GitLab, Forgejo, Gitea, another forge or none), the checks are attested on the
|
|
117
117
|
workstation and every command says what it cannot verify ([PDR-0018](docs/pdr/0018-git-is-required-the-forge-is-the-project-s-choice.md)).
|
|
118
118
|
|
|
@@ -154,7 +154,7 @@ CHECKLIST = [ # (rule, setting, action)
|
|
|
154
154
|
("G20", "Hold branches `nstack/hold/**/*` cannot be force-pushed nor deleted",
|
|
155
155
|
"Settings → Rules → Rulesets, a ruleset on the branches nstack/hold/**/* — not nstack/hold/**, "
|
|
156
156
|
"which stops at a slash — with nobody on its bypass list: block force pushes and restrict deletions"),
|
|
157
|
-
# G21: where another
|
|
157
|
+
# G21: where another agent approves (PDR-0021), the one who pushed last never
|
|
158
158
|
# approves what they pushed — the author's own fix included.
|
|
159
159
|
("G21", "The most recent push approved by someone other than its pusher (approval: agent)",
|
|
160
160
|
f"{RULESET}: require approval of the most recent reviewable push"),
|
|
@@ -361,7 +361,7 @@ CHECKS: dict[str, Callable[[GitHub], bool]] = {
|
|
|
361
361
|
|
|
362
362
|
def _default_owner(root: Path) -> tuple[str, str]:
|
|
363
363
|
"""L6: `*` first, so that the code owner review covers every path (ADR-0004) — or, where
|
|
364
|
-
another
|
|
364
|
+
another agent approves (PDR-0021), a human named for what one must approve and
|
|
365
365
|
no default owner, so that the rest is the agents'."""
|
|
366
366
|
from napkinstack.project import answer, forge, ownership
|
|
367
367
|
|
|
@@ -395,7 +395,7 @@ def _kept_for_a_human(root: Path, shown: str, text: str) -> tuple[str, str]:
|
|
|
395
395
|
default, missing = owners.unowned(root, text)
|
|
396
396
|
if default:
|
|
397
397
|
return GAP, (f"{shown} names a default owner: on approval agent, it leaves no change another "
|
|
398
|
-
"
|
|
398
|
+
"agent may approve.\nAction: remove the `*` rule; name a human for what "
|
|
399
399
|
"one must approve (.nstack/handbook/07-governance.md §7).")
|
|
400
400
|
if missing:
|
|
401
401
|
return GAP, (f"{shown} names no owner for " + ", ".join(f"`{path}`" for path in missing)
|
|
@@ -409,7 +409,7 @@ def _kept_for_a_human(root: Path, shown: str, text: str) -> tuple[str, str]:
|
|
|
409
409
|
if len(agents) < 2:
|
|
410
410
|
return OK, (f"The history shows {len(agents) or 'no'} agent identit{'y' if len(agents) == 1 else 'ies'}"
|
|
411
411
|
+ (f" ({agents[0]})" if agents else "") + ": an agent never approves what its own identity "
|
|
412
|
-
"pushed, so until another
|
|
412
|
+
"pushed, so until another agent works under an identity of its own, every "
|
|
413
413
|
"change waits for a human, as on approval human.")
|
|
414
414
|
return OK, ""
|
|
415
415
|
|
|
@@ -471,7 +471,7 @@ def assembled(manifests: list[Path], root: Path) -> None:
|
|
|
471
471
|
|
|
472
472
|
|
|
473
473
|
def kept_for_a_human(root: Path) -> None:
|
|
474
|
-
"""M13: where another
|
|
474
|
+
"""M13: where another agent approves, what a human must approve is named in the
|
|
475
475
|
code owners file. The forge reads that file from the base branch, so a pull request raising a
|
|
476
476
|
module's criticality without its line would be approved by an agent: refused here, in CI."""
|
|
477
477
|
from napkinstack import owners
|
|
@@ -489,7 +489,7 @@ def kept_for_a_human(root: Path) -> None:
|
|
|
489
489
|
default, missing = owners.unowned(root, path.read_text(encoding="utf-8"))
|
|
490
490
|
if default:
|
|
491
491
|
fail("code owners", "M13", f"{shown} names a default owner: on approval agent, no change is left "
|
|
492
|
-
"for another
|
|
492
|
+
"for another agent.\n Action: remove the `*` rule.")
|
|
493
493
|
for kept in missing:
|
|
494
494
|
fail("code owners", "M13", f"{shown} ends `{kept}` on no owner: on approval agent, a human "
|
|
495
495
|
f"approves it.\n {action}")
|
|
@@ -103,8 +103,8 @@ def create(root: Path, name: str, owner: str, criticality: str, user_facing: boo
|
|
|
103
103
|
codeowners = ownership(root)
|
|
104
104
|
line = f"/modules/{name}/"
|
|
105
105
|
if answer(root, "approval") == "agent" and criticality in ("prototype", "standard"):
|
|
106
|
-
# Another
|
|
107
|
-
print(f"No code owner line for {line}: on approval agent, another
|
|
106
|
+
# Another agent approves it (PDR-0021): no code owner, whose review a human's alone satisfies.
|
|
107
|
+
print(f"No code owner line for {line}: on approval agent, another agent approves a "
|
|
108
108
|
f"{criticality} module.")
|
|
109
109
|
elif codeowners.is_file():
|
|
110
110
|
content = codeowners.read_text(encoding="utf-8")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Who must approve a change, read from the code owners file as the forge reads it (PDR-0021).
|
|
3
3
|
|
|
4
|
-
On `approval: agent`, another
|
|
4
|
+
On `approval: agent`, another agent approves what the code owners file names no
|
|
5
5
|
owner for; a human, what it names. So every path a human must approve — the project's rules,
|
|
6
6
|
its records, its answers, the code owners file itself, every module neither `prototype` nor
|
|
7
7
|
`standard` — must end on a rule with an owner. A pattern is read as a gitignore pattern, the
|
|
@@ -400,10 +400,20 @@ def check_cycle(root: Path, body: str, labels: set[str], today: datetime.date, f
|
|
|
400
400
|
elif not plan.one_of(states[match[1]], {"ready", "in-progress"}):
|
|
401
401
|
fail("K3", f"deliverable {match[1]} is '{states[match[1]]}': work starts on a ready "
|
|
402
402
|
"deliverable (definition of ready).")
|
|
403
|
-
elif states[match[1]] == "ready":
|
|
404
|
-
# Said, not refused
|
|
405
|
-
|
|
406
|
-
|
|
403
|
+
elif states[match[1]] == "ready" and not _held(root, match[1]):
|
|
404
|
+
# Said, not refused. The hold says the work started; the cycle file stays the decider's
|
|
405
|
+
# record, never changed by a delivery — under `docs/`, a human would have to approve it.
|
|
406
|
+
warn(f"[K3] deliverable {match[1]} is still 'ready' and nobody holds it while this pull request "
|
|
407
|
+
f"delivers it.\n Action: nstack claim {match[1]} — the hold says it is in progress; "
|
|
408
|
+
f"{path.name} changes in the plan's own pull requests, never in a delivery's.")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _held(root: Path, ident: str) -> bool:
|
|
412
|
+
"""Whether someone holds the deliverable; holds that cannot be read count as none (K5 says so)."""
|
|
413
|
+
from napkinstack import hold
|
|
414
|
+
|
|
415
|
+
found = hold.judge(root, ident)
|
|
416
|
+
return found is not None and not isinstance(found, str)
|
|
407
417
|
|
|
408
418
|
|
|
409
419
|
def identities(root: Path, base: str, head: str) -> set[str]:
|
|
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
|
{napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/docs/runbook.md
RENAMED
|
File without changes
|
{napkinstack-0.19.2 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/tests/.gitkeep
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|