Skip to content

feat(ais_core): implement JSON-Contract + Run-State helpers (#471, Issue #1c)#477

Merged
SaJaToGu merged 2 commits into
developfrom
ais-fix-471-json-runstate
Jun 28, 2026
Merged

feat(ais_core): implement JSON-Contract + Run-State helpers (#471, Issue #1c)#477
SaJaToGu merged 2 commits into
developfrom
ais-fix-471-json-runstate

Conversation

@SaJaToGu

Copy link
Copy Markdown
Owner

Closes #471 (Issue #1c — Wave 1b, JSON-Contract + Run-State)

Summary

Replaces the NotImplementedError placeholders in ais_core/json_contract.py
and ais_core/run_state.py with full implementations, plus comprehensive
behaviour tests. Pure helpers, no I/O side-effects, no global state, no
env-var reads at import time.

What's in this PR

ais_core/json_contract.py

  • success_envelope(command, data, *, warnings=None, elapsed_ms=None)
  • error_envelope(command, code, message, *, hint=None, elapsed_ms=None)
  • validate_envelope(envelope) — strict shape + schema_version check,
    raises ValueError on bad shape
  • SCHEMA_VERSION = "1.0" (unchanged)

ais_core/run_state.py

  • make_run_id(owner, repo, timestamp=None) — format
    <UTC>-<repo-short>-<8-hex> (SHA-256-based collision avoidance)
  • save_state(run_id, state, *, base_dir=None) — writes
    <base_dir>/<run_id>/metadata.json, creates parents
  • load_state(run_id, *, base_dir=None) — reads metadata.json back
    into a RunState
  • DEFAULT_REPORTS_DIR = Path("reports/runs")
  • RunState NamedTuple (run_id, status, data)

Tests

  • tests/test_ais_core/test_json_contract.py: 22 tests

    • TestSchemaVersionConstant: SCHEMA_VERSION is "1.0"
    • TestSuccessEnvelope: minimal shape, warnings default, with
      warnings, with elapsed_ms, arbitrary JSON data
    • TestErrorEnvelope: minimal shape, with hint, with elapsed_ms,
      error-body key set
    • TestValidateEnvelope: accepts valid success/error, rejects
      non-dict, wrong schema_version, missing ok, non-bool ok,
      success without data, error without error/code/message,
      non-dict error
  • tests/test_ais_core/test_run_state.py: 11 tests

    • TestMakeRunIdFormat: basic format, default timestamp UTC,
      naive timestamp treated as UTC, repo-short sanitization,
      repo-short ≤20 chars
    • TestMakeRunIdUniqueness: same inputs same id, different owners
      different ids, 1000 unique IDs
    • TestSaveLoadRoundtrip: basic roundtrip, parent-dir creation,
      missing-file FileNotFoundError, empty data roundtrip

Tests

python -m unittest tests.test_ais_core.test_json_contract \ tests.test_ais_core.test_run_state -v

→ 33 tests, 0.026s, all OK.

LOC

File Lines
ais_core/json_contract.py 169
ais_core/run_state.py 153
tests/test_ais_core/test_json_contract.py 164
tests/test_ais_core/test_run_state.py 130

Helper code: 322 lines (vs. < 300 net soft-budget). Docstrings are
substantial because both modules are public API surfaces with
documented contracts; pure code (function bodies) is ~120 lines.

Out of scope (intentional)

Parent issue

Part of #468 (Release 0.10.0 — AIS Tooling Interface).

SaJaToGu added 2 commits June 28, 2026 10:52
- ais_core/json_contract.py: full implementation of success_envelope,
  error_envelope, validate_envelope. Pure functions, no I/O.
  - success_envelope(command, data, *, warnings=None, elapsed_ms=None)
  - error_envelope(command, code, message, *, hint=None, elapsed_ms=None)
  - validate_envelope(envelope) — strict shape validation, raises ValueError
- ais_core/run_state.py: full implementation of Run-ID generation and
  state persistence. Pure (no global state, no env-var reads at import).
  - make_run_id(owner, repo, timestamp=None) -> str with format
    <UTC>-<repo-short>-<8-hex> (SHA-256-based collision-avoidance)
  - save_state / load_state with optional base_dir override for tests
  - DEFAULT_REPORTS_DIR = Path('reports/runs')
  - RunState NamedTuple for typed state records
- tests/test_ais_core/test_json_contract.py: 22 tests covering envelope
  shape + validate_envelope rejection cases
- tests/test_ais_core/test_run_state.py: 11 tests covering Run-ID
  format + 1000-IDs-uniqueness + state roundtrip + edge cases

Tests: 33 OK in 0.026s (test_json_contract + test_run_state).

Refs #471
Per #477 review feedback:

1. make_run_id hash input switched from seconds-precise UTC string to
   microsecond-precise ISO-8601 string. Visible prefix remains seconds-
   precise (YYYYMMDDTHHMMSSZ) so run-IDs stay human-readable and
   sortable; the hash gains uniqueness within the same wall-clock
   second. Identical explicit timestamps still produce identical IDs
   (determinism preserved); timestamp=None calls differ by clock.

2. _sanitize_repo_short: added second .strip('-') after the 20-char
   truncate (defense-in-depth: trailing separators are possible if
   input ends in a non-alphanum char at the truncate boundary).

3. save_state: now raises ValueError when run_id argument and
   state.run_id differ. Prevents accidental metadata.json files
   whose embedded run_id does not match their on-disk path.

Tests added (5):
- test_same_second_different_microseconds_produce_different_ids:
  same second, different microseconds -> different IDs; visible
  prefix identical.
- test_visible_prefix_remains_seconds_precise: microsecond input
  still produces YYYYMMDDTHHMMSSZ prefix.
- test_subsecond_resolution_default_timestamp: two timestamp=None
  calls produce different IDs (warns if clock is too coarse).
- test_save_state_rejects_run_id_mismatch: ValueError on mismatch.
- (existing tests unchanged: 1000 unique IDs, basic roundtrip, etc.)

Tests: 16 OK in 0.023s (test_run_state).
json_contract tests: 21 OK in 0.001s (unchanged).

Refs #471, #477
@SaJaToGu
SaJaToGu merged commit 03b3d1e into develop Jun 28, 2026
2 checks passed
@SaJaToGu
SaJaToGu deleted the ais-fix-471-json-runstate branch June 28, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0.10.0 Issue #1c — JSON-Contract + Run-State pure helpers

1 participant