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.
Files changed (74) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/COX_FILE_FORMAT_SPECIFICATION_v1.0.md +40 -200
  3. package/FINALIZATION.md +50 -0
  4. package/LocalAppManager/Dashboard/analytics_sidebar_panels/analytics_sidebar_panels.cox +56 -0
  5. package/LocalAppManager/Dashboard/analytics_sidebar_panels/analytics_sidebar_panels_screenshot.placeholder.txt +1 -0
  6. package/LocalAppManager/Dashboard/dashboard.cox +62 -0
  7. package/LocalAppManager/Dashboard/dashboard_screenshot.placeholder.txt +1 -0
  8. package/LocalAppManager/Dashboard/left_navigation_sidebar/left_navigation_sidebar.cox +56 -0
  9. package/LocalAppManager/Dashboard/left_navigation_sidebar/left_navigation_sidebar_screenshot.placeholder.txt +1 -0
  10. package/LocalAppManager/Dashboard/projects_grid_cards/projects_grid_cards.cox +56 -0
  11. package/LocalAppManager/Dashboard/projects_grid_cards/projects_grid_cards_screenshot.placeholder.txt +1 -0
  12. package/LocalAppManager/Dashboard/top_header_stats_bar/top_header_stats_bar.cox +56 -0
  13. package/LocalAppManager/Dashboard/top_header_stats_bar/top_header_stats_bar_screenshot.placeholder.txt +1 -0
  14. package/MANIFEST.in +20 -0
  15. package/README.md +67 -154
  16. package/bin/cox-protocol.js +23 -97
  17. package/cox_protocol/__init__.py +2 -0
  18. package/cox_protocol/__main__.py +3 -0
  19. package/cox_protocol/compiler.py +273 -0
  20. package/cox_protocol/cox_cli.py +146 -0
  21. package/cox_protocol/cox_lib.py +599 -0
  22. package/cox_protocol/package_target.py +262 -0
  23. package/cox_protocol/react_target.py +306 -0
  24. package/cox_protocol/scaffold.py +56 -0
  25. package/cox_protocol/schemas/cox.component.v1.schema.json +180 -0
  26. package/cox_protocol/schemas/cox.crop_metadata.v1.schema.json +83 -0
  27. package/cox_protocol/schemas/cox.manifest.v1.schema.json +172 -0
  28. package/cox_protocol/schemas/cox.page.v1.schema.json +242 -0
  29. package/cox_protocol/schemas/cox.protocol.v1.schema.json +38 -0
  30. package/cox_protocol/starter/README.md +7 -0
  31. package/cox_protocol/starter/manifest.cox +26 -0
  32. package/cox_protocol/starter/page/action_button/action_button.cox +34 -0
  33. package/cox_protocol/starter/page/action_button/reference.svg +1 -0
  34. package/cox_protocol/starter/page/page.cox +39 -0
  35. package/cox_protocol/starter/page/reference.svg +1 -0
  36. package/editor-support/vscode/CHANGELOG.md +6 -0
  37. package/editor-support/vscode/README.md +2 -1
  38. package/editor-support/vscode/package.json +3 -3
  39. package/editor-support/vscode/snippets/cox-common.json +1 -1
  40. package/editor-support/vscode/snippets/cox-component.json +2 -1
  41. package/editor-support/vscode/snippets/cox-crop-metadata.json +1 -1
  42. package/editor-support/vscode/snippets/cox-manifest.json +1 -1
  43. package/editor-support/vscode/snippets/cox-page.json +2 -1
  44. package/editor-support/vscode/snippets/cox-protocol.json +1 -1
  45. package/examples/quickstart/README.md +7 -0
  46. package/examples/quickstart/manifest.cox +26 -0
  47. package/examples/quickstart/page/action_button/action_button.cox +34 -0
  48. package/examples/quickstart/page/action_button/reference.svg +1 -0
  49. package/examples/quickstart/page/page.cox +39 -0
  50. package/examples/quickstart/page/reference.svg +1 -0
  51. package/guides/contract-demo.md +24 -0
  52. package/guides/npm-installation.md +16 -287
  53. package/manifest.cox +7 -4
  54. package/package.json +30 -14
  55. package/protocols/recursive_decomposition_protocol.cox +8 -8
  56. package/pyproject.toml +17 -6
  57. package/schemas/cox.component.v1.schema.json +17 -4
  58. package/schemas/cox.crop_metadata.v1.schema.json +1 -1
  59. package/schemas/cox.manifest.v1.schema.json +1 -1
  60. package/schemas/cox.page.v1.schema.json +17 -4
  61. package/schemas/cox.protocol.v1.schema.json +1 -1
  62. package/scripts/check_distribution.py +87 -0
  63. package/scripts/demo.py +62 -0
  64. package/scripts/package-vscode.mjs +12 -0
  65. package/scripts/release_check.py +86 -0
  66. package/tests/conftest.py +21 -0
  67. package/tests/test_cli_and_typescript.py +123 -0
  68. package/tests/test_compiler.py +279 -0
  69. package/tests/test_validation.py +215 -0
  70. package/tools/__init__.py +2 -2
  71. package/tools/cox_cli.py +2 -123
  72. package/tools/cox_lib.py +2 -558
  73. package/tools/package_target.py +2 -214
  74. package/tools/react_target.py +2 -278
@@ -1,215 +1,3 @@
1
- from __future__ import annotations
1
+ """Compatibility import for the original source checkout. Use cox_protocol."""
2
2
 
3
- import hashlib
4
- import json
5
- import zipfile
6
- from pathlib import Path
7
- from typing import TYPE_CHECKING, Any, Iterable
8
-
9
- from . import __version__
10
- from .cox_lib import relative_path
11
-
12
-
13
- if TYPE_CHECKING:
14
- from .cox_lib import ValidationSummary
15
-
16
-
17
- FIXED_ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
18
- EXCLUDED_PARTS = {
19
- ".git",
20
- ".mypy_cache",
21
- ".pytest_cache",
22
- ".ruff_cache",
23
- ".venv",
24
- "__pycache__",
25
- "node_modules",
26
- }
27
- EXCLUDED_SUFFIXES = {".pyc", ".pyo", ".tgz"}
28
- EXCLUDED_FILENAMES = {
29
- ".DS_Store",
30
- "cox-guides.zip",
31
- "cox_protocol_package_compiled.zip",
32
- "llm_agent_zip_skills_bundle.zip",
33
- }
34
-
35
-
36
- def sha256_bytes(content: bytes) -> str:
37
- return hashlib.sha256(content).hexdigest()
38
-
39
-
40
- def sha256_file(path: Path) -> str:
41
- digest = hashlib.sha256()
42
- with path.open("rb") as handle:
43
- for chunk in iter(lambda: handle.read(1024 * 1024), b""):
44
- digest.update(chunk)
45
- return digest.hexdigest()
46
-
47
-
48
- def package_filename(summary: "ValidationSummary") -> str:
49
- manifest = summary.documents[summary.manifest_path]
50
- raw_name = str(manifest.get("PackageName") or "cox_protocol_package.zip")
51
- file_name = Path(raw_name).name
52
- if not file_name.endswith(".zip"):
53
- file_name = f"{file_name}.zip"
54
- if file_name == ".zip":
55
- return "cox_protocol_package.zip"
56
- return file_name
57
-
58
-
59
- def is_safe_project_file(
60
- path: Path,
61
- project_root: Path,
62
- package_output: Path,
63
- ) -> bool:
64
- resolved = path.resolve()
65
- try:
66
- relative = resolved.relative_to(project_root.resolve())
67
- except ValueError:
68
- return False
69
-
70
- if resolved == package_output.resolve():
71
- return False
72
- if relative.parts[:2] == ("build", "package"):
73
- return False
74
- if any(part in EXCLUDED_PARTS for part in relative.parts):
75
- return False
76
- if relative.name in EXCLUDED_FILENAMES:
77
- return False
78
- if relative.suffix in EXCLUDED_SUFFIXES:
79
- return False
80
- return True
81
-
82
-
83
- def iter_files(path: Path) -> Iterable[Path]:
84
- if path.is_file():
85
- yield path
86
- return
87
- if path.is_dir():
88
- for child in path.rglob("*"):
89
- if child.is_file() and not child.is_symlink():
90
- yield child
91
-
92
-
93
- def collect_package_files(
94
- summary: "ValidationSummary",
95
- target_results: dict[str, dict[str, Any]],
96
- package_output: Path,
97
- ) -> list[Path]:
98
- candidates: list[Path] = [summary.manifest_path, *summary.referenced_paths]
99
-
100
- for target_name, result in target_results.items():
101
- if target_name == "package":
102
- continue
103
- for key in ("output_root", "manifest"):
104
- value = result.get(key)
105
- if isinstance(value, Path):
106
- candidates.append(value)
107
-
108
- files_by_relative_path: dict[str, Path] = {}
109
- for candidate in candidates:
110
- for file_path in iter_files(candidate):
111
- if not is_safe_project_file(
112
- file_path,
113
- summary.project_root,
114
- package_output,
115
- ):
116
- continue
117
- relative = relative_path(summary.project_root, file_path)
118
- files_by_relative_path[relative] = file_path.resolve()
119
-
120
- return [
121
- files_by_relative_path[key]
122
- for key in sorted(files_by_relative_path)
123
- ]
124
-
125
-
126
- def zip_write_bytes(
127
- archive: zipfile.ZipFile,
128
- archive_path: str,
129
- content: bytes,
130
- ) -> None:
131
- info = zipfile.ZipInfo(archive_path, FIXED_ZIP_TIMESTAMP)
132
- info.compress_type = zipfile.ZIP_DEFLATED
133
- info.external_attr = 0o644 << 16
134
- archive.writestr(info, content)
135
-
136
-
137
- def emit_package_archive(
138
- summary: "ValidationSummary",
139
- build_root: Path,
140
- target_results: dict[str, dict[str, Any]],
141
- ) -> dict[str, Any]:
142
- output_root = build_root / "package"
143
- output_root.mkdir(parents=True, exist_ok=True)
144
-
145
- archive_path = output_root / package_filename(summary)
146
- source_files = collect_package_files(
147
- summary,
148
- target_results,
149
- archive_path,
150
- )
151
- archive_root = summary.project_root.name
152
-
153
- file_records = []
154
- for source_file in source_files:
155
- relative = relative_path(summary.project_root, source_file)
156
- file_records.append(
157
- {
158
- "path": f"{archive_root}/{relative}",
159
- "source": relative,
160
- "size": source_file.stat().st_size,
161
- "sha256": sha256_file(source_file),
162
- }
163
- )
164
-
165
- package_manifest = {
166
- "generatedWith": f"cox {__version__}",
167
- "deterministic": True,
168
- "archiveRoot": archive_root,
169
- "projectName": summary.documents[summary.manifest_path].get(
170
- "ProjectName"
171
- ),
172
- "packageName": archive_path.name,
173
- "fileCount": len(file_records),
174
- "files": file_records,
175
- }
176
- manifest_bytes = (
177
- json.dumps(package_manifest, indent=2, sort_keys=True) + "\n"
178
- ).encode("utf-8")
179
-
180
- with zipfile.ZipFile(archive_path, "w") as archive:
181
- for source_file, file_record in zip(source_files, file_records):
182
- zip_write_bytes(
183
- archive,
184
- str(file_record["path"]),
185
- source_file.read_bytes(),
186
- )
187
- zip_write_bytes(
188
- archive,
189
- f"{archive_root}/package.manifest.json",
190
- manifest_bytes,
191
- )
192
-
193
- archive_bytes = archive_path.read_bytes()
194
- archive_sha256 = sha256_bytes(archive_bytes)
195
- disk_manifest = {
196
- **package_manifest,
197
- "archive": {
198
- "path": relative_path(summary.project_root, archive_path),
199
- "size": archive_path.stat().st_size,
200
- "sha256": archive_sha256,
201
- },
202
- }
203
- manifest_path = output_root / f"{archive_path.stem}.manifest.json"
204
- manifest_path.write_text(
205
- json.dumps(disk_manifest, indent=2, sort_keys=True) + "\n",
206
- encoding="utf-8",
207
- )
208
-
209
- return {
210
- "output_root": output_root,
211
- "archive": archive_path,
212
- "manifest": manifest_path,
213
- "file_count": len(file_records) + 1,
214
- "sha256": archive_sha256,
215
- }
3
+ from cox_protocol.package_target import * # noqa: F401,F403
@@ -1,279 +1,3 @@
1
- from __future__ import annotations
1
+ """Compatibility import for the original source checkout. Use cox_protocol."""
2
2
 
3
- import json
4
- import os
5
- from pathlib import Path
6
- from typing import TYPE_CHECKING, Any
7
-
8
- from . import __version__
9
-
10
- if TYPE_CHECKING:
11
- from .cox_lib import ValidationSummary
12
-
13
-
14
- def pascal_case(value: str) -> str:
15
- pieces = value.replace("-", "_").split("_")
16
- return "".join(piece.capitalize() for piece in pieces if piece)
17
-
18
-
19
- def relative_import(from_dir: Path, target_dir: Path) -> str:
20
- import_path = os.path.relpath(target_dir, from_dir).replace(os.sep, "/")
21
- if import_path.startswith("."):
22
- return import_path
23
- return f"./{import_path}"
24
-
25
-
26
- def document_for_instruction(
27
- summary: "ValidationSummary",
28
- instruction_path: str,
29
- ) -> dict[str, Any]:
30
- document_path = (summary.project_root / instruction_path).resolve()
31
- try:
32
- return summary.documents[document_path]
33
- except KeyError as exc:
34
- raise RuntimeError(
35
- f"Missing parsed COX document for {instruction_path}"
36
- ) from exc
37
-
38
-
39
- def format_comment_block(lines: list[str]) -> str:
40
- body = "\n".join(f" * {line}" for line in lines)
41
- return f"/**\n{body}\n */"
42
-
43
-
44
- def format_list(items: list[str]) -> str:
45
- return ", ".join(items) if items else "none"
46
-
47
-
48
- def write_text(path: Path, content: str) -> None:
49
- path.parent.mkdir(parents=True, exist_ok=True)
50
- path.write_text(content, encoding="utf-8")
51
-
52
-
53
- def component_comment_lines(component: dict[str, Any]) -> list[str]:
54
- visual_style = component.get("VisualStyle", {})
55
- responsive = component.get("Responsive", {})
56
- interactions = component.get("Interactions", {})
57
- hints = component.get("ImplementationHints", [])
58
- return [
59
- f"COX component slug: {component.get('Slug', 'unknown')}",
60
- f"Role: {component.get('Role', 'unspecified')}",
61
- f"Token refs: {format_list(visual_style.get('Tokens', []))}",
62
- f"Desktop: {responsive.get('Desktop', 'unspecified')}",
63
- f"Tablet: {responsive.get('Tablet', 'unspecified')}",
64
- f"Mobile: {responsive.get('Mobile', 'unspecified')}",
65
- (
66
- "Interaction notes: "
67
- f"{format_list(interactions.get('Inferred', []))}"
68
- ),
69
- f"Implementation hints: {format_list(hints)}",
70
- ]
71
-
72
-
73
- def page_comment_lines(page: dict[str, Any]) -> list[str]:
74
- layout = page.get("Layout", {})
75
- regions = page.get("VisibleStructure", {}).get("Regions", [])
76
- return [
77
- f"COX page slug: {page.get('Slug', 'unknown')}",
78
- f"Page type: {page.get('PageType', 'unspecified')}",
79
- f"Visible regions: {format_list(regions)}",
80
- f"Desktop layout: {layout.get('Desktop', 'unspecified')}",
81
- f"Tablet layout: {layout.get('Tablet', 'unspecified')}",
82
- f"Mobile layout: {layout.get('Mobile', 'unspecified')}",
83
- ]
84
-
85
-
86
- def render_component_stub(component: dict[str, Any]) -> str:
87
- component_name = pascal_case(component["Slug"])
88
- comment_block = format_comment_block(component_comment_lines(component))
89
- role = json.dumps(component.get("Role", "Component"))
90
- lines = [
91
- 'import type { ReactNode } from "react";',
92
- "",
93
- comment_block,
94
- f"export interface {component_name}Props {{",
95
- " children?: ReactNode;",
96
- "}",
97
- "",
98
- f"export function {component_name}({{",
99
- " children,",
100
- f"}}: {component_name}Props) {{",
101
- " return (",
102
- (
103
- " <section "
104
- f"data-cox-slug=\"{component['Slug']}\" aria-label={role}>"
105
- ),
106
- (
107
- " {/* TODO: Replace this structural stub with the real UI "
108
- "implementation. */}"
109
- ),
110
- " {children}",
111
- " </section>",
112
- " );",
113
- "}",
114
- ]
115
- return "\n".join(lines) + "\n"
116
-
117
-
118
- def render_page_stub(
119
- page: dict[str, Any],
120
- component_entries: list[dict[str, Any]],
121
- react_root: Path,
122
- ) -> str:
123
- page_name = pascal_case(page["Slug"]) + "Page"
124
- page_dir = react_root / "pages" / page["ProjectName"] / pascal_case(
125
- page["Slug"]
126
- )
127
- import_lines: list[str] = []
128
- render_lines: list[str] = []
129
-
130
- for component_entry in component_entries:
131
- component_name = pascal_case(component_entry["Slug"])
132
- component_dir = react_root / "components" / component_name
133
- import_path = relative_import(page_dir, component_dir)
134
- import_lines.append(
135
- f'import {{ {component_name} }} from "{import_path}";'
136
- )
137
- render_lines.append(f" <{component_name} />")
138
-
139
- comment_block = format_comment_block(page_comment_lines(page))
140
- imports = "\n".join(import_lines)
141
- rendered_components = "\n".join(render_lines)
142
- if not rendered_components:
143
- rendered_components = (
144
- " {/* TODO: Wire child components from the COX page spec. */}"
145
- )
146
-
147
- return (
148
- f"{imports}\n\n"
149
- f"{comment_block}\n"
150
- f"export interface {page_name}Props {{}}\n\n"
151
- f"export function {page_name}(_: {page_name}Props) {{\n"
152
- " return (\n"
153
- f" <main data-cox-page=\"{page['Slug']}\">\n"
154
- f"{rendered_components}\n"
155
- " </main>\n"
156
- " );\n"
157
- "}\n"
158
- )
159
-
160
-
161
- def emit_react_stubs(
162
- summary: "ValidationSummary",
163
- build_root: Path,
164
- ) -> dict[str, Any]:
165
- react_root = build_root / "react"
166
- component_records: dict[str, dict[str, Any]] = {}
167
- page_records: list[dict[str, Any]] = []
168
-
169
- for page_entry in summary.build_index["pages"]:
170
- page_payload = document_for_instruction(
171
- summary,
172
- page_entry["instruction"],
173
- )
174
- page_dir = (
175
- react_root
176
- / "pages"
177
- / page_payload["ProjectName"]
178
- / pascal_case(page_entry["slug"])
179
- )
180
- page_output = page_dir / "index.tsx"
181
-
182
- page_component_entries: list[dict[str, Any]] = []
183
- for component_entry in page_entry["components"]:
184
- slug = component_entry["slug"]
185
- component_payload = document_for_instruction(
186
- summary,
187
- component_entry["instruction"],
188
- )
189
- component_name = pascal_case(slug)
190
- component_dir = react_root / "components" / component_name
191
- component_output = component_dir / "index.tsx"
192
- if slug not in component_records:
193
- write_text(
194
- component_output,
195
- render_component_stub(component_payload),
196
- )
197
- component_records[slug] = {
198
- "slug": slug,
199
- "componentName": component_name,
200
- "output": component_output.relative_to(
201
- react_root
202
- ).as_posix(),
203
- }
204
-
205
- page_component_entries.append(
206
- {
207
- "Slug": slug,
208
- "ComponentName": component_name,
209
- }
210
- )
211
-
212
- write_text(
213
- page_output,
214
- render_page_stub(page_payload, page_component_entries, react_root),
215
- )
216
- page_records.append(
217
- {
218
- "projectName": page_payload["ProjectName"],
219
- "slug": page_payload["Slug"],
220
- "componentName": pascal_case(page_payload["Slug"]) + "Page",
221
- "output": page_output.relative_to(react_root).as_posix(),
222
- "children": page_component_entries,
223
- }
224
- )
225
-
226
- barrel_lines: list[str] = []
227
- for component in sorted(
228
- component_records.values(),
229
- key=lambda item: item["componentName"],
230
- ):
231
- component_name = component["componentName"]
232
- barrel_lines.append(
233
- (
234
- f'export {{ {component_name} }} '
235
- f'from "./components/{component_name}";'
236
- )
237
- )
238
- barrel_lines.append(
239
- (
240
- f'export type {{ {component_name}Props }} '
241
- f'from "./components/{component_name}";'
242
- )
243
- )
244
-
245
- for page in sorted(page_records, key=lambda item: item["componentName"]):
246
- page_component_name = page["componentName"]
247
- page_path = page["output"].removesuffix("/index.tsx")
248
- barrel_lines.append(
249
- f'export {{ {page_component_name} }} from "./{page_path}";'
250
- )
251
- barrel_lines.append(
252
- (
253
- f'export type {{ {page_component_name}Props }} '
254
- f'from "./{page_path}";'
255
- )
256
- )
257
-
258
- write_text(react_root / "index.ts", "\n".join(barrel_lines) + "\n")
259
-
260
- manifest_payload = {
261
- "generatedWith": f"cox {__version__}",
262
- "target": "react",
263
- "components": sorted(
264
- component_records.values(),
265
- key=lambda item: item["componentName"],
266
- ),
267
- "pages": page_records,
268
- }
269
- write_text(
270
- react_root / "manifest.react.json",
271
- json.dumps(manifest_payload, indent=2, ensure_ascii=True) + "\n",
272
- )
273
-
274
- return {
275
- "output_root": react_root,
276
- "component_count": len(component_records),
277
- "page_count": len(page_records),
278
- "manifest": react_root / "manifest.react.json",
279
- }
3
+ from cox_protocol.react_target import * # noqa: F401,F403