Fix: Python conformance — close the two real gaps PLAN-001 found, then promote
Fixes the two genuine, currently-existing discrepancies PLAN-001's master-comparison mode found between Python and TypeScript, re-validates until compare-with-master.sh python passes cleanly, then executes the promotion step that's never happened: flipping Python from "📅 Planned" to "✅ Available" in README.md, with its own quickstart doc.
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 C, accepted 2026-07-08
Prerequisite: PLAN-001-master-comparison-mode.md — completed, found these bugs
Goal: compare-with-master.sh python passes with zero mismatches, and README.md/python/README.md accurately reflect that Python is a real, available implementation — not an LLM's self-declared verdict in a file nobody checks (per the investigation's [Q6] decision).
Last Updated: 2026-07-08
Problem
PLAN-001 ran the real, current Python implementation against TypeScript and found two genuine bugs (not the three historical ones — those are already fixed). Both root-caused precisely before writing this plan, not assumed:
Bug 1: response_json dropped entirely on most entries (real library bug)
python/src/logger.py's JSONFormatter.format() (line ~172) correctly sets log_entry["response_json"] = record.response_json, which is legitimately None on most log calls (job status, job progress, errors, and the initial "starting lookup" entries all have no response yet). But line 181 then unconditionally calls:
def remove_undefined_fields(obj: Dict[str, Any]) -> Dict[str, Any]:
"""Remove None fields for cleaner JSON output."""
return {k: v for k, v in obj.items() if v is not None}
This strips every None-valued key, including the intentionally-null response_json — this is exactly the anti-pattern 07-anti-patterns.md already documents ("DON'T: Conditionally add responseJSON field ... Always add responseJSON, value null when no response exists"). Confirmed empirically: entries with an actual response value keep the field; entries with None lose it entirely.
Bug 2: exception_message trailing-space difference (test script bug, not the library)
Not a logger.py bug — the two E2E test programs construct their error messages differently:
// typescript/test/e2e/company-lookup/company-lookup.ts:151
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
# python/test/e2e/company-lookup/company-lookup.py:77
raise Exception(f'HTTP {response.status_code}:')
TypeScript includes the raw response body (data) after the status code — for this test's 404 response, data is an empty string, so the message ends up "HTTP 404: " (trailing space, then nothing). Python's version never includes the response body at all, and has no trailing space either. This is a test-scenario inconsistency, not a logger library defect — the fix belongs in company-lookup.py, not logger.py.
Phase 1: Fix the real library bug (response_json)
Tasks
- 1.1 Removed the
remove_undefined_fields()call (and the now-dead helper function itself — its only call site) fromJSONFormatter.format(). Confirmed no other field reaching that dict construction can legitimately be "present but should be null-stripped" —span_idand the exception fields are only ever added when they have a real value, so the call was doing nothing correct for anything except wrongly strippingresponse_json. - 1.2 Found a deeper issue while auditing
input_json, not just confirming one: entries 1 and 17 (the lifecycle-only "Company Lookup Service started/finished" logs) started failing after the 1.1 fix — TypeScript's test script callssovdev_log(...)with only 4 positional args for these two calls (noinput_json/response_jsonat all), andtypescript/src/logger.ts's ownremove_undefined_fields(value !== undefined, confirmed by reading it) preserves explicitnullbut omits truly-undefinedfields. Python's plainNone-default parameters can't distinguish "caller omitted the argument" from "caller explicitly passedNone" — both collapse to the same value. Fixed properly with a_NOT_PROVIDEDsentinel (module-level, documented inline):sovdev_log()'s and.log()'sinput_json/response_jsondefaults changed fromNoneto_NOT_PROVIDED;create_log_entry()now only adds the key tolog_entrywhen the value isn't the sentinel — omitted when not passed,nullwhen explicitly passed asNone, exactly matching TypeScript's actual (not just documented) behavior. Scoped narrowly tosovdev_log()—sovdev_log_job_status/sovdev_log_job_progressalways synthesize a realinput_jsondict internally and have noresponse_jsonparameter at all, so they were never exposed to this. - 1.3 Confirmed no regression:
write_log()'sif "input_json"/"response_json" in log_entrychecks (unchanged) already correctly handle the new conditional-omission — a key genuinely absent fromlog_entrywas always going to be skipped there.span_idand exception fields are unaffected (they were never using_NOT_PROVIDEDand were already only added when a real value existed).
Validation
cd /workspace/python/test/e2e/company-lookup && ./run-test.sh --skip-validation
cd /workspace/specification/tools && ./compare-with-master.sh python
Done, against the real devcontainer (built once for PLAN-001, still running): response_json/input_json mismatches fully gone. compare-with-master.sh python now reports only the 2 exception_message errors that are Phase 2's scope — confirming this fix is complete and didn't regress anything else.
Phase 1: DONE.
response_json-related mismatches gone from the output; no new mismatches introduced (e.g., a wrongly-added span_id: null on entries that shouldn't have one).
Phase 2: Fix the test-script inconsistency (exception_message)
Tasks
- 2.1 Confirmed empirically before changing anything: made the exact same live request Python's test makes (
GET https://data.brreg.no/enhetsregisteret/api/enheter/974652846) and checkedresponse.textdirectly —''(empty string), matching TypeScript's accumulateddatafor the same request. Updatedcompany-lookup.py's error-raising toraise Exception(f'HTTP {response.status_code}: {response.text}'), matchingcompany-lookup.ts's`HTTP ${res.statusCode}: ${data}`exactly.
Validation
cd /workspace/python/test/e2e/company-lookup && ./run-test.sh --skip-validation
cd /workspace/specification/tools && ./compare-with-master.sh python
Done, against the real devcontainer: zero mismatches. compare-with-master.sh python now reports ✅ All 17 entries match TypeScript's output — the first automated, re-runnable proof of "identical output" this project has ever had.
Phase 2: DONE.
Phase 3: Full conformance validation
Tasks
- 3.1
compare-with-master.sh python(via direct comparator invocation, real devcontainer): zero mismatches, confirmed at the end of Phase 2 already. - 3.2 Ran what this environment allows:
run-full-validation.sh pythonfails immediately at itskubectlcluster check (no Kubernetes/monitoring stack reachable from this session) — expected and out of scope to fix here. Ran the file-log-only validator directly instead:validate-log-format.pyondev.logpasses cleanly (17/17 entries, schema-valid). Found a real but pre-existing, universal bug while running--error-logmode:_validate_error_log()checkslog.get("severity") != "ERROR", but the current schema useslevel/lowercase, notseverity/uppercase — it fails on every log, not just Python's. Confirmed by running the identical check against TypeScript'serror.log: fails identically. This is a bug invalidate-log-format.pyitself, unrelated to this plan's scope (it doesn't discriminate between implementations — it's broken for the already-"✅ Available" master too) — noted for a future fix, not blocking this plan or casting doubt on Python's conformance. OTLP/Loki/Prometheus/Tempo/Grafana steps were not run (require the Kubernetes stack, unavailable from this session). - 3.3 Re-confirmed by direct code read (not full re-investigation): metric names still use underscores (
sovdev_operations_totaletc.), enum conversion still uses.valuenotstr(), and the OTLPextradict still includestimestamp. All three historically-documented bugs remain fixed.
Validation
compare-with-master.sh python exit code 0 — confirmed. This is the actual, automated "done" signal per the investigation's [Q6] decision — not a self-written summary in python/llm-work/.
Phase 3: DONE, with the caveat noted above: OTLP-backend/Grafana validation wasn't possible from this host-only session (no Kubernetes cluster reachable). File-log conformance — the thing this plan is actually about — is fully verified.
Phase 4: The promotion step (the thing that's never actually happened)
Tasks
- 4.1 Wrote
python/README.md, portingtypescript/README.md's structure — checked Python's actual public API (python/src/__init__.py's__all__) against every code example before writing it, not assumed. Found two more real, honest-to-document gaps this way: Python has nosovdev_validate_config()/sovdev_test_otlp_connection()(TypeScript has both — flagged explicitly in a "Not yet available" section rather than silently omitted or, worse, documented as if they existed), andsovdev_log()has no manualtrace_idoverride parameter (noted inline). Also caught thatsovdev_flush()is synchronous in Python, unlike TypeScript'sasyncversion — every example uses it correctly. Verified the Quick Start example actually runs, not just read for plausibility: ran it against the real devcontainer, confirmed console output and the exact JSON file content it produces. - 4.2 Updated all 7 "Python...Planned" mentions in
README.md(found by grep, not assumed complete): tagline, Supported Languages table, Choose Your Path list, Quick Start section (added a real Python subsection with install/link, matching TypeScript's), Documentation section, Repository Status. Also fixed an unrelated stale reference found while editing this exact area:README.mdpointed at a script,run-company-lookup-validate.sh, that has never existed inspecification/tools/— corrected to the realrun-full-validation.sh/compare-with-master.sh. And updated Repository Status's spec version (v1.1.0→v2.0.0, matchingspecification/README.md's own current footer) since it was directly adjacent and already stale. - 4.3
python/README.mdlinked fromREADME.mdin the same 3 placestypescript/README.mdis (Supported Languages table, Choose Your Path, Documentation section).
Validation
Re-ran compare-with-master.sh python (via direct comparator invocation) one final time after all Phase 4 edits, immediately before considering this done: zero mismatches, unchanged from Phase 2/3. Phase 4 only touched docs, not python/src/, so this was a sanity check that nothing drifted, not an expectation of new findings.
Phase 4: DONE. All phases complete.
Acceptance Criteria
-
compare-with-master.sh pythonpasses with zero mismatches — confirmed multiple times, most recently after all Phase 4 doc edits -
python/src/logger.py'sresponse_json/input_jsonfix doesn't regress genuinely-optional fields —span_idand exception fields were never touched by the sentinel change, confirmed by code read -
python/test/e2e/company-lookup/company-lookup.py's exception message construction matches TypeScript's approach (includes response body) — confirmed via a live request that the two now produce byte-identical messages -
python/README.mdexists and is a real quickstart doc, not a stub — every code example checked against real function signatures, Quick Start example actually run against the real implementation -
README.mdaccurately shows Python as "✅ Available" everywhere it currently said "📅 Planned" — all 7 mentions found by grep, all updated - The three historically-documented bugs (metric-naming, enum-conversion, OTLP-timestamp) are re-confirmed still fixed
Files to Modify
python/src/logger.py(fixresponse_json/input_jsonhandling — removedremove_undefined_fields(), added_NOT_PROVIDEDsentinel)python/test/e2e/company-lookup/company-lookup.py(fix exception message construction)python/README.md(new)README.md(promotion: flipped Python's status in 7 places, plus one unrelated stale-script-name fix found nearby)