feat: tentative retirement floors and owned expiring waivers (#34, #80) - #91
Conversation
The Anthropic deprecations page opens with a model-status table that lists active models with "Not sooner than <date>" retirement dates. The refresh now parses that table for the Anthropic provider only and publishes those dates as shutdown with date_precision "earliest" and no announced date. Announcement tables always win for the same model; status rows for deprecated or retired models without an announcement are skipped with a parser note; an N/A row drops a previously tentative date. The checker treats a publisher-clock earliest date with no announcement as a tentative floor: the finding is always "scheduled", never "retiring" or "retired", and safe_until is the later of the floor and the publisher's minimum-notice policy date. Human output labels the floor as tentative and not announced. Plans and the bot create no migration work for it. No schema or feed files change; the weekly refresh regenerates the feed. Implemented by Codex (gpt-5.6-luna) from a Claude brief with one correction round on the checker rule; reviewed and tested by Claude.
A new "waivers" list in .model-eol.json (root and path overrides) records an owned, expiring exception for a live model dependency: model, optional paths and via, reason, owner, and an absolute expires date. Waivers are validated fail-closed with the same strictness as the rest of the config and capped at 500. A waived finding stays in every report with a waiver object (reason, owner, expires, active). An active waiver removes a retired or retiring finding from the exit-1 total, the badge counts, and the alert errors, and plans record it with reason "waived" so the bot creates no work. On the expiry date the finding is actionable again and reports say why. The bot closes existing work as stale when a waiver begins, naming the owner and expiry, and treats an expired waiver as a fresh reintroduction by binding the expiry into the work metadata so an earlier dismissal is not inherited. Schemas for bot config, check, inventory, and plan gain the waiver field; CycloneDX output exposes active suppression as component properties. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed and tested by Claude.
# Conflicts: # lib/feeds.mjs
Adversarial review findings on the waiver work: the report schemas had made waiver a required finding field, which rejected every existing model-eol/check@0.1 document; the CycloneDX exporter marked a whole component waived when only some of its occurrences were; and a waiver could not name the publisher clock because configured-channel validation rejected the literal. waiver is now optional-nullable in the check, inventory, and plan schemas, CycloneDX emits waiver_active "true" only when every occurrence is waived and "partial" otherwise, and waiver.via accepts publisher and publisher-fallback. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
…e parser The first cut keyed the tentative-floor rule on earliest precision with no announced date. The Google feed carries 43 real deprecations in exactly that shape, so a retired gemini-2.0-flash reference stopped failing CI. date_precision now has a third value, "tentative", meaning a publisher not-before date for a model whose deprecation is not announced. Only that value triggers the floor rule; earliest keeps its scheduled-date meaning on every clock. Feed and report schemas accept the new value, the diff prints "(tentative)", and human output reads "not sooner than". Adversarial review also found two fail-open paths in the status-table parser. Unrecognised retirement text now throws instead of yielding an id-only record, non-active rows without an announcement entry produce an announced plus exact-shutdown record instead of a notice, and a status merge never deletes a committed announced date or exact shutdown. Implemented by Codex (gpt-5.6-luna) from Claude briefs; reviewed by Claude.
# Conflicts: # README.md # lib/feeds.mjs # scripts/test-document-validation.mjs
# Conflicts: # README.md # scripts/test-document-validation.mjs
|
Review gate status (2026-09-03):
|
|
Codex adversarial pass 2 ran (2026-09-03, after the outage). Verdict: needs-attention, 4 findings, all accepted and being fixed on this branch:
Cleared by the review: Google |
Second adversarial pass: the tentative branch in lifecycleFor ran before retired handling and ignored announced, so contradictory records could clear CI. Feed validation now rejects tentative with announced and tentative on a retired distribution; the checker judges retired status first and applies the floor only when the judged clock has no announced date. The Anthropic status parser allows only Active, Deprecated, and Retired, refuses an Active row with a Deprecated date, and refuses a shutdown before its announcement. Distribution via values "publisher" and "publisher-fallback" are reserved so they cannot collide with the checker's publisher clock or with publisher-clock waivers. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
Second adversarial pass: every finding carried waiver: null, and the 0.1 report schemas published by 0.5.2 use additionalProperties false, so a consumer pinned to those schemas rejected every new report. Findings, model references, plan items, and plan issues now include waiver only when a waiver matched (active or expired), so a repository without waivers produces the same shape as 0.5.2. The 0.5.2 check, inventory, and plan schemas are snapshotted under test/fixture/schemas-0.5.2 and default output is validated against them. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
# Conflicts: # README.md
|
Codex adversarial pass 3 ran. All four pass-2 fixes cleared (waiver omitted when unmatched and default output validates against pinned 0.5.2 schemas; retired status wins and tentative floors require no announcement; parser state allowlist and date ordering; reserved publisher clock names). Three new findings, all accepted and being fixed on this branch:
|
Third adversarial pass: a reference whose matched waiver had expired exported no waiver properties, so the audit artifact lost the owner, reason, expiry, and the fact that a waiver lapsed. A component whose occurrences carry only expired matches now exports waiver_active "false" with the matched waiver metadata; active-plus-expired stays "partial". Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
Third adversarial pass: the status merge ignored the parsed row state, so a Retired row could not replace stale committed dates and an Active tentative row on a previously announced model produced a feed that failed validation. Each status record now carries its state; Active rows retract committed lifecycle and replacement fields before applying the floor, and Deprecated or Retired rows replace the committed dates. The header matcher indexes cells by label so extra or reordered columns are tolerated, a header with the tentative column but missing other required columns throws, and an Anthropic page with no status table throws. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
# Conflicts: # README.md
|
Codex adversarial pass 4 ran. All three pass-3 fixes cleared (state-aware status merge with every result passing
|
…rser Fourth adversarial pass: a header-only status table counted as parsed, so stale active lifecycle data could survive silently, and a status table with an extra Recommended replacement column was claimed by the generic announcement classifier and halted the refresh. The status parser now counts parsed rows before announcement deduplication and throws on zero rows or on duplicate required headers; the generic classifier skips any Anthropic table whose header carries the tentative column. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
|
Codex adversarial pass 5 ran. Both pass-4 code fixes cleared (header-only status tables fail; duplicate headers fail; status tables with a Recommended replacement column stay out of the announcement parser; rows counted before deduplication). Waivers, expiry, reports, bot reconciliation, schemas, tentative floors, and reserved clocks all cleared again. One remaining finding, accepted: status-table ownership matches the exact |
Fifth adversarial pass: status-table ownership matched the exact th text "tentative retirement date", so a footnote marker on the label or a header row built from td cells let the generic announcement classifier claim the table and emit false exact dates. Header labels are now normalised (case, footnote markers, trailing punctuation, whitespace), the signature is a contains-match on "tentative retirement", and a td-only first row counts as the header when the table has no th cells at all. Both the status parser and the generic guard share the detection. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
|
Codex adversarial pass 6 ran. The pass-5 fix cleared on all three requested probes (footnoted One medium finding remains, accepted: a leading spanning title row above the real header defeats the first-row ownership check, so the generic classifier can still claim a status table. Fix in progress: scan the whole leading |
Sixth adversarial pass: a leading spanning title row above the real header defeated the first-row ownership check, so the generic announcement classifier could still claim a status table. Detection now scans every leading all-th row for the status signature, requires exactly one such row, and starts data parsing after the complete header block. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
|
Codex adversarial pass 7 ran. The pass-6 fix cleared on every probe (leading spanning title row, two signature rows, title plus footnoted label, title plus footnote plus replacement column), and the review re-cleared status validation, announcement precedence, waiver scoping, expiry, bot reconciliation, report propagation, CycloneDX evidence, schema references, pinned compatibility, reserved clocks, and semantic diffs. One medium finding, accepted: the publisher's minimum-notice policy also extended |
Seventh adversarial pass: the publisher's minimum-notice policy also extended safe_until on a distributor's tentative clock, claiming runway that channel never established. The policy floor now applies only on the publisher and publisher-fallback clocks.
|
Codex adversarial pass 8 ran (bounded probes only). The pass-7 fix cleared: publisher and fallback floors extend to the policy date, a distributor floor keeps its own date. Waivers, expiry, bot suppression, CycloneDX evidence, schemas, compatibility policy, reserved clocks, header drift, semantic diffs, and feed-refresh PR reuse re-cleared. Two findings, both accepted:
|
Eighth adversarial pass: human output claimed a publisher-policy guarantee for every tentative floor, including distributor clocks where no policy applies, and a tentative entry without a shutdown date passed validation and rendered "not sooner than null". The policy clause now appears only when safe_until is later than the floor, and tentative precision requires a shutdown date at both the model and distribution level.
Eighth adversarial pass: a status row whose id already had an announcement entry was dropped without comparison, so a page that lists a model as Active with a tentative floor while also announcing its retirement produced no signal. An Active row for an announced model now throws, and a Deprecated or Retired row must match the announcement's announced and shutdown dates or throw. Announcement aliases take part in the match. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
|
Codex adversarial pass 9 ran (bounded probes). Both pass-8 fixes cleared: same-id Active conflicts throw, matching Deprecated/Retired rows deduplicate, date conflicts throw, policy text appears only when it extended Three findings:
|
Ninth adversarial pass: an announcement cell listing several model tokens kept only the first as id, so an Active status row for the alias bypassed the conflict check and the merge replaced the exact shutdown with a tentative floor. Anthropic announcement records now carry the extra tokens as aliases, matched rows compare shutdown unconditionally so a Deprecated N/A row against a dated announcement throws, and an Anthropic announcement row with an N/A shutdown yields an announced-only record. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed by Claude.
Adversarial review of the reuse path merged in PR 90: it force-pushed any open feed-refresh/* branch without checking base, ownership, or review state, so it could erase a maintainer's commits or replace a reviewed diff. Reuse now requires base main, only "model-eol feed refresh" committers beyond origin/main, and no completed review decision; otherwise a new PR is opened and the older one is named in the body. The push uses --force-with-lease against the fetched remote head. Implemented by Codex (gpt-5.6-luna) from a Claude brief; reviewed and tested by Claude.
|
Codex adversarial pass 10 ran (bounded probes). Both pass-9 fixes cleared: multi-token announcement aliases reach the status conflict check and survive the merge deduplicated; Deprecated One finding, accepted: a Retired status row with |
Tenth adversarial pass: the retired-date guard ran only for status rows with no matching announcement, so a Retired row with N/A matched against an announcement without a shutdown parsed and became a non-actionable watch record. A Retired row now needs a shutdown date regardless of announcement matching.
|
Codex adversarial pass 11 ran (bounded probes). The pass-10 fix cleared: matched, unmatched, and alias-matched Retired One medium finding, accepted: a status data row with an empty model cell but populated lifecycle cells is skipped, so a partial DOM failure can drop a model and let the merge erase its committed tentative date. Fix: skip only wholly empty rows; a populated row without a model id throws. |
Eleventh adversarial pass: a status data row with an empty model cell but populated lifecycle cells was skipped, so a partial DOM failure could drop a model and let the merge erase its committed tentative date. Only wholly empty spacer rows are skipped now; a populated row without a model id throws.
|
Codex adversarial pass 12 ran (bounded probes). The pass-11 fix cleared: a wholly empty spacer row is skipped, a populated row with an empty model cell throws. Waivers, tentative lifecycle, policy floors, merge, validation, reporting, scanner propagation, plans, distributor guards, schemas, fixtures, and docs re-cleared. One medium finding, accepted: a status-shaped table whose tentative column is renamed to |
Twelfth adversarial pass: a status-shaped table whose tentative column was renamed still carried a Current state column, was not recognised as a status table, and fell through to the announcement parser as false exact dates. Either column now marks a status table, after which the missing required column throws.
|
Codex adversarial pass 13 ran (bounded probes). The pass-12 fix cleared: a renamed tentative column with One medium finding, accepted: a status date cell containing two dates (for example an ISO date followed by a human date) parses as the first. Fix: a status date cell must contain exactly one date candidate. |
Thirteenth adversarial pass: a status date cell holding an ISO date and a human date parsed as the first one because the residual check removed one of each. A status date cell must now contain exactly one date candidate.
|
Codex adversarial pass 14: approve, no material findings. Cleared: date parsing (single dates pass; mixed or duplicate candidates throw); Anthropic fixture ingestion, announcement conflicts, status drift guards, feed validation; tentative lifecycle on publisher and distributor clocks; waiver matching, alias/path/channel scope, UTC expiry, reporting, planning, bot reconciliation; schema alignment, compatibility fixtures, documentation, diff hygiene. Gate summary for this PR: 14 adversarial passes, 25 findings fixed on this branch, 1 rejected with rationale (pre-waiver dismissal invalidation), 1 policy decision documented in SPEC.md (additive 0.1 line), 1 deferred to PR 92 (feed-refresh reuse lease). Full |
|
Feature UAT on real local repositories (read-only, checker at HEAD): Tentative floors. Built
Retired and retiring are identical; every delta is Waivers. Temporary config waiving |
Rewrites the prose in shorter sentences with more paragraph breaks and removes figurative phrasing in favour of direct statements. Splits the policy section into ignores-and-waivers, coverage limits, and monorepos, and adds a jq example for reading a schedule without tentative floors. Every command, code block, sample output line, link, and the auto-updated feeds line are unchanged.
|
README: plain-language pass (shorter sentences, direct statements, section split for the policy chapter) and a |
# Conflicts: # README.md
|
Bundled PR 92 (leased, ownership-checked feed-refresh PR reuse) into this branch; the only overlap was one README sentence, resolved in favour of the plain-language paragraph. Full suite green including the workflow assertions in scripts/test-public-site.mjs. |
Closes #34. Closes #80.
Tentative retirement floors (#34)
Anthropic's deprecations page lists a "Not sooner than " tentative retirement date for every active model. The refresh now parses that status table for the Anthropic provider and publishes those dates as
shutdownwith a newdate_precision: "tentative"and noannounceddate.tentativeis a third precision value inSPEC.mdand the feed and report schemas.earliestkeeps its meaning and behavior in full.scheduled, neverretiringorretired, andsafe_untilis the later of the floor and the publisher policy date. It cannot fail CI and the bot opens no work for it.announcedplus exactshutdownrecord. Unrecognised lifecycle text throws; a status merge never deletes a committedannouncedor exactshutdown.feeds/anthropic.json(11 tentative rows on today's page).Sample:
Waivers (#80)
waiversin.model-eol.json(root and path overrides) records an owned, expiring exception on a live dependency:model, optionalpathsandvia,reason,owner, absoluteexpires. Fail-closed validation, cap of 500.check,inventory,schedule,alert,plan, and CycloneDX with awaiverobject (reason, owner, expires, active).waivedso the bot creates no work.waiveris optional in the 0.1 report schemas so existing documents still validate. CycloneDX marks a componentwaiver_active: trueonly when every occurrence is waived,partialotherwise.viaaccepts a distributor id,publisher, orpublisher-fallback.Sample:
README
One pass to integrate both features and tighten the policy sections. Sample lines are real output.
Verification
npm testgreen on the merged branch.main(ok 1845 / retired 644 / retiring 42 / scheduled 10).earliestdeprecations; parser fail-open paths; status merge erasingannounced;waiverrequired in 0.1 schemas; CycloneDX mixed-component waiver; publisher-clock waivers). One rejected: invalidating pre-waiver human dismissals.Implemented by Codex from Claude briefs with three correction rounds; reviewed, merged, and tested by Claude.