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.
- {napkinstack-0.6.2 → napkinstack-0.7.1}/PKG-INFO +19 -24
- {napkinstack-0.6.2 → napkinstack-0.7.1}/README.md +18 -23
- {napkinstack-0.6.2 → napkinstack-0.7.1}/pyproject.toml +1 -1
- {napkinstack-0.6.2 → napkinstack-0.7.1}/pyproject.toml.orig +1 -1
- napkinstack-0.7.1/src/napkinstack/assurance.py +74 -0
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/cli.py +24 -9
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/compat.py +7 -7
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/discovery.py +4 -4
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/doctor.py +77 -25
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/boundaries.py +7 -7
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/manifests.py +34 -14
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/plan.py +104 -9
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/pr_scope.py +5 -5
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/modules.py +123 -13
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/project.py +204 -7
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/provenance.py +1 -1
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/pull_request.py +73 -27
- napkinstack-0.7.1/src/napkinstack/settings.py +247 -0
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/skills.py +6 -6
- napkinstack-0.7.1/src/napkinstack/templates/contracts/AGENTS.md +26 -0
- napkinstack-0.7.1/src/napkinstack/templates/contracts/MANIFEST.yaml +29 -0
- napkinstack-0.7.1/src/napkinstack/templates/contracts/README.md +8 -0
- napkinstack-0.7.1/src/napkinstack/templates/contracts/docs/runbook.md +15 -0
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/templates/module/AGENTS.md +1 -1
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/templates/module/MANIFEST.yaml +9 -6
- napkinstack-0.7.1/src/napkinstack/templates/module/README.md +24 -0
- napkinstack-0.7.1/src/napkinstack/templates/module/tests/.gitkeep +0 -0
- napkinstack-0.6.2/src/napkinstack/templates/module/README.md +0 -27
- {napkinstack-0.6.2 → napkinstack-0.7.1}/LICENSE +0 -0
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/__init__.py +0 -0
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/__init__.py +0 -0
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/fitness/hygiene.py +0 -0
- {napkinstack-0.6.2 → napkinstack-0.7.1}/src/napkinstack/landed.py +0 -0
- {napkinstack-0.6.2/src/napkinstack/templates/module/docs/adr → napkinstack-0.7.1/src/napkinstack/templates/contracts/tests}/.gitkeep +0 -0
- {napkinstack-0.6.2/src/napkinstack/templates/module/src → napkinstack-0.7.1/src/napkinstack/templates/module/docs/adr}/.gitkeep +0 -0
- {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.
|
|
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.
|
|
24
|
-
>
|
|
25
|
-
>
|
|
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.
|
|
95
|
-
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.
|
|
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.
|
|
9
|
-
>
|
|
10
|
-
>
|
|
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.
|
|
80
|
-
uv tool install "napkinstack @ git+https://github.com/NapkinStack/engineering-os@v0.
|
|
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 |
|
|
@@ -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
|
|
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-
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
"(
|
|
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 (
|
|
155
|
-
"a new version beside it (
|
|
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 (
|
|
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,
|
|
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 =
|
|
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
|