Skip to content

feat(migrate): ote-migrate, for the version bump a sed gets wrong - #112

Merged
hhkaos merged 1 commit into
mainfrom
feat/97-ote-migrate
Nov 4, 2026
Merged

hhkaos merged 1 commit into
mainfrom
feat/97-ote-migrate

Conversation

@hhkaos

@hhkaos hhkaos commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Closes the last open part of #97, and the last package checkbox on #101.

What it actually does, and why that needs a package

It rewrites specVersion. That is the whole JSON change — every published OTE release has been additive, so no valid document has ever needed a structural rewrite to move up. That is verified rather than assumed: the output is validated against the target version before it is returned, so a future release that breaks the pattern fails loudly here instead of shipping bad feeds.

The reason it is not sed -i s/0.4.0/0.6.0/ is D032.

Before 0.5 a part of a multi-part event carried its own complete offers; there was no way to say "this workshop costs 60 on top of the conference ticket". From 0.5 there is — partOf.offers plus "scope": "add-on". The trap is that the old document stays valid: an offer with no scope is read as admission, a complete way to attend that part. So a workshop that really costs 240 + 60 goes on publishing, validly and machine-readably, that it costs 60.

Nothing can detect that automatically — whether the number is the whole price or an extra is a fact about the event, not about the JSON. So ote-migrate names the events and asks. It never rewrites pricing, per the repo-wide rule that connectors do not invent data. On the spec's own multipart example it raises all three questions:

✓ feed.json — OTE 0.4.0 → 0.6.0

  ! events[1].offers (D032)
    This event ("Taller de testing en Python") is part of a multi-part set and
    carries its own offers. From 0.5 an offer without a "scope" is read as
    "admission" — a complete way to attend this part. If any of these prices is
    really a surcharge on top of the set's ticket, mark it "scope": "add-on" and
    put the shared admission in partOf.offers, or the feed will under-report what
    attending costs.

  3 things to check by hand. The document is valid either way — these are about
  what it means.

Warnings never change the exit code. They are decisions, not faults.

Two decisions made here rather than in the issue

Scope is every published version, not just 0.4→0.6. #101 scopes this 0.4→0.6, but the adopter it flags as the live problem — corunajug.org — is on 0.3, which left the support window when 0.5 and 0.6 published the same day. Checked empirically: that feed and a 0.1 feed both become valid 0.6 by changing specVersion alone, so the wider range cost nothing and is the only version of this tool that helps the person who most needs to run it. Easy to narrow if you'd rather.

It validates against the declared version directly, not through validateDocument. validateDocument applies the support policy, which calls an out-of-window version an error — so routing through it would refuse exactly the documents that have to be migrated. loadValidators is exported for this. Before/after on the 0.3 fixture:

$ ote-validate feed-0.3.json
✗ feed-0.3.json (feed) — OTE 0.3.0 … outside the support window … migrate to 0.6.0.   (exit 3)

$ ote-migrate feed-0.3.json --write
✓ feed-0.3.json — OTE 0.3.0 → 0.6.0                                                    (exit 0)

Refuses rather than guessing

A missing or never-published specVersion; an unpublished target; a downgrade (dropping fields a newer version added is silent data loss); and input already invalid at the version it claims — migrating that produces an invalid document wearing a newer label, and a later reader cannot tell the damage came from here.

Spec version

No spec-version change. The repo stays on OTE Spec 0.6.0; this package spans 0.1.0 → 0.6.0, which is why its own version is 0.1.0 rather than tracking the spec minor like the exporters do.

Testing

27 tests (migrate.test.ts, cli.test.ts). pnpm lint, build, typecheck and test green across the workspace.

Fixtures are derived from the spec's own 0.6 examples with the 0.5 fields removed, so the "0.4 document" is one the 0.4 schemas actually accept rather than one invented by hand.

Before merging — release note

pnpm publish -r picks the package up by itself, but npm trusted publishing cannot be configured for a package that does not exist yet. The first release has to be published by hand, then the trusted publisher added in its npm settings — as publish.yml already warns. A missed OIDC config surfaces as a confusing 404.

🤖 Generated with Claude Code

Closes the last open part of #97, and the last package checkbox on #101.

What it does to the JSON is one field. Every published OTE release has been
additive — 0.1→0.4 added optional fields, 0.5 added six, 0.6 changed no
schema at all — so no valid document has ever needed a structural rewrite to
move up. Verified rather than assumed: the output is validated against the
target version before it is returned, so a future release that breaks the
pattern fails here instead of shipping bad feeds.

The reason it is a package and not `sed -i s/0.4.0/0.6.0/` is D032. Before
0.5 a part of a multi-part event carried its own complete `offers`; there was
no way to say "this workshop costs 60 on top of the conference ticket". From
0.5 there is — `partOf.offers` plus `scope: "add-on"` — and the old document
stays **valid** either way, because an offer with no `scope` reads as
`admission`. So a workshop that really costs 240 + 60 goes on publishing,
validly and machine-readably, that it costs 60. Nothing can detect that:
whether the number is the whole price or an extra is a fact about the event,
not about the JSON. `ote-migrate` names the events and asks; it never
rewrites pricing, per the repo-wide rule that connectors do not invent data.
On the spec's own multipart example it raises all three questions.

Two things worth flagging, both decided here rather than in the issue:

**Scope is every published version, not just 0.4→0.6.** The tracker scoped
this 0.4→0.6, but the adopter it flags as the live problem — corunajug.org —
is on 0.3, which left the support window when 0.5 and 0.6 published the same
day. Checked empirically: that feed and a 0.1 feed both become valid 0.6 by
changing `specVersion` alone, so the wider range cost nothing and is the only
version of this tool that helps the person who most needs it.

**It validates against the declared version directly, not through
`validateDocument`.** `validateDocument` applies the support policy, which
calls an out-of-window version an error — so routing through it would refuse
exactly the documents that have to be migrated. `loadValidators` is exported
for this.

Refuses rather than guessing: a missing or never-published `specVersion`, an
unpublished target, a downgrade (dropping fields a newer version added is
silent data loss), and input already invalid at the version it claims —
migrating that produces an invalid document wearing a newer label.

Warnings never change the exit code. They are decisions, not faults.

Fixtures are derived from the spec's own 0.6 examples with the 0.5 fields
removed, so the "0.4 document" is one the 0.4 schemas actually accept rather
than one invented by hand. 27 tests; `pnpm lint`, `build`, `typecheck` and
`test` green across the workspace.

Note for release: `pnpm publish -r` picks the package up by itself, but npm
trusted publishing cannot be configured for a package that does not exist
yet. The first release has to be published by hand, then the trusted
publisher added in its npm settings — as publish.yml already warns.

Co-Authored-By: Claude Opus 5 <[email protected]>
@hhkaos
hhkaos merged commit 871cc46 into main Nov 4, 2026
1 check passed
@hhkaos
hhkaos deleted the feat/97-ote-migrate branch November 4, 2026 17:11
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.

1 participant