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