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.
@@ -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
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.5-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
40
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.51-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vico = vico.cli:app
@@ -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
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.2.5"
4
+
5
+ __all__ = ["__version__"]
vico/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
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