Skip to content

x-altair-idempotency round-trip activation: forward emit + reverse import + drift gate #175

@tonydspaniard

Description

@tonydspaniard

Part of #171. Depends on #174 (spec block).

Goal

Activate the x-altair-idempotency OpenAPI extension end-to-end. The key + JSON Schema already landed in #163; this issue closes the loop now that the corresponding idempotency: spec block exists (#174):

  • spec:emit-openapi writes x-altair-idempotency when the spec carries the block
  • openapi:import reads it back and emits the spec block
  • openapi:roundtrip (#164) gains the extension to its compared set so a regression that drops it fails the gate

Why

Until this lands, an openapi:import of a document with x-altair-idempotency produces a spec without an idempotency: block — meaning the imported endpoint has no idempotency protection even though the source said it should. Worse, the round-trip succeeds silently, because the round-trip gate (#164) deliberately does not compare reserved-but-inactive extensions today.

Once this issue ships, the round-trip is honest end-to-end: a doc that claims x-altair-idempotency produces a spec that claims idempotency:, which scaffolds an Action that actually enforces it.

Changes

Forward (spec:emit-openapi)

Altair\Scaffold\Emitter\OpenApiEmitter::renderAltairExtensions():

if ($spec->idempotency !== null) {
    $extensions['x-altair-idempotency'] = [
        'ttl' => $spec->idempotency->ttl,
        'scope' => $spec->idempotency->scope,
    ];
    // 'mode' is server-side concern; not round-tripped.
}

Reverse (openapi:import)

Altair\Scaffold\Spec\Emitter\OperationMapper::map():

$idempotency = $this->extensionMap($operation, 'x-altair-idempotency');
if ($idempotency !== null && isset($idempotency['ttl']) && is_string($idempotency['ttl'])) {
    $spec['idempotency'] = [
        'ttl' => $idempotency['ttl'],
        'scope' => isset($idempotency['scope']) && is_string($idempotency['scope']) ? $idempotency['scope'] : 'tenant',
        'mode' => 'optional',  // safe default — source didn't carry it
    ];
}

Round-trip gate (openapi:roundtrip)

Altair\Scaffold\Cli\OpenApiRoundtripRunner grows x-altair-idempotency in:

  • the projection's compared field set
  • the per-extension drift check loop

Acceptance criteria

  • A spec carrying idempotency: { ttl: 24h } produces an OpenAPI fragment containing x-altair-idempotency: { ttl: 24h, scope: tenant }
  • An OpenAPI doc carrying x-altair-idempotency: { ttl: 24h } imports into a spec with the equivalent idempotency: block
  • openapi:roundtrip on a fixture with x-altair-idempotency reports clean
  • A deliberately-broken emitter (e.g. forgetting to write the extension) causes openapi:roundtrip --check to exit 1 with a kind: extension_drift entry locating x-altair-idempotency
  • Updates to docs:

Out of scope

Notes

This is the smallest of the sub-issues but the most load-bearing for the "agent can rely on the spec" property. The drift-gate hookup is what prevents a future refactor from silently breaking idempotency preservation on round-trip.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions