napkinstack 0.6.2__tar.gz → 0.7.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 (36) hide show
  1. {napkinstack-0.6.2 → napkinstack-0.7.1}/PKG-INFO +19 -24
  2. {napkinstack-0.6.2 → napkinstack-0.7.1}/README.md +18 -23
  3. {napkinstack-0.6.2 → napkinstack-0.7.1}/pyproject.toml +1 -1
  4. {napkinstack-0.6.2 → napkinstack-0.7.1}/pyproject.toml.orig +1 -1
  5. napkinstack-0.7.1/src/napkinstack/assurance.py +74 -0
  6. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/cli.py +24 -9
  7. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/compat.py +7 -7
  8. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/discovery.py +4 -4
  9. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/doctor.py +77 -25
  10. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/boundaries.py +7 -7
  11. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/manifests.py +34 -14
  12. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/plan.py +104 -9
  13. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/pr_scope.py +5 -5
  14. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/modules.py +123 -13
  15. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/project.py +204 -7
  16. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/provenance.py +1 -1
  17. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/pull_request.py +73 -27
  18. napkinstack-0.7.1/src/napkinstack/settings.py +247 -0
  19. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/skills.py +6 -6
  20. napkinstack-0.7.1/src/napkinstack/templates/contracts/AGENTS.md +26 -0
  21. napkinstack-0.7.1/src/napkinstack/templates/contracts/MANIFEST.yaml +29 -0
  22. napkinstack-0.7.1/src/napkinstack/templates/contracts/README.md +8 -0
  23. napkinstack-0.7.1/src/napkinstack/templates/contracts/docs/runbook.md +15 -0
  24. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/templates/module/AGENTS.md +1 -1
  25. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/templates/module/MANIFEST.yaml +9 -6
  26. napkinstack-0.7.1/src/napkinstack/templates/module/README.md +24 -0
  27. napkinstack-0.7.1/src/napkinstack/templates/module/tests/.gitkeep +0 -0
  28. napkinstack-0.6.2/src/napkinstack/templates/module/README.md +0 -27
  29. {napkinstack-0.6.2 → napkinstack-0.7.1}/LICENSE +0 -0
  30. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/__init__.py +0 -0
  31. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/__init__.py +0 -0
  32. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/hygiene.py +0 -0
  33. {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/landed.py +0 -0
  34. {napkinstack-0.6.2/src/napkinstack/templates/module/docs/adr → napkinstack-0.7.1/src/napkinstack/templates/contracts/tests}/.gitkeep +0 -0
  35. {napkinstack-0.6.2/src/napkinstack/templates/module/src → napkinstack-0.7.1/src/napkinstack/templates/module/docs/adr}/.gitkeep +0 -0
  36. {napkinstack-0.6.2/src/napkinstack/templates/module/tests → napkinstack-0.7.1/src/napkinstack/templates/module/src}/.gitkeep +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napkinstack
3
- Version: 0.6.2
3
+ Version: 0.7.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,24 +20,9 @@ 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.6.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): what the
24
- > framework announces, it refuses — and what it cannot refuse, it says. The boundaries read the
25
- > contracts a module uses, a contract version someone relies on changes only with a proof that
26
- > runs in the base's tree, the module checks and stale approvals are enforced, a verifier is not
27
- > an author, and every verdict names the framework that gave it. Since v0.6.0, a document still
28
- > holding its template's words is refused, a deprecated module's consumers are named with their
29
- > removal date, and a contract change runs the checks of both sides — the producer and every
30
- > declared consumer. The diagnosis says whether anything refuses at all; a record names what
31
- > reached the default branch outside a pull request, and never refuses it. A first pilot
32
- > project, private, starts from here.
33
- > Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
34
- > [move to English](docs/governance/plans/2026-09-16-english-migration.md),
35
- > [frame, verify, approve](docs/governance/plans/2026-09-16-v0.3.0-frame-verify-approve.md),
36
- > [every barrier refuses](docs/governance/plans/2026-09-19-v0.4.0-every-barrier-refuses.md),
37
- > [a proof that cannot be rewritten](docs/governance/plans/2026-09-20-v0.5.0-a-proof-that-cannot-be-rewritten.md),
38
- > [the conformance suite](docs/governance/plans/2026-09-20-m12-conformance-suite.md),
39
- > [what is announced refuses](docs/governance/plans/2026-09-20-v0.6.0-what-is-announced-refuses.md),
40
- > [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
23
+ > **Status: v0.7.1, published** on [PyPI](https://pypi.org/project/napkinstack/).
24
+ > What each version changes, engine and project apart: [`CHANGELOG.md`](CHANGELOG.md).
25
+ > What is open: [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
41
26
 
42
27
  ## A project's journey
43
28
 
@@ -62,6 +47,14 @@ flowchart LR
62
47
  The project owns its skeleton and adapts it freely. Every new version reaches it on
63
48
  demand, merged with its adaptations; the conflicts are left to the team.
64
49
 
50
+ Five files are never merged again, because their content is the product's: the front page
51
+ `README.md`, the security policy, the module map and the two decision indexes. `nstack init`
52
+ writes them as a form — headings and the place of a diagram — and each is asked for when its
53
+ moment comes, never before: the sentence at the discovery's go, the page at the charter's
54
+ acceptance, the map at a module's first code. The foundation's manual sits one click away,
55
+ in `.nstack/handbook/INSTALL.md`
56
+ ([PDR-0010](docs/pdr/0010-the-framework-imposes-the-form.md)).
57
+
65
58
  ```mermaid
66
59
  flowchart LR
67
60
  V1["Skeleton v0.1<br/>common base"]:::ref --> F{"Three-way<br/>merge"}
@@ -91,8 +84,8 @@ To try it without installing anything, or to install it from this repository ins
91
84
  registry — always pinned to a release tag:
92
85
 
93
86
  ```bash
94
- uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.5.0" nstack init my-project
95
- uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.5.0"
87
+ uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.7.1" nstack init my-project
88
+ uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.7.1"
96
89
  ```
97
90
 
98
91
  **Two channels, one published artefact.** The registry publishes: the PyPI artefact of a
@@ -113,11 +106,13 @@ Each project then pins its version and changes it through `nstack update`.
113
106
  |---|---|
114
107
  | `nstack init <folder>` | Creates the project: skeleton, git repository, initial commit, GitHub checklist |
115
108
  | `nstack doctor` | Checks the workstation and the GitHub settings, read-only |
109
+ | `nstack settings` | Prints the calls that would bring the GitHub settings to the checklist; changes nothing |
116
110
  | `nstack new-module <name> <owner> <criticality>` | Creates a module, with no imposed stack |
111
+ | `nstack new-contract <name>` | Creates a contract and, the first time, the module that holds them all |
117
112
  | `nstack check`, `test`, `bootstrap` `[module]`; `nstack run <module>` | Run the commands declared in the module's manifest |
118
113
  | `nstack discover <idea-file>` | Starts a discovery for your agent: the idea kept, its document created |
119
114
  | `nstack plan` | The discovery, the charter and the cycles: formats, one cycle at a time, closures |
120
- | `nstack fitness` | Manifests, boundaries between modules, skills, plan, hygiene |
115
+ | `nstack fitness` | Manifests, boundaries between modules, skills, plan, hygiene — each also runs alone: `manifests`, `boundaries`, `skills`, `plan`, `hygiene` |
121
116
  | `nstack pr-scope` | One PR = one module, review budget |
122
117
  | `nstack modules [--changed-since <base>]` | The project's modules, or those with a file changed since a base |
123
118
  | `nstack compat [module] --base <base>` | A contract version consumed or stable changes only with the project's merged comparator |
@@ -155,8 +150,8 @@ generation; dotted: a versioned dependency.
155
150
 
156
151
  | Path | Role |
157
152
  |---|---|
158
- | `skeleton/` | What every project receives: kernel, playbooks, handbook, CI, hooks |
159
- | `copier.yml` | The questions asked at creation (a Copier template, ADR-0001) |
153
+ | `skeleton/` | What every project receives: kernel, playbooks, handbook and its manual, CI, hooks — and five forms the project writes |
154
+ | `copier.yml` | The questions asked at creation, and the files no update touches again (a Copier template, ADR-0001) |
160
155
  | `src/napkinstack/` | The engine, the `nstack` command |
161
156
  | `platform/` | The engine module's envelope: manifest, runbook, tests |
162
157
  | `PRODUCT.md`, `docs/governance/` | The working context for NapkinStack itself |
@@ -5,24 +5,9 @@ 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.6.0, published** ([PyPI](https://pypi.org/project/napkinstack/)): what the
9
- > framework announces, it refuses — and what it cannot refuse, it says. The boundaries read the
10
- > contracts a module uses, a contract version someone relies on changes only with a proof that
11
- > runs in the base's tree, the module checks and stale approvals are enforced, a verifier is not
12
- > an author, and every verdict names the framework that gave it. Since v0.6.0, a document still
13
- > holding its template's words is refused, a deprecated module's consumers are named with their
14
- > removal date, and a contract change runs the checks of both sides — the producer and every
15
- > declared consumer. The diagnosis says whether anything refuses at all; a record names what
16
- > reached the default branch outside a pull request, and never refuses it. A first pilot
17
- > project, private, starts from here.
18
- > Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
19
- > [move to English](docs/governance/plans/2026-09-16-english-migration.md),
20
- > [frame, verify, approve](docs/governance/plans/2026-09-16-v0.3.0-frame-verify-approve.md),
21
- > [every barrier refuses](docs/governance/plans/2026-09-19-v0.4.0-every-barrier-refuses.md),
22
- > [a proof that cannot be rewritten](docs/governance/plans/2026-09-20-v0.5.0-a-proof-that-cannot-be-rewritten.md),
23
- > [the conformance suite](docs/governance/plans/2026-09-20-m12-conformance-suite.md),
24
- > [what is announced refuses](docs/governance/plans/2026-09-20-v0.6.0-what-is-announced-refuses.md),
25
- > [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
8
+ > **Status: v0.7.1, published** on [PyPI](https://pypi.org/project/napkinstack/).
9
+ > What each version changes, engine and project apart: [`CHANGELOG.md`](CHANGELOG.md).
10
+ > What is open: [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
26
11
 
27
12
  ## A project's journey
28
13
 
@@ -47,6 +32,14 @@ flowchart LR
47
32
  The project owns its skeleton and adapts it freely. Every new version reaches it on
48
33
  demand, merged with its adaptations; the conflicts are left to the team.
49
34
 
35
+ Five files are never merged again, because their content is the product's: the front page
36
+ `README.md`, the security policy, the module map and the two decision indexes. `nstack init`
37
+ writes them as a form — headings and the place of a diagram — and each is asked for when its
38
+ moment comes, never before: the sentence at the discovery's go, the page at the charter's
39
+ acceptance, the map at a module's first code. The foundation's manual sits one click away,
40
+ in `.nstack/handbook/INSTALL.md`
41
+ ([PDR-0010](docs/pdr/0010-the-framework-imposes-the-form.md)).
42
+
50
43
  ```mermaid
51
44
  flowchart LR
52
45
  V1["Skeleton v0.1<br/>common base"]:::ref --> F{"Three-way<br/>merge"}
@@ -76,8 +69,8 @@ To try it without installing anything, or to install it from this repository ins
76
69
  registry — always pinned to a release tag:
77
70
 
78
71
  ```bash
79
- uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.5.0" nstack init my-project
80
- uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.5.0"
72
+ uvx --from "git+https://github.com/NapkinStack/engineering-os@v0.7.1" nstack init my-project
73
+ uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.7.1"
81
74
  ```
82
75
 
83
76
  **Two channels, one published artefact.** The registry publishes: the PyPI artefact of a
@@ -98,11 +91,13 @@ Each project then pins its version and changes it through `nstack update`.
98
91
  |---|---|
99
92
  | `nstack init <folder>` | Creates the project: skeleton, git repository, initial commit, GitHub checklist |
100
93
  | `nstack doctor` | Checks the workstation and the GitHub settings, read-only |
94
+ | `nstack settings` | Prints the calls that would bring the GitHub settings to the checklist; changes nothing |
101
95
  | `nstack new-module <name> <owner> <criticality>` | Creates a module, with no imposed stack |
96
+ | `nstack new-contract <name>` | Creates a contract and, the first time, the module that holds them all |
102
97
  | `nstack check`, `test`, `bootstrap` `[module]`; `nstack run <module>` | Run the commands declared in the module's manifest |
103
98
  | `nstack discover <idea-file>` | Starts a discovery for your agent: the idea kept, its document created |
104
99
  | `nstack plan` | The discovery, the charter and the cycles: formats, one cycle at a time, closures |
105
- | `nstack fitness` | Manifests, boundaries between modules, skills, plan, hygiene |
100
+ | `nstack fitness` | Manifests, boundaries between modules, skills, plan, hygiene — each also runs alone: `manifests`, `boundaries`, `skills`, `plan`, `hygiene` |
106
101
  | `nstack pr-scope` | One PR = one module, review budget |
107
102
  | `nstack modules [--changed-since <base>]` | The project's modules, or those with a file changed since a base |
108
103
  | `nstack compat [module] --base <base>` | A contract version consumed or stable changes only with the project's merged comparator |
@@ -140,8 +135,8 @@ generation; dotted: a versioned dependency.
140
135
 
141
136
  | Path | Role |
142
137
  |---|---|
143
- | `skeleton/` | What every project receives: kernel, playbooks, handbook, CI, hooks |
144
- | `copier.yml` | The questions asked at creation (a Copier template, ADR-0001) |
138
+ | `skeleton/` | What every project receives: kernel, playbooks, handbook and its manual, CI, hooks — and five forms the project writes |
139
+ | `copier.yml` | The questions asked at creation, and the files no update touches again (a Copier template, ADR-0001) |
145
140
  | `src/napkinstack/` | The engine, the `nstack` command |
146
141
  | `platform/` | The engine module's envelope: manifest, runbook, tests |
147
142
  | `PRODUCT.md`, `docs/governance/` | The working context for NapkinStack itself |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "napkinstack"
3
- version = "0.6.2"
3
+ version = "0.7.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.6.2"
3
+ version = "0.7.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"
@@ -0,0 +1,74 @@
1
+ """
2
+ The machine half of the assurance matrix (`.nstack/handbook/05-workflow.md` §7).
3
+
4
+ `assurance` here is the word of DO-178C's *Design Assurance Level*, of ISO 26262, of the
5
+ OpenSSF Baseline's "advanced high assurance", and of plain *quality assurance*: how much
6
+ verification the cost of a failure buys. It is not `insurance`, and it is not the French
7
+ word it is spelled like.
8
+
9
+ That table is the single source of what a criticality value requires (D67). Its rows are marked
10
+ *machine* or *owed*: the machine ones are here, the owed ones belong to a human and this module
11
+ does not pretend to know them.
12
+
13
+ Before it, one threshold lived in four places — `pull_request.SHEET_CRITICALITIES`,
14
+ `modules.NEEDS_RUNBOOK`, `modules.NEEDS_SHEET` and a literal inside M8 — which is how a
15
+ framework comes to describe one fact three times in its documents and spend it as one in its
16
+ code. A rule that reads the matrix here cannot drift from the table that announces it;
17
+ `platform/tests/test_assurance_contract.py` guards the other direction.
18
+
19
+ One row of the table is deliberately absent: **e2e**. A check can read that a module
20
+ declares an `e2e` verb; it cannot read that the verb runs end-to-end scenarios rather than
21
+ the unit suite under another name. This repository's own `platform` module declares its
22
+ whole oracle as `test`, so the rule would have asked it to alias one command as two — and a
23
+ rule a one-line alias satisfies teaches the alias (P6). The row is owed to a human.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ # Ascending, so that "the value below" is the previous one. M3 validates a manifest against it.
29
+ CRITICALITIES = ("prototype", "standard", "high", "critical")
30
+
31
+ # What each value requires, machine rows only:
32
+ # sheet a test sheet run by a verifier who is not an author (T1)
33
+ # runbook a runbook declared in the manifest and present (M8)
34
+ # rerun_at_head every scenario re-run at the head; no confirmation stands in for a
35
+ # re-run, which is what the top value costs (T4)
36
+ # `"user_facing"` means: required only when the module declares a user-visible surface (M10).
37
+ MATRIX: dict[str, dict[str, bool | str]] = {
38
+ "prototype": {"sheet": False, "runbook": False, "rerun_at_head": False},
39
+ "standard": {"sheet": "user_facing", "runbook": False, "rerun_at_head": False},
40
+ "high": {"sheet": True, "runbook": True, "rerun_at_head": False},
41
+ "critical": {"sheet": True, "runbook": True, "rerun_at_head": True},
42
+ }
43
+
44
+
45
+ def requires(criticality: str | None, row: str, user_facing: bool = False) -> bool:
46
+ """Does `criticality` require `row`? An unknown value reads as the strictest: M3 refuses it
47
+ where a manifest is judged, and until a reader fixes that the safe reading is the expensive
48
+ one — never the cheap one."""
49
+ rule = MATRIX.get(criticality or "", MATRIX["critical"])[row]
50
+ return bool(user_facing) if rule == "user_facing" else bool(rule)
51
+
52
+
53
+ def below(criticality: str | None) -> str | None:
54
+ """The value one step down, or None at the bottom and for a value the engine does not know."""
55
+ if criticality not in CRITICALITIES:
56
+ return None
57
+ index = CRITICALITIES.index(criticality)
58
+ return CRITICALITIES[index - 1] if index else None
59
+
60
+
61
+ def relief(criticality: str | None, row: str, user_facing: bool = False) -> str:
62
+ """What the value below would have required instead, as half a sentence, or "" when there is
63
+ nothing below or nothing would change.
64
+
65
+ P6 asks a refusal to name an action. On a threshold the action is not always "do the work":
66
+ it can be "this module is not what you declared it to be". Saying what the neighbouring
67
+ value costs lets a reader judge the declaration instead of only obeying it — which is the
68
+ decision an agent took alone when nothing told it where to stop (D70)."""
69
+ lower = below(criticality)
70
+ if lower is None or requires(lower, row, user_facing):
71
+ return ""
72
+ if MATRIX[lower][row] == "user_facing":
73
+ return f"at {lower} it would be required only for a module a user sees"
74
+ return f"at {lower} it would not be required"
@@ -6,7 +6,8 @@ import argparse
6
6
  import json
7
7
  from pathlib import Path
8
8
 
9
- from napkinstack import (__version__, compat, discovery, doctor, landed, modules, provenance,
9
+ from napkinstack import assurance
10
+ from napkinstack import (__version__, compat, discovery, doctor, landed, modules, provenance, settings,
10
11
  pull_request, skills)
11
12
  from napkinstack.fitness import boundaries, hygiene, manifests, plan, pr_scope
12
13
 
@@ -65,7 +66,7 @@ def build_parser() -> argparse.ArgumentParser:
65
66
  lambda a: manifests.run(a.root))
66
67
  _add(sub, "boundaries", "declared graph against real graph (B1-B8)",
67
68
  lambda a: boundaries.run(a.root))
68
- _add(sub, "plan", "the discovery, the charter and the cycles (C1-C7)", lambda a: plan.run(a.root))
69
+ _add(sub, "plan", "the discovery, the charter and the cycles (C1-C8)", lambda a: plan.run(a.root))
69
70
  sk = _add(sub, "skills", "generates or checks the skills (S1-S4)",
70
71
  lambda a: skills.run(a.root, check_only=a.check))
71
72
  sk.add_argument("--check", action="store_true", help="check without writing")
@@ -73,16 +74,24 @@ def build_parser() -> argparse.ArgumentParser:
73
74
  lambda a: hygiene.run(a.root))
74
75
  _add(sub, "fitness", "manifests + boundaries + skills + plan + hygiene",
75
76
  lambda a: _fitness(a.root))
76
- _add(sub, "doctor", "diagnoses the workstation and the GitHub settings, read-only (PDR-0001)",
77
+ _add(sub, "doctor", "diagnoses the workstation and the GitHub settings, read-only",
77
78
  lambda a: doctor.run(a.root))
79
+ _add(sub, "settings", "prints the calls that would bring the GitHub settings to the checklist; changes nothing",
80
+ lambda a: settings.run(a.root))
78
81
  nm = _add(sub, "new-module", "creates a module and its guardrails, with no imposed stack",
79
82
  lambda a: modules.create(a.root, a.name, a.owner, a.criticality, a.user_facing))
80
83
  nm.add_argument("name", help="module name, kebab-case")
81
84
  nm.add_argument("owner", help="GitHub team, organisation/team, or a user when the project has "
82
85
  "no organisation")
83
- nm.add_argument("criticality", choices=["prototype", "standard", "high", "critical"])
86
+ nm.add_argument("criticality", choices=list(assurance.CRITICALITIES),
87
+ help="what a failure of this module costs — prototype: nothing, it is "
88
+ "thrown away and nothing depends on it · standard: real code, no "
89
+ "money and no personal data · high: a day of work · critical: a "
90
+ "key, an order, money, or someone's personal data. The decider "
91
+ "settles it (.nstack/handbook/05-workflow.md §7)")
84
92
  nm.add_argument("--user-facing", action="store_true",
85
- help="a user sees this module: its pull requests carry a test sheet")
93
+ help="a user sees this module: from standard up, its pull requests carry a "
94
+ "test sheet (.nstack/handbook/05-workflow.md §7)")
86
95
  for verb, help_text in (("bootstrap", "prepares one module, or all of them (commands.bootstrap)"),
87
96
  ("check", "format, lint, types of one module, or all (commands.check)"),
88
97
  ("test", "tests of one module, or of all of them (commands.test)"),
@@ -105,6 +114,10 @@ def build_parser() -> argparse.ArgumentParser:
105
114
  ps = _add(sub, "pr-scope", "one PR = one module, review budget (P1-P2)",
106
115
  lambda a: pr_scope.run(a.root, a.base))
107
116
  ps.add_argument("--base", default="origin/main")
117
+ nc = _add(sub, "new-contract", "creates a contract — and, the first time, the module that holds them all",
118
+ lambda a: modules.create_contract(a.root, a.name, a.owner))
119
+ nc.add_argument("name", help="the contract's name, kebab-case")
120
+ nc.add_argument("--owner", help="owner of the contracts module (default: the foundation's owner)")
108
121
  pc = _add(sub, "pr-check", "test sheet and cycle, read from the pull request description (T1-T5, K1-K4)",
109
122
  lambda a: pull_request.run(a.root, a.base, a.body_file))
110
123
  pc.add_argument("--base", default="origin/main")
@@ -113,10 +126,10 @@ def build_parser() -> argparse.ArgumentParser:
113
126
  lambda a: landed.run(a.root, a.span, landed.from_environment(a.root)))
114
127
  ld.add_argument("--span", default="HEAD~1..HEAD",
115
128
  help="the commits to read, BEFORE..AFTER (default: the last commit)")
116
- ds = _add(sub, "discover", "starts a discovery from an idea file, for the team's agent (PDR-0002)",
129
+ ds = _add(sub, "discover", "starts a discovery from an idea file, for the team's agent",
117
130
  lambda a: discovery.run(a.root, a.idea))
118
131
  ds.add_argument("idea", type=Path, help="the idea, a .md or .txt file")
119
- ini = sub.add_parser("init", help="creates a project from the skeleton (PDR-0001)")
132
+ ini = sub.add_parser("init", help="creates a project from the skeleton")
120
133
  ini.add_argument("destination", type=Path, help="project folder, missing or empty")
121
134
  ini.add_argument("--project-name", help="project name (asked when absent)")
122
135
  ini.add_argument("--github-repo", help="GitHub repository, organisation/name (asked when absent)")
@@ -125,13 +138,13 @@ def build_parser() -> argparse.ArgumentParser:
125
138
  ini.add_argument("--source", help="template: URL or path (default: the NapkinStack repository)")
126
139
  ini.add_argument("--ref", help="skeleton version, tag vX.Y.Z (default: the one of nstack)")
127
140
  ini.set_defaults(func=_init)
128
- up = _add(sub, "update", "merges a NapkinStack version onto a branch to review (PDR-0001)",
141
+ up = _add(sub, "update", "merges a NapkinStack version onto a branch to review",
129
142
  _update)
130
143
  up.add_argument("--ref", help="target version, tag vX.Y.Z (default: the one of nstack)")
131
144
  return parser
132
145
 
133
146
 
134
- JUDGES = {"manifests", "boundaries", "plan", "skills", "hygiene", "fitness", "doctor", "pr-scope",
147
+ JUDGES = {"manifests", "boundaries", "plan", "skills", "hygiene", "fitness", "doctor", "settings", "pr-scope",
135
148
  "pr-check", "compat", "landed"} # the commands whose verdict depends on the framework's rules
136
149
 
137
150
 
@@ -139,4 +152,6 @@ def main(argv: list[str] | None = None) -> int:
139
152
  args = build_parser().parse_args(argv)
140
153
  if args.command in JUDGES:
141
154
  print(provenance.judged_by(args.root), flush=True)
155
+ if said := plan.stage_line(args.root):
156
+ print(said, flush=True)
142
157
  return args.func(args)
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Contract versions (D30): a merged version that someone relies on changes only with a proof
3
- that the change is additive (docs/os/03-contracts.md §3 and §4).
3
+ that the change is additive (.nstack/handbook/03-contracts.md §3 and §4).
4
4
 
5
5
  Rule:
6
6
  V1 a frozen contract version changes only when the compatibility command of the module
@@ -15,7 +15,7 @@ those under provides[].path; a new version beside it changes nothing merged. A v
15
15
  provided at the head is judged wherever it now lives: moving it is no removal (D36).
16
16
 
17
17
  The command is the project's (P1: no format assumed): an OpenAPI, protobuf or JSON Schema
18
- comparator, named in docs/tooling-profile.md. It runs from the holding module's folder with
18
+ comparator, named in .nstack/tooling-profile.md. It runs from the holding module's folder with
19
19
  NSTACK_CONTRACT the contract's name catalog-api
20
20
  NSTACK_VERSION the version v1
21
21
  NSTACK_BASE_PATH the version as merged, extracted to a temporary folder
@@ -133,7 +133,7 @@ def run(root: Path, module: str | None, base: str) -> int:
133
133
  if holder is None:
134
134
  failures.append(f"[V1] {label} changed at '{target}', which no module holds: nothing declares "
135
135
  "how its versions are compared.\n Action: keep contracts under contracts/ "
136
- "(docs/os/03-contracts.md §2).")
136
+ "(.nstack/handbook/03-contracts.md §2).")
137
137
  continue
138
138
  if module is not None and holder[0] != module:
139
139
  continue
@@ -151,8 +151,8 @@ def run(root: Path, module: str | None, base: str) -> int:
151
151
  if not command:
152
152
  failures.append(f"[V1] {label} changed, and no merged compatibility command proves the "
153
153
  f"change compatible.\n Action: declare commands.compat in {where} in a "
154
- "pull request of its own (docs/tooling-profile.md), or publish the change as "
155
- "a new version beside it (docs/os/03-contracts.md §4).")
154
+ "pull request of its own (.nstack/tooling-profile.md), or publish the change as "
155
+ "a new version beside it (.nstack/handbook/03-contracts.md §4).")
156
156
  continue
157
157
  with tempfile.TemporaryDirectory() as temporary:
158
158
  tree = _base_tree(root, fork, Path(temporary))
@@ -161,7 +161,7 @@ def run(root: Path, module: str | None, base: str) -> int:
161
161
  failures.append(f"[V1] {label}: module '{name}' has no folder at the base, so its "
162
162
  f"compatibility command cannot be run as merged.\n Action: "
163
163
  "declare the command where the module now lives, in a pull request "
164
- "of its own (docs/os/03-contracts.md §4).")
164
+ "of its own (.nstack/handbook/03-contracts.md §4).")
165
165
  continue
166
166
  env = {**os.environ, "NSTACK_CONTRACT": contract, "NSTACK_VERSION": version,
167
167
  "NSTACK_BASE_PATH": str(tree / path), "NSTACK_HEAD_PATH": str(root / target)}
@@ -172,7 +172,7 @@ def run(root: Path, module: str | None, base: str) -> int:
172
172
  f"did not prove the change compatible (exit {code}).\n"
173
173
  " Action: read the comparator's output above. If the change is "
174
174
  "breaking, publish it as a new version beside this one and migrate its "
175
- "consumers (expand/contract, docs/os/03-contracts.md §4). If the command "
175
+ "consumers (expand/contract, .nstack/handbook/03-contracts.md §4). If the command "
176
176
  "could not run — a missing tool, no network — fix the command: V1 cannot "
177
177
  "pass without a proof.")
178
178
  else:
@@ -3,7 +3,7 @@ nstack discover — starts a project's discovery from an idea file (PDR-0002, ex
3
3
 
4
4
  Deterministic, no model called: the idea is kept in docs/project/inputs/, the discovery
5
5
  document is created from its template, and the instruction for the team's agent is
6
- printed. The conversation itself happens in the agent (playbooks/discovery.md).
6
+ printed. The conversation itself happens in the agent (.nstack/playbooks/discovery.md).
7
7
 
8
8
  Usage : nstack discover <idea-file> [--root ROOT]
9
9
  Output: 0 when the discovery is started, 1 otherwise.
@@ -17,7 +17,7 @@ from pathlib import Path
17
17
  from napkinstack.fitness.plan import DISCOVERY, PROJECT
18
18
 
19
19
  INPUTS = PROJECT / "inputs"
20
- TEMPLATE = PROJECT / "_DISCOVERY_TEMPLATE.md"
20
+ TEMPLATE = Path(".nstack") / "forms" / "discovery.md" # the framework's forms live in its one folder (PDR-0012)
21
21
  SUFFIXES = {".md", ".txt"}
22
22
 
23
23
 
@@ -33,7 +33,7 @@ def run(root: Path, idea: Path) -> int:
33
33
  return 1
34
34
  if (root / DISCOVERY).exists():
35
35
  print(f"FAIL [discover] {DISCOVERY} already exists: one discovery per project.\n Action: "
36
- "continue it in your agent (playbooks/discovery.md), as a new round of the same document.")
36
+ "continue it in your agent (.nstack/playbooks/discovery.md), as a new round of the same document.")
37
37
  return 1
38
38
  kept = root / INPUTS / idea.name
39
39
  if kept.exists() and kept.resolve() != idea.resolve():
@@ -47,7 +47,7 @@ def run(root: Path, idea: Path) -> int:
47
47
  (root / DISCOVERY).write_text(template.replace("<idea file>", source), encoding="utf-8")
48
48
  print(f"Discovery started: {source} kept, {DISCOVERY} created.")
49
49
  print("\nNext, in your agent:")
50
- print(f" Follow playbooks/discovery.md on {source}: interview me, one question at a time.")
50
+ print(f" Follow .nstack/playbooks/discovery.md on {source}: interview me, one question at a time.")
51
51
  print("Then, in another session, have the document challenged (stage 5); the decider decides: "
52
52
  "go, clarify or kill.")
53
53
  return 0