Feature: Master-Comparison Mode for Cross-Language Conformance
Adds an automated diff between a candidate language's E2E test output and TypeScript's (the master implementation's) output for the same run, closing the one real gap identified in the multi-language conformance investigation.
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md - The implementation process
- PLANS.md - Plan structure and best practices
Status: Completed
Completed: 2026-07-08
Investigation: INVESTIGATE-multi-language-conformance.md — Option B, accepted 2026-07-08
Goal: A re-runnable command that proves (or disproves) "candidate language X produces the same output as TypeScript for the same input," reusing the existing schema/test/tool infrastructure rather than replacing it.
Last Updated: 2026-07-08
Problem Summary
specification/tools/ already validates that a single implementation's output is well-formed and internally consistent (file → OTLP → Loki/Prometheus/Tempo → Grafana). Nothing compares two implementations' outputs against each other. The one time this comparison happened, it was manual and one-off (python/llm-work/FINAL_COMPARISON.md, 2025-10-28) — a hand-written table, never re-run, now 8+ months stale. This let three real bugs (metric-naming, enum-conversion, missing-field) ship undetected until someone happened to check the right Grafana panel.
This plan builds the missing piece: a script that runs TypeScript's and a candidate language's fixed E2E scenario (08-testprogram-company-lookup.md), captures both file logs, and diffs them field-by-field — with TypeScript's live output as the permanent answer key, never a stored fixture.
Explicitly out of scope for this plan (deferred to PLAN-002-python-conformance.md): actually running this against Python and fixing what it finds. This plan only builds and proves the tool works.
Phase 1: Design — comparison boundary, matching strategy, normalization rules
Tasks
- 1.1 Confirm the comparison boundary is file-log JSON only (
logs/dev.log/logs/error.log, both languages already write these — confirmed present for both TypeScript and Python). Do not add OTLP-wire or Grafana-API comparison in this plan; note it as a possible future extension if file-log comparison proves insufficient. Confirmed sufficient — see 1.2. - 1.2 Reconstruct the historical enum-conversion bug's exact code path. Confirmed by reading current
python/src/logger.py:level_str = level.value if isinstance(level, SOVDEV_LOGLEVELS) else str(level)is computed once, at each public API entry point (sovdev_log,sovdev_log_job_status, etc., lines 890/916/944), then passed down intowrite_log(level, log_entry), which uses that same string both to setlog_entry["level"](the file log) and to computemap_to_python_level(level)(the OTLP severity mapping). One shared conversion feeds both outputs — the historical bug would have corrupted both simultaneously. File-log-only comparison is sufficient for this bug class, independently confirmed by the fact thatlog-entry-schema.json's strictlevelenum would also reject a bad value like"SOVDEV_LOGLEVELS.ERROR"outright. The Loki-severity schema gap (Phase 3) remains worth closing as defense-in-depth for the OTLP path specifically, but it is not the only thing that would have caught this bug. - 1.3 Entry-matching strategy: position-based, confirmed sound —
company-lookup.ts/.pyexecute the same fixed, deterministic sequence of log calls (verified: transaction logs with/without response, one with an exception,job.statusstart/complete, 4×job.progress). If entry counts differ between the two files, report that as a top-level mismatch before attempting per-entry comparison. - 1.4 Normalization rules, finalized against the actual schema (
specification/schemas/log-entry-schema.json's full field list) and the actual test scripts:- Excluded from comparison (legitimately/expectedly different per run or per language, not compared at all):
timestamp,trace_id,span_id,event_id,session_id(all per-run values);service_name(differs by design — confirmed both test scripts defaultOTEL_SERVICE_NAMEtosovdev-test-company-lookup-{typescript,python}, a language-specific suffix);exception_stacktrace(language-specific formatting/file-paths/frame text will never literally match across languages — check presence only: non-empty whenexception_typeis set, not content equality) - Must match exactly:
level,message,service_version(confirmed hardcoded"1.0.0"in both test scripts — legitimately comparable, unlikeservice_name),function_name,log_type,input_json,response_json,exception_type,exception_message peer_service— conditionally excluded, refined during Phase 4's real run (this file's original version had it as a flat "must match exactly" field; that produced false positives the moment real data was compared — see Phase 4.2's peer_service note):PEER_SERVICES.INTERNALresolves at runtime to that same log entry's ownservice_namein both languages, so it inheritsservice_name's legitimate per-language difference. Only comparepeer_servicewhen it's not self-referential (i.e., a real external ID like"SYS1234567", confirmed to match exactly for those entries) — skip the comparison when both sides showpeer_service == service_nameon their own side, and flag an INTERNAL-resolution mismatch if only one side does.
- Excluded from comparison (legitimately/expectedly different per run or per language, not compared at all):
- 1.5 Name and place the new files:
specification/tests/compare-log-files.py(the comparison logic, alongside the existingvalidate-*.pyvalidators) andspecification/tools/compare-with-master.sh(a thin bash wrapper matching the existingquery-*.sh/validate-log-format.shconventions).
Validation
User confirms the design decisions above (especially 1.2's finding and 1.3's matching strategy) before implementation starts.
Phase 1: DONE — all design decisions confirmed against actual code/schema, not assumed.
Phase 2: Implement the comparator
Tasks
- 2.1 Wrote
specification/tests/compare-log-files.py: loads two NDJSON log files, applies the Phase 1 normalization rules, position-matches entries, reports every field-level mismatch (entry index, field name, TypeScript's expected value, candidate's actual value), plus a top-level entry-count-mismatch check.py_compileclean. - 2.2
--jsonoutput implemented, matchingvalidate-log-format.py's convention exactly (sameColors/print_*helper shape). - 2.3 Wrote
specification/tools/compare-with-master.sh <candidate-language>: locates both log files, verifies both exist with a clear error naming the missingrun-test.shstep, and invokescompare-log-files.py— viadct-execwhen run from the host (not the legacy hardcoded-container-name pattern the three older scripts still use), directly when already inside the devcontainer.bash -nclean;shellcheckclean except two infos/warnings that exactly match the pre-existing convention already used byvalidate-log-format.sh(unquoted${options}word-splitting,options="$@") — left consistent with the codebase rather than "fixed" into an inconsistent one-off style. - 2.4 Added Step 9: Master Comparison to
specification/tools/README.md's validation sequence, explaining why it's the authoritative "identical output" check (Steps 1-8 confirm internal consistency, not cross-language match). Updatedspecification/README.md's Success Criteria: criterion 5 now requirescompare-with-master.sh {language}to pass, replacing the old unverifiable "output structure identical to TypeScript reference" bullet.
Verified against synthetic fixtures before any real devcontainer run (Phase 4 does the real-implementation run): confirmed the comparator (a) passes when only the excluded/normalized fields differ, (b) catches the historical enum-conversion bug pattern with a precise diagnosis, (c) catches a dropped required field the same way, and (d) reports an entry-count mismatch cleanly rather than crashing or comparing misaligned entries. All four in both human-readable and --json modes.
Validation
cd /workspace/typescript/test/e2e/company-lookup && ./run-test.sh
cd /workspace/python/test/e2e/company-lookup && ./run-test.sh
cd /workspace/specification/tools && ./compare-with-master.sh python
User confirms the command runs and produces readable output (pass or fail) against the current, real Python implementation. This real-devcontainer run is Phase 4's job (this session only has host access; Phase 4 requires the actual E2E tests, which need the OTel toolchain and DevContainer-only dependencies).
Phase 2: DONE — code written and verified against synthetic fixtures; real-implementation run deferred to Phase 4 as planned.
Phase 3: Close the confirmed Loki severity-validation gap
Tasks
- 3.1 Added
severity_textandseverity_numberconstraints tospecification/schemas/loki-response-schema.json'sstreamdefinition (the same placeservice_name/service_versionare already constrained — confirmed viavalidate-loki-response.pythat severity fields are checked as stream labels, not inside the parsedlog_entrybody). Values grounded in evidence, not guessed:severity_text: enum["TRACE","DEBUG","INFO","WARN","ERROR","FATAL"]— uppercase, perspecification/02-field-definitions.md's documented examples and every JSON example across03-implementation-patterns.md/06-test-scenarios.mdseverity_number: typed as a string matching^([1-9]|1[0-9]|2[0-4])$(1-24, OTel'sSeverityNumberrange), not a native integer — caught this myself during testing: Loki stream labels are always strings (map[string]string), matching this same schema's existingpatternPropertiesconvention that types every other label asstring. My first draft used"type": "integer", which would have been a real bug (false negatives on any correctly-typed real Loki response) had it shipped untested.
- 3.2 Verified empirically, no code changes needed:
validate-loki-response.py'sDraft7Validatorruns against the whole response (Step 1, before the presence-only check in Step 2), and the schema'sdata.result[].itemsalready$refs thestreamdefinition — so the new constraints are picked up automatically.
Verified against synthetic Loki-response fixtures: a bad severity_text ("SOVDEV_LOGLEVELS.ERROR") is now rejected with a precise diagnosis; a realistic, correctly-typed valid response passes; an out-of-range severity_number ("99") is rejected. Schema re-validated as well-formed JSON after editing.
Phase 3: DONE.
Validation
Run validate-loki-response.py against a captured Loki response containing a deliberately-wrong severity_text (e.g., manually edit a captured JSON fixture to say "SOVDEV_LOGLEVELS.ERROR") and confirm it now fails where it previously would have passed. Done above, with synthetic fixtures (no real Loki access from this host session); should be spot-checked once more against a real captured response during Phase 4/PLAN-002, but the schema-level behavior is already proven correct.
Phase 4: Prove the comparator actually catches regressions
Tasks
- 4.1 Sanity check, real devcontainer: built TypeScript (
build-sovdevlogger.sh) and installed Python'srequirements.txt, ran TypeScript's E2E test twice independently, compared run 1 against run 2 — zero mismatches. No false positives from run-to-run variance. - 4.1a (unplanned, discovered during first real cross-language run) First real TypeScript-vs-Python comparison produced spurious
peer_servicemismatches on everyINTERNAL-type entry. Root-caused (not just patched around):PEER_SERVICES.INTERNALresolves at runtime to that entry's ownservice_name, confirmed by inspecting raw log entries from both languages. Fixedcompare-log-files.py'speer_servicehandling per the updated Phase 1.4 rule above; re-ran and confirmed the false positives were gone while realpeer_servicevalues (e.g.SYS1234567) still compared correctly. - 4.2 Synthetic regression test, all three historically-documented bugs, each reintroduced in isolation (backed up the real
python/src/logger.py, swapped in a one-line-modified copy, ran the real E2E test, compared, then restored and verifieddiffshowed the file identical again before moving to the next bug — never left the real file mutated). Result: mixed, and more precise than Phase 1 assumed — see "Correction" below.- Enum conversion (
str(level)instead of.value): caught immediately —Entry 11, field 'level': expected 'error' (TypeScript), got 'info'. Confirms Phase 1.2's finding: this bug's shared code path does hit the file log. - Dots in metric names: not observable in file-log comparison, and was never going to be — metrics are a separate OTel signal that never touches
dev.log. Confirmed empirically (zero new diffs after reintroducing) and by design (there is no metric data anywhere in a log file). - Missing timestamp in OTLP export: not observable in file-log comparison either, and for the same class of reason — confirmed by reading
write_log(): the file log is serialized fromlog_entrydirectly (JSONFileHandler, a completely separate code path from theextradict this bug affected, which only ever fed the OTLP/Loki export).
- Enum conversion (
- 4.3 All scratch files and backups removed;
git statusonpython/src/logger.pyconfirmed clean (anddiffagainst the pre-edit backup confirmed byte-identical) after each of the two real-file swaps, and again at the end. No implementation code changes survive this plan.
Validation
Two of the three historically-documented bugs are structurally invisible to file-log comparison — this needed correcting, not just confirming. Phase 1 and the parent investigation both said file-log comparison "would have caught all three known Python bugs" (see INVESTIGATE-multi-language-conformance.md, Option B's pros, and this plan's own original Phase 1.2 framing). That's wrong for two of the three: metric-naming and the OTLP-only timestamp bug never touch a log file at all, by construction — no amount of file-log diffing was ever going to catch them. Only the enum-conversion bug is genuinely file-log-visible (which Phase 1.2's actual code-tracing was correct about, specifically). Corrected the overclaim in INVESTIGATE-multi-language-conformance.md (see that file's own revision note) rather than let it stand now that it's been empirically tested rather than assumed. File-log comparison remains valuable and worth having — it's just not the complete answer to "identical output," and a future extension to OTLP-payload or metrics comparison (noted as out-of-scope in Phase 1.1) would be needed to close that specific gap.
Bonus finding, not scratch/synthetic — this is the real, current Python implementation: comparing today's actual python/src/logger.py output against TypeScript's surfaced genuine, previously-undocumented discrepancies (none of the three historical bugs — those are legitimately fixed):
response_jsonis completely missing (not evennull) on 13 of 17 entries where TypeScript includes it asnull— a live violation of the documented "always includeresponse_json, never omit it" design principle (00-design-principles.mddecision,07-anti-patterns.md's "DON'T: Conditionally add responseJSON field")exception_messagehas a trailing-space difference on both error entries: TypeScript's"HTTP 404: "vs. Python's"HTTP 404:"(missing space before the emptystatusText)
These are real findings for PLAN-002 to fix — not discarded, unlike the synthetic regressions.
Acceptance Criteria
-
specification/tools/compare-with-master.sh <language>exists, is documented inspecification/tools/README.md, and runs from inside the devcontainer per the existing tool conventions - Comparing TypeScript against itself produces zero mismatches
- Of the three historically-documented bug patterns, the one that's actually observable in a file log (enum conversion) is caught with a correct, specific diagnosis when synthetically reintroduced; the other two (metric naming, OTLP-only timestamp) are confirmed — empirically, not assumed — to be outside file-log comparison's reach by construction, and this limitation is now documented rather than silently missed
-
specification/schemas/loki-response-schema.jsonconstrainsseverity_text/severity_number, closing the gap confirmed during the investigation - No changes made to
python/src/logger.py,typescript/src/logger.ts, or any other implementation code — verified viagit statusand byte-for-bytediffafter every real-file swap during Phase 4's synthetic testing
Phase 4: DONE. All phases complete.
Implementation Notes
- TypeScript is the master, not a stored fixture — the comparator always runs TypeScript's test fresh and diffs against that run's actual output, never a checked-in "golden" log file. This avoids the exact staleness problem
FINAL_COMPARISON.mdhad. - Keep
compare-log-files.pyindependent of any single language pair — its interface should be "two log file paths in, mismatch report out," socompare-with-master.sh goorcompare-with-master.sh csharpwork identically once those implementations exist, with no changes to the comparator itself.
Files to Modify
specification/tests/compare-log-files.py(new)specification/tools/compare-with-master.sh(new)specification/tools/README.md(add new step to validation sequence)specification/README.md(update Success Criteria)specification/schemas/loki-response-schema.json(addseverity_text/severity_numberconstraints)