atomadic-forge 0.3.2__py3-none-any.whl

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 (131) hide show
  1. atomadic_forge/__init__.py +12 -0
  2. atomadic_forge/__main__.py +5 -0
  3. atomadic_forge/a0_qk_constants/__init__.py +1 -0
  4. atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
  5. atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
  6. atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
  7. atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
  8. atomadic_forge/a0_qk_constants/error_codes.py +296 -0
  9. atomadic_forge/a0_qk_constants/forge_types.py +89 -0
  10. atomadic_forge/a0_qk_constants/gen_language.py +116 -0
  11. atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
  12. atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
  13. atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
  14. atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
  15. atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
  16. atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
  17. atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
  18. atomadic_forge/a0_qk_constants/tier_names.py +47 -0
  19. atomadic_forge/a1_at_functions/__init__.py +1 -0
  20. atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
  21. atomadic_forge/a1_at_functions/agent_memory.py +139 -0
  22. atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
  23. atomadic_forge/a1_at_functions/agent_summary.py +277 -0
  24. atomadic_forge/a1_at_functions/body_extractor.py +306 -0
  25. atomadic_forge/a1_at_functions/card_renderer.py +210 -0
  26. atomadic_forge/a1_at_functions/certify_checks.py +445 -0
  27. atomadic_forge/a1_at_functions/chat_context.py +170 -0
  28. atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
  29. atomadic_forge/a1_at_functions/classify_tier.py +115 -0
  30. atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
  31. atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
  32. atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
  33. atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
  34. atomadic_forge/a1_at_functions/config_io.py +68 -0
  35. atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
  36. atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
  37. atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
  38. atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
  39. atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
  40. atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
  41. atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
  42. atomadic_forge/a1_at_functions/error_hints.py +105 -0
  43. atomadic_forge/a1_at_functions/evolution_log.py +94 -0
  44. atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
  45. atomadic_forge/a1_at_functions/generation_quality.py +322 -0
  46. atomadic_forge/a1_at_functions/import_repair.py +211 -0
  47. atomadic_forge/a1_at_functions/import_smoke.py +102 -0
  48. atomadic_forge/a1_at_functions/js_parser.py +539 -0
  49. atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
  50. atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
  51. atomadic_forge/a1_at_functions/llm_client.py +554 -0
  52. atomadic_forge/a1_at_functions/local_signer.py +134 -0
  53. atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
  54. atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
  55. atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
  56. atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
  57. atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
  58. atomadic_forge/a1_at_functions/policy_loader.py +107 -0
  59. atomadic_forge/a1_at_functions/preflight_change.py +227 -0
  60. atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
  61. atomadic_forge/a1_at_functions/provider_detect.py +157 -0
  62. atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
  63. atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
  64. atomadic_forge/a1_at_functions/recipes.py +186 -0
  65. atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
  66. atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
  67. atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
  68. atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
  69. atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
  70. atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
  71. atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
  72. atomadic_forge/a1_at_functions/scout_walk.py +309 -0
  73. atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
  74. atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
  75. atomadic_forge/a1_at_functions/stub_detector.py +158 -0
  76. atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
  77. atomadic_forge/a1_at_functions/synergy_render.py +252 -0
  78. atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
  79. atomadic_forge/a1_at_functions/test_runner.py +196 -0
  80. atomadic_forge/a1_at_functions/test_selector.py +122 -0
  81. atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
  82. atomadic_forge/a1_at_functions/tool_composer.py +130 -0
  83. atomadic_forge/a1_at_functions/transcript_log.py +70 -0
  84. atomadic_forge/a1_at_functions/wire_check.py +260 -0
  85. atomadic_forge/a2_mo_composites/__init__.py +1 -0
  86. atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
  87. atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
  88. atomadic_forge/a2_mo_composites/plan_store.py +164 -0
  89. atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
  90. atomadic_forge/a3_og_features/__init__.py +1 -0
  91. atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
  92. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
  93. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
  94. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
  95. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
  96. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
  97. atomadic_forge/a3_og_features/demo_runner.py +502 -0
  98. atomadic_forge/a3_og_features/emergent_feature.py +95 -0
  99. atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
  100. atomadic_forge/a3_og_features/forge_enforce.py +107 -0
  101. atomadic_forge/a3_og_features/forge_evolve.py +176 -0
  102. atomadic_forge/a3_og_features/forge_loop.py +528 -0
  103. atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
  104. atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
  105. atomadic_forge/a3_og_features/lsp_server.py +98 -0
  106. atomadic_forge/a3_og_features/mcp_server.py +160 -0
  107. atomadic_forge/a3_og_features/setup_wizard.py +337 -0
  108. atomadic_forge/a3_og_features/synergy_feature.py +65 -0
  109. atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
  110. atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
  111. atomadic_forge/commands/__init__.py +1 -0
  112. atomadic_forge/commands/_registry.py +36 -0
  113. atomadic_forge/commands/audit.py +142 -0
  114. atomadic_forge/commands/chat.py +133 -0
  115. atomadic_forge/commands/commandsmith.py +178 -0
  116. atomadic_forge/commands/config_cmd.py +145 -0
  117. atomadic_forge/commands/demo.py +142 -0
  118. atomadic_forge/commands/emergent.py +124 -0
  119. atomadic_forge/commands/emergent_then_synergy.py +70 -0
  120. atomadic_forge/commands/evolve.py +122 -0
  121. atomadic_forge/commands/evolve_then_iterate.py +70 -0
  122. atomadic_forge/commands/feature_then_emergent.py +111 -0
  123. atomadic_forge/commands/iterate.py +140 -0
  124. atomadic_forge/commands/synergy.py +96 -0
  125. atomadic_forge/commands/synergy_then_emergent.py +70 -0
  126. atomadic_forge-0.3.2.dist-info/METADATA +471 -0
  127. atomadic_forge-0.3.2.dist-info/RECORD +131 -0
  128. atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
  129. atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
  130. atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
  131. atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,140 @@
1
+ """``forge iterate`` — LLM ↔ Forge loop (the headline play).
2
+
3
+ Plug Forge's tier-law substrate in front of any LLM (Anthropic / OpenAI /
4
+ Ollama / a stub for offline runs) and produce architecturally-coherent
5
+ code from intent. Forge enforces the 5-tier law every turn; the LLM only
6
+ ships when wire passes and certify clears the threshold.
7
+
8
+ Examples
9
+ --------
10
+ # With Anthropic (set ANTHROPIC_API_KEY):
11
+ forge iterate "discord bot that summarises uploaded PDFs" ./out
12
+
13
+ # Pre-flight (no LLM call) — show the system + first prompt:
14
+ forge iterate "..." ./out --no-apply
15
+
16
+ # Local Ollama:
17
+ FORGE_OLLAMA=1 FORGE_OLLAMA_MODEL=qwen2.5-coder:7b \\
18
+ forge iterate "..." ./out
19
+
20
+ # Stub mode (deterministic, for tests):
21
+ forge iterate "..." ./out --provider stub
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ from pathlib import Path
28
+ from typing import Annotated
29
+
30
+ import click
31
+ import typer
32
+
33
+ from atomadic_forge.a1_at_functions.provider_resolver import (
34
+ PROVIDER_HELP,
35
+ resolve_provider,
36
+ )
37
+ from atomadic_forge.a3_og_features.forge_loop import run_iterate
38
+
39
+ COMMAND_NAME = "iterate"
40
+ COMMAND_HELP = ("Architecturally-coherent code generation: LLM emits, "
41
+ "Forge enforces, loop iterates until certify clears.")
42
+
43
+
44
+ app = typer.Typer(no_args_is_help=True, help=COMMAND_HELP)
45
+
46
+
47
+ def _resolve_provider(name: str) -> object:
48
+ try:
49
+ return resolve_provider(name)
50
+ except ValueError as exc:
51
+ raise typer.BadParameter(str(exc)) from exc
52
+
53
+
54
+ @app.command("run")
55
+ def run_cmd(
56
+ intent: Annotated[str, typer.Argument(help="One-paragraph description "
57
+ "of what to build.")],
58
+ output: Annotated[Path, typer.Argument(
59
+ file_okay=False, dir_okay=True, resolve_path=True)],
60
+ package: Annotated[str, typer.Option("--package")] = "generated",
61
+ seed_repo: Annotated[list[Path] | None, typer.Option("--seed",
62
+ exists=True, file_okay=False, dir_okay=True, resolve_path=True,
63
+ help="Sibling repo(s) whose catalog is offered to the LLM as building blocks. Repeatable.")] = None,
64
+ provider: Annotated[str, typer.Option("--provider",
65
+ help=PROVIDER_HELP)] = "auto",
66
+ max_iterations: Annotated[int, typer.Option("--max-iterations")] = 5,
67
+ max_fix_rounds: Annotated[int, typer.Option(
68
+ "--max-fix-rounds",
69
+ help="Per-turn budget for compiler-feedback fix rounds (Lane A W3). "
70
+ "When the just-emitted package fails import_smoke, the loop "
71
+ "sends the LLM the error trace and asks for a minimal patch, "
72
+ "up to N times before continuing to the next iterate turn. "
73
+ "Default 0 = disabled.")] = 0,
74
+ target_score: Annotated[float, typer.Option("--target-score")] = 75.0,
75
+ apply: Annotated[bool, typer.Option("--apply/--no-apply")] = True,
76
+ json_out: Annotated[bool, typer.Option("--json")] = False,
77
+ ) -> None:
78
+ """Run the iterate loop."""
79
+ output.mkdir(parents=True, exist_ok=True)
80
+ llm = _resolve_provider(provider)
81
+ try:
82
+ report = run_iterate(
83
+ intent,
84
+ output=output,
85
+ package=package,
86
+ seed_repo=seed_repo,
87
+ llm=llm, # type: ignore[arg-type]
88
+ max_iterations=max_iterations,
89
+ max_fix_rounds=max_fix_rounds,
90
+ target_score=target_score,
91
+ apply=apply,
92
+ )
93
+ except RuntimeError as exc:
94
+ raise click.ClickException(str(exc)) from exc
95
+ if json_out:
96
+ typer.echo(json.dumps(report, indent=2, default=str))
97
+ return
98
+ typer.echo(f"\nForge iterate ({'APPLY' if apply else 'PRE-FLIGHT'})")
99
+ typer.echo("-" * 60)
100
+ typer.echo(f" llm: {report.get('llm')}")
101
+ typer.echo(f" package: {report.get('package')}")
102
+ if not apply:
103
+ typer.echo(" output_root: (none — pre-flight)")
104
+ typer.echo(f" first_prompt: {len(report.get('first_prompt', ''))} chars")
105
+ typer.echo(f" system_prompt: {len(report.get('system_prompt', ''))} chars")
106
+ return
107
+ typer.echo(f" output: {report['output_root']}/src/{report['package']}")
108
+ typer.echo(f" iterations: {report['iterations']}")
109
+ typer.echo(f" files written: {report['files_written_total']}")
110
+ typer.echo(f" converged: {report['converged']}")
111
+ final_wire = report.get('final_wire') or {}
112
+ final_cert = report.get('final_certify') or {}
113
+ typer.echo(f" final wire: {final_wire.get('verdict', '?')} "
114
+ f"({final_wire.get('violation_count', 0)} violations)")
115
+ typer.echo(f" final score: {final_cert.get('score', 0)}/100")
116
+ if final_cert.get("issues"):
117
+ for issue in final_cert["issues"]:
118
+ typer.echo(f" - {issue}")
119
+
120
+
121
+ @app.command("preflight")
122
+ def preflight_cmd(
123
+ intent: Annotated[str, typer.Argument()],
124
+ package: Annotated[str, typer.Option("--package")] = "generated",
125
+ seed_repo: Annotated[Path | None, typer.Option("--seed",
126
+ exists=True, file_okay=False, dir_okay=True, resolve_path=True)] = None,
127
+ ) -> None:
128
+ """Print the system prompt + first user prompt without calling any LLM."""
129
+ from atomadic_forge.a1_at_functions.forge_feedback import (
130
+ pack_initial_intent,
131
+ system_prompt,
132
+ )
133
+ from atomadic_forge.a1_at_functions.scout_walk import harvest_repo
134
+
135
+ seeds = harvest_repo(seed_repo)["symbols"] if seed_repo else None
136
+ typer.echo("# === SYSTEM PROMPT ===\n")
137
+ typer.echo(system_prompt())
138
+ typer.echo("\n# === FIRST USER PROMPT ===\n")
139
+ typer.echo(pack_initial_intent(intent, package=package,
140
+ seed_catalog=seeds))
@@ -0,0 +1,96 @@
1
+ """``atomadic-forge synergy`` — find producer/consumer pairs that aren't wired yet,
2
+ optionally implement an adapter that wires them.
3
+
4
+ Operates one level above ``emergent``: emergent looks at *symbol* compositions
5
+ inside the type graph; synergy looks at *feature/CLI verb* relationships
6
+ across the operator surface (file artifacts, schemas, phase order).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Annotated
14
+
15
+ import typer
16
+
17
+ from atomadic_forge.a3_og_features.synergy_feature import SynergyScan
18
+
19
+ COMMAND_NAME = "synergy"
20
+ COMMAND_HELP = ("Find feature/CLI synergies (producer-consumer pairs that "
21
+ "aren't wired together) and optionally implement adapters.")
22
+
23
+
24
+ app = typer.Typer(no_args_is_help=True, help=COMMAND_HELP)
25
+
26
+
27
+ def _resolve_src_root() -> Path:
28
+ here = Path(__file__).resolve()
29
+ return here.parent.parent.parent # commands/ -> atomadic_forge/ -> src/
30
+
31
+
32
+ @app.command("scan")
33
+ def scan_cmd(
34
+ package: Annotated[str, typer.Option("--package")] = "atomadic_forge",
35
+ src_root: Annotated[Path | None, typer.Option("--src-root",
36
+ exists=True, file_okay=False, dir_okay=True, resolve_path=True)] = None,
37
+ top_n: Annotated[int, typer.Option("--top-n")] = 20,
38
+ json_out: Annotated[bool, typer.Option("--json")] = False,
39
+ save: Annotated[Path | None, typer.Option("--save",
40
+ file_okay=True, dir_okay=False, resolve_path=True)] = None,
41
+ ) -> None:
42
+ """Walk the CLI surface and report top-scoring synergy candidates."""
43
+ root = src_root or _resolve_src_root()
44
+ scanner = SynergyScan(src_root=root, package=package)
45
+ report = scanner.scan(top_n=top_n)
46
+ if save:
47
+ SynergyScan.save_report(report, save)
48
+ if json_out:
49
+ typer.echo(json.dumps(report, indent=2, default=str))
50
+ return
51
+
52
+ typer.echo(f"\nSynergy scan — {package}")
53
+ typer.echo("-" * 60)
54
+ typer.echo(f" features harvested: {report['feature_count']}")
55
+ typer.echo(f" candidates: {report['candidate_count']}\n")
56
+ for i, c in enumerate(report["candidates"], 1):
57
+ typer.echo(f" #{i:2d} {c['candidate_id']} score={c['score']:.0f}")
58
+ typer.echo(f" kind: {c['kind']}")
59
+ typer.echo(f" wire: {c['producer']} → {c['consumer']}")
60
+ typer.echo(f" adapter: {c['proposed_adapter_name']}")
61
+ typer.echo(f" why: {'; '.join(c['why'])}")
62
+ typer.echo("")
63
+
64
+
65
+ @app.command("implement")
66
+ def implement_cmd(
67
+ candidate_id: Annotated[str, typer.Argument()],
68
+ report_path: Annotated[Path, typer.Argument(
69
+ exists=True, file_okay=True, dir_okay=False, resolve_path=True)],
70
+ package: Annotated[str, typer.Option("--package")] = "atomadic_forge",
71
+ src_root: Annotated[Path | None, typer.Option("--src-root",
72
+ exists=True, file_okay=False, dir_okay=True, resolve_path=True)] = None,
73
+ ) -> None:
74
+ """Materialize one candidate as a new commands/<name>.py adapter."""
75
+ report = json.loads(report_path.read_text(encoding="utf-8"))
76
+ scanner = SynergyScan(src_root=src_root or _resolve_src_root(),
77
+ package=package)
78
+ target = scanner.implement(candidate_id, report)
79
+ typer.echo(f"Wrote {target}")
80
+ typer.echo("Run ``atomadic-forge commandsmith sync`` to register the new verb.")
81
+
82
+
83
+ @app.command("show")
84
+ def show_cmd(
85
+ candidate_id: Annotated[str, typer.Argument()],
86
+ report_path: Annotated[Path, typer.Argument(
87
+ exists=True, file_okay=True, dir_okay=False, resolve_path=True)],
88
+ ) -> None:
89
+ """Print one candidate's full breakdown."""
90
+ report = json.loads(report_path.read_text(encoding="utf-8"))
91
+ match = next((c for c in report["candidates"]
92
+ if c["candidate_id"] == candidate_id), None)
93
+ if match is None:
94
+ typer.secho(f"candidate {candidate_id} not in report", fg="red", err=True)
95
+ raise typer.Exit(1)
96
+ typer.echo(json.dumps(match, indent=2))
@@ -0,0 +1,70 @@
1
+ """
2
+ Auto-synthesized synergy adapter (syn-4a31a10c).
3
+
4
+ Producer: synergy
5
+ Consumer: emergent
6
+ Kind: json_artifact
7
+ Score: 42
8
+
9
+ Why this synergy was detected:
10
+ - synergy emits json-out
11
+ - emergent accepts json_out
12
+
13
+ Re-emit with ``atomadic-forge synergy implement <id>`` after surfaces change.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ from pathlib import Path
23
+ from typing import Annotated
24
+
25
+ import typer
26
+
27
+ COMMAND_NAME = 'synergy-then-emergent'
28
+ COMMAND_HELP = 'Run synergy to emit a JSON artifact, then feed it to emergent for the next phase.'
29
+
30
+ app = typer.Typer(no_args_is_help=False, help='Run synergy to emit a JSON artifact, then feed it to emergent for the next phase.')
31
+
32
+ @app.callback(invoke_without_command=True)
33
+ def run(
34
+ ctx: typer.Context,
35
+ producer_args: Annotated[list[str] | None, typer.Argument(
36
+ help='Args forwarded to the producer command.')] = None,
37
+ ) -> None:
38
+ """Run synergy → capture artifact → feed to emergent."""
39
+ if ctx.invoked_subcommand is not None:
40
+ return
41
+ producer_args = producer_args or []
42
+ with tempfile.TemporaryDirectory(prefix='synergy-') as tmp:
43
+ artifact = Path(tmp) / 'producer.json'
44
+ cmd_a = [sys.executable, '-m',
45
+ 'atomadic_forge.a4_sy_orchestration.cli',
46
+ 'synergy', *producer_args,
47
+ '--json-out', str(artifact)]
48
+ rc = subprocess.run(cmd_a, capture_output=False).returncode
49
+ if rc != 0:
50
+ typer.secho(f'producer exited {rc}', fg='red', err=True)
51
+ raise typer.Exit(rc)
52
+ cmd_b = [sys.executable, '-m',
53
+ 'atomadic_forge.a4_sy_orchestration.cli',
54
+ 'emergent', str(artifact)]
55
+ rc = subprocess.run(cmd_b, capture_output=False).returncode
56
+ if rc != 0:
57
+ typer.secho(f'consumer exited {rc}', fg='red', err=True)
58
+ raise typer.Exit(rc)
59
+ try:
60
+ data = json.loads(artifact.read_text(encoding='utf-8'))
61
+ except (OSError, json.JSONDecodeError):
62
+ data = None
63
+ typer.echo(json.dumps({
64
+ 'synergy': 'syn-4a31a10c',
65
+ 'producer': 'synergy',
66
+ 'consumer': 'emergent',
67
+ 'artifact_size_bytes': artifact.stat().st_size,
68
+ 'producer_payload_keys': sorted(data.keys()) if isinstance(data, dict) else None,
69
+ }, indent=2))
70
+