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,557 @@
1
+ # pyright: reportMissingTypeStubs=false
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Sequence
9
+
10
+ import yaml # type: ignore[import-untyped]
11
+ from jsonschema import Draft202012Validator # type: ignore[import-untyped]
12
+
13
+
14
+ SCHEMA_FILENAMES = {
15
+ "cox.manifest.v1": "cox.manifest.v1.schema.json",
16
+ "cox.page.v1": "cox.page.v1.schema.json",
17
+ "cox.component.v1": "cox.component.v1.schema.json",
18
+ "cox.protocol.v1": "cox.protocol.v1.schema.json",
19
+ "cox.crop_metadata.v1": "cox.crop_metadata.v1.schema.json",
20
+ }
21
+
22
+ SUPPORTED_COMPILE_TARGETS = ("json", "react", "package")
23
+
24
+
25
+ class CoxError(RuntimeError):
26
+ """Raised when COX validation or compilation fails."""
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class ValidationSummary:
31
+ project_root: Path
32
+ manifest_path: Path
33
+ documents: dict[Path, dict[str, Any]]
34
+ referenced_paths: list[Path]
35
+ warnings: list[str]
36
+ build_index: dict[str, Any]
37
+
38
+
39
+ def resolve_project_root(target: Path) -> tuple[Path, Path]:
40
+ resolved_target = target.resolve()
41
+ search_root = (
42
+ resolved_target
43
+ if resolved_target.is_dir()
44
+ else resolved_target.parent
45
+ )
46
+
47
+ direct_manifest = search_root / "manifest.cox"
48
+ if direct_manifest.exists():
49
+ return search_root, direct_manifest
50
+
51
+ for candidate_root in [search_root, *search_root.parents]:
52
+ manifest_path = candidate_root / "manifest.cox"
53
+ if manifest_path.exists():
54
+ return candidate_root, manifest_path
55
+
56
+ raise CoxError(f"Could not locate manifest.cox from {target}")
57
+
58
+
59
+ def read_yaml_document(path: Path) -> dict[str, Any]:
60
+ try:
61
+ content = path.read_text(encoding="utf-8")
62
+ except UnicodeDecodeError as exc:
63
+ raise CoxError(f"{path}: file is not valid UTF-8") from exc
64
+
65
+ try:
66
+ document = yaml.safe_load(content)
67
+ except yaml.YAMLError as exc:
68
+ raise CoxError(f"{path}: YAML parsing failed: {exc}") from exc
69
+
70
+ if not isinstance(document, dict):
71
+ raise CoxError(f"{path}: top-level COX document must be a mapping")
72
+
73
+ return document
74
+
75
+
76
+ def read_json_document(path: Path) -> dict[str, Any]:
77
+ try:
78
+ return json.loads(path.read_text(encoding="utf-8"))
79
+ except json.JSONDecodeError as exc:
80
+ raise CoxError(f"{path}: JSON parsing failed: {exc}") from exc
81
+
82
+
83
+ def load_schema(schema_dir: Path, schema_name: str) -> dict[str, Any]:
84
+ schema_file = SCHEMA_FILENAMES.get(schema_name)
85
+ if schema_file is None:
86
+ raise CoxError(f"Unknown COX schema id: {schema_name}")
87
+
88
+ schema_path = schema_dir / schema_file
89
+ if not schema_path.exists():
90
+ raise CoxError(f"Schema file is missing: {schema_path}")
91
+
92
+ return read_json_document(schema_path)
93
+
94
+
95
+ def relative_path(root: Path, path: Path) -> str:
96
+ return path.resolve().relative_to(root.resolve()).as_posix()
97
+
98
+
99
+ def format_report_path(root: Path, path: Path) -> str:
100
+ try:
101
+ return relative_path(root, path)
102
+ except ValueError:
103
+ return path.resolve().as_posix()
104
+
105
+
106
+ def unique_paths(paths: list[Path]) -> list[Path]:
107
+ ordered: list[Path] = []
108
+ seen: set[Path] = set()
109
+ for path in paths:
110
+ resolved = path.resolve()
111
+ if resolved in seen:
112
+ continue
113
+ seen.add(resolved)
114
+ ordered.append(resolved)
115
+ return ordered
116
+
117
+
118
+ def validate_against_schema(
119
+ document: dict[str, Any],
120
+ path: Path,
121
+ schema_dir: Path,
122
+ ) -> list[str]:
123
+ schema_name = document.get("Schema")
124
+ if not isinstance(schema_name, str) or not schema_name:
125
+ return [f"{path}: missing Schema field"]
126
+
127
+ schema = load_schema(schema_dir, schema_name)
128
+ validator = Draft202012Validator(schema)
129
+ issues: list[str] = []
130
+
131
+ sorted_errors = sorted(
132
+ validator.iter_errors(document),
133
+ key=lambda item: list(item.absolute_path),
134
+ )
135
+ for error in sorted_errors:
136
+ location = ".".join(str(part) for part in error.absolute_path)
137
+ prefix = f"{path}: {location}" if location else str(path)
138
+ issues.append(f"{prefix}: {error.message}")
139
+
140
+ return issues
141
+
142
+
143
+ def build_manifest_index(
144
+ manifest: dict[str, Any],
145
+ project_root: Path,
146
+ ) -> dict[str, Any]:
147
+ reference_docs = [
148
+ project_root / path for path in manifest.get("ReferenceDocs", [])
149
+ ]
150
+ guide_docs = [
151
+ project_root / path for path in manifest.get("GuideDocs", [])
152
+ ]
153
+ protocol_docs = [
154
+ project_root / path for path in manifest.get("ProtocolDocs", [])
155
+ ]
156
+ skill_bundles = [
157
+ project_root / path for path in manifest.get("SkillBundles", [])
158
+ ]
159
+ schema_files = [
160
+ project_root / path for path in manifest.get("Schemas", [])
161
+ ]
162
+ tooling_files = [
163
+ project_root / path for path in manifest.get("ToolingFiles", [])
164
+ ]
165
+
166
+ page_entries: list[dict[str, Any]] = []
167
+ cox_documents = protocol_docs.copy()
168
+ for optional_cox_path in [*guide_docs, *skill_bundles]:
169
+ if optional_cox_path.suffix == ".cox":
170
+ cox_documents.append(optional_cox_path)
171
+ elif optional_cox_path.exists() and optional_cox_path.is_dir():
172
+ cox_documents.extend(optional_cox_path.rglob("*.cox"))
173
+ referenced_paths = (
174
+ reference_docs
175
+ + guide_docs
176
+ + protocol_docs
177
+ + skill_bundles
178
+ + schema_files
179
+ + tooling_files
180
+ )
181
+
182
+ for page in manifest.get("Pages", []):
183
+ page_instruction = project_root / page["PageInstruction"]
184
+ page_screenshot = project_root / page["PageScreenshotPlaceholder"]
185
+ referenced_paths.extend([page_instruction, page_screenshot])
186
+ cox_documents.append(page_instruction)
187
+
188
+ component_entries: list[dict[str, Any]] = []
189
+ for component in page.get("Components", []):
190
+ component_instruction = (
191
+ project_root / component["ComponentInstruction"]
192
+ )
193
+ component_screenshot = (
194
+ project_root / component["ComponentScreenshotPlaceholder"]
195
+ )
196
+ referenced_paths.extend(
197
+ [component_instruction, component_screenshot]
198
+ )
199
+ cox_documents.append(component_instruction)
200
+ component_entries.append(
201
+ {
202
+ "slug": component["Slug"],
203
+ "folder": component["Folder"],
204
+ "instruction": relative_path(
205
+ project_root,
206
+ component_instruction,
207
+ ),
208
+ "screenshot": relative_path(
209
+ project_root,
210
+ component_screenshot,
211
+ ),
212
+ }
213
+ )
214
+
215
+ page_entries.append(
216
+ {
217
+ "pageName": page["PageName"],
218
+ "slug": page["Slug"],
219
+ "folder": page["Folder"],
220
+ "instruction": relative_path(project_root, page_instruction),
221
+ "screenshot": relative_path(project_root, page_screenshot),
222
+ "components": component_entries,
223
+ }
224
+ )
225
+
226
+ return {
227
+ "reference_docs": [
228
+ relative_path(project_root, path) for path in reference_docs
229
+ ],
230
+ "guide_docs": [
231
+ relative_path(project_root, path) for path in guide_docs
232
+ ],
233
+ "protocol_docs": [
234
+ relative_path(project_root, path) for path in protocol_docs
235
+ ],
236
+ "skill_bundles": [
237
+ relative_path(project_root, path) for path in skill_bundles
238
+ ],
239
+ "schema_files": [
240
+ relative_path(project_root, path) for path in schema_files
241
+ ],
242
+ "tooling_files": [
243
+ relative_path(project_root, path) for path in tooling_files
244
+ ],
245
+ "pages": page_entries,
246
+ "cox_documents": unique_paths(
247
+ [project_root / "manifest.cox", *cox_documents]
248
+ ),
249
+ "referenced_paths": unique_paths(referenced_paths),
250
+ }
251
+
252
+
253
+ def validate_project(target: Path) -> ValidationSummary:
254
+ project_root, manifest_path = resolve_project_root(target)
255
+ schema_dir = project_root / "schemas"
256
+ manifest = read_yaml_document(manifest_path)
257
+
258
+ issues = validate_against_schema(manifest, manifest_path, schema_dir)
259
+ manifest_index = build_manifest_index(manifest, project_root)
260
+
261
+ missing_paths = [
262
+ path
263
+ for path in manifest_index["referenced_paths"]
264
+ if not path.exists()
265
+ ]
266
+ if missing_paths:
267
+ missing_lines = [
268
+ f"Missing referenced path: {relative_path(project_root, path)}"
269
+ for path in missing_paths
270
+ ]
271
+ missing_text = "\n".join(missing_lines)
272
+ issues.append(missing_text)
273
+
274
+ documents: dict[Path, dict[str, Any]] = {manifest_path.resolve(): manifest}
275
+ for document_path in manifest_index["cox_documents"]:
276
+ document = read_yaml_document(document_path)
277
+ documents[document_path.resolve()] = document
278
+ issues.extend(
279
+ validate_against_schema(document, document_path, schema_dir)
280
+ )
281
+
282
+ discovered_cox_files = [
283
+ path.resolve()
284
+ for path in project_root.rglob("*.cox")
285
+ if "build" not in path.parts
286
+ ]
287
+ referenced_cox_files = set(manifest_index["cox_documents"])
288
+ unreferenced_files = [
289
+ path
290
+ for path in discovered_cox_files
291
+ if path not in referenced_cox_files
292
+ ]
293
+ warnings: list[str] = []
294
+ if unreferenced_files:
295
+ unreferenced_text = ", ".join(
296
+ relative_path(project_root, path)
297
+ for path in sorted(unreferenced_files)
298
+ )
299
+ warnings.append(
300
+ "Unreferenced COX files: " + unreferenced_text
301
+ )
302
+
303
+ if issues:
304
+ raise CoxError("\n".join(issues))
305
+
306
+ return ValidationSummary(
307
+ project_root=project_root,
308
+ manifest_path=manifest_path.resolve(),
309
+ documents=documents,
310
+ referenced_paths=manifest_index["referenced_paths"],
311
+ warnings=warnings,
312
+ build_index=manifest_index,
313
+ )
314
+
315
+
316
+ def write_json(path: Path, payload: dict[str, Any]) -> None:
317
+ path.parent.mkdir(parents=True, exist_ok=True)
318
+ serialized = json.dumps(payload, indent=2, ensure_ascii=True) + "\n"
319
+ path.write_text(serialized, encoding="utf-8")
320
+
321
+
322
+ def normalize_compile_targets(targets: Sequence[str] | None) -> list[str]:
323
+ requested_targets = list(targets or ["json"])
324
+ expanded_targets: list[str] = []
325
+
326
+ for raw_target in requested_targets:
327
+ for piece in raw_target.split(","):
328
+ target = piece.strip().lower()
329
+ if not target:
330
+ continue
331
+ if target == "all":
332
+ expanded_targets.extend(SUPPORTED_COMPILE_TARGETS)
333
+ continue
334
+ expanded_targets.append(target)
335
+
336
+ normalized_targets: list[str] = []
337
+ for target in expanded_targets or ["json"]:
338
+ if target not in SUPPORTED_COMPILE_TARGETS:
339
+ supported = ", ".join(SUPPORTED_COMPILE_TARGETS)
340
+ raise CoxError(
341
+ f"Unsupported compile target '{target}'. "
342
+ f"Supported targets: {supported}"
343
+ )
344
+ if target not in normalized_targets:
345
+ normalized_targets.append(target)
346
+
347
+ return normalized_targets
348
+
349
+
350
+ def emit_json_target(
351
+ summary: ValidationSummary,
352
+ build_root: Path,
353
+ ) -> dict[str, Any]:
354
+ normalized_root = build_root / "normalized"
355
+
356
+ for document_path, payload in summary.documents.items():
357
+ if document_path == summary.manifest_path:
358
+ continue
359
+ output_path = normalized_root / document_path.relative_to(
360
+ summary.project_root
361
+ )
362
+ write_json(output_path.with_suffix(".json"), payload)
363
+
364
+ return {
365
+ "output_root": normalized_root,
366
+ "document_count": len(summary.documents) - 1,
367
+ }
368
+
369
+
370
+ def build_report(
371
+ summary: ValidationSummary,
372
+ build_root: Path,
373
+ target_results: dict[str, dict[str, Any]],
374
+ ) -> str:
375
+ warning_lines = summary.warnings or ["none"]
376
+ compiled_manifest = format_report_path(
377
+ summary.project_root,
378
+ build_root / "manifest.compiled.json",
379
+ )
380
+ build_root_label = format_report_path(summary.project_root, build_root)
381
+ verification_report = format_report_path(
382
+ summary.project_root,
383
+ build_root / "reports" / "verification_report.txt",
384
+ )
385
+ report_lines = [
386
+ "CompiledVerificationReport:",
387
+ ' CoxVersion: "1.0"',
388
+ f' ProjectRoot: "{summary.project_root.name}"',
389
+ (
390
+ " Manifest: "
391
+ f'"{relative_path(summary.project_root, summary.manifest_path)}"'
392
+ ),
393
+ f' ValidatedDocuments: {len(summary.documents)}',
394
+ f' GuideDocs: {len(summary.build_index["guide_docs"])}',
395
+ f' SkillBundles: {len(summary.build_index["skill_bundles"])}',
396
+ f' CompiledManifest: "{compiled_manifest}"',
397
+ f' ReferencedPathsChecked: {len(summary.referenced_paths)}',
398
+ " Targets:",
399
+ ]
400
+ report_lines.extend(
401
+ f' - "{target_name}"' for target_name in target_results
402
+ )
403
+ report_lines.append(" Warnings:")
404
+ report_lines.extend(f' - "{line}"' for line in warning_lines)
405
+ report_lines.extend(
406
+ [
407
+ " Outputs:",
408
+ f' BuildRoot: "{build_root_label}"',
409
+ f' VerificationReport: "{verification_report}"',
410
+ f' ManifestCompiledJson: "{compiled_manifest}"',
411
+ ]
412
+ )
413
+
414
+ json_result = target_results.get("json")
415
+ if json_result is not None:
416
+ json_output_root = format_report_path(
417
+ summary.project_root,
418
+ json_result["output_root"],
419
+ )
420
+ report_lines.extend(
421
+ [
422
+ " JsonTarget:",
423
+ f' OutputRoot: "{json_output_root}"',
424
+ (
425
+ " DocumentCount: "
426
+ f'{json_result["document_count"]}'
427
+ ),
428
+ ]
429
+ )
430
+
431
+ react_result = target_results.get("react")
432
+ if react_result is not None:
433
+ react_output_root = format_report_path(
434
+ summary.project_root,
435
+ react_result["output_root"],
436
+ )
437
+ react_manifest = format_report_path(
438
+ summary.project_root,
439
+ react_result["manifest"],
440
+ )
441
+ report_lines.extend(
442
+ [
443
+ " ReactTarget:",
444
+ f' OutputRoot: "{react_output_root}"',
445
+ (
446
+ " PageCount: "
447
+ f'{react_result["page_count"]}'
448
+ ),
449
+ (
450
+ " ComponentCount: "
451
+ f'{react_result["component_count"]}'
452
+ ),
453
+ (
454
+ " Manifest: "
455
+ f'"{react_manifest}"'
456
+ ),
457
+ ]
458
+ )
459
+
460
+ package_result = target_results.get("package")
461
+ if package_result is not None:
462
+ package_output_root = format_report_path(
463
+ summary.project_root,
464
+ package_result["output_root"],
465
+ )
466
+ package_archive = format_report_path(
467
+ summary.project_root,
468
+ package_result["archive"],
469
+ )
470
+ package_manifest = format_report_path(
471
+ summary.project_root,
472
+ package_result["manifest"],
473
+ )
474
+ report_lines.extend(
475
+ [
476
+ " PackageTarget:",
477
+ f' OutputRoot: "{package_output_root}"',
478
+ f' Archive: "{package_archive}"',
479
+ f' Manifest: "{package_manifest}"',
480
+ f' FileCount: {package_result["file_count"]}',
481
+ f' Sha256: "{package_result["sha256"]}"',
482
+ ]
483
+ )
484
+
485
+ return "\n".join(report_lines) + "\n"
486
+
487
+
488
+ def compile_project(
489
+ target: Path,
490
+ out_dir: Path,
491
+ targets: Sequence[str] | None = None,
492
+ ) -> dict[str, Any]:
493
+ summary = validate_project(target)
494
+ compile_targets = normalize_compile_targets(targets)
495
+ build_root = (
496
+ out_dir if out_dir.is_absolute() else summary.project_root / out_dir
497
+ )
498
+ reports_root = build_root / "reports"
499
+ target_results: dict[str, dict[str, Any]] = {}
500
+
501
+ if "json" in compile_targets:
502
+ target_results["json"] = emit_json_target(summary, build_root)
503
+
504
+ if "react" in compile_targets:
505
+ from .react_target import emit_react_stubs
506
+
507
+ target_results["react"] = emit_react_stubs(summary, build_root)
508
+
509
+ if "package" in compile_targets:
510
+ from .package_target import emit_package_archive
511
+
512
+ target_results["package"] = emit_package_archive(
513
+ summary,
514
+ build_root,
515
+ target_results,
516
+ )
517
+
518
+ manifest_payload = {
519
+ "compiledWith": "cox 1.0.0",
520
+ "mimeType": "application/vnd.cox+yaml",
521
+ "targets": compile_targets,
522
+ "manifest": summary.documents[summary.manifest_path],
523
+ "resolvedFiles": {
524
+ "referenceDocs": summary.build_index["reference_docs"],
525
+ "guideDocs": summary.build_index["guide_docs"],
526
+ "protocolDocs": summary.build_index["protocol_docs"],
527
+ "skillBundles": summary.build_index["skill_bundles"],
528
+ "schemaFiles": summary.build_index["schema_files"],
529
+ "toolingFiles": summary.build_index["tooling_files"],
530
+ "pages": summary.build_index["pages"],
531
+ },
532
+ "targetOutputs": {
533
+ target_name: {
534
+ key: value.as_posix() if isinstance(value, Path) else value
535
+ for key, value in result.items()
536
+ }
537
+ for target_name, result in target_results.items()
538
+ },
539
+ "warnings": summary.warnings,
540
+ }
541
+ write_json(build_root / "manifest.compiled.json", manifest_payload)
542
+
543
+ reports_root.mkdir(parents=True, exist_ok=True)
544
+ report_path = reports_root / "verification_report.txt"
545
+ report_path.write_text(
546
+ build_report(summary, build_root, target_results),
547
+ encoding="utf-8",
548
+ )
549
+
550
+ return {
551
+ "build_root": build_root,
552
+ "targets": compile_targets,
553
+ "target_results": target_results,
554
+ "compiled_manifest": build_root / "manifest.compiled.json",
555
+ "report": report_path,
556
+ "warnings": summary.warnings,
557
+ }