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