80a9824a4e89c52624e49ca3014e3e6b3dfe8cde
29
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
80a9824a4e |
test(381): golden coverage for Sequential (YAML) playout builder; document Scripted deferral
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 28s
PR Gates / Docs update reminder (pull_request) Successful in 46s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m53s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m29s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Extends the #163 PlayoutBuildGoldenTests in-memory net to the Sequential (YAML) builder: a committed fixture (Goldens/Fixtures/sequential-schedule.yml) with two `count: 2` instructions over one chronological collection, built via SequentialPlayoutBuilder over the pinned window. The count/all/duration handlers do UTC-only arithmetic off the caller-supplied start, so the case is TZ-independent (passes, not skips, under a non-UTC TZ) and needs no Assume guard. Non-vacuity: a fixture count tweak flips the golden + the contiguity assertion. Scripted is deliberately excluded from the golden net — ScriptedPlayoutBuilder shells out via Cli.Wrap to an external process that drives SchedulingEngine over HTTP, which no in-memory golden can characterize. Recorded as the Done-when "documented decision" arm in docs/decisions.md (testing.scripted-playout-golden-deferred) + docs/testing.md; the scripted integration harness is tracked as follow-up #563. fixes #381 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
60a0c50578 |
fix(552): fold #552 security-review findings
Cold review (no Critical/High). Folded: - Low: clamp JWT:BrowserTokenLifetimeMinutes to a 24h max so a seconds-vs-minutes typo can't mint a multi-year bearer token (non-positive/unparseable still falls back to 60 min). - Low: reset the SPA iptv-token cache on the preview panel's Retry and on each troubleshooting Play, so a stale token (key rotated) or a stale "JWT disabled" latch (backend reconfigured since page load) can't wedge a user-initiated retry. Deferred to #559 (tracked): redact access_token from Serilog request logs and set no-store on token-bearing /iptv manifests — pre-existing properties of the shared ?access_token= transport (Jellyfin/M3U already use it), now bounded by the 60-min lifetime; cross-cutting fixes beyond this feature's scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f8ae4d62ab |
fix(552): mint a short-lived JWT so the SPA reaches /iptv/* under JWT auth
Under a JWT-enabled deployment (JWT:IssuerSigningKey set), /iptv/* is gated by ConditionalIptvAuthorizeFilter and the "jwt" scheme does not accept the SPA's ctv-session cookie, and nothing minted a JWT for the browser. So the #60 channel preview was declared Unavailable and could not run at all. Add GET /api/v1/auth/iptv-token (session-gated, on the [IgnoreApi] AuthController): mints a short-lived global token via JwtHelper.GenerateBrowserToken (60 min default, JWT:BrowserTokenLifetimeMinutes override), 204 when JWT is disabled. The SPA's new withIptvToken(url) helper appends it as ?access_token= to the manifest URL (a no-op when JWT is off), used by the channel-preview panel and the troubleshooting screen. Mapper.GetPreview drops its iptvJwtEnabled -> Unavailable guard; preview is now JWT-agnostic. Live-E2E under JWT: /iptv manifest 401s without a token and passes with a valid one (garbage token -> 401); token endpoint 401s anonymous, mints with a session. Honest finding: the issue's point 2 (troubleshooting screen broken under JWT) does not reproduce -- its live.m3u8 is static-served (UseStaticFiles at /iptv/session), outside the JWT filter, so it was never gated. The withIptvToken call there is a harmless defensive no-op. Docs: security.iptv-browser-token (api-auth-security.md), amended api.channel-preview-capability, spa-conventions §5b. No OpenAPI change (IgnoreApi + unchanged Preview schema). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
22d4023263 | docs(60): record the channel-preview capability decision | ||
|
|
928784ba48 |
fix(135): from-lineup advanced overrides can express "clear to none"
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 17s
PR Gates / Docs update reminder (pull_request) Successful in 19s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m44s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 17m8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 20m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
CreateChannelFromLineupHandler resolved every advanced override with advanced.X ?? template.X, so null always meant INHERIT and a channel could not drop a template-set watermark / filler / preferred language. Add an optional typed `clear` enum list to CreateChannelFromLineupAdvancedOptions: omitted/null still inherits (byte-stable for existing clients), a field named in `clear` is forced to none. Set+clear of the same field is a 422. The enum (CreateChannelFromLineupClearField) lives in ErsatzTV.Core so the OpenAPI string-enum scan renders it as a string enum, matching every sibling advanced-options enum. Handler resolves clearable fields once via ResolveClearable and validates set/clear conflicts via ValidateClear; reference validation skips existence checks for cleared (null) refs. SPA: the shared advancedOptions model re-adds a real "None" option to the five id selects (watermark + fillers) in both the Channel Builder and the Auto-Tune DetailPanel, routed through a CLEAR overrides sentinel that applyOverridesToRequest folds into advanced.clear (never leaking onto the wire as a field value). The backend enum also covers the preferred audio/subtitle language strings for machine clients; the SPA text inputs keep "empty = inherit" (tri-state deferred). Docs: api-conventions.md §2, spa-conventions.md §11, decisions.md record api.from-lineup-clear-to-none; v1.json + generated TS regenerated. fixes #135 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1ce5743bc1 |
fix(539): WorkAheadSlots.Release clamps before decrementing, reports unbalance in-band
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 16s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m31s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 20m12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Three Low findings from the #536 clamp re-review, unreachable today (one guarded release site) but filed against the day a second release site is added. - §1: Release() now reads the count and CAS-decrements only when current > 0, so it never publishes a negative count even transiently. The prior decrement-first-then-clamp shape dipped to -1, which a concurrent TryAcquire could read as phantom room and over-admit at the limit (re-opening the #529 QSV pool exhaustion). It records the unbalanced release synchronously on the offending thread rather than blaming a later innocent release. - §3: Release() returns bool; HlsSessionWorker logs a warning on the false (unbalanced) return — the one in-band signal a future second release site would need. WorkAheadSlots stays logger-free by design. - §2: UnbalancedReleases doc-comment corrected — it can under-count (an over-release while count > 0 cancels a coexisting leak and goes unrecorded); no false positives, but zero does not prove correctness. Test: Release_Unbalanced_NeverPublishesNegativeCount (2M unbalanced releases vs 4 count-samplers) with a documented, verified negative control (reverting to the decrement-first body makes readers observe the transient -1). Adds a decisions.md entry (ffmpeg.work-ahead-slot-release-never-negative). fixes #539 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
12e5c3d26f |
docs(542): record the workflow lore, then prune the kickoff doc to instructions
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 13s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 16s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m44s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 20m14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Timothy asked why the kickoff handoff doc stores historical narrative when it should be instructions. It shouldn't — its own lore section is chartered as "STANDING workflow/orchestration rules only" with the why belonging in docs/decisions.md. But an inventory of every bullet against the decision corpus inverted the premise: only ~8 of ~38 were actually covered. 19 had no record anywhere and 11 more were half-covered, so that single file was the ONLY copy of the mandatory review rubric, the whole CI-triage vocabulary, the build-concurrency policy, the H12 session-end audit, and the plumbing-merge recipe. Pruning first would have destroyed them. So the records come first. New topic file docs/decisions/workflow-process.md carries 32 records (ci.*, process.*, testing.*) covering every NONE and PARTIAL the inventory found, including the Gitea `?milestones=` no-op bug whose only copy was the archived selector section this prune deletes. Only then the prune: HARD CONSTRAINTS and the lore section become one- or two-line rules, each citing the decision key that holds its evidence, and the 40-line "Archived — do not follow" section is gone. 636 -> 353 lines, with every cited key verified to resolve against the corpus. The aggregate corpus budget is re-baselined 4800 -> 5600 with the reason in the code: the corpus grew because knowledge MOVED into it, which is the system working, not drift. refs #542 |
||
|
|
92bf8b083d |
chore(541): fast-forward the shared checkout at session end (H13)
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m34s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m55s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A session was handed docs/handoffs/chicorytv-issue-queue.md pasted out of /Users/timothy/ersatztv while that tree was 81 commits behind, so it still described the queue protocol #520 retired the day before (read tracker command was ever run against that tree, so every existing "never read its HEAD" guard was irrelevant: a stale checkout serves stale FILES, and docs are what a kickoff depends on. Nothing broke only because selection went through scripts/select-queue.sh. The lore bullet on that tree already prescribed the shape of the fix for its earlier failure modes — "a design flaw, not a discipline failure; a check does not stay true" — so this removes the stale condition instead of adding another check. scripts/refresh-shared-checkout.sh fast-forwards the tree to origin/main and reinstalls web/node_modules when the lockfile moved. It is deliberately timid: it refuses and changes nothing when the tree is not on main, is dirty, is ahead, or is mid-rebase/merge, and it never switches branches, stashes or discards. A NO-OP is a normal outcome. Uses npm ci rather than npm install — the first version used install, which rewrote package-lock.json and left the tree dirty, i.e. the exact state the next run refuses on, so it would have disabled itself after one use. Asserts the tree is clean at exit. refs #541 |
||
|
|
dfed9a393b |
fix(68): rebuild on-demand channel guide (and mirrors) on thaw
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 27s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m19s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m26s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m38s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m52s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
An on-demand channel (`PlayoutMode.OnDemand`) already is the "resume where I left off" feature: `Playout.OnDemandCheckpoint` persists the viewer's position and `PlayoutTimeShifter.TimeShift` slides the materialized timeline forward on tune-in so the paused item is active again. Because it rewrites `GuideStart`/ `GuideFinish` alongside `Start`/`Finish`, guide and playback freeze together — structurally avoiding the free-running-wall-clock desync #68 was filed about. The one gap: `TimeShift` rewrote the stored `PlayoutItem` rows but the XMLTV guide is served from a cached fragment that only `RefreshChannelData` rebuilds, and the tune-in path never enqueued it. So an external EPG client polling after a thaw could see a stale timeline until the next incidental rebuild. Fix: `IPlayoutTimeShifter.TimeShift` now returns the channel numbers whose cached guide is stale — the shifted channel plus any channels that mirror it (the same fan-out `BuildPlayoutHandler` already does) — and `TimeShiftOnDemandPlayoutHandler` enqueues a `RefreshChannelData` for each on `CancellationToken.None` (post-commit side effect must not be abandoned if the session token cancels). Tests: handler enqueues a rebuild per stale channel (+ mirror + no-shift cases); `PlayoutTimeShifter` reports source+mirrors on a shift, empty on Continuous / zero-offset / active-unforced, and correctly seeds+shifts a never-watched playout. Non-vacuity of the enqueue proven by a compiling negative control. Docs: channels.md (On-demand resume section), domain-model.md, decisions.md (scheduling.ondemand-guide-refresh-on-thaw). Per-viewer resume is out of scope (single per-channel checkpoint; #68 says per-channel suffices). fixes #68 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c0d3dab190 |
fix(536): enforce workAheadSegmenterLimit with an atomic slot claim [decisions-edit]
The slot check and its increment straddled an await: `Run` compared `Volatile.Read(ref _workAheadCount)` against a DB-backed limit, and the increment happened later inside `Transcode`. Every simultaneous tune-in therefore observed `0 < limit` and started unthrottled — three concurrent tunes on prod with a limit of 1 all ran with no `-readrate`. `Interlocked` on the write side alone buys nothing when the read side is a separate, earlier load (same class as #231/#250). Extract the counter into a `WorkAheadSlots` pool whose `TryAcquire(limit)` claims via compare-exchange, so the count never even transiently exceeds the limit that the QSV hardware-frame pool sizing (#529) is derived from. `Run` claims the slot and passes ownership in; `Transcode(bool ownsWorkAheadSlot, ...)` derives `realtime` from it and releases it in its existing `finally`, keeping acquire/release one-for-one. Acquisition stays in the caller because `Run` sets `_state` from the outcome and `Transcode` reads that state on entry to pick the item start time. Tests hammer 8 threads x 20k rounds (a single Barrier round does not collide on this hardware); the documented negative control reinstates the check-then-act body and produces 15912 over-claiming rounds of 20000. Also annotates the #350 decision record, whose "every concurrent tune-in falls back to the throttled path" bullet described the intent rather than the behaviour. fixes #536 |
||
|
|
f355a4a96b |
docs(524): triage #237's 111 comments — no decision retrofit is owed
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 10s
Build ErsatzTV Image / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 37s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 37s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Exhaustive triage of the closed tracker ersatztv#237, whose 111 comments server-management#642 excludes from MemPalace ingestion (over the per-file cap) and #520 removes from startup. #524's premise was that facts living only in those comments would be orphaned and need curating into lifecycle records. Result: zero decision-shaped orphans. Every durable decision-shaped fact is already held by the decision corpus or by the individual issue the comment narrates -- which the exporter does ingest. The tracker was always the lossy copy, because the session protocol required the fuller closing record on the worked issue first. - docs/decisions.md: new active record docs.tracker-comment-retrofit, leading with the reusable rule (check the worked issue BEFORE the decision corpus) and the consequence for #642's benchmark row, which has no valid subject. - docs/tracker-retrofit-triage-237.md: the audit trail -- method, per-comment classification of all 111, totals, and the one candidate raised and disproved (#497's Guids/Directors scope, stated more fully on its own issue). - docs/handoffs/chicorytv-issue-queue.md: sweeps the two genuinely orphaned LORE facts the triage surfaced (e2e-local.sh readiness probe hanging on a reused config dir; troubleshooting playback cannot exercise channel branding). - docs/README.md: index the new doc. The claim is deliberately narrow: no *decision-shaped* orphans. The lore bucket was classified but not coverage-checked, and it was not empty -- hence the sweep. fixes #524 |
||
|
|
b27c950943 |
fix(529): address review — decision schema, #350 forward pointer, warning log, save-time normalization [decisions-edit]
Cold adversarial review returned BLOCKED on the documentation half. Addressed: - The new decision record carried no lifecycle metadata block, taking the repo from 82/82 to 83/82 and making it invisible to the by-key catalog lookup that #521 established the same day. Added key/status/since/supersedes/superseded-by (ffmpeg.qsv-extra-hw-frames-floor) and regenerated docs/decisions/README.md; decisions_validate.py now reports OK with no legacy-unmigrated notice. - The entry claimed to correct the #350 record but left that record untouched, so the stale "the burst is bounded" claim stayed authoritative for anyone resolving ffmpeg.hls-cold-start-burst. Added a forward-pointing correction note there (hence the [decisions-edit] token on this commit). - The floor was applied silently. QsvPipelineBuilder.SetAccelState now logs a warning naming both the configured and applied value, because raising a deliberately small pool costs additional surfaces (64 NV12 1080p surfaces is roughly 190 MiB, 760 MiB at 4K) on memory-constrained iGPUs. - Narrowed an overstated claim in the entry: 1..63 are untested, not known-bad. We raise them because the risk is a channel serving nothing, not because asking for less is illegitimate. Recorded as a deliberate over-reach with a stated cost. - Corrected a factual error: SubtitleScaleQsvFilter also formats extra_hw_frames but is dead code with no construction site, so it is NOT covered by the guard. - Config-vs-behavior mismatch: Create/UpdateFFmpegProfileHandler now normalize on save so stored rows converge on what the pipeline runs, and the SPA field carries min=64 rather than defaulting the display to 0. Render-time flooring is kept as the net that fixes existing deployments with no migration; the remaining gap for un-resaved rows is recorded as an accepted residual. - Tests strengthened: pinned to the literal measured 64 rather than to the constant (so lowering the floor cannot quietly satisfy them), added a negative-value case, added a deinterlace-upload case, and replaced the narrow ShouldNotContain with a regex asserting EVERY extra_hw_frames occurrence in the command is >= the minimum. Negative control re-run against the strengthened tests: reverting the floor fails 5, with the build verified succeeded first. Full suite green (4086 .NET, 891 web). Review finding that needed no change: the "single point" claim was independently verified — no bypass exists, every FFmpegState construction routes through MaybeQsvExtraHardwareFrames. Refs #350, #516, #519. |
||
|
|
aa76994825 |
docs(525): correct logo-download rejection status to 422, not 400
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 8s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / decisions lifecycle (pull_request) Successful in 24s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m21s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m34s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 13m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m49s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Live-E2E of the write path (scripts/e2e-local.sh) confirmed the feature end to end — good URL -> cached /iptv/logos/<hash>, M3U emits the cached URL, cached logo serves back as PNG, rejected save leaves the prior logo intact — and caught that a bad/non-image/oversized logo rejects as 422 (BaseError -> ValidationProblemDetails via ToErrorResult), not 400. 400 remains model-binding-only. Corrected channels.md, api-conventions.md, and the graphics.channel-logo-caching decision Rule + catalog. |
||
|
|
56a18cd5dc |
docs(525): record download-on-save decision; update channels + api-conventions
New lifecycle record graphics.channel-logo-caching (supersedes: none — narrows for YAML image elements). Regenerated the active catalog. channels.md + api- conventions.md updated for the new save-time download + 400s. |
||
|
|
1b207db87c |
fix(520,521): clear stale migration-map + narrow convention-docs-session-start rule to the task-map protocol [decisions-edit]
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 13s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 16s
Build ErsatzTV Image / decisions lifecycle (pull_request) Successful in 37s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m22s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m19s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m22s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fb6720ea27 |
fix(521): de-dup 6 overlapped records; guard duplicate metadata blocks; exclude retrieval-eval; complete eval bank [decisions-edit]
- Exclude docs/decisions/retrieval-eval.md from active decision parsing (_NON_DECISION_FILES); its `## N.` eval-question headings were being miscounted as 7 legacy-unmigrated records. - Add decisions_lib.metadata_line_count() + a decisions_validate guard that fails a record with more than one `key:` metadata line, so a stacked-metadata-block migration bug (which the parser silently tolerated by reading only the first block) can't recur unnoticed. TDD: test_duplicate_metadata_block_fails / test_single_metadata_block_passes. - De-duplicate the 6 docs/decisions.md records left with two stacked metadata blocks (scan.getoraddfolder-db-lookup #488, scan.musicvideo-reconciliation #494, scan.jellyfin-mixed-content-library #489, iptv.logo-drives-bug-preset #67, ffmpeg.qsv-decode-encode-split #498, ci.small-lane-git-only server-management#639), merging the union of Signals/paths/issues/Mechanics from both blocks and keeping the richer Rule wording; rationale prose untouched. - Fill in the deferred Q6b row in docs/decisions/retrieval-eval.md now that startup.parallel-orientation is active in docs/decisions.md, scoring it as a real active-vs-superseded question against the archived docs.queue-state-gitea-tracker. - Regenerate docs/decisions/README.md via build_decisions_catalog.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4ac18e06bc | merge(520): parallel-orientation startup + #237 retirement [decisions-edit] | ||
|
|
962dc2a31a |
feat(520): parallel orientation+selection startup; retire #237 as live state; #642 retrieval bullets [decisions-edit]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c3b2b4d4bd |
docs(521): retrieval-eval question bank (active-record selection + citation)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e79f62402 |
feat(521): migrate final 11 scheduling/CI gap records; corpus fully migrated [decisions-edit]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
63a6fe80fb |
feat(521): migrate topic-file decision records to lifecycle schema [decisions-edit]
Adds metadata blocks (key/status/since/supersedes/superseded-by + Rule/Signals/Mechanics) to all 26 legacy-unmigrated ## records across optimistic-concurrency.md (7, concurrency.*), api-auth-security.md (9, security.*), release-ci-governance.md (7 remaining legacy records, release.*), and spa-modularization.md (3, spa.*). Heading text and rationale prose are byte-identical; only metadata was inserted, per the key-table assignments. security.session-auth-dual-credential (#295 PR1) gets a one-line cross-reference noting it narrows the OIDC-inert sub-claim of security.blazor-removal-auth-posture (#206), per orchestrator adjudication — both stay status: active with supersedes/superseded-by: none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f93458c76c |
fix(521): whole-branch review — correct Gitea anchors, guard archive+demotion, reciprocal links, stale append-only refs, budget warning [decisions-edit]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b1e7e08884 |
feat(521): rework decisions guard — lifecycle validator + narrow [decisions-edit]; header + CI rewrite [decisions-edit]
Retire the line-level append-only mechanic (ersatztv#303 H9) in favor of the lifecycle validator built in Tasks 1-5. .claude/hooks/decisions-guard.sh is now a thin fail-open shim around scripts/decisions_validate.py; .husky/pre-commit calls it for the structural (working-tree) checks, .husky/commit-msg drops the old staged/[decisions-edit]-deletion block and keeps only the Co-Authored-By check. The Gitea decisions-guard job is renamed "decisions lifecycle" and now runs decisions_validate.py --base/--head (structural + body-diff + no-vanish) and build_decisions_catalog.py --check (active catalog drift), with actions/setup-python@v5 added since the bare `small` lane doesn't guarantee python3; the old 1800-line consolidation-floor step is removed (replaced by the validator's aggregate active-corpus budget). docs/decisions.md's header is rewritten from append-only to lifecycle framing (metadata schema, statuses, generated catalog, archive, same-PR supersession); [decisions-edit] is re-scoped (not removed) to rationale-prose edits/factual corrections only. docs/ci-cd.md's release ritual and hook/job descriptions are rewritten to match. Also fixes a pre-existing validator false-positive surfaced while sanity-checking against origin/main: Task 6's #303 H9/H3 split (commit d09be57e) renamed the archived record's heading away from the pre-split original, which the validator's heading-based relocation check reads as "removed without an archive copy." Restored the archived heading to match the original text (functionally unchanged — still status: superseded, same key) and updated the two prose cross-references (migration-map.md, release-ci-governance.md) that pointed at the old anchor. PR1 scope only (per brief): does NOT wire the kickoff-guard CI step or touch the kickoff/README/select-queue docs — that's Task 8/PR2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
417279c072 |
fix(521): split #303 H9/H3 bundle — H3 root-screenshot guard stays active, only H9 archived [decisions-edit]
The archived docs.append-only-guard record bundled the genuinely-superseded H9 (decisions.md append-only) with the independent, still-active H3 (root-screenshot pre-commit guard), relocating both into archive/ and burying an active decision in the history wing. Split into two records: docs.append-only-guard stays in archive/release-ci-governance.md, status: superseded, H3 content removed and replaced with a forward pointer; ci.root-screenshot-guard is a new status: active record in the active docs/decisions/release-ci-governance.md, with the H3 rationale paragraph lifted byte-identical from the original bundle. Updated migration-map.md (new row + a "Migration conventions" note: never relocate an active decision to archive/) and regenerated docs/decisions/README.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eada44deb1 |
feat(521): reconcile #390/#406 + #411 + append-only supersession; seed exemplars [decisions-edit]
Migrate #406 (ci.runner-placement) and #412 (ci.peak-anon-measurement) to the lifecycle schema as exemplars; #390 and #411 confirmed prose-only (no standalone record). Dogfood the append-only->lifecycle supersession: docs.append-only-guard moved to archive/, superseded by new active docs.decision-lifecycle. Stand up docs/decisions/archive/ + migration-map.md. Refs #521 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
458ab2111f |
fix(521): catalog _anchor collapses punctuation runs; single trailing newline
_anchor() mapped each space/hyphen to its own '-' without collapsing runs, so the standard heading separator " — " (space, em-dash, space) produced a double hyphen in every generated anchor. Since nearly every real decision record heading uses that separator, this made the catalog emit a dead link for essentially every row. Fix: after building the char list, collapse consecutive '-' into one and strip leading/trailing '-' via re.sub, matching how Goldmark/GitHub/Gitea sluggers behave. Also fixed main() writing an extra trailing newline (want already ends in "\n", then "+ \n" appended a second one) so docs/decisions/README.md now ends with exactly one trailing newline; --check still compares via .strip(). Added test_anchor_collapses_em_dash_and_keeps_underscore to pin the anchor behavior against the reported iptv.base_url case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
67619b2bf6 |
feat(521): active-catalog generator
Adds scripts/build_decisions_catalog.py, which renders docs/decisions/README.md as a compact table of only 'active' decision records (sorted by key), and its test scripts/tests/test_build_catalog.py. Supports --check for CI drift detection. No decision records are migrated yet, so the generated catalog is currently empty (banner + header only) — expected at this stage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2281f2e764 |
fix(308): idempotent concurrent Add*ToCollection instead of a composite-PK 500
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 40s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7m25s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m59s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m3s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Two concurrent adds of the same item both membership-check it absent, both insert the CollectionItem composite key, and the loser's SaveChangesForcingVersion threw an uncaught DbUpdateException (SQLite 19 / MySQL 1062) -> 500. Now the loser is an idempotent no-op. - ConcurrencyExtensions.TrySaveChangesForcingVersion: bool-returning sibling that catches only a classified unique/PK violation and returns false. - 10 single-item Add*ToCollection handlers: return Unit.Default (no-op, skip fan-out) on false — the racing winner already inserted + rotated + rebuilt. - Bulk AddItemsToCollection: retry on a fresh context against recomputed membership so a partial-overlap collision doesn't drop the non-colliding items (bounded loop; common no-collision path runs once). - Provider detection via a TvContext.IsUniqueConstraintViolation static delegate (matches the existing IsSqlite/LastInsertedRowId provider seam), wired from Startup to SqliteErrorClassifier / MySqlErrorClassifier. - Add*ToPlaylist is NOT affected (PlaylistItem has its own identity PK; a playlist may legitimately contain the same item more than once). Tests: a negative-control anchor proves the race genuinely throws a classified exception; end-to-end handler tests reproduce a real cross-connection race via a shared-cache SQLite harness + a SavingChanges interceptor (the single-conn in-memory fixture cannot). Every fix-dependent test verified to fail with the catch disabled. Docs: api-conventions.md §7a (idempotent insert under concurrency) + decisions/optimistic-concurrency.md. fixes #308 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a2c056dd7a |
docs(release): prepare v26.9.0 promotion [decisions-edit]
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (push) Has been cancelled
Consolidate docs/decisions.md (1923 -> 1028) by extracting four cohesive topic clusters into docs/decisions/ (optimistic-concurrency, api-auth-security, release-ci-governance, spa-modularization) — content relocated verbatim (lossless; all rationale + reversals preserved), main Index rebuilt to reference the topic files plus the remaining in-file entries, docs/README.md points back at the decisions Index. Add the v26.9.0 row to the ci-cd.md version table. refs #340 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |