napkinstack 0.1.0__py3-none-any.whl
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/__init__.py +5 -0
- napkinstack/cli.py +103 -0
- napkinstack/doctor.py +278 -0
- napkinstack/fitness/__init__.py +0 -0
- napkinstack/fitness/boundaries.py +222 -0
- napkinstack/fitness/manifests.py +198 -0
- napkinstack/fitness/pr_scope.sh +79 -0
- napkinstack/modules.py +132 -0
- napkinstack/project.py +170 -0
- napkinstack/skills.py +159 -0
- napkinstack/templates/module/AGENTS.md +36 -0
- napkinstack/templates/module/MANIFEST.yaml +77 -0
- napkinstack/templates/module/README.md +27 -0
- napkinstack/templates/module/docs/adr/.gitkeep +0 -0
- napkinstack/templates/module/src/.gitkeep +0 -0
- napkinstack/templates/module/tests/.gitkeep +0 -0
- napkinstack-0.1.0.dist-info/METADATA +139 -0
- napkinstack-0.1.0.dist-info/RECORD +21 -0
- napkinstack-0.1.0.dist-info/WHEEL +4 -0
- napkinstack-0.1.0.dist-info/entry_points.txt +3 -0
- napkinstack-0.1.0.dist-info/licenses/LICENSE +21 -0
napkinstack/skills.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Génère les skills Claude Code à partir des playbooks.
|
|
3
|
+
|
|
4
|
+
Les playbooks sont la SOURCE DE VÉRITÉ (docs/os/06-decisions.md §9). Les skills en
|
|
5
|
+
sont dérivées : fichiers générés, jamais édités à la main, jamais commités.
|
|
6
|
+
|
|
7
|
+
Pourquoi générer plutôt qu'écrire directement des skills :
|
|
8
|
+
- portabilité — l'OS doit rester utilisable par un agent qui ne connaît pas les
|
|
9
|
+
skills. Un outil est un adaptateur, jamais une fondation
|
|
10
|
+
(docs/tooling-profile.md).
|
|
11
|
+
- source unique — deux copies d'une même règle divergent toujours.
|
|
12
|
+
|
|
13
|
+
Contrôles (mode --check, exécuté en CI) :
|
|
14
|
+
S1 chaque playbook a une entrée dans .nstack/skills.yaml
|
|
15
|
+
S2 chaque entrée pointe vers un playbook existant
|
|
16
|
+
S3 les skills générées correspondent aux playbooks actuels
|
|
17
|
+
S4 nom et description conformes à la spécification Agent Skills
|
|
18
|
+
|
|
19
|
+
S3 ne s'applique que si .claude/skills/ existe. Les skills sont gitignorées : un clone
|
|
20
|
+
vierge, donc la CI, n'en a aucune, et aucune ne peut y être désynchronisée.
|
|
21
|
+
|
|
22
|
+
Une racine sans playbooks/ ni .nstack/skills.yaml, comme le dépôt NapkinStack lui-même,
|
|
23
|
+
n'est pas concernée.
|
|
24
|
+
|
|
25
|
+
Usage :
|
|
26
|
+
nstack skills [--root RACINE] # génère .claude/skills/
|
|
27
|
+
nstack skills --check [--root RACINE] # vérifie sans écrire
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import hashlib
|
|
33
|
+
import re
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
import yaml
|
|
37
|
+
|
|
38
|
+
SKILL_NAME = re.compile(r"[a-z0-9]+(-[a-z0-9]+)*")
|
|
39
|
+
BANNER = (
|
|
40
|
+
"<!-- GÉNÉRÉ depuis {source} par nstack skills — NE PAS ÉDITER.\n"
|
|
41
|
+
" Modifier le playbook, puis relancer `nstack skills`. -->"
|
|
42
|
+
)
|
|
43
|
+
MAPPING = Path(".nstack") / "skills.yaml"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalise(text: str) -> str:
|
|
47
|
+
"""Description sur une seule ligne, pour un frontmatter YAML propre."""
|
|
48
|
+
return " ".join(text.split())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build(root: Path, name: str, entry: dict) -> tuple[Path, str]:
|
|
52
|
+
body = (root / entry["source"]).read_text(encoding="utf-8")
|
|
53
|
+
# Sérialisé, jamais concaténé : « : » ou « # » dans une description casserait le YAML.
|
|
54
|
+
frontmatter = yaml.safe_dump(
|
|
55
|
+
{"name": name, "description": normalise(entry["description"])},
|
|
56
|
+
allow_unicode=True, sort_keys=False, width=float("inf"),
|
|
57
|
+
)
|
|
58
|
+
content = "---\n" + frontmatter + "---\n\n" + BANNER.format(source=entry["source"]) + "\n\n" + body
|
|
59
|
+
return root / ".claude" / "skills" / name / "SKILL.md", content
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def digest(text: str) -> str:
|
|
63
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run(root: Path, check_only: bool = False) -> int:
|
|
67
|
+
map_file = root / MAPPING
|
|
68
|
+
playbook_dir = root / "playbooks"
|
|
69
|
+
skill_dir = root / ".claude" / "skills"
|
|
70
|
+
|
|
71
|
+
if not map_file.is_file():
|
|
72
|
+
if not playbook_dir.is_dir():
|
|
73
|
+
print(f"Skills : non applicable, ni playbooks/ ni {MAPPING} dans {root}.")
|
|
74
|
+
return 0
|
|
75
|
+
print(f" ÉCHEC [S1] {MAPPING} introuvable dans {root} : aucun playbook n'a d'entrée.\n"
|
|
76
|
+
f" Créer {MAPPING}, une entrée par playbook (source, description).")
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
brut = yaml.safe_load(map_file.read_text(encoding="utf-8")) or {}
|
|
81
|
+
except yaml.YAMLError as exc:
|
|
82
|
+
print(f" ÉCHEC [S2] {MAPPING} illisible : {exc}")
|
|
83
|
+
return 1
|
|
84
|
+
mapping = brut.get("skills") if isinstance(brut, dict) else None
|
|
85
|
+
if not isinstance(mapping, dict):
|
|
86
|
+
print(f" ÉCHEC [S2] {MAPPING} : section skills attendue, un dictionnaire nom → source et "
|
|
87
|
+
"description.")
|
|
88
|
+
return 1
|
|
89
|
+
failures: list[str] = [f"[S2] skill '{name}' : entrée invalide, source et description attendues"
|
|
90
|
+
for name, entry in mapping.items() if not isinstance(entry, dict)]
|
|
91
|
+
mapping = {name: entry for name, entry in mapping.items() if isinstance(entry, dict)}
|
|
92
|
+
|
|
93
|
+
# S1 — tout playbook doit avoir une entrée
|
|
94
|
+
declared = {Path(e["source"]).name for e in mapping.values() if e.get("source")}
|
|
95
|
+
for playbook in sorted(playbook_dir.glob("*.md")):
|
|
96
|
+
if playbook.name not in declared:
|
|
97
|
+
failures.append(
|
|
98
|
+
f"[S1] {playbook.relative_to(root)} n'a pas d'entrée dans "
|
|
99
|
+
f"{MAPPING}.\n Ajouter une description, ou retirer le "
|
|
100
|
+
f"playbook s'il ne sert plus (docs/os/10-mesure.md §6)."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# S2 — toute entrée doit pointer vers un playbook existant
|
|
104
|
+
for name, entry in mapping.items():
|
|
105
|
+
if not (root / entry.get("source", "")).is_file():
|
|
106
|
+
failures.append(f"[S2] skill '{name}' : source introuvable ({entry.get('source')})")
|
|
107
|
+
if not normalise(entry.get("description", "")):
|
|
108
|
+
failures.append(f"[S2] skill '{name}' : description vide")
|
|
109
|
+
|
|
110
|
+
# S4 — spécification Agent Skills (https://agentskills.io/specification)
|
|
111
|
+
for name, entry in mapping.items():
|
|
112
|
+
if len(name) > 64 or not SKILL_NAME.fullmatch(name):
|
|
113
|
+
failures.append(
|
|
114
|
+
f"[S4] skill '{name}' : nom invalide. 1 à 64 caractères, a-z, 0-9 et "
|
|
115
|
+
"tirets simples, sans tiret au début ni à la fin."
|
|
116
|
+
)
|
|
117
|
+
if len(normalise(entry.get("description", ""))) > 1024:
|
|
118
|
+
failures.append(f"[S4] skill '{name}' : description de plus de 1024 caractères")
|
|
119
|
+
|
|
120
|
+
if failures:
|
|
121
|
+
for failure in failures:
|
|
122
|
+
print(f" ÉCHEC {failure}")
|
|
123
|
+
return 1
|
|
124
|
+
|
|
125
|
+
# S3 — génération ou comparaison
|
|
126
|
+
if check_only and not skill_dir.is_dir():
|
|
127
|
+
print(f"Skills : S1, S2 et S4 conformes. S3 non applicable : "
|
|
128
|
+
f"{skill_dir.relative_to(root)}/ absent, aucune skill générée ici.")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
stale: list[str] = []
|
|
132
|
+
written = 0
|
|
133
|
+
for name, entry in sorted(mapping.items()):
|
|
134
|
+
path, content = build(root, name, entry)
|
|
135
|
+
if check_only:
|
|
136
|
+
if not path.is_file():
|
|
137
|
+
stale.append(f"[S3] skill '{name}' absente de .claude/skills/")
|
|
138
|
+
elif digest(path.read_text(encoding="utf-8")) != digest(content):
|
|
139
|
+
stale.append(f"[S3] skill '{name}' désynchronisée de {entry['source']}")
|
|
140
|
+
else:
|
|
141
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
path.write_text(content, encoding="utf-8")
|
|
143
|
+
written += 1
|
|
144
|
+
|
|
145
|
+
if check_only:
|
|
146
|
+
if stale:
|
|
147
|
+
for item in stale:
|
|
148
|
+
print(f" ÉCHEC {item}")
|
|
149
|
+
print("\nLancer `nstack skills` pour régénérer.")
|
|
150
|
+
return 1
|
|
151
|
+
print(f"Skills : {len(mapping)} synchronisées avec les playbooks.")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
print(f"Skills générées dans .claude/skills/ : {written}")
|
|
155
|
+
for name in sorted(mapping):
|
|
156
|
+
print(f" - {name}")
|
|
157
|
+
print("\nElles se déclenchent seules selon leur description ; l'agent peut aussi")
|
|
158
|
+
print("les invoquer par leur nom. Le kernel reste dans AGENTS.md.")
|
|
159
|
+
return 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# {{MODULE_NAME}} — instructions locales
|
|
2
|
+
|
|
3
|
+
> Uniquement ce qui est **spécifique à ce module**.
|
|
4
|
+
> Ne jamais dupliquer une règle du kernel (`/AGENTS.md`) : c'est du contexte gaspillé
|
|
5
|
+
> et une source de divergence.
|
|
6
|
+
>
|
|
7
|
+
> Budget indicatif : 100 lignes.
|
|
8
|
+
|
|
9
|
+
## Responsabilité
|
|
10
|
+
|
|
11
|
+
<Une phrase. Identique à celle du MANIFEST.>
|
|
12
|
+
|
|
13
|
+
## Ce que ce module ne fait pas
|
|
14
|
+
|
|
15
|
+
<Les confusions probables, et vers quel module renvoyer.>
|
|
16
|
+
|
|
17
|
+
## Conventions internes non devinables
|
|
18
|
+
|
|
19
|
+
<Nommage, organisation, choix qui surprendraient quelqu'un d'extérieur.>
|
|
20
|
+
|
|
21
|
+
## Invariants métier
|
|
22
|
+
|
|
23
|
+
<Ce qui doit rester vrai en toutes circonstances. Idéalement, chacun a un test.>
|
|
24
|
+
|
|
25
|
+
## Pièges connus
|
|
26
|
+
|
|
27
|
+
<Ce qui a déjà cassé ici. Chaque entrée devrait devenir un test — voir
|
|
28
|
+
docs/os/10-mesure.md §4.>
|
|
29
|
+
|
|
30
|
+
## Zones à ne pas modifier sans validation
|
|
31
|
+
|
|
32
|
+
<Quoi, et pourquoi.>
|
|
33
|
+
|
|
34
|
+
## Commandes non standards
|
|
35
|
+
|
|
36
|
+
<Uniquement ce qui s'écarte des verbes du MANIFEST.>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Source de vérité machine-lisible du module.
|
|
2
|
+
# Lu par : les humains (onboarding), les agents (scoping), la CI (fitness functions).
|
|
3
|
+
# Toute divergence entre ce fichier et le code fait échouer la CI.
|
|
4
|
+
|
|
5
|
+
module:
|
|
6
|
+
name: "{{MODULE_NAME}}"
|
|
7
|
+
|
|
8
|
+
# UNE phrase. Si elle nécessite un "et", le module fait probablement deux choses.
|
|
9
|
+
responsibility: >
|
|
10
|
+
TODO — décrire la capacité métier de ce module en une phrase.
|
|
11
|
+
|
|
12
|
+
owner: "{{OWNER}}" # une ÉQUIPE GitHub organisation/équipe, jamais un individu
|
|
13
|
+
contact: "@{{OWNER}}"
|
|
14
|
+
|
|
15
|
+
# Proposé | Actif | Maintenance | Déprécié | Retiré (docs/os/02-modules.md §6)
|
|
16
|
+
lifecycle: Proposé
|
|
17
|
+
|
|
18
|
+
# prototype | standard | eleve | critique (docs/os/07-gouvernance.md §6)
|
|
19
|
+
criticality: "{{CRITICALITY}}"
|
|
20
|
+
|
|
21
|
+
# Nom du module dans le code, s'il diffère du dossier (package, namespace).
|
|
22
|
+
# Utilisé par la détection de frontières.
|
|
23
|
+
# code_name: {{MODULE_NAME}}
|
|
24
|
+
|
|
25
|
+
# Uniquement si lifecycle = Déprécié. Check rouge si la date est dépassée.
|
|
26
|
+
# deprecation:
|
|
27
|
+
# replaced_by:
|
|
28
|
+
# removal_date: AAAA-MM-JJ
|
|
29
|
+
|
|
30
|
+
# Contrats PRODUITS par ce module.
|
|
31
|
+
provides: []
|
|
32
|
+
# - contract: {{MODULE_NAME}}-api
|
|
33
|
+
# version: v1
|
|
34
|
+
# path: contracts/{{MODULE_NAME}}-api/v1
|
|
35
|
+
# stability: experimental # experimental | stable | deprecated
|
|
36
|
+
# # removal_date: AAAA-MM-JJ # obligatoire si stability = deprecated
|
|
37
|
+
|
|
38
|
+
# Contrats CONSOMMÉS — c'est le graphe DÉCLARÉ.
|
|
39
|
+
# La CI le compare au graphe RÉEL extrait du code : tout écart = violation.
|
|
40
|
+
consumes: []
|
|
41
|
+
# - contract: identity-api
|
|
42
|
+
# version: v1
|
|
43
|
+
# module: identity
|
|
44
|
+
|
|
45
|
+
# Données possédées. Personne d'autre n'y accède directement.
|
|
46
|
+
data:
|
|
47
|
+
owns: []
|
|
48
|
+
shared: [] # toute exception exige un ADR nommé
|
|
49
|
+
|
|
50
|
+
# Dépendances externes significatives.
|
|
51
|
+
# Chacune déclare sa stratégie de sortie (docs/os/06-decisions.md §3).
|
|
52
|
+
dependencies: []
|
|
53
|
+
# - name:
|
|
54
|
+
# purpose:
|
|
55
|
+
# adr:
|
|
56
|
+
# exit_strategy:
|
|
57
|
+
|
|
58
|
+
# Verbes standards : mêmes noms partout, commandes de la stack du module
|
|
59
|
+
# (docs/os/09-plateforme.md §2). `nstack <verbe> <module>` les exécute depuis ce dossier.
|
|
60
|
+
# Aucune stack n'est imposée : remplacer chaque commande « à déclarer ».
|
|
61
|
+
commands:
|
|
62
|
+
check: "echo 'commands.check à déclarer dans MANIFEST.yaml' >&2; exit 1" # < 2 min : format, lint, types
|
|
63
|
+
test: "echo 'commands.test à déclarer dans MANIFEST.yaml' >&2; exit 1" # ne démarre JAMAIS un autre module
|
|
64
|
+
# bootstrap: # facultatif : rendre le module exploitable depuis un clone vierge
|
|
65
|
+
# run: # facultatif : démarrage local, avec doublures pour les dépendances
|
|
66
|
+
|
|
67
|
+
# Budget de revue. Hérite du projet si absent (docs/os/05-workflow.md §4).
|
|
68
|
+
review_budget:
|
|
69
|
+
max_lines: 400
|
|
70
|
+
max_files: 15
|
|
71
|
+
max_modules: 1 # non ajustable
|
|
72
|
+
|
|
73
|
+
docs:
|
|
74
|
+
readme: README.md
|
|
75
|
+
agents: AGENTS.md
|
|
76
|
+
adr: docs/adr/
|
|
77
|
+
# runbook: docs/runbook.md # obligatoire si criticality >= eleve
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{MODULE_NAME}}
|
|
2
|
+
|
|
3
|
+
<Une phrase : à quoi sert ce module.>
|
|
4
|
+
|
|
5
|
+
- **Owner** : {{OWNER}}
|
|
6
|
+
- **Criticité** : {{CRITICALITY}}
|
|
7
|
+
- **Manifest** : [MANIFEST.yaml](./MANIFEST.yaml)
|
|
8
|
+
|
|
9
|
+
## Démarrer
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
nstack bootstrap {{MODULE_NAME}} # depuis un clone vierge, si déclaré
|
|
13
|
+
nstack check {{MODULE_NAME}} # format, lint, types — < 2 min
|
|
14
|
+
nstack test {{MODULE_NAME}} # ne démarre aucun autre module
|
|
15
|
+
nstack run {{MODULE_NAME}} # local, avec doublures pour les dépendances
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Commandes : section `commands` du MANIFEST, à déclarer pour la stack du module.
|
|
19
|
+
|
|
20
|
+
## Contrats
|
|
21
|
+
|
|
22
|
+
- Produits : voir `provides` dans le MANIFEST
|
|
23
|
+
- Consommés : voir `consumes` dans le MANIFEST
|
|
24
|
+
|
|
25
|
+
## Décisions
|
|
26
|
+
|
|
27
|
+
Voir `docs/adr/`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: napkinstack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework de travail pour faire travailler plusieurs équipes et leurs agents sur un même dépôt : modules, contrats, garde-fous en CI.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: pyyaml>=6.0
|
|
8
|
+
Requires-Dist: copier==9.18.2
|
|
9
|
+
Requires-Dist: pre-commit==4.6.2
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Project-URL: Homepage, https://github.com/NapkinStack/engineering-os
|
|
12
|
+
Project-URL: Source, https://github.com/NapkinStack/engineering-os
|
|
13
|
+
Project-URL: Issues, https://github.com/NapkinStack/engineering-os/issues
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# NapkinStack
|
|
17
|
+
|
|
18
|
+
Framework de travail pour faire travailler plusieurs équipes et leurs agents sur un même
|
|
19
|
+
dépôt : modules, contrats, garde-fous en CI. Sur le modèle de Django ou Rails, une commande
|
|
20
|
+
crée le projet, qui reçoit ensuite les nouvelles versions à sa demande ; aucune stack
|
|
21
|
+
applicative n'est imposée. Positionnement et vocabulaire : [`PRODUCT.md`](PRODUCT.md) §1.
|
|
22
|
+
|
|
23
|
+
> **État : en construction (v0.1.0).** Toutes les commandes fonctionnent depuis ce dépôt ;
|
|
24
|
+
> la première version publiée sur PyPI arrive au chantier M5, avant un premier projet
|
|
25
|
+
> pilote. Suivi : [feuille de route](docs/governance/plans/2026-09-15-moteur-v0.1.0.md),
|
|
26
|
+
> [`docs/governance/chantiers.md`](docs/governance/chantiers.md).
|
|
27
|
+
|
|
28
|
+
## Le parcours d'un projet
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
flowchart LR
|
|
32
|
+
I["Installer<br/>uv tool install"]:::cmd --> N["nstack init"]:::cmd
|
|
33
|
+
N --> G["Publier sur GitHub<br/>appliquer la checklist"]:::humain
|
|
34
|
+
G --> D["nstack doctor<br/>lecture seule"]:::cmd
|
|
35
|
+
D --> M["nstack new-module"]:::cmd
|
|
36
|
+
M --> W["Travail en PR<br/>l'équipe et son agent"]:::humain
|
|
37
|
+
W --> U["nstack update<br/>branche fusionnée"]:::cmd
|
|
38
|
+
U --> P["PR relue<br/>validée par la CI"]:::humain
|
|
39
|
+
P -->|"version suivante"| U
|
|
40
|
+
|
|
41
|
+
classDef cmd fill:#1f2937,color:#fff
|
|
42
|
+
classDef humain fill:#065f46,color:#fff
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Légende** — gris : commande NapkinStack · vert : action de l'équipe. Décision :
|
|
46
|
+
[PDR-0001](docs/pdr/0001-creer-un-projet-et-recevoir-les-evolutions.md).
|
|
47
|
+
|
|
48
|
+
Le projet possède son squelette et l'adapte librement. Chaque nouvelle version lui arrive
|
|
49
|
+
à sa demande, fusionnée avec ses adaptations ; les conflits restent à l'équipe.
|
|
50
|
+
|
|
51
|
+
```mermaid
|
|
52
|
+
flowchart LR
|
|
53
|
+
V1["Squelette v0.1<br/>base commune"]:::ref --> F{"Fusion<br/>à 3 voies"}
|
|
54
|
+
V2["Squelette v0.2<br/>correctifs NapkinStack"]:::ns --> F
|
|
55
|
+
PR["Projet<br/>adaptations de l'équipe"]:::equipe --> F
|
|
56
|
+
F -->|"lignes différentes"| B["Branche de mise à jour<br/>correctifs + adaptations"]:::ok
|
|
57
|
+
F -->|"même ligne modifiée"| X["Conflit marqué<br/>commit refusé"]:::ko
|
|
58
|
+
|
|
59
|
+
classDef ref fill:#374151,color:#fff
|
|
60
|
+
classDef ns fill:#1e3a8a,color:#fff
|
|
61
|
+
classDef equipe fill:#065f46,color:#fff
|
|
62
|
+
classDef ok fill:#065f46,color:#fff
|
|
63
|
+
classDef ko fill:#7c2d12,color:#fff
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Légende** — gris : version dont le projet est issu · bleu : nouvelle version · vert :
|
|
67
|
+
travail de l'équipe et résultat accepté · rouge : conflit laissé à l'équipe.
|
|
68
|
+
|
|
69
|
+
## Installer
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv tool install napkinstack --with-executables-from pre-commit # prérequis : uv et git
|
|
73
|
+
nstack init mon-projet
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Disponible dès la publication de la v0.1.0 sur PyPI ; chaque projet épingle ensuite sa
|
|
77
|
+
version et la change par `nstack update`.
|
|
78
|
+
|
|
79
|
+
## Les commandes
|
|
80
|
+
|
|
81
|
+
| Commande | Rôle |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `nstack init <dossier>` | Crée le projet : squelette, dépôt git, commit initial, checklist GitHub |
|
|
84
|
+
| `nstack doctor` | Vérifie le poste et les réglages GitHub, en lecture seule |
|
|
85
|
+
| `nstack new-module <nom> <organisation>/<équipe> <criticité>` | Crée un module, sans stack imposée |
|
|
86
|
+
| `nstack check`, `test`, `bootstrap` `[module]` ; `nstack run <module>` | Exécutent les commandes déclarées dans le manifest du module |
|
|
87
|
+
| `nstack fitness` | Manifests, frontières entre modules, skills |
|
|
88
|
+
| `nstack pr-scope` | Une PR = un module, budget de revue |
|
|
89
|
+
| `nstack skills` | Expose les playbooks en skills pour l'agent |
|
|
90
|
+
| `nstack update` | Pose la nouvelle version sur une branche à relire |
|
|
91
|
+
|
|
92
|
+
**Prérequis** : uv et git. Les garde-fous bloquent vraiment sur un dépôt GitHub public, ou
|
|
93
|
+
privé sous l'offre Team ou Pro ; sur un dépôt privé de l'offre Free, la CI informe sans
|
|
94
|
+
bloquer ([précision de PDR-0001](docs/pdr/0001-creer-un-projet-et-recevoir-les-evolutions.md)).
|
|
95
|
+
|
|
96
|
+
**IA** : NapkinStack n'en embarque aucune. L'agent de l'équipe (Claude Code, Codex,
|
|
97
|
+
Copilot…) lit le kernel et les playbooks, lance les commandes, et la CI accepte ou refuse
|
|
98
|
+
ses propositions comme celles de n'importe quel contributeur.
|
|
99
|
+
|
|
100
|
+
## Ce dépôt
|
|
101
|
+
|
|
102
|
+
```mermaid
|
|
103
|
+
flowchart LR
|
|
104
|
+
S["skeleton/<br/>squelette de projet"]:::livre -->|"copier.yml"| P["Projet d'une équipe"]:::projet
|
|
105
|
+
E["src/napkinstack/<br/>moteur nstack"]:::livre -.->|"version épinglée"| P
|
|
106
|
+
A["PRODUCT.md · docs/governance/<br/>platform/ · CI du dépôt"]:::interne
|
|
107
|
+
|
|
108
|
+
classDef livre fill:#1e3a8a,color:#fff
|
|
109
|
+
classDef projet fill:#065f46,color:#fff
|
|
110
|
+
classDef interne fill:#374151,color:#fff
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Légende** — bleu : livré aux projets · vert : projet généré, qui possède son squelette ·
|
|
114
|
+
gris : développement de NapkinStack, jamais copié (PDR-0001 R6). Trait plein : génération ;
|
|
115
|
+
pointillé : dépendance versionnée.
|
|
116
|
+
|
|
117
|
+
| Chemin | Rôle |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `skeleton/` | Ce que reçoit chaque projet : kernel, playbooks, manuel, CI, hooks |
|
|
120
|
+
| `copier.yml` | Questions posées à la création (gabarit Copier, ADR-0001) |
|
|
121
|
+
| `src/napkinstack/` | Le moteur, commande `nstack` |
|
|
122
|
+
| `platform/` | Enveloppe du module moteur : manifest, runbook, tests |
|
|
123
|
+
| `PRODUCT.md`, `docs/governance/` | Contexte de travail sur NapkinStack |
|
|
124
|
+
| `docs/adr/`, `docs/pdr/` | Décisions de NapkinStack |
|
|
125
|
+
|
|
126
|
+
## Développer NapkinStack
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
uv sync # prérequis : uv
|
|
130
|
+
uv run pre-commit install
|
|
131
|
+
uv run nstack fitness # garde-fous du dépôt
|
|
132
|
+
uv run bash platform/tests/run.sh # oracle : chaque garde-fou prouve qu'il sait échouer
|
|
133
|
+
uv run nstack init /tmp/essai --source . --ref HEAD # projet d'essai depuis l'arbre de travail
|
|
134
|
+
uv run nstack doctor --root /tmp/essai # poste et réglages GitHub, en lecture seule
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Contribuer : [`CONTRIBUTING.md`](CONTRIBUTING.md), après [`PRODUCT.md`](PRODUCT.md).
|
|
138
|
+
|
|
139
|
+
Licence : [MIT](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
napkinstack/__init__.py,sha256=NzkIDEWzZjyddt3O4Xo0SfjmzJ5sFv109T84dk4VnDY,158
|
|
2
|
+
napkinstack/cli.py,sha256=wd77mKByEwaV38NG-HR0414Y2_3f_q9xanNzJpBUzMc,5072
|
|
3
|
+
napkinstack/doctor.py,sha256=Aj-X97_1KBotW90farXRE_eyIvsWxa92iNIDC1k_I_4,13573
|
|
4
|
+
napkinstack/fitness/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
napkinstack/fitness/boundaries.py,sha256=VybKCHpnFAdfFpHVSawJGsUF4NVXsVahK2qtO2Y2we4,8654
|
|
6
|
+
napkinstack/fitness/manifests.py,sha256=UkW9r97EzF6BKW8BE6ji7Pi8jNlHErqDa2graVV3QCE,8024
|
|
7
|
+
napkinstack/fitness/pr_scope.sh,sha256=qozdju-j9v0I1fL6qHDqEXprO0tGSklSRys1T40z-N0,3026
|
|
8
|
+
napkinstack/modules.py,sha256=HnBzyhp9vjeYz4xX7p6Rh46NoFKapVC0NypLC6fy8mw,5813
|
|
9
|
+
napkinstack/project.py,sha256=0mFruUQu6Ny1hfP4UJWI7zOk85fp1hxufiliDCu9sso,8686
|
|
10
|
+
napkinstack/skills.py,sha256=eyrBug-g6dzHXS6pt1Gy8GiBV_m1kLBsnMa4eaUo240,6743
|
|
11
|
+
napkinstack/templates/module/AGENTS.md,sha256=MriXXCiu6wrpCmMm20SQXZY-TRaBtgoOGnMNbvnjwuk,906
|
|
12
|
+
napkinstack/templates/module/MANIFEST.yaml,sha256=SbpHi49LEWFQ9slQCrbhxmqW640E5Sq49Z8qn0O3RQw,2910
|
|
13
|
+
napkinstack/templates/module/README.md,sha256=NTVy4yGEk6pTYZtZkYZEKANRz-KzwftU2MAG_9sj6YY,716
|
|
14
|
+
napkinstack/templates/module/docs/adr/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
napkinstack/templates/module/src/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
napkinstack/templates/module/tests/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
napkinstack-0.1.0.dist-info/licenses/LICENSE,sha256=EJcqdCHOa6dE67xGECfIbP-_0AmafpbAWjlQyqO--T0,1068
|
|
18
|
+
napkinstack-0.1.0.dist-info/WHEEL,sha256=_d8F1e7SqtoW6CDj4Gi8lFC26a_7I17R7zPLCKTp4Fg,81
|
|
19
|
+
napkinstack-0.1.0.dist-info/entry_points.txt,sha256=zUNcg3_nsshrzjGhv-DRdegu5Eb-rha_4DckESq3NkU,49
|
|
20
|
+
napkinstack-0.1.0.dist-info/METADATA,sha256=k3L9a8WwKa0ZBti9saxzbtcmipH19g7qXUAycFAJvPQ,6321
|
|
21
|
+
napkinstack-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NapkinStack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|