{"record":{"id":"d57516af8bd995af","repo":"bmad-code-org/BMAD-METHOD","slug":"field-mismatch-after-write","errorCode":null,"errorMessage":"{field} mismatch after write","messagePattern":"(.+?) mismatch after write","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/bmm-skills/plan/bmad-sprint-planning/scripts/sprint_plan.py","lineNumber":457,"sourceCode":"        \"warnings\": warnings,\n        **report,\n    }\n\n    if args.dry_run:\n        print(json.dumps(result, default=str))\n        return\n\n    try:\n        payload = _dump_bytes(yaml, doc)\n        _atomic_write(args.status_file, payload, original_mode)\n        verify_yaml = _make_yaml()\n        with io.open(args.status_file, \"r\", encoding=\"utf-8\") as fh:\n            reread = verify_yaml.load(fh)\n        if dict(reread.get(\"development_status\") or {}) != {k: v for k, v in dev.items()}:\n            raise ValueError(\"development_status mismatch after write\")\n        for field in (\"generated\", \"last_updated\", \"project\"):\n            if str(reread.get(field)) != str(doc[field]):\n                raise ValueError(f\"{field} mismatch after write\")\n    except Exception as exc:\n        if original_bytes is not None:\n            try:\n                _atomic_write(args.status_file, original_bytes, original_mode)\n                restored = True\n            except Exception:\n                restored = False\n        else:\n            Path(args.status_file).unlink(missing_ok=True)\n            restored = True\n        _fail(f\"write or validation failed, original {'restored' if restored else 'NOT restored'}: {exc}\",\n              restored=restored)\n    print(json.dumps(result, default=str))\n\n\ndef _parse_stamp(value):\n    from datetime import datetime\n","sourceCodeStart":439,"sourceCodeEnd":475,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/src/bmm-skills/plan/bmad-sprint-planning/scripts/sprint_plan.py#L439-L475","documentation":"Companion to the development_status check: after the atomic write and re-read, the scalar header fields `generated`, `last_updated`, and `project` must match `str(doc[field])`. It fires when YAML coerces one of those scalars on re-read so its `str()` differs from the written value — most often because the field looks like a date or number and YAML returns a `datetime.date`/`int` object whose `str()` is not byte-identical to the original string. The exception handler restores the original file (or deletes it) and fails loud, so the file is never left half-mutated.","triggerScenarios":"`generated`/`last_updated` is written as an ISO date string that the YAML loader turns into a `datetime.date`; `project` is a bare number or a YAML-reserved word. The check `str(reread.get(field)) != str(doc[field])` then trips because the re-read object's `str()` (e.g. `'2026-08-12'` from a date vs `'2026-08-12'` string is usually equal, but a `datetime` gives `'2026-08-12 00:00:00'`).","commonSituations":"Passing `--date 2026-08-12` where the loader returns a `datetime` whose repr adds time components; a project name that is purely numeric; a version bump of the YAML library that newly date-parses a previously-opaque string.","solutions":["Confirm `--date`, `--project`, and any inherited `generated` value are plain strings and not bare numbers or full datetimes.","If `generated` carries a time component from an older file, normalise it to a `YYYY-MM-DD` string at the source.","Force the emitter to quote these three fields as explicit strings (a custom representer or `default_style='\"'`) so the loader cannot coerce them.","Reproduce with `--dry-run` (which skips the write) and then read the file back by hand to see which field's re-read type diverges."],"exampleFix":"# before: last_updated stored as a bare date, loader returns datetime.date\nlast_updated: 2026-08-12\n\n# after: quote it so it stays a string\nlast_updated: \"2026-08-12\"\n\n# or in code, coerce before writing:\ndoc[field] = str(doc[field])","handlingStrategy":"validation","validationCode":"def scalar_roundtrips(value, yaml_lib) -> bool:\n    dumped = yaml_lib.dump(str(value))\n    back = yaml_lib.load(dumped)\n    return str(back) == str(value)\n\nfields = {f: doc[f] for f in (\"generated\",\"last_updated\",\"project\")}\nbad = [f for f,v in fields.items() if not scalar_roundtrips(v, yaml)]\nif bad:\n    raise SystemExit(f\"fields would not round-trip: {bad}\")","typeGuard":"def is_iso_date_str(s: object) -> bool:\n    if not isinstance(s, str): return False\n    try:\n        datetime.strptime(s, \"%Y-%m-%d\"); return True\n    except ValueError:\n        return False","tryCatchPattern":"# Treat non-zero exit as fatal; the handler has already restored the file.\nimport subprocess\nres = subprocess.run([\"python\",\"sprint_plan.py\",...], capture_output=True)\nif res.returncode != 0:\n    # message reports restoration status; do NOT retry the same inputs\n    raise RuntimeError(res.stderr.decode())","preventionTips":["Pass --date as a strict YYYY-MM-DD string, never a full datetime.","Keep project names alphanumeric; quote anything that looks numeric in the source.","Pin YAML library versions to keep date-coercion behaviour stable.","Use --dry-run to validate the document before the write is attempted."],"tags":["yaml","data-integrity","round-trip","sprint-planning","config"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}