napkinstack 0.19.3__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.
Files changed (39) hide show
  1. {napkinstack-0.19.3 → napkinstack-0.19.4}/PKG-INFO +4 -4
  2. {napkinstack-0.19.3 → napkinstack-0.19.4}/README.md +3 -3
  3. {napkinstack-0.19.3 → napkinstack-0.19.4}/pyproject.toml +1 -1
  4. {napkinstack-0.19.3 → napkinstack-0.19.4}/pyproject.toml.orig +1 -1
  5. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/doctor.py +4 -4
  6. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/fitness/manifests.py +2 -2
  7. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/modules.py +2 -2
  8. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/owners.py +1 -1
  9. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/pull_request.py +14 -4
  10. {napkinstack-0.19.3 → napkinstack-0.19.4}/LICENSE +0 -0
  11. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/__init__.py +0 -0
  12. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/assurance.py +0 -0
  13. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/attest.py +0 -0
  14. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/board.py +0 -0
  15. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/cli.py +0 -0
  16. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/compat.py +0 -0
  17. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/discovery.py +0 -0
  18. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/evidence.py +0 -0
  19. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/fitness/__init__.py +0 -0
  20. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/fitness/boundaries.py +0 -0
  21. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/fitness/hygiene.py +0 -0
  22. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/fitness/plan.py +0 -0
  23. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/fitness/pr_scope.py +0 -0
  24. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/hold.py +0 -0
  25. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/landed.py +0 -0
  26. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/project.py +0 -0
  27. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/provenance.py +0 -0
  28. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/settings.py +0 -0
  29. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/skills.py +0 -0
  30. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/AGENTS.md +0 -0
  31. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/MANIFEST.yaml +0 -0
  32. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/README.md +0 -0
  33. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/docs/runbook.md +0 -0
  34. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/contracts/tests/.gitkeep +0 -0
  35. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/module/AGENTS.md +0 -0
  36. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/module/MANIFEST.yaml +0 -0
  37. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/module/README.md +0 -0
  38. {napkinstack-0.19.3 → napkinstack-0.19.4}/src/napkinstack/templates/module/src/.gitkeep +0 -0
  39. {napkinstack-0.19.3 → 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
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.3, published** on [PyPI](https://pypi.org/project/napkinstack/).
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.3" nstack init my-project
89
- uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.19.3"
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
@@ -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.3, published** on [PyPI](https://pypi.org/project/napkinstack/).
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.3" nstack init my-project
73
- uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.19.3"
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "napkinstack"
3
- version = "0.19.3"
3
+ version = "0.19.4"
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.19.3"
3
+ version = "0.19.4"
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"
@@ -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 contributor's agent approves (PDR-0021), the one who pushed last never
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 contributor's agent approves (PDR-0021), a human named for what one must approve and
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
- "contributor's agent may approve.\nAction: remove the `*` rule; name a human for what "
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 contributor's agent works under an identity of its own, every "
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 contributor's agent approves, what a human must approve is named in the
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 contributor's agent.\n Action: remove the `*` rule.")
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 contributor's 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 contributor's agent approves a "
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 contributor's agent approves what the code owners file names no
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: the state is the decider's record, and this pull request knows (PDR-0002).
405
- warn(f"[K3] deliverable {match[1]} is still 'ready' while this pull request delivers it.\n"
406
- f" Action: set its state: in-progress in {path.name}, in this pull request.")
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