cox-protocol-package 1.0.2 → 1.2.0
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.
- package/CHANGELOG.md +22 -0
- package/COX_FILE_FORMAT_SPECIFICATION_v1.0.md +40 -200
- package/FINALIZATION.md +50 -0
- package/LocalAppManager/Dashboard/analytics_sidebar_panels/analytics_sidebar_panels.cox +56 -0
- package/LocalAppManager/Dashboard/analytics_sidebar_panels/analytics_sidebar_panels_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/dashboard.cox +62 -0
- package/LocalAppManager/Dashboard/dashboard_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/left_navigation_sidebar/left_navigation_sidebar.cox +56 -0
- package/LocalAppManager/Dashboard/left_navigation_sidebar/left_navigation_sidebar_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/projects_grid_cards/projects_grid_cards.cox +56 -0
- package/LocalAppManager/Dashboard/projects_grid_cards/projects_grid_cards_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/top_header_stats_bar/top_header_stats_bar.cox +56 -0
- package/LocalAppManager/Dashboard/top_header_stats_bar/top_header_stats_bar_screenshot.placeholder.txt +1 -0
- package/MANIFEST.in +20 -0
- package/README.md +67 -154
- package/bin/cox-protocol.js +23 -97
- package/cox_protocol/__init__.py +2 -0
- package/cox_protocol/__main__.py +3 -0
- package/cox_protocol/compiler.py +273 -0
- package/cox_protocol/cox_cli.py +146 -0
- package/cox_protocol/cox_lib.py +599 -0
- package/cox_protocol/package_target.py +262 -0
- package/cox_protocol/react_target.py +306 -0
- package/cox_protocol/scaffold.py +56 -0
- package/cox_protocol/schemas/cox.component.v1.schema.json +180 -0
- package/cox_protocol/schemas/cox.crop_metadata.v1.schema.json +83 -0
- package/cox_protocol/schemas/cox.manifest.v1.schema.json +172 -0
- package/cox_protocol/schemas/cox.page.v1.schema.json +242 -0
- package/cox_protocol/schemas/cox.protocol.v1.schema.json +38 -0
- package/cox_protocol/starter/README.md +7 -0
- package/cox_protocol/starter/manifest.cox +26 -0
- package/cox_protocol/starter/page/action_button/action_button.cox +34 -0
- package/cox_protocol/starter/page/action_button/reference.svg +1 -0
- package/cox_protocol/starter/page/page.cox +39 -0
- package/cox_protocol/starter/page/reference.svg +1 -0
- package/editor-support/vscode/CHANGELOG.md +6 -0
- package/editor-support/vscode/README.md +2 -1
- package/editor-support/vscode/package.json +3 -3
- package/editor-support/vscode/snippets/cox-common.json +1 -1
- package/editor-support/vscode/snippets/cox-component.json +2 -1
- package/editor-support/vscode/snippets/cox-crop-metadata.json +1 -1
- package/editor-support/vscode/snippets/cox-manifest.json +1 -1
- package/editor-support/vscode/snippets/cox-page.json +2 -1
- package/editor-support/vscode/snippets/cox-protocol.json +1 -1
- package/examples/quickstart/README.md +7 -0
- package/examples/quickstart/manifest.cox +26 -0
- package/examples/quickstart/page/action_button/action_button.cox +34 -0
- package/examples/quickstart/page/action_button/reference.svg +1 -0
- package/examples/quickstart/page/page.cox +39 -0
- package/examples/quickstart/page/reference.svg +1 -0
- package/guides/contract-demo.md +24 -0
- package/guides/npm-installation.md +16 -287
- package/manifest.cox +7 -4
- package/package.json +30 -14
- package/protocols/recursive_decomposition_protocol.cox +8 -8
- package/pyproject.toml +17 -6
- package/schemas/cox.component.v1.schema.json +17 -4
- package/schemas/cox.crop_metadata.v1.schema.json +1 -1
- package/schemas/cox.manifest.v1.schema.json +1 -1
- package/schemas/cox.page.v1.schema.json +17 -4
- package/schemas/cox.protocol.v1.schema.json +1 -1
- package/scripts/check_distribution.py +87 -0
- package/scripts/demo.py +62 -0
- package/scripts/package-vscode.mjs +12 -0
- package/scripts/release_check.py +86 -0
- package/tests/conftest.py +21 -0
- package/tests/test_cli_and_typescript.py +123 -0
- package/tests/test_compiler.py +279 -0
- package/tests/test_validation.py +215 -0
- package/tools/__init__.py +2 -2
- package/tools/cox_cli.py +2 -123
- package/tools/cox_lib.py +2 -558
- package/tools/package_target.py +2 -214
- package/tools/react_target.py +2 -278
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import pytest
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
from cox_protocol.cox_cli import app
|
|
7
|
+
from cox_protocol.cox_lib import (
|
|
8
|
+
CoxError,
|
|
9
|
+
read_yaml_document,
|
|
10
|
+
schema_issues,
|
|
11
|
+
validate_project,
|
|
12
|
+
)
|
|
13
|
+
from cox_protocol.scaffold import init_project
|
|
14
|
+
from conftest import ROOT, change
|
|
15
|
+
|
|
16
|
+
runner = CliRunner()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def code(exc):
|
|
20
|
+
return exc.value.issues[0].code
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_self_contained_starter_and_repository(project):
|
|
24
|
+
assert not (project / "schemas").exists()
|
|
25
|
+
assert len(validate_project(project).documents) == 3
|
|
26
|
+
assert len(validate_project(ROOT).documents) >= 17
|
|
27
|
+
for source in (ROOT / "schemas").glob("*.json"):
|
|
28
|
+
assert (
|
|
29
|
+
source.read_bytes()
|
|
30
|
+
== (ROOT / "cox_protocol/schemas" / source.name).read_bytes()
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize(
|
|
35
|
+
"body,expected",
|
|
36
|
+
[
|
|
37
|
+
("[broken", "YAML_INVALID"),
|
|
38
|
+
("[]", "DOCUMENT_TYPE"),
|
|
39
|
+
("hello: one\nhello: two", "YAML_INVALID"),
|
|
40
|
+
("hello: &a [*a]", "YAML_INVALID"),
|
|
41
|
+
("hello: 2026-01-01", "YAML_INVALID"),
|
|
42
|
+
("hello: .nan", "YAML_INVALID"),
|
|
43
|
+
("true: value", "YAML_INVALID"),
|
|
44
|
+
("hello: !!python/object:object {}", "YAML_INVALID"),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
def test_invalid_documents_never_traceback(tmp_path, body, expected):
|
|
48
|
+
p = tmp_path / "bad.cox"
|
|
49
|
+
p.write_text(body)
|
|
50
|
+
with pytest.raises(CoxError) as exc:
|
|
51
|
+
read_yaml_document(p)
|
|
52
|
+
assert code(exc) == expected
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_utf8(tmp_path):
|
|
56
|
+
p = tmp_path / "bad.cox"
|
|
57
|
+
p.write_bytes(b"\xff")
|
|
58
|
+
with pytest.raises(CoxError) as exc:
|
|
59
|
+
read_yaml_document(p)
|
|
60
|
+
assert code(exc) == "UTF8_INVALID"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize(
|
|
64
|
+
"key,value",
|
|
65
|
+
[
|
|
66
|
+
("Pages", None),
|
|
67
|
+
("Pages", []),
|
|
68
|
+
("CoxVersion", 1),
|
|
69
|
+
("Schema", "invented"),
|
|
70
|
+
("FileType", "component_spec"),
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
def test_bad_manifest_is_diagnostic(project, key, value):
|
|
74
|
+
change(project, "manifest.cox", lambda d: d.update({key: value}))
|
|
75
|
+
result = runner.invoke(app, ["validate", str(project), "--json"])
|
|
76
|
+
assert result.exit_code == 1, result.output
|
|
77
|
+
assert json.loads(result.output)["ok"] is False
|
|
78
|
+
assert "Traceback" not in result.output
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
"path",
|
|
83
|
+
["../outside", "/tmp/outside", "C:\\outside", "page/../../outside", "missing.md"],
|
|
84
|
+
)
|
|
85
|
+
def test_reference_boundaries(project, path):
|
|
86
|
+
change(project, "manifest.cox", lambda d: d.update(ReferenceDocs=[path]))
|
|
87
|
+
with pytest.raises(CoxError):
|
|
88
|
+
validate_project(project)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_symlink_reference(project, tmp_path):
|
|
92
|
+
other = tmp_path / "other"
|
|
93
|
+
other.write_text("outside")
|
|
94
|
+
(project / "link").symlink_to(other)
|
|
95
|
+
change(project, "manifest.cox", lambda d: d.update(ReferenceDocs=["link"]))
|
|
96
|
+
with pytest.raises(CoxError) as exc:
|
|
97
|
+
validate_project(project)
|
|
98
|
+
assert code(exc) == "PATH_SYMLINK"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize(
|
|
102
|
+
"key,value", [("Slug", "wrong"), ("ProjectName", "wrong"), ("PageName", "wrong")]
|
|
103
|
+
)
|
|
104
|
+
def test_page_binding_consistency(project, key, value):
|
|
105
|
+
change(project, "page/page.cox", lambda d: d.update({key: value}))
|
|
106
|
+
with pytest.raises(CoxError) as exc:
|
|
107
|
+
validate_project(project)
|
|
108
|
+
assert code(exc) == "BINDING_MISMATCH"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_shared_component_outside_page_directory(project):
|
|
112
|
+
shutil.move(str(project / "page/action_button"), project / "shared")
|
|
113
|
+
|
|
114
|
+
def update(d):
|
|
115
|
+
c = d["Pages"][0]["Components"][0]
|
|
116
|
+
c.update(
|
|
117
|
+
Folder="shared",
|
|
118
|
+
ComponentInstruction="shared/action_button.cox",
|
|
119
|
+
ComponentScreenshotPlaceholder="shared/reference.svg",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
change(project, "manifest.cox", update)
|
|
123
|
+
change(
|
|
124
|
+
project,
|
|
125
|
+
"page/page.cox",
|
|
126
|
+
lambda d: d["Children"]["Components"][0].update(Folder="../shared"),
|
|
127
|
+
)
|
|
128
|
+
assert len(validate_project(project).documents) == 3
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_duplicate_components_and_evidence_typos(project):
|
|
132
|
+
change(
|
|
133
|
+
project,
|
|
134
|
+
"manifest.cox",
|
|
135
|
+
lambda d: d["Pages"][0]["Components"].append(
|
|
136
|
+
d["Pages"][0]["Components"][0].copy()
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
with pytest.raises(CoxError) as exc:
|
|
140
|
+
validate_project(project)
|
|
141
|
+
assert code(exc) == "SLUG_DUPLICATE"
|
|
142
|
+
change(project, "manifest.cox", lambda d: d["Pages"][0]["Components"].pop())
|
|
143
|
+
change(
|
|
144
|
+
project,
|
|
145
|
+
"page/action_button/action_button.cox",
|
|
146
|
+
lambda d: d["EvidenceSummary"].update(Visble=["typo"]),
|
|
147
|
+
)
|
|
148
|
+
with pytest.raises(CoxError) as exc:
|
|
149
|
+
validate_project(project)
|
|
150
|
+
assert code(exc) == "SCHEMA_INVALID"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_protocol_and_crop_document_types(project):
|
|
154
|
+
protocol = {
|
|
155
|
+
"CoxVersion": "1.0",
|
|
156
|
+
"FileType": "protocol_doc",
|
|
157
|
+
"Slug": "guide",
|
|
158
|
+
"Schema": "cox.protocol.v1",
|
|
159
|
+
"ProtocolName": "Guide",
|
|
160
|
+
"Purpose": "Example",
|
|
161
|
+
}
|
|
162
|
+
assert not schema_issues(protocol, Path("guide.cox"))
|
|
163
|
+
import yaml
|
|
164
|
+
|
|
165
|
+
template = (
|
|
166
|
+
(ROOT / "templates/crop_metadata.cox.template")
|
|
167
|
+
.read_text()
|
|
168
|
+
.replace("{component_slug}", "action_button")
|
|
169
|
+
)
|
|
170
|
+
crop = yaml.safe_load(template)
|
|
171
|
+
# Fill authoring placeholders before validating the concrete document.
|
|
172
|
+
assert not schema_issues(crop, Path("crop_metadata.cox"))
|
|
173
|
+
(project / "guide.cox").write_text(yaml.safe_dump(protocol))
|
|
174
|
+
(project / "crop.cox").write_text(yaml.safe_dump(crop))
|
|
175
|
+
change(
|
|
176
|
+
project,
|
|
177
|
+
"manifest.cox",
|
|
178
|
+
lambda d: d.update(ProtocolDocs=["guide.cox"], GuideDocs=["crop.cox"]),
|
|
179
|
+
)
|
|
180
|
+
assert len(validate_project(project).documents) == 5
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_init_and_strict_warnings(tmp_path):
|
|
184
|
+
root = init_project(tmp_path / "new", "New Project")
|
|
185
|
+
assert validate_project(root).warnings == []
|
|
186
|
+
with pytest.raises(CoxError):
|
|
187
|
+
init_project(root)
|
|
188
|
+
original = (root / "manifest.cox").read_bytes()
|
|
189
|
+
(root / "extra.cox").write_text("unreferenced: true")
|
|
190
|
+
result = runner.invoke(app, ["validate", str(root), "--json", "--strict"])
|
|
191
|
+
assert result.exit_code == 1
|
|
192
|
+
assert json.loads(result.output)["errors"][0]["code"] == "WARNINGS_AS_ERRORS"
|
|
193
|
+
assert (root / "manifest.cox").read_bytes() == original
|
|
194
|
+
with pytest.raises(CoxError):
|
|
195
|
+
init_project(tmp_path / "bad", "../bad")
|
|
196
|
+
assert not (tmp_path / "bad").exists()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_directory_collections_cannot_load_symlink_documents(project, tmp_path):
|
|
200
|
+
outside = tmp_path / "outside.cox"
|
|
201
|
+
outside.write_text("This file must never be parsed")
|
|
202
|
+
folder = project / "guides"
|
|
203
|
+
folder.mkdir()
|
|
204
|
+
(folder / "alias.cox").symlink_to(outside)
|
|
205
|
+
change(project, "manifest.cox", lambda d: d.update(GuideDocs=["guides"]))
|
|
206
|
+
with pytest.raises(CoxError) as exc:
|
|
207
|
+
validate_project(project)
|
|
208
|
+
assert code(exc) == "PATH_SYMLINK"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.parametrize("name", ["CON", "aux.txt", "trailing.", " ", "with/slash"])
|
|
212
|
+
def test_init_rejects_nonportable_names(tmp_path, name):
|
|
213
|
+
with pytest.raises(CoxError):
|
|
214
|
+
init_project(tmp_path / "new", name)
|
|
215
|
+
assert not (tmp_path / "new").exists()
|
package/tools/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
from cox_protocol import __version__
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
__all__ = ["__version__"]
|
package/tools/cox_cli.py
CHANGED
|
@@ -1,127 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import typer # type: ignore[import-untyped]
|
|
8
|
-
from rich.console import Console # type: ignore[import-untyped]
|
|
9
|
-
from rich.table import Table # type: ignore[import-untyped]
|
|
10
|
-
|
|
11
|
-
from . import __version__
|
|
12
|
-
from .cox_lib import CoxError, compile_project, validate_project
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
app = typer.Typer(
|
|
16
|
-
add_completion=False,
|
|
17
|
-
no_args_is_help=True,
|
|
18
|
-
help="Validate and compile COX projects.",
|
|
19
|
-
)
|
|
20
|
-
console = Console()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def print_validation_summary(
|
|
24
|
-
project_label: str,
|
|
25
|
-
document_count: int,
|
|
26
|
-
warnings: list[str],
|
|
27
|
-
) -> None:
|
|
28
|
-
table = Table(show_header=False, box=None)
|
|
29
|
-
table.add_row("Project", project_label)
|
|
30
|
-
table.add_row("Validated documents", str(document_count))
|
|
31
|
-
table.add_row("Warnings", str(len(warnings)))
|
|
32
|
-
console.print(table)
|
|
33
|
-
for warning in warnings:
|
|
34
|
-
console.print(f"[yellow]- {warning}[/yellow]")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@app.command("validate")
|
|
38
|
-
def validate_command(
|
|
39
|
-
project_path: Path = typer.Argument(
|
|
40
|
-
Path("."),
|
|
41
|
-
exists=True,
|
|
42
|
-
resolve_path=True,
|
|
43
|
-
),
|
|
44
|
-
) -> None:
|
|
45
|
-
"""Validate all COX documents referenced by manifest.cox."""
|
|
46
|
-
try:
|
|
47
|
-
summary = validate_project(project_path)
|
|
48
|
-
except CoxError as exc:
|
|
49
|
-
console.print(f"[bold red]Validation failed[/bold red]\n{exc}")
|
|
50
|
-
raise typer.Exit(code=1) from exc
|
|
51
|
-
|
|
52
|
-
console.print(
|
|
53
|
-
"[bold green]COX validation passed[/bold green] "
|
|
54
|
-
f"[dim](v{__version__})[/dim]"
|
|
55
|
-
)
|
|
56
|
-
print_validation_summary(
|
|
57
|
-
summary.project_root.as_posix(),
|
|
58
|
-
len(summary.documents),
|
|
59
|
-
summary.warnings,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@app.command("compile")
|
|
64
|
-
def compile_command(
|
|
65
|
-
project_path: Path = typer.Argument(
|
|
66
|
-
Path("."),
|
|
67
|
-
exists=True,
|
|
68
|
-
resolve_path=True,
|
|
69
|
-
),
|
|
70
|
-
out_dir: Path = typer.Option(
|
|
71
|
-
Path("build"),
|
|
72
|
-
"--out",
|
|
73
|
-
"-o",
|
|
74
|
-
resolve_path=False,
|
|
75
|
-
),
|
|
76
|
-
targets: list[str] = typer.Option(
|
|
77
|
-
[],
|
|
78
|
-
"--target",
|
|
79
|
-
"-t",
|
|
80
|
-
help="Compilation target(s) to emit. Repeat the flag or use commas.",
|
|
81
|
-
),
|
|
82
|
-
) -> None:
|
|
83
|
-
"""Validate the project and emit normalized JSON outputs."""
|
|
84
|
-
try:
|
|
85
|
-
result = compile_project(project_path, out_dir, targets=targets)
|
|
86
|
-
except CoxError as exc:
|
|
87
|
-
console.print(f"[bold red]Compilation failed[/bold red]\n{exc}")
|
|
88
|
-
raise typer.Exit(code=1) from exc
|
|
89
|
-
|
|
90
|
-
console.print(
|
|
91
|
-
"[bold green]COX compilation passed[/bold green] "
|
|
92
|
-
f"[dim](v{__version__})[/dim]"
|
|
93
|
-
)
|
|
94
|
-
table = Table(show_header=False, box=None)
|
|
95
|
-
table.add_row("Build root", result["build_root"].as_posix())
|
|
96
|
-
table.add_row("Targets", ", ".join(result["targets"]))
|
|
97
|
-
json_result = result["target_results"].get("json")
|
|
98
|
-
if json_result is not None:
|
|
99
|
-
table.add_row(
|
|
100
|
-
"Normalized documents",
|
|
101
|
-
str(json_result["document_count"]),
|
|
102
|
-
)
|
|
103
|
-
react_result = result["target_results"].get("react")
|
|
104
|
-
if react_result is not None:
|
|
105
|
-
table.add_row("React pages", str(react_result["page_count"]))
|
|
106
|
-
table.add_row(
|
|
107
|
-
"React components",
|
|
108
|
-
str(react_result["component_count"]),
|
|
109
|
-
)
|
|
110
|
-
table.add_row(
|
|
111
|
-
"React manifest",
|
|
112
|
-
react_result["manifest"].as_posix(),
|
|
113
|
-
)
|
|
114
|
-
package_result = result["target_results"].get("package")
|
|
115
|
-
if package_result is not None:
|
|
116
|
-
table.add_row("Package archive", package_result["archive"].as_posix())
|
|
117
|
-
table.add_row("Package files", str(package_result["file_count"]))
|
|
118
|
-
table.add_row("Package sha256", package_result["sha256"])
|
|
119
|
-
table.add_row("Compiled manifest", result["compiled_manifest"].as_posix())
|
|
120
|
-
table.add_row("Verification report", result["report"].as_posix())
|
|
121
|
-
console.print(table)
|
|
122
|
-
for warning in result["warnings"]:
|
|
123
|
-
console.print(f"[yellow]- {warning}[/yellow]")
|
|
1
|
+
"""Compatibility entry point; the installed CLI uses cox_protocol."""
|
|
124
2
|
|
|
3
|
+
from cox_protocol.cox_cli import app
|
|
125
4
|
|
|
126
5
|
if __name__ == "__main__":
|
|
127
6
|
app()
|