nexu 0.2.5__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.
- nexu-0.2.5.dist-info/METADATA +162 -0
- nexu-0.2.5.dist-info/RECORD +25 -0
- nexu-0.2.5.dist-info/WHEEL +4 -0
- nexu-0.2.5.dist-info/entry_points.txt +2 -0
- nexu-0.2.5.dist-info/licenses/LICENSE +16 -0
- vico/__init__.py +5 -0
- vico/__main__.py +4 -0
- vico/blueprint.py +73 -0
- vico/capsule.py +122 -0
- vico/cli.py +219 -0
- vico/diff.py +35 -0
- vico/drift.py +36 -0
- vico/export_prompt.py +100 -0
- vico/files.py +51 -0
- vico/freeze.py +26 -0
- vico/git.py +22 -0
- vico/hashing.py +16 -0
- vico/init_project.py +62 -0
- vico/intract.py +125 -0
- vico/iterate.py +44 -0
- vico/models.py +154 -0
- vico/paths.py +35 -0
- vico/promote.py +31 -0
- vico/status.py +35 -0
- vico/verify.py +236 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexu
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: Visual Intent Contract Orchestrator: freeze project slices, evolve capsules, verify intent contracts.
|
|
5
|
+
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: capsule,contracts,intent,llm,mock,static-analysis,workflow
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: pyyaml>=6.0
|
|
19
|
+
Requires-Dist: rich>=13.0
|
|
20
|
+
Requires-Dist: typer>=0.12.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: costs>=0.1.20; extra == 'dev'
|
|
23
|
+
Requires-Dist: goal>=2.1.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
25
|
+
Requires-Dist: pfix>=0.1.60; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Provides-Extra: llm
|
|
29
|
+
Requires-Dist: litellm>=1.0; extra == 'llm'
|
|
30
|
+
Provides-Extra: watch
|
|
31
|
+
Requires-Dist: watchdog>=4.0; extra == 'watch'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Vico
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## AI Cost Tracking
|
|
38
|
+
|
|
39
|
+
   
|
|
40
|
+
  
|
|
41
|
+
|
|
42
|
+
- 🤖 **LLM usage:** $0.5133 (5 commits)
|
|
43
|
+
- 👤 **Human dev:** ~$231 (2.3h @ $100/h, 30min dedup)
|
|
44
|
+
|
|
45
|
+
Generated on 2026-05-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
**Vico** — **Visual Intent Contract Orchestrator**.
|
|
50
|
+
|
|
51
|
+
Vico is a Python package and CLI for creating small, isolated project capsules from a large codebase.
|
|
52
|
+
It helps you freeze a baseline, extract a slice of code/data/contracts, evolve that slice through multiple
|
|
53
|
+
LLM or human iterations, and verify the result against formal intent contracts before promoting it back.
|
|
54
|
+
|
|
55
|
+
The core workflow is:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
freeze → capsule create → blueprint → iterate → export-prompt → verify → promote
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Vico is designed to work with **Intract**-style intent contracts, but it can run as a standalone prototype.
|
|
62
|
+
The goal is not to make an LLM magically correct. The goal is to keep the LLM inside a small, versioned,
|
|
63
|
+
contract-bound sandbox and detect when its output diverges from declared intent.
|
|
64
|
+
|
|
65
|
+
## What changed in 0.2.0
|
|
66
|
+
|
|
67
|
+
The second iteration adds the missing practical loop around capsules:
|
|
68
|
+
|
|
69
|
+
- baseline hash lock per capsule,
|
|
70
|
+
- capsule diff against the frozen slice,
|
|
71
|
+
- source drift check against the original files,
|
|
72
|
+
- generated UI/API/test blueprint,
|
|
73
|
+
- LLM-ready prompt export,
|
|
74
|
+
- capsule status command,
|
|
75
|
+
- richer verification with evidence for outputs, forbidden effects, required intents and secret-like values.
|
|
76
|
+
|
|
77
|
+
## Why Vico?
|
|
78
|
+
|
|
79
|
+
Long-running IDE prompting has a common failure mode:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
large repo + vague task + many steps = context drift and hallucinated implementation
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Vico changes the operating model:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
large repo
|
|
89
|
+
↓ freeze baseline
|
|
90
|
+
small capsule
|
|
91
|
+
↓ evolve only this capsule
|
|
92
|
+
verified result
|
|
93
|
+
↓ promote to the real project
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Install locally
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m venv .venv
|
|
100
|
+
. .venv/bin/activate
|
|
101
|
+
pip install -e .[dev]
|
|
102
|
+
vico --help
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## First run
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
vico init .
|
|
109
|
+
vico freeze . --name baseline
|
|
110
|
+
vico capsule create . --name menu-icons --domain menu --include "examples/frontend_view/src/**" --route /menu-icons
|
|
111
|
+
vico capsule blueprint menu-icons --print
|
|
112
|
+
vico capsule iterate menu-icons --steps 3 --goal "Add preview, confidence and reason fields"
|
|
113
|
+
vico capsule export-prompt menu-icons
|
|
114
|
+
vico capsule verify menu-icons
|
|
115
|
+
vico capsule diff menu-icons
|
|
116
|
+
vico capsule drift menu-icons
|
|
117
|
+
vico capsule promote menu-icons --dry-run
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Important folders
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
src/vico/ Python package
|
|
124
|
+
docs/ documentation
|
|
125
|
+
examples/ runnable example projects
|
|
126
|
+
tests/ unit tests
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Documentation
|
|
130
|
+
|
|
131
|
+
Start here:
|
|
132
|
+
|
|
133
|
+
- [Docs index](docs/README.md)
|
|
134
|
+
- [Getting started](docs/getting-started.md)
|
|
135
|
+
- [Architecture](docs/architecture.md)
|
|
136
|
+
- [Commands](docs/commands.md)
|
|
137
|
+
- [Capsule format](docs/capsule-format.md)
|
|
138
|
+
- [Intent contracts](docs/intent-contracts.md)
|
|
139
|
+
- [Verification model](docs/verification.md)
|
|
140
|
+
- [Examples](docs/examples.md)
|
|
141
|
+
- [Roadmap](docs/roadmap.md)
|
|
142
|
+
|
|
143
|
+
## Main commands
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
vico init .
|
|
147
|
+
vico freeze . --name baseline
|
|
148
|
+
vico capsule create . --name my-slice --include "src/my_module/**"
|
|
149
|
+
vico capsule list
|
|
150
|
+
vico capsule status my-slice
|
|
151
|
+
vico capsule blueprint my-slice
|
|
152
|
+
vico capsule iterate my-slice --steps 10 --goal "Evolve final screen"
|
|
153
|
+
vico capsule export-prompt my-slice
|
|
154
|
+
vico capsule diff my-slice
|
|
155
|
+
vico capsule drift my-slice
|
|
156
|
+
vico capsule verify my-slice
|
|
157
|
+
vico capsule promote my-slice --dry-run
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
Licensed under Apache-2.0.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
vico/__init__.py,sha256=Za4pJa4NUfmTkuozhc_cumhz9R20gJreE1o6APfdQV8,85
|
|
2
|
+
vico/__main__.py,sha256=Qd-f8z2Q2vpiEP2x6PBFsJrpACWDVxFKQk820MhFmHo,59
|
|
3
|
+
vico/blueprint.py,sha256=gZZaQ8sQFPcIggVk2Kkct3Fptu5H-kmwVBkYTZW8FcA,2308
|
|
4
|
+
vico/capsule.py,sha256=BhlldQS1_bxM_YAMiFvp6RcRiUBIZuI-GOBrvvxDgd8,4139
|
|
5
|
+
vico/cli.py,sha256=5omthXaj39M0_uIdspPJrUjUtcHI3AMzGFxy8Gqk74M,9321
|
|
6
|
+
vico/diff.py,sha256=PgEudYl0PUwDvo3AWGMcHKXl4qbHyqM8tN63BQOyhRA,1163
|
|
7
|
+
vico/drift.py,sha256=e6Q9-8oZiAWxrWFcOdEBC3JCacT3k_2Kbrg_GmmotmU,1279
|
|
8
|
+
vico/export_prompt.py,sha256=g5imi0ZbdLl37ri5_9rcut3P5Gq5EskOiTAKydZd5yo,3068
|
|
9
|
+
vico/files.py,sha256=FTw6RoVA2ZqVVifBXbSYJQjt0WEdDkkpWl4EO38k3fA,1400
|
|
10
|
+
vico/freeze.py,sha256=hV35Rc60jloe2Blkb_eefgL5Knl6wi7Mfo1DCig2VKE,857
|
|
11
|
+
vico/git.py,sha256=nNb5NK742864BsPLlIERrQ5BmwryuPv5AnZ6u839fH0,528
|
|
12
|
+
vico/hashing.py,sha256=YWfXowKAujajxLccu6xLEJf6WhB5kI-NhnmAdU12fKA,406
|
|
13
|
+
vico/init_project.py,sha256=nNPwerrnPQ-NABWNMUgdmt_ernN-Ov7FCDUqyEbZo_k,2217
|
|
14
|
+
vico/intract.py,sha256=T3Oc-QKYqaqfSNEWcslVHEwosLTKlnxu_AM8g3DsHwM,4325
|
|
15
|
+
vico/iterate.py,sha256=Y1vPfuJxUV1tg9lR5obY6Fm9X4KOqmiwTmgmeKj4xTQ,1774
|
|
16
|
+
vico/models.py,sha256=i3cp43SARCJmWOt6MoSCRf-fcE4Rn66zFeUWOA8lSkc,4480
|
|
17
|
+
vico/paths.py,sha256=Ntmgw7Rdy6OiF2SRM3BwDQEz8Kgavg2U04lHTGj81tg,766
|
|
18
|
+
vico/promote.py,sha256=6_97KK4L_PEbBcjaxXYI3VXWJW7lKdwB-Ec1zcwwbEc,1079
|
|
19
|
+
vico/status.py,sha256=_-2Mi1zWQU32-beMb1LL7xu8B3Dwtydgcw4zkbUI5ac,1245
|
|
20
|
+
vico/verify.py,sha256=cSp4ML67sa9sGKkIZWh-E1SSidahjOv0mukKOlL0gAM,8356
|
|
21
|
+
nexu-0.2.5.dist-info/METADATA,sha256=ozAQj9BC9_vdVb7styWhdDBwUn1DoX5j0YjXhtHeleY,5162
|
|
22
|
+
nexu-0.2.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
23
|
+
nexu-0.2.5.dist-info/entry_points.txt,sha256=k2shiqY3NKmRNxX2naq1rXE4vuoxhqhZ2AaFI2YwWbc,38
|
|
24
|
+
nexu-0.2.5.dist-info/licenses/LICENSE,sha256=LohVyrbMSdy_MRztx2hmoCEm6khRfo7doDiZ3SpJCNs,600
|
|
25
|
+
nexu-0.2.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
|
|
4
|
+
Copyright 2026 Vico contributors
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
you may not use this file except in compliance with the License.
|
|
8
|
+
You may obtain a copy of the License at
|
|
9
|
+
|
|
10
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
See the License for the specific language governing permissions and
|
|
16
|
+
limitations under the License.
|
vico/__init__.py
ADDED
vico/__main__.py
ADDED
vico/blueprint.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .capsule import load_capsule
|
|
6
|
+
from .intract import read_manifest_contracts
|
|
7
|
+
from .models import utc_now, write_yaml
|
|
8
|
+
from .paths import capsule_dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_blueprint(root: Path, name: str) -> dict:
|
|
12
|
+
capsule = load_capsule(root, name)
|
|
13
|
+
base = capsule_dir(root, name)
|
|
14
|
+
contracts = read_manifest_contracts(base / capsule.contracts_manifest)
|
|
15
|
+
routes = capsule.selection.routes
|
|
16
|
+
endpoints = capsule.selection.endpoints
|
|
17
|
+
|
|
18
|
+
screens = []
|
|
19
|
+
for route in routes or [f"/capsules/{name}"]:
|
|
20
|
+
screens.append(
|
|
21
|
+
{
|
|
22
|
+
"route": route,
|
|
23
|
+
"layout": "capsule_preview",
|
|
24
|
+
"sections": [
|
|
25
|
+
{"id": "intent_summary", "type": "summary"},
|
|
26
|
+
{"id": "input_fixtures", "type": "data_panel"},
|
|
27
|
+
{"id": "preview", "type": "mock_surface"},
|
|
28
|
+
{"id": "evidence", "type": "verification_panel"},
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
api = []
|
|
34
|
+
for endpoint in endpoints:
|
|
35
|
+
method, _, path = endpoint.partition(":")
|
|
36
|
+
api.append(
|
|
37
|
+
{
|
|
38
|
+
"method": method or "GET",
|
|
39
|
+
"path": path or endpoint,
|
|
40
|
+
"kind": "mock_endpoint",
|
|
41
|
+
"response": {"status": "ok", "source": "capsule_fixture"},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
blueprint = {
|
|
46
|
+
"version": "vico.blueprint.v1",
|
|
47
|
+
"capsule": name,
|
|
48
|
+
"created_at": utc_now(),
|
|
49
|
+
"type": capsule.type,
|
|
50
|
+
"domain": capsule.selection.domain,
|
|
51
|
+
"contracts": [
|
|
52
|
+
{
|
|
53
|
+
"id": contract.contract_id,
|
|
54
|
+
"intent": contract.intent,
|
|
55
|
+
"input": contract.input,
|
|
56
|
+
"output": contract.output,
|
|
57
|
+
"effect": contract.effect,
|
|
58
|
+
"forbid": contract.forbid,
|
|
59
|
+
}
|
|
60
|
+
for contract in contracts
|
|
61
|
+
],
|
|
62
|
+
"ui": {"screens": screens},
|
|
63
|
+
"api": {"endpoints": api},
|
|
64
|
+
"tests": {
|
|
65
|
+
"suggested": [
|
|
66
|
+
"contract_outputs_are_present",
|
|
67
|
+
"forbidden_effects_are_absent",
|
|
68
|
+
"capsule_promotion_plan_is_reviewable",
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
write_yaml(base / "blueprints" / "blueprint.yaml", blueprint)
|
|
73
|
+
return blueprint
|
vico/capsule.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .files import collect_files, rel
|
|
7
|
+
from .git import current_git_sha
|
|
8
|
+
from .hashing import sha256_file
|
|
9
|
+
from .models import Capsule, CapsuleSelection, read_yaml, utc_now, write_yaml
|
|
10
|
+
from .paths import capsule_dir, capsules_dir, ensure_project_dirs
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_contract_manifest(capsule: Capsule) -> dict:
|
|
14
|
+
intent = f"evolve:{capsule.name.replace('-', '_')}"
|
|
15
|
+
return {
|
|
16
|
+
"version": "intract.v1",
|
|
17
|
+
"project": {"name": capsule.name, "intent": intent},
|
|
18
|
+
"contracts": [
|
|
19
|
+
{
|
|
20
|
+
"id": f"capsule.{capsule.name}.main",
|
|
21
|
+
"scope": "module",
|
|
22
|
+
"intent": intent,
|
|
23
|
+
"priority": 2,
|
|
24
|
+
"domain": capsule.selection.domain,
|
|
25
|
+
"input": ["frozen_slice", "fixtures", "user_goal"],
|
|
26
|
+
"output": ["evolved_capsule", "promotion_plan", "evidence_map"],
|
|
27
|
+
"effect": ["read"],
|
|
28
|
+
"forbid": ["destructive_write", "secret_leak"],
|
|
29
|
+
"require": ["validate.contracts", "verify.capsule"],
|
|
30
|
+
"validate": ["input_presence", "output_presence", "no_forbidden_effect"],
|
|
31
|
+
"meaning": "evolve this isolated project slice without mutating the source project",
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_capsule(
|
|
38
|
+
root: Path,
|
|
39
|
+
name: str,
|
|
40
|
+
*,
|
|
41
|
+
domain: str = "general",
|
|
42
|
+
include: list[str] | None = None,
|
|
43
|
+
exclude: list[str] | None = None,
|
|
44
|
+
routes: list[str] | None = None,
|
|
45
|
+
endpoints: list[str] | None = None,
|
|
46
|
+
snapshot_id: str | None = None,
|
|
47
|
+
) -> Capsule:
|
|
48
|
+
ensure_project_dirs(root)
|
|
49
|
+
include = include or ["src/**", "contracts/**", "frontend/**", "backend/**", "shared/**", "*.py", "*.md"]
|
|
50
|
+
selection = CapsuleSelection(
|
|
51
|
+
domain=domain,
|
|
52
|
+
routes=routes or [],
|
|
53
|
+
endpoints=endpoints or [],
|
|
54
|
+
include=include,
|
|
55
|
+
exclude=exclude or [],
|
|
56
|
+
)
|
|
57
|
+
target = capsule_dir(root, name)
|
|
58
|
+
if target.exists():
|
|
59
|
+
raise FileExistsError(f"Capsule already exists: {target}")
|
|
60
|
+
|
|
61
|
+
copied: list[str] = []
|
|
62
|
+
baseline_files: dict[str, str] = {}
|
|
63
|
+
capsule = Capsule(
|
|
64
|
+
name=name,
|
|
65
|
+
source_project_root=str(root),
|
|
66
|
+
source_snapshot_id=snapshot_id,
|
|
67
|
+
source_git_sha=current_git_sha(root),
|
|
68
|
+
selection=selection,
|
|
69
|
+
baseline_files=baseline_files,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for subdir in [
|
|
73
|
+
target / "src",
|
|
74
|
+
target / "fixtures",
|
|
75
|
+
target / "iterations" / "S0",
|
|
76
|
+
target / "evidence",
|
|
77
|
+
target / "prompts",
|
|
78
|
+
target / "blueprints",
|
|
79
|
+
]:
|
|
80
|
+
subdir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
for source in collect_files(root, include=include, exclude=selection.exclude):
|
|
83
|
+
relative = rel(source, root)
|
|
84
|
+
destination = target / "src" / relative
|
|
85
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
shutil.copy2(source, destination)
|
|
87
|
+
copied.append(relative)
|
|
88
|
+
baseline_files[relative] = sha256_file(source)
|
|
89
|
+
|
|
90
|
+
write_yaml(target / "capsule.yaml", capsule.to_dict())
|
|
91
|
+
write_yaml(target / "intract.yaml", default_contract_manifest(capsule))
|
|
92
|
+
write_yaml(
|
|
93
|
+
target / "iterations" / "S0" / "state.yaml",
|
|
94
|
+
{
|
|
95
|
+
"state": "S0",
|
|
96
|
+
"created_at": utc_now(),
|
|
97
|
+
"description": "Frozen capsule baseline copied from source project.",
|
|
98
|
+
"copied_files": copied,
|
|
99
|
+
"baseline_lock": {
|
|
100
|
+
"source_git_sha": capsule.source_git_sha,
|
|
101
|
+
"source_snapshot_id": capsule.source_snapshot_id,
|
|
102
|
+
"file_count": len(baseline_files),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
return capsule
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def list_capsules(root: Path) -> list[str]:
|
|
110
|
+
base = capsules_dir(root)
|
|
111
|
+
if not base.exists():
|
|
112
|
+
return []
|
|
113
|
+
return sorted(path.name for path in base.iterdir() if path.is_dir())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_capsule(root: Path, name: str) -> Capsule:
|
|
117
|
+
data = read_yaml(capsule_dir(root, name) / "capsule.yaml")
|
|
118
|
+
return Capsule.from_dict(data)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def save_capsule(root: Path, capsule: Capsule) -> None:
|
|
122
|
+
write_yaml(capsule_dir(root, capsule.name) / "capsule.yaml", capsule.to_dict())
|
vico/cli.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
|
|
11
|
+
from .blueprint import build_blueprint
|
|
12
|
+
from .capsule import create_capsule, list_capsules
|
|
13
|
+
from .diff import diff_capsule
|
|
14
|
+
from .drift import check_source_drift
|
|
15
|
+
from .export_prompt import export_iteration_prompt
|
|
16
|
+
from .freeze import freeze_project
|
|
17
|
+
from .init_project import init_project
|
|
18
|
+
from .iterate import iterate_capsule
|
|
19
|
+
from .paths import project_root
|
|
20
|
+
from .promote import build_promotion_plan
|
|
21
|
+
from .status import capsule_status
|
|
22
|
+
from .verify import verify_capsule
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(help="Vico — Visual Intent Contract Orchestrator.")
|
|
25
|
+
capsule_app = typer.Typer(help="Create, iterate, verify and promote project capsules.")
|
|
26
|
+
app.add_typer(capsule_app, name="capsule")
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def init(path: Annotated[str, typer.Argument(help="Project root.")] = ".") -> None:
|
|
32
|
+
"""Initialize Vico files in a project."""
|
|
33
|
+
root = project_root(path)
|
|
34
|
+
created = init_project(root)
|
|
35
|
+
if not created:
|
|
36
|
+
console.print("[green]Vico already initialized.[/green]")
|
|
37
|
+
return
|
|
38
|
+
for item in created:
|
|
39
|
+
console.print(f"[green]created[/green] {item.relative_to(root)}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def freeze(
|
|
44
|
+
path: Annotated[str, typer.Argument(help="Project root.")] = ".",
|
|
45
|
+
name: Annotated[str, typer.Option("--name", "-n", help="Snapshot name.")] = "baseline",
|
|
46
|
+
include: Annotated[list[str] | None, typer.Option("--include", "-i", help="Glob include.")] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Freeze a lightweight hash snapshot of the current project."""
|
|
49
|
+
root = project_root(path)
|
|
50
|
+
snapshot = freeze_project(root, name=name, include=include)
|
|
51
|
+
console.print(f"[green]snapshot[/green] {snapshot.id}")
|
|
52
|
+
console.print(f"files: {len(snapshot.files)}")
|
|
53
|
+
if snapshot.git_sha:
|
|
54
|
+
console.print(f"git: {snapshot.git_sha[:12]}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@capsule_app.command("create")
|
|
58
|
+
def capsule_create(
|
|
59
|
+
path: Annotated[str, typer.Argument(help="Project root.")] = ".",
|
|
60
|
+
name: Annotated[str, typer.Option("--name", "-n", help="Capsule name.")] = "capsule",
|
|
61
|
+
domain: Annotated[str, typer.Option("--domain", "-d", help="Domain name.")] = "general",
|
|
62
|
+
include: Annotated[list[str] | None, typer.Option("--include", "-i", help="Glob include pattern.")] = None,
|
|
63
|
+
route: Annotated[list[str] | None, typer.Option("--route", help="UI route covered by capsule.")] = None,
|
|
64
|
+
endpoint: Annotated[list[str] | None, typer.Option("--endpoint", help="Endpoint covered by capsule.")] = None,
|
|
65
|
+
snapshot: Annotated[str | None, typer.Option("--snapshot", help="Source snapshot id.")] = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Create an isolated capsule from selected project files."""
|
|
68
|
+
root = project_root(path)
|
|
69
|
+
capsule = create_capsule(
|
|
70
|
+
root,
|
|
71
|
+
name,
|
|
72
|
+
domain=domain,
|
|
73
|
+
include=include,
|
|
74
|
+
routes=route,
|
|
75
|
+
endpoints=endpoint,
|
|
76
|
+
snapshot_id=snapshot,
|
|
77
|
+
)
|
|
78
|
+
build_blueprint(root, capsule.name)
|
|
79
|
+
console.print(f"[green]capsule created[/green] {capsule.name}")
|
|
80
|
+
console.print(f"location: .vico/capsules/{capsule.name}")
|
|
81
|
+
console.print(f"baseline files: {len(capsule.baseline_files)}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@capsule_app.command("list")
|
|
85
|
+
def capsule_list(path: Annotated[str, typer.Argument(help="Project root.")] = ".") -> None:
|
|
86
|
+
"""List local capsules."""
|
|
87
|
+
root = project_root(path)
|
|
88
|
+
names = list_capsules(root)
|
|
89
|
+
if not names:
|
|
90
|
+
console.print("[yellow]No capsules found.[/yellow]")
|
|
91
|
+
return
|
|
92
|
+
table = Table("Capsule")
|
|
93
|
+
for name in names:
|
|
94
|
+
table.add_row(name)
|
|
95
|
+
console.print(table)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@capsule_app.command("status")
|
|
99
|
+
def capsule_status_command(
|
|
100
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
101
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Show capsule status, latest iteration, diff counters and verification summary."""
|
|
104
|
+
root = project_root(path)
|
|
105
|
+
status = capsule_status(root, name)
|
|
106
|
+
console.print(f"[bold]{status['name']}[/bold] ({status['type']})")
|
|
107
|
+
console.print(f"domain: {status['domain']}")
|
|
108
|
+
console.print(f"latest iteration: {status['latest_iteration']}")
|
|
109
|
+
files = status["files"]
|
|
110
|
+
table = Table("Counter", "Value")
|
|
111
|
+
for key, value in files.items():
|
|
112
|
+
table.add_row(key, str(value))
|
|
113
|
+
console.print(table)
|
|
114
|
+
if status.get("verification"):
|
|
115
|
+
verification = status["verification"]
|
|
116
|
+
console.print(f"verification: {verification.get('status')} score={verification.get('score')}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@capsule_app.command("iterate")
|
|
120
|
+
def capsule_iterate(
|
|
121
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
122
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
123
|
+
steps: Annotated[int, typer.Option("--steps", "-s", help="How many planned iterations to create.")] = 1,
|
|
124
|
+
goal: Annotated[str, typer.Option("--goal", "-g", help="Iteration goal.")] = "Evolve capsule safely.",
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Create planned S1..Sn iteration folders and prompts."""
|
|
127
|
+
root = project_root(path)
|
|
128
|
+
created = iterate_capsule(root, name, steps=steps, goal=goal)
|
|
129
|
+
console.print(f"[green]created iterations[/green] {', '.join(created)}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@capsule_app.command("blueprint")
|
|
133
|
+
def capsule_blueprint(
|
|
134
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
135
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
136
|
+
print_yaml: Annotated[bool, typer.Option("--print/--no-print", help="Print generated YAML.")] = False,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Generate a UI/API/test blueprint from capsule selection and Intract contracts."""
|
|
139
|
+
root = project_root(path)
|
|
140
|
+
blueprint = build_blueprint(root, name)
|
|
141
|
+
console.print(f"[green]blueprint[/green] .vico/capsules/{name}/blueprints/blueprint.yaml")
|
|
142
|
+
if print_yaml:
|
|
143
|
+
import yaml
|
|
144
|
+
|
|
145
|
+
console.print(Syntax(yaml.safe_dump(blueprint, sort_keys=False, allow_unicode=True), "yaml"))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@capsule_app.command("export-prompt")
|
|
149
|
+
def capsule_export_prompt(
|
|
150
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
151
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
152
|
+
iteration: Annotated[str | None, typer.Option("--iteration", "-i", help="Iteration id, e.g. S3.")] = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Export an LLM-ready prompt constrained by capsule contracts and blueprint."""
|
|
155
|
+
root = project_root(path)
|
|
156
|
+
export = export_iteration_prompt(root, name, iteration=iteration)
|
|
157
|
+
console.print(f"[green]prompt exported[/green] {Path(export.path).relative_to(root)}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@capsule_app.command("diff")
|
|
161
|
+
def capsule_diff(
|
|
162
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
163
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Compare capsule src files against the frozen baseline lock."""
|
|
166
|
+
root = project_root(path)
|
|
167
|
+
report = diff_capsule(root, name)
|
|
168
|
+
table = Table("Kind", "Count")
|
|
169
|
+
table.add_row("added", str(len(report.added)))
|
|
170
|
+
table.add_row("modified", str(len(report.modified)))
|
|
171
|
+
table.add_row("deleted", str(len(report.deleted)))
|
|
172
|
+
table.add_row("unchanged", str(len(report.unchanged)))
|
|
173
|
+
console.print(table)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@capsule_app.command("drift")
|
|
177
|
+
def capsule_drift(
|
|
178
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
179
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Check whether the original source files changed since capsule creation."""
|
|
182
|
+
root = project_root(path)
|
|
183
|
+
report = check_source_drift(root, name)
|
|
184
|
+
color = "green" if report["status"] == "pass" else "yellow"
|
|
185
|
+
console.print(f"status: [{color}]{report['status']}[/]")
|
|
186
|
+
console.print(f"changed: {len(report['changed'])}")
|
|
187
|
+
console.print(f"missing: {len(report['missing'])}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@capsule_app.command("verify")
|
|
191
|
+
def capsule_verify(
|
|
192
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
193
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Verify a capsule against basic intent-contract gates."""
|
|
196
|
+
root = project_root(path)
|
|
197
|
+
report = verify_capsule(root, name)
|
|
198
|
+
color = "green" if report.status == "pass" else "red" if report.status == "fail" else "yellow"
|
|
199
|
+
console.print(f"status: [{color}]{report.status}[/]")
|
|
200
|
+
console.print(f"score: {report.score:.2f}")
|
|
201
|
+
table = Table("Gate", "Status", "Message")
|
|
202
|
+
for finding in report.findings:
|
|
203
|
+
table.add_row(finding.code, finding.status, finding.message)
|
|
204
|
+
console.print(table)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@capsule_app.command("promote")
|
|
208
|
+
def capsule_promote(
|
|
209
|
+
name: Annotated[str, typer.Argument(help="Capsule name.")],
|
|
210
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Project root.")] = ".",
|
|
211
|
+
dry_run: Annotated[bool, typer.Option("--dry-run/--apply", help="Only create a promotion plan.")] = True,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Build a promotion plan for copying capsule changes back to the source project."""
|
|
214
|
+
root = project_root(path)
|
|
215
|
+
plan = build_promotion_plan(root, name)
|
|
216
|
+
console.print(f"[green]promotion plan[/green] .vico/capsules/{name}/promotion-plan.yaml")
|
|
217
|
+
console.print(f"files to review: {len(plan['files_to_review'])}")
|
|
218
|
+
if not dry_run:
|
|
219
|
+
console.print("[yellow]Apply mode is intentionally not implemented in MVP. Review the plan first.[/yellow]")
|
vico/diff.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .capsule import load_capsule
|
|
6
|
+
from .files import collect_files, rel
|
|
7
|
+
from .hashing import sha256_file
|
|
8
|
+
from .models import CapsuleDiff, write_yaml
|
|
9
|
+
from .paths import capsule_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def diff_capsule(root: Path, name: str) -> CapsuleDiff:
|
|
13
|
+
capsule = load_capsule(root, name)
|
|
14
|
+
base = capsule_dir(root, name)
|
|
15
|
+
src = base / "src"
|
|
16
|
+
current: dict[str, str] = {
|
|
17
|
+
rel(path, src): sha256_file(path)
|
|
18
|
+
for path in collect_files(src)
|
|
19
|
+
}
|
|
20
|
+
baseline = capsule.baseline_files
|
|
21
|
+
|
|
22
|
+
added = sorted(path for path in current if path not in baseline)
|
|
23
|
+
deleted = sorted(path for path in baseline if path not in current)
|
|
24
|
+
modified = sorted(path for path, digest in current.items() if path in baseline and baseline[path] != digest)
|
|
25
|
+
unchanged = sorted(path for path, digest in current.items() if path in baseline and baseline[path] == digest)
|
|
26
|
+
|
|
27
|
+
report = CapsuleDiff(
|
|
28
|
+
capsule=name,
|
|
29
|
+
added=added,
|
|
30
|
+
modified=modified,
|
|
31
|
+
deleted=deleted,
|
|
32
|
+
unchanged=unchanged,
|
|
33
|
+
)
|
|
34
|
+
write_yaml(base / "evidence" / "diff.yaml", report.to_dict())
|
|
35
|
+
return report
|