Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eamos-core/orchestrator/os/deck-ir/_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ archetype; the gates run **on the IR**; the cosmetic `emit_*` hop turns it into
DOCX / SVG / XLSX. Same manifest → same IR → same bytes.

```yaml
ir_version: <int> # the IR contract version (#62); emitters refuse a mismatch
generator: eamos-render # provenance of the artifact
deliverable: <type, e.g. presentation> # from the manifest's deliverables[] (RFC-0002 §4)
format: <e.g. speaker | detailed> # presentation render mode
length: <short | medium | long>
Expand Down Expand Up @@ -42,3 +44,8 @@ review_appendix: # the "verify before the room
never drops a `required` section).
- **Deterministic serialization.** JSON, `indent=2`, `ensure_ascii=False`, insertion order;
`review_appendix` sorted by binding key. No clocks, no randomness, no filesystem order.
- **Versioned contract (#62).** Every projection (all six IR families) stamps `ir_version` +
`generator`. `ir_version` increments on **any breaking change to block/field shapes**; every
emitter validates it before rendering and refuses a mismatch (or an absent field — a pre-#62
artifact) with *"re-render the manifest"* — a persisted `build/*.json` outlives tool runs, and a
silent mis-render is worse than a one-command re-render.
9 changes: 9 additions & 0 deletions .eamos-core/tools/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ def read_text(path, what="file"):
return fh.read()


def require_ir_version(ir, tool, supported):
"""Refuse an IR artifact from a different contract version (#62). A persisted build/*.json
outlives tool runs — a silent mis-render is worse than a one-command re-render."""
v = ir.get("ir_version") if isinstance(ir, dict) else None
if v != supported:
raise SystemExit(f"{tool}: IR version {v!r} not supported (expected {supported}); "
"re-render the manifest")


def utf8_stdio():
"""Force UTF-8 stdout/stderr regardless of the platform code page. Call first in every main()."""
for stream in (sys.stdout, sys.stderr):
Expand Down
3 changes: 3 additions & 0 deletions .eamos-core/tools/emit_docx.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import _cli # utf8_stdio (#54)
import labels # chrome labels as data (#61)

SUPPORTED_IR_VERSION = 1 # the IR contract this emitter was written for (#62)

AMBER = (0xB8, 0x6B, 0x00)
MUTED = (0x70, 0x70, 0x70)

Expand Down Expand Up @@ -128,6 +130,7 @@ def main():

with open(args.deck_ir, encoding="utf-8") as fh:
ir = json.load(fh)
_cli.require_ir_version(ir, "emit_docx", SUPPORTED_IR_VERSION)
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
n = build_docx(ir, args.out)
print(f"emit_docx: OK — pre-read ({n} paragraphs) -> {args.out}")
Expand Down
3 changes: 3 additions & 0 deletions .eamos-core/tools/emit_md.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import _cli # utf8_stdio (#54)
import labels # chrome labels as data (#61)

SUPPORTED_IR_VERSION = 1 # the IR contract this emitter was written for (#62)


def _lab(lang, key):
# Template chrome in the deck's output language (the section labels, not the content).
Expand Down Expand Up @@ -111,6 +113,7 @@ def main():

with open(args.deck_ir, encoding="utf-8") as fh:
ir = json.load(fh)
_cli.require_ir_version(ir, "emit_md", SUPPORTED_IR_VERSION)
text = render_quiz_md(ir) if ir.get("deliverable") == "interview_quiz" else render_md(ir)
if args.out:
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
Expand Down
3 changes: 3 additions & 0 deletions .eamos-core/tools/emit_pptx.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import _cli # utf8_stdio (#54)
import labels # chrome labels as data (#61)

SUPPORTED_IR_VERSION = 1 # the IR contract this emitter was written for (#62)

NAVY = (0x1E, 0x27, 0x61) # title
BODY = (0x2B, 0x2B, 0x2B) # body text
AMBER = (0xB8, 0x6B, 0x00) # assumed / to-verify
Expand Down Expand Up @@ -144,6 +146,7 @@ def main():

with open(args.deck_ir, encoding="utf-8") as fh:
ir = json.load(fh)
_cli.require_ir_version(ir, "emit_pptx", SUPPORTED_IR_VERSION)
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
n = build_pptx(ir, args.out)
print(f"emit_pptx: OK — {n} slides -> {args.out}")
Expand Down
3 changes: 3 additions & 0 deletions .eamos-core/tools/emit_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import _cli # utf8_stdio (#54)
import labels # chrome labels as data (#61)

SUPPORTED_IR_VERSION = 1 # the IR contract this emitter was written for (#62)

# Per visual_style palette (background, accent, body, amber, muted, card). Unknown styles fall back
# to professional. Layout is shared; only the palette/theme changes (RFC-0002 §11-1).
THEMES = {
Expand Down Expand Up @@ -244,6 +246,7 @@ def main():

with open(args.ir_json, encoding="utf-8") as fh:
ir = json.load(fh)
_cli.require_ir_version(ir, "emit_svg", SUPPORTED_IR_VERSION)
deliv = ir.get("deliverable")
if deliv == "mindmap":
svg, kind, note = build_mindmap_svg(ir), "mindmap", f"{len(ir.get('branches', []))} branches"
Expand Down
3 changes: 3 additions & 0 deletions .eamos-core/tools/emit_xlsx.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import _cli # utf8_stdio (#54)
import labels # chrome labels as data (#61)

SUPPORTED_IR_VERSION = 1 # the IR contract this emitter was written for (#62)

def _lab(lang, key):
return labels.lab(lang, "xlsx", key) # chrome labels as data (#61)

Expand Down Expand Up @@ -97,6 +99,7 @@ def main():

with open(args.data_ir, encoding="utf-8") as fh:
ir = json.load(fh)
_cli.require_ir_version(ir, "emit_xlsx", SUPPORTED_IR_VERSION)
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
n = build_xlsx(ir, args.out)
print(f"emit_xlsx: OK — KPI table ({n} rows) -> {args.out}")
Expand Down
20 changes: 17 additions & 3 deletions .eamos-core/tools/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
ROUTING = os.path.join(CORE, "orchestrator", "os", "intake", "routing.yaml")

BIND_RE = re.compile(r"\{\{\s*([a-z][a-z0-9_.]*)\s*\}\}")
# The IR contract version (#62), stamped into every projection. The IR is a persisted artifact
# (build/*.json) that outlives tool runs and crosses the determinism boundary into the emitters —
# bump on ANY breaking change to block/field shapes, so an emitter refuses instead of mis-rendering.
IR_VERSION = 1
GENERATOR = "eamos-render"
# The provenance enum. Anything else — a typo, a missing field, a future value — is treated as
# assumed (fail closed, #53) and flagged for the grounding gate: only the explicit `sourced`
# may render plain.
Expand Down Expand Up @@ -360,6 +365,8 @@ def build_deck_ir(manifest, archetype, altitude=None, function=None):
]

deck_ir = {
"ir_version": IR_VERSION,
"generator": GENERATOR,
"deliverable": deliverable.get("type", "presentation"),
"format": deliverable.get("format", "speaker"),
"length": deliverable.get("length", "medium"),
Expand Down Expand Up @@ -424,6 +431,8 @@ def build_infographic_ir(manifest, archetype, altitude=None, function=None):
for k in sorted(acc["assumed"])
]
info_ir = {
"ir_version": IR_VERSION,
"generator": GENERATOR,
"deliverable": "infographic",
"orientation": params.get("orientation", "portrait"),
"visual_style": params.get("visual_style", "professional"),
Expand Down Expand Up @@ -485,6 +494,8 @@ def build_data_ir(manifest, archetype, altitude=None, function=None):
for k in sorted(acc["assumed"])
]
return {
"ir_version": IR_VERSION,
"generator": GENERATOR,
"deliverable": "table_chart",
"chart": params.get("chart", "none"),
"output_lang": lang,
Expand Down Expand Up @@ -531,7 +542,8 @@ def build_graph_ir(manifest, archetype, altitude=None, function=None):
"assumption": ledger[k].get("assumption", ""), "fill_from": ledger[k].get("fill_from", "")}
for k in sorted(acc["assumed"])
]
return {"deliverable": "mindmap", "output_lang": lang, "root": manifest.get("objective", ""),
return {"ir_version": IR_VERSION, "generator": GENERATOR,
"deliverable": "mindmap", "output_lang": lang, "root": manifest.get("objective", ""),
"branches": branches, "review_appendix": review_appendix}, acc


Expand Down Expand Up @@ -570,7 +582,8 @@ def build_quiz_ir(manifest, archetype, altitude=None, function=None):
"assumption": ledger[k].get("assumption", ""), "fill_from": ledger[k].get("fill_from", "")}
for k in sorted(acc["assumed"])
]
return {"deliverable": "interview_quiz", "output_lang": lang, "title": manifest.get("objective", ""),
return {"ir_version": IR_VERSION, "generator": GENERATOR,
"deliverable": "interview_quiz", "output_lang": lang, "title": manifest.get("objective", ""),
"questions": questions, "review_appendix": review_appendix}, acc


Expand Down Expand Up @@ -611,7 +624,8 @@ def build_topology_ir(manifest, archetype=None, altitude=None, function=None):
"assumption": ledger[k].get("assumption", ""), "fill_from": ledger[k].get("fill_from", "")}
for k in sorted(acc["assumed"])
]
return {"deliverable": "architecture", "output_lang": lang, "title": manifest.get("objective", ""),
return {"ir_version": IR_VERSION, "generator": GENERATOR,
"deliverable": "architecture", "output_lang": lang, "title": manifest.get("objective", ""),
"nodes": nodes, "edges": edges, "review_appendix": review_appendix}, acc


Expand Down
27 changes: 27 additions & 0 deletions .eamos-core/tools/tests/test_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,33 @@ def test_empty_identity_preps_without_traceback(self):
self.assertEqual(quiet(facilitate.prep, p, 60), 0)


class TestIRVersionGate(unittest.TestCase):
def test_emitter_rejects_unversioned_ir(self):
# A pre-#62 (or future) artifact must be refused with a one-command recovery, never
# mis-rendered silently.
import subprocess
with tempfile.TemporaryDirectory() as d:
p = os.path.join(d, "old-ir.json")
with open(p, "w", encoding="utf-8") as fh:
json.dump({"deliverable": "presentation", "slides": []}, fh)
out = subprocess.run([sys.executable, os.path.join(TOOLS, "emit_md.py"), p],
capture_output=True, text=True)
self.assertNotEqual(out.returncode, 0)
self.assertIn("re-render the manifest", out.stderr)
self.assertNotIn("Traceback", out.stderr)

def test_emitter_accepts_current_ir(self):
import subprocess
with tempfile.TemporaryDirectory() as d:
ir_path, md_path = os.path.join(d, "ir.json"), os.path.join(d, "deck.md")
r1 = subprocess.run([sys.executable, os.path.join(TOOLS, "render.py"), Q3, "--out", ir_path],
capture_output=True, text=True)
self.assertEqual(r1.returncode, 0, r1.stderr)
r2 = subprocess.run([sys.executable, os.path.join(TOOLS, "emit_md.py"), ir_path, "--out", md_path],
capture_output=True, text=True)
self.assertEqual(r2.returncode, 0, r2.stderr)


class TestPipedStdout(unittest.TestCase):
def test_piped_stdout_survives_non_utf8_code_page(self):
# On Windows a piped stdout picks the ANSI code page (cp1252), which cannot encode the
Expand Down
11 changes: 11 additions & 0 deletions .eamos-core/tools/tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ def test_invalid_provenance_fails_closed_in_topology(self):
self.assertIn("sys.platform", acc["invalid_provenance"])


class TestIRVersion(unittest.TestCase):
def test_every_projection_is_stamped(self):
m, arch = load("qbr-c-level")
for builder in (render.build_deck_ir, render.build_infographic_ir, render.build_data_ir,
render.build_graph_ir, render.build_quiz_ir, render.build_topology_ir):
with self.subTest(ir=builder.__name__):
ir, _ = builder(m, arch)
self.assertEqual(ir["ir_version"], render.IR_VERSION) # versioned contract (#62)
self.assertEqual(ir["generator"], "eamos-render")


class TestDecisionContract(unittest.TestCase):
def test_decision_contract_renders_through_md(self):
import emit_md
Expand Down
Loading